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<T>` 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.
This commit is contained in:
Jonathan Johnson 2023-12-29 13:21:39 -08:00
parent 999f920f8c
commit 9e4e079bf5
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
16 changed files with 309 additions and 133 deletions

View file

@ -7,12 +7,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## 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 ### Fixed
- The root widget is now included in the search for widgets to accept focus. - 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 - Widgets that have been laid out with a 0px width or height no longer have
their `redraw` functions called nor can they receive focus. their `redraw` functions called nor can they receive focus.
- `Grid` now synchronizes removal of widgets from `GridWidgets` correctly. - `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 ### Added
@ -43,6 +64,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
existence. existence.
- `Dynamic::readers()` returns the number of `DynamicReader`s for the dynamic in - `Dynamic::readers()` returns the number of `DynamicReader`s for the dynamic in
existence. existence.
- `RunningWindow::kludgine_id()` returns a unique id for that window.
- `WindowLocal<T>` 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 [99]: https://github.com/khonsulabs/cushy/issues/99

10
Cargo.lock generated
View file

@ -35,9 +35,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.6" version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a" checksum = "77c3a9648d43b9cd48db467b3f87fdd6e146bcc88ab0180006cef2179fe11d01"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"getrandom", "getrandom",
@ -1155,9 +1155,9 @@ checksum = "d7e253b574775d0ebd7975c471fc18f72f0775a4d42b563b5fbc3c4068aa1075"
[[package]] [[package]]
name = "kempt" name = "kempt"
version = "0.2.2" version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13a912e97bbe1ec7cbead29896008766dedc4790350c55aece45998fde067200" checksum = "4a37a6bdb52eae6acb1efbab069af766555a6d77712380fd48876b83543d3071"
[[package]] [[package]]
name = "khronos-egl" name = "khronos-egl"
@ -1179,7 +1179,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
[[package]] [[package]]
name = "kludgine" name = "kludgine"
version = "0.7.0" version = "0.7.0"
source = "git+https://github.com/khonsulabs/kludgine#a38466b021ce55c26c6d533191a87a22109016b2" source = "git+https://github.com/khonsulabs/kludgine#0bda2a6dc273aa49338f23ea5190aefdf037d740"
dependencies = [ dependencies = [
"ahash", "ahash",
"alot", "alot",

View file

@ -8,20 +8,32 @@ const INTRO: &str = "This example demonstrates the DebugContext, which allows ob
fn main() -> cushy::Result { fn main() -> cushy::Result {
let app = PendingApp::default(); let app = PendingApp::default();
let dbg = DebugContext::default(); let info = DebugContext::default();
let window_count = Dynamic::new(0_usize);
let total_windows = Dynamic::new(0_usize);
dbg.observe("Open Windows", &window_count); let window_count = Dynamic::new(0_usize);
dbg.observe("Total Windows", &total_windows); let total_windows = info.dbg("Total Windows", Dynamic::new(0_usize));
dbg.clone().open(&app)?; 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 INTRO
.and("Open a Window".into_button().on_click({ .and(open_window_button)
let app = app.as_app();
move |()| open_a_window(&window_count, &total_windows, &dbg, &app)
}))
.into_rows() .into_rows()
.centered() .centered()
.run_in(app) .run_in(app)
@ -30,7 +42,7 @@ fn main() -> cushy::Result {
fn open_a_window( fn open_a_window(
window_count: &Dynamic<usize>, window_count: &Dynamic<usize>,
total_windows: &Dynamic<usize>, total_windows: &Dynamic<usize>,
dbg: &DebugContext, info: &DebugContext,
app: &dyn Application, app: &dyn Application,
) { ) {
*window_count.lock() += 1; *window_count.lock() += 1;
@ -39,10 +51,9 @@ fn open_a_window(
*total *total
}); });
let window_title = format!("Window #{window_number}"); 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); let value = dbg.dbg("Slider", Dynamic::new(0_u8));
dbg.observe("Slider", &value);
let window_count = window_count.clone(); let window_count = window_count.clone();
let _ = format!("This is window {window_number}.") let _ = format!("This is window {window_number}.")

View file

@ -12,7 +12,7 @@ use kludgine::app::winit::event::{
}; };
use kludgine::app::winit::window::CursorIcon; use kludgine::app::winit::window::CursorIcon;
use kludgine::shapes::{Shape, StrokeOptions}; use kludgine::shapes::{Shape, StrokeOptions};
use kludgine::{Color, Kludgine}; use kludgine::{Color, Kludgine, KludgineId};
use crate::animation::ZeroToOne; use crate::animation::ZeroToOne;
use crate::graphics::Graphics; use crate::graphics::Graphics;
@ -24,9 +24,7 @@ use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair};
use crate::tree::Tree; use crate::tree::Tree;
use crate::utils::IgnorePoison; use crate::utils::IgnorePoison;
use crate::value::{IntoValue, Value}; use crate::value::{IntoValue, Value};
use crate::widget::{ use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance};
EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance, WidgetRef,
};
use crate::window::{CursorState, RunningWindow, ThemeMode}; use crate::window::{CursorState, RunningWindow, ThemeMode};
use crate::ConstraintLimit; use crate::ConstraintLimit;
@ -893,6 +891,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
tree, tree,
effective_styles: current_node.effective_styles(), effective_styles: current_node.effective_styles(),
cache: WidgetCacheKey { cache: WidgetCacheKey {
kludgine_id: Some(window.kludgine_id()),
theme_mode, theme_mode,
enabled, enabled,
}, },
@ -941,6 +940,7 @@ impl<'context, 'window> WidgetContext<'context, 'window> {
WidgetContext { WidgetContext {
effective_styles, effective_styles,
cache: WidgetCacheKey { cache: WidgetCacheKey {
kludgine_id: self.cache.kludgine_id,
theme_mode, theme_mode,
enabled: current_node.enabled(&self.handle()), enabled: current_node.enabled(&self.handle()),
}, },
@ -1333,17 +1333,6 @@ impl ManageWidget for WidgetInstance {
} }
} }
impl ManageWidget for WidgetRef {
type Managed = Option<MountedWidget>;
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 { impl ManageWidget for MountedWidget {
type Managed = Self; type Managed = Self;
@ -1383,6 +1372,7 @@ impl<T> MapManagedWidget<T> for MountedWidget {
/// keys are not equal, the widget should clear all caches. /// keys are not equal, the widget should clear all caches.
#[derive(Debug, Clone, Copy, Eq, PartialEq)] #[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub struct WidgetCacheKey { pub struct WidgetCacheKey {
kludgine_id: Option<KludgineId>,
theme_mode: ThemeMode, theme_mode: ThemeMode,
enabled: bool, enabled: bool,
} }
@ -1390,6 +1380,7 @@ pub struct WidgetCacheKey {
impl Default for WidgetCacheKey { impl Default for WidgetCacheKey {
fn default() -> Self { fn default() -> Self {
Self { Self {
kludgine_id: None,
theme_mode: ThemeMode::default().inverse(), theme_mode: ThemeMode::default().inverse(),
enabled: false, enabled: false,
} }

View file

@ -17,23 +17,43 @@ pub struct DebugContext {
} }
impl 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 /// When the final reference to `value` is dropped, this observation will
/// automatically be removed. /// automatically be removed.
pub fn observe<T>(&self, label: impl Into<String>, value: &Dynamic<T>) ///
/// This function is designed to work similarly to the [`dbg!`] macro.
pub fn dbg<T>(&self, label: impl Into<String>, value: Dynamic<T>) -> Dynamic<T>
where 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<T, Widget, MakeObserver>(
&self,
label: impl Into<String>,
value: &Dynamic<T>,
make_observer: MakeObserver,
) where
T: Clone + Send + Sync + 'static,
MakeObserver: FnOnce(Dynamic<T>) -> Widget,
Widget: MakeWidget,
{ {
let reader = value.create_reader(); let reader = value.create_reader();
let id = self.section.map_ref(|section| { let id = self.section.map_ref(|section| {
section.values.lock().push(Box::new(RegisteredValue { section.values.lock().push(Box::new(RegisteredValue {
label: label.into(), label: label.into(),
value: reader.clone(), value: reader.clone(),
widget: value widget: make_observer(value.weak_clone()).make_widget(),
.weak_clone()
.map_each(|value| format!("{value:?}"))
.make_widget(),
})) }))
}); });
let this = self.clone(); let this = self.clone();
@ -79,7 +99,7 @@ impl DebugContext {
fn into_window(self) -> Window { fn into_window(self) -> Window {
self.section self.section
.map_ref(|section| section.widget.clone()) .map_ref(|section| section.widget.clone())
.vertical_scroll() // .vertical_scroll()
.into_window() .into_window()
.titled("Cushy Debugger") .titled("Cushy Debugger")
} }
@ -173,7 +193,7 @@ impl DebugSection {
let value_grid = Grid::from_rows(values.map_each(|values| { let value_grid = Grid::from_rows(values.map_each(|values| {
values values
.iter() .iter()
.map(|o| [o.label().make_widget(), o.widget().clone()]) .map(|o| (o.label(), o.widget().clone().align_left()))
.collect::<GridWidgets<2>>() .collect::<GridWidgets<2>>()
})); }));

View file

@ -83,15 +83,17 @@ impl Tree {
let node = &mut data.nodes[widget]; let node = &mut data.nodes[widget];
node.layout = Some(rect); node.layout = Some(rect);
let mut children_to_offset = node.children.clone(); if !node.children.is_empty() {
while let Some(child) = children_to_offset.pop() { let mut children_to_offset = node.children.clone();
if let Some(layout) = data while let Some(child) = children_to_offset.pop() {
.nodes if let Some(layout) = data
.get_mut(child) .nodes
.and_then(|child| child.layout.as_mut()) .get_mut(child)
{ .and_then(|child| child.layout.as_mut())
layout.origin += rect.origin; {
children_to_offset.extend(data.nodes[child].children.iter().copied()); 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<WeakTree> for Tree {
fn eq(&self, other: &WeakTree) -> bool {
Arc::as_ptr(&self.data) == Weak::as_ptr(&other.0)
}
}
pub(crate) struct HoverResults { pub(crate) struct HoverResults {
pub unhovered: Vec<MountedWidget>, pub unhovered: Vec<MountedWidget>,
pub hovered: Vec<MountedWidget>, pub hovered: Vec<MountedWidget>,

View file

@ -268,7 +268,7 @@ impl<T> Dynamic<T> {
#[must_use] #[must_use]
pub fn weak_clone(&self) -> Self pub fn weak_clone(&self) -> Self
where where
T: Clone + PartialEq + Send + 'static, T: Clone + Send + 'static,
{ {
let weak_source = self.downgrade(); let weak_source = self.downgrade();
let weak_out = Dynamic::new(self.get()); let weak_out = Dynamic::new(self.get());
@ -276,7 +276,7 @@ impl<T> Dynamic<T> {
let weak_out = weak_out.clone(); let weak_out = weak_out.clone();
move || { move || {
if let Some(source) = weak_source.upgrade() { if let Some(source) = weak_source.upgrade() {
weak_out.set(source.get()); *weak_out.lock() = source.get();
} }
} }
})); }));

View file

@ -19,7 +19,9 @@ use kludgine::app::winit::window::CursorIcon;
use kludgine::Color; use kludgine::Color;
use crate::app::{Application, Open, PendingApp, Run}; 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::{ use crate::styles::components::{
FontFamily, FontStyle, FontWeight, Heading1FontFamily, Heading1Style, Heading1Weight, FontFamily, FontStyle, FontWeight, Heading1FontFamily, Heading1Style, Heading1Weight,
Heading2FontFamily, Heading2Style, Heading2Weight, Heading3FontFamily, Heading3Style, Heading2FontFamily, Heading2Style, Heading2Weight, Heading3FontFamily, Heading3Style,
@ -42,7 +44,7 @@ use crate::widgets::{
Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space, Align, Button, Checkbox, Collapse, Container, Disclose, Expand, Layers, Resize, Scroll, Space,
Stack, Style, Themed, ThemedMode, Validated, Wrap, 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; use crate::ConstraintLimit;
/// A type that makes up a graphical user interface. /// 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. /// The widget has been removed from its parent widget.
#[allow(unused_variables)] #[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`. /// Returns true if this widget should respond to mouse input at `location`.
#[allow(unused_variables)] #[allow(unused_variables)]
@ -2237,24 +2241,34 @@ impl MountableChild for MountedWidget {
/// A child widget /// A child widget
#[derive(Clone)] #[derive(Clone)]
pub enum WidgetRef { pub struct WidgetRef {
/// An unmounted child widget instance: WidgetInstance,
Unmounted(WidgetInstance), mounted: WindowLocal<MountedWidget>,
/// A mounted child widget
Mounted(MountedWidget),
} }
impl WidgetRef { impl WidgetRef {
/// Returns a new unmounted child /// Returns a new unmounted child
pub fn new(widget: impl MakeWidget) -> Self { 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. /// Returns this child, mounting it in the process if necessary.
pub fn mount_if_needed<'window>(&mut self, context: &mut impl AsEventContext<'window>) { pub fn mount_if_needed<'window>(&mut self, context: &mut impl AsEventContext<'window>) {
if let WidgetRef::Unmounted(instance) = self { self.mounted_for_context(context);
*self = WidgetRef::Mounted(context.push_child(instance.clone()));
}
} }
/// Returns this child, mounting it in the process if necessary. /// Returns this child, mounting it in the process if necessary.
@ -2262,39 +2276,39 @@ impl WidgetRef {
&mut self, &mut self,
context: &mut impl AsEventContext<'window>, context: &mut impl AsEventContext<'window>,
) -> MountedWidget { ) -> MountedWidget {
self.mount_if_needed(context); self.mounted_for_context(context).clone()
}
let Self::Mounted(widget) = self else { /// Returns this child, mounting it in the process if necessary.
unreachable!("just initialized") #[must_use]
}; pub fn as_mounted(&self, context: &WidgetContext<'_, '_>) -> Option<&MountedWidget> {
widget.clone() self.mounted.get(context)
} }
/// Returns the a reference to the underlying widget instance. /// Returns the a reference to the underlying widget instance.
#[must_use] #[must_use]
pub fn widget(&self) -> &WidgetInstance { pub const fn widget(&self) -> &WidgetInstance {
match self { &self.instance
WidgetRef::Unmounted(widget) => widget, }
WidgetRef::Mounted(managed) => &managed.widget,
/// 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<WidgetId> for WidgetRef { impl AsRef<WidgetId> for WidgetRef {
fn as_ref(&self) -> &WidgetId { fn as_ref(&self) -> &WidgetId {
match self { self.instance.as_ref()
WidgetRef::Unmounted(widget) => widget.as_ref(),
WidgetRef::Mounted(widget) => widget.as_ref(),
}
} }
} }
impl Debug for WidgetRef { impl Debug for WidgetRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { Debug::fmt(&self.instance, f)
Self::Unmounted(arg0) => Debug::fmt(arg0, f),
Self::Mounted(arg0) => Debug::fmt(arg0, f),
}
} }
} }
@ -2302,11 +2316,18 @@ impl Eq for WidgetRef {}
impl PartialEq for WidgetRef { impl PartialEq for WidgetRef {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
if let (WidgetRef::Mounted(this), WidgetRef::Mounted(other)) = (self, other) { self.instance == other.instance
this == other }
} else { }
self.widget() == other.widget()
} impl ManageWidget for WidgetRef {
type Managed = Option<MountedWidget>;
fn manage(&self, context: &WidgetContext<'_, '_>) -> Self::Managed {
self.mounted
.get(context)
.cloned()
.or_else(|| context.tree.widget(self.instance.id()))
} }
} }

View file

@ -21,6 +21,7 @@ use crate::styles::components::{
use crate::styles::{ColorExt, Styles}; use crate::styles::{ColorExt, Styles};
use crate::value::{Dynamic, IntoValue, Value}; use crate::value::{Dynamic, IntoValue, Value};
use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED}; use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED};
use crate::window::WindowLocal;
use crate::FitMeasuredSize; use crate::FitMeasuredSize;
/// A clickable button. /// A clickable button.
@ -33,13 +34,18 @@ pub struct Button {
/// The kind of button to draw. /// The kind of button to draw.
pub kind: Value<ButtonKind>, pub kind: Value<ButtonKind>,
focusable: bool, focusable: bool,
per_window: WindowLocal<PerWindow>,
}
#[derive(Debug, Default)]
struct PerWindow {
buttons_pressed: usize, buttons_pressed: usize,
cached_state: CacheState, cached_state: CacheState,
active_colors: Option<Dynamic<ButtonColors>>, active_colors: Option<Dynamic<ButtonColors>>,
color_animation: AnimationHandle, color_animation: AnimationHandle,
} }
#[derive(Debug, Eq, PartialEq, Clone, Copy)] #[derive(Default, Debug, Eq, PartialEq, Clone, Copy)]
struct CacheState { struct CacheState {
key: WidgetCacheKey, key: WidgetCacheKey,
kind: ButtonKind, kind: ButtonKind,
@ -133,14 +139,8 @@ impl Button {
Self { Self {
content: content.widget_ref(), content: content.widget_ref(),
on_click: None, on_click: None,
cached_state: CacheState { per_window: WindowLocal::default(),
key: WidgetCacheKey::default(),
kind: ButtonKind::default(),
},
buttons_pressed: 0,
active_colors: None,
kind: Value::Constant(ButtonKind::default()), kind: Value::Constant(ButtonKind::default()),
color_animation: AnimationHandle::default(),
focusable: true, focusable: true,
} }
} }
@ -229,7 +229,9 @@ impl Button {
let kind = self.kind.get_tracking_redraw(context); let kind = self.kind.get_tracking_redraw(context);
let visual_state = Self::visual_style(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(), key: context.cache_key(),
kind, kind,
}; };
@ -247,33 +249,46 @@ impl Button {
fn update_colors(&mut self, context: &mut WidgetContext<'_, '_>, immediate: bool) { fn update_colors(&mut self, context: &mut WidgetContext<'_, '_>, immediate: bool) {
let new_style = self.determine_stateful_colors(context); 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)) => { (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)) .over(Duration::from_millis(150))
.with_easing(context.get(&Easing)) .with_easing(context.get(&Easing))
.spawn(); .spawn();
} }
(true, Some(style)) => { (true, Some(style)) => {
style.set(new_style); style.set(new_style);
self.color_animation.clear(); window_local.color_animation.clear();
} }
_ => { _ => {
let new_style = Dynamic::new(new_style); let new_style = Dynamic::new(new_style);
let foreground = new_style.map_each(|s| s.foreground); 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)); context.attach_styles(Styles::new().with(&TextColor, foreground));
} }
} }
} }
fn current_style(&mut self, context: &mut WidgetContext<'_, '_>) -> ButtonColors { 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); 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); context.redraw_when_changed(style);
style.get() style.get()
} }
@ -353,7 +368,10 @@ impl Widget for Button {
#![allow(clippy::similar_names)] #![allow(clippy::similar_names)]
let current_style = self.kind.get_tracking_redraw(context); 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); self.update_colors(context, false);
} }
@ -414,7 +432,7 @@ impl Widget for Button {
_button: MouseButton, _button: MouseButton,
context: &mut EventContext<'_, '_>, context: &mut EventContext<'_, '_>,
) -> EventHandling { ) -> EventHandling {
self.buttons_pressed += 1; self.per_window.entry(context).or_default().buttons_pressed += 1;
context.activate(); context.activate();
HANDLED HANDLED
} }
@ -446,8 +464,9 @@ impl Widget for Button {
_button: MouseButton, _button: MouseButton,
context: &mut EventContext<'_, '_>, context: &mut EventContext<'_, '_>,
) { ) {
self.buttons_pressed -= 1; let window_local = self.per_window.entry(context).or_default();
if self.buttons_pressed == 0 { window_local.buttons_pressed -= 1;
if window_local.buttons_pressed == 0 {
context.deactivate(); context.deactivate();
if let (true, Some(location)) = (self.focusable, location) { if let (true, Some(location)) = (self.focusable, location) {
@ -472,7 +491,7 @@ impl Widget for Button {
.into_upx(context.gfx.scale()) .into_upx(context.gfx.scale())
.round(); .round();
let double_padding = padding * 2; 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 available_space = available_space.map(|space| space - double_padding);
let size = context.for_other(&mounted).layout(available_space); let size = context.for_other(&mounted).layout(available_space);
let size = available_space.fit_measured(size, context.gfx.scale()); 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<'_, '_>) { 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 // If we have no buttons pressed, the event should fire on activate not
// on deactivate. // on deactivate.
if self.buttons_pressed == 0 { if window_local.buttons_pressed == 0 {
self.invoke_on_click(context); self.invoke_on_click(context);
} }
self.update_colors(context, true); self.update_colors(context, true);

View file

@ -110,7 +110,7 @@ impl DiscloseIndicator {
contents: WidgetRef::new(contents.collapse_vertically(collapsed.clone())), contents: WidgetRef::new(contents.collapse_vertically(collapsed.clone())),
collapsed, collapsed,
hovering_indicator: false, hovering_indicator: false,
label: label.map(WidgetRef::Unmounted), label: label.map(WidgetRef::new),
target_colors: None, target_colors: None,
color: Dynamic::new(Color::CLEAR_WHITE), color: Dynamic::new(Color::CLEAR_WHITE),
stroke_color: Dynamic::new(Color::CLEAR_WHITE), stroke_color: Dynamic::new(Color::CLEAR_WHITE),

View file

@ -381,7 +381,7 @@ impl GridLayout {
scale: Fraction, scale: Fraction,
mut measure: impl FnMut(usize, usize, Size<ConstraintLimit>, bool) -> Size<UPx>, mut measure: impl FnMut(usize, usize, Size<ConstraintLimit>, bool) -> Size<UPx>,
) -> Size<UPx> { ) -> Size<UPx> {
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 available_space = space_constraint.max();
let known_gutters = gutter.saturating_mul(UPx::new( let known_gutters = gutter.saturating_mul(UPx::new(
(self.children.len() - self.fit_to_content.len()) (self.children.len() - self.fit_to_content.len())
@ -391,6 +391,13 @@ impl GridLayout {
let allocated_space = let allocated_space =
self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters; self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters;
let mut remaining = available_space.saturating_sub(allocated_space); 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 // 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 // widgets an opportunity to lay themselves out in the full area. This
// requires one extra layout call, so we avoid persisting layouts during // requires one extra layout call, so we avoid persisting layouts during

View file

@ -9,6 +9,7 @@ use crate::context::{GraphicsContext, LayoutContext};
use crate::styles::components::TextColor; use crate::styles::components::TextColor;
use crate::value::{Dynamic, Generation, IntoValue, Value}; use crate::value::{Dynamic, Generation, IntoValue, Value};
use crate::widget::{Widget, WidgetInstance}; use crate::widget::{Widget, WidgetInstance};
use crate::window::WindowLocal;
use crate::ConstraintLimit; use crate::ConstraintLimit;
/// A read-only text widget. /// A read-only text widget.
@ -16,7 +17,7 @@ use crate::ConstraintLimit;
pub struct Label { pub struct Label {
/// The contents of the label. /// The contents of the label.
pub text: Value<String>, pub text: Value<String>,
prepared_text: Option<(MeasuredText<Px>, Option<Generation>, Px, Color)>, prepared_text: WindowLocal<(MeasuredText<Px>, Option<Generation>, Px, Color)>,
} }
impl Label { impl Label {
@ -24,7 +25,7 @@ impl Label {
pub fn new(text: impl IntoValue<String>) -> Self { pub fn new(text: impl IntoValue<String>) -> Self {
Self { Self {
text: text.into_value(), text: text.into_value(),
prepared_text: None, prepared_text: WindowLocal::default(),
} }
} }
@ -35,14 +36,15 @@ impl Label {
width: Px, width: Px,
) -> &MeasuredText<Px> { ) -> &MeasuredText<Px> {
let check_generation = self.text.generation(); let check_generation = self.text.generation();
match &self.prepared_text { match self.prepared_text.get(context) {
Some((prepared, prepared_generation, prepared_width, prepared_color)) Some((prepared, prepared_generation, prepared_width, prepared_color))
if prepared.can_render_to(&context.gfx) if prepared.can_render_to(&context.gfx)
&& *prepared_generation == check_generation && *prepared_generation == check_generation
&& *prepared_color == color && *prepared_color == color
&& (*prepared_width == width && (*prepared_width == width
|| ((*prepared_width < width || prepared.size.width <= width) || (*prepared_width < width
&& prepared.line_height == prepared.size.height)) => {} || (prepared.size.width <= width
&& prepared.line_height == prepared.size.height))) => {}
_ => { _ => {
context.apply_current_font_settings(); context.apply_current_font_settings();
let measured = self.text.map(|text| { let measured = self.text.map(|text| {
@ -50,12 +52,13 @@ impl Label {
.gfx .gfx
.measure_text(Text::new(text, color).wrap_at(width)) .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 self.prepared_text
.as_ref() .get(context)
.map(|(prepared, _, _, _)| prepared) .map(|(prepared, _, _, _)| prepared)
.expect("always initialized") .expect("always initialized")
} }
@ -86,12 +89,16 @@ impl Widget for Label {
let width = available_space.width.max().try_into().unwrap_or(Px::MAX); let width = available_space.width.max().try_into().unwrap_or(Px::MAX);
let prepared = self.prepared_text(context, color, width); 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 { fn summarize(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.debug_tuple("Label").field(&self.text).finish() 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 { macro_rules! impl_make_widget {

View file

@ -178,7 +178,7 @@ impl Widget for OverlayLayer {
let state = self.state.lock(); let state = self.state.lock();
for child in &state.overlays { for child in &state.overlays {
let WidgetRef::Mounted(mounted) = &child.widget else { let Some(mounted) = child.widget.as_mounted(context) else {
continue; continue;
}; };

View file

@ -92,10 +92,7 @@ impl Stack {
{ {
(child, size) (child, size)
} else { } else {
( (WidgetRef::new(widget.clone()), GridDimension::FitContent)
WidgetRef::Unmounted(widget.clone()),
GridDimension::FitContent,
)
}; };
drop(guard); drop(guard);
this.insert(index, widget.mounted(context)); this.insert(index, widget.mounted(context));

View file

@ -2,7 +2,7 @@ use std::fmt::Debug;
use figures::Size; use figures::Size;
use crate::context::{AsEventContext, LayoutContext}; use crate::context::LayoutContext;
use crate::value::{Dynamic, DynamicReader, IntoDynamic}; use crate::value::{Dynamic, DynamicReader, IntoDynamic};
use crate::widget::{WidgetInstance, WidgetRef, WrapperWidget}; use crate::widget::{WidgetInstance, WidgetRef, WrapperWidget};
use crate::ConstraintLimit; use crate::ConstraintLimit;
@ -55,10 +55,7 @@ impl WrapperWidget for Switcher {
context: &mut LayoutContext<'_, '_, '_, '_, '_>, context: &mut LayoutContext<'_, '_, '_, '_, '_>,
) -> Size<ConstraintLimit> { ) -> Size<ConstraintLimit> {
if self.source.has_updated() { if self.source.has_updated() {
let removed = std::mem::replace(&mut self.child, WidgetRef::new(self.source.get())); self.child.unmount_in(context);
if let WidgetRef::Mounted(removed) = removed {
context.remove_child(&removed);
}
} }
context.invalidate_when_changed(&self.source); context.invalidate_when_changed(&self.source);
available_space available_space

View file

@ -1,6 +1,7 @@
//! Types for displaying a [`Widget`] inside of a desktop window. //! Types for displaying a [`Widget`] inside of a desktop window.
use std::cell::RefCell; use std::cell::RefCell;
use std::collections::hash_map;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::hash::Hash; use std::hash::Hash;
use std::ops::{Deref, DerefMut, Not}; use std::ops::{Deref, DerefMut, Not};
@ -23,7 +24,7 @@ use kludgine::app::WindowBehavior as _;
use kludgine::cosmic_text::{fontdb, Family, FamilyOwned}; use kludgine::cosmic_text::{fontdb, Family, FamilyOwned};
use kludgine::render::Drawing; use kludgine::render::Drawing;
use kludgine::wgpu::CompositeAlphaMode; use kludgine::wgpu::CompositeAlphaMode;
use kludgine::Kludgine; use kludgine::{Kludgine, KludgineId};
use tracing::Level; use tracing::Level;
use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne};
@ -47,6 +48,7 @@ use crate::{initialize_tracing, ConstraintLimit};
/// A currently running Cushy window. /// A currently running Cushy window.
pub struct RunningWindow<'window> { pub struct RunningWindow<'window> {
window: kludgine::app::Window<'window, WindowCommand>, window: kludgine::app::Window<'window, WindowCommand>,
kludgine_id: KludgineId,
invalidation_status: InvalidationStatus, invalidation_status: InvalidationStatus,
cushy: Cushy, cushy: Cushy,
focused: Dynamic<bool>, focused: Dynamic<bool>,
@ -57,6 +59,7 @@ pub struct RunningWindow<'window> {
impl<'window> RunningWindow<'window> { impl<'window> RunningWindow<'window> {
pub(crate) fn new( pub(crate) fn new(
window: kludgine::app::Window<'window, WindowCommand>, window: kludgine::app::Window<'window, WindowCommand>,
kludgine_id: KludgineId,
invalidation_status: &InvalidationStatus, invalidation_status: &InvalidationStatus,
cushy: &Cushy, cushy: &Cushy,
focused: &Dynamic<bool>, focused: &Dynamic<bool>,
@ -65,6 +68,7 @@ impl<'window> RunningWindow<'window> {
) -> Self { ) -> Self {
Self { Self {
window, window,
kludgine_id,
invalidation_status: invalidation_status.clone(), invalidation_status: invalidation_status.clone(),
cushy: cushy.clone(), cushy: cushy.clone(),
focused: focused.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 /// Returns a dynamic that is updated whenever this window's focus status
/// changes. /// changes.
#[must_use] #[must_use]
@ -820,6 +832,7 @@ where
let mut behavior = T::initialize( let mut behavior = T::initialize(
&mut RunningWindow::new( &mut RunningWindow::new(
window, window,
graphics.id(),
&redraw_status, &redraw_status,
&cushy, &cushy,
&focused, &focused,
@ -886,6 +899,7 @@ where
let resizable = window.winit().is_resizable(); let resizable = window.winit().is_resizable();
let mut window = RunningWindow::new( let mut window = RunningWindow::new(
window, window,
graphics.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1012,13 +1026,14 @@ where
fn close_requested( fn close_requested(
&mut self, &mut self,
window: kludgine::app::Window<'_, WindowCommand>, window: kludgine::app::Window<'_, WindowCommand>,
_kludgine: &mut Kludgine, kludgine: &mut Kludgine,
) -> bool { ) -> bool {
Self::request_close( Self::request_close(
&mut self.should_close, &mut self.should_close,
&mut self.behavior, &mut self.behavior,
&mut RunningWindow::new( &mut RunningWindow::new(
window, window,
kludgine.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1093,6 +1108,7 @@ where
}; };
let mut window = RunningWindow::new( let mut window = RunningWindow::new(
window, window,
kludgine.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1137,6 +1153,7 @@ where
let mut window = RunningWindow::new( let mut window = RunningWindow::new(
window, window,
kludgine.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1173,6 +1190,7 @@ where
.unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget")); .unwrap_or_else(|| self.tree.widget(self.root.id()).expect("missing widget"));
let mut window = RunningWindow::new( let mut window = RunningWindow::new(
window, window,
kludgine.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1206,6 +1224,7 @@ where
let mut window = RunningWindow::new( let mut window = RunningWindow::new(
window, window,
kludgine.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1258,6 +1277,7 @@ where
if self.cursor.widget.take().is_some() { if self.cursor.widget.take().is_some() {
let mut window = RunningWindow::new( let mut window = RunningWindow::new(
window, window,
kludgine.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1288,6 +1308,7 @@ where
) { ) {
let mut window = RunningWindow::new( let mut window = RunningWindow::new(
window, window,
kludgine.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1390,7 +1411,7 @@ where
fn event( fn event(
&mut self, &mut self,
mut window: kludgine::app::Window<'_, WindowCommand>, mut window: kludgine::app::Window<'_, WindowCommand>,
_kludgine: &mut Kludgine, kludgine: &mut Kludgine,
event: WindowCommand, event: WindowCommand,
) { ) {
match event { match event {
@ -1400,6 +1421,7 @@ where
WindowCommand::RequestClose => { WindowCommand::RequestClose => {
let mut window = RunningWindow::new( let mut window = RunningWindow::new(
window, window,
kludgine.id(),
&self.redraw_status, &self.redraw_status,
&self.cushy, &self.cushy,
&self.focused, &self.focused,
@ -1741,3 +1763,46 @@ struct PendingWindowHandle {
handle: OnceLock<kludgine::app::WindowHandle<WindowCommand>>, handle: OnceLock<kludgine::app::WindowHandle<WindowCommand>>,
commands: Mutex<Vec<WindowCommand>>, commands: Mutex<Vec<WindowCommand>>,
} }
/// A collection that stores an instance of `T` per window.
///
/// This is a convenience wrapper around a `HashMap<KludgineId, T>`.
#[derive(Debug, Clone)]
pub struct WindowLocal<T> {
by_window: AHashMap<KludgineId, T>,
}
impl<T> WindowLocal<T> {
/// 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<T> {
self.by_window.remove(&context.kludgine_id())
}
}
impl<T> Default for WindowLocal<T> {
fn default() -> Self {
Self {
by_window: AHashMap::default(),
}
}
}