diff --git a/Cargo.lock b/Cargo.lock index 1a64d5f..734e3d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -309,10 +309,11 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.84" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ + "jobserver", "libc", ] @@ -564,7 +565,7 @@ checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" [[package]] name = "figures" version = "0.1.0" -source = "git+https://github.com/khonsulabs/figures#7b41393c44d4def606790e340c98450b603010b4" +source = "git+https://github.com/khonsulabs/figures#52d06f3623cdb47128f1537fdadfe190f7afa88e" dependencies = [ "bytemuck", "euclid", @@ -955,6 +956,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c37f63953c4c63420ed5fd3d6d398c719489b9f872b9fa683262f8edd363c7d" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.65" @@ -990,7 +1000,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.1.0" -source = "git+https://github.com/khonsulabs/kludgine#09790aafb5a9c3b0da034387adead9960eb06bc7" +source = "git+https://github.com/khonsulabs/kludgine#c0135da62309896fab58a2f5b517c32cde151fb3" dependencies = [ "ahash", "alot", @@ -1828,9 +1838,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.23" +version = "0.38.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb93593068e9babdad10e4fce47dc9b3ac25315a72a59766ffd9e9a71996a04" +checksum = "9ad981d6c340a49cdc40a1028d9c6084ec7e9fa33fcb839cab656a267071e234" dependencies = [ "bitflags 2.4.1", "errno", @@ -2941,18 +2951,18 @@ checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" [[package]] name = "zerocopy" -version = "0.7.25" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557" +checksum = "e97e415490559a91254a2979b4829267a57d2fcd741a98eee8b722fb57289aa0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.25" +version = "0.7.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b" +checksum = "dd7e48ccf166952882ca8bd778a43502c64f33bf94c12ebe2a7f08e5a0f6689f" dependencies = [ "proc-macro2", "quote", diff --git a/examples/buttons.rs b/examples/buttons.rs index 170ffa6..7ebb3f8 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -1,29 +1,49 @@ use gooey::value::Dynamic; use gooey::widget::MakeWidget; -use gooey::widgets::button::ButtonOutline; -use gooey::widgets::Button; +use gooey::widgets::button::ButtonKind; +use gooey::widgets::{Button, Checkbox}; use gooey::Run; -use kludgine::Color; -// begin rustme snippet: readme fn main() -> gooey::Result { - // Create a dynamic usize. - let count = Dynamic::new(0_isize); + let clicked_label = Dynamic::new(String::from("Click a Button")); + let default_is_outline = Dynamic::new(false); + let default_button_style = default_is_outline.map_each(|is_outline| { + if *is_outline { + ButtonKind::Outline + } else { + ButtonKind::Solid + } + }); - // Create a new button with a label that is produced by mapping the contents - // of `count`. - Button::new(count.map_each(ToString::to_string)) - // Set the `on_click` callback to a closure that increments the counter. - .on_click(count.with_clone(|count| move |_| count.set(count.get() + 1))) + clicked_label + .clone() .and( - // Creates a second, outlined button - Button::new(count.map_each(ToString::to_string)) - // Set the `on_click` callback to a closure that decrements the counter. - .on_click(count.with_clone(|count| move |_| count.set(count.get() - 1))) - .with(&ButtonOutline, Color::DARKRED), + Button::new("Normal Button") + .on_click( + clicked_label.with_clone(|label| { + move |_| label.set(String::from("Clicked Normal Button")) + }), + ) + .and( + Button::new("Outline Button") + .on_click(clicked_label.with_clone(|label| { + move |_| label.set(String::from("Clicked Outline Button")) + })) + .kind(ButtonKind::Outline), + ) + .and( + Button::new("Default Button") + .on_click(clicked_label.with_clone(|label| { + move |_| label.set(String::from("Clicked Default Button")) + })) + .kind(default_button_style) + .into_default(), + ) + .and(Checkbox::new(default_is_outline, "Set Default to Outline")) + .into_columns(), ) - .into_columns() - // Run the button as an an application. + .into_rows() + .centered() + .expand() .run() } -// end rustme snippet diff --git a/examples/checkbox.rs b/examples/checkbox.rs new file mode 100644 index 0000000..18a5df4 --- /dev/null +++ b/examples/checkbox.rs @@ -0,0 +1,19 @@ +use gooey::value::Dynamic; +use gooey::widget::MakeWidget; +use gooey::widgets::checkbox::CheckboxState; +use gooey::widgets::Checkbox; +use gooey::Run; + +fn main() -> gooey::Result { + let checkbox_state = Dynamic::new(CheckboxState::Checked); + let label = checkbox_state.map_each(|state| format!("Check Me! Current: {state:?}")); + + Checkbox::new(checkbox_state.clone(), label) + .and("Maybe".into_button().on_click(move |()| { + checkbox_state.update(CheckboxState::Indeterminant); + })) + .into_columns() + .centered() + .expand() + .run() +} diff --git a/examples/theme.rs b/examples/theme.rs index 6067b5c..948c53b 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use gooey::animation::ZeroToOne; use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::{ @@ -87,26 +85,14 @@ fn dark_mode_slider() -> (Dynamic, impl MakeWidget) { ) } -fn create_paired_string(initial_value: T) -> (Dynamic, Dynamic) -where - T: ToString + PartialEq + FromStr + Default + Send + Sync + 'static, -{ - let float = Dynamic::new(initial_value); - let text = float.map_each_unique(|f| f.to_string()); - text.for_each(float.with_clone(|float| { - move |text: &String| { - let _result = float.try_update(text.parse().unwrap_or_default()); - } - })); - (float, text) -} - fn color_editor( initial_color: ColorSource, label: &str, ) -> (Dynamic, impl MakeWidget) { - let (hue, hue_text) = create_paired_string(initial_color.hue.into_degrees()); - let (saturation, saturation_text) = create_paired_string(initial_color.saturation); + let hue = Dynamic::new(initial_color.hue.into_degrees()); + let hue_text = hue.linked_string(); + let saturation = Dynamic::new(initial_color.saturation); + let saturation_text = saturation.linked_string(); let color = (&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation)); @@ -274,6 +260,7 @@ fn surface_theme(theme: Dynamic) -> impl MakeWidget { fn color_theme(theme: Dynamic, label: &str) -> impl MakeWidget { let color = theme.map_each(|theme| theme.color); let dim_color = theme.map_each(|theme| theme.color_dim); + let bright_color = theme.map_each(|theme| theme.color_bright); let on_color = theme.map_each(|theme| theme.on_color); let container = theme.map_each(|theme| theme.container); let on_container = theme.map_each(|theme| theme.on_container); @@ -284,6 +271,11 @@ fn color_theme(theme: Dynamic, label: &str) -> impl MakeWidget { &format!("{label} Dim"), on_color.clone(), )) + .and(swatch( + bright_color.clone(), + &format!("{label} bright"), + on_color.clone(), + )) .and(swatch( on_color.clone(), &format!("On {label}"), diff --git a/src/context.rs b/src/context.rs index a7cc3ae..3782beb 100644 --- a/src/context.rs +++ b/src/context.rs @@ -481,7 +481,7 @@ impl<'context, 'window, 'clip, 'gfx, 'pass> GraphicsContext<'context, 'window, ' /// Strokes an outline around this widget's contents. pub fn stroke_outline(&mut self, color: Color, options: StrokeOptions) where - Unit: ScreenScale, + Unit: ScreenScale, { let visible_rect = Rect::from(self.gfx.region().size - (Px(1), Px(1))); let focus_ring = diff --git a/src/lib.rs b/src/lib.rs index 7309d9b..908878f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,7 +25,7 @@ use std::ops::Sub; pub use kludgine; use kludgine::app::winit::error::EventLoopError; use kludgine::figures::units::UPx; -use kludgine::figures::{Fraction, IntoUnsigned, ScreenUnit}; +use kludgine::figures::{Fraction, ScreenUnit}; pub use names::Name; pub use utils::{Lazy, WithClone}; @@ -60,7 +60,7 @@ impl ConstraintLimit { where Unit: ScreenUnit, { - let measured = measured.into_px(scale).into_unsigned(); + let measured = measured.into_upx(scale); match self { ConstraintLimit::Known(size) => size.max(measured), ConstraintLimit::ClippedAfter(_) => measured, diff --git a/src/styles.rs b/src/styles.rs index 7cadfea..ddc560e 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -12,7 +12,7 @@ use std::sync::Arc; use ahash::AHashMap; use kludgine::figures::units::{Lp, Px, UPx}; -use kludgine::figures::{Fraction, IntoUnsigned, Rect, ScreenScale, Size}; +use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Rect, ScreenScale, Size}; use kludgine::Color; use palette::{IntoColor, Okhsl, OklabHue, Srgb}; @@ -376,6 +376,7 @@ impl From for Dimension { impl ScreenScale for Dimension { type Lp = Lp; type Px = Px; + type UPx = UPx; fn into_px(self, scale: kludgine::figures::Fraction) -> Px { match self { @@ -398,6 +399,17 @@ impl ScreenScale for Dimension { fn from_lp(lp: Lp, _scale: kludgine::figures::Fraction) -> Self { Self::from(lp) } + + fn into_upx(self, scale: Fraction) -> Self::UPx { + match self { + Dimension::Px(px) => px.into_unsigned(), + Dimension::Lp(lp) => lp.into_upx(scale), + } + } + + fn from_upx(px: Self::UPx, _scale: Fraction) -> Self { + Self::from(px.into_signed()) + } } impl Mul for Dimension { @@ -469,10 +481,10 @@ impl DimensionRange { #[must_use] pub fn clamp(&self, mut size: UPx, scale: Fraction) -> UPx { if let Some(min) = self.minimum() { - size = size.max(min.into_px(scale).into_unsigned()); + size = size.max(min.into_upx(scale)); } if let Some(max) = self.maximum() { - size = size.min(max.into_px(scale).into_unsigned()); + size = size.min(max.into_upx(scale)); } size } @@ -1152,6 +1164,8 @@ pub struct ColorTheme { pub color: Color, /// The primary color, dimmed for de-emphasized or disabled content. pub color_dim: Color, + /// The primary color, brightened for highlighting content. + pub color_bright: Color, /// The color for content that sits atop the primary color. pub on_color: Color, /// The backgrond color for containers. @@ -1167,6 +1181,7 @@ impl ColorTheme { Self { color: source.color(40), color_dim: source.color(30), + color_bright: source.color(45), on_color: source.color(100), container: source.color(90), on_container: source.color(10), @@ -1179,6 +1194,7 @@ impl ColorTheme { Self { color: source.color(70), color_dim: source.color(60), + color_bright: source.color(75), on_color: source.color(10), container: source.color(30), on_container: source.color(90), diff --git a/src/styles/components.rs b/src/styles/components.rs index 2e95ef4..ae50004 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -96,6 +96,8 @@ define_components! { SurfaceColor(Color, "surface_color", .surface.color) /// The [`Color`] to use when rendering text. TextColor(Color, "text_color", .surface.on_color) + /// The [`Color`] to use when rendering text in a more subdued tone. + TextColorVariant(Color, "text_color_variant", .surface.on_color_variant) /// A [`Color`] to be used as a highlight color. HighlightColor(Color,"highlight_color",.primary.color.with_alpha(128)) /// Intrinsic, uniform padding for a widget. @@ -122,6 +124,8 @@ define_components! { AutoFocusableControls(FocusableWidgets, "focus", FocusableWidgets::default()) /// A [`Color`] to be used as the background color of a widget. WidgetBackground(Color, "widget_backgrond_color", Color::CLEAR_WHITE) + /// A [`Color`] to be used to accent a widget. + WidgetAccentColor(Color, "widget_accent_color", .primary.color) /// A [`Color`] to be used as an outline color. OutlineColor(Color, "outline_color", .surface.outline) /// A [`Color`] to be used as an outline color. diff --git a/src/value.rs b/src/value.rs index a19dacc..c378755 100644 --- a/src/value.rs +++ b/src/value.rs @@ -5,6 +5,7 @@ use std::fmt::{Debug, Display}; use std::future::Future; use std::ops::{Deref, DerefMut}; use std::panic::AssertUnwindSafe; +use std::str::FromStr; use std::sync::{Arc, Condvar, Mutex, MutexGuard, TryLockError}; use std::task::{Poll, Waker}; use std::thread::ThreadId; @@ -43,6 +44,74 @@ impl Dynamic { })) } + /// Returns a new dynamic that has its contents linked with `self` by the + /// pair of mapping functions provided. + /// + /// When the returned dynamic is updated, `r_into_t` will be invoked. This + /// function accepts `&R` and can return `T`, or `Option`. If a value is + /// produced, `self` will be updated with the new value. + /// + /// When `self` is updated, `t_into_r` will be invoked. This function + /// accepts `&T` and can return `R` or `Option`. If a value is produced, + /// the returned dynamic will be updated with the new value. + /// + /// # Panics + /// + /// This function panics if calling `t_into_r` with the current contents of + /// the Dynamic produces a `None` value. This requirement is only for the + /// first invocation, and it is guaranteed to occur before this function + /// returns. + pub fn linked( + &self, + mut t_into_r: TIntoR, + mut r_into_t: RIntoT, + ) -> Dynamic + where + T: PartialEq + Send + 'static, + R: PartialEq + Send + 'static, + TIntoRResult: Into> + Send + 'static, + RIntoTResult: Into> + Send + 'static, + TIntoR: FnMut(&T) -> TIntoRResult + Send + 'static, + RIntoT: FnMut(&R) -> RIntoTResult + Send + 'static, + { + let initial_r = self + .map_ref(&mut t_into_r) + .into() + .expect("t_into_r must succeed with the current value"); + let r = Dynamic::new(initial_r); + r.with_clone(move |r| { + self.for_each(move |t| { + if let Some(update) = t_into_r(t).into() { + let _result = r.try_update(update); + } + }); + }); + + self.with_clone(|t| { + r.with_for_each(move |r| { + if let Some(update) = r_into_t(r).into() { + let _result = t.try_update(update); + } + }) + }) + } + + /// Creates a [linked](Self::linked) dynamic containing a `String`. + /// + /// When `self` is updated, [`ToString::to_string()`] will be called to + /// produce a new string value to store in the returned dynamic. + /// + /// When the returned dynamic is updated, [`str::parse`](std::str) is called + /// to produce a new `T`. If an error is returned, `self` will not be + /// updated. Otherwise, `self` will be updated with the produced value. + #[must_use] + pub fn linked_string(&self) -> Dynamic + where + T: ToString + FromStr + PartialEq + Send + 'static, + { + self.linked(ToString::to_string, |s: &String| s.parse().ok()) + } + /// Maps the contents with read-only access. /// /// # Panics @@ -749,6 +818,45 @@ impl DynamicReader { value } + /// Returns a clone of the currently contained value. + /// + /// This function marks the currently stored value as being read. + /// + /// `context` will be invalidated when the value is updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + pub fn get_tracking_refresh(&mut self, context: &WidgetContext<'_, '_>) -> T + where + T: Clone, + { + self.source.redraw_when_changed(context.handle()); + self.get() + } + + /// Returns a clone of the currently contained value. + /// + /// This function marks the currently stored value as being read. + /// + /// `context` will be invalidated when the value is updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + pub fn get_tracking_invalidate(&mut self, context: &WidgetContext<'_, '_>) -> T + where + T: Clone, + { + self.source + .invalidate_when_changed(context.handle(), context.widget().id()); + self.get() + } + /// Blocks the current thread until the contained value has been updated or /// there are no remaining writers for the value. /// diff --git a/src/widget.rs b/src/widget.rs index be850d9..1cb5c29 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -228,6 +228,18 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { /// Returns the child widget. fn child_mut(&mut self) -> &mut WidgetRef; + /// Draws the background of the widget. + /// + /// This is invoked before the wrapped widget is drawn. + #[allow(unused_variables)] + fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {} + + /// Draws the foreground of the widget. + /// + /// This is invoked after the wrapped widget is drawn. + #[allow(unused_variables)] + fn redraw_foreground(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {} + /// Returns the rectangle that the child widget should occupy given /// `available_space`. #[allow(unused_variables)] @@ -266,7 +278,7 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> WrappedLayout { - Size::::new( + Size::new( available_space .width .fit_measured(size.width, context.gfx.scale()), @@ -419,8 +431,12 @@ where context.gfx.fill(color); } + self.redraw_background(context); + let child = self.child_mut().mounted(&mut context.as_event_context()); context.for_other(&child).redraw(); + + self.redraw_foreground(context); } fn layout( @@ -627,9 +643,12 @@ pub trait MakeWidget: Sized { /// Resizes `self` to `width`. /// - /// `width` can be an individual - /// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a - /// range. + /// `width` can be an any of: + /// + /// - [`Dimension`] + /// - [`Px`] + /// - [`Lp`](crate::kludgine::figures::units::Lp) + /// - A range of any fo the above. #[must_use] fn width(self, width: impl Into) -> Resize { Resize::from_width(width, self) @@ -637,9 +656,12 @@ pub trait MakeWidget: Sized { /// Resizes `self` to `height`. /// - /// `height` can be an individual - /// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a - /// range. + /// `height` can be an any of: + /// + /// - [`Dimension`] + /// - [`Px`] + /// - [`Lp`](crate::kludgine::figures::units::Lp) + /// - A range of any fo the above. #[must_use] fn height(self, height: impl Into) -> Resize { Resize::from_height(height, self) diff --git a/src/widgets.rs b/src/widgets.rs index b43bc77..0c0061e 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -3,6 +3,7 @@ mod align; pub mod button; mod canvas; +pub mod checkbox; pub mod container; mod expand; mod input; @@ -21,6 +22,7 @@ mod tilemap; pub use align::Align; pub use button::Button; pub use canvas::Canvas; +pub use checkbox::Checkbox; pub use container::Container; pub use expand::Expand; pub use input::Input; diff --git a/src/widgets/align.rs b/src/widgets/align.rs index f7c7106..5ee0b72 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -1,7 +1,7 @@ use std::fmt::Debug; use kludgine::figures::units::UPx; -use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; +use kludgine::figures::{Fraction, IntoSigned, Point, Rect, ScreenScale, Size}; use crate::context::{AsEventContext, LayoutContext}; use crate::styles::{Edges, FlexibleDimension}; @@ -125,15 +125,11 @@ impl FrameInfo { fn new(scale: Fraction, a: FlexibleDimension, b: FlexibleDimension) -> Self { let a = match a { FlexibleDimension::Auto => None, - FlexibleDimension::Dimension(dimension) => { - Some(dimension.into_px(scale).into_unsigned()) - } + FlexibleDimension::Dimension(dimension) => Some(dimension.into_upx(scale)), }; let b = match b { FlexibleDimension::Auto => None, - FlexibleDimension::Dimension(dimension) => { - Some(dimension.into_px(scale).into_unsigned()) - } + FlexibleDimension::Dimension(dimension) => Some(dimension.into_upx(scale)), }; Self { a, b } } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index c55b5ce..75f2b40 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -4,16 +4,17 @@ use std::time::Duration; use kludgine::app::winit::event::{DeviceId, ElementState, KeyEvent, MouseButton}; use kludgine::figures::units::{Lp, Px, UPx}; -use kludgine::figures::{IntoSigned, IntoUnsigned, Point, Rect, ScreenScale, Size}; -use kludgine::shapes::StrokeOptions; +use kludgine::figures::{IntoSigned, Point, Rect, ScreenScale, Size}; +use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::Color; use crate::animation::{AnimationHandle, AnimationTarget, LinearInterpolate, Spawn}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::styles::components::{ - AutoFocusableControls, Easing, IntrinsicPadding, OpaqueWidgetColor, SurfaceColor, TextColor, + AutoFocusableControls, Easing, HighlightColor, IntrinsicPadding, OpaqueWidgetColor, + OutlineColor, SurfaceColor, TextColor, }; -use crate::styles::Styles; +use crate::styles::{ColorExt, Styles}; use crate::utils::ModifiersExt; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED, IGNORED}; @@ -27,17 +28,100 @@ pub struct Button { pub on_click: Option>, /// The enabled state of the button. pub enabled: Value, - currently_enabled: bool, + /// The kind of button to draw. + pub kind: Value, buttons_pressed: usize, - active_style: Option>, + cached_state: CacheState, + active_colors: Option>, color_animation: AnimationHandle, } +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +struct CacheState { + enabled: bool, + kind: ButtonKind, +} + +/// The type of a [`Button`] or similar clickable widget. +#[derive(Debug, Default, Eq, PartialEq, Clone, Copy)] +pub enum ButtonKind { + /// A solid button. + #[default] + Solid, + /// An outline button, which uses the same colors as [`ButtonKind::Solid`] + /// but swaps the outline and background colors. + Outline, + /// A transparent button, which is transparent until it is hovered. + Transparent, +} + +impl ButtonKind { + /// Returns the [`ButtonColors`] to apply for a + /// [default](MakeWidget::into_default) button. + #[must_use] + pub fn colors_for_default( + self, + visual_state: VisualState, + context: &WidgetContext<'_, '_>, + ) -> ButtonColors { + match self { + ButtonKind::Solid => match visual_state { + VisualState::Normal => ButtonColors { + background: context.theme().primary.color, + foreground: context.theme().primary.on_color, + outline: context.get(&ButtonOutline), + }, + VisualState::Hovered => ButtonColors { + background: context.theme().primary.color_bright, + foreground: context.theme().primary.on_color, + outline: context.get(&ButtonHoverOutline), + }, + VisualState::Active => ButtonColors { + background: context.theme().primary.color_dim, + foreground: context.theme().primary.on_color, + outline: context.get(&ButtonActiveOutline), + }, + VisualState::Disabled => ButtonColors { + background: context.theme().primary.color_dim, + foreground: context.theme().primary.on_color, + outline: context.get(&ButtonDisabledOutline), + }, + }, + ButtonKind::Outline | ButtonKind::Transparent => match visual_state { + VisualState::Normal => ButtonColors { + background: context.get(&ButtonOutline), + foreground: context.theme().primary.color, + outline: context.theme().primary.color, + }, + VisualState::Hovered => ButtonColors { + background: context.get(&ButtonHoverOutline), + foreground: context.theme().primary.color, + outline: context.theme().primary.color_bright, + }, + VisualState::Active => ButtonColors { + background: context.get(&ButtonActiveOutline), + foreground: context.theme().primary.color, + outline: context.theme().surface.color, + }, + VisualState::Disabled => ButtonColors { + background: context.get(&ButtonDisabledOutline), + foreground: context.theme().primary.on_color, + outline: context.theme().primary.color_dim, + }, + }, + } + } +} + +/// The coloring to apply to a [`Button`] or button-like widget. #[derive(Debug, PartialEq, Eq, Clone, Copy, LinearInterpolate)] -struct ButtonStyle { - background: Color, - foreground: Color, - outline: Color, +pub struct ButtonColors { + /// The background color of the button. + pub background: Color, + /// The foreground (text) color of the button. + pub foreground: Color, + /// A color to use to surround the button. + pub outline: Color, } impl Button { @@ -47,13 +131,24 @@ impl Button { content: content.widget_ref(), on_click: None, enabled: Value::Constant(true), - currently_enabled: true, + cached_state: CacheState { + enabled: true, + kind: ButtonKind::default(), + }, buttons_pressed: 0, - active_style: None, + active_colors: None, + kind: Value::Constant(ButtonKind::default()), color_animation: AnimationHandle::default(), } } + /// Sets the button's `kind` and returns self. + #[must_use] + pub fn kind(mut self, kind: impl IntoValue) -> Self { + self.kind = kind.into_value(); + self + } + /// Sets the `on_click` callback and returns self. /// /// This callback will be invoked each time the button is clicked. @@ -70,7 +165,6 @@ impl Button { #[must_use] pub fn enabled(mut self, enabled: impl IntoValue) -> Self { self.enabled = enabled.into_value(); - self.currently_enabled = self.enabled.get(); self } @@ -82,37 +176,72 @@ impl Button { } } - fn update_colors(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) { - let new_style = match () { - () if !self.enabled.get() => ButtonStyle { - background: context.get(&ButtonDisabledBackground), - foreground: context.get(&ButtonDisabledForeground), - outline: context.get(&ButtonDisabledOutline), + fn visual_style(&self, context: &WidgetContext<'_, '_>) -> VisualState { + if !self.enabled.get_tracked(context) { + VisualState::Disabled + } else if context.active() { + VisualState::Active + } else if context.hovered() { + VisualState::Hovered + } else { + VisualState::Normal + } + } + + /// Returns the coloring to apply to a [`ButtonKind::Transparent`] button. + #[must_use] + pub fn colors_for_transparent( + visual_state: VisualState, + context: &WidgetContext<'_, '_>, + ) -> ButtonColors { + match visual_state { + VisualState::Normal => ButtonColors { + background: Color::CLEAR_BLACK, + foreground: context.get(&TextColor), + outline: context.get(&ButtonOutline), }, - // TODO this probably should use actual style. - () if context.is_default() => ButtonStyle { - background: context.theme().primary.color, - foreground: context.theme().primary.on_color, - outline: Color::CLEAR_BLACK, + VisualState::Hovered => ButtonColors { + background: context.get(&OpaqueWidgetColor), + foreground: context.get(&TextColor), + outline: context.get(&ButtonHoverOutline), }, - () if context.active() => ButtonStyle { + VisualState::Active => ButtonColors { background: context.get(&ButtonActiveBackground), foreground: context.get(&ButtonActiveForeground), outline: context.get(&ButtonActiveOutline), }, - () if context.hovered() => ButtonStyle { - background: context.get(&ButtonHoverBackground), - foreground: context.get(&ButtonHoverForeground), - outline: context.get(&ButtonHoverOutline), - }, - () => ButtonStyle { - background: context.get(&ButtonBackground), - foreground: context.get(&ButtonForeground), - outline: context.get(&ButtonOutline), + VisualState::Disabled => ButtonColors { + background: Color::CLEAR_BLACK, + foreground: context.theme().surface.on_color_variant, + outline: context.get(&ButtonDisabledOutline), }, + } + } + + fn determine_stateful_colors(&mut self, context: &WidgetContext<'_, '_>) -> ButtonColors { + let kind = self.kind.get_tracked(context); + let visual_state = self.visual_style(context); + + self.cached_state = CacheState { + enabled: !matches!(visual_state, VisualState::Disabled), + kind, }; - match (immediate, &self.active_style) { + if context.is_default() { + kind.colors_for_default(visual_state, context) + } else { + match kind { + ButtonKind::Transparent => Self::colors_for_transparent(visual_state, context), + ButtonKind::Solid => visual_state.solid_colors(context), + ButtonKind::Outline => visual_state.outline_colors(context), + } + } + } + + fn update_colors(&mut self, context: &WidgetContext<'_, '_>, immediate: bool) { + let new_style = self.determine_stateful_colors(context); + + match (immediate, &self.active_colors) { (false, Some(style)) => { self.color_animation = (style.transition_to(new_style)) .over(Duration::from_millis(150)) @@ -126,43 +255,139 @@ impl Button { _ => { let new_style = Dynamic::new(new_style); let foreground = new_style.map_each(|s| s.foreground); - self.active_style = Some(new_style); + self.active_colors = Some(new_style); context.attach_styles(Styles::new().with(&TextColor, foreground)); } } } - fn current_style(&mut self, context: &WidgetContext<'_, '_>) -> ButtonStyle { - if self.active_style.is_none() { + fn current_style(&mut self, context: &WidgetContext<'_, '_>) -> ButtonColors { + if self.active_colors.is_none() { self.update_colors(context, false); } - let style = self.active_style.as_ref().expect("always initialized"); + let style = self.active_colors.as_ref().expect("always initialized"); context.redraw_when_changed(style); style.get() } } +/// The effective visual state of an element. +/// +/// While an element may be multiple states (e.g., active and hovered), when +/// rendering a widget sometimes a single visual style must take priority. This +/// enum represents the various states a widget may be in for such a decision. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum VisualState { + /// The widget should render in its normal state. + Normal, + /// The widget should render in reaction to the mouse cursor being above the + /// widget. + Hovered, + /// The widget should render in reaction to the widget being clicked on or + /// activated by the user. + Active, + /// The widget should render in a way to convey to the user it is not + /// enabled for interaction. + Disabled, +} + +impl VisualState { + /// Returns the colors to apply to a [`ButtonKind::Solid`] [`Button`] or + /// button-like widget. + #[must_use] + pub fn solid_colors(self, context: &WidgetContext<'_, '_>) -> ButtonColors { + match self { + VisualState::Normal => ButtonColors { + background: context.get(&ButtonBackground), + foreground: context.get(&ButtonForeground), + outline: context.get(&ButtonOutline), + }, + VisualState::Hovered => ButtonColors { + background: context.get(&ButtonHoverBackground), + foreground: context.get(&ButtonHoverForeground), + outline: context.get(&ButtonHoverOutline), + }, + VisualState::Active => ButtonColors { + background: context.get(&ButtonActiveBackground), + foreground: context.get(&ButtonActiveForeground), + outline: context.get(&ButtonActiveOutline), + }, + VisualState::Disabled => ButtonColors { + background: context.get(&ButtonDisabledBackground), + foreground: context.get(&ButtonDisabledForeground), + outline: context.get(&ButtonDisabledOutline), + }, + } + } + + /// Returns the colors to apply to a [`ButtonKind::Outline`] [`Button`] or + /// button-like widget. + #[must_use] + pub fn outline_colors(self, context: &WidgetContext<'_, '_>) -> ButtonColors { + let solid = self.solid_colors(context); + ButtonColors { + background: solid.outline, + foreground: solid.foreground, + outline: solid.background, + } + } +} + impl Widget for Button { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { #![allow(clippy::similar_names)] - let enabled = self.enabled.get(); + let enabled = self.enabled.get_tracked(context); + // TODO This seems ugly. It needs context, so it can't be moved into the // dynamic system. - if self.currently_enabled != enabled { + let current_style = self.kind.get_tracked(context); + if self.cached_state.enabled != enabled || self.cached_state.kind != current_style { self.update_colors(context, false); - self.currently_enabled = enabled; } - self.enabled.redraw_when_changed(context); - let style = self.current_style(context); context.gfx.fill(style.background); - context.stroke_outline::(style.outline, StrokeOptions::default()); + let two_lp_stroke = StrokeOptions::lp_wide(Lp::points(2)); + context.stroke_outline(style.outline, two_lp_stroke); if context.focused() { - context.draw_focus_ring(); + if current_style == ButtonKind::Transparent { + let two_lp_stroke = two_lp_stroke.into_px(context.gfx.scale()); + let focus_color = context.get(&HighlightColor); + // Some states of a transparent button have solid background + // colors. most_contrasting from a 0-alpha color is not a + // meaningful measurement, so we only start measuring contrast + // once we reach 50% opacity. If we ever add solid background + // tracking (), + // we should use that color for most_contrasting always. + let color = if style.background.alpha() > 128 { + style + .background + .most_contrasting(&[focus_color, context.get(&TextColor)]) + } else { + focus_color + }; + + let inset = context.get(&IntrinsicPadding).into_px(context.gfx.scale()); + + let focus_ring = Shape::stroked_rect( + Rect::new( + Point::new(inset, inset), + context.gfx.region().size - inset * 2, + ), + color, + two_lp_stroke, + ); + context + .gfx + .draw_shape(&focus_ring, Point::default(), None, None); + } else if context.is_default() { + context.stroke_outline(context.get(&OutlineColor), two_lp_stroke); + } else { + context.draw_focus_ring(); + } } let content = self.content.mounted(&mut context.as_event_context()); @@ -237,10 +462,7 @@ impl Widget for Button { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - let padding = context - .get(&IntrinsicPadding) - .into_px(context.gfx.scale()) - .into_unsigned(); + let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale()); let mounted = self.content.mounted(&mut context.as_event_context()); let size = context.for_other(&mounted).layout(available_space); context.set_child_layout( @@ -317,7 +539,7 @@ define_components! { ButtonActiveBackground(Color, "active_background_color", .surface.color) /// The background color of the button when the mouse cursor is hovering over /// it. - ButtonHoverBackground(Color, "hover_background_color", .surface.bright_color) + ButtonHoverBackground(Color, "hover_background_color", .surface.lowest_container) /// The background color of the button when the mouse cursor is hovering over /// it. ButtonDisabledBackground(Color, "disabled_background_color", .surface.dim_color) @@ -334,12 +556,12 @@ define_components! { /// The outline color of the button. ButtonOutline(Color, "outline_color", Color::CLEAR_BLACK) /// The outline color of the button when it is active (depressed). - ButtonActiveOutline(Color, "active_outline_color", contrasting!(ButtonActiveBackground, ButtonOutline, TextColor, SurfaceColor)) + ButtonActiveOutline(Color, "active_outline_color", Color::CLEAR_BLACK) /// The outline color of the button when the mouse cursor is hovering over /// it. - ButtonHoverOutline(Color, "hover_outline_color", contrasting!(ButtonHoverBackground, ButtonOutline, TextColor, SurfaceColor)) + ButtonHoverOutline(Color, "hover_outline_color", Color::CLEAR_BLACK) /// The outline color of the button when the mouse cursor is hovering over /// it. - ButtonDisabledOutline(Color, "disabled_outline_color", contrasting!(ButtonDisabledBackground, ButtonOutline, TextColor, SurfaceColor)) + ButtonDisabledOutline(Color, "disabled_outline_color", Color::CLEAR_BLACK) } } diff --git a/src/widgets/checkbox.rs b/src/widgets/checkbox.rs new file mode 100644 index 0000000..ffa9144 --- /dev/null +++ b/src/widgets/checkbox.rs @@ -0,0 +1,233 @@ +//! A tri-state, labelable checkbox widget. +use std::error::Error; +use std::fmt::Display; +use std::ops::Not; + +use kludgine::figures::units::{Lp, Px}; +use kludgine::figures::{IntoUnsigned, Point, Rect, ScreenScale, Size}; +use kludgine::shapes::{PathBuilder, Shape, StrokeOptions}; + +use crate::context::{GraphicsContext, LayoutContext}; +use crate::styles::components::{ + IntrinsicPadding, LineHeight, OutlineColor, TextColor, WidgetAccentColor, +}; +use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; +use crate::widget::{MakeWidget, WidgetInstance, WidgetRef, WrappedLayout, WrapperWidget}; +use crate::widgets::button::ButtonKind; +use crate::ConstraintLimit; + +/// A labeled-widget that supports three states: Checked, Unchecked, and +/// Indeterminant +pub struct Checkbox { + /// The state (value) of the checkbox. + pub state: Dynamic, + /// The button kind to use as the basis for this checkbox. Checkboxes + /// default to [`ButtonKind::Transparent`]. + pub kind: Value, + label: WidgetInstance, +} + +impl Checkbox { + /// Returns a new checkbox that updates `state` when clicked. `label` is + /// drawn next to the checkbox and is also clickable to toggle the checkbox. + /// + /// `state` can also be a `Dynamic` if there is no need to represent + /// an indeterminant state. + pub fn new(state: impl IntoDynamic, label: impl MakeWidget) -> Self { + Self { + state: state.into_dynamic(), + kind: Value::Constant(ButtonKind::Transparent), + label: label.make_widget(), + } + } + + /// Updates the button kind to use as the basis for this checkbox, and + /// returns self. + /// + /// Checkboxes default to [`ButtonKind::Transparent`]. + #[must_use] + pub fn kind(mut self, kind: impl IntoValue) -> Self { + self.kind = kind.into_value(); + self + } +} + +impl MakeWidget for Checkbox { + fn make_widget(self) -> WidgetInstance { + CheckboxLabel { + value: self.state.create_reader(), + label: WidgetRef::new(self.label), + } + .into_button() + .on_click(move |()| { + let mut value = self.state.lock(); + *value = !*value; + }) + .kind(self.kind) + .make_widget() + } +} + +/// The state/value of a [`Checkbox`]. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum CheckboxState { + /// The checkbox should display showing that it is neither checked or + /// unchecked. + /// + /// This state is used to represent concepts such as: + /// + /// - States that are neither true/false, or on/off. + /// - States that are partially true or partially on. + Indeterminant, + /// The checkbox should display in an unchecked/off/false state. + Unchecked, + /// The checkbox should display in an checked/on/true state. + Checked, +} + +impl From for CheckboxState { + fn from(value: bool) -> Self { + if value { + Self::Checked + } else { + Self::Unchecked + } + } +} + +impl TryFrom for bool { + type Error = CheckboxToBoolError; + + fn try_from(value: CheckboxState) -> Result { + match value { + CheckboxState::Checked => Ok(true), + CheckboxState::Unchecked => Ok(false), + CheckboxState::Indeterminant => Err(CheckboxToBoolError), + } + } +} + +impl Not for CheckboxState { + type Output = Self; + + fn not(self) -> Self::Output { + match self { + Self::Indeterminant | Self::Unchecked => Self::Checked, + Self::Checked => Self::Unchecked, + } + } +} + +impl IntoDynamic for Dynamic { + fn into_dynamic(self) -> Dynamic { + self.linked( + |bool| CheckboxState::from(*bool), + |tri_state: &CheckboxState| bool::try_from(*tri_state).ok(), + ) + } +} + +/// An [`CheckboxState::Indeterminant`] was encountered when converting to a +/// `bool`. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct CheckboxToBoolError; + +impl Display for CheckboxToBoolError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("CheckboxState was Indeterminant") + } +} + +impl Error for CheckboxToBoolError {} + +#[derive(Debug)] +struct CheckboxLabel { + value: DynamicReader, + label: WidgetRef, +} + +impl WrapperWidget for CheckboxLabel { + fn child_mut(&mut self) -> &mut WidgetRef { + &mut self.label + } + + fn position_child( + &mut self, + size: Size, + _available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> WrappedLayout { + let checkbox_size = context.get(&LineHeight).into_px(context.gfx.scale()); // TODO create a component? + let padding = context.get(&IntrinsicPadding).into_px(context.gfx.scale()); + let label_inset = checkbox_size + padding; + let size_with_checkbox = Size::new(size.width + label_inset, size.height).into_unsigned(); + WrappedLayout { + child: Rect::new(Point::new(label_inset, Px(0)), size), + size: size_with_checkbox, + } + } + + fn redraw_background(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + let checkbox_size = context.get(&LineHeight).into_px(context.gfx.scale()); + let padding = context.get(&IntrinsicPadding).into_px(context.gfx.scale()); + let checkbox_rect = Rect::new( + Point::new(padding, padding), + Size::new(checkbox_size, checkbox_size), + ); + let stroke_options = StrokeOptions::lp_wide(Lp::points(2)).into_px(context.gfx.scale()); + match self.value.get_tracking_refresh(context) { + state @ (CheckboxState::Checked | CheckboxState::Indeterminant) => { + let color = context.get(&WidgetAccentColor); + context.gfx.draw_shape( + &Shape::filled_rect(checkbox_rect, color), + Point::default(), + None, + None, + ); + let icon_area = checkbox_rect.inset(Lp::points(3).into_px(context.gfx.scale())); + let text_color = context.get(&TextColor); + let center = icon_area.origin + icon_area.size / 2; + if matches!(state, CheckboxState::Checked) { + context.gfx.draw_shape( + &PathBuilder::new(Point::new(icon_area.origin.x, center.y)) + .line_to(Point::new( + icon_area.origin.x + icon_area.size.width / 4, + icon_area.origin.y + icon_area.size.height * 3 / 4, + )) + .line_to(Point::new( + icon_area.origin.x + icon_area.size.width, + icon_area.origin.y, + )) + .build() + .stroke(text_color, stroke_options), + Point::default(), + None, + None, + ); + } else { + context.gfx.draw_shape( + &PathBuilder::new(Point::new(icon_area.origin.x, center.y)) + .line_to(Point::new( + icon_area.origin.x + icon_area.size.width, + center.y, + )) + .build() + .stroke(text_color, stroke_options), + Point::default(), + None, + None, + ); + } + } + CheckboxState::Unchecked => { + let color = context.get(&OutlineColor); + context.gfx.draw_shape( + &Shape::stroked_rect(checkbox_rect, color, stroke_options), + Point::default(), + None, + None, + ); + } + } + } +} diff --git a/src/widgets/container.rs b/src/widgets/container.rs index 3b22687..7b28c4d 100644 --- a/src/widgets/container.rs +++ b/src/widgets/container.rs @@ -193,11 +193,7 @@ impl WrapperWidget for Container { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - let padding_amount = self - .padding(context) - .size() - .into_px(context.gfx.scale()) - .into_unsigned(); + let padding_amount = self.padding(context).size().into_upx(context.gfx.scale()); Size::new( available_space.width - padding_amount.width, available_space.height - padding_amount.height, diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index 050ac2c..b654f52 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -1,4 +1,3 @@ -use kludgine::figures::units::UPx; use kludgine::figures::{IntoSigned, Size}; use crate::context::{AsEventContext, LayoutContext}; @@ -133,6 +132,6 @@ impl WrapperWidget for Expand { ), }; - Size::::new(width, height).into_signed().into() + Size::new(width, height).into_signed().into() } } diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 4b74396..c294600 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -351,10 +351,7 @@ impl Widget for Input { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - let padding = context - .get(&IntrinsicPadding) - .into_px(context.gfx.scale()) - .into_unsigned(); + let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale()); if self.needs_to_select_all { self.needs_to_select_all = false; self.select_all(); diff --git a/src/widgets/label.rs b/src/widgets/label.rs index 0a573f0..a488902 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -1,7 +1,7 @@ //! A read-only text widget. use kludgine::figures::units::{Px, UPx}; -use kludgine::figures::{IntoUnsigned, Point, ScreenScale, Size}; +use kludgine::figures::{Point, ScreenScale, Size}; use kludgine::text::{MeasuredText, Text, TextOrigin}; use kludgine::Color; @@ -79,10 +79,7 @@ impl Widget for Label { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - let padding = context - .get(&IntrinsicPadding) - .into_px(context.gfx.scale()) - .into_unsigned(); + let padding = context.get(&IntrinsicPadding).into_upx(context.gfx.scale()); let color = context.get(&TextColor); let width = available_space.width.max().try_into().unwrap_or(Px::MAX); let prepared = self.prepared_text(context, color, width); diff --git a/src/widgets/resize.rs b/src/widgets/resize.rs index 241655a..eb51cfc 100644 --- a/src/widgets/resize.rs +++ b/src/widgets/resize.rs @@ -1,5 +1,4 @@ -use kludgine::figures::units::UPx; -use kludgine::figures::{Fraction, IntoSigned, IntoUnsigned, ScreenScale, Size}; +use kludgine::figures::{Fraction, IntoSigned, ScreenScale, Size}; use crate::context::{AsEventContext, LayoutContext}; use crate::styles::DimensionRange; @@ -48,9 +47,12 @@ impl Resize { /// Resizes `self` to `width`. /// - /// `width` can be an individual - /// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a - /// range. + /// `width` can be an any of: + /// + /// - [`Dimension`](crate::styles::Dimension) + /// - [`Px`](crate::kludgine::figures::units::Px) + /// - [`Lp`](crate::kludgine::figures::units::Lp) + /// - A range of any fo the above. #[must_use] pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); @@ -59,9 +61,12 @@ impl Resize { /// Resizes `self` to `height`. /// - /// `width` can be an individual - /// [`Dimension`]/[`Px`]/[`Lp`](crate::kludgine::figures::units::Lp) or a - /// range. + /// `height` can be an any of: + /// + /// - [`Dimension`](crate::styles::Dimension) + /// - [`Px`](crate::kludgine::figures::units::Px) + /// - [`Lp`](crate::kludgine::figures::units::Lp) + /// - A range of any fo the above. #[must_use] pub fn height(mut self, height: impl Into) -> Self { self.height = height.into(); @@ -94,8 +99,8 @@ impl WrapperWidget for Resize { (self.width.exact_dimension(), self.height.exact_dimension()) { Size::new( - width.into_px(context.gfx.scale()).into_unsigned(), - height.into_px(context.gfx.scale()).into_unsigned(), + width.into_upx(context.gfx.scale()), + height.into_upx(context.gfx.scale()), ) } else { let available_space = Size::new( @@ -104,7 +109,7 @@ impl WrapperWidget for Resize { ); context.for_other(&child).layout(available_space) }; - Size::::new( + Size::new( self.width.clamp(size.width, context.gfx.scale()), self.height.clamp(size.height, context.gfx.scale()), ) @@ -121,9 +126,7 @@ fn override_constraint( match constraint { ConstraintLimit::Known(size) => ConstraintLimit::Known(range.clamp(size, scale)), ConstraintLimit::ClippedAfter(clipped_after) => match (range.minimum(), range.maximum()) { - (Some(min), Some(max)) if min == max => { - ConstraintLimit::Known(min.into_px(scale).into_unsigned()) - } + (Some(min), Some(max)) if min == max => ConstraintLimit::Known(min.into_upx(scale)), _ => ConstraintLimit::ClippedAfter(range.clamp(clipped_after, scale)), }, } diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index cd733ff..91f848e 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -172,8 +172,7 @@ impl Widget for Scroll { let (mut scroll, current_max_scroll) = self.constrain_scroll(); let control_size = - Size::::new(available_space.width.max(), available_space.height.max()) - .into_signed(); + Size::new(available_space.width.max(), available_space.height.max()).into_signed(); let max_extents = Size::new( if self.enabled.x { ConstraintLimit::ClippedAfter((control_size.width).into_unsigned()) diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index 7fabf9c..a1fc92d 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -5,15 +5,15 @@ use std::panic::UnwindSafe; use kludgine::app::winit::event::{DeviceId, MouseButton}; use kludgine::figures::units::{Lp, Px, UPx}; use kludgine::figures::{ - FloatConversion, FromComponents, IntoComponents, IntoSigned, IntoUnsigned, Point, Ranged, Rect, - ScreenScale, Size, + FloatConversion, FromComponents, IntoComponents, IntoSigned, Point, Ranged, Rect, ScreenScale, + Size, }; use kludgine::shapes::Shape; use kludgine::{Color, Origin}; use crate::animation::{LinearInterpolate, PercentBetween}; use crate::context::{EventContext, GraphicsContext, LayoutContext}; -use crate::styles::components::OpaqueWidgetColor; +use crate::styles::components::{OpaqueWidgetColor, WidgetAccentColor}; use crate::styles::Dimension; use crate::value::{Dynamic, IntoDynamic, IntoValue, Value}; use crate::widget::{EventHandling, Widget, HANDLED}; @@ -223,14 +223,10 @@ where available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - self.knob_size = context - .get(&KnobSize) - .into_px(context.gfx.scale()) - .into_unsigned(); + self.knob_size = context.get(&KnobSize).into_upx(context.gfx.scale()); let minimum_size = context .get(&MinimumSliderSize) - .into_px(context.gfx.scale()) - .into_unsigned(); + .into_upx(context.gfx.scale()); match (available_space.width, available_space.height) { (ConstraintLimit::Known(width), ConstraintLimit::Known(height)) => { @@ -321,7 +317,7 @@ define_components! { /// The minimum length of the slidable dimension. MinimumSliderSize(Dimension, "minimum_size", |context| context.get(&KnobSize) * 2) /// The color of the draggable portion of the knob. - KnobColor(Color, "knob_color", .primary.color) // TODO make this pull from a component multiple widgets can share + KnobColor(Color, "knob_color", @WidgetAccentColor) /// The color of the track that the knob rests on. TrackColor(Color,"track_color", |context| context.get(&KnobColor)) /// The color of the track that the knob rests on. diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index 92feb66..2d15673 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -377,7 +377,7 @@ impl Layout { Dimension::Px(size) => self.allocated_space.0 += size.into_unsigned(), Dimension::Lp(size) => self.allocated_space.1 += size, } - min.into_px(scale).into_unsigned() + min.into_upx(scale) } }; self.layouts.insert( @@ -397,8 +397,7 @@ impl Layout { ) -> Size { let (space_constraint, other_constraint) = self.orientation.split_size(available); let available_space = space_constraint.max(); - let allocated_space = - self.allocated_space.0 + self.allocated_space.1.into_px(scale).into_unsigned(); + let allocated_space = self.allocated_space.0 + self.allocated_space.1.into_upx(scale); let mut remaining = available_space.saturating_sub(allocated_space); // If our `other_constraint` is not known, we will need to give child // widgets an opportunity to lay themselves out in the full area. This @@ -450,9 +449,7 @@ impl Layout { let (_, measured) = self.orientation.split_size(measure( index, self.orientation.make_size( - ConstraintLimit::Known( - self.layouts[index].size.into_px(scale).into_unsigned(), - ), + ConstraintLimit::Known(self.layouts[index].size.into_upx(scale)), other_constraint, ), !needs_final_layout, @@ -475,9 +472,7 @@ impl Layout { self.orientation.split_size(measure( index, self.orientation.make_size( - ConstraintLimit::Known( - self.layouts[index].size.into_px(scale).into_unsigned(), - ), + ConstraintLimit::Known(self.layouts[index].size.into_upx(scale)), ConstraintLimit::Known(self.other), ), true, diff --git a/src/window.rs b/src/window.rs index a0ab113..531e2c0 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,5 +1,4 @@ -//! Types for displaying a [`Widget`](crate::widget::Widget) inside of a desktop -//! window. +//! Types for displaying a [`Widget`] inside of a desktop window. use std::cell::RefCell; use std::ffi::OsStr; @@ -382,14 +381,14 @@ where .map_or(Px::MAX, |height| height.into_px(graphics.scale())); let new_min_size = (min_width > 0 || min_height > 0) - .then_some(Size::::new(min_width, min_height).into_unsigned()); + .then_some(Size::new(min_width, min_height).into_unsigned()); if new_min_size != self.min_inner_size && resizable { window.set_min_inner_size(new_min_size); self.min_inner_size = new_min_size; } let new_max_size = (max_width > 0 || max_height > 0) - .then_some(Size::::new(max_width, max_height).into_unsigned()); + .then_some(Size::new(max_width, max_height).into_unsigned()); if new_max_size != self.max_inner_size && resizable { window.set_max_inner_size(new_max_size);