From 83e44912eec74047be12d630d9ccaea16e100020 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 2 Jan 2024 13:13:26 -0800 Subject: [PATCH] ReadOnly, Owned, IntoSource, more Closes #98 This finishes my initial refactoring of the dynamic system to add support for several dataflows including: - Pure data sources that can be implemented using an `Owned` at the root of a graph of `Dynamic`/`DynamicReader`s. - Read-only data sinks. I thought this would be more useful across other widgets, but in general, Progress and Label seem like the only types that this applies to currently. - The ability to mix/match Dynamic/DynamicReader in tuple-based for_each/map_each. --- CHANGELOG.md | 26 ++ examples/basic-button.rs | 11 +- examples/counter.rs | 9 +- examples/debug-window.rs | 4 +- examples/slider.rs | 7 +- src/context.rs | 30 ++- src/styles.rs | 2 +- src/utils.rs | 22 +- src/value.rs | 545 ++++++++++++++++++++++++++++++++++----- src/widget.rs | 8 +- src/widgets/grid.rs | 2 +- src/widgets/label.rs | 55 ++-- src/widgets/layers.rs | 2 +- src/widgets/progress.rs | 64 ++--- src/widgets/stack.rs | 2 +- src/widgets/tilemap.rs | 2 +- src/widgets/wrap.rs | 2 +- src/window.rs | 3 +- 18 files changed, 641 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f60a5..c1f129a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 parameter. This type tracks whether the reference is accessed using `DerefMut`, allowing `map_mut` to skip invoking change callbacks if only `Deref` is used. +- `redraw_when_changed()`/`invalidate_when_changed()` from some types have been + moved to the `Trackable` trait. This was to ensure all trackable types provide + the same API. +- `Label` has been refactored to accept any `Display` type. As a result of this, + `Label::text` is now named `display` and `Label::new()` now accepts an + `IntoReadOnly` instead of `IntoValue`. ### Fixed @@ -44,6 +50,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 are waiting for value when the last `Dynamic` is dropped. - Compatibility with Rust v1.70.0 has been restored, and continuous integration testing the MSRV has been added. +- `Progress` now utilizes `IntoSource` instead of + `IntoDynamic`. In general, this should not cause any code breakages + unless the traits were being used in generics. ### Changed @@ -52,6 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 the cache is invalidated. - All `Dynamic` mapping functions now utilize weak references, and clean up as necessary if a value is not able to be upgraded. +- `ForEach`/`MapEach`'s implementations for tuples are now defined using + `Source` and `DynamicRead`. This allows combinations of `Dynamic`s + and `DynamicReader`s to be used in for_each/map_each expressions. ### Added @@ -84,6 +96,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Source` and `Destination` are new traits that contain the reactive data model's API interface. `Dynamic` implements both traits, and `DynamicReader` implements only `Source`. +- `DynamicRead` is a new trait that provides read-only access to a dynamic's + contents. +- `IntoReadOnly` is a new trait that types can implement to convert into a + `ReadOnly`. +- `IntoReader` is a new trait that types can implement to convert into a + `DynamicReader`. +- `ReadOnly` is a type similar to `Value` but instead of possibly being a + `Dynamic`, `ReadOnly::Reader` contains a `DynamicReader`. This type can + be used where widgets that receive a value but never mutate it. +- `Owned` is a new type that can be used where no shared ownership is + necessary. This type uses a `RefCell` internally instead of an `Arc` + + `Mutex`. `Owned` implements `Source` and `Destination`. +- `GenerationalValue` now implements `Default` when `T` does. +- `Value` now implements `From>`. [99]: https://github.com/khonsulabs/cushy/issues/99 [120]: https://github.com/khonsulabs/cushy/issues/120 diff --git a/examples/basic-button.rs b/examples/basic-button.rs index 1043c59..6a33f28 100644 --- a/examples/basic-button.rs +++ b/examples/basic-button.rs @@ -1,4 +1,4 @@ -use cushy::value::{Destination, Dynamic, Source}; +use cushy::value::{Destination, Dynamic, IntoReader, Source}; use cushy::widget::MakeWidget; use cushy::Run; @@ -6,11 +6,12 @@ use cushy::Run; fn main() -> cushy::Result { // Create a dynamic usize. let count = Dynamic::new(0_isize); - // Create a dynamic that contains `count.to_string()` - let count_label = count.map_each(ToString::to_string); - // Create a new button whose text is our dynamic string. - count_label + // Create a new label displaying `count` + count + .clone() + .into_label() + // Use the label as the contents of a button .into_button() // Set the `on_click` callback to a closure that increments the counter. .on_click(move |_| count.set(count.get() + 1)) diff --git a/examples/counter.rs b/examples/counter.rs index 6a1df68..27f2d7a 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,15 +1,14 @@ -use std::string::ToString; - -use cushy::value::{Dynamic, Source}; +use cushy::value::{Dynamic, IntoReader}; use cushy::widget::MakeWidget; use cushy::Run; use figures::units::Lp; fn main() -> cushy::Result { let counter = Dynamic::new(0i32); - let label = counter.map_each(ToString::to_string); - label + counter + .clone() + .into_label() .width(Lp::points(100)) .and("+".into_button().on_click(counter.with_clone(|counter| { move |_| { diff --git a/examples/debug-window.rs b/examples/debug-window.rs index 2a45288..2d37015 100644 --- a/examples/debug-window.rs +++ b/examples/debug-window.rs @@ -1,5 +1,5 @@ use cushy::debug::DebugContext; -use cushy::value::{Destination, Dynamic, Source}; +use cushy::value::{Destination, Dynamic, IntoReader}; use cushy::widget::MakeWidget; use cushy::widgets::slider::Slidable; use cushy::{Application, Open, PendingApp}; @@ -25,7 +25,7 @@ fn main() -> cushy::Result { info.observe("Open Windows", &window_count, |window_count| { window_count - .map_each(ToString::to_string) + .into_label() .and(open_window_button.clone()) .into_columns() }); diff --git a/examples/slider.rs b/examples/slider.rs index 6b41960..559b8cb 100644 --- a/examples/slider.rs +++ b/examples/slider.rs @@ -1,5 +1,5 @@ use cushy::animation::{LinearInterpolate, PercentBetween}; -use cushy::value::{Destination, Dynamic, ForEach, Source}; +use cushy::value::{Destination, Dynamic, ForEach, IntoReader, Source}; use cushy::widget::MakeWidget; use cushy::widgets::checkbox::Checkable; use cushy::widgets::input::InputValue; @@ -31,7 +31,6 @@ fn u8_slider() -> impl MakeWidget { let max = Dynamic::new(u8::MAX); let max_text = max.linked_string(); let value = Dynamic::new(128_u8); - let value_text = value.map_each(ToString::to_string); "Min" .and(min_text.into_input()) @@ -39,8 +38,8 @@ fn u8_slider() -> impl MakeWidget { .and(max_text.into_input()) .into_columns() .centered() - .and(value.slider_between(min, max)) - .and(value_text.centered()) + .and(value.clone().slider_between(min, max)) + .and(value.into_label().centered()) .into_rows() } diff --git a/src/context.rs b/src/context.rs index 0667601..1ceda6c 100644 --- a/src/context.rs +++ b/src/context.rs @@ -969,12 +969,12 @@ impl<'context, 'window> WidgetContext<'context, 'window> { /// Ensures that this widget will be redrawn when `value` has been updated. pub fn redraw_when_changed(&self, value: &impl Trackable) { - value.redraw_when_changed(self.handle()); + value.inner_redraw_when_changed(self.handle()); } /// Ensures that this widget will be redrawn when `value` has been updated. pub fn invalidate_when_changed(&self, value: &impl Trackable) { - value.invalidate_when_changed(self.handle(), self.current_node.id()); + value.inner_invalidate_when_changed(self.handle(), self.current_node.id()); } /// Returns the last layout of this widget. @@ -1344,7 +1344,27 @@ impl Default for WidgetCacheKey { } /// A type that can be tracked to refresh or invalidate widgets. -pub trait Trackable: sealed::Trackable {} +pub trait Trackable: sealed::Trackable { + /// Marks the widget for redraw when this value is updated. + /// + /// This function has no effect if the value is constant. + fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>) + where + Self: Sized, + { + context.redraw_when_changed(self); + } + + /// Marks the widget for redraw when this value is updated. + /// + /// This function has no effect if the value is constant. + fn invalidate_when_changed(&self, context: &WidgetContext<'_, '_>) + where + Self: Sized, + { + context.invalidate_when_changed(self); + } +} impl Trackable for T where T: sealed::Trackable {} @@ -1359,8 +1379,8 @@ pub(crate) mod sealed { use crate::window::WindowHandle; pub trait Trackable { - fn redraw_when_changed(&self, handle: WindowHandle); - fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId); + fn inner_redraw_when_changed(&self, handle: WindowHandle); + fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId); } #[derive(Default, Clone)] diff --git a/src/styles.rs b/src/styles.rs index f2d9a59..14929c6 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -20,7 +20,7 @@ pub use palette::OklabHue; use palette::{IntoColor, Okhsl, Srgb}; use crate::animation::{EasingFunction, ZeroToOne}; -use crate::context::WidgetContext; +use crate::context::{Trackable, WidgetContext}; use crate::names::Name; use crate::utils::Lazy; use crate::value::{Dynamic, IntoValue, Source, Value}; diff --git a/src/utils.rs b/src/utils.rs index cbb9782..9fd9075 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -7,18 +7,32 @@ use kludgine::app::winit::event::Modifiers; use kludgine::app::winit::keyboard::ModifiersState; /// Invokes the provided macro with a pattern that can be matched using this -/// `macro_rules!` expression: `$($type:ident $field:tt $var:ident),+`, where `$type` is an -/// identifier to use for the generic parameter and `$field` is the field index -/// inside of the tuple. +/// `macro_rules!` expression: `$($type:ident $field:tt $var:ident),+`, where +/// `$type` is an identifier to use for the generic parameter and `$field` is +/// the field index inside of the tuple. +/// +/// If `impl_all_tuples!(macro_name, 2)` is provided, an additional identifier +/// will be provided before `$field`. macro_rules! impl_all_tuples { ($macro_name:ident) => { + impl_all_tuples!($macro_name, 1); + }; + ($macro_name:ident, 1) => { $macro_name!(T0 0 t0); $macro_name!(T0 0 t0, T1 1 t1); $macro_name!(T0 0 t0, T1 1 t1, T2 2 t2); $macro_name!(T0 0 t0, T1 1 t1, T2 2 t2, T3 3 t3); $macro_name!(T0 0 t0, T1 1 t1, T2 2 t2, T3 3 t3, T4 4 t4); $macro_name!(T0 0 t0, T1 1 t1, T2 2 t2, T3 3 t3, T4 4 t4, T5 5 t5); - } + }; + ($macro_name:ident, 2) => { + $macro_name!(T0 Y0 0 t0); + $macro_name!(T0 Y0 0 t0, T1 Y1 1 t1); + $macro_name!(T0 Y0 0 t0, T1 Y1 1 t1, T2 Y2 2 t2); + $macro_name!(T0 Y0 0 t0, T1 Y1 1 t1, T2 Y2 2 t2, T3 Y3 3 t3); + $macro_name!(T0 Y0 0 t0, T1 Y1 1 t1, T2 Y2 2 t2, T3 Y3 3 t3, T4 Y4 4 t4); + $macro_name!(T0 Y0 0 t0, T1 Y1 1 t1, T2 Y2 2 t2, T3 Y3 3 t3, T4 Y4 4 t4, T5 Y5 5 t5); + }; } /// Invokes a function with a clone of `self`. diff --git a/src/value.rs b/src/value.rs index 02d1d48..3cdbba7 100644 --- a/src/value.rs +++ b/src/value.rs @@ -1,5 +1,6 @@ //! Types for storing and interacting with values in Widgets. +use std::cell::{Ref, RefCell, RefMut}; use std::collections::HashMap; use std::fmt::{self, Debug, Display}; use std::future::Future; @@ -22,7 +23,7 @@ use crate::utils::{run_in_bg, IgnorePoison, WithClone}; use crate::widget::{ Children, MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance, }; -use crate::widgets::{Radio, Select, Space, Switcher}; +use crate::widgets::{Label, Radio, Select, Space, Switcher}; use crate::window::WindowHandle; /// A source of one or more `T` values. @@ -607,14 +608,14 @@ pub struct Mutable<'a, T> { #[derive(Debug)] enum Mutated<'a> { - Tracked(&'a mut bool), + External(&'a mut bool), Ignored, } impl Mutated<'_> { fn set(&mut self, mutated: bool) { match self { - Self::Tracked(value) => **value = mutated, + Self::External(value) => **value = mutated, Self::Ignored => {} } } @@ -643,7 +644,7 @@ impl<'a, T> Mutable<'a, T> { *mutated = false; Self { value, - mutated: Mutated::Tracked(mutated), + mutated: Mutated::External(mutated), } } } @@ -657,6 +658,201 @@ impl<'a, T> From<&'a mut T> for Mutable<'a, T> { } } +/// A unique, reactive value. +/// +/// This type is useful for situations where a value is owned by exactly one +/// type but needs to have reactivity through [`Source`]/[`Destination`]. +/// +/// A [`Dynamic`] utilizes a [`Arc`] + [`Mutex`] to support updating its values +/// from multiple threads. This type utilizes a [`RefCell`], preventing it from +/// being shared between multiple threads. +#[derive(Default)] +pub struct Owned { + wrapped: RefCell>, + callbacks: Arc>, +} + +impl Owned { + /// Returns a new reactive value. + pub fn new(value: T) -> Self { + Self { + wrapped: RefCell::new(GenerationalValue { + value, + generation: Generation::default(), + }), + callbacks: Arc::default(), + } + } + + /// Borrows the contents of this value with read-only access. + pub fn borrow(&self) -> OwnedRef<'_, T> { + OwnedRef(self.wrapped.borrow()) + } + + /// Borrows the contents of this value with exclusive access. + /// + /// When the returned type is accessed through [`DerefMut`], all associated + /// reactive callbacks will be invoked upon dropping the returned + /// [`OwnedMut`]. + pub fn borrow_mut(&mut self) -> OwnedMut<'_, T> { + OwnedMut { + borrowed: self.wrapped.borrow_mut(), + accessed_mut: false, + owned: self, + } + } + + /// Returns the contained value. + pub fn into_inner(self) -> T { + self.wrapped.into_inner().value + } +} + +impl Source for Owned { + fn try_map_generational( + &self, + map: impl FnOnce(&GenerationalValue) -> R, + ) -> Result { + Ok(map(&self.wrapped.borrow())) + } + + fn for_each_generational_try(&self, for_each: F) -> CallbackHandle + where + T: Send + 'static, + F: for<'a> FnMut(&'a GenerationalValue) -> Result<(), CallbackDisconnected> + + Send + + 'static, + { + let mut callbacks = self.callbacks.active.lock().ignore_poison(); + CallbackHandle(CallbackHandleInner::Single(CallbackHandleData { + id: Some(callbacks.push(Box::new(for_each))), + callbacks: self.callbacks.clone(), + })) + } + + fn for_each_generational_cloned_try(&self, mut for_each: F) -> CallbackHandle + where + T: Clone + Send + 'static, + F: FnMut(GenerationalValue) -> Result<(), CallbackDisconnected> + Send + 'static, + { + self.for_each_generational_try(move |value| for_each(value.clone())) + } +} + +impl Destination for Owned +where + T: 'static, +{ + fn try_map_mut(&self, map: impl FnOnce(Mutable<'_, T>) -> R) -> Result { + let mut updated = false; + let result = map(Mutable::new( + &mut self.wrapped.borrow_mut().value, + &mut updated, + )); + if updated { + self.callbacks.invoke(&self.wrapped.borrow()); + } + Ok(result) + } +} + +/// A read-only reference to the value in an [`Owned`]. +pub struct OwnedRef<'a, T>(Ref<'a, GenerationalValue>) +where + T: 'static; + +impl Deref for OwnedRef<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// An exclusive reference to the value contained in an [`Owned`]. +/// +/// This type tracks if the referenced value is accessed through [`DerefMut`]. +/// If it is, reactive callbacks associated with the [`Owned`] value will be +/// invoked. +pub struct OwnedMut<'a, T> +where + T: 'static, +{ + owned: &'a Owned, + borrowed: RefMut<'a, GenerationalValue>, + accessed_mut: bool, +} + +impl Deref for OwnedMut<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.borrowed.value + } +} + +impl DerefMut for OwnedMut<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.accessed_mut = true; + &mut self.borrowed.value + } +} + +impl Drop for OwnedMut<'_, T> +where + T: 'static, +{ + fn drop(&mut self) { + if self.accessed_mut { + self.owned.callbacks.invoke(&self.borrowed); + } + } +} + +struct OwnedCallbacks { + active: Mutex>>>, +} + +impl Default for OwnedCallbacks { + fn default() -> Self { + Self { + active: Mutex::default(), + } + } +} + +impl OwnedCallbacks +where + T: 'static, +{ + pub fn invoke(&self, value: &GenerationalValue) { + let mut callbacks = self.active.lock().ignore_poison(); + callbacks.drain_filter(|callback| callback.updated(value).is_err()); + } +} + +impl CallbackCollection for OwnedCallbacks +where + T: 'static, +{ + fn remove(&self, id: LotId) { + self.active.lock().ignore_poison().remove(id); + } +} + +trait OwnedCallbackFn: Send + 'static { + fn updated(&mut self, value: &GenerationalValue) -> Result<(), CallbackDisconnected>; +} + +impl OwnedCallbackFn for F +where + F: for<'a> FnMut(&'a GenerationalValue) -> Result<(), CallbackDisconnected> + Send + 'static, +{ + fn updated(&mut self, value: &GenerationalValue) -> Result<(), CallbackDisconnected> { + self(value) + } +} + /// An instance of a value that provides APIs to observe and react to its /// contents. pub struct Dynamic(Arc>); @@ -665,19 +861,7 @@ impl Dynamic { /// Creates a new instance wrapping `value`. pub fn new(value: T) -> Self { Self(Arc::new(DynamicData { - state: Mutex::new(State { - wrapped: GenerationalValue { - value, - generation: Generation::default(), - }, - callbacks: Arc::default(), - windows: AHashSet::new(), - readers: 0, - wakers: Vec::new(), - widgets: AHashSet::new(), - on_disconnect: Some(Vec::new()), - source_callback: CallbackHandle::default(), - }), + state: Mutex::new(State::new(value)), during_callback_state: Mutex::default(), sync: Condvar::default(), })) @@ -839,14 +1023,6 @@ impl Dynamic { with_clone(self.clone()) } - pub(crate) fn redraw_when_changed(&self, window: WindowHandle) { - self.0.redraw_when_changed(window); - } - - pub(crate) fn invalidate_when_changed(&self, window: WindowHandle, widget: WidgetId) { - self.0.invalidate_when_changed(window, widget); - } - /// Returns a new reference-based reader for this dynamic value. /// /// # Panics @@ -884,6 +1060,10 @@ impl Dynamic { /// thread. #[must_use] pub fn lock(&self) -> DynamicGuard<'_, T> { + self.lock_inner() + } + + fn lock_inner(&self) -> DynamicGuard<'_, T, READONLY> { DynamicGuard { guard: self.0.state().expect("deadlocked"), accessed_mut: false, @@ -1007,12 +1187,12 @@ impl MakeWidgetWithTag for Dynamic> { } impl context::sealed::Trackable for Dynamic { - fn redraw_when_changed(&self, handle: WindowHandle) { - self.redraw_when_changed(handle); + fn inner_redraw_when_changed(&self, handle: WindowHandle) { + self.0.redraw_when_changed(handle); } - fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) { - self.invalidate_when_changed(handle, id); + fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) { + self.0.invalidate_when_changed(handle, id); } } @@ -1240,6 +1420,10 @@ impl Display for DeadlockError { } } +trait CallbackCollection: Send + Sync + 'static { + fn remove(&self, id: LotId); +} + /// A handle to a callback installed on a [`Dynamic`]. When dropped, the /// callback will be uninstalled. /// @@ -1262,7 +1446,7 @@ enum CallbackHandleInner { struct CallbackHandleData { id: Option, - callbacks: Arc, + callbacks: Arc, } impl Debug for CallbackHandle { @@ -1328,8 +1512,7 @@ impl CallbackHandleData { impl Drop for CallbackHandleData { fn drop(&mut self) { if let Some(id) = self.id { - let mut data = self.callbacks.callbacks.lock().ignore_poison(); - data.callbacks.remove(id); + self.callbacks.remove(id); } } } @@ -1399,6 +1582,22 @@ struct State { } impl State { + fn new(value: T) -> Self { + Self { + wrapped: GenerationalValue { + value, + generation: Generation::default(), + }, + callbacks: Arc::default(), + windows: AHashSet::new(), + readers: 0, + wakers: Vec::new(), + widgets: AHashSet::new(), + on_disconnect: Some(Vec::new()), + source_callback: CallbackHandle::default(), + } + } + fn note_changed(&mut self) -> ChangeCallbacks { self.wrapped.generation = self.wrapped.generation.next(); @@ -1481,6 +1680,13 @@ struct ChangeCallbacksData { sync: Condvar, } +impl CallbackCollection for ChangeCallbacksData { + fn remove(&self, id: LotId) { + let mut data = self.callbacks.lock().ignore_poison(); + data.callbacks.remove(id); + } +} + struct CallbacksList { callbacks: Lots>, invoked_at: Instant, @@ -1569,7 +1775,7 @@ where } /// A value stored in a [`Dynamic`] with its [`Generation`]. -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Default, Clone, Debug, Eq, PartialEq)] pub struct GenerationalValue { /// The stored value. pub value: T, @@ -1858,11 +2064,11 @@ impl DynamicReader { } impl context::sealed::Trackable for DynamicReader { - fn redraw_when_changed(&self, handle: WindowHandle) { + fn inner_redraw_when_changed(&self, handle: WindowHandle) { self.source.redraw_when_changed(handle); } - fn invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) { + fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) { self.source.invalidate_when_changed(handle, id); } } @@ -1997,6 +2203,78 @@ impl Generation { } } +/// A type that can convert into a `ReadOnly`. +pub trait IntoReadOnly { + /// Returns `self` as a `ReadOnly`. + fn into_read_only(self) -> ReadOnly; +} + +impl IntoReadOnly for T { + fn into_read_only(self) -> ReadOnly { + ReadOnly::Constant(self) + } +} + +impl IntoReadOnly for ReadOnly { + fn into_read_only(self) -> ReadOnly { + self + } +} + +impl IntoReadOnly for Value { + fn into_read_only(self) -> ReadOnly { + match self { + Value::Constant(value) => ReadOnly::Constant(value), + Value::Dynamic(dynamic) => ReadOnly::Reader(dynamic.into_reader()), + } + } +} + +impl IntoReadOnly for Dynamic { + fn into_read_only(self) -> ReadOnly { + self.create_reader().into_read_only() + } +} + +impl IntoReadOnly for DynamicReader { + fn into_read_only(self) -> ReadOnly { + ReadOnly::Reader(self) + } +} + +impl IntoReadOnly for Owned { + fn into_read_only(self) -> ReadOnly { + ReadOnly::Constant(self.into_inner()) + } +} + +/// A type that can be converted into a [`DynamicReader`]. +pub trait IntoReader { + /// Returns this value as a reader. + fn into_reader(self) -> DynamicReader; + + /// Returns `self` being `Display`ed in a [`Label`] widget. + fn into_label(self) -> Label + where + Self: Sized, + T: Debug + Display + Send + 'static, + { + Label::new(self.into_reader()) + } +} + +impl IntoReader for Dynamic { + fn into_reader(self) -> DynamicReader { + self.into_reader() + } +} + +impl IntoReader for DynamicReader { + fn into_reader(self) -> DynamicReader { + self + } +} + /// A type that can convert into a `Dynamic`. pub trait IntoDynamic { /// Returns `self` as a dynamic. @@ -2086,6 +2364,90 @@ impl GetWidget for Vec { impl Switchable for W where W: IntoDynamic {} +/// A value that can only be read from. +pub enum ReadOnly { + /// A value that will not ever change externally. + Constant(T), + /// A value that is read from a dynamic. + Reader(DynamicReader), +} + +impl ReadOnly { + /// Returns a clone of the currently stored value. + #[must_use] + pub fn get(&self) -> T + where + T: Clone, + { + match self { + Self::Constant(value) => value.clone(), + Self::Reader(value) => value.get(), + } + } + + /// Returns the current generation of the data stored, if the contained + /// value is [`Dynamic`]. + pub fn generation(&self) -> Option { + match self { + Self::Constant(_) => None, + Self::Reader(value) => Some(value.generation()), + } + } + + /// Maps the current contents to `map` and returns the result. + pub fn map(&self, map: impl FnOnce(&T) -> R) -> R { + match self { + Self::Constant(value) => map(value), + Self::Reader(dynamic) => dynamic.map_ref(map), + } + } + + /// Returns a new value that is updated using `U::from(T.clone())` each time + /// `self` is updated. + #[must_use] + pub fn map_each(&self, mut map: F) -> ReadOnly + where + T: Send + 'static, + F: for<'a> FnMut(&'a T) -> R + Send + 'static, + R: PartialEq + Send + 'static, + { + match self { + Self::Constant(value) => ReadOnly::Constant(map(value)), + Self::Reader(dynamic) => ReadOnly::Reader(dynamic.map_each(map).into_reader()), + } + } +} + +impl From> for ReadOnly { + fn from(value: DynamicReader) -> Self { + Self::Reader(value) + } +} + +impl From> for ReadOnly { + fn from(value: Dynamic) -> Self { + Self::from(value.into_reader()) + } +} + +impl From> for ReadOnly { + fn from(value: Owned) -> Self { + Self::Constant(value.into_inner()) + } +} + +impl Debug for ReadOnly +where + T: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Constant(arg0) => Debug::fmt(arg0, f), + Self::Reader(arg0) => Debug::fmt(arg0, f), + } + } +} + /// A value that may be either constant or dynamic. pub enum Value { /// A value that will not ever change externally. @@ -2205,26 +2567,42 @@ impl Value { Value::Dynamic(value) => Some(value.generation()), } } +} - /// Marks the widget for redraw when this value is updated. - /// - /// This function has no effect if the value is constant. - pub fn redraw_when_changed(&self, context: &WidgetContext<'_, '_>) { - if let Value::Dynamic(dynamic) = self { - context.redraw_when_changed(dynamic); +impl crate::context::sealed::Trackable for ReadOnly { + fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) { + if let ReadOnly::Reader(dynamic) = self { + dynamic.inner_invalidate_when_changed(handle, id); } } - /// Marks the widget for redraw when this value is updated. - /// - /// This function has no effect if the value is constant. - pub fn invalidate_when_changed(&self, context: &WidgetContext<'_, '_>) { - if let Value::Dynamic(dynamic) = self { - context.invalidate_when_changed(dynamic); + fn inner_redraw_when_changed(&self, handle: WindowHandle) { + if let ReadOnly::Reader(dynamic) = self { + dynamic.inner_redraw_when_changed(handle); } } } +impl crate::context::sealed::Trackable for Value { + fn inner_invalidate_when_changed(&self, handle: WindowHandle, id: WidgetId) { + if let Value::Dynamic(dynamic) = self { + dynamic.inner_invalidate_when_changed(handle, id); + } + } + + fn inner_redraw_when_changed(&self, handle: WindowHandle) { + if let Value::Dynamic(dynamic) = self { + dynamic.inner_redraw_when_changed(handle); + } + } +} + +impl From> for Value { + fn from(value: Dynamic) -> Self { + Self::Dynamic(value) + } +} + impl IntoDynamic for Value { fn into_dynamic(self) -> Dynamic { match self { @@ -2285,9 +2663,9 @@ impl<'a> IntoValue for &'a str { } } -impl IntoValue for Dynamic<&'static str> { - fn into_value(self) -> Value { - self.map_each(ToString::to_string).into_value() +impl<'a> IntoReadOnly for &'a str { + fn into_read_only(self) -> ReadOnly { + ReadOnly::Constant(self.to_string()) } } @@ -2327,10 +2705,13 @@ pub trait ForEach { } macro_rules! impl_tuple_for_each { - ($($type:ident $field:tt $var:ident),+) => { - impl<$($type,)+> ForEach<($($type,)+)> for ($(&Dynamic<$type>,)+) + ($($type:ident $source:ident $field:tt $var:ident),+) => { + impl<$($type,$source,)+> ForEach<($($type,)+)> for ($(&$source,)+) where - $($type: Send + 'static,)+ + $( + $source: DynamicRead<$type> + Source<$type> + Clone + Send + 'static, + $type: Send + 'static, + )+ { type Ref<'a> = ($(&'a $type,)+); @@ -2408,7 +2789,7 @@ macro_rules! impl_tuple_for_each { ) => { $handles += $var.for_each((&$for_each, $(&$rvar,)+).with_clone(|(for_each, $($rvar,)+)| { move |$var: &$type| { - $(let $rvar = $rvar.lock();)+ + $(let $rvar = $rvar.read();)+ let mut for_each = for_each.lock().ignore_poison(); (for_each)(($(&$avar,)+)); @@ -2417,7 +2798,26 @@ macro_rules! impl_tuple_for_each { }; } -impl_all_tuples!(impl_tuple_for_each); +/// Read access to a value stored in a [`Dynamic`]. +pub trait DynamicRead { + /// Returns a guard that provides exclusive, read-only access to the value + /// contained wihtin this dynamic. + fn read(&self) -> DynamicGuard<'_, T, true>; +} + +impl DynamicRead for Dynamic { + fn read(&self) -> DynamicGuard<'_, T, true> { + self.lock_inner() + } +} + +impl DynamicRead for DynamicReader { + fn read(&self) -> DynamicGuard<'_, T, true> { + self.lock() + } +} + +impl_all_tuples!(impl_tuple_for_each, 2); /// A type that can create a `Dynamic` from a `T` passed into a mapping /// function. @@ -2435,11 +2835,14 @@ pub trait MapEach { } macro_rules! impl_tuple_map_each { - ($($type:ident $field:tt $var:ident),+) => { - impl MapEach<($($type,)+), U> for ($(&Dynamic<$type>,)+) + ($($type:ident $source:ident $field:tt $var:ident),+) => { + impl MapEach<($($type,)+), U> for ($(&$source,)+) where U: PartialEq + Send + 'static, - $($type: Send + 'static),+ + $( + $type: Send + 'static, + $source: DynamicRead<$type> + Source<$type> + Clone + Send + 'static, + )+ { type Ref<'a> = ($(&'a $type,)+); @@ -2448,7 +2851,7 @@ macro_rules! impl_tuple_map_each { F: for<'a> FnMut(Self::Ref<'a>) -> U + Send + 'static, { let dynamic = { - $(let $var = self.$field.lock();)+ + $(let $var = self.$field.read();)+ Dynamic::new(map_each(($(&$var,)+))) }; @@ -2465,7 +2868,7 @@ macro_rules! impl_tuple_map_each { }; } -impl_all_tuples!(impl_tuple_map_each); +impl_all_tuples!(impl_tuple_map_each, 2); /// A type that can have a `for_each` operation applied to it. pub trait ForEachCloned { @@ -2476,10 +2879,13 @@ pub trait ForEachCloned { } macro_rules! impl_tuple_for_each_cloned { - ($($type:ident $field:tt $var:ident),+) => { - impl<$($type,)+> ForEachCloned<($($type,)+)> for ($(&Dynamic<$type>,)+) + ($($type:ident $source:ident $field:tt $var:ident),+) => { + impl<$($type,$source,)+> ForEachCloned<($($type,)+)> for ($(&$source,)+) where - $($type: Clone + Send + 'static,)+ + $( + $type: Clone + Send + 'static, + $source: Source<$type> + Clone + Send + 'static, + )+ { #[allow(unused_mut)] @@ -2566,7 +2972,7 @@ macro_rules! impl_tuple_for_each_cloned { }; } -impl_all_tuples!(impl_tuple_for_each_cloned); +impl_all_tuples!(impl_tuple_for_each_cloned, 2); /// A type that can create a `Dynamic` from a `T` passed into a mapping /// function. @@ -2579,11 +2985,14 @@ pub trait MapEachCloned { } macro_rules! impl_tuple_map_each_cloned { - ($($type:ident $field:tt $var:ident),+) => { - impl MapEachCloned<($($type,)+), U> for ($(&Dynamic<$type>,)+) + ($($type:ident $source:ident $field:tt $var:ident),+) => { + impl MapEachCloned<($($type,)+), U> for ($(&$source,)+) where U: PartialEq + Send + 'static, - $($type: Clone + Send + 'static),+ + $( + $type: Clone + Send + 'static, + $source: Source<$type> + Clone + Send + 'static, + )+ { fn map_each_cloned(&self, mut map_each: F) -> Dynamic @@ -2608,7 +3017,7 @@ macro_rules! impl_tuple_map_each_cloned { }; } -impl_all_tuples!(impl_tuple_map_each_cloned); +impl_all_tuples!(impl_tuple_map_each_cloned, 2); /// The status of validating data. #[derive(Debug, Default, Clone, Eq, PartialEq)] diff --git a/src/widget.rs b/src/widget.rs index 9bf7589..4767a04 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -19,6 +19,7 @@ use kludgine::app::winit::window::CursorIcon; use kludgine::Color; use crate::app::{Application, Open, PendingApp, Run}; +use crate::context::sealed::Trackable as _; use crate::context::{ AsEventContext, EventContext, GraphicsContext, LayoutContext, ManageWidget, WidgetContext, }; @@ -149,8 +150,9 @@ use crate::ConstraintLimit; /// layout functions, it needs to ensure that all depended upon [`Dynamic`]s are /// tracked using one of the various /// `*_tracking_redraw()`/`*_tracking_invalidate()` functions. For example, -/// [`Dynamic::get_tracking_redraw()`] and -/// [`Dynamic::get_tracking_invalidate()`]. +/// [`Source::get_tracking_redraw()`](crate::value::Source::get_tracking_redraw) +/// and +/// [`Source::get_tracking_invalidate()`](crate::value::Source::get_tracking_invalidate). /// /// # Hover State: Hit Testing /// @@ -1564,7 +1566,7 @@ impl WidgetInstance { pub(crate) fn enabled(&self, context: &WindowHandle) -> bool { if let Value::Dynamic(dynamic) = &self.data.enabled { - dynamic.redraw_when_changed(context.clone()); + dynamic.inner_redraw_when_changed(context.clone()); } self.data.enabled.get() } diff --git a/src/widgets/grid.rs b/src/widgets/grid.rs index a4b690a..cc6bcce 100644 --- a/src/widgets/grid.rs +++ b/src/widgets/grid.rs @@ -10,7 +10,7 @@ use figures::units::{Lp, UPx}; use figures::{Fraction, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size}; use intentional::{Assert, Cast}; -use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::styles::components::IntrinsicPadding; use crate::styles::Dimension; use crate::value::{Generation, IntoValue, Value}; diff --git a/src/widgets/label.rs b/src/widgets/label.rs index b05833e..d9bc7c8 100644 --- a/src/widgets/label.rs +++ b/src/widgets/label.rs @@ -1,30 +1,37 @@ //! A read-only text widget. +use std::fmt::Write; + use figures::units::{Px, UPx}; use figures::{Point, Round, Size}; use kludgine::text::{MeasuredText, Text, TextOrigin}; use kludgine::{CanRenderTo, Color, DrawableExt}; -use crate::context::{GraphicsContext, LayoutContext}; +use crate::context::{GraphicsContext, LayoutContext, Trackable}; use crate::styles::components::TextColor; -use crate::value::{Dynamic, Generation, IntoValue, Value}; +use crate::value::{Dynamic, Generation, IntoReadOnly, ReadOnly, Value}; use crate::widget::{Widget, WidgetInstance}; use crate::window::WindowLocal; use crate::ConstraintLimit; /// A read-only text widget. #[derive(Debug)] -pub struct Label { +pub struct Label { /// The contents of the label. - pub text: Value, + pub display: ReadOnly, + displayed: String, prepared_text: WindowLocal<(MeasuredText, Option, Px, Color)>, } -impl Label { +impl Label +where + T: std::fmt::Debug + std::fmt::Display + Send + 'static, +{ /// Returns a new label that displays `text`. - pub fn new(text: impl IntoValue) -> Self { + pub fn new(text: impl IntoReadOnly) -> Self { Self { - text: text.into_value(), + display: text.into_read_only(), + displayed: String::new(), prepared_text: WindowLocal::default(), } } @@ -35,7 +42,7 @@ impl Label { color: Color, width: Px, ) -> &MeasuredText { - let check_generation = self.text.generation(); + let check_generation = self.display.generation(); match self.prepared_text.get(context) { Some((prepared, prepared_generation, prepared_width, prepared_color)) if prepared.can_render_to(&context.gfx) @@ -44,10 +51,14 @@ impl Label { && *prepared_width == width => {} _ => { context.apply_current_font_settings(); - let measured = self.text.map(|text| { + let measured = self.display.map(|text| { + self.displayed.clear(); + if let Err(err) = write!(&mut self.displayed, "{text}") { + tracing::error!("Error invoking Display: {err}"); + } context .gfx - .measure_text(Text::new(text, color).wrap_at(width)) + .measure_text(Text::new(&self.displayed, color).wrap_at(width)) }); self.prepared_text .set(context, (measured, check_generation, width, color)); @@ -61,9 +72,12 @@ impl Label { } } -impl Widget for Label { +impl Widget for Label +where + T: std::fmt::Debug + std::fmt::Display + Send + 'static, +{ fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - self.text.invalidate_when_changed(context); + self.display.invalidate_when_changed(context); let size = context.gfx.region().size; let center = Point::from(size) / 2; @@ -90,7 +104,7 @@ impl Widget for Label { } 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.display).finish() } fn unmounted(&mut self, context: &mut crate::context::EventContext<'_, '_>) { @@ -99,19 +113,20 @@ impl Widget for Label { } macro_rules! impl_make_widget { - ($($type:ty),*) => { + ($($type:ty => $kind:ty),*) => { $(impl crate::widget::MakeWidgetWithTag for $type { fn make_with_tag(self, id: crate::widget::WidgetTag) -> WidgetInstance { - Label::new(self).make_with_tag(id) + Label::<$kind>::new(self).make_with_tag(id) } })* }; } impl_make_widget!( - &'_ str, - String, - Value, - Dynamic, - Dynamic<&'static str> + &'_ str => String, + String => String, + Dynamic => String, + Dynamic<&'static str> => &'static str, + Value => String, + ReadOnly => String ); diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index ae5ab61..689c8b2 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -12,7 +12,7 @@ use intentional::Assert; use crate::animation::easings::EaseOutQuadradic; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; -use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::utils::IgnorePoison; use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value}; use crate::widget::{ diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs index f761a4e..62cca88 100644 --- a/src/widgets/progress.rs +++ b/src/widgets/progress.rs @@ -12,7 +12,7 @@ use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic}; use crate::animation::{ AnimationHandle, AnimationTarget, IntoAnimate, PercentBetween, Spawn, ZeroToOne, }; -use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, MapEach, Source, Value}; +use crate::value::{Destination, Dynamic, IntoReadOnly, IntoReader, MapEach, ReadOnly, Source}; use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance}; use crate::widgets::slider::{InactiveTrackColor, Slidable, TrackColor, TrackSize}; use crate::widgets::Data; @@ -20,7 +20,7 @@ use crate::widgets::Data; /// A bar-shaped progress indicator. #[derive(Debug)] pub struct ProgressBar { - progress: Value, + progress: ReadOnly, spinner: bool, } @@ -29,16 +29,16 @@ impl ProgressBar { #[must_use] pub const fn indeterminant() -> Self { Self { - progress: Value::Constant(Progress::Indeterminant), + progress: ReadOnly::Constant(Progress::Indeterminant), spinner: false, } } /// Returns a new progress bar that displays `progress`. #[must_use] - pub fn new(progress: impl IntoDynamic) -> Self { + pub fn new(progress: impl IntoReadOnly) -> Self { Self { - progress: Value::Dynamic(progress.into_dynamic()), + progress: progress.into_read_only(), spinner: false, } } @@ -100,7 +100,7 @@ impl MakeWidgetWithTag for ProgressBar { ); match self.progress { - Value::Dynamic(progress) => { + ReadOnly::Reader(progress) => { let callback = progress.for_each(move |progress| { update_progress_bar( *progress, @@ -112,7 +112,9 @@ impl MakeWidgetWithTag for ProgressBar { }); Data::new_wrapping((callback, progress), slider).make_widget() } - Value::Constant(_) => Data::new_wrapping(indeterminant_animation, slider).make_widget(), + ReadOnly::Constant(_) => { + Data::new_wrapping(indeterminant_animation, slider).make_widget() + } } } } @@ -193,32 +195,28 @@ fn update_progress_bar( } /// A value that can be used in a progress indicator. -pub trait Progressable: IntoDynamic + Sized +pub trait Progressable: IntoReader + Sized where T: ProgressValue + Send, { /// Returns a new progress bar that displays progress from `T::MIN` to /// `T::MAX`. fn progress_bar(self) -> ProgressBar { - ProgressBar::new( - self.into_dynamic() - .map_each(|value| value.to_progress(None)), - ) + ProgressBar::new(self.into_reader().map_each(|value| value.to_progress(None))) } /// Returns a new progress bar that displays progress from `T::MIN` to /// `max`. The maximum value can be either a `T` or an `Option`. If /// `None` is the maximum value, an indeterminant progress bar will be /// displayed. - fn progress_bar_to(self, max: impl IntoValue) -> ProgressBar + fn progress_bar_to(self, max: impl IntoReadOnly) -> ProgressBar where - T: Send, T::Value: PartialEq + Ranged + Send + Clone, { - let max = max.into_value(); + let max = max.into_read_only(); match max { - Value::Constant(max) => self.progress_bar_between(::MIN..=max), - Value::Dynamic(max) => { + ReadOnly::Constant(max) => self.progress_bar_between(::MIN..=max), + ReadOnly::Reader(max) => { self.progress_bar_between(max.map_each(|max| ::MIN..=max.clone())) } } @@ -230,26 +228,28 @@ where /// displayed. fn progress_bar_between(self, range: Range) -> ProgressBar where - T: Send, T::Value: Send, - Range: IntoValue>, + Range: IntoReadOnly>, { - let value = self.into_dynamic(); - let range = range.into_value(); - match range { - Value::Constant(range) => ProgressBar::new( - value.map_each(move |value| value.to_progress(Some(range.start()..=range.end()))), - ), - Value::Dynamic(range) => { - ProgressBar::new((&range, &value).map_each(|(range, value)| { - value.to_progress(Some(range.start()..=range.end())) - })) - } - } + let value = self.into_reader(); + let range = range.into_read_only(); + ProgressBar::new(match range { + ReadOnly::Constant(range) => value + .map_each(move |value| value.to_progress(Some(range.start()..=range.end()))) + .into_reader(), + ReadOnly::Reader(range) => (&range, &value) + .map_each(|(range, value)| value.to_progress(Some(range.start()..=range.end()))) + .into_reader(), + }) } } -impl Progressable for Dynamic where U: ProgressValue + Send {} +impl Progressable for T +where + T: IntoReader + Send, + U: ProgressValue + Send, +{ +} /// A value that can be used in a progress indicator. pub trait ProgressValue: 'static { diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index b15272e..d99fd3e 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -3,7 +3,7 @@ use figures::units::UPx; use figures::{IntoSigned, Rect, Round, ScreenScale, Size}; -use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::styles::components::IntrinsicPadding; use crate::styles::FlexibleDimension; use crate::value::{Generation, IntoValue, Value}; diff --git a/src/widgets/tilemap.rs b/src/widgets/tilemap.rs index 4737e65..9a3b3c4 100644 --- a/src/widgets/tilemap.rs +++ b/src/widgets/tilemap.rs @@ -8,7 +8,7 @@ use kludgine::app::winit::window::CursorIcon; use kludgine::tilemap; use kludgine::tilemap::TileMapFocus; -use crate::context::{EventContext, GraphicsContext, LayoutContext}; +use crate::context::{EventContext, GraphicsContext, LayoutContext, Trackable}; use crate::tick::Tick; use crate::value::{Dynamic, IntoValue, Value}; use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; diff --git a/src/widgets/wrap.rs b/src/widgets/wrap.rs index fcc9c79..111067a 100644 --- a/src/widgets/wrap.rs +++ b/src/widgets/wrap.rs @@ -5,7 +5,7 @@ use figures::units::{Px, UPx}; use figures::{IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, Zero}; use intentional::Cast; -use crate::context::{AsEventContext, GraphicsContext, LayoutContext}; +use crate::context::{AsEventContext, GraphicsContext, LayoutContext, Trackable}; use crate::styles::components::{IntrinsicPadding, LayoutOrder}; use crate::styles::{FlexibleDimension, HorizontalOrder}; use crate::value::{IntoValue, Value}; diff --git a/src/window.rs b/src/window.rs index 503ecf9..97626cb 100644 --- a/src/window.rs +++ b/src/window.rs @@ -31,7 +31,8 @@ use crate::animation::{LinearInterpolate, PercentBetween, ZeroToOne}; use crate::app::{Application, Cushy, Open, PendingApp, Run}; use crate::context::sealed::InvalidationStatus; use crate::context::{ - AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, WidgetContext, + AsEventContext, EventContext, Exclusive, GraphicsContext, LayoutContext, Trackable, + WidgetContext, }; use crate::graphics::{FontState, Graphics}; use crate::styles::{Edges, FontFamilyList, ThemePair};