From 9e4e079bf537177b5dbac485e2718d61e64d8f26 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Fri, 29 Dec 2023 13:21:39 -0800 Subject: [PATCH] WindowLocal + Custom Observers This cascaded into a lot more work than expected. However, in general, if one clones a `WidgetInstance` and shares it between two windows, it should now work. Widget authors must ensure that when they cache information, they do so with either a `WidgetCacheKey` or use a `WindowLocal` if per-window state is desired. This is demonstrated in the debug-window example, where the counter of open windows is next to a clone of the same button from the main window that opens a new window. --- CHANGELOG.md | 24 +++++++++++ Cargo.lock | 10 ++--- examples/debug-window.rs | 41 +++++++++++------- src/context.rs | 21 +++------- src/debug.rs | 38 +++++++++++++---- src/tree.rs | 34 +++++++++++---- src/value.rs | 4 +- src/widget.rs | 89 +++++++++++++++++++++++++--------------- src/widgets/button.rs | 62 ++++++++++++++++++---------- src/widgets/disclose.rs | 2 +- src/widgets/grid.rs | 9 +++- src/widgets/label.rs | 23 +++++++---- src/widgets/layers.rs | 2 +- src/widgets/stack.rs | 5 +-- src/widgets/switcher.rs | 7 +--- src/window.rs | 71 ++++++++++++++++++++++++++++++-- 16 files changed, 309 insertions(+), 133 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a7887e..ee91470 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Breaking Changes + +- `WidgetRef` is now a `struct` instead of an enum. This refactor changes the + mounted state to be stored in a `WindowLocal`, ensuring `WidgetRef`s work + properly when used in a `WidgetInstance` shared between multiple windows. +- `WidgetRef::unmount_in` should be called when the widget is being unmounted to + clean up individual window state. + ### Fixed - The root widget is now included in the search for widgets to accept focus. - Widgets that have been laid out with a 0px width or height no longer have their `redraw` functions called nor can they receive focus. - `Grid` now synchronizes removal of widgets from `GridWidgets` correctly. +- `WidgetInstance`s can now be shared between windows. Any unpredictable + behaviors when doing this should be reported, as some widgets may still have + state that should be moved into a `WindowLocal` type. +- `Grid` no longer passes `ConstraintLimit::Fill` along to children when it + contains more than one element. Previously, if rows contained widgets that + filled the given space, this would cause the grid to calculate layouts + incorrectly. + +### Changed + +- `WidgetCacheKey` now includes the `KludgineId` of the context it was created + from. This ensures if a `WidgetInstance` moves or is shared between windows, + the cache is invalidated. ### Added @@ -43,6 +64,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 existence. - `Dynamic::readers()` returns the number of `DynamicReader`s for the dynamic in existence. +- `RunningWindow::kludgine_id()` returns a unique id for that window. +- `WindowLocal` is a `HashMap`-based type that stores data on a per-window + basis using `RunningWindow::kludgine_id()` as the key. [99]: https://github.com/khonsulabs/cushy/issues/99 diff --git a/Cargo.lock b/Cargo.lock index d269d2c..aea66b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,9 +35,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "ahash" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" +checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01" dependencies = [ "cfg-if", "getrandom", @@ -1155,9 +1155,9 @@ checksum = "d7e253b574775d0ebd7975c471fc18f72f0775a4d42b563b5fbc3c4068aa1075" [[package]] name = "kempt" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13a912e97bbe1ec7cbead29896008766dedc4790350c55aece45998fde067200" +checksum = "4a37a6bdb52eae6acb1efbab069af766555a6d77712380fd48876b83543d3071" [[package]] name = "khronos-egl" @@ -1179,7 +1179,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.7.0" -source = "git+https://github.com/khonsulabs/kludgine#a38466b021ce55c26c6d533191a87a22109016b2" +source = "git+https://github.com/khonsulabs/kludgine#0bda2a6dc273aa49338f23ea5190aefdf037d740" dependencies = [ "ahash", "alot", diff --git a/examples/debug-window.rs b/examples/debug-window.rs index db1bc3b..338fe4a 100644 --- a/examples/debug-window.rs +++ b/examples/debug-window.rs @@ -8,20 +8,32 @@ const INTRO: &str = "This example demonstrates the DebugContext, which allows ob fn main() -> cushy::Result { let app = PendingApp::default(); - let dbg = DebugContext::default(); - let window_count = Dynamic::new(0_usize); - let total_windows = Dynamic::new(0_usize); + let info = DebugContext::default(); - dbg.observe("Open Windows", &window_count); - dbg.observe("Total Windows", &total_windows); - dbg.clone().open(&app)?; + let window_count = Dynamic::new(0_usize); + let total_windows = info.dbg("Total Windows", Dynamic::new(0_usize)); + let open_window_button = "Open a Window" + .into_button() + .on_click({ + let app = app.as_app(); + let info = info.clone(); + let window_count = window_count.clone(); + let total_windows = total_windows.clone(); + move |()| open_a_window(&window_count, &total_windows, &info, &app) + }) + .make_widget(); + + info.observe("Open Windows", &window_count, |window_count| { + window_count + .map_each(ToString::to_string) + .and(open_window_button.clone()) + .into_columns() + }); + + info.clone().open(&app)?; INTRO - .and("Open a Window".into_button().on_click({ - let app = app.as_app(); - - move |()| open_a_window(&window_count, &total_windows, &dbg, &app) - })) + .and(open_window_button) .into_rows() .centered() .run_in(app) @@ -30,7 +42,7 @@ fn main() -> cushy::Result { fn open_a_window( window_count: &Dynamic, total_windows: &Dynamic, - dbg: &DebugContext, + info: &DebugContext, app: &dyn Application, ) { *window_count.lock() += 1; @@ -39,10 +51,9 @@ fn open_a_window( *total }); let window_title = format!("Window #{window_number}"); - let dbg = dbg.section(&window_title); + let dbg = info.section(&window_title); - let value = Dynamic::new(0_u8); - dbg.observe("Slider", &value); + let value = dbg.dbg("Slider", Dynamic::new(0_u8)); let window_count = window_count.clone(); let _ = format!("This is window {window_number}.") diff --git a/src/context.rs b/src/context.rs index 43c254d..5c01752 100644 --- a/src/context.rs +++ b/src/context.rs @@ -12,7 +12,7 @@ use kludgine::app::winit::event::{ }; use kludgine::app::winit::window::CursorIcon; use kludgine::shapes::{Shape, StrokeOptions}; -use kludgine::{Color, Kludgine}; +use kludgine::{Color, Kludgine, KludgineId}; use crate::animation::ZeroToOne; use crate::graphics::Graphics; @@ -24,9 +24,7 @@ use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair}; use crate::tree::Tree; use crate::utils::IgnorePoison; use crate::value::{IntoValue, Value}; -use crate::widget::{ - EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance, WidgetRef, -}; +use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance}; use crate::window::{CursorState, RunningWindow, ThemeMode}; use crate::ConstraintLimit; @@ -893,6 +891,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { tree, effective_styles: current_node.effective_styles(), cache: WidgetCacheKey { + kludgine_id: Some(window.kludgine_id()), theme_mode, enabled, }, @@ -941,6 +940,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> { WidgetContext { effective_styles, cache: WidgetCacheKey { + kludgine_id: self.cache.kludgine_id, theme_mode, enabled: current_node.enabled(&self.handle()), }, @@ -1333,17 +1333,6 @@ impl ManageWidget for WidgetInstance { } } -impl ManageWidget for WidgetRef { - type Managed = Option; - - fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed { - match self { - WidgetRef::Unmounted(instance) => context.tree.widget(instance.id()), - WidgetRef::Mounted(instance) => Some(instance.clone()), - } - } -} - impl ManageWidget for MountedWidget { type Managed = Self; @@ -1383,6 +1372,7 @@ impl MapManagedWidget for MountedWidget { /// keys are not equal, the widget should clear all caches. #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct WidgetCacheKey { + kludgine_id: Option, theme_mode: ThemeMode, enabled: bool, } @@ -1390,6 +1380,7 @@ pub struct WidgetCacheKey { impl Default for WidgetCacheKey { fn default() -> Self { Self { + kludgine_id: None, theme_mode: ThemeMode::default().inverse(), enabled: false, } diff --git a/src/debug.rs b/src/debug.rs index 2ce59ba..06eecf2 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -17,23 +17,43 @@ pub struct DebugContext { } impl DebugContext { - /// Observes `value` using `label` in this debug context. + /// Observes `value` by showing the `Debug` output. Returns `value`. /// /// When the final reference to `value` is dropped, this observation will /// automatically be removed. - pub fn observe(&self, label: impl Into, value: &Dynamic) + /// + /// This function is designed to work similarly to the [`dbg!`] macro. + pub fn dbg(&self, label: impl Into, value: Dynamic) -> Dynamic where - T: PartialEq + Clone + Debug + Send + Sync + 'static, + T: Clone + Debug + Send + Sync + 'static, + { + self.observe(label, &value, |value| { + value.map_each(|value| format!("{value:?}")).make_widget() + }); + value + } + + /// Observes `value` by attaching the widget created by `make_observer` to + /// this context. + /// + /// When the final reference to `value` is dropped, this observation will + /// automatically be removed. + pub fn observe( + &self, + label: impl Into, + value: &Dynamic, + make_observer: MakeObserver, + ) where + T: Clone + Send + Sync + 'static, + MakeObserver: FnOnce(Dynamic) -> Widget, + Widget: MakeWidget, { let reader = value.create_reader(); let id = self.section.map_ref(|section| { section.values.lock().push(Box::new(RegisteredValue { label: label.into(), value: reader.clone(), - widget: value - .weak_clone() - .map_each(|value| format!("{value:?}")) - .make_widget(), + widget: make_observer(value.weak_clone()).make_widget(), })) }); let this = self.clone(); @@ -79,7 +99,7 @@ impl DebugContext { fn into_window(self) -> Window { self.section .map_ref(|section| section.widget.clone()) - .vertical_scroll() + // .vertical_scroll() .into_window() .titled("Cushy Debugger") } @@ -173,7 +193,7 @@ impl DebugSection { let value_grid = Grid::from_rows(values.map_each(|values| { values .iter() - .map(|o| [o.label().make_widget(), o.widget().clone()]) + .map(|o| (o.label(), o.widget().clone().align_left())) .collect::>() })); diff --git a/src/tree.rs b/src/tree.rs index 425c660..2b119c9 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -83,15 +83,17 @@ impl Tree { let node = &mut data.nodes[widget]; node.layout = Some(rect); - let mut children_to_offset = node.children.clone(); - while let Some(child) = children_to_offset.pop() { - if let Some(layout) = data - .nodes - .get_mut(child) - .and_then(|child| child.layout.as_mut()) - { - layout.origin += rect.origin; - children_to_offset.extend(data.nodes[child].children.iter().copied()); + if !node.children.is_empty() { + let mut children_to_offset = node.children.clone(); + while let Some(child) = children_to_offset.pop() { + if let Some(layout) = data + .nodes + .get_mut(child) + .and_then(|child| child.layout.as_mut()) + { + layout.origin += rect.origin; + children_to_offset.extend(data.nodes[child].children.iter().copied()); + } } } } @@ -399,6 +401,20 @@ impl Tree { } } +impl Eq for Tree {} + +impl PartialEq for Tree { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.data, &other.data) + } +} + +impl PartialEq for Tree { + fn eq(&self, other: &WeakTree) -> bool { + Arc::as_ptr(&self.data) == Weak::as_ptr(&other.0) + } +} + pub(crate) struct HoverResults { pub unhovered: Vec, pub hovered: Vec, diff --git a/src/value.rs b/src/value.rs index b902d64..602187a 100644 --- a/src/value.rs +++ b/src/value.rs @@ -268,7 +268,7 @@ impl Dynamic { #[must_use] pub fn weak_clone(&self) -> Self where - T: Clone + PartialEq + Send + 'static, + T: Clone + Send + 'static, { let weak_source = self.downgrade(); let weak_out = Dynamic::new(self.get()); @@ -276,7 +276,7 @@ impl Dynamic { let weak_out = weak_out.clone(); move || { if let Some(source) = weak_source.upgrade() { - weak_out.set(source.get()); + *weak_out.lock() = source.get(); } } })); diff --git a/src/widget.rs b/src/widget.rs index c2296b1..9bf7589 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -19,7 +19,9 @@ use kludgine::app::winit::window::CursorIcon; use kludgine::Color; use crate::app::{Application, Open, PendingApp, Run}; -use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, WidgetContext}; +use crate::context::{ + AsEventContext, EventContext, GraphicsContext, LayoutContext, ManageWidget, WidgetContext, +}; use crate::styles::components::{ FontFamily, FontStyle, FontWeight, Heading1FontFamily, Heading1Style, Heading1Weight, Heading2FontFamily, Heading2Style, Heading2Weight, Heading3FontFamily, Heading3Style, @@ -42,7 +44,7 @@ use crate::widgets::{ Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space, Stack, Style, Themed, ThemedMode, Validated, Wrap, }; -use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior, WindowHandle}; +use crate::window::{RunningWindow, ThemeMode, Window, WindowBehavior, WindowHandle, WindowLocal}; use crate::ConstraintLimit; /// A type that makes up a graphical user interface. @@ -620,7 +622,9 @@ pub trait WrapperWidget: Debug + Send + 'static { /// The widget has been removed from its parent widget. #[allow(unused_variables)] - fn unmounted(&mut self, context: &mut EventContext<'_, '_>) {} + fn unmounted(&mut self, context: &mut EventContext<'_, '_>) { + self.child_mut().unmount_in(context); + } /// Returns true if this widget should respond to mouse input at `location`. #[allow(unused_variables)] @@ -2237,24 +2241,34 @@ impl MountableChild for MountedWidget { /// A child widget #[derive(Clone)] -pub enum WidgetRef { - /// An unmounted child widget - Unmounted(WidgetInstance), - /// A mounted child widget - Mounted(MountedWidget), +pub struct WidgetRef { + instance: WidgetInstance, + mounted: WindowLocal, } impl WidgetRef { /// Returns a new unmounted child pub fn new(widget: impl MakeWidget) -> Self { - Self::Unmounted(widget.make_widget()) + Self { + instance: widget.make_widget(), + mounted: WindowLocal::default(), + } + } + + /// Returns this child, mounting it in the process if necessary. + fn mounted_for_context<'window>( + &mut self, + context: &mut impl AsEventContext<'window>, + ) -> &MountedWidget { + let mut context = context.as_event_context(); + self.mounted + .entry(&context) + .or_insert_with(|| context.push_child(self.instance.clone())) } /// Returns this child, mounting it in the process if necessary. pub fn mount_if_needed<'window>(&mut self, context: &mut impl AsEventContext<'window>) { - if let WidgetRef::Unmounted(instance) = self { - *self = WidgetRef::Mounted(context.push_child(instance.clone())); - } + self.mounted_for_context(context); } /// Returns this child, mounting it in the process if necessary. @@ -2262,39 +2276,39 @@ impl WidgetRef { &mut self, context: &mut impl AsEventContext<'window>, ) -> MountedWidget { - self.mount_if_needed(context); + self.mounted_for_context(context).clone() + } - let Self::Mounted(widget) = self else { - unreachable!("just initialized") - }; - widget.clone() + /// Returns this child, mounting it in the process if necessary. + #[must_use] + pub fn as_mounted(&self, context: &WidgetContext<'_, '_>) -> Option<&MountedWidget> { + self.mounted.get(context) } /// Returns the a reference to the underlying widget instance. #[must_use] - pub fn widget(&self) -> &WidgetInstance { - match self { - WidgetRef::Unmounted(widget) => widget, - WidgetRef::Mounted(managed) => &managed.widget, + pub const fn widget(&self) -> &WidgetInstance { + &self.instance + } + + /// Unmounts this widget from the window belonging to `context`, if needed. + pub fn unmount_in<'window>(&mut self, context: &mut impl AsEventContext<'window>) { + let mut context = context.as_event_context(); + if let Some(mounted) = self.mounted.clear_for(&context) { + context.remove_child(&mounted); } } } impl AsRef for WidgetRef { fn as_ref(&self) -> &WidgetId { - match self { - WidgetRef::Unmounted(widget) => widget.as_ref(), - WidgetRef::Mounted(widget) => widget.as_ref(), - } + self.instance.as_ref() } } impl Debug for WidgetRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Unmounted(arg0) => Debug::fmt(arg0, f), - Self::Mounted(arg0) => Debug::fmt(arg0, f), - } + Debug::fmt(&self.instance, f) } } @@ -2302,11 +2316,18 @@ impl Eq for WidgetRef {} impl PartialEq for WidgetRef { fn eq(&self, other: &Self) -> bool { - if let (WidgetRef::Mounted(this), WidgetRef::Mounted(other)) = (self, other) { - this == other - } else { - self.widget() == other.widget() - } + self.instance == other.instance + } +} + +impl ManageWidget for WidgetRef { + type Managed = Option; + + fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed { + self.mounted + .get(context) + .cloned() + .or_else(|| context.tree.widget(self.instance.id())) } } diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 76d4574..936266c 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -21,6 +21,7 @@ use crate::styles::components::{ use crate::styles::{ColorExt, Styles}; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED}; +use crate::window::WindowLocal; use crate::FitMeasuredSize; /// A clickable button. @@ -33,13 +34,18 @@ pub struct Button { /// The kind of button to draw. pub kind: Value, focusable: bool, + per_window: WindowLocal, +} + +#[derive(Debug, Default)] +struct PerWindow { buttons_pressed: usize, cached_state: CacheState, active_colors: Option>, color_animation: AnimationHandle, } -#[derive(Debug, Eq, PartialEq, Clone, Copy)] +#[derive(Default, Debug, Eq, PartialEq, Clone, Copy)] struct CacheState { key: WidgetCacheKey, kind: ButtonKind, @@ -133,14 +139,8 @@ impl Button { Self { content: content.widget_ref(), on_click: None, - cached_state: CacheState { - key: WidgetCacheKey::default(), - kind: ButtonKind::default(), - }, - buttons_pressed: 0, - active_colors: None, + per_window: WindowLocal::default(), kind: Value::Constant(ButtonKind::default()), - color_animation: AnimationHandle::default(), focusable: true, } } @@ -229,7 +229,9 @@ impl Button { let kind = self.kind.get_tracking_redraw(context); let visual_state = Self::visual_style(context); - self.cached_state = CacheState { + let window_local = self.per_window.entry(context).or_default(); + + window_local.cached_state = CacheState { key: context.cache_key(), kind, }; @@ -247,33 +249,46 @@ impl Button { fn update_colors(&mut self, context: &mut WidgetContext<'_, '_>, immediate: bool) { let new_style = self.determine_stateful_colors(context); + let window_local = self.per_window.entry(context).or_default(); - match (immediate, &self.active_colors) { + match (immediate, &window_local.active_colors) { (false, Some(style)) => { - self.color_animation = (style.transition_to(new_style)) + window_local.color_animation = (style.transition_to(new_style)) .over(Duration::from_millis(150)) .with_easing(context.get(&Easing)) .spawn(); } (true, Some(style)) => { style.set(new_style); - self.color_animation.clear(); + window_local.color_animation.clear(); } _ => { let new_style = Dynamic::new(new_style); let foreground = new_style.map_each(|s| s.foreground); - self.active_colors = Some(new_style); + window_local.active_colors = Some(new_style); context.attach_styles(Styles::new().with(&TextColor, foreground)); } } } fn current_style(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors { - if self.active_colors.is_none() { + if self + .per_window + .entry(context) + .or_default() + .active_colors + .is_none() + { self.update_colors(context, false); } - let style = self.active_colors.as_ref().expect("always initialized"); + let style = self + .per_window + .entry(context) + .or_default() + .active_colors + .as_ref() + .expect("always initialized"); context.redraw_when_changed(style); style.get() } @@ -353,7 +368,10 @@ impl Widget for Button { #![allow(clippy::similar_names)] let current_style = self.kind.get_tracking_redraw(context); - if self.cached_state.key != context.cache_key() || self.cached_state.kind != current_style { + let window_local = self.per_window.entry(context).or_default(); + if window_local.cached_state.key != context.cache_key() + || window_local.cached_state.kind != current_style + { self.update_colors(context, false); } @@ -414,7 +432,7 @@ impl Widget for Button { _button: MouseButton, context: &mut EventContext<'_, '_>, ) -> EventHandling { - self.buttons_pressed += 1; + self.per_window.entry(context).or_default().buttons_pressed += 1; context.activate(); HANDLED } @@ -446,8 +464,9 @@ impl Widget for Button { _button: MouseButton, context: &mut EventContext<'_, '_>, ) { - self.buttons_pressed -= 1; - if self.buttons_pressed == 0 { + let window_local = self.per_window.entry(context).or_default(); + window_local.buttons_pressed -= 1; + if window_local.buttons_pressed == 0 { context.deactivate(); if let (true, Some(location)) = (self.focusable, location) { @@ -472,7 +491,7 @@ impl Widget for Button { .into_upx(context.gfx.scale()) .round(); let double_padding = padding * 2; - let mounted = self.content.mounted(&mut context.as_event_context()); + let mounted = self.content.mounted(context); let available_space = available_space.map(|space| space - double_padding); let size = context.for_other(&mounted).layout(available_space); let size = available_space.fit_measured(size, context.gfx.scale()); @@ -510,9 +529,10 @@ impl Widget for Button { } fn activate(&mut self, context: &mut EventContext<'_, '_>) { + let window_local = self.per_window.entry(context).or_default(); // If we have no buttons pressed, the event should fire on activate not // on deactivate. - if self.buttons_pressed == 0 { + if window_local.buttons_pressed == 0 { self.invoke_on_click(context); } self.update_colors(context, true); diff --git a/src/widgets/disclose.rs b/src/widgets/disclose.rs index 86c6060..3f31839 100644 --- a/src/widgets/disclose.rs +++ b/src/widgets/disclose.rs @@ -110,7 +110,7 @@ impl DiscloseIndicator { contents: WidgetRef::new(contents.collapse_vertically(collapsed.clone())), collapsed, hovering_indicator: false, - label: label.map(WidgetRef::Unmounted), + label: label.map(WidgetRef::new), target_colors: None, color: Dynamic::new(Color::CLEAR_WHITE), stroke_color: Dynamic::new(Color::CLEAR_WHITE), diff --git a/src/widgets/grid.rs b/src/widgets/grid.rs index 18bf7c0..a4b690a 100644 --- a/src/widgets/grid.rs +++ b/src/widgets/grid.rs @@ -381,7 +381,7 @@ impl GridLayout { scale: Fraction, mut measure: impl FnMut(usize, usize, Size, bool) -> Size, ) -> Size { - let (space_constraint, other_constraint) = self.orientation.split_size(available); + let (space_constraint, mut other_constraint) = self.orientation.split_size(available); let available_space = space_constraint.max(); let known_gutters = gutter.saturating_mul(UPx::new( (self.children.len() - self.fit_to_content.len()) @@ -391,6 +391,13 @@ impl GridLayout { let allocated_space = self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters; let mut remaining = available_space.saturating_sub(allocated_space); + + if self.elements_per_child > 1 { + // When we are in multi-row mode, we force a size-to-fit mode for + // children. Trying to ask each row to fill will never work. + other_constraint = ConstraintLimit::SizeToFit(other_constraint.max()); + } + // 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 // requires one extra layout call, so we avoid persisting layouts during diff --git a/src/widgets/label.rs b/src/widgets/label.rs index ec9c76e..1331f91 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -9,6 +9,7 @@ use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::TextColor; use crate::value::{Dynamic, Generation, IntoValue, Value}; use crate::widget::{Widget, WidgetInstance}; +use crate::window::WindowLocal; use crate::ConstraintLimit; /// A read-only text widget. @@ -16,7 +17,7 @@ use crate::ConstraintLimit; pub struct Label { /// The contents of the label. pub text: Value, - prepared_text: Option<(MeasuredText, Option, Px, Color)>, + prepared_text: WindowLocal<(MeasuredText, Option, Px, Color)>, } impl Label { @@ -24,7 +25,7 @@ impl Label { pub fn new(text: impl IntoValue) -> Self { Self { text: text.into_value(), - prepared_text: None, + prepared_text: WindowLocal::default(), } } @@ -35,14 +36,15 @@ impl Label { width: Px, ) -> &MeasuredText { let check_generation = self.text.generation(); - match &self.prepared_text { + match self.prepared_text.get(context) { Some((prepared, prepared_generation, prepared_width, prepared_color)) if prepared.can_render_to(&context.gfx) && *prepared_generation == check_generation && *prepared_color == color && (*prepared_width == width - || ((*prepared_width < width || prepared.size.width <= width) - && prepared.line_height == prepared.size.height)) => {} + || (*prepared_width < width + || (prepared.size.width <= width + && prepared.line_height == prepared.size.height))) => {} _ => { context.apply_current_font_settings(); let measured = self.text.map(|text| { @@ -50,12 +52,13 @@ impl Label { .gfx .measure_text(Text::new(text, color).wrap_at(width)) }); - self.prepared_text = Some((measured, check_generation, width, color)); + self.prepared_text + .set(context, (measured, check_generation, width, color)); } } self.prepared_text - .as_ref() + .get(context) .map(|(prepared, _, _, _)| prepared) .expect("always initialized") } @@ -86,12 +89,16 @@ impl Widget for Label { let width = available_space.width.max().try_into().unwrap_or(Px::MAX); let prepared = self.prepared_text(context, color, width); - prepared.size.try_cast().unwrap_or_default() + prepared.size.try_cast().unwrap_or_default().ceil() } fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fmt.debug_tuple("Label").field(&self.text).finish() } + + fn unmounted(&mut self, context: &mut crate::context::EventContext<'_, '_>) { + self.prepared_text.clear_for(context); + } } macro_rules! impl_make_widget { diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 50a4194..03bfb0d 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -178,7 +178,7 @@ impl Widget for OverlayLayer { let state = self.state.lock(); for child in &state.overlays { - let WidgetRef::Mounted(mounted) = &child.widget else { + let Some(mounted) = child.widget.as_mounted(context) else { continue; }; diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index 35daf20..b15272e 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -92,10 +92,7 @@ impl Stack { { (child, size) } else { - ( - WidgetRef::Unmounted(widget.clone()), - GridDimension::FitContent, - ) + (WidgetRef::new(widget.clone()), GridDimension::FitContent) }; drop(guard); this.insert(index, widget.mounted(context)); diff --git a/src/widgets/switcher.rs b/src/widgets/switcher.rs index 2259f8a..17ac2ee 100644 --- a/src/widgets/switcher.rs +++ b/src/widgets/switcher.rs @@ -2,7 +2,7 @@ use std::fmt::Debug; use figures::Size; -use crate::context::{AsEventContext, LayoutContext}; +use crate::context::LayoutContext; use crate::value::{Dynamic, DynamicReader, IntoDynamic}; use crate::widget::{WidgetInstance, WidgetRef, WrapperWidget}; use crate::ConstraintLimit; @@ -55,10 +55,7 @@ impl WrapperWidget for Switcher { context: &mut LayoutContext<'_, '_, '_, '_, '_>, ) -> Size { if self.source.has_updated() { - let removed = std::mem::replace(&mut self.child, WidgetRef::new(self.source.get())); - if let WidgetRef::Mounted(removed) = removed { - context.remove_child(&removed); - } + self.child.unmount_in(context); } context.invalidate_when_changed(&self.source); available_space diff --git a/src/window.rs b/src/window.rs index b7eeb25..d2ca9d2 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,6 +1,7 @@ //! Types for displaying a [`Widget`] inside of a desktop window. use std::cell::RefCell; +use std::collections::hash_map; use std::ffi::OsStr; use std::hash::Hash; use std::ops::{Deref, DerefMut, Not}; @@ -23,7 +24,7 @@ use kludgine::app::WindowBehavior as _; use kludgine::cosmic_text::{fontdb, Family, FamilyOwned}; use kludgine::render::Drawing; use kludgine::wgpu::CompositeAlphaMode; -use kludgine::Kludgine; +use kludgine::{Kludgine, KludgineId}; use tracing::Level; use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; @@ -47,6 +48,7 @@ use crate::{initialize_tracing, ConstraintLimit}; /// A currently running Cushy window. pub struct RunningWindow<'window> { window: kludgine::app::Window<'window, WindowCommand>, + kludgine_id: KludgineId, invalidation_status: InvalidationStatus, cushy: Cushy, focused: Dynamic, @@ -57,6 +59,7 @@ pub struct RunningWindow<'window> { impl<'window> RunningWindow<'window> { pub(crate) fn new( window: kludgine::app::Window<'window, WindowCommand>, + kludgine_id: KludgineId, invalidation_status: &InvalidationStatus, cushy: &Cushy, focused: &Dynamic, @@ -65,6 +68,7 @@ impl<'window> RunningWindow<'window> { ) -> Self { Self { window, + kludgine_id, invalidation_status: invalidation_status.clone(), cushy: cushy.clone(), focused: focused.clone(), @@ -73,6 +77,14 @@ impl<'window> RunningWindow<'window> { } } + /// Returns the [`KludgineId`] of this window. + /// + /// Each window has its own unique `KludgineId`. + #[must_use] + pub const fn kludgine_id(&self) -> KludgineId { + self.kludgine_id + } + /// Returns a dynamic that is updated whenever this window's focus status /// changes. #[must_use] @@ -820,6 +832,7 @@ where let mut behavior = T::initialize( &mut RunningWindow::new( window, + graphics.id(), &redraw_status, &cushy, &focused, @@ -886,6 +899,7 @@ where let resizable = window.winit().is_resizable(); let mut window = RunningWindow::new( window, + graphics.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1012,13 +1026,14 @@ where fn close_requested( &mut self, window: kludgine::app::Window<'_, WindowCommand>, - _kludgine: &mut Kludgine, + kludgine: &mut Kludgine, ) -> bool { Self::request_close( &mut self.should_close, &mut self.behavior, &mut RunningWindow::new( window, + kludgine.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1093,6 +1108,7 @@ where }; let mut window = RunningWindow::new( window, + kludgine.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1137,6 +1153,7 @@ where let mut window = RunningWindow::new( window, + kludgine.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1173,6 +1190,7 @@ where .unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget")); let mut window = RunningWindow::new( window, + kludgine.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1206,6 +1224,7 @@ where let mut window = RunningWindow::new( window, + kludgine.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1258,6 +1277,7 @@ where if self.cursor.widget.take().is_some() { let mut window = RunningWindow::new( window, + kludgine.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1288,6 +1308,7 @@ where ) { let mut window = RunningWindow::new( window, + kludgine.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1390,7 +1411,7 @@ where fn event( &mut self, mut window: kludgine::app::Window<'_, WindowCommand>, - _kludgine: &mut Kludgine, + kludgine: &mut Kludgine, event: WindowCommand, ) { match event { @@ -1400,6 +1421,7 @@ where WindowCommand::RequestClose => { let mut window = RunningWindow::new( window, + kludgine.id(), &self.redraw_status, &self.cushy, &self.focused, @@ -1741,3 +1763,46 @@ struct PendingWindowHandle { handle: OnceLock>, commands: Mutex>, } + +/// A collection that stores an instance of `T` per window. +/// +/// This is a convenience wrapper around a `HashMap`. +#[derive(Debug, Clone)] +pub struct WindowLocal { + by_window: AHashMap, +} + +impl WindowLocal { + /// Looks up the entry for this window. + /// + /// Internally this API uses [`HashMap::entry`](hash_map::HashMap::entry). + pub fn entry(&mut self, context: &WidgetContext<'_, '_>) -> hash_map::Entry<'_, KludgineId, T> { + self.by_window.entry(context.kludgine_id()) + } + + /// Sets `value` as the local value for `context`'s window. + pub fn set(&mut self, context: &WidgetContext<'_, '_>, value: T) { + self.by_window.insert(context.kludgine_id(), value); + } + + /// Looks up the value for this window, returning None if not found. + /// + /// Internally this API uses [`HashMap::get`](hash_map::HashMap::get). + #[must_use] + pub fn get(&self, context: &WidgetContext<'_, '_>) -> Option<&T> { + self.by_window.get(&context.kludgine_id()) + } + + /// Removes any stored value for this window. + pub fn clear_for(&mut self, context: &WidgetContext<'_, '_>) -> Option { + self.by_window.remove(&context.kludgine_id()) + } +} + +impl Default for WindowLocal { + fn default() -> Self { + Self { + by_window: AHashMap::default(), + } + } +}