diff --git a/README.md b/README.md index 92451af7f..10e9c94bf 100644 --- a/README.md +++ b/README.md @@ -56,14 +56,13 @@ Installation and dependencies #### Rust -KAS requires [Rust] version 1.52 or greater (currently in **beta**: *usually* -we maintain compatibility with the latest stable release). -Using the **nightly** channel does have a couple of advantages: +KAS requires a recent [Rust] compiler. Currently, version 1.52 or greater is +required. Using the **nightly** channel does have a few advantages: - Proceedural macros emit better diagnostics. In some cases, diagnostics are missed without nightly rustc, hence **nightly is recommended for development**. -- Documentation generated via `cargo doc` requires nightly for links -- A few minor option things: see [Feature flags](#feature-flags) below. +- The `nightly` (`min_spec`) feature allows some visual improvements (see below). +- The `doc_cfg` feature may be used for API docs. ### Quick-start @@ -137,8 +136,9 @@ The `kas` crate has the following feature flags: Additionally, the following flags require a nightly compiler: - `nightly`: enables "more stable" unstable features -- `min_spec` (enabled by `nightly`): use `min_specialization` to draw - underlines in `AccelLabel` +- `min_spec` (enabled by `nightly`): use `min_specialization` for some visual + improvements: scrolled regions are drawn under scrollbars, + underlines on checkbox accelerator keys show with the Alt key. - `spec`: use `specialization` to enable `TryFormat` - `gat`: compatibility with `kas-text/gat` diff --git a/crates/kas-core/src/draw/color.rs b/crates/kas-core/src/draw/color.rs index 00fc0b5a8..e24d46abe 100644 --- a/crates/kas-core/src/draw/color.rs +++ b/crates/kas-core/src/draw/color.rs @@ -57,9 +57,14 @@ impl Rgba { Self::rgba(s, s, s, a) } + /// Get the sum of the three colour components + pub fn sum(self) -> f32 { + self.r + self.g + self.b + } + /// Average three colour components (desaturate) pub fn average(self) -> Self { - Self::ga((self.r + self.g + self.b) * (1.0 / 3.0), self.a) + Self::ga(self.sum() * (1.0 / 3.0), self.a) } /// Multiply and clamp three colour components @@ -131,9 +136,14 @@ impl Rgb { Self::rgb(s, s, s) } + /// Get the sum of the three colour components + pub fn sum(self) -> f32 { + self.r + self.g + self.b + } + /// Average three colour components (desaturate) pub fn average(self) -> Self { - Self::grey((self.r + self.g + self.b) * (1.0 / 3.0)) + Self::grey(self.sum() * (1.0 / 3.0)) } /// Multiply and clamp three colour components diff --git a/crates/kas-core/src/draw/handle.rs b/crates/kas-core/src/draw/handle.rs index f6951240a..afd787d5d 100644 --- a/crates/kas-core/src/draw/handle.rs +++ b/crates/kas-core/src/draw/handle.rs @@ -138,6 +138,8 @@ pub trait SizeHandle { fn pixels_from_em(&self, em: f32) -> f32; /// Size of a frame around child widget(s) + /// + /// This already includes the margins specified by [`Self::frame_margins`]. fn frame(&self, vert: bool) -> FrameRules; /// Frame/margin around a menu entry @@ -160,6 +162,9 @@ pub trait SizeHandle { /// Widgets must not draw in outer margins. fn outer_margins(&self) -> Margins; + /// The margin around frames and separators + fn frame_margins(&self) -> Margins; + /// The margin around text elements /// /// Similar to [`Self::outer_margins`], but intended for things like text @@ -467,6 +472,9 @@ impl> SizeHandle for R { fn outer_margins(&self) -> Margins { self.deref().outer_margins() } + fn frame_margins(&self) -> Margins { + self.deref().frame_margins() + } fn text_margins(&self) -> Margins { self.deref().text_margins() } diff --git a/crates/kas-core/src/geom/vector.rs b/crates/kas-core/src/geom/vector.rs index 7bdeefe7c..82cd05ffb 100644 --- a/crates/kas-core/src/geom/vector.rs +++ b/crates/kas-core/src/geom/vector.rs @@ -146,6 +146,9 @@ macro_rules! impl_vec2 { /// Zero pub const ZERO: $T = $T::splat(0.0); + /// One + pub const ONE: $T = $T::splat(1.0); + /// Positive infinity pub const INFINITY: $T = $T::splat(<$f>::INFINITY); @@ -335,6 +338,14 @@ macro_rules! impl_vec2 { } } + impl Div<$T> for $f { + type Output = $T; + #[inline] + fn div(self, rhs: $T) -> Self::Output { + $T(self / rhs.0, self / rhs.1) + } + } + impl From<($f, $f)> for $T { #[inline] fn from(arg: ($f, $f)) -> Self { diff --git a/crates/kas-theme/src/colors.rs b/crates/kas-theme/src/colors.rs index 9e7603032..b73fbecb0 100644 --- a/crates/kas-theme/src/colors.rs +++ b/crates/kas-theme/src/colors.rs @@ -6,7 +6,8 @@ //! Colour schemes use kas::draw::color::{Rgba, Rgba8Srgb}; -use kas::draw::{InputState, TextClass}; +use kas::draw::InputState; +use std::str::FromStr; const MULT_DEPRESS: f32 = 0.75; const MULT_HIGHLIGHT: f32 = 1.25; @@ -24,22 +25,30 @@ pub struct Colors { pub edit_bg: C, /// Background colour of `EditBox` (error state) pub edit_bg_error: C, - /// Text colour in an `EditBox` + /// Theme accent + /// + /// This should be a bold colour, used for small details. + pub accent: C, + /// Soft version of accent + /// + /// A softer version of the accent colour, used for block elements in some themes. + pub accent_soft: C, + /// Highlight colour for keyboard navigation + /// + /// This may be the same as `accent`. It should contrast well with + /// `accent_soft`. Themes should use `nav_focus` over `accent` where a + /// strong contrast is required. + pub nav_focus: C, + /// Normal text colour (over background) pub text: C, - /// Selected tect colour - pub text_sel: C, + /// Opposing text colour (e.g. white if `text` is black) + pub text_invert: C, + /// Disabled text colour + pub text_disabled: C, /// Selected text background colour + /// + /// This may be the same as `accent_soft`. pub text_sel_bg: C, - /// Text colour in a `Label` - pub label_text: C, - /// Text colour on a `TextButton` - pub button_text: C, - /// Highlight colour for keyboard navigation - pub nav_focus: C, - /// Colour of a `TextButton` - pub button: C, - /// Colour of mark within a `CheckBox` or `RadioBox` - pub checkbox: C, } /// [`Colors`] parameterised for reading and writing using sRGB @@ -53,16 +62,15 @@ impl From for ColorsLinear { Colors { background: col.background.into(), frame: col.frame.into(), + accent: col.accent.into(), + accent_soft: col.accent_soft.into(), + nav_focus: col.nav_focus.into(), edit_bg: col.edit_bg.into(), edit_bg_error: col.edit_bg_error.into(), text: col.text.into(), - text_sel: col.text_sel.into(), + text_invert: col.text_invert.into(), + text_disabled: col.text_disabled.into(), text_sel_bg: col.text_sel_bg.into(), - label_text: col.label_text.into(), - button_text: col.button_text.into(), - nav_focus: col.nav_focus.into(), - button: col.button.into(), - checkbox: col.checkbox.into(), } } } @@ -72,16 +80,15 @@ impl From for ColorsSrgb { Colors { background: col.background.into(), frame: col.frame.into(), + accent: col.accent.into(), + accent_soft: col.accent_soft.into(), + nav_focus: col.nav_focus.into(), edit_bg: col.edit_bg.into(), edit_bg_error: col.edit_bg_error.into(), text: col.text.into(), - text_sel: col.text_sel.into(), + text_invert: col.text_invert.into(), + text_disabled: col.text_disabled.into(), text_sel_bg: col.text_sel_bg.into(), - label_text: col.label_text.into(), - button_text: col.button_text.into(), - nav_focus: col.nav_focus.into(), - button: col.button.into(), - checkbox: col.checkbox.into(), } } } @@ -89,81 +96,78 @@ impl From for ColorsSrgb { impl Default for ColorsLinear { #[inline] fn default() -> Self { - Colors::white_blue() + ColorsSrgb::default().into() } } impl Default for ColorsSrgb { #[inline] fn default() -> Self { - ColorsLinear::default().into() + ColorsSrgb::light() } } -// NOTE: these colour schemes are defined using linear (Rgba) colours instead of -// sRGB (Rgba8Srgb) colours for historical reasons. Either should be fine. -impl ColorsLinear { - /// White background with blue activable items - pub fn white_blue() -> Self { +impl ColorsSrgb { + /// Default "light" scheme + pub fn light() -> Self { Colors { - background: Rgba::grey(1.0), - frame: Rgba::grey(0.7), - edit_bg: Rgba::grey(1.0), - edit_bg_error: Rgba::rgb(1.0, 0.5, 0.5), - text: Rgba::grey(0.0), - text_sel: Rgba::grey(1.0), - text_sel_bg: Rgba::rgb(0.15, 0.525, 0.75), - label_text: Rgba::grey(0.0), - button_text: Rgba::grey(1.0), - nav_focus: Rgba::rgb(0.9, 0.65, 0.4), - button: Rgba::rgb(0.2, 0.7, 1.0), - checkbox: Rgba::rgb(0.2, 0.7, 1.0), + background: Rgba8Srgb::from_str("#FAFAFA").unwrap(), + frame: Rgba8Srgb::from_str("#BCBCBC").unwrap(), + accent: Rgba8Srgb::from_str("#7E3FF2").unwrap(), + accent_soft: Rgba8Srgb::from_str("#B38DF9").unwrap(), + nav_focus: Rgba8Srgb::from_str("#7E3FF2").unwrap(), + edit_bg: Rgba8Srgb::from_str("#FAFAFA").unwrap(), + edit_bg_error: Rgba8Srgb::from_str("#FFBCBC").unwrap(), + text: Rgba8Srgb::from_str("#000000").unwrap(), + text_invert: Rgba8Srgb::from_str("#FFFFFF").unwrap(), + text_disabled: Rgba8Srgb::from_str("#AAAAAA").unwrap(), + text_sel_bg: Rgba8Srgb::from_str("#A172FA").unwrap(), } } - /// Light scheme - pub fn light() -> Self { + /// Dark scheme + pub fn dark() -> Self { Colors { - background: Rgba::grey(0.9), - frame: Rgba::rgb(0.8, 0.8, 0.9), - edit_bg: Rgba::grey(1.0), - edit_bg_error: Rgba::rgb(1.0, 0.5, 0.5), - text: Rgba::grey(0.0), - text_sel: Rgba::grey(0.0), - text_sel_bg: Rgba::rgb(0.8, 0.72, 0.24), - label_text: Rgba::grey(0.0), - button_text: Rgba::grey(0.0), - nav_focus: Rgba::rgb(0.9, 0.65, 0.4), - button: Rgba::rgb(1.0, 0.9, 0.3), - checkbox: Rgba::grey(0.4), + background: Rgba8Srgb::from_str("#404040").unwrap(), + frame: Rgba8Srgb::from_str("#AAAAAA").unwrap(), + accent: Rgba8Srgb::from_str("#F74C00").unwrap(), + accent_soft: Rgba8Srgb::from_str("#E77346").unwrap(), + nav_focus: Rgba8Srgb::from_str("#A33100").unwrap(), + edit_bg: Rgba8Srgb::from_str("#595959").unwrap(), + edit_bg_error: Rgba8Srgb::from_str("#FFBCBC").unwrap(), + text: Rgba8Srgb::from_str("#FFFFFF").unwrap(), + text_invert: Rgba8Srgb::from_str("#000000").unwrap(), + text_disabled: Rgba8Srgb::from_str("#CBCBCB").unwrap(), + text_sel_bg: Rgba8Srgb::from_str("#E77346").unwrap(), } } - /// Dark scheme - pub fn dark() -> Self { + /// Blue scheme + pub fn blue() -> Self { Colors { - background: Rgba::grey(0.2), - frame: Rgba::grey(0.4), - edit_bg: Rgba::grey(0.1), - edit_bg_error: Rgba::rgb(1.0, 0.5, 0.5), - text: Rgba::grey(1.0), - text_sel: Rgba::grey(1.0), - text_sel_bg: Rgba::rgb(0.6, 0.3, 0.1), - label_text: Rgba::grey(1.0), - button_text: Rgba::grey(1.0), - nav_focus: Rgba::rgb(1.0, 0.7, 0.5), - button: Rgba::rgb(0.5, 0.1, 0.1), - checkbox: Rgba::rgb(0.5, 0.1, 0.1), + background: Rgba8Srgb::from_str("#FFFFFF").unwrap(), + frame: Rgba8Srgb::from_str("#DADADA").unwrap(), + accent: Rgba8Srgb::from_str("#7CDAFF").unwrap(), + accent_soft: Rgba8Srgb::from_str("#7CDAFF").unwrap(), + nav_focus: Rgba8Srgb::from_str("#3B697A").unwrap(), + edit_bg: Rgba8Srgb::from_str("#FFFFFF").unwrap(), + edit_bg_error: Rgba8Srgb::from_str("#FFBCBC").unwrap(), + text: Rgba8Srgb::from_str("#000000").unwrap(), + text_invert: Rgba8Srgb::from_str("#FFFFFF").unwrap(), + text_disabled: Rgba8Srgb::from_str("#AAAAAA").unwrap(), + text_sel_bg: Rgba8Srgb::from_str("#6CC0E1").unwrap(), } } +} +impl ColorsLinear { /// Adjust a colour depending on state pub fn adjust_for_state(col: Rgba, state: InputState) -> Rgba { if state.disabled { col.average() } else if state.depress { col.multiply(MULT_DEPRESS) - } else if state.hover { + } else if state.hover || state.char_focus { col.multiply(MULT_HIGHLIGHT).max(MIN_HIGHLIGHT) } else { col @@ -190,18 +194,24 @@ impl ColorsLinear { } } - /// Get colour for a button, depending on state + /// Get accent colour, adjusted for state + #[inline] + pub fn accent_state(&self, state: InputState) -> Rgba { + Self::adjust_for_state(self.accent, state) + } + + /// Get soft accent colour, adjusted for state #[inline] - pub fn button_state(&self, state: InputState) -> Rgba { - Self::adjust_for_state(self.button, state) + pub fn accent_soft_state(&self, state: InputState) -> Rgba { + Self::adjust_for_state(self.accent_soft, state) } /// Get colour for a checkbox mark, depending on state pub fn check_mark_state(&self, state: InputState, checked: bool) -> Option { if checked { - Some(Self::adjust_for_state(self.checkbox, state)) + Some(Self::adjust_for_state(self.accent, state)) } else if state.depress { - Some(self.checkbox.multiply(MULT_DEPRESS)) + Some(self.accent.multiply(MULT_DEPRESS)) } else { None } @@ -210,24 +220,19 @@ impl ColorsLinear { /// Get background highlight colour of a menu entry, if any pub fn menu_entry(&self, state: InputState) -> Option { if state.depress || state.nav_focus { - Some(self.button.multiply(MULT_DEPRESS)) + Some(self.accent_soft.multiply(MULT_DEPRESS)) } else { None } } - /// Get colour of a scrollbar, depending on state - #[inline] - pub fn scrollbar_state(&self, state: InputState) -> Rgba { - self.button_state(state) - } - - /// Get text colour from class - pub fn text_class(&self, class: TextClass) -> Rgba { - match class { - TextClass::Label | TextClass::MenuLabel | TextClass::LabelScroll => self.label_text, - TextClass::Button => self.button_text, - TextClass::Edit | TextClass::EditMulti => self.text, + /// Get appropriate text colour over the given background + pub fn text_over(&self, bg: Rgba) -> Rgba { + let bg_sum = bg.sum(); + if (bg_sum - self.text_invert.sum()).abs() > (bg_sum - self.text.sum()).abs() { + self.text_invert + } else { + self.text } } } diff --git a/crates/kas-theme/src/config.rs b/crates/kas-theme/src/config.rs index 838e8ae22..c6dc36411 100644 --- a/crates/kas-theme/src/config.rs +++ b/crates/kas-theme/src/config.rs @@ -5,7 +5,7 @@ //! Theme configuration -use crate::{ColorsLinear, ColorsSrgb, ThemeConfig}; +use crate::{ColorsSrgb, ThemeConfig}; use kas::draw::TextClass; use kas::text::fonts::{fonts, AddMode, FontSelector}; use kas::TkAction; @@ -236,9 +236,9 @@ mod defaults { pub fn color_schemes() -> BTreeMap { let mut schemes = BTreeMap::new(); - schemes.insert("".to_string(), ColorsLinear::white_blue().into()); - schemes.insert("light".to_string(), ColorsLinear::light().into()); - schemes.insert("dark".to_string(), ColorsLinear::dark().into()); + schemes.insert("light".to_string(), ColorsSrgb::light()); + schemes.insert("dark".to_string(), ColorsSrgb::dark()); + schemes.insert("blue".to_string(), ColorsSrgb::blue()); schemes } diff --git a/crates/kas-theme/src/dim.rs b/crates/kas-theme/src/dim.rs index 4d9fcaf1a..bc817e46c 100644 --- a/crates/kas-theme/src/dim.rs +++ b/crates/kas-theme/src/dim.rs @@ -26,12 +26,16 @@ pub struct Parameters { pub outer_margin: f32, /// Margin inside a frame before contents pub inner_margin: f32, + /// Margin around frames and seperators + pub frame_margin: f32, /// Margin between text elements pub text_margin: f32, /// Frame size pub frame_size: f32, /// Button frame size (non-flat outer region) pub button_frame: f32, + /// Checkbox inner size in Points + pub checkbox_inner: f32, /// Scrollbar minimum handle size pub scrollbar_size: Vec2, /// Slider minimum handle size @@ -51,6 +55,7 @@ pub struct Dimensions { pub min_line_length: i32, pub outer_margin: u16, pub inner_margin: u16, + pub frame_margin: u16, pub text_margin: u16, pub frame: i32, pub button_frame: i32, @@ -73,6 +78,7 @@ impl Dimensions { let outer_margin = (params.outer_margin * scale_factor).cast_nearest(); let inner_margin = (params.inner_margin * scale_factor).cast_nearest(); + let frame_margin = (params.frame_margin * scale_factor).cast_nearest(); let text_margin = (params.text_margin * scale_factor).cast_nearest(); let frame = (params.frame_size * scale_factor).cast_nearest(); Dimensions { @@ -84,10 +90,12 @@ impl Dimensions { min_line_length: (8.0 * dpem).cast_nearest(), outer_margin, inner_margin, + frame_margin, text_margin, frame, button_frame: (params.button_frame * scale_factor).cast_nearest(), - checkbox: i32::conv_nearest(9.0 * dpp) + 2 * (i32::from(inner_margin) + frame), + checkbox: i32::conv_nearest(params.checkbox_inner * dpp) + + 2 * (i32::from(inner_margin) + frame), scrollbar: Size::from(params.scrollbar_size * scale_factor), slider: Size::from(params.slider_size * scale_factor), progress_bar: Size::from(params.progress_bar * scale_factor), @@ -143,7 +151,7 @@ impl SizeHandle for Window { } fn frame(&self, _vert: bool) -> FrameRules { - FrameRules::new_sym(self.dims.frame, 0, 0) + FrameRules::new_sym(self.dims.frame, 0, self.dims.frame_margin) } fn menu_frame(&self, vert: bool) -> FrameRules { let mut size = self.dims.frame; @@ -168,6 +176,10 @@ impl SizeHandle for Window { Margins::splat(self.dims.outer_margin) } + fn frame_margins(&self) -> Margins { + Margins::splat(self.dims.frame_margin) + } + fn text_margins(&self) -> Margins { Margins::splat(self.dims.text_margin) } diff --git a/crates/kas-theme/src/flat_theme.rs b/crates/kas-theme/src/flat_theme.rs index 40e0d6f05..be7f72b96 100644 --- a/crates/kas-theme/src/flat_theme.rs +++ b/crates/kas-theme/src/flat_theme.rs @@ -75,12 +75,15 @@ impl FlatTheme { const DIMS: dim::Parameters = dim::Parameters { outer_margin: 8.0, inner_margin: 1.2, + frame_margin: 2.4, text_margin: 2.0, - frame_size: 4.0, - button_frame: 6.0, + frame_size: 2.4, + // NOTE: visual thickness is (button_frame * scale_factor).round() * (1 - BG_SHRINK_FACTOR) + button_frame: 2.4, + checkbox_inner: 5.0, scrollbar_size: Vec2::splat(8.0), - slider_size: Vec2(12.0, 25.0), - progress_bar: Vec2::splat(12.0), + slider_size: Vec2(16.0, 16.0), + progress_bar: Vec2::splat(8.0), }; pub struct DrawHandle<'a, DS: DrawSharedImpl> { @@ -200,50 +203,6 @@ impl ThemeApi for FlatTheme { } } -impl<'a, DS: DrawSharedImpl> DrawHandle<'a, DS> -where - DS::Draw: DrawRoundedImpl, -{ - /// Draw an edit box with optional navigation highlight. - /// Return the inner rect. - /// - /// - `outer`: define position via outer rect - /// - `bg_col`: colour of background - /// - `nav_col`: colour of navigation highlight, if visible - fn draw_edit_box(&mut self, outer: Rect, bg_col: Rgba, nav_col: Option) -> Quad { - let outer = Quad::from(outer); - let inner1 = outer.shrink(self.window.dims.frame as f32 * BG_SHRINK_FACTOR); - let inner2 = outer.shrink(self.window.dims.frame as f32); - - self.draw.rect(inner1, bg_col); - - // We draw over the inner rect, taking advantage of the fact that - // rounded frames get drawn after flat rects. - self.draw - .rounded_frame(outer, inner2, BG_SHRINK_FACTOR, self.cols.frame); - - if let Some(col) = nav_col { - self.draw.rounded_frame(inner1, inner2, 0.6, col); - } - - inner2 - } - - /// Draw a handle (for slider, scrollbar) - fn draw_handle(&mut self, rect: Rect, state: InputState) { - let outer = Quad::from(rect); - let thickness = outer.size().min_comp() / 2.0; - let inner = outer.shrink(thickness); - let col = self.cols.scrollbar_state(state); - self.draw.rounded_frame(outer, inner, 0.0, col); - - if let Some(col) = self.cols.nav_region(state) { - let outer = outer.shrink(thickness / 4.0); - self.draw.rounded_frame(outer, inner, 0.6, col); - } - } -} - impl<'a, DS: DrawSharedImpl> draw::DrawHandle for DrawHandle<'a, DS> where DS::Draw: DrawRoundedImpl, @@ -319,24 +278,24 @@ where self.draw.frame(outer, inner, col); } - fn text(&mut self, pos: Coord, text: &TextDisplay, class: TextClass) { + fn text(&mut self, pos: Coord, text: &TextDisplay, _: TextClass) { let pos = pos; - let col = self.cols.text_class(class); + let col = self.cols.text; self.draw.text(pos.into(), text, col); } - fn text_effects(&mut self, pos: Coord, text: &dyn TextApi, class: TextClass) { + fn text_effects(&mut self, pos: Coord, text: &dyn TextApi, _: TextClass) { self.draw.text_col_effects( (pos).into(), text.display(), - self.cols.text_class(class), + self.cols.text, text.effect_tokens(), ); } - fn text_accel(&mut self, pos: Coord, text: &Text, state: bool, class: TextClass) { + fn text_accel(&mut self, pos: Coord, text: &Text, state: bool, _: TextClass) { let pos = Vec2::from(pos); - let col = self.cols.text_class(class); + let col = self.cols.text; if state { let effects = text.text().effect_tokens(); self.draw.text_col_effects(pos, text.as_ref(), col, effects); @@ -350,10 +309,11 @@ where pos: Coord, text: &TextDisplay, range: Range, - class: TextClass, + _: TextClass, ) { let pos = Vec2::from(pos); - let col = self.cols.text_class(class); + let col = self.cols.text; + let sel_col = self.cols.text_over(self.cols.text_sel_bg); // Draw background: for (p1, p2) in &text.highlight_lines(range.clone()) { @@ -372,7 +332,7 @@ where Effect { start: range.start.cast(), flags: Default::default(), - aux: self.cols.text_sel, + aux: sel_col, }, Effect { start: range.end.cast(), @@ -383,11 +343,11 @@ where self.draw.text_effects(pos, text, &effects); } - fn edit_marker(&mut self, pos: Coord, text: &TextDisplay, class: TextClass, byte: usize) { + fn edit_marker(&mut self, pos: Coord, text: &TextDisplay, _: TextClass, byte: usize) { let width = self.window.dims.font_marker_width; let pos = Vec2::from(pos); - let mut col = self.cols.text_class(class); + let mut col = self.cols.nav_focus; for cursor in text.text_glyph_pos(byte).rev() { let mut p1 = pos + Vec2::from(cursor.pos); let mut p2 = p1; @@ -421,47 +381,86 @@ where fn button(&mut self, rect: Rect, col: Option, state: InputState) { let outer = Quad::from(rect); - let col = col.map(|c| c.into()).unwrap_or(self.cols.button); + + let col = if state.nav_focus && !state.disabled { + self.cols.accent_soft + } else { + col.map(|c| c.into()).unwrap_or(self.cols.background) + }; let col = ColorsLinear::adjust_for_state(col, state); + if col != self.cols.background { + let inner = outer.shrink(self.window.dims.button_frame as f32 * BG_SHRINK_FACTOR); + self.draw.rect(inner, col); + } + let col = self.cols.nav_region(state).unwrap_or(self.cols.frame); let inner = outer.shrink(self.window.dims.button_frame as f32); - self.draw.rounded_frame(outer, inner, 0.0, col); - self.draw.rect(inner, col); + self.draw.rounded_frame(outer, inner, BG_SHRINK_FACTOR, col); + } - if let Some(col) = self.cols.nav_region(state) { - let outer = outer.shrink(self.window.dims.inner_margin as f32); - self.draw.rounded_frame(outer, inner, 0.6, col); + fn edit_box(&mut self, rect: Rect, mut state: InputState) { + let outer = Quad::from(rect); + + state.depress = false; + let col = match state.error { + true => self.cols.edit_bg_error, + false => self.cols.edit_bg, + }; + let col = ColorsLinear::adjust_for_state(col, state); + if col != self.cols.background { + let inner = outer.shrink(self.window.dims.button_frame as f32 * BG_SHRINK_FACTOR); + self.draw.rect(inner, col); } - } - fn edit_box(&mut self, rect: Rect, state: InputState) { - let bg_col = self.cols.bg_col(state); - self.draw_edit_box(rect, bg_col, self.cols.nav_region(state)); + let inner = outer.shrink(self.window.dims.button_frame as f32); + self.draw + .rounded_frame(outer, inner, BG_SHRINK_FACTOR, self.cols.frame); + + if state.nav_focus { + let r = 0.5 * self.window.dims.button_frame as f32; + let y = outer.b.1 - r; + let a = Vec2(outer.a.0 + r, y); + let b = Vec2(outer.b.0 - r, y); + self.draw.rounded_line(a, b, r, self.cols.nav_focus); + } } fn checkbox(&mut self, rect: Rect, checked: bool, state: InputState) { - let bg_col = self.cols.bg_col(state); - let nav_col = self.cols.nav_region(state).or(Some(bg_col)); + let outer = Quad::from(rect); - let inner = self.draw_edit_box(rect, bg_col, nav_col); + let col = ColorsLinear::adjust_for_state(self.cols.background, state); + if col != self.cols.background { + let inner = outer.shrink(self.window.dims.button_frame as f32 * BG_SHRINK_FACTOR); + self.draw.rect(inner, col); + } + + let col = self.cols.nav_region(state).unwrap_or(self.cols.frame); + let inner = outer.shrink(self.window.dims.button_frame as f32); + self.draw.rounded_frame(outer, inner, BG_SHRINK_FACTOR, col); if let Some(col) = self.cols.check_mark_state(state, checked) { - let radius = inner.size().sum() * (1.0 / 16.0); - let inner = inner.shrink(self.window.dims.inner_margin as f32 + radius); - self.draw.rounded_line(inner.a, inner.b, radius, col); - self.draw.rounded_line(inner.ab(), inner.ba(), radius, col); + let inner = inner.shrink((2 * self.window.dims.inner_margin) as f32); + self.draw.rect(inner, col); } } fn radiobox(&mut self, rect: Rect, checked: bool, state: InputState) { - let bg_col = self.cols.bg_col(state); - let nav_col = self.cols.nav_region(state).or(Some(bg_col)); + let outer = Quad::from(rect); - let inner = self.draw_edit_box(rect, bg_col, nav_col); + let col = ColorsLinear::adjust_for_state(self.cols.background, state); + if col != self.cols.background { + self.draw.circle(outer, 0.0, col); + } + + let col = self.cols.nav_region(state).unwrap_or(self.cols.frame); + const F: f32 = 2.0 * (1.0 - BG_SHRINK_FACTOR); // match checkbox frame + let r = 1.0 - F * self.window.dims.button_frame as f32 / rect.size.0 as f32; + self.draw.circle(outer, r, col); if let Some(col) = self.cols.check_mark_state(state, checked) { - let inner = inner.shrink(self.window.dims.inner_margin as f32); - self.draw.circle(inner, 0.5, col); + let r = self.window.dims.button_frame + 2 * self.window.dims.inner_margin as i32; + let inner = outer.shrink(r as f32); + self.draw.circle(inner, 0.0, col); } } @@ -469,41 +468,73 @@ where // track let outer = Quad::from(rect); let inner = outer.shrink(outer.size().min_comp() / 2.0); - let col = self.cols.frame; + let mut col = self.cols.frame; + col.a = 0.5; // HACK self.draw.rounded_frame(outer, inner, 0.0, col); // handle - self.draw_handle(h_rect, state); + let outer = Quad::from(h_rect); + let r = outer.size().min_comp() * 0.125; + let outer = outer.shrink(r); + let inner = outer.shrink(3.0 * r); + let col = ColorsLinear::adjust_for_state(self.cols.frame, state); + self.draw.rounded_frame(outer, inner, 0.0, col); } fn slider(&mut self, rect: Rect, h_rect: Rect, dir: Direction, state: InputState) { // track let mut outer = Quad::from(rect); - outer = match dir.is_horizontal() { - true => outer.shrink_vec(Vec2(0.0, outer.size().1 * (3.0 / 8.0))), - false => outer.shrink_vec(Vec2(outer.size().0 * (3.0 / 8.0), 0.0)), + let mid = Vec2::from(h_rect.pos + h_rect.size / 2); + let (mut first, mut second); + if dir.is_horizontal() { + outer = outer.shrink_vec(Vec2(0.0, outer.size().1 * (1.0 / 3.0))); + first = outer; + second = outer; + first.b.0 = mid.0; + second.a.0 = mid.0; + } else { + outer = outer.shrink_vec(Vec2(outer.size().0 * (1.0 / 3.0), 0.0)); + first = outer; + second = outer; + first.b.1 = mid.1; + second.a.1 = mid.1; }; - let inner = outer.shrink(outer.size().min_comp() / 2.0); - let col = self.cols.frame; - self.draw.rounded_frame(outer, inner, 0.0, col); - // handle - self.draw_handle(h_rect, state); + let dist = outer.size().min_comp() / 2.0; + let inner = first.shrink(dist); + self.draw.rounded_frame(first, inner, 0.0, self.cols.accent); + let inner = second.shrink(dist); + self.draw + .rounded_frame(second, inner, 1.0 / 3.0, self.cols.frame); + + // handle; force it to be square + let size = Size::splat(h_rect.size.0.min(h_rect.size.1)); + let offset = Offset::from((h_rect.size - size) / 2); + let outer = Quad::from(Rect::new(h_rect.pos + offset, size)); + + let col = if state.nav_focus && !state.disabled { + self.cols.accent_soft + } else { + self.cols.background + }; + let col = ColorsLinear::adjust_for_state(col, state); + self.draw.circle(outer, 0.0, col); + let col = self.cols.nav_region(state).unwrap_or(self.cols.frame); + self.draw.circle(outer, 14.0 / 16.0, col); } fn progress_bar(&mut self, rect: Rect, dir: Direction, _: InputState, value: f32) { - let outer = Quad::from(rect); - let mut inner = outer.shrink(self.window.dims.frame as f32); - let col = self.cols.frame; - self.draw.rounded_frame(outer, inner, BG_SHRINK_FACTOR, col); + let mut outer = Quad::from(rect); + let inner = outer.shrink(outer.size().min_comp() / 2.0); + self.draw.rounded_frame(outer, inner, 0.75, self.cols.frame); if dir.is_horizontal() { - inner.b.0 = inner.a.0 + value * (inner.b.0 - inner.a.0); + outer.b.0 = outer.a.0 + value * (outer.b.0 - outer.a.0); } else { - inner.b.1 = inner.a.1 + value * (inner.b.1 - inner.a.1); + outer.b.1 = outer.a.1 + value * (outer.b.1 - outer.a.1); } - let col = self.cols.button; - self.draw.rect(inner, col); + let inner = outer.shrink(outer.size().min_comp() / 2.0); + self.draw.rounded_frame(outer, inner, 0.0, self.cols.accent); } fn image(&mut self, id: ImageId, rect: Rect) { diff --git a/crates/kas-theme/src/shaded_theme.rs b/crates/kas-theme/src/shaded_theme.rs index fab3807e1..0c0ea367e 100644 --- a/crates/kas-theme/src/shaded_theme.rs +++ b/crates/kas-theme/src/shaded_theme.rs @@ -58,9 +58,11 @@ impl ShadedTheme { const DIMS: dim::Parameters = dim::Parameters { outer_margin: 6.0, inner_margin: 1.2, + frame_margin: 1.2, text_margin: 2.0, frame_size: 5.0, button_frame: 5.0, + checkbox_inner: 9.0, scrollbar_size: Vec2::splat(8.0), slider_size: Vec2(12.0, 25.0), progress_bar: Vec2::splat(12.0), @@ -205,7 +207,7 @@ where let outer = Quad::from(rect); let thickness = outer.size().min_comp() / 2.0; let inner = outer.shrink(thickness); - let col = self.cols.scrollbar_state(state); + let col = self.cols.accent_soft_state(state); self.draw.shaded_round_frame(outer, inner, (0.0, 0.6), col); if let Some(col) = self.cols.nav_region(state) { @@ -317,7 +319,7 @@ where fn button(&mut self, rect: Rect, col: Option, state: InputState) { let outer = Quad::from(rect); let inner = outer.shrink(self.window.dims.button_frame as f32); - let col = col.map(|c| c.into()).unwrap_or(self.cols.button); + let col = col.map(|c| c.into()).unwrap_or(self.cols.accent_soft); let col = ColorsLinear::adjust_for_state(col, state); self.draw.shaded_round_frame(outer, inner, (0.0, 0.6), col); @@ -398,7 +400,7 @@ where } let thickness = outer.size().min_comp() / 2.0; let inner = outer.shrink(thickness); - let col = self.cols.button; + let col = self.cols.accent_soft; self.draw.shaded_round_frame(outer, inner, (0.0, 0.6), col); } diff --git a/crates/kas-wgpu/src/draw/custom.rs b/crates/kas-wgpu/src/draw/custom.rs index e607e8c04..8649594de 100644 --- a/crates/kas-wgpu/src/draw/custom.rs +++ b/crates/kas-wgpu/src/draw/custom.rs @@ -61,7 +61,9 @@ pub trait CustomPipe: 'static { type Window: CustomWindow; /// Construct a window associated with this pipeline - fn new_window(&self, device: &wgpu::Device, size: Size) -> Self::Window; + /// + /// Note: [`Self::resize`] will be called before usage. + fn new_window(&self, device: &wgpu::Device) -> Self::Window; /// Called whenever the window is resized fn resize( @@ -164,7 +166,7 @@ pub enum Void {} /// A dummy implementation (does nothing) impl CustomPipe for () { type Window = (); - fn new_window(&self, _: &wgpu::Device, _: Size) -> Self::Window {} + fn new_window(&self, _: &wgpu::Device) -> Self::Window {} fn resize(&self, _: &mut Self::Window, _: &wgpu::Device, _: &wgpu::Queue, _: Size) {} } diff --git a/crates/kas-wgpu/src/draw/draw_pipe.rs b/crates/kas-wgpu/src/draw/draw_pipe.rs index b251e32a6..50127b0ac 100644 --- a/crates/kas-wgpu/src/draw/draw_pipe.rs +++ b/crates/kas-wgpu/src/draw/draw_pipe.rs @@ -12,7 +12,7 @@ use super::*; use kas::cast::Cast; use kas::draw::color::Rgba; use kas::draw::*; -use kas::geom::{Coord, Quad, Rect, Size, Vec2}; +use kas::geom::{Quad, Rect, Size, Vec2}; use kas::text::{Effect, TextDisplay}; use kas_theme::DrawShadedImpl; @@ -96,17 +96,12 @@ impl DrawPipe { } /// Construct per-window state - pub fn new_window(&self, size: Size) -> DrawWindow { - let vsize = Vec2::from(size); - let scale: Scale = [-0.5 * vsize.0, 0.5 * vsize.1, 2.0 / vsize.0, -2.0 / vsize.1]; - - let rect = Rect::new(Coord::ZERO, size); - - let custom = self.custom.new_window(&self.device, size); + pub fn new_window(&self) -> DrawWindow { + let custom = self.custom.new_window(&self.device); DrawWindow { - scale, - clip_regions: vec![(rect, Offset::ZERO)], + scale: Default::default(), + clip_regions: vec![Default::default()], images: Default::default(), shaded_square: Default::default(), shaded_round: Default::default(), @@ -130,12 +125,9 @@ impl DrawPipe { window.clip_regions[0].0.size = size; let vsize = Vec2::from(size); - window.scale = [ - -0.5 * vsize.0, - -0.5 * vsize.1, - 2.0 / vsize.0, - -2.0 / vsize.1, - ]; + let off = vsize * -0.5; + let scale = 2.0 / vsize; + window.scale = [off.0, off.1, scale.0, -scale.1]; self.custom .resize(&mut window.custom, &self.device, &self.queue, size); diff --git a/crates/kas-wgpu/src/draw/flat_round.rs b/crates/kas-wgpu/src/draw/flat_round.rs index 5f62ddc4b..5154f8414 100644 --- a/crates/kas-wgpu/src/draw/flat_round.rs +++ b/crates/kas-wgpu/src/draw/flat_round.rs @@ -12,8 +12,8 @@ use kas::geom::{Quad, Vec2}; use std::mem::size_of; /// Offset relative to the size of a pixel used by the fragment shader to -/// implement multi-sampling. -const OFFSET: f32 = 0.125; +/// implement 4x multi-sampling. The pattern is defined by the fragment shader. +const AA_OFFSET: f32 = 0.5 * std::f32::consts::FRAC_1_SQRT_2; // NOTE(opt): in theory we could reduce data transmission to the GPU by 1/3 by // sending quads (two triangles) as instances in triangle-strip mode. The @@ -56,7 +56,7 @@ impl Pipeline { label: Some("FR render_pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { - module: &shaders.vert_122, + module: &shaders.vert_flat_round, entry_point: "main", buffers: &[wgpu::VertexBufferLayout { array_stride: size_of::() as wgpu::BufferAddress, @@ -130,7 +130,7 @@ impl Window { let na = -nb; // Since we take the mid-point, all offsets are uniform - let p = Vec2::splat(OFFSET / radius); + let p = Vec2::splat(AA_OFFSET / radius); let p1my = p1 - vy; let p1py = p1 + vy; @@ -163,7 +163,7 @@ impl Window { ]); } - /// Bounds on input: `0 ≤ inner_radius ≤ 1`. + /// Bounds on input: `0 ≤ inner_radius ≤ 1`. pub fn circle(&mut self, pass: PassId, rect: Quad, inner_radius: f32, col: Rgba) { let aa = rect.a; let bb = rect.b; @@ -173,20 +173,21 @@ impl Window { return; } - let inner = inner_radius.max(0.0).min(1.0); + let inner = inner_radius.clamp(0.0, 1.0); + let inner = inner * inner; // shader compares to square let ab = Vec2(aa.0, bb.1); let ba = Vec2(bb.0, aa.1); let mid = (aa + bb) * 0.5; let n0 = Vec2::splat(0.0); - let nb = (bb - aa).sign(); + let nb = Vec2::ONE; // = (bb - aa).sign(); let na = -nb; let nab = Vec2(na.0, nb.1); let nba = Vec2(nb.0, na.1); // Since we take the mid-point, all offsets are uniform - let p = nb / (bb - mid) * OFFSET; + let p = nb / (bb - mid) * AA_OFFSET; let aa = Vertex::new2(aa, col, inner, na, p); let ab = Vertex::new2(ab, col, inner, nab, p); @@ -249,10 +250,10 @@ impl Window { let n0a = Vec2(0.0, na.1); let n0b = Vec2(0.0, nb.1); - let paa = na / (aa - cc) * OFFSET; - let pab = nab / (ab - cd) * OFFSET; - let pba = nba / (ba - dc) * OFFSET; - let pbb = nb / (bb - dd) * OFFSET; + let paa = na / (aa - cc) * AA_OFFSET; + let pab = nab / (ab - cd) * AA_OFFSET; + let pba = nba / (ba - dc) * AA_OFFSET; + let pbb = nb / (bb - dd) * AA_OFFSET; // We must add corners separately to ensure correct interpolation of dir // values, hence need 16 points: diff --git a/crates/kas-wgpu/src/draw/shaded_round.rs b/crates/kas-wgpu/src/draw/shaded_round.rs index 0a258612a..efeafb0fa 100644 --- a/crates/kas-wgpu/src/draw/shaded_round.rs +++ b/crates/kas-wgpu/src/draw/shaded_round.rs @@ -13,8 +13,8 @@ use std::f32::consts::FRAC_PI_2; use std::mem::size_of; /// Offset relative to the size of a pixel used by the fragment shader to -/// implement multi-sampling. -const OFFSET: f32 = 0.125; +/// implement 4x multi-sampling. The pattern is defined by the fragment shader. +const OFFSET: f32 = 0.5 * std::f32::consts::FRAC_1_SQRT_2; #[repr(C)] #[derive(Clone, Copy, Debug)] @@ -52,7 +52,7 @@ impl Pipeline { label: Some("SR render_pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { - module: &shaders.vert_222, + module: &shaders.vert_shaded_round, entry_point: "main", buffers: &[wgpu::VertexBufferLayout { array_stride: size_of::() as wgpu::BufferAddress, diff --git a/crates/kas-wgpu/src/draw/shaded_square.rs b/crates/kas-wgpu/src/draw/shaded_square.rs index 8214c09f7..d6513db1a 100644 --- a/crates/kas-wgpu/src/draw/shaded_square.rs +++ b/crates/kas-wgpu/src/draw/shaded_square.rs @@ -41,7 +41,7 @@ impl Pipeline { label: Some("SS render_pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { - module: &shaders.vert_2, + module: &shaders.vert_shaded_square, entry_point: "main", buffers: &[wgpu::VertexBufferLayout { array_stride: size_of::() as wgpu::BufferAddress, diff --git a/crates/kas-wgpu/src/draw/shaders.rs b/crates/kas-wgpu/src/draw/shaders.rs index e2339c715..5257aa8a5 100644 --- a/crates/kas-wgpu/src/draw/shaders.rs +++ b/crates/kas-wgpu/src/draw/shaders.rs @@ -9,9 +9,9 @@ use wgpu::{include_spirv, ShaderModule}; /// Shader manager pub struct ShaderManager { - pub vert_122: ShaderModule, - pub vert_2: ShaderModule, - pub vert_222: ShaderModule, + pub vert_flat_round: ShaderModule, + pub vert_shaded_square: ShaderModule, + pub vert_shaded_round: ShaderModule, pub vert_image: ShaderModule, pub vert_glyph: ShaderModule, pub frag_flat_round: ShaderModule, @@ -29,9 +29,9 @@ macro_rules! create { impl ShaderManager { pub fn new(device: &wgpu::Device) -> Self { - let vert_122 = create!(device, "shaders/scaled_122.vert.spv"); - let vert_2 = create!(device, "shaders/scaled_2.vert.spv"); - let vert_222 = create!(device, "shaders/scaled_222.vert.spv"); + let vert_flat_round = create!(device, "shaders/flat_round.vert.spv"); + let vert_shaded_square = create!(device, "shaders/shaded_square.vert.spv"); + let vert_shaded_round = create!(device, "shaders/shaded_round.vert.spv"); let vert_image = create!(device, "shaders/image.vert.spv"); let vert_glyph = create!(device, "shaders/glyph.vert.spv"); @@ -44,9 +44,9 @@ impl ShaderManager { ShaderManager { vert_image, vert_glyph, - vert_122, - vert_2, - vert_222, + vert_flat_round, + vert_shaded_square, + vert_shaded_round, frag_flat_round, frag_shaded_square, frag_shaded_round, diff --git a/crates/kas-wgpu/src/draw/shaders/flat_round.frag b/crates/kas-wgpu/src/draw/shaders/flat_round.frag index e540dbea2..efae18976 100644 --- a/crates/kas-wgpu/src/draw/shaders/flat_round.frag +++ b/crates/kas-wgpu/src/draw/shaders/flat_round.frag @@ -23,8 +23,8 @@ float sample_a(vec2 pos) { void main() { // Multi-sample alpha to avoid ugly aliasing. - vec2 off1 = vec2(off.x, 3.0 * off.y); - vec2 off2 = vec2(3.0 * off.x, off.y); + vec2 off1 = vec2(off.x, 0.0); + vec2 off2 = vec2(0.0, off.y); float alpha = sample_a(pos + off1) + sample_a(pos - off1) + sample_a(pos + off2) diff --git a/crates/kas-wgpu/src/draw/shaders/flat_round.frag.spv b/crates/kas-wgpu/src/draw/shaders/flat_round.frag.spv index be16d4cbc..5e9b85c74 100644 Binary files a/crates/kas-wgpu/src/draw/shaders/flat_round.frag.spv and b/crates/kas-wgpu/src/draw/shaders/flat_round.frag.spv differ diff --git a/crates/kas-wgpu/src/draw/shaders/scaled_122.vert b/crates/kas-wgpu/src/draw/shaders/flat_round.vert similarity index 100% rename from crates/kas-wgpu/src/draw/shaders/scaled_122.vert rename to crates/kas-wgpu/src/draw/shaders/flat_round.vert diff --git a/crates/kas-wgpu/src/draw/shaders/scaled_122.vert.spv b/crates/kas-wgpu/src/draw/shaders/flat_round.vert.spv similarity index 100% rename from crates/kas-wgpu/src/draw/shaders/scaled_122.vert.spv rename to crates/kas-wgpu/src/draw/shaders/flat_round.vert.spv diff --git a/crates/kas-wgpu/src/draw/shaders/shaded_round.frag b/crates/kas-wgpu/src/draw/shaders/shaded_round.frag index 2cf41e3ce..41e4674b4 100644 --- a/crates/kas-wgpu/src/draw/shaders/shaded_round.frag +++ b/crates/kas-wgpu/src/draw/shaders/shaded_round.frag @@ -27,8 +27,8 @@ float sample_a(vec2 dir) { void main() { // Multi-sample alpha to avoid ugly aliasing. A single colour sample is adequate. - vec2 off1 = vec2(off.x, 3.0 * off.y); - vec2 off2 = vec2(3.0 * off.x, off.y); + vec2 off1 = vec2(off.x, 0.0); + vec2 off2 = vec2(0.0, off.y); float alpha = sample_a(dir + off1) + sample_a(dir - off1) + sample_a(dir + off2) diff --git a/crates/kas-wgpu/src/draw/shaders/shaded_round.frag.spv b/crates/kas-wgpu/src/draw/shaders/shaded_round.frag.spv index 7fbe5cc1c..4c3418e3c 100644 Binary files a/crates/kas-wgpu/src/draw/shaders/shaded_round.frag.spv and b/crates/kas-wgpu/src/draw/shaders/shaded_round.frag.spv differ diff --git a/crates/kas-wgpu/src/draw/shaders/scaled_222.vert b/crates/kas-wgpu/src/draw/shaders/shaded_round.vert similarity index 100% rename from crates/kas-wgpu/src/draw/shaders/scaled_222.vert rename to crates/kas-wgpu/src/draw/shaders/shaded_round.vert diff --git a/crates/kas-wgpu/src/draw/shaders/scaled_222.vert.spv b/crates/kas-wgpu/src/draw/shaders/shaded_round.vert.spv similarity index 100% rename from crates/kas-wgpu/src/draw/shaders/scaled_222.vert.spv rename to crates/kas-wgpu/src/draw/shaders/shaded_round.vert.spv diff --git a/crates/kas-wgpu/src/draw/shaders/scaled_2.vert b/crates/kas-wgpu/src/draw/shaders/shaded_square.vert similarity index 100% rename from crates/kas-wgpu/src/draw/shaders/scaled_2.vert rename to crates/kas-wgpu/src/draw/shaders/shaded_square.vert diff --git a/crates/kas-wgpu/src/draw/shaders/scaled_2.vert.spv b/crates/kas-wgpu/src/draw/shaders/shaded_square.vert.spv similarity index 100% rename from crates/kas-wgpu/src/draw/shaders/scaled_2.vert.spv rename to crates/kas-wgpu/src/draw/shaders/shaded_square.vert.spv diff --git a/crates/kas-wgpu/src/window.rs b/crates/kas-wgpu/src/window.rs index f89a196ce..23dfcfe55 100644 --- a/crates/kas-wgpu/src/window.rs +++ b/crates/kas-wgpu/src/window.rs @@ -53,9 +53,7 @@ impl>> Window { ) -> Result { let time = Instant::now(); - // Create draw immediately (with Size::ZERO) to find ideal window size let scale_factor = shared.scale_factor as f32; - let mut draw = shared.draw.draw.new_window(Size::ZERO); let mut theme_window = shared.theme.new_window(scale_factor); let mut mgr = ManagerState::new(shared.config.clone()); @@ -87,7 +85,7 @@ impl>> Window { let size: Size = window.inner_size().into(); info!("Constucted new window with size {:?}", size); - // draw was initially created with Size::ZERO; we must resize + let mut draw = shared.draw.draw.new_window(); shared.draw.draw.resize(&mut draw, size); let surface = unsafe { shared.instance.create_surface(&window) }; diff --git a/crates/kas-widgets/src/button.rs b/crates/kas-widgets/src/button.rs index ad7725cdc..6aa293ec8 100644 --- a/crates/kas-widgets/src/button.rs +++ b/crates/kas-widgets/src/button.rs @@ -23,6 +23,7 @@ pub struct Button, M: 'static> { keys1: VirtualKeyCodes, frame_size: Size, frame_offset: Offset, + ideal_size: Size, color: Option, #[widget_derive] #[widget] @@ -37,6 +38,7 @@ impl, M: 'static> Debug for Button { .field("keys1", &self.keys1) .field("frame_size", &self.frame_size) .field("frame_offset", &self.frame_offset) + .field("ideal_size", &self.ideal_size) .field("color", &self.color) .field("label", &self.label) .finish_non_exhaustive() @@ -64,10 +66,14 @@ impl, M: 'static> Layout for Button { let (rules, offset, size) = frame_rules.surround_as_margin(content_rules); self.frame_size.set_component(axis, size); self.frame_offset.set_component(axis, offset); + self.ideal_size.set_component(axis, rules.ideal_size()); rules } - fn set_rect(&mut self, mgr: &mut Manager, mut rect: Rect, align: AlignHints) { + fn set_rect(&mut self, mgr: &mut Manager, rect: Rect, align: AlignHints) { + let mut rect = align + .complete(Align::Centre, Align::Centre) + .aligned_rect(self.ideal_size, rect); self.core.rect = rect; rect.pos += self.frame_offset; rect.size -= self.frame_size; @@ -89,6 +95,7 @@ impl> Button { keys1: Default::default(), frame_size: Default::default(), frame_offset: Default::default(), + ideal_size: Default::default(), color: None, label, on_push: None, @@ -110,6 +117,7 @@ impl> Button { keys1: self.keys1, frame_size: self.frame_size, frame_offset: self.frame_offset, + ideal_size: self.ideal_size, color: self.color, label: self.label, on_push: Some(Rc::new(f)), @@ -205,6 +213,7 @@ pub struct TextButton { keys1: VirtualKeyCodes, frame_size: Size, frame_offset: Offset, + ideal_size: Size, color: Option, label: Text, on_push: Option Option>>, @@ -217,6 +226,7 @@ impl Debug for TextButton { .field("keys1", &self.keys1) .field("frame_size", &self.frame_size) .field("frame_offset", &self.frame_offset) + .field("ideal_size", &self.ideal_size) .field("color", &self.color) .field("label", &self.label) .finish_non_exhaustive() @@ -245,10 +255,14 @@ impl Layout for TextButton { let (rules, offset, size) = frame_rules.surround_as_margin(content_rules); self.frame_size.set_component(axis, size); self.frame_offset.set_component(axis, offset); + self.ideal_size.set_component(axis, rules.ideal_size()); rules } fn set_rect(&mut self, _: &mut Manager, rect: Rect, align: AlignHints) { + let rect = align + .complete(Align::Centre, Align::Centre) + .aligned_rect(self.ideal_size, rect); self.core.rect = rect; let size = rect.size - self.frame_size; self.label.update_env(|env| { @@ -276,6 +290,7 @@ impl TextButton { keys1: Default::default(), frame_size: Default::default(), frame_offset: Default::default(), + ideal_size: Default::default(), color: None, label: text, on_push: None, @@ -297,6 +312,7 @@ impl TextButton { keys1: self.keys1, frame_size: self.frame_size, frame_offset: self.frame_offset, + ideal_size: self.ideal_size, color: self.color, label: self.label, on_push: Some(Rc::new(f)), diff --git a/crates/kas-widgets/src/editbox.rs b/crates/kas-widgets/src/editbox.rs index 948179c2e..c7e2494a9 100644 --- a/crates/kas-widgets/src/editbox.rs +++ b/crates/kas-widgets/src/editbox.rs @@ -353,7 +353,7 @@ impl Layout for EditBox { if !self.rect().contains(coord) { return None; } - self.inner.find_id(coord).or(Some(self.id())) + Some(self.inner.id()) } fn draw(&self, draw_handle: &mut dyn DrawHandle, mgr: &event::ManagerState, disabled: bool) { @@ -407,7 +407,7 @@ impl std::ops::DerefMut for EditBox { /// line-wrapping and a larger vertical height). This mode is only recommended /// for short texts for performance reasons. #[derive(Clone, Default, Debug, Widget)] -#[widget(config(key_nav = true, cursor_icon = event::CursorIcon::Text))] +#[widget(config(key_nav = true, hover_highlight = true, cursor_icon = event::CursorIcon::Text))] #[handler(handle=noauto, generics = <> where G: EditGuard)] pub struct EditField { #[widget_core] diff --git a/crates/kas-widgets/src/progress.rs b/crates/kas-widgets/src/progress.rs index 2a6b87c8e..03ab01636 100644 --- a/crates/kas-widgets/src/progress.rs +++ b/crates/kas-widgets/src/progress.rs @@ -17,6 +17,7 @@ pub struct ProgressBar { #[widget_core] core: CoreData, direction: D, + width: i32, value: f32, } @@ -39,6 +40,7 @@ impl ProgressBar { ProgressBar { core: Default::default(), direction, + width: 0, value: 0.0, } } @@ -81,10 +83,20 @@ impl Layout for ProgressBar { if self.direction.is_vertical() == axis.is_vertical() { SizeRules::new(size.0, size.0, margins, Stretch::High) } else { + self.width = size.1; SizeRules::fixed(size.1, margins) } } + fn set_rect(&mut self, _: &mut Manager, rect: Rect, align: AlignHints) { + let mut ideal_size = Size::splat(self.width); + ideal_size.set_component(self.direction, i32::MAX); + let rect = align + .complete(Align::Centre, Align::Centre) + .aligned_rect(ideal_size, rect); + self.core.rect = rect; + } + fn draw(&self, draw_handle: &mut dyn DrawHandle, mgr: &ManagerState, disabled: bool) { let dir = self.direction.as_direction(); let state = self.input_state(mgr, disabled); diff --git a/crates/kas-widgets/src/scrollbar.rs b/crates/kas-widgets/src/scrollbar.rs index 1c52fd58a..70460c4f2 100644 --- a/crates/kas-widgets/src/scrollbar.rs +++ b/crates/kas-widgets/src/scrollbar.rs @@ -21,6 +21,7 @@ pub struct ScrollBar { core: CoreData, direction: D, // Terminology assumes vertical orientation: + width: i32, min_handle_len: i32, handle_len: i32, handle_value: i32, // contract: > 0 @@ -48,6 +49,7 @@ impl ScrollBar { ScrollBar { core: Default::default(), direction, + width: 0, min_handle_len: 0, handle_len: 0, handle_value: 1, @@ -207,11 +209,17 @@ impl Layout for ScrollBar { if self.direction.is_vertical() == axis.is_vertical() { SizeRules::new(min_len, min_len, margins, Stretch::High) } else { + self.width = size.1; SizeRules::fixed(size.1, margins) } } fn set_rect(&mut self, mgr: &mut Manager, rect: Rect, align: AlignHints) { + let mut ideal_size = Size::splat(self.width); + ideal_size.set_component(self.direction, i32::MAX); + let rect = align + .complete(Align::Centre, Align::Centre) + .aligned_rect(ideal_size, rect); self.core.rect = rect; self.handle.set_rect(mgr, rect, align); let _ = self.update_handle(); @@ -491,6 +499,17 @@ impl ScrollBars { pub fn inner_mut(&mut self) -> &mut W { &mut self.inner } + + fn draw_(&self, draw_handle: &mut dyn DrawHandle, mgr: &event::ManagerState, disabled: bool) { + let disabled = disabled || self.is_disabled(); + if self.show_bars.0 { + self.horiz_bar.draw(draw_handle, mgr, disabled); + } + if self.show_bars.1 { + self.vert_bar.draw(draw_handle, mgr, disabled); + } + self.inner.draw(draw_handle, mgr, disabled); + } } impl Scrollable for ScrollBars { @@ -578,15 +597,38 @@ impl Layout for ScrollBars { .or(Some(self.id())) } + #[cfg(feature = "min_spec")] + default fn draw( + &self, + draw_handle: &mut dyn DrawHandle, + mgr: &event::ManagerState, + disabled: bool, + ) { + self.draw_(draw_handle, mgr, disabled); + } + #[cfg(not(feature = "min_spec"))] fn draw(&self, draw_handle: &mut dyn DrawHandle, mgr: &event::ManagerState, disabled: bool) { - let disabled = disabled || self.is_disabled(); - if self.show_bars.0 { - self.horiz_bar.draw(draw_handle, mgr, disabled); - } - if self.show_bars.1 { - self.vert_bar.draw(draw_handle, mgr, disabled); - } - self.inner.draw(draw_handle, mgr, disabled); + self.draw_(draw_handle, mgr, disabled); + } +} + +#[cfg(feature = "min_spec")] +impl Layout for ScrollBars> { + fn draw(&self, draw_handle: &mut dyn DrawHandle, mgr: &event::ManagerState, disabled: bool) { + let disabled = disabled || self.is_disabled() || self.inner.is_disabled(); + // Enlarge clip region to *our* rect: + draw_handle.with_clip_region(self.core.rect, self.inner.scroll_offset(), &mut |handle| { + self.inner.inner().draw(handle, mgr, disabled) + }); + // Use a second clip region to force draw order: + draw_handle.with_clip_region(self.core.rect, Offset::ZERO, &mut |draw_handle| { + if self.show_bars.0 { + self.horiz_bar.draw(draw_handle, mgr, disabled); + } + if self.show_bars.1 { + self.vert_bar.draw(draw_handle, mgr, disabled); + } + }); } } diff --git a/crates/kas-widgets/src/separator.rs b/crates/kas-widgets/src/separator.rs index fcbc0a0a7..855a85890 100644 --- a/crates/kas-widgets/src/separator.rs +++ b/crates/kas-widgets/src/separator.rs @@ -49,7 +49,8 @@ impl Separator { impl Layout for Separator { fn size_rules(&mut self, size_handle: &mut dyn SizeHandle, axis: AxisInfo) -> SizeRules { - SizeRules::extract_fixed(axis, size_handle.separator(), Default::default()) + let margins = size_handle.frame_margins(); + SizeRules::extract_fixed(axis, size_handle.separator(), margins) } fn draw(&self, draw_handle: &mut dyn DrawHandle, _: &event::ManagerState, _: bool) { diff --git a/examples/gallery.rs b/examples/gallery.rs index 08b343334..af9bcebb4 100644 --- a/examples/gallery.rs +++ b/examples/gallery.rs @@ -103,8 +103,8 @@ fn main() -> Result<(), kas::shell::Error> { #[cfg(feature = "stack_dst")] let theme = kas::theme::MultiTheme::builder() - .add("shaded", kas::theme::ShadedTheme::new()) .add("flat", kas::theme::FlatTheme::new()) + .add("shaded", kas::theme::ShadedTheme::new()) .build(); #[cfg(not(feature = "stack_dst"))] let theme = kas::theme::ShadedTheme::new(); @@ -119,8 +119,8 @@ fn main() -> Result<(), kas::shell::Error> { } let themes = vec![ - MenuEntry::new("&Shaded", Menu::Theme("shaded")).boxed_menu(), MenuEntry::new("&Flat", Menu::Theme("flat")).boxed_menu(), + MenuEntry::new("&Shaded", Menu::Theme("shaded")).boxed_menu(), ]; // Enumerate colour schemes. Access through the toolkit since this handles // config loading. @@ -253,7 +253,7 @@ fn main() -> Result<(), kas::shell::Error> { .on_select(|_, index| Some(Item::Combo((index + 1).cast()))), #[widget(row=8, col=0)] _ = Label::new("Slider"), #[widget(row=8, col=1, handler = handle_slider)] s = - Slider::::new(-2, 2, 1).with_value(0), + Slider::::new(0, 10, 1).with_value(0), #[widget(row=9, col=0)] _ = Label::new("ScrollBar"), #[widget(row=9, col=1, handler = handle_scroll)] sc: ScrollBar = ScrollBar::new().with_limits(100, 20), @@ -324,7 +324,7 @@ fn main() -> Result<(), kas::shell::Error> { fn activations(&mut self, mgr: &mut Manager, item: Item) -> VoidResponse { match item { Item::Button => println!("Clicked!"), - Item::LightTheme => mgr.adjust_theme(|theme| theme.set_scheme("")), + Item::LightTheme => mgr.adjust_theme(|theme| theme.set_scheme("light")), Item::DarkTheme => mgr.adjust_theme(|theme| theme.set_scheme("dark")), Item::Check(b) => println!("CheckBox: {}", b), Item::Combo(c) => println!("ComboBox: {}", c), diff --git a/examples/mandlebrot/mandlebrot.rs b/examples/mandlebrot/mandlebrot.rs index 06dc36962..0cdbd07c2 100644 --- a/examples/mandlebrot/mandlebrot.rs +++ b/examples/mandlebrot/mandlebrot.rs @@ -147,7 +147,7 @@ struct PipeWindow { impl CustomPipe for Pipe { type Window = PipeWindow; - fn new_window(&self, _: &wgpu::Device, _: Size) -> Self::Window { + fn new_window(&self, _: &wgpu::Device) -> Self::Window { let push_constants = PushConstants { p: DVec2::splat(0.0), q: DVec2::splat(1.0),