From e70e92726ce345a668f955b396deb06fcdaa55b0 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 2 Jan 2024 09:00:29 -0800 Subject: [PATCH] Source + Destination (breaking) Refs #98 This refactor overhauls the reactive system to move all the reactive methods to traits. The side effect of this change is that now DynamicReader's API is the same as Dynamic's API, but because it only implements Source, DynamicReader does not offer any mutation functions. While it's unfortunate to have more traits to include to use Cushy, this seems like the best option, and it offers a path to try to integrate this into the tuple ForEach/MapEach traits. Unfortunately, my attempt at doing those in this set of changes led to issues specifying generic associated lifetimes for the DynamicGuard. But, I was also in the middle of this larger refactoring, so it might be that a fresh attempt will succeed. --- CHANGELOG.md | 11 + examples/animation.rs | 2 +- examples/background-tasks.rs | 2 +- examples/basic-button.rs | 2 +- examples/buttonception.rs | 2 +- examples/buttons.rs | 2 +- examples/checkbox.rs | 2 +- examples/contacts.rs | 2 +- examples/containers.rs | 4 +- examples/counter.rs | 2 +- examples/custom-widgets.rs | 2 +- examples/debug-window.rs | 4 +- examples/focus-order.rs | 2 +- examples/focus.rs | 2 +- examples/gameui.rs | 6 +- examples/image.rs | 2 +- examples/login.rs | 2 +- examples/multi-window.rs | 8 +- examples/slider.rs | 2 +- examples/switcher.rs | 2 +- examples/theme.rs | 16 +- examples/tic-tac-toe.rs | 2 +- examples/tilemap.rs | 4 +- examples/validation.rs | 2 +- examples/window-properties.rs | 2 +- src/animation.rs | 2 +- src/context.rs | 2 +- src/debug.rs | 2 +- src/styles.rs | 2 +- src/tick.rs | 4 +- src/value.rs | 1267 +++++++++++++++++++-------------- src/widgets/align.rs | 12 +- src/widgets/button.rs | 2 +- src/widgets/checkbox.rs | 2 +- src/widgets/collapse.rs | 2 +- src/widgets/color.rs | 2 +- src/widgets/container.rs | 2 +- src/widgets/disclose.rs | 2 +- src/widgets/image.rs | 2 +- src/widgets/input.rs | 4 +- src/widgets/layers.rs | 4 +- src/widgets/progress.rs | 2 +- src/widgets/radio.rs | 2 +- src/widgets/scroll.rs | 2 +- src/widgets/select.rs | 2 +- src/widgets/slider.rs | 6 +- src/widgets/style.rs | 12 +- src/widgets/switcher.rs | 4 +- src/widgets/validated.rs | 4 +- src/window.rs | 4 +- 50 files changed, 829 insertions(+), 611 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8cd79c..f9f60a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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. +- `Dynamic` and `DynamicReader` have had most of their functions moved + into the traits `Source` and `Destination`. This unifies the APIs + between the two types, and offers a path for other specialized reactive data + types to all share a unified API. +- `map_mut` now takes a `Mutable<'_, T>` parameter instead of an `&mut T` + parameter. This type tracks whether the reference is accessed using + `DerefMut`, allowing `map_mut` to skip invoking change callbacks if only + `Deref` is used. ### Fixed @@ -73,6 +81,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `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. +- `Source` and `Destination` are new traits that contain the reactive data + model's API interface. `Dynamic` implements both traits, and + `DynamicReader` implements only `Source`. [99]: https://github.com/khonsulabs/cushy/issues/99 [120]: https://github.com/khonsulabs/cushy/issues/120 diff --git a/examples/animation.rs b/examples/animation.rs index dd0d944..8e7c867 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -1,7 +1,7 @@ use std::time::Duration; use cushy::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn}; -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic}; use cushy::widget::MakeWidget; use cushy::widgets::progress::Progressable; use cushy::{Run, WithClone}; diff --git a/examples/background-tasks.rs b/examples/background-tasks.rs index a07c57a..885eb2b 100644 --- a/examples/background-tasks.rs +++ b/examples/background-tasks.rs @@ -4,7 +4,7 @@ use std::time::Duration; use cushy::animation::ZeroToOne; -use cushy::value::{Dynamic, Switchable}; +use cushy::value::{Destination, Dynamic, Switchable}; use cushy::widget::MakeWidget; use cushy::widgets::progress::{Progress, Progressable}; use cushy::Run; diff --git a/examples/basic-button.rs b/examples/basic-button.rs index 356be50..1043c59 100644 --- a/examples/basic-button.rs +++ b/examples/basic-button.rs @@ -1,4 +1,4 @@ -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::Run; diff --git a/examples/buttonception.rs b/examples/buttonception.rs index bd61bec..c5f8a8a 100644 --- a/examples/buttonception.rs +++ b/examples/buttonception.rs @@ -16,7 +16,7 @@ //! ┃ ┃ ┣ clicked_button Label //! ``` -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic}; use cushy::widget::MakeWidget; use cushy::widgets::button::{ButtonHoverBackground, ButtonHoverForeground}; use cushy::Run; diff --git a/examples/buttons.rs b/examples/buttons.rs index 281602a..76fe271 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -1,4 +1,4 @@ -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::button::ButtonKind; use cushy::Run; diff --git a/examples/checkbox.rs b/examples/checkbox.rs index cce6cec..eb09aea 100644 --- a/examples/checkbox.rs +++ b/examples/checkbox.rs @@ -1,4 +1,4 @@ -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::checkbox::{Checkable, CheckboxState}; use cushy::Run; diff --git a/examples/contacts.rs b/examples/contacts.rs index c5b53ab..340d64b 100644 --- a/examples/contacts.rs +++ b/examples/contacts.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use cushy::value::{Dynamic, MapEach}; +use cushy::value::{Dynamic, MapEach, Source}; use cushy::widget::{Children, MakeWidget}; use cushy::widgets::input::InputValue; use cushy::Run; diff --git a/examples/containers.rs b/examples/containers.rs index f937069..52d06d3 100644 --- a/examples/containers.rs +++ b/examples/containers.rs @@ -1,4 +1,4 @@ -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic}; use cushy::widget::{MakeWidget, WidgetInstance}; use cushy::widgets::container::ContainerShadow; use cushy::window::ThemeMode; @@ -22,7 +22,7 @@ fn set_of_containers(repeat: usize, theme_mode: Dynamic) -> WidgetIns "Toggle Theme Mode" .into_button() .on_click(move |_| { - theme_mode.map_mut(|mode| mode.toggle()); + theme_mode.map_mut(|mut mode| mode.toggle()); }) .make_widget() }; diff --git a/examples/counter.rs b/examples/counter.rs index 7131a0b..6a1df68 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,6 +1,6 @@ use std::string::ToString; -use cushy::value::Dynamic; +use cushy::value::{Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::Run; use figures::units::Lp; diff --git a/examples/custom-widgets.rs b/examples/custom-widgets.rs index 603c623..1e68df8 100644 --- a/examples/custom-widgets.rs +++ b/examples/custom-widgets.rs @@ -1,7 +1,7 @@ //! This example shows two approaches to writing custom widgets: implementing //! traits or using the [`Custom`] widget with callbacks. -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetTag, HANDLED}; use cushy::widgets::Custom; use cushy::Run; diff --git a/examples/debug-window.rs b/examples/debug-window.rs index 338fe4a..2a45288 100644 --- a/examples/debug-window.rs +++ b/examples/debug-window.rs @@ -1,5 +1,5 @@ use cushy::debug::DebugContext; -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::slider::Slidable; use cushy::{Application, Open, PendingApp}; @@ -46,7 +46,7 @@ fn open_a_window( app: &dyn Application, ) { *window_count.lock() += 1; - let window_number = total_windows.map_mut(|total| { + let window_number = total_windows.map_mut(|mut total| { *total += 1; *total }); diff --git a/examples/focus-order.rs b/examples/focus-order.rs index eb82c56..1d31113 100644 --- a/examples/focus-order.rs +++ b/examples/focus-order.rs @@ -1,6 +1,6 @@ use std::process::exit; -use cushy::value::{Dynamic, MapEach}; +use cushy::value::{Dynamic, MapEach, Source}; use cushy::widget::{MakeWidget, MakeWidgetWithTag, WidgetTag}; use cushy::widgets::grid::{Grid, GridDimension, GridWidgets}; use cushy::widgets::input::{InputValue, MaskedString}; diff --git a/examples/focus.rs b/examples/focus.rs index 0b32b95..d7fe8b6 100644 --- a/examples/focus.rs +++ b/examples/focus.rs @@ -1,4 +1,4 @@ -use cushy::value::Dynamic; +use cushy::value::{Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::input::InputValue; use cushy::widgets::slider::Slidable; diff --git a/examples/gameui.rs b/examples/gameui.rs index 814bd86..68053c1 100644 --- a/examples/gameui.rs +++ b/examples/gameui.rs @@ -1,4 +1,4 @@ -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic}; use cushy::widget::{MakeWidget, HANDLED, IGNORED}; use cushy::widgets::input::InputValue; use cushy::Run; @@ -20,8 +20,8 @@ fn main() -> cushy::Result { .and(chat_message.clone().into_input().on_key(move |input| { match (input.state, input.logical_key) { (ElementState::Pressed, Key::Named(NamedKey::Enter)) => { - let new_message = chat_message.map_mut(std::mem::take); - chat_log.map_mut(|chat_log| { + let new_message = chat_message.take(); + chat_log.map_mut(|mut chat_log| { chat_log.push_str(&new_message); chat_log.push('\n'); }); diff --git a/examples/image.rs b/examples/image.rs index c43f667..575167d 100644 --- a/examples/image.rs +++ b/examples/image.rs @@ -1,5 +1,5 @@ use cushy::animation::ZeroToOne; -use cushy::value::{Dynamic, MapEachCloned}; +use cushy::value::{Dynamic, MapEachCloned, Source}; use cushy::widget::MakeWidget; use cushy::widgets::image::{Aspect, ImageScaling}; use cushy::widgets::slider::Slidable; diff --git a/examples/login.rs b/examples/login.rs index efcdfc9..7e61eff 100644 --- a/examples/login.rs +++ b/examples/login.rs @@ -1,6 +1,6 @@ use std::process::exit; -use cushy::value::{Dynamic, Validations}; +use cushy::value::{Dynamic, Source, Validations}; use cushy::widget::MakeWidget; use cushy::widgets::input::{InputValue, MaskedString}; use cushy::widgets::layers::OverlayLayer; diff --git a/examples/multi-window.rs b/examples/multi-window.rs index add53fb..88bf6f9 100644 --- a/examples/multi-window.rs +++ b/examples/multi-window.rs @@ -1,5 +1,5 @@ use cushy::kludgine::include_texture; -use cushy::value::{Dynamic, MapEach}; +use cushy::value::{Destination, Dynamic, MapEach}; use cushy::widget::MakeWidget; use cushy::widgets::Image; use cushy::window::PendingWindow; @@ -63,13 +63,13 @@ fn open_another_window( counter: &Dynamic, texture: &LazyTexture, ) { - let my_number = counter.map_mut(|count| { + let my_number = counter.map_mut(|mut count| { *count += 1; *count }); let open_windows = open_windows.clone(); - open_windows.map_mut(|open_windows| *open_windows += 1); + open_windows.map_mut(|mut open_windows| *open_windows += 1); let window = PendingWindow::default(); let handle = window.handle(); @@ -87,7 +87,7 @@ fn open_another_window( .into_rows() .centered(), ) - .on_close(move || open_windows.map_mut(|open_windows| *open_windows -= 1)) + .on_close(move || open_windows.map_mut(|mut open_windows| *open_windows -= 1)) .open(app) .expect("error opening another window"); } diff --git a/examples/slider.rs b/examples/slider.rs index 7803c8c..6b41960 100644 --- a/examples/slider.rs +++ b/examples/slider.rs @@ -1,5 +1,5 @@ use cushy::animation::{LinearInterpolate, PercentBetween}; -use cushy::value::{Dynamic, ForEach}; +use cushy::value::{Destination, Dynamic, ForEach, Source}; use cushy::widget::MakeWidget; use cushy::widgets::checkbox::Checkable; use cushy::widgets::input::InputValue; diff --git a/examples/switcher.rs b/examples/switcher.rs index e0fe1e4..8662888 100644 --- a/examples/switcher.rs +++ b/examples/switcher.rs @@ -1,4 +1,4 @@ -use cushy::value::{Dynamic, Switchable}; +use cushy::value::{Destination, Dynamic, Switchable}; use cushy::widget::{MakeWidget, WidgetInstance}; use cushy::Run; diff --git a/examples/theme.rs b/examples/theme.rs index 2aaa71c..36f0615 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -5,7 +5,7 @@ use cushy::styles::{ ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair, }; -use cushy::value::{Dynamic, MapEachCloned}; +use cushy::value::{Destination, Dynamic, MapEachCloned, Source}; use cushy::widget::MakeWidget; use cushy::widgets::checkbox::Checkable; use cushy::widgets::color::ColorSourcePicker; @@ -218,9 +218,10 @@ fn color_editor(color: &Dynamic) -> impl MakeWidget { hue.for_each_cloned({ let color = color.clone(); move |hue| { - let mut source = color.get(); - source.hue = OklabHue::new(hue); - color.set(source); + if let Ok(mut source) = color.try_get() { + source.hue = OklabHue::new(hue); + color.set(source); + } } }) .persist(); @@ -231,9 +232,10 @@ fn color_editor(color: &Dynamic) -> impl MakeWidget { .for_each_cloned({ let color = color.clone(); move |saturation| { - let mut source = color.get(); - source.saturation = saturation; - color.set(source); + if let Ok(mut source) = color.try_get() { + source.saturation = saturation; + color.set(source); + } } }) .persist(); diff --git a/examples/tic-tac-toe.rs b/examples/tic-tac-toe.rs index 2319e49..ba09414 100644 --- a/examples/tic-tac-toe.rs +++ b/examples/tic-tac-toe.rs @@ -3,7 +3,7 @@ use std::iter; use std::ops::Not; use std::time::SystemTime; -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::button::ButtonKind; use cushy::{Run, WithClone}; diff --git a/examples/tilemap.rs b/examples/tilemap.rs index 77cbe42..b01f620 100644 --- a/examples/tilemap.rs +++ b/examples/tilemap.rs @@ -11,7 +11,7 @@ use cushy::kludgine::tilemap::{ DebugGrid, Object, ObjectLayer, TileArray, TileKind, TileMapFocus, TILE_SIZE, }; use cushy::kludgine::Color; -use cushy::value::Dynamic; +use cushy::value::{Destination, Dynamic}; use cushy::widgets::TileMap; use cushy::{Run, Tick}; use figures::FloatConversion; @@ -71,7 +71,7 @@ fn main() -> cushy::Result { let cursor_pos = input.mouse.as_ref().map(|mouse| mouse.position); - layers.map_mut(|layers| { + layers.map_mut(|mut layers| { let player = &mut layers.1[myself]; let animation_tag = match direction.x.total_cmp(&0.) { diff --git a/examples/validation.rs b/examples/validation.rs index 85f917f..d501c88 100644 --- a/examples/validation.rs +++ b/examples/validation.rs @@ -1,4 +1,4 @@ -use cushy::value::{Dynamic, Validations}; +use cushy::value::{Destination, Dynamic, Validations}; use cushy::widget::MakeWidget; use cushy::widgets::input::InputValue; use cushy::Run; diff --git a/examples/window-properties.rs b/examples/window-properties.rs index d07d1d0..20c070d 100644 --- a/examples/window-properties.rs +++ b/examples/window-properties.rs @@ -1,4 +1,4 @@ -use cushy::value::Dynamic; +use cushy::value::{Dynamic, Source}; use cushy::widget::{MakeWidget, WidgetInstance}; use cushy::Run; use figures::Size; diff --git a/src/animation.rs b/src/animation.rs index 6f8ae60..be7e974 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -57,7 +57,7 @@ use kludgine::Color; use crate::animation::easings::Linear; use crate::styles::{Component, RequireInvalidation}; use crate::utils::{run_in_bg, IgnorePoison}; -use crate::value::Dynamic; +use crate::value::{Destination, Dynamic, Source}; static ANIMATIONS: Mutex = Mutex::new(Animating::new()); static NEW_ANIMATIONS: Condvar = Condvar::new(); diff --git a/src/context.rs b/src/context.rs index 9d96ee2..0667601 100644 --- a/src/context.rs +++ b/src/context.rs @@ -19,7 +19,7 @@ use crate::styles::components::{ }; use crate::styles::{ComponentDefinition, Styles, Theme, ThemePair}; use crate::tree::Tree; -use crate::value::{IntoValue, Value}; +use crate::value::{IntoValue, Source, Value}; use crate::widget::{EventHandling, MountedWidget, RootBehavior, WidgetId, WidgetInstance}; use crate::window::{CursorState, RunningWindow, ThemeMode}; use crate::ConstraintLimit; diff --git a/src/debug.rs b/src/debug.rs index c01a063..bcf98fa 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use alot::OrderedLots; -use crate::value::{Dynamic, DynamicReader, ForEach, WeakDynamic}; +use crate::value::{Dynamic, DynamicReader, ForEach, Source, WeakDynamic}; use crate::widget::{Children, MakeWidget, WidgetInstance}; use crate::widgets::grid::{Grid, GridWidgets}; use crate::window::Window; diff --git a/src/styles.rs b/src/styles.rs index c4adb6b..f2d9a59 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -23,7 +23,7 @@ use crate::animation::{EasingFunction, ZeroToOne}; use crate::context::WidgetContext; use crate::names::Name; use crate::utils::Lazy; -use crate::value::{Dynamic, IntoValue, Value}; +use crate::value::{Dynamic, IntoValue, Source, Value}; #[macro_use] pub mod components; diff --git a/src/tick.rs b/src/tick.rs index fe757ee..daad98e 100644 --- a/src/tick.rs +++ b/src/tick.rs @@ -11,7 +11,7 @@ use kludgine::app::winit::keyboard::Key; use crate::context::WidgetContext; use crate::utils::IgnorePoison; -use crate::value::Dynamic; +use crate::value::{Destination, Dynamic}; use crate::widget::{EventHandling, HANDLED, IGNORED}; /// A fixed-rate callback that provides access to tracked input on its @@ -211,7 +211,7 @@ where // Signal that we have a new frame, which will cause the widget to // redraw. - data.tick_number.map_mut(|tick| *tick += 1); + data.tick_number.map_mut(|mut tick| *tick += 1); // Wait for a frame to be rendered. while state.keep_running { diff --git a/src/value.rs b/src/value.rs index 1cbc55c..1eaf32b 100644 --- a/src/value.rs +++ b/src/value.rs @@ -17,7 +17,7 @@ use intentional::Assert; use kempt::{Map, Sort}; use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn}; -use crate::context::{self, WidgetContext}; +use crate::context::{self, Trackable, WidgetContext}; use crate::utils::{run_in_bg, IgnorePoison, WithClone}; use crate::widget::{ Children, MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance, @@ -25,6 +25,638 @@ use crate::widget::{ use crate::widgets::{Radio, Select, Space, Switcher}; use crate::window::WindowHandle; +/// A source of one or more `T` values. +pub trait Source { + /// Maps the contents with read-only access, providing access to the value's + /// [`Generation`]. + fn try_map_generational( + &self, + map: impl FnOnce(&GenerationalValue) -> R, + ) -> Result; + + /// Maps the contents with read-only access, providing access to the value's + /// [`Generation`]. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + fn map_generational(&self, map: impl FnOnce(&GenerationalValue) -> R) -> R { + self.try_map_generational(map).expect("deadlocked") + } + + /// Returns the current generation of the value. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + fn generation(&self) -> Generation { + self.map_generational(GenerationalValue::generation) + } + + /// Maps the contents with read-only access. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + fn map_ref(&self, map: impl FnOnce(&T) -> R) -> R { + self.map_generational(|gen| map(&gen.value)) + } + + /// Returns a clone of the currently contained value. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + fn get(&self) -> T + where + T: Clone, + { + self.map_ref(T::clone) + } + + /// Maps the contents with read-only access. + fn try_map_ref(&self, map: impl FnOnce(&T) -> R) -> Result { + self.try_map_generational(|gen| map(&gen.value)) + } + + /// Returns a clone of the currently contained value. + fn try_get(&self) -> Result + where + T: Clone, + { + self.try_map_generational(|gen| gen.value.clone()) + } + + /// Returns a clone of the currently contained value. + /// + /// `context` will be invalidated when the value is updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + fn get_tracking_redraw(&self, context: &WidgetContext<'_, '_>) -> T + where + T: Clone, + Self: Trackable + Sized, + { + context.redraw_when_changed(self); + self.get() + } + + /// Returns a clone of the currently contained value. + /// + /// `context` will be invalidated when the value is updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + fn get_tracking_invalidate(&self, context: &WidgetContext<'_, '_>) -> T + where + T: Clone, + Self: Trackable + Sized, + { + context.invalidate_when_changed(self); + self.get() + } + + /// Attaches `for_each` to this value so that it is invoked each time the + /// value's contents are updated. + /// + /// Returning `Err(CallbackDisconnected)` will prevent the callback from + /// being invoked again. + fn for_each_generational_try(&self, for_each: F) -> CallbackHandle + where + T: Send + 'static, + F: for<'a> FnMut(&'a GenerationalValue) -> Result<(), CallbackDisconnected> + + Send + + 'static; + + /// Attaches `for_each` to this value and its [`Generation`] so that it is + /// invoked each time the value's contents are updated. + fn for_each_generational(&self, mut for_each: F) -> CallbackHandle + where + T: Send + 'static, + F: for<'a> FnMut(&'a GenerationalValue) + Send + 'static, + { + self.for_each_generational_try(move |value| { + for_each(value); + Ok(()) + }) + } + + /// Attaches `for_each` to this value so that it is invoked each time the + /// value's contents are updated. + /// + /// Returning `Err(CallbackDisconnected)` will prevent the callback from + /// being invoked again. + fn for_each_try(&self, mut for_each: F) -> CallbackHandle + where + T: Send + 'static, + F: for<'a> FnMut(&'a T) -> Result<(), CallbackDisconnected> + Send + 'static, + { + self.for_each_generational_try(move |gen| for_each(&gen.value)) + } + + /// Attaches `for_each` to this value so that it is invoked each time the + /// value's contents are updated. + fn for_each(&self, mut for_each: F) -> CallbackHandle + where + T: Send + 'static, + F: for<'a> FnMut(&'a T) + Send + 'static, + { + self.for_each_try(move |value| { + for_each(value); + Ok(()) + }) + } + + /// Attaches `for_each` to this value so that it is invoked each time the + /// value's contents are updated. + /// + /// Returning `Err(CallbackDisconnected)` will prevent the callback from + /// being invoked again. + fn for_each_generational_cloned_try(&self, for_each: F) -> CallbackHandle + where + T: Clone + Send + 'static, + F: FnMut(GenerationalValue) -> Result<(), CallbackDisconnected> + Send + 'static; + + /// Attaches `for_each` to this value so that it is invoked each time the + /// value's contents are updated. + fn for_each_cloned_try(&self, mut for_each: F) -> CallbackHandle + where + T: Clone + Send + 'static, + F: FnMut(T) -> Result<(), CallbackDisconnected> + Send + 'static, + { + self.for_each_generational_cloned_try(move |gen| for_each(gen.value)) + } + + /// Attaches `for_each` to this value so that it is invoked each time the + /// value's contents are updated. + fn for_each_cloned(&self, mut for_each: F) -> CallbackHandle + where + T: Clone + Send + 'static, + F: FnMut(T) + Send + 'static, + { + self.for_each_cloned_try(move |value| { + for_each(value); + Ok(()) + }) + } + + /// Returns a new dynamic that contains the updated contents of this dynamic + /// at most once every `period`. + #[must_use] + fn debounced_every(&self, period: Duration) -> Dynamic + where + T: PartialEq + Clone + Send + Sync + 'static, + { + let debounced = Dynamic::new(self.get()); + let mut debounce = Debounce::new(debounced.clone(), period); + let callback = self.for_each_cloned(move |value| debounce.update(value)); + debounced.set_source(callback); + debounced + } + + /// Returns a new dynamic that contains the updated contents of this dynamic + /// delayed by `period`. Each time this value is updated, the delay is + /// reset. + #[must_use] + fn debounced_with_delay(&self, period: Duration) -> Dynamic + where + T: PartialEq + Clone + Send + Sync + 'static, + { + let debounced = Dynamic::new(self.get()); + let mut debounce = Debounce::new(debounced.clone(), period).extending(); + let callback = self.for_each_cloned(move |value| debounce.update(value)); + debounced.set_source(callback); + debounced + } + + /// Creates a new dynamic value that contains the result of invoking `map` + /// each time this value is changed. + fn map_each_generational(&self, mut map: F) -> Dynamic + where + T: Send + 'static, + F: for<'a> FnMut(&'a GenerationalValue) -> R + Send + 'static, + R: PartialEq + Send + 'static, + { + let mapped = Dynamic::new(self.map_generational(&mut map)); + let mapped_weak = mapped.downgrade(); + mapped.set_source(self.for_each_generational_try(move |value| { + let mapped = mapped_weak.upgrade().ok_or(CallbackDisconnected)?; + mapped.set(map(value)); + Ok(()) + })); + mapped + } + + /// Creates a new dynamic value that contains the result of invoking `map` + /// each time this value is changed. + fn map_each(&self, mut map: F) -> Dynamic + where + T: Send + 'static, + F: for<'a> FnMut(&'a T) -> R + Send + 'static, + R: PartialEq + Send + 'static, + { + self.map_each_generational(move |gen| map(&gen.value)) + } + + /// Creates a new dynamic value that contains the result of invoking `map` + /// each time this value is changed. + fn map_each_cloned(&self, mut map: F) -> Dynamic + where + T: Clone + Send + 'static, + F: FnMut(T) -> R + Send + 'static, + R: PartialEq + Send + 'static, + { + let mapped = Dynamic::new(map(self.get())); + let mapped_weak = mapped.downgrade(); + mapped.set_source(self.for_each_cloned_try(move |value| { + let mapped = mapped_weak.upgrade().ok_or(CallbackDisconnected)?; + mapped.set(map(value)); + Ok(()) + })); + mapped + } + + /// Returns a new dynamic that is updated using `U::from(T.clone())` each + /// time `self` is updated. + #[must_use] + fn map_each_into(&self) -> Dynamic + where + U: PartialEq + From + Send + 'static, + T: Clone + Send + 'static, + { + self.map_each(|value| U::from(value.clone())) + } + + /// Returns a new dynamic that is updated using `U::from(&T)` each + /// time `self` is updated. + #[must_use] + fn map_each_to(&self) -> Dynamic + where + U: PartialEq + for<'a> From<&'a T> + Send + 'static, + T: Clone + Send + 'static, + { + self.map_each(|value| U::from(value)) + } +} + +/// A destination for values of type `T`. +pub trait Destination { + /// Maps the contents with exclusive access. Before returning from this + /// function, all observers will be notified that the contents have been + /// updated. + fn try_map_mut(&self, map: impl FnOnce(Mutable<'_, T>) -> R) -> Result; + + /// Maps the contents with exclusive access. Before returning from this + /// function, all observers will be notified that the contents have been + /// updated. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + fn map_mut(&self, map: impl FnOnce(Mutable<'_, T>) -> R) -> R { + self.try_map_mut(map).expect("deadlocked") + } + + /// Replaces the contents with `new_value` if `new_value` is different than + /// the currently stored value. If the value is updated, the previous + /// contents are returned. + /// + /// + /// Before returning from this function, all observers will be notified that + /// the contents have been updated. + /// + /// # Errors + /// + /// - [`ReplaceError::NoChange`]: Returned when `new_value` is equal to the + /// currently stored value. + /// - [`ReplaceError::Deadlock`]: Returned when the current thread already + /// has exclusive access to the contents of this dynamic. + fn try_replace(&self, new_value: T) -> Result> + where + T: PartialEq, + { + match self.try_map_mut(|mut value| { + if *value == new_value { + Err(ReplaceError::NoChange(new_value)) + } else { + Ok(std::mem::replace(&mut *value, new_value)) + } + }) { + Ok(old) => old, + Err(DeadlockError) => Err(ReplaceError::Deadlock), + } + } + + /// Replaces the contents with `new_value`, returning the previous contents. + /// Before returning from this function, all observers will be notified that + /// the contents have been updated. + /// + /// If the calling thread has exclusive access to the contents of this + /// dynamic, this call will return None and the value will not be updated. + /// If detecting this is important, use [`Self::try_replace()`]. + fn replace(&self, new_value: T) -> Option + where + T: PartialEq, + { + self.try_replace(new_value).ok() + } + + /// Stores `new_value` in this dynamic. Before returning from this function, + /// all observers will be notified that the contents have been updated. + /// + /// If the calling thread has exclusive access to the contents of this + /// dynamic, this call will return None and the value will not be updated. + /// If detecting this is important, use [`Self::try_replace()`]. + fn set(&self, new_value: T) + where + T: PartialEq, + { + let _old = self.replace(new_value); + } + + /// Replaces the current value with `new_value` if the current value is + /// equal to `expected_current`. + /// + /// Returns `Ok` with the overwritten value upon success. + /// + /// # Errors + /// + /// - [`TryCompareSwapError::Deadlock`]: This operation would result in a + /// thread deadlock. + /// - [`TryCompareSwapError::CurrentValueMismatch`]: The current value did + /// not match `expected_current`. The `T` returned is a clone of the + /// currently stored value. + fn try_compare_swap( + &self, + expected_current: &T, + new_value: T, + ) -> Result> + where + T: Clone + PartialEq, + { + match self.try_map_mut(|mut value| { + if &*value == expected_current { + Ok(std::mem::replace(&mut *value, new_value)) + } else { + Err(TryCompareSwapError::CurrentValueMismatch(value.clone())) + } + }) { + Ok(old) => old, + Err(_) => Err(TryCompareSwapError::Deadlock), + } + } + + /// Replaces the current value with `new_value` if the current value is + /// equal to `expected_current`. + /// + /// Returns `Ok` with the overwritten value upon success. + /// + /// # Errors + /// + /// Returns `Err` with the currently stored value when `expected_current` + /// does not match the currently stored value. + fn compare_swap(&self, expected_current: &T, new_value: T) -> Result + where + T: Clone + PartialEq, + { + match self.try_compare_swap(expected_current, new_value) { + Ok(old) => Ok(old), + Err(TryCompareSwapError::Deadlock) => unreachable!("deadlocked"), + Err(TryCompareSwapError::CurrentValueMismatch(value)) => Err(value), + } + } + + /// Updates the value to the result of invoking [`Not`] on the current + /// value. This function returns the new value. + #[allow(clippy::must_use_candidate)] + fn toggle(&self) -> T + where + T: Not + Clone, + { + self.map_mut(|mut value| { + *value = !value.clone(); + value.clone() + }) + } + + /// Returns the currently stored value, replacing the current contents with + /// `T::default()`. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + fn take(&self) -> T + where + Self: Source, + T: Default, + { + self.map_mut(|mut value| std::mem::take(&mut *value)) + } + + /// Checks if the currently stored value is different than `T::default()`, + /// and if so, returns `Some(self.take())`. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + fn take_if_not_default(&self) -> Option + where + T: Default + PartialEq, + { + let default = T::default(); + self.map_mut(|mut value| { + if *value == default { + None + } else { + Some(std::mem::replace(&mut *value, default)) + } + }) + } +} + +impl Source for Arc> { + fn try_map_generational( + &self, + map: impl FnOnce(&GenerationalValue) -> R, + ) -> Result { + let state = self.state()?; + Ok(map(&state.wrapped)) + } + + fn for_each_generational_try(&self, mut for_each: F) -> CallbackHandle + where + T: Send + 'static, + F: for<'a> FnMut(&'a GenerationalValue) -> Result<(), CallbackDisconnected> + + Send + + 'static, + { + let this = WeakDynamic(Arc::downgrade(self)); + DynamicData::for_each(self, move || { + let this = this.upgrade().ok_or(CallbackDisconnected)?; + this.map_generational(&mut for_each)?; + Ok(()) + }) + } + + fn for_each_generational_cloned_try(&self, mut for_each: F) -> CallbackHandle + where + T: Clone + Send + 'static, + F: FnMut(GenerationalValue) -> Result<(), CallbackDisconnected> + Send + 'static, + { + let this = WeakDynamic(Arc::downgrade(self)); + DynamicData::for_each(self, move || { + let this = this.upgrade().ok_or(CallbackDisconnected)?; + + if let Ok(value) = this.try_map_generational(GenerationalValue::clone) { + for_each(value)?; + } + + Ok(()) + }) + } +} + +impl Source for Dynamic { + fn try_map_generational( + &self, + map: impl FnOnce(&GenerationalValue) -> R, + ) -> Result { + self.0.try_map_generational(map) + } + + fn for_each_generational_try(&self, for_each: F) -> CallbackHandle + where + T: Send + 'static, + F: for<'a> FnMut(&'a GenerationalValue) -> Result<(), CallbackDisconnected> + + Send + + 'static, + { + self.0.for_each_generational_try(for_each) + } + + fn for_each_generational_cloned_try(&self, for_each: F) -> CallbackHandle + where + T: Clone + Send + 'static, + F: FnMut(GenerationalValue) -> Result<(), CallbackDisconnected> + Send + 'static, + { + self.0.for_each_generational_cloned_try(for_each) + } +} + +impl Source for DynamicReader { + fn try_map_generational( + &self, + map: impl FnOnce(&GenerationalValue) -> R, + ) -> Result { + self.source.try_map_generational(|generational| { + *self.read_generation.lock().ignore_poison() = generational.generation; + map(generational) + }) + } + + fn for_each_generational_try(&self, for_each: F) -> CallbackHandle + where + T: Send + 'static, + F: for<'a> FnMut(&'a GenerationalValue) -> Result<(), CallbackDisconnected> + + Send + + 'static, + { + self.source.for_each_generational_try(for_each) + } + + fn for_each_generational_cloned_try(&self, for_each: F) -> CallbackHandle + where + T: Clone + Send + 'static, + F: FnMut(GenerationalValue) -> Result<(), CallbackDisconnected> + Send + 'static, + { + self.source.for_each_generational_cloned_try(for_each) + } +} + +impl Destination for Dynamic { + fn try_map_mut(&self, map: impl FnOnce(Mutable<'_, T>) -> R) -> Result { + self.0.map_mut(map) + } +} + +/// A `mut` reference to `T` that tracks whether the contents have been accessed +/// through `DerefMut`. +#[derive(Debug)] +pub struct Mutable<'a, T> { + value: &'a mut T, + mutated: Mutated<'a>, +} + +#[derive(Debug)] +enum Mutated<'a> { + Tracked(&'a mut bool), + Ignored, +} + +impl Mutated<'_> { + fn set(&mut self, mutated: bool) { + match self { + Self::Tracked(value) => **value = mutated, + Self::Ignored => {} + } + } +} + +impl Deref for Mutable<'_, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.value + } +} + +impl DerefMut for Mutable<'_, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.mutated.set(true); + self.value + } +} + +impl<'a, T> Mutable<'a, T> { + /// Creates a new wrapper that sets `mutated` to true when `DerefMut` is + /// used to access `value`. + #[must_use] + pub fn new(value: &'a mut T, mutated: &'a mut bool) -> Self { + *mutated = false; + Self { + value, + mutated: Mutated::Tracked(mutated), + } + } +} + +impl<'a, T> From<&'a mut T> for Mutable<'a, T> { + fn from(value: &'a mut T) -> Self { + Self { + value, + mutated: Mutated::Ignored, + } + } +} + /// An instance of a value that provides APIs to observe and react to its /// contents. pub struct Dynamic(Arc>); @@ -114,27 +746,30 @@ impl Dynamic { TIntoR: FnMut(&T) -> TIntoRResult + Send + 'static, RIntoT: FnMut(&R) -> RIntoTResult + Send + 'static, { - let initial_r = self - .map_ref(|v| t_into_r(v)) - .into() - .expect("t_into_r must succeed with the current value"); - let r = Dynamic::new(initial_r); - r.with_clone(move |r| { - self.for_each(move |t| { - if let Some(update) = t_into_r(t).into() { - let _result = r.replace(update); - } - }) - .persist(); - }); + let r = Dynamic::new( + self.map_ref(|v| t_into_r(v)) + .into() + .expect("t_into_r must succeed with the current value"), + ); + let r_weak = r.downgrade(); + r.set_source(self.for_each_try(move |t| { + let r = r_weak.upgrade().ok_or(CallbackDisconnected)?; + if let Some(update) = t_into_r(t).into() { + r.set(update); + } + Ok(()) + })); - self.with_clone(|t| { - r.with_for_each(move |r| { - if let Some(update) = r_into_t(r).into() { - let _result = t.replace(update); - } - }) - }) + let t_weak = self.downgrade(); + self.set_source(r.for_each_try(move |r| { + let t = t_weak.upgrade().ok_or(CallbackDisconnected)?; + if let Some(update) = r_into_t(r).into() { + let _result = t.replace(update); + } + Ok(()) + })); + + r } /// Creates a [linked](Self::linked) dynamic containing a `String`. @@ -153,53 +788,6 @@ impl Dynamic { self.linked(ToString::to_string, |s: &String| s.parse().ok()) } - /// Maps the contents with read-only access. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - pub fn map_ref(&self, map: impl FnOnce(&T) -> R) -> R { - self.map_generational(|gen| map(&gen.value)) - } - - /// Maps the contents with read-only access, providing access to the value's - /// [`Generation`]. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - pub fn map_generational(&self, map: impl FnOnce(&GenerationalValue) -> R) -> R { - let state = self.state().expect("deadlocked"); - map(&state.wrapped) - } - - /// Maps the contents with exclusive access. Before returning from this - /// function, all observers will be notified that the contents have been - /// updated. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - pub fn map_mut(&self, map: impl FnOnce(&mut T) -> R) -> R { - self.0.map_mut(|value, _| map(value)).expect("deadlocked") - } - - /// Updates the value to the result of invoking [`Not`] on the current - /// value. This function returns the new value. - #[allow(clippy::must_use_candidate)] - pub fn toggle(&self) -> T - where - T: Not + Clone, - { - self.map_mut(|value| { - *value = !value.clone(); - value.clone() - }) - } - /// Sets the current `source` for this dynamic with `source`. /// /// A dynamic can have multiple source callbacks. @@ -210,102 +798,6 @@ impl Dynamic { self.state().assert("deadlocked").source_callback += source; } - /// Returns a new dynamic that contains the updated contents of this dynamic - /// at most once every `period`. - #[must_use] - pub fn debounced_every(&self, period: Duration) -> Self - where - T: PartialEq + Clone + Send + Sync + 'static, - { - let debounced = Dynamic::new(self.get()); - let mut debounce = Debounce::new(debounced.clone(), period); - let callback = self.for_each_cloned(move |value| debounce.update(value)); - debounced.set_source(callback); - debounced - } - - /// Returns a new dynamic that contains the updated contents of this dynamic - /// delayed by `period`. Each time this value is updated, the delay is - /// reset. - #[must_use] - pub fn debounced_with_delay(&self, period: Duration) -> Self - where - T: PartialEq + Clone + Send + Sync + 'static, - { - let debounced = Dynamic::new(self.get()); - let mut debounce = Debounce::new(debounced.clone(), period).extending(); - let callback = self.for_each_cloned(move |value| debounce.update(value)); - debounced.set_source(callback); - debounced - } - - /// Returns a new dynamic that is updated using `U::from(T.clone())` each - /// time `self` is updated. - #[must_use] - pub fn map_each_into(&self) -> Dynamic - where - U: PartialEq + From + Send + 'static, - T: Clone + Send + 'static, - { - self.map_each(|value| U::from(value.clone())) - } - - /// Returns a new dynamic that is updated using `U::from(&T)` each - /// time `self` is updated. - #[must_use] - pub fn map_each_to(&self) -> Dynamic - where - U: PartialEq + for<'a> From<&'a T> + Send + 'static, - T: Clone + Send + 'static, - { - self.map_each(|value| U::from(value)) - } - - /// Attaches `for_each` to this value so that it is invoked each time the - /// value's contents are updated. - pub fn for_each(&self, mut for_each: F) -> CallbackHandle - where - T: Send + 'static, - F: for<'a> FnMut(&'a T) + Send + 'static, - { - let this = self.downgrade(); - self.0.for_each(move || { - let this = this.upgrade().ok_or(CallbackDisconnected)?; - this.map_ref(&mut for_each); - Ok(()) - }) - } - - /// Attaches `for_each` to this value and its [`Generation`] so that it is - /// invoked each time the value's contents are updated. - pub fn for_each_generational(&self, mut for_each: F) -> CallbackHandle - where - T: Send + 'static, - F: for<'a> FnMut(&'a GenerationalValue) + Send + 'static, - { - let this = self.downgrade(); - self.0.for_each(move || { - let this = this.upgrade().ok_or(CallbackDisconnected)?; - this.map_generational(&mut for_each); - Ok(()) - }) - } - - /// Attaches `for_each` to this value so that it is invoked each time the - /// value's contents are updated. - pub fn for_each_cloned(&self, mut for_each: F) -> CallbackHandle - where - T: Clone + Send + 'static, - F: FnMut(T) + Send + 'static, - { - let this = self.downgrade(); - self.0.for_each(move || { - let this = this.upgrade().ok_or(CallbackDisconnected)?; - for_each(this.get()); - Ok(()) - }) - } - /// Attaches `for_each` to this value so that it is invoked each time the /// value's contents are updated. This function returns `self`. #[must_use] @@ -318,51 +810,6 @@ impl Dynamic { self } - /// Creates a new dynamic value that contains the result of invoking `map` - /// each time this value is changed. - pub fn map_each(&self, mut map: F) -> Dynamic - where - T: Send + 'static, - F: for<'a> FnMut(&'a T) -> R + Send + 'static, - R: PartialEq + Send + 'static, - { - let this = self.downgrade(); - self.0.map_each(move || { - let this = this.upgrade().ok_or(CallbackDisconnected)?; - Ok(this.map_ref(&mut map)) - }) - } - - /// Creates a new dynamic value that contains the result of invoking `map` - /// each time this value is changed. - pub fn map_each_generational(&self, mut map: F) -> Dynamic - where - T: Send + 'static, - F: for<'a> FnMut(&'a GenerationalValue) -> R + Send + 'static, - R: PartialEq + Send + 'static, - { - let this = self.downgrade(); - self.0.map_each(move || { - let this = this.upgrade().ok_or(CallbackDisconnected)?; - Ok(this.map_generational(&mut map)) - }) - } - - /// Creates a new dynamic value that contains the result of invoking `map` - /// each time this value is changed. - pub fn map_each_cloned(&self, mut map: F) -> Dynamic - where - T: Clone + Send + 'static, - F: FnMut(T) -> R + Send + 'static, - R: PartialEq + Send + 'static, - { - let this = self.downgrade(); - self.0.map_each(move || { - let this = this.upgrade().ok_or(CallbackDisconnected)?; - Ok(map(this.get())) - }) - } - /// A helper function that invokes `with_clone` with a clone of self. This /// code may produce slightly more readable code. /// @@ -398,201 +845,6 @@ impl Dynamic { self.0.invalidate_when_changed(window, widget); } - /// Returns a clone of the currently contained value. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn get(&self) -> T - where - T: Clone, - { - self.0.get().expect("deadlocked").value - } - - /// Returns a clone of the currently contained value. - /// - /// `context` will be invalidated when the value is updated. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn get_tracking_redraw(&self, context: &WidgetContext<'_, '_>) -> T - where - T: Clone, - { - context.redraw_when_changed(self); - self.get() - } - - /// Returns a clone of the currently contained value. - /// - /// `context` will be invalidated when the value is updated. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn get_tracking_invalidate(&self, context: &WidgetContext<'_, '_>) -> T - where - T: Clone, - { - context.invalidate_when_changed(self); - self.get() - } - - /// Returns the currently stored value, replacing the current contents with - /// `T::default()`. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn take(&self) -> T - where - T: Default, - { - std::mem::take(&mut self.lock()) - } - - /// Checks if the currently stored value is different than `T::default()`, - /// and if so, returns `Some(self.take())`. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn take_if_not_default(&self) -> Option - where - T: Default + PartialEq, - { - let default = T::default(); - let mut guard = self.lock(); - if *guard == default { - None - } else { - Some(std::mem::replace(&mut guard, default)) - } - } - - /// Replaces the contents with `new_value`, returning the previous contents. - /// Before returning from this function, all observers will be notified that - /// the contents have been updated. - /// - /// If the calling thread has exclusive access to the contents of this - /// dynamic, this call will return None and the value will not be updated. - /// If detecting this is important, use [`Self::try_replace()`]. - pub fn replace(&self, new_value: T) -> Option - where - T: PartialEq, - { - self.try_replace(new_value).ok() - } - - /// Replaces the contents with `new_value` if `new_value` is different than - /// the currently stored value. If the value is updated, the previous - /// contents are returned. - /// - /// - /// Before returning from this function, all observers will be notified that - /// the contents have been updated. - /// - /// # Errors - /// - /// - [`ReplaceError::NoChange`]: Returned when `new_value` is equal to the - /// currently stored value. - /// - [`ReplaceError::Deadlock`]: Returned when the current thread already - /// has exclusive access to the contents of this dynamic. - pub fn try_replace(&self, new_value: T) -> Result> - where - T: PartialEq, - { - match self.0.map_mut(|value, changed| { - if *value == new_value { - *changed = false; - Err(ReplaceError::NoChange(new_value)) - } else { - Ok(std::mem::replace(value, new_value)) - } - }) { - Ok(old) => old, - Err(_) => Err(ReplaceError::Deadlock), - } - } - - /// Stores `new_value` in this dynamic. Before returning from this function, - /// all observers will be notified that the contents have been updated. - /// - /// If the calling thread has exclusive access to the contents of this - /// dynamic, this call will return None and the value will not be updated. - /// If detecting this is important, use [`Self::try_replace()`]. - pub fn set(&self, new_value: T) - where - T: PartialEq, - { - let _old = self.replace(new_value); - } - - /// Replaces the current value with `new_value` if the current value is - /// equal to `expected_current`. - /// - /// Returns `Ok` with the overwritten value upon success. - /// - /// # Errors - /// - /// - [`TryCompareSwapError::Deadlock`]: This operation would result in a - /// thread deadlock. - /// - [`TryCompareSwapError::CurrentValueMismatch`]: The current value did - /// not match `expected_current`. The `T` returned is a clone of the - /// currently stored value. - pub fn try_compare_swap( - &self, - expected_current: &T, - new_value: T, - ) -> Result> - where - T: Clone + PartialEq, - { - match self.0.map_mut(|value, changed| { - if value == expected_current { - Ok(std::mem::replace(value, new_value)) - } else { - *changed = false; - Err(TryCompareSwapError::CurrentValueMismatch(value.clone())) - } - }) { - Ok(old) => old, - Err(_) => Err(TryCompareSwapError::Deadlock), - } - } - - /// Replaces the current value with `new_value` if the current value is - /// equal to `expected_current`. - /// - /// Returns `Ok` with the overwritten value upon success. - /// - /// # Errors - /// - /// Returns `Err` with the currently stored value when `expected_current` - /// does not match the currently stored value. - pub fn compare_swap(&self, expected_current: &T, new_value: T) -> Result - where - T: Clone + PartialEq, - { - match self.try_compare_swap(expected_current, new_value) { - Ok(old) => Ok(old), - Err(TryCompareSwapError::Deadlock) => unreachable!("deadlocked"), - Err(TryCompareSwapError::CurrentValueMismatch(value)) => Err(value), - } - } - /// Returns a new reference-based reader for this dynamic value. /// /// # Panics @@ -604,7 +856,7 @@ impl Dynamic { self.state().expect("deadlocked").readers += 1; DynamicReader { source: self.0.clone(), - read_generation: self.0.state().expect("deadlocked").wrapped.generation, + read_generation: Mutex::new(self.0.state().expect("deadlocked").wrapped.generation), } } @@ -641,17 +893,6 @@ impl Dynamic { self.0.state() } - /// Returns the current generation of the value. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn generation(&self) -> Generation { - self.state().expect("deadlocked").wrapped.generation - } - /// Returns a pending transition for this value to `new_value`. pub fn transition_to(&self, new_value: T) -> DynamicTransition where @@ -925,19 +1166,12 @@ impl DynamicData { state.widgets.insert((window, widget)); } - pub fn get(&self) -> Result, DeadlockError> - where - T: Clone, - { - self.state().map(|state| state.wrapped.clone()) - } - - pub fn map_mut(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result { + pub fn map_mut(&self, map: impl FnOnce(Mutable) -> R) -> Result { let mut state = self.state()?; let (old, callbacks) = { let state = &mut *state; - let mut changed = true; - let result = map(&mut state.wrapped.value, &mut changed); + let mut changed = false; + let result = map(Mutable::new(&mut state.wrapped.value, &mut changed)); let callbacks = changed.then(|| state.note_changed()); (result, callbacks) @@ -961,25 +1195,6 @@ impl DynamicData { callbacks: state.callbacks.clone(), })) } - - pub fn map_each(&self, mut map: F) -> Dynamic - where - F: for<'a> FnMut() -> Result + Send + 'static, - R: PartialEq + Send + 'static, - { - let initial_value = map().expect("initial map returned Disconnected"); - let mapped_value = Dynamic::new(initial_value); - let returned = mapped_value.clone(); - - let callback = self.for_each(move || { - mapped_value.set(map()?); - Ok(()) - }); - - returned.set_source(callback); - - returned - } } /// A callback function is no longer connected to its source. @@ -1013,7 +1228,7 @@ pub enum ReplaceError { /// Currently Cushy is only able to detect deadlocks where a single thread tries /// to lock the same [`Dynamic`] multiple times. #[derive(Debug)] -struct DeadlockError; +pub struct DeadlockError; impl std::error::Error for DeadlockError {} @@ -1410,13 +1625,13 @@ impl DerefMut for GenerationalValue { /// If the contents are accessed through [`DerefMut`], all obververs will be /// notified of a change when this guard is dropped. #[derive(Debug)] -pub struct DynamicGuard<'a, T> { +pub struct DynamicGuard<'a, T, const READONLY: bool = false> { guard: DynamicMutexGuard<'a, T>, accessed_mut: bool, prevent_notifications: bool, } -impl DynamicGuard<'_, T> { +impl DynamicGuard<'_, T, READONLY> { /// Returns the generation of the value at the time of locking the dynamic. /// /// Even if this guard accesses the data through [`DerefMut`], this value @@ -1433,7 +1648,7 @@ impl DynamicGuard<'_, T> { } } -impl<'a, T> Deref for DynamicGuard<'a, T> { +impl<'a, T, const READONLY: bool> Deref for DynamicGuard<'a, T, READONLY> { type Target = T; fn deref(&self) -> &Self::Target { @@ -1441,14 +1656,14 @@ impl<'a, T> Deref for DynamicGuard<'a, T> { } } -impl<'a, T> DerefMut for DynamicGuard<'a, T> { +impl<'a, T> DerefMut for DynamicGuard<'a, T, false> { fn deref_mut(&mut self) -> &mut Self::Target { self.accessed_mut = true; &mut self.guard.wrapped.value } } -impl Drop for DynamicGuard<'_, T> { +impl Drop for DynamicGuard<'_, T, READONLY> { fn drop(&mut self) { if self.accessed_mut && !self.prevent_notifications { let mut callbacks = Some(self.guard.note_changed()); @@ -1527,22 +1742,34 @@ impl PartialEq> for Dynamic { /// A reader that tracks the last generation accessed through this reader. pub struct DynamicReader { source: Arc>, - read_generation: Generation, + read_generation: Mutex, } impl DynamicReader { - /// Maps the contents of the dynamic value and returns the result. + /// Returns an read-only, exclusive reference to the contents of this + /// dynamic. /// - /// This function marks the currently stored value as being read. + /// This call will block until all other guards for this dynamic have been + /// dropped. /// /// # Panics /// /// This function panics if this value is already locked by the current /// thread. - pub fn map_ref(&mut self, map: impl FnOnce(&T) -> R) -> R { - let state = self.source.state().expect("deadlocked"); - self.read_generation = state.wrapped.generation; - map(&state.wrapped.value) + #[must_use] + pub fn lock(&self) -> DynamicGuard<'_, T, true> { + DynamicGuard { + guard: self.source.state().expect("deadlocked"), + accessed_mut: false, + prevent_notifications: false, + } + } + + /// Returns the current generation that has been accessed through this + /// reader. + #[must_use] + pub fn read_generation(&self) -> Generation { + *self.read_generation.lock().ignore_poison() } /// Returns true if the dynamic has been modified since the last time the @@ -1554,64 +1781,7 @@ impl DynamicReader { /// thread. #[must_use] pub fn has_updated(&self) -> bool { - self.source.state().expect("deadlocked").wrapped.generation != self.read_generation - } - - /// Returns a clone of the currently contained value. - /// - /// This function marks the currently stored value as being read. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn get(&mut self) -> T - where - T: Clone, - { - let GenerationalValue { value, generation } = self.source.get().expect("deadlocked"); - self.read_generation = generation; - value - } - - /// Returns a clone of the currently contained value. - /// - /// This function marks the currently stored value as being read. - /// - /// `context` will be invalidated when the value is updated. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn get_tracking_redraw(&mut self, context: &WidgetContext<'_, '_>) -> T - where - T: Clone, - { - self.source.redraw_when_changed(context.handle()); - self.get() - } - - /// Returns a clone of the currently contained value. - /// - /// This function marks the currently stored value as being read. - /// - /// `context` will be invalidated when the value is updated. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn get_tracking_invalidate(&mut self, context: &WidgetContext<'_, '_>) -> T - where - T: Clone, - { - self.source - .invalidate_when_changed(context.handle(), context.widget().id()); - self.get() + self.source.state().expect("deadlocked").wrapped.generation != self.read_generation() } /// Blocks the current thread until the contained value has been updated or @@ -1623,7 +1793,7 @@ impl DynamicReader { /// /// This function panics if this value is already locked by the current /// thread. - pub fn block_until_updated(&mut self) -> bool { + pub fn block_until_updated(&self) -> bool { assert!( self.source .during_callback_state @@ -1636,7 +1806,7 @@ impl DynamicReader { ); let mut state = self.source.state.lock().ignore_poison(); loop { - if state.wrapped.generation != self.read_generation { + if state.wrapped.generation != self.read_generation() { return true; } else if state.readers == Arc::strong_count(&self.source) || state.on_disconnect.is_none() @@ -1660,7 +1830,7 @@ impl DynamicReader { /// updated or there are no remaining writers for the value. /// /// Returns true if a newly updated value was discovered. - pub fn wait_until_updated(&mut self) -> BlockUntilUpdatedFuture<'_, T> { + pub fn wait_until_updated(&self) -> BlockUntilUpdatedFuture<'_, T> { BlockUntilUpdatedFuture(self) } @@ -1702,7 +1872,7 @@ where fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DynamicReader") .field("source", &DebugDynamicData(&self.source)) - .field("read_generation", &self.read_generation.0) + .field("read_generation", &self.read_generation().0) .finish() } } @@ -1712,7 +1882,7 @@ impl Clone for DynamicReader { self.source.state().expect("deadlocked").readers += 1; Self { source: self.source.clone(), - read_generation: self.read_generation, + read_generation: Mutex::new(self.read_generation()), } } } @@ -1730,14 +1900,14 @@ impl Drop for DynamicReader { /// Yeilds true if a newly updated value was discovered. #[derive(Debug)] #[must_use = "futures must be .await'ed to be executed"] -pub struct BlockUntilUpdatedFuture<'a, T>(&'a mut DynamicReader); +pub struct BlockUntilUpdatedFuture<'a, T>(&'a DynamicReader); impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> { type Output = bool; fn poll(self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { let mut state = self.0.source.state().expect("deadlocked"); - if state.wrapped.generation != self.0.read_generation { + if state.wrapped.generation != self.0.read_generation() { return Poll::Ready(true); } else if state.readers == Arc::strong_count(&self.0.source) || state.on_disconnect.is_none() @@ -1753,7 +1923,7 @@ impl<'a, T> Future for BlockUntilUpdatedFuture<'a, T> { #[test] fn disconnecting_reader_from_dynamic() { let value = Dynamic::new(1); - let mut ref_reader = value.create_reader(); + let ref_reader = value.create_reader(); drop(value); assert!(!ref_reader.block_until_updated()); } @@ -1761,9 +1931,9 @@ fn disconnecting_reader_from_dynamic() { #[test] fn disconnecting_reader_threaded() { let a = Dynamic::new(1); - let mut a_reader = a.create_reader(); + let a_reader = a.create_reader(); let b = Dynamic::new(1); - let mut b_reader = b.create_reader(); + let b_reader = b.create_reader(); let thread = std::thread::spawn(move || { b.set(2); @@ -1787,9 +1957,9 @@ fn disconnecting_reader_threaded() { #[test] fn disconnecting_reader_async() { let a = Dynamic::new(1); - let mut a_reader = a.create_reader(); + let a_reader = a.create_reader(); let b = Dynamic::new(1); - let mut b_reader = b.create_reader(); + let b_reader = b.create_reader(); let async_thread = std::thread::spawn(move || { pollster::block_on(async move { @@ -1973,9 +2143,9 @@ impl Value { } /// Maps the current contents with exclusive access and returns the result. - pub fn map_mut(&mut self, map: impl FnOnce(&mut T) -> R) -> R { + pub fn map_mut(&mut self, map: impl FnOnce(Mutable<'_, T>) -> R) -> R { match self { - Value::Constant(value) => map(value), + Value::Constant(value) => map(Mutable::from(value)), Value::Dynamic(dynamic) => dynamic.map_mut(map), } } @@ -2596,7 +2766,7 @@ impl Validations { where T: Send + 'static, { - self.invalid.map_mut(|invalid| *invalid += 1); + self.invalid.map_mut(|mut invalid| *invalid += 1); let invalid_count = self.invalid.clone(); let dynamic = dynamic.clone(); @@ -2610,9 +2780,9 @@ impl Validations { }; if invalid != new_invalid { if new_invalid { - invalid_count.map_mut(|invalid| *invalid += 1); + invalid_count.map_mut(|mut invalid| *invalid += 1); } else { - invalid_count.map_mut(|invalid| *invalid -= 1); + invalid_count.map_mut(|mut invalid| *invalid -= 1); } invalid = new_invalid; } @@ -2892,3 +3062,34 @@ fn ref_counts() { assert!(invoked.get()); assert!(!reader.connected()); } + +#[test] +fn linked_short_circuit() { + let usize = Dynamic::new(0_usize); + let string = usize.linked_string(); + + string.map_ref(|s| assert_eq!(s, "0")); + string.set(String::from("1")); + assert_eq!(usize.get(), 1); + usize.set(2); + string.map_ref(|s| assert_eq!(s, "2")); +} + +#[test] +fn graph_shortcircuit() { + let a = Dynamic::new(0_usize); + let doubled = a.map_each_cloned(|a| a * 2); + let quadrupled = doubled.map_each_cloned(|a| a * 2); + a.set_source(quadrupled.for_each_cloned({ + let a = a.clone(); + move |quad| a.set(quad / 4) + })); + + assert_eq!(a.get(), 0); + assert_eq!(quadrupled.get(), 0); + a.set(1); + assert_eq!(quadrupled.get(), 4); + quadrupled.set(16); + assert_eq!(a.get(), 4); + assert_eq!(doubled.get(), 8); +} diff --git a/src/widgets/align.rs b/src/widgets/align.rs index b3402f4..9f2219c 100644 --- a/src/widgets/align.rs +++ b/src/widgets/align.rs @@ -36,7 +36,7 @@ impl Align { #[must_use] pub fn align_left(mut self) -> Self { self.edges - .map_mut(|edges| edges.left = FlexibleDimension::ZERO); + .map_mut(|mut edges| edges.left = FlexibleDimension::ZERO); self } @@ -44,7 +44,7 @@ impl Align { #[must_use] pub fn align_top(mut self) -> Self { self.edges - .map_mut(|edges| edges.top = FlexibleDimension::ZERO); + .map_mut(|mut edges| edges.top = FlexibleDimension::ZERO); self } @@ -52,7 +52,7 @@ impl Align { #[must_use] pub fn align_bottom(mut self) -> Self { self.edges - .map_mut(|edges| edges.bottom = FlexibleDimension::ZERO); + .map_mut(|mut edges| edges.bottom = FlexibleDimension::ZERO); self } @@ -60,14 +60,14 @@ impl Align { #[must_use] pub fn align_right(mut self) -> Self { self.edges - .map_mut(|edges| edges.right = FlexibleDimension::ZERO); + .map_mut(|mut edges| edges.right = FlexibleDimension::ZERO); self } /// Sets the left and right edges of alignment to 0 and returns self. #[must_use] pub fn fit_horizontally(mut self) -> Self { - self.edges.map_mut(|edges| { + self.edges.map_mut(|mut edges| { edges.left = FlexibleDimension::ZERO; edges.right = FlexibleDimension::ZERO; }); @@ -77,7 +77,7 @@ impl Align { /// Sets the top and bottom edges of alignment to 0 and returns self. #[must_use] pub fn fit_vertically(mut self) -> Self { - self.edges.map_mut(|edges| { + self.edges.map_mut(|mut edges| { edges.top = FlexibleDimension::ZERO; edges.bottom = FlexibleDimension::ZERO; }); diff --git a/src/widgets/button.rs b/src/widgets/button.rs index aaa1506..016d0d4 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -19,7 +19,7 @@ use crate::styles::components::{ HighlightColor, IntrinsicPadding, OpaqueWidgetColor, OutlineColor, SurfaceColor, TextColor, }; use crate::styles::{ColorExt, Styles}; -use crate::value::{Dynamic, IntoValue, Value}; +use crate::value::{Destination, Dynamic, IntoValue, Source, Value}; use crate::widget::{Callback, EventHandling, MakeWidget, Widget, WidgetRef, HANDLED}; use crate::window::WindowLocal; use crate::FitMeasuredSize; diff --git a/src/widgets/checkbox.rs b/src/widgets/checkbox.rs index 8a26f9f..11f949e 100644 --- a/src/widgets/checkbox.rs +++ b/src/widgets/checkbox.rs @@ -10,7 +10,7 @@ use kludgine::shapes::{PathBuilder, Shape, StrokeOptions}; use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::{LineHeight, OutlineColor, TextColor, WidgetAccentColor}; use crate::styles::Dimension; -use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; +use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Source, Value}; use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance}; use crate::widgets::button::ButtonKind; use crate::ConstraintLimit; diff --git a/src/widgets/collapse.rs b/src/widgets/collapse.rs index 3027d00..d89caac 100644 --- a/src/widgets/collapse.rs +++ b/src/widgets/collapse.rs @@ -6,7 +6,7 @@ use figures::Size; use crate::animation::easings::{EaseInQuadradic, EaseOutQuadradic}; use crate::animation::{AnimationHandle, AnimationTarget, EasingFunction, Spawn}; use crate::context::LayoutContext; -use crate::value::{Dynamic, IntoDynamic}; +use crate::value::{Dynamic, IntoDynamic, Source}; use crate::widget::{MakeWidget, WidgetRef, WrappedLayout, WrapperWidget}; use crate::ConstraintLimit; diff --git a/src/widgets/color.rs b/src/widgets/color.rs index 5db525b..624b5d6 100644 --- a/src/widgets/color.rs +++ b/src/widgets/color.rs @@ -12,7 +12,7 @@ use crate::animation::ZeroToOne; use crate::context::{EventContext, GraphicsContext}; use crate::styles::components::{HighlightColor, OutlineColor, TextColor}; use crate::styles::{ColorExt, ColorSource}; -use crate::value::{Dynamic, IntoValue, Value}; +use crate::value::{Destination, Dynamic, IntoValue, Source, Value}; use crate::widget::{EventHandling, Widget, HANDLED}; /// A widget that selects a [`ColorSource`]. diff --git a/src/widgets/container.rs b/src/widgets/container.rs index 2e7555f..254503e 100644 --- a/src/widgets/container.rs +++ b/src/widgets/container.rs @@ -10,7 +10,7 @@ use kludgine::Color; use crate::context::{EventContext, GraphicsContext, LayoutContext, WidgetContext}; use crate::styles::components::{CornerRadius, IntrinsicPadding, Opacity, SurfaceColor}; use crate::styles::{Component, ContainerLevel, Dimension, Edges, RequireInvalidation, Styles}; -use crate::value::{Dynamic, IntoValue, Value}; +use crate::value::{Dynamic, IntoValue, Source, Value}; use crate::widget::{MakeWidget, RootBehavior, Widget, WidgetInstance, WidgetRef}; use crate::ConstraintLimit; diff --git a/src/widgets/disclose.rs b/src/widgets/disclose.rs index 0e8204a..321ca26 100644 --- a/src/widgets/disclose.rs +++ b/src/widgets/disclose.rs @@ -13,7 +13,7 @@ use crate::animation::{AnimationHandle, AnimationTarget, Spawn}; use crate::context::{EventContext, LayoutContext}; use crate::styles::components::{HighlightColor, IntrinsicPadding, LineHeight, OutlineColor}; use crate::styles::Dimension; -use crate::value::{Dynamic, IntoDynamic, IntoValue, Value}; +use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value}; use crate::widget::{ EventHandling, MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance, WidgetRef, WidgetTag, HANDLED, IGNORED, diff --git a/src/widgets/image.rs b/src/widgets/image.rs index 76174a5..eeaaa9d 100644 --- a/src/widgets/image.rs +++ b/src/widgets/image.rs @@ -6,7 +6,7 @@ use kludgine::{AnyTexture, CollectedTexture, LazyTexture, SharedTexture, Texture use crate::animation::ZeroToOne; use crate::context::LayoutContext; -use crate::value::{IntoValue, Value}; +use crate::value::{IntoValue, Source, Value}; use crate::widget::Widget; use crate::ConstraintLimit; diff --git a/src/widgets/input.rs b/src/widgets/input.rs index 67f66af..bcfbc93 100644 --- a/src/widgets/input.rs +++ b/src/widgets/input.rs @@ -25,7 +25,7 @@ use zeroize::Zeroizing; use crate::context::{EventContext, GraphicsContext, LayoutContext}; use crate::styles::components::{HighlightColor, IntrinsicPadding, OutlineColor, TextColor}; use crate::utils::ModifiersExt; -use crate::value::{Dynamic, Generation, IntoDynamic, IntoValue, Value}; +use crate::value::{Destination, Dynamic, Generation, IntoDynamic, IntoValue, Source, Value}; use crate::widget::{Callback, EventHandling, Widget, HANDLED, IGNORED}; use crate::{ConstraintLimit, Lazy}; @@ -189,7 +189,7 @@ where } fn replace_range(&mut self, start: Cursor, end: Cursor, new_text: &str) { - self.value.map_mut(|value| { + self.value.map_mut(|mut value| { let value = value.as_string_mut(); let start = start.offset.min(value.len().saturating_sub(1)); let end = end.offset.min(value.len()); diff --git a/src/widgets/layers.rs b/src/widgets/layers.rs index 03bfb0d..ae5ab61 100644 --- a/src/widgets/layers.rs +++ b/src/widgets/layers.rs @@ -14,7 +14,7 @@ use crate::animation::easings::EaseOutQuadradic; use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, ZeroToOne}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::utils::IgnorePoison; -use crate::value::{Dynamic, DynamicGuard, IntoValue, Value}; +use crate::value::{Destination, Dynamic, DynamicGuard, IntoValue, Source, Value}; use crate::widget::{ Callback, Children, MakeWidget, MountedChildren, MountedWidget, Widget, WidgetId, WidgetRef, WrapperWidget, @@ -593,7 +593,7 @@ impl OverlayBuilder<'_> { #[must_use] pub fn show(self) -> OverlayHandle { self.fade_in(); - self.overlay.state.map_mut(|state| { + self.overlay.state.map_mut(|mut state| { state.new_overlays += 1; OverlayHandle { state: self.overlay.state.clone(), diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs index 85225eb..f761a4e 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::{Dynamic, IntoDynamic, IntoValue, MapEach, Value}; +use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, MapEach, Source, Value}; use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance}; use crate::widgets::slider::{InactiveTrackColor, Slidable, TrackColor, TrackSize}; use crate::widgets::Data; diff --git a/src/widgets/radio.rs b/src/widgets/radio.rs index eae447f..48b64b5 100644 --- a/src/widgets/radio.rs +++ b/src/widgets/radio.rs @@ -9,7 +9,7 @@ use kludgine::DrawableExt; use crate::context::{GraphicsContext, LayoutContext}; use crate::styles::components::{LineHeight, OutlineColor, WidgetAccentColor}; use crate::styles::Dimension; -use crate::value::{Dynamic, DynamicReader, IntoDynamic, IntoValue, Value}; +use crate::value::{Destination, Dynamic, DynamicReader, IntoDynamic, IntoValue, Source, Value}; use crate::widget::{MakeWidget, MakeWidgetWithTag, Widget, WidgetInstance}; use crate::widgets::button::ButtonKind; use crate::ConstraintLimit; diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index 9c316fa..153cad8 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -13,7 +13,7 @@ use crate::animation::{AnimationHandle, AnimationTarget, IntoAnimate, Spawn, Zer use crate::context::{AsEventContext, EventContext, LayoutContext}; use crate::styles::components::{EasingIn, EasingOut, LineHeight}; use crate::styles::Dimension; -use crate::value::Dynamic; +use crate::value::{Destination, Dynamic, Source}; use crate::widget::{EventHandling, MakeWidget, Widget, WidgetRef, HANDLED, IGNORED}; use crate::ConstraintLimit; diff --git a/src/widgets/select.rs b/src/widgets/select.rs index 7a53daa..4b608d9 100644 --- a/src/widgets/select.rs +++ b/src/widgets/select.rs @@ -5,7 +5,7 @@ use kludgine::Color; use crate::styles::components::OutlineColor; use crate::styles::{Component, DynamicComponent}; -use crate::value::{Dynamic, IntoDynamic, IntoValue, MapEach, Value}; +use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, MapEach, Source, Value}; use crate::widget::{MakeWidget, MakeWidgetWithTag, WidgetInstance}; use crate::widgets::button::{ButtonBackground, ButtonHoverBackground, ButtonKind}; diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index 2c3de1d..b720620 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -19,7 +19,7 @@ use crate::styles::components::{ WidgetAccentColor, }; use crate::styles::{Dimension, HorizontalOrder, VerticalOrder, VisualOrder}; -use crate::value::{Dynamic, IntoDynamic, IntoValue, Value}; +use crate::value::{Destination, Dynamic, IntoDynamic, IntoValue, Source, Value}; use crate::widget::{EventHandling, Widget, HANDLED, IGNORED}; use crate::ConstraintLimit; @@ -445,7 +445,7 @@ where let mut max = self.maximum.get_tracking_redraw(context); if max < min { - self.maximum.map_mut(|max| *max = min.clone()); + self.maximum.map_mut(|mut max| *max = min.clone()); max = min.clone(); } let mut value_clamped = false; @@ -472,7 +472,7 @@ where if value_clamped { self.value - .map_mut(|v| *v = T::from_parts(start_value.clone(), end_value.clone())); + .map_mut(|mut v| *v = T::from_parts(start_value.clone(), end_value.clone())); } let start_percent = start_value.percent_between(&min, &max); diff --git a/src/widgets/style.rs b/src/widgets/style.rs index a40867a..e423d0a 100644 --- a/src/widgets/style.rs +++ b/src/widgets/style.rs @@ -11,7 +11,7 @@ use crate::styles::components::{ use crate::styles::{ ComponentDefinition, IntoComponentValue, IntoDynamicComponentValue, StoredComponent, Styles, }; -use crate::value::{IntoValue, Value}; +use crate::value::{Destination, IntoValue, Mutable, Value}; use crate::widget::{MakeWidget, WidgetRef, WrapperWidget}; /// A widget that applies a set of [`Styles`] to all contained widgets. @@ -31,9 +31,9 @@ impl Style { } } - fn map_styles_mut(&mut self, map: impl FnOnce(&mut Styles) -> R) -> R { + fn map_styles_mut(&mut self, map: impl FnOnce(Mutable<'_, Styles>) -> R) -> R { match &mut self.styles { - Value::Constant(styles) => map(styles), + Value::Constant(styles) => map(Mutable::from(styles)), Value::Dynamic(dynamic) => dynamic.map_mut(map), } } @@ -48,7 +48,7 @@ impl Style { where Value: IntoComponentValue, { - self.map_styles_mut(|styles| { + self.map_styles_mut(|mut styles| { styles.insert(name, component.into_value()); }); self @@ -64,7 +64,7 @@ impl Style { where Value: IntoComponentValue, { - self.map_styles_mut(|styles| { + self.map_styles_mut(|mut styles| { styles.insert_named( name.name().into_owned(), StoredComponent::local(component.into_value()), @@ -84,7 +84,7 @@ impl Style { where Value: IntoComponentValue, { - self.map_styles_mut(|styles| { + self.map_styles_mut(|mut styles| { styles.insert_dynamic(name, dynamic); }); self diff --git a/src/widgets/switcher.rs b/src/widgets/switcher.rs index 422e1b6..a412123 100644 --- a/src/widgets/switcher.rs +++ b/src/widgets/switcher.rs @@ -3,7 +3,7 @@ use std::fmt::Debug; use figures::Size; use crate::context::LayoutContext; -use crate::value::{Dynamic, DynamicReader, IntoDynamic}; +use crate::value::{Dynamic, DynamicReader, IntoDynamic, Source}; use crate::widget::{WidgetInstance, WidgetRef, WrapperWidget}; use crate::ConstraintLimit; @@ -37,7 +37,7 @@ impl Switcher { /// `widget_factory` each time `value` changes. #[must_use] pub fn new(source: impl IntoDynamic) -> Self { - let mut source = source.into_dynamic().into_reader(); + let source = source.into_dynamic().into_reader(); let child = WidgetRef::new(source.get()); Self { source, child } } diff --git a/src/widgets/validated.rs b/src/widgets/validated.rs index a7df260..e589a74 100644 --- a/src/widgets/validated.rs +++ b/src/widgets/validated.rs @@ -8,7 +8,9 @@ use crate::styles::components::{ ErrorColor, LineHeight, LineHeight2, OutlineColor, TextColor, TextSize, TextSize2, }; use crate::styles::Dimension; -use crate::value::{Dynamic, IntoDynamic, IntoValue, MapEach, Validation, Value}; +use crate::value::{ + Destination, Dynamic, IntoDynamic, IntoValue, MapEach, Source, Validation, Value, +}; use crate::widget::{MakeWidget, MakeWidgetWithTag, WidgetInstance, WidgetRef, WrapperWidget}; /// A widget that displays validation information around another widget. diff --git a/src/window.rs b/src/window.rs index 29290bd..503ecf9 100644 --- a/src/window.rs +++ b/src/window.rs @@ -37,7 +37,9 @@ use crate::graphics::{FontState, Graphics}; use crate::styles::{Edges, FontFamilyList, ThemePair}; use crate::tree::Tree; use crate::utils::ModifiersExt; -use crate::value::{Dynamic, DynamicReader, Generation, IntoDynamic, IntoValue, Value}; +use crate::value::{ + Destination, Dynamic, DynamicReader, Generation, IntoDynamic, IntoValue, Source, Value, +}; use crate::widget::{ EventHandling, MakeWidget, MountedWidget, OnceCallback, RootBehavior, Widget, WidgetId, WidgetInstance, HANDLED, IGNORED,