From ee3813f44df7d006ebd93dd5f32a63128cfd00cf Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Mon, 13 Nov 2023 09:14:38 -0800 Subject: [PATCH] Switcher, h/v expand --- Cargo.lock | 22 +++------- examples/switcher.rs | 46 ++++++++++++++++++++ examples/theme.rs | 36 ++++++++++++---- src/animation.rs | 46 +++++++++++++++++++- src/styles/components.rs | 2 +- src/tree.rs | 1 + src/widget.rs | 24 +++++++++-- src/widgets.rs | 2 + src/widgets/expand.rs | 77 +++++++++++++++++++++++++++------- src/widgets/stack.rs | 75 +++++++++++++++++++-------------- src/widgets/switcher.rs | 91 ++++++++++++++++++++++++++++++++++++++++ src/window.rs | 29 +++++++++---- 12 files changed, 364 insertions(+), 87 deletions(-) create mode 100644 examples/switcher.rs create mode 100644 src/widgets/switcher.rs diff --git a/Cargo.lock b/Cargo.lock index 536883c..6d73255 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -281,11 +281,10 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.83" +version = "1.0.84" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "0f8e7c90afad890484a21653d08b6e209ae34770fb5ee298f9c699fcc1e5c856" dependencies = [ - "jobserver", "libc", ] @@ -838,15 +837,6 @@ 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" @@ -1970,9 +1960,9 @@ dependencies = [ [[package]] name = "tracing-log" -version = "0.1.4" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f751112709b4e791d8ce53e32c4ed2d353565a795ce84da2285393f41557bdf2" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", @@ -1981,9 +1971,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.17" +version = "0.3.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" dependencies = [ "matchers", "nu-ansi-term", diff --git a/examples/switcher.rs b/examples/switcher.rs new file mode 100644 index 0000000..ea43ea6 --- /dev/null +++ b/examples/switcher.rs @@ -0,0 +1,46 @@ +use gooey::value::Dynamic; +use gooey::widget::{MakeWidget, WidgetInstance}; +use gooey::widgets::{Button, Label, Switcher}; +use gooey::Run; + +#[derive(Debug)] +enum ActiveContent { + Intro, + Success, +} + +fn main() -> gooey::Result { + let active = Dynamic::new(ActiveContent::Intro); + + Switcher::new(active.clone(), move |content| match content { + ActiveContent::Intro => intro(active.clone()), + ActiveContent::Success => success(active.clone()), + }) + .contain() + .centered() + .expand() + .run() +} + +fn intro(active: Dynamic) -> WidgetInstance { + const INTRO: &str = "This example demonstrates the Switcher widget, which uses a mapping function to convert from a generic type to the widget it uses for its contents."; + Label::new(INTRO) + .and( + Button::new("Switch!") + .on_click(move |_| active.set(ActiveContent::Success)) + .centered(), + ) + .into_rows() + .make_widget() +} + +fn success(active: Dynamic) -> WidgetInstance { + Label::new("The value changed to `ActiveContent::Success`!") + .and( + Button::new("Start Over") + .on_click(move |_| active.set(ActiveContent::Intro)) + // .centered(), + ) + .into_rows() + .make_widget() +} diff --git a/examples/theme.rs b/examples/theme.rs index c03ca97..599bedb 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -5,10 +5,9 @@ use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::{ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair}; use gooey::value::{Dynamic, MapEach}; use gooey::widget::MakeWidget; -use gooey::widgets::{Input, Label, Scroll, Slider, Stack, Themed}; +use gooey::widgets::{Input, Label, ModeSwitch, Scroll, Slider, Stack, Themed}; use gooey::window::ThemeMode; use gooey::Run; -use kludgine::figures::units::Lp; use kludgine::Color; const PRIMARY_HUE: f32 = 240.; @@ -59,16 +58,22 @@ fn main() -> gooey::Result { .and(neutral_editor) .and(neutral_variant_editor), )) - .and(theme(default_theme.map_each(|theme| theme.dark), "Dark")) - .and(theme(default_theme.map_each(|theme| theme.light), "Light")) .and(fixed_themes( default_theme.map_each(|theme| theme.primary_fixed), default_theme.map_each(|theme| theme.secondary_fixed), default_theme.map_each(|theme| theme.tertiary_fixed), + )) + .and(theme( + default_theme.map_each(|theme| theme.dark), + ThemeMode::Dark, + )) + .and(theme( + default_theme.map_each(|theme| theme.light), + ThemeMode::Light, )), ), ) - .pad_by(Lp::points(16)) + .pad() .expand() .into_window() .with_theme_mode(theme_mode) @@ -132,6 +137,7 @@ fn fixed_themes( .and(fixed_theme(secondary, "Secondary")) .and(fixed_theme(tertiary, "Tertiary")), ) + .contain() .expand() } @@ -156,12 +162,18 @@ fn fixed_theme(theme: Dynamic, label: &str) -> impl MakeWidget { color, )), ) + .contain() .expand() } -fn theme(theme: Dynamic, label: &str) -> impl MakeWidget { - Stack::rows( - Label::new(label) +fn theme(theme: Dynamic, mode: ThemeMode) -> impl MakeWidget { + ModeSwitch::new( + mode, + Stack::rows( + Label::new(match mode { + ThemeMode::Light => "Light", + ThemeMode::Dark => "Dark", + }) .and( Stack::columns( color_theme(theme.map_each(|theme| theme.primary), "Primary") @@ -175,9 +187,12 @@ fn theme(theme: Dynamic, label: &str) -> impl MakeWidget { )) .and(color_theme(theme.map_each(|theme| theme.error), "Error")), ) + .contain() .expand(), ) .and(surface_theme(theme.map_each(|theme| theme.surface))), + ) + .contain(), ) .expand() } @@ -199,6 +214,7 @@ fn surface_theme(theme: Dynamic) -> impl MakeWidget { on_color.clone(), )), ) + .contain() .expand() .and( Stack::columns( @@ -228,6 +244,7 @@ fn surface_theme(theme: Dynamic) -> impl MakeWidget { on_color.clone(), )), ) + .contain() .expand(), ) .and( @@ -254,9 +271,11 @@ fn surface_theme(theme: Dynamic) -> impl MakeWidget { on_color, )), ) + .contain() .expand(), ), ) + .contain() .expand() } @@ -289,6 +308,7 @@ fn color_theme(theme: Dynamic, label: &str) -> impl MakeWidget { container, )), ) + .contain() .expand() } diff --git a/src/animation.rs b/src/animation.rs index 14d680e..beb529c 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -22,14 +22,14 @@ //! use gooey::value::Dynamic; //! //! let value = Dynamic::new(0); -//! +//! let mut reader = value.create_reader(); //! value //! .transition_to(100) //! .over(Duration::from_millis(100)) //! .with_easing(EaseInOutElastic) //! .launch(); +//! drop(value); //! -//! let mut reader = value.into_reader(); //! while reader.block_until_updated() { //! println!("{}", reader.get()); //! } @@ -726,6 +726,48 @@ impl LinearInterpolate for Color { } } +/// A wrapper that implements [`LinearInterpolate`] such that the value switches +/// after 50%. +/// +/// This wrapper can be used to add [`LinearInterpolate`] to types that normally +/// don't support interpolation. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct BinaryLerp(T); + +impl LinearInterpolate for BinaryLerp +where + T: Clone + PartialEq, +{ + fn lerp(&self, target: &Self, percent: f32) -> Self { + if false.lerp(&true, percent) { + target.clone() + } else { + self.clone() + } + } +} + +/// A wrapper that implements [`LinearInterpolate`] such that the target value +/// is immediately returned as long as percent is > 0. +/// +/// This wrapper can be used to add [`LinearInterpolate`] to types that normally +/// don't support interpolation. +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct ImmediateLerp(T); + +impl LinearInterpolate for ImmediateLerp +where + T: Clone + PartialEq, +{ + fn lerp(&self, target: &Self, percent: f32) -> Self { + if percent > 0. { + target.clone() + } else { + self.clone() + } + } +} + /// Calculates the ratio of one value against a minimum and maximum. pub trait PercentBetween { /// Return the percentage that `self` is between `min` and `max`. diff --git a/src/styles/components.rs b/src/styles/components.rs index 02d15a2..2e95ef4 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -30,7 +30,7 @@ use crate::styles::{Dimension, FocusableWidgets, VisualOrder}; /// /// This component defaults to picking a contrasting color between `TextColor` and `SurfaceColor` /// ContrastingColor(Color, "contrasting_color", contrasting!(ThemedComponent, TextColor, SurfaceColor)) /// /// This component shows how to use a closure for nearly infinite flexibility in computing the default value. -/// ClosureDefaultComponent(Color, "closure_component", |context| context.query_style(&TextColor)) +/// ClosureDefaultComponent(Color, "closure_component", |context| context.get(&TextColor)) /// } /// } /// ``` diff --git a/src/tree.rs b/src/tree.rs index 83c7aeb..58c5e71 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -418,6 +418,7 @@ impl TreeData { while let Some(node) = detached_nodes.pop() { let mut node = self.nodes.remove(node).expect("detached node missing"); + self.nodes_by_id.remove(&node.widget.id()); detached_nodes.append(&mut node.children); } } diff --git a/src/widget.rs b/src/widget.rs index 0ead588..a72586f 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -43,6 +43,12 @@ pub trait Widget: Send + UnwindSafe + Debug + 'static { context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size; + /// Return true if this widget should expand to fill the window when it is + /// the root widget. + fn expand_if_at_root(&self) -> Option { + Some(false) + } + /// The widget has been mounted into a parent widget. #[allow(unused_variables)] fn mounted(&mut self, context: &mut EventContext<'_, '_>) {} @@ -227,9 +233,8 @@ pub trait WrapperWidget: Debug + Send + UnwindSafe + 'static { available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> WrappedLayout { - let child = self.child_mut().mounted(&mut context.as_event_context()); - let adjusted_space = self.adjust_child_constraint(available_space, context); + let child = self.child_mut().mounted(&mut context.as_event_context()); let size = context .for_other(&child) .layout(adjusted_space) @@ -420,9 +425,8 @@ where available_space: Size, context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { - let child = self.child_mut().mounted(&mut context.as_event_context()); - let layout = self.layout_child(available_space, context); + let child = self.child_mut().mounted(&mut context.as_event_context()); context.set_child_layout(&child, layout.child); layout.size } @@ -606,6 +610,18 @@ pub trait MakeWidget: Sized { Expand::weighted(weight, self) } + /// Expands `self` to grow to fill its parent horizontally. + #[must_use] + fn expand_horizontally(self) -> Expand { + Expand::horizontal(self) + } + + /// Expands `self` to grow to fill its parent vertically. + #[must_use] + fn expand_vertically(self) -> Expand { + Expand::horizontal(self) + } + /// Aligns `self` to the center vertically and horizontally. #[must_use] fn centered(self) -> Align { diff --git a/src/widgets.rs b/src/widgets.rs index 72e1789..d090af2 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -14,6 +14,7 @@ mod slider; mod space; pub mod stack; mod style; +mod switcher; mod themed; mod tilemap; @@ -31,5 +32,6 @@ pub use slider::Slider; pub use space::Space; pub use stack::Stack; pub use style::Style; +pub use switcher::Switcher; pub use themed::Themed; pub use tilemap::TileMap; diff --git a/src/widgets/expand.rs b/src/widgets/expand.rs index 4bb28a0..050ac2c 100644 --- a/src/widgets/expand.rs +++ b/src/widgets/expand.rs @@ -12,12 +12,17 @@ use crate::ConstraintLimit; /// [`Expand`]ed widget. #[derive(Debug)] pub struct Expand { - /// The weight to use when splitting available space with multiple - /// [`Expand`] widgets. - pub weight: u8, + kind: ExpandKind, child: WidgetRef, } +#[derive(Debug)] +enum ExpandKind { + Weighted(u8), + Horizontal, + Vertical, +} + impl Default for Expand { fn default() -> Self { Self::empty() @@ -30,7 +35,25 @@ impl Expand { pub fn new(child: impl MakeWidget) -> Self { Self { child: WidgetRef::new(child), - weight: 1, + kind: ExpandKind::Weighted(1), + } + } + + /// Returns a widget that expands `child` to fill the parent widget horizontally. + #[must_use] + pub fn horizontal(child: impl MakeWidget) -> Self { + Self { + child: WidgetRef::new(child), + kind: ExpandKind::Horizontal, + } + } + + /// Returns a widget that expands `child` to fill the parent widget vertically. + #[must_use] + pub fn vertical(child: impl MakeWidget) -> Self { + Self { + child: WidgetRef::new(child), + kind: ExpandKind::Vertical, } } @@ -39,7 +62,7 @@ impl Expand { pub fn empty() -> Self { Self { child: WidgetRef::new(Space::clear()), - weight: 1, + kind: ExpandKind::Weighted(1), } } @@ -51,7 +74,7 @@ impl Expand { pub fn weighted(weight: u8, child: impl MakeWidget) -> Self { Self { child: WidgetRef::new(child), - weight, + kind: ExpandKind::Weighted(weight), } } @@ -60,6 +83,14 @@ impl Expand { pub const fn child(&self) -> &WidgetRef { &self.child } + + #[must_use] + pub(crate) fn weight(&self) -> Option { + match self.kind { + ExpandKind::Weighted(weight) => Some(weight), + ExpandKind::Horizontal | ExpandKind::Vertical => None, + } + } } impl WrapperWidget for Expand { @@ -79,15 +110,29 @@ impl WrapperWidget for Expand { let child = self.child.mounted(&mut context.as_event_context()); let size = context.for_other(&child).layout(available_space); - Size::::new( - available_space - .width - .fit_measured(size.width, context.gfx.scale()), - available_space - .height - .fit_measured(size.height, context.gfx.scale()), - ) - .into_signed() - .into() + let (width, height) = match &self.kind { + ExpandKind::Weighted(_) => ( + available_space + .width + .fit_measured(size.width, context.gfx.scale()), + available_space + .height + .fit_measured(size.height, context.gfx.scale()), + ), + ExpandKind::Horizontal => ( + available_space + .width + .fit_measured(size.width, context.gfx.scale()), + size.height, + ), + ExpandKind::Vertical => ( + size.width, + available_space + .height + .fit_measured(size.height, context.gfx.scale()), + ), + }; + + Size::::new(width, height).into_signed().into() } } diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index d47a899..118cdb4 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -85,37 +85,38 @@ impl Stack { } else { // This is a brand new child. let guard = widget.lock(); - let (mut widget, dimension) = - if let Some(expand) = guard.downcast_ref::() { - let weight = expand.weight; - ( - expand.child().clone(), - StackDimension::Fractional { weight }, - ) - } else if let Some((child, size)) = - guard.downcast_ref::().and_then(|r| { - let range = match self.layout.orientation.orientation { - StackOrientation::Row => r.height, - StackOrientation::Column => r.width, - }; - range.minimum().map(|min| { - ( - r.child().clone(), - StackDimension::Measured { - min, - _max: range.end, - }, - ) - }) + let (mut widget, dimension) = if let Some((weight, expand)) = guard + .downcast_ref::() + .and_then(|expand| expand.weight().map(|weight| (weight, expand))) + { + ( + expand.child().clone(), + StackDimension::Fractional { weight }, + ) + } else if let Some((child, size)) = + guard.downcast_ref::().and_then(|r| { + let range = match self.layout.orientation.orientation { + StackOrientation::Row => r.height, + StackOrientation::Column => r.width, + }; + range.minimum().map(|min| { + ( + r.child().clone(), + StackDimension::Measured { + min, + _max: range.end, + }, + ) }) - { - (child, size) - } else { - ( - WidgetRef::Unmounted(widget.clone()), - StackDimension::FitContent, - ) - }; + }) + { + (child, size) + } else { + ( + WidgetRef::Unmounted(widget.clone()), + StackDimension::FitContent, + ) + }; drop(guard); self.synced_children.insert(index, widget.mounted(context)); @@ -449,7 +450,7 @@ impl Layout { ConstraintLimit::Known(self.layouts[index].size.into_px(scale).into_unsigned()), other_constraint, ), - true, + false, )); self.other = self.other.max(measured); } @@ -459,6 +460,18 @@ impl Layout { ConstraintLimit::ClippedAfter(clip_limit) => self.other.min(clip_limit), }; + // Finally layout the widgets with the final constraints + for index in 0..self.children.len() { + self.orientation.split_size(measure( + index, + self.orientation.make_size( + ConstraintLimit::Known(self.layouts[index].size.into_px(scale).into_unsigned()), + ConstraintLimit::Known(self.other), + ), + true, + )); + } + self.orientation.make_size(offset, self.other) } } diff --git a/src/widgets/switcher.rs b/src/widgets/switcher.rs new file mode 100644 index 0000000..fd9cd18 --- /dev/null +++ b/src/widgets/switcher.rs @@ -0,0 +1,91 @@ +use std::fmt::Debug; +use std::panic::UnwindSafe; + +use kludgine::figures::Size; + +use crate::context::{AsEventContext, LayoutContext}; +use crate::value::{Generation, IntoValue, Value}; +use crate::widget::{MakeWidget, WidgetInstance, WidgetRef, WrapperWidget}; +use crate::ConstraintLimit; + +/// A widget that switches its contents based on a value of `T`. +pub struct Switcher { + value: Value, + value_generation: Option, + factory: Box>, + child: WidgetRef, +} + +impl Switcher { + /// Returns a new widget that replaces its contents with the result of + /// `widget_factory` each time `value` changes. + #[must_use] + pub fn new(value: impl IntoValue, mut widget_factory: F) -> Self + where + F: for<'a> FnMut(&'a T) -> W + Send + UnwindSafe + 'static, + W: MakeWidget, + { + let value = value.into_value(); + let value_generation = value.generation(); + let child = WidgetRef::new(value.map(|value| widget_factory(value))); + Self { + value, + value_generation, + factory: Box::new(widget_factory), + child, + } + } +} + +impl Debug for Switcher +where + T: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Switcher") + .field("value", &self.value) + .field("child", &self.child) + .finish_non_exhaustive() + } +} + +impl WrapperWidget for Switcher +where + T: Debug + Send + UnwindSafe + 'static, +{ + fn child_mut(&mut self) -> &mut WidgetRef { + &mut self.child + } + + // TODO this should be moved to an invalidated() event once we have it. + fn adjust_child_constraint( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + let current_generation = self.value.generation(); + if self.value_generation != current_generation { + self.value_generation = current_generation; + let new_child = WidgetRef::new(self.value.map(|value| self.factory.invoke(value))); + let removed = std::mem::replace(&mut self.child, new_child); + if let WidgetRef::Mounted(removed) = removed { + context.remove_child(&removed); + } + } + available_space + } +} + +trait SwitchMap: UnwindSafe + Send { + fn invoke(&mut self, value: &T) -> WidgetInstance; +} + +impl SwitchMap for F +where + F: for<'a> FnMut(&'a T) -> W + Send + UnwindSafe, + W: MakeWidget, +{ + fn invoke(&mut self, value: &T) -> WidgetInstance { + self(value).make_widget() + } +} diff --git a/src/window.rs b/src/window.rs index 87ac3f9..f14c46e 100644 --- a/src/window.rs +++ b/src/window.rs @@ -36,7 +36,9 @@ use crate::styles::ThemePair; use crate::tree::Tree; use crate::utils::ModifiersExt; use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; -use crate::widget::{EventHandling, ManagedWidget, Widget, WidgetInstance, HANDLED, IGNORED}; +use crate::widget::{ + EventHandling, ManagedWidget, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED, +}; use crate::widgets::{Expand, Resize}; use crate::window::sealed::WindowCommand; use crate::{initialize_tracing, ConstraintLimit, Run}; @@ -841,6 +843,9 @@ where if let Some(state) = self.mouse_state.devices.get(&device_id) { // Mouse Drag for (button, handler) in state { + let Some(handler) = self.root.tree.widget(*handler) else { + continue; + }; let mut context = EventContext::new( WidgetContext::new( handler.clone(), @@ -878,7 +883,7 @@ where if widget_context.hit_test(relative) { widget_context.hover(relative); drop(widget_context); - self.mouse_state.widget = Some(widget); + self.mouse_state.widget = Some(widget.id()); break; } } @@ -934,9 +939,13 @@ where ) .clear_focus(); - if let (ElementState::Pressed, Some(location), Some(hovered)) = - (state, &self.mouse_state.location, &self.mouse_state.widget) - { + if let (ElementState::Pressed, Some(location), Some(hovered)) = ( + state, + &self.mouse_state.location, + self.mouse_state + .widget + .and_then(|id| self.root.tree.widget(id)), + ) { if let Some(handler) = recursively_handle_event( &mut EventContext::new( WidgetContext::new( @@ -958,7 +967,7 @@ where .devices .entry(device_id) .or_default() - .insert(button, handler); + .insert(button, handler.id()); } } } @@ -972,7 +981,9 @@ where if device_buttons.is_empty() { self.mouse_state.devices.remove(&device_id); } - + let Some(handler) = self.root.tree.widget(handler) else { + return; + }; let mut context = EventContext::new( WidgetContext::new( handler, @@ -1036,8 +1047,8 @@ fn recursively_handle_event( #[derive(Default)] struct MouseState { location: Option>, - widget: Option, - devices: AHashMap>, + widget: Option, + devices: AHashMap>, } pub(crate) mod sealed {