diff --git a/.rustme/docs.md b/.rustme/docs.md index 2543870..0a32b17 100644 --- a/.rustme/docs.md +++ b/.rustme/docs.md @@ -3,9 +3,16 @@ [![Documentation for `main` branch](https://img.shields.io/badge/docs-main-informational)]($docs$) Gooey is an experimental Graphical User Interface (GUI) crate for the Rust -programming language. It is built using [`Kludgine`][kludgine], which is powered -by [`winit`][winit] and [`wgpu`][wgpu]. It is incredibly early in development, -and is being developed for a game that will hopefully be developed shortly. +programming language. It is powered by: + +- [`Kludgine`][kludgine], a 2d graphics library powered by: + - [`winit`][winit] for windowing/input + - [`wgpu`][wgpu] for graphics + - [`cosmic_text`][cosmic_text] +- [`palette`][palette] +- [`arboard`][arboard] + +## Getting Started with Gooey The [`Widget`][widget] trait is the building block of Gooey: Every user interface element implements `Widget`. A full list of built-in widgets can be @@ -19,9 +26,25 @@ increments its own label: $../examples/basic-button.rs:readme$ ``` +A great way to learn more about Gooey is to explore the [examples +directory][examples]. Nearly every feature in Gooey was initially tested by +creating an example. + +## Project Status + +This project is early in development, but is quickly becoming a decent +framework. It is considered experimental and unspported at this time, and the +primary focus for [@ecton][ecton] is to use this for his own projects. Feature +requests and bug fixes will be prioritized based on @ecton's own needs. + [widget]: $widget$ [kludgine]: https://github.com/khonsulabs/kludgine [wgpu]: https://github.com/gfx-rs/wgpu [winit]: https://github.com/rust-windowing/winit [widgets]: $widgets$ [button-example]: https://github.com/khonsulabs/gooey/tree/$ref-name$/examples/basic-button.rs +[examples]: https://github.com/khonsulabs/gooey/tree/$ref-name$/examples/ +[cosmic_text]: https://github.com/pop-os/cosmic-text +[palette]: https://github.com/Ogeon/palette +[arboard]: https://github.com/1Password/arboard +[ecton]: https://github.com/khonsulabs/ecton diff --git a/examples/checkbox.rs b/examples/checkbox.rs index 48195d9..9108f5a 100644 --- a/examples/checkbox.rs +++ b/examples/checkbox.rs @@ -11,7 +11,7 @@ fn main() -> gooey::Result { .clone() .into_checkbox(label) .and("Maybe".into_button().on_click(move |()| { - checkbox_state.update(CheckboxState::Indeterminant); + checkbox_state.set(CheckboxState::Indeterminant); })) .into_columns() .centered() diff --git a/examples/slider.rs b/examples/slider.rs index 8ce52df..ac77310 100644 --- a/examples/slider.rs +++ b/examples/slider.rs @@ -46,12 +46,12 @@ fn u8_slider() -> impl MakeWidget { fn u8_range_slider() -> impl MakeWidget { let range = Dynamic::new(42..=127); - let start = range.map_each_unique(|range| *range.start()); - let end = range.map_each_unique(|range| *range.end()); + let start = range.map_each(|range| *range.start()); + let end = range.map_each(|range| *range.end()); (&start, &end).for_each({ let range = range.clone(); move |(start, end)| { - let _result = range.try_update(*start..=*end); + range.set(*start..=*end); } }); diff --git a/examples/switcher.rs b/examples/switcher.rs index 0d35052..7a0cd08 100644 --- a/examples/switcher.rs +++ b/examples/switcher.rs @@ -2,7 +2,7 @@ use gooey::value::{Dynamic, Switchable}; use gooey::widget::{MakeWidget, WidgetInstance}; use gooey::Run; -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] enum ActiveContent { Intro, Success, diff --git a/examples/theme.rs b/examples/theme.rs index c29fdb7..515f679 100644 --- a/examples/theme.rs +++ b/examples/theme.rs @@ -1,73 +1,153 @@ use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::{ - ColorScheme, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, ThemePair, + ColorScheme, ColorSchemeBuilder, ColorSource, ColorTheme, FixedTheme, SurfaceTheme, Theme, + ThemePair, }; -use gooey::value::{Dynamic, MapEach}; +use gooey::value::{Dynamic, MapEachCloned}; use gooey::widget::MakeWidget; +use gooey::widgets::checkbox::Checkable; use gooey::widgets::input::InputValue; use gooey::widgets::slider::Slidable; +use gooey::widgets::Space; use gooey::window::ThemeMode; use gooey::Run; +use kludgine::figures::units::Lp; use kludgine::Color; +use palette::OklabHue; + +struct Scheme { + primary: Primary, + secondary: Other, + tertiary: Other, + error: Other, + neutral: Other, + neutral_variant: Other, +} + +impl From for Scheme { + fn from(scheme: ColorScheme) -> Self { + Self { + primary: scheme.primary, + secondary: scheme.secondary, + tertiary: scheme.tertiary, + error: scheme.error, + neutral: scheme.neutral, + neutral_variant: scheme.neutral_variant, + } + } +} + +impl Scheme { + pub fn map(&self, mut map: impl FnMut(T) -> R) -> Scheme + where + T: Clone, + { + Scheme { + primary: map(self.primary.clone()), + secondary: map(self.secondary.clone()), + tertiary: map(self.tertiary.clone()), + error: map(self.error.clone()), + neutral: map(self.neutral.clone()), + neutral_variant: map(self.neutral_variant.clone()), + } + } +} + +impl Scheme { + pub fn map_labeled( + &self, + primary: impl FnOnce(Primary) -> NewPrimary, + mut map: impl FnMut(&str, Other) -> NewOther, + ) -> Scheme + where + Primary: Clone, + Other: Clone, + { + Scheme { + primary: primary(self.primary.clone()), + secondary: map("Secondary", self.secondary.clone()), + tertiary: map("Tertiary", self.tertiary.clone()), + error: map("Error", self.error.clone()), + neutral: map("Netural", self.neutral.clone()), + neutral_variant: map("Neutral Variant", self.neutral_variant.clone()), + } + } +} fn main() -> gooey::Result { - let scheme = ColorScheme::default(); - let (primary, primary_editor) = color_editor(scheme.primary, "Primary"); - let (secondary, secondary_editor) = color_editor(scheme.secondary, "Secondary"); - let (tertiary, tertiary_editor) = color_editor(scheme.tertiary, "Tertiary"); - let (error, error_editor) = color_editor(scheme.error, "Error"); - let (neutral, neutral_editor) = color_editor(scheme.neutral, "Neutral"); - let (neutral_variant, neutral_variant_editor) = - color_editor(scheme.neutral_variant, "Neutral Variant"); - let (theme_mode, theme_switcher) = dark_mode_slider(); + let (theme_mode, theme_switcher) = dark_mode_picker(); - let default_theme = ( - &primary, - &secondary, - &tertiary, - &error, - &neutral, - &neutral_variant, + let scheme = Scheme::from(ColorScheme::default()); + let sources = scheme.map(Dynamic::new); + let editors = sources.map_labeled( + |primary| { + swatch_label("Primary", &primary) + .and(color_editor(&primary)) + .into_rows() + .make_widget() + }, + |label, source| { + let (enabled, editor) = optional_editor(label, &source); + let opt_color = + (&enabled, &source).map_each_cloned(|(enabled, source)| enabled.then_some(source)); + (opt_color, editor) + }, + ); + let color_scheme = ( + &sources.primary, + &editors.secondary.0, + &editors.tertiary.0, + &editors.error.0, + &editors.neutral.0, + &editors.neutral_variant.0, ) - .map_each( - |(primary, secondary, tertiary, error, neutral, neutral_variant)| { - ThemePair::from(ColorScheme { - primary: *primary, - secondary: *secondary, - tertiary: *tertiary, - error: *error, - neutral: *neutral, - neutral_variant: *neutral_variant, - }) + .map_each_cloned( + move |(primary, secondary, tertiary, error, neutral, neutral_variant)| { + let mut scheme = ColorSchemeBuilder::new(primary); + scheme.secondary = secondary; + scheme.tertiary = tertiary; + scheme.error = error; + scheme.neutral = neutral; + scheme.neutral_variant = neutral_variant; + scheme.build() }, ); + color_scheme.for_each_cloned(move |scheme| { + sources.primary.set(scheme.primary); + sources.secondary.set(scheme.secondary); + sources.tertiary.set(scheme.tertiary); + sources.error.set(scheme.error); + sources.neutral.set(scheme.neutral); + sources.neutral_variant.set(scheme.neutral_variant); + }); + let theme = color_scheme.map_each_cloned(ThemePair::from); let editors = theme_switcher - .and(primary_editor) - .and(secondary_editor) - .and(tertiary_editor) - .and(error_editor) - .and(neutral_editor) - .and(neutral_variant_editor) + .and(editors.primary) + .and(editors.secondary.1) + .and(editors.tertiary.1) + .and(editors.error.1) + .and(editors.neutral.1) + .and(editors.neutral_variant.1) .into_rows() .vertical_scroll(); editors .and(fixed_themes( - default_theme.map_each(|theme| theme.primary_fixed), - default_theme.map_each(|theme| theme.secondary_fixed), - default_theme.map_each(|theme| theme.tertiary_fixed), + theme.map_each(|theme| theme.primary_fixed), + theme.map_each(|theme| theme.secondary_fixed), + theme.map_each(|theme| theme.tertiary_fixed), )) - .and(theme( - default_theme.map_each(|theme| theme.dark), + .and(theme_preview( + theme.map_each(|theme| theme.dark), ThemeMode::Dark, )) - .and(theme( - default_theme.map_each(|theme| theme.light), + .and(theme_preview( + theme.map_each(|theme| theme.light), ThemeMode::Light, )) .into_columns() - .themed(default_theme) + .themed(theme) .pad() .expand() .into_window() @@ -75,36 +155,68 @@ fn main() -> gooey::Result { .run() } -fn dark_mode_slider() -> (Dynamic, impl MakeWidget) { - let theme_mode = Dynamic::default(); +fn dark_mode_picker() -> (Dynamic, impl MakeWidget) { + let dark = Dynamic::new(true); + let theme_mode = dark.map_each(|dark| { + if *dark { + ThemeMode::Dark + } else { + ThemeMode::Light + } + }); + + (theme_mode.clone(), dark.into_checkbox("Dark Mode")) +} + +fn swatch_label(label: &str, color: &Dynamic) -> impl MakeWidget { + Space::colored(color.map_each(|source| source.color(0.5))) + .width(Lp::mm(1)) + .and(label) + .into_columns() +} + +fn optional_editor(label: &str, color: &Dynamic) -> (Dynamic, impl MakeWidget) { + let enabled = Dynamic::new(false); + let hide_editor = enabled.map_each(|enabled| !enabled); ( - theme_mode.clone(), - "Theme Mode".and(theme_mode.slider()).into_rows(), + enabled.clone(), + enabled + .clone() + .into_checkbox(swatch_label(label, color)) + .and(color_editor(color).collapse_vertically(hide_editor)) + .into_rows(), ) } -fn color_editor( - initial_color: ColorSource, - label: &str, -) -> (Dynamic, impl MakeWidget) { - let hue = Dynamic::new(initial_color.hue.into_degrees()); +fn color_editor(color: &Dynamic) -> impl MakeWidget { + let hue = color.map_each(|color| color.hue.into_positive_degrees()); + hue.for_each_cloned({ + let color = color.clone(); + move |hue| { + let mut source = color.get(); + source.hue = OklabHue::new(hue); + color.set(source); + } + }); + let hue_text = hue.linked_string(); - let saturation = Dynamic::new(initial_color.saturation); + let saturation = color.map_each(|color| color.saturation); + saturation.for_each_cloned({ + let color = color.clone(); + move |saturation| { + let mut source = color.get(); + source.saturation = saturation; + color.set(source); + } + }); let saturation_text = saturation.linked_string(); - let color = - (&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation)); - - ( - color, - label - .and(hue.slider_between(0., 360.)) - .and(hue_text.into_input()) - .and(saturation.slider()) - .and(saturation_text.into_input()) - .into_rows(), - ) + hue.slider_between(0., 359.99) + .and(hue_text.into_input()) + .and(saturation.slider()) + .and(saturation_text.into_input()) + .into_rows() } fn fixed_themes( @@ -146,7 +258,7 @@ fn fixed_theme(theme: Dynamic, label: &str) -> impl MakeWidget { .expand() } -fn theme(theme: Dynamic, mode: ThemeMode) -> impl MakeWidget { +fn theme_preview(theme: Dynamic, mode: ThemeMode) -> impl MakeWidget { match mode { ThemeMode::Light => "Light", ThemeMode::Dark => "Dark", diff --git a/examples/tic-tac-toe.rs b/examples/tic-tac-toe.rs index b28de4c..a702ed1 100644 --- a/examples/tic-tac-toe.rs +++ b/examples/tic-tac-toe.rs @@ -26,7 +26,7 @@ fn main() -> gooey::Result { .run() } -#[derive(Default, Debug)] +#[derive(Default, Debug, Eq, PartialEq)] enum AppState { #[default] Playing, @@ -187,8 +187,8 @@ fn square(row: usize, column: usize, game: &Dynamic) -> impl MakeWidg return; }; - if enabled.update(false) { - label.update(player.to_string()); + if enabled.replace(false).is_some() { + label.set(player.to_string()); } }); }); diff --git a/src/animation.rs b/src/animation.rs index 22a0330..f3ee8f0 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -215,11 +215,11 @@ where fn update(&self, percent: f32) { self.change .dynamic - .update(self.start.lerp(&self.change.new_value, percent)); + .set(self.start.lerp(&self.change.new_value, percent)); } fn finish(&self) { - self.change.dynamic.update(self.change.new_value.clone()); + self.change.dynamic.set(self.change.new_value.clone()); } } @@ -458,7 +458,7 @@ pub struct RunningAnimation { /// A handle to a spawned animation. When dropped, the associated animation will /// be stopped. -#[derive(Default, Debug)] +#[derive(Default, Debug, PartialEq, Eq)] #[must_use] pub struct AnimationHandle(Option); @@ -1236,6 +1236,16 @@ impl RequireInvalidation for EasingFunction { } } +impl PartialEq for EasingFunction { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Fn(l0), Self::Fn(r0)) => l0 == r0, + (Self::Custom(l0), Self::Custom(r0)) => Arc::ptr_eq(l0, r0), + _ => false, + } + } +} + /// Performs easing for value interpolation. pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static { /// Eases a value ranging between zero and one. The resulting value does not diff --git a/src/styles.rs b/src/styles.rs index 186b3ac..a1d25f7 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -137,7 +137,7 @@ where impl IntoComponentValue for Value where - T: Clone, + T: Clone + Send + 'static, Component: From, { fn into_component_value(self) -> Value { @@ -147,7 +147,7 @@ where impl IntoComponentValue for Dynamic where - T: Clone, + T: Clone + Send + 'static, Component: From, { fn into_component_value(self) -> Value { @@ -201,7 +201,7 @@ impl IntoIterator for Styles { // } /// A value of a style component. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum Component { /// A color. Color(Color), @@ -781,6 +781,12 @@ impl CustomComponent { } } +impl PartialEq for CustomComponent { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + impl RequireInvalidation for CustomComponent { fn requires_invalidation(&self) -> bool { self.0.requires_invalidation() @@ -1112,7 +1118,7 @@ impl IntoValue for Lp { } /// A set of light and dark [`Theme`]s. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct ThemePair { /// The theme to use when the user interface is in light mode. pub light: Theme, @@ -1987,6 +1993,16 @@ pub trait ProtoColor: Sized { } } +impl<'a> ProtoColor for &'a ColorSource { + fn hue(&self) -> OklabHue { + self.hue + } + + fn saturation(&self) -> Option { + Some(self.saturation) + } +} + impl ProtoColor for f32 { fn hue(&self) -> OklabHue { (*self).into() diff --git a/src/styles/components.rs b/src/styles/components.rs index c13db05..d2ec563 100644 --- a/src/styles/components.rs +++ b/src/styles/components.rs @@ -94,7 +94,7 @@ define_components! { /// The [`Dimension`] to use as the size to render text. TextSize(Dimension, "text_size", Dimension::Lp(Lp::points(12))) /// The [`Dimension`] to use to space multiple lines of text. - LineHeight(Dimension,"line_height",Dimension::Lp(Lp::points(14))) + LineHeight(Dimension,"line_height",Dimension::Lp(Lp::points(16))) /// The [`Color`] of the surface for the user interface to draw upon. SurfaceColor(Color, "surface_color", .surface.color) /// The [`Color`] to use when rendering text. diff --git a/src/value.rs b/src/value.rs index 4eceb05..b282701 100644 --- a/src/value.rs +++ b/src/value.rs @@ -8,11 +8,12 @@ use std::str::FromStr; use std::sync::{Arc, Mutex, MutexGuard, TryLockError}; use std::task::{Poll, Waker}; use std::thread::ThreadId; +use std::time::Duration; use ahash::AHashSet; use intentional::Assert; -use crate::animation::{DynamicTransition, LinearInterpolate}; +use crate::animation::{DynamicTransition, IntoAnimate, LinearInterpolate, Spawn}; use crate::context::sealed::WindowHandle; use crate::context::{self, WidgetContext}; use crate::utils::{IgnorePoison, UnwindsafeCondvar, WithClone}; @@ -33,7 +34,7 @@ impl Dynamic { value, generation: Generation::default(), }, - callbacks: Vec::new(), + callbacks: Arc::default(), windows: AHashSet::new(), readers: 0, wakers: Vec::new(), @@ -82,7 +83,7 @@ impl Dynamic { r.with_clone(move |r| { self.for_each(move |t| { if let Some(update) = t_into_r(t).into() { - let _result = r.try_update(update); + let _result = r.replace(update); } }); }); @@ -90,7 +91,7 @@ impl Dynamic { self.with_clone(|t| { r.with_for_each(move |r| { if let Some(update) = r_into_t(r).into() { - let _result = t.try_update(update); + let _result = t.replace(update); } }) }) @@ -153,8 +154,8 @@ impl Dynamic { #[must_use] pub fn map_each_into(&self) -> Dynamic where - U: From + Send + 'static, - T: Clone, + U: PartialEq + From + Send + 'static, + T: Clone + Send + 'static, { self.map_each(|value| U::from(value.clone())) } @@ -164,8 +165,8 @@ impl Dynamic { #[must_use] pub fn map_each_to(&self) -> Dynamic where - U: for<'a> From<&'a T> + Send + 'static, - T: Clone, + U: PartialEq + for<'a> From<&'a T> + Send + 'static, + T: Clone + Send + 'static, { self.map_each(|value| U::from(value)) } @@ -174,19 +175,37 @@ impl Dynamic { /// value's contents are updated. pub fn for_each(&self, mut for_each: F) where + T: Send + 'static, F: for<'a> FnMut(&'a T) + Send + 'static, { - self.0.for_each(move |gen| for_each(&gen.value)); + let this = self.clone(); + self.0.for_each(move || { + this.map_ref(&mut for_each); + }); + } + + /// 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) + where + T: Clone + Send + 'static, + F: FnMut(T) + Send + 'static, + { + let this = self.clone(); + self.0.for_each(move || { + for_each(this.get()); + }); } /// 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] - pub fn with_for_each(self, mut for_each: F) -> Self + pub fn with_for_each(self, for_each: F) -> Self where + T: Send + 'static, F: for<'a> FnMut(&'a T) + Send + 'static, { - self.0.for_each(move |gen| for_each(&gen.value)); + self.for_each(for_each); self } @@ -194,23 +213,24 @@ impl Dynamic { /// 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: Send + 'static, + R: PartialEq + Send + 'static, { - self.0.map_each(move |gen| map(&gen.value)) + let this = self.clone(); + self.0.map_each(move || this.map_ref(&mut map)) } /// Creates a new dynamic value that contains the result of invoking `map` /// each time this value is changed. - /// - /// This version of `map_each` uses [`Dynamic::try_update`] to prevent - /// deadlocks and debounce dependent values. - pub fn map_each_unique(&self, mut map: F) -> Dynamic + pub fn map_each_cloned(&self, mut map: F) -> Dynamic where - F: for<'a> FnMut(&'a T) -> R + Send + 'static, - R: Send + PartialEq + 'static, + T: Clone + Send + 'static, + F: FnMut(T) -> R + Send + 'static, + R: PartialEq + Send + 'static, { - self.0.map_each_unique(move |gen| map(&gen.value)) + let this = self.clone(); + self.0.map_each(move || map(this.get())) } /// A helper function that invokes `with_clone` with a clone of self. This @@ -336,77 +356,62 @@ impl Dynamic { /// Before returning from this function, all observers will be notified that /// the contents have been updated. /// - /// # Panics + /// 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. /// - /// This function panics if this value is already locked by the current - /// thread. - #[must_use] - pub fn replace(&self, new_value: T) -> T { - self.0 - .map_mut(|value, _| std::mem::replace(value, new_value)) - .expect("deadlocked") + /// + /// 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, + { + let cell = Cell::new(Some(new_value)); + match self.0.map_mut(|value, changed| { + let new_value = cell.take().assert("only one callback will be invoked"); + 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. /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - pub fn set(&self, new_value: T) { + /// 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); } - /// Updates this dynamic with `new_value`, but only if `new_value` is not - /// equal to the currently stored value. - /// - /// Returns true if the value was updated. - /// - /// # Panics - /// - /// This function panics if this value is already locked by the current - /// thread. - pub fn update(&self, new_value: T) -> bool - where - T: PartialEq, - { - self.0 - .map_mut(|value, changed| { - if *value == new_value { - *changed = false; - false - } else { - *value = new_value; - true - } - }) - .expect("deadlocked") - } - - /// Attempt to store `new_value` in `self`. If the value cannot be stored - /// due to a deadlock, it is returned as an error. - /// - /// Returns true if the value was updated. - pub fn try_update(&self, new_value: T) -> Result - where - T: PartialEq, - { - let cell = Cell::new(Some(new_value)); - self.0 - .map_mut(|value, changed| { - let new_value = cell.take().assert("only one callback will be invoked"); - if *value == new_value { - *changed = false; - false - } else { - *value = new_value; - true - } - }) - .map_err(|_| cell.take().assert("only one callback will be invoked")) - } - /// Returns a new reference-based reader for this dynamic value. /// /// # Panics @@ -644,17 +649,16 @@ impl DynamicData { pub fn map_mut(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result { let mut state = self.state()?; - let old = { + let (old, callbacks) = { let state = &mut *state; let mut changed = true; let result = map(&mut state.wrapped.value, &mut changed); - if changed { - state.note_changed(); - } + let callbacks = changed.then(|| state.note_changed()); - result + (result, callbacks) }; drop(state); + drop(callbacks); self.sync.notify_all(); @@ -663,55 +667,44 @@ impl DynamicData { pub fn for_each(&self, map: F) where - F: for<'a> FnMut(&'a GenerationalValue) + Send + 'static, + F: for<'a> FnMut() + Send + 'static, { - let mut state = self.state().expect("deadlocked"); - state.callbacks.push(Box::new(map)); + let state = self.state().expect("deadlocked"); + let mut callbacks = state.callbacks.lock().ignore_poison(); + callbacks.push(Box::new(map)); } pub fn map_each(&self, mut map: F) -> Dynamic where - F: for<'a> FnMut(&'a GenerationalValue) -> R + Send + 'static, - R: Send + 'static, - { - let mut state = self.state().expect("deadlocked"); - let initial_value = map(&state.wrapped); - let mapped_value = Dynamic::new(initial_value); - let returned = mapped_value.clone(); - state - .callbacks - .push(Box::new(move |updated: &GenerationalValue| { - mapped_value.set(map(updated)); - })); - - returned - } - - pub fn map_each_unique(&self, mut map: F) -> Dynamic - where - F: for<'a> FnMut(&'a GenerationalValue) -> R + Send + 'static, + F: for<'a> FnMut() -> R + Send + 'static, R: PartialEq + Send + 'static, { - let mut state = self.state().expect("deadlocked"); - let initial_value = map(&state.wrapped); + let initial_value = map(); let mapped_value = Dynamic::new(initial_value); let returned = mapped_value.clone(); - state - .callbacks - .push(Box::new(move |updated: &GenerationalValue| { - let _deadlock = mapped_value.try_update(map(updated)); - })); + + self.for_each(move || { + mapped_value.set(map()); + }); returned } } +/// An error occurred while updating a value in a [`Dynamic`]. +pub enum ReplaceError { + /// The value was already equal to the one set. + NoChange(T), + /// The current thread already has exclusive access to this dynamic. + Deadlock, +} + /// A deadlock occurred accessing a [`Dynamic`]. /// /// Currently Gooey is only able to detect deadlocks where a single thread tries /// to lock the same [`Dynamic`] multiple times. #[derive(Debug)] -pub struct DeadlockError; +struct DeadlockError; impl std::error::Error for DeadlockError {} @@ -723,7 +716,7 @@ impl Display for DeadlockError { struct State { wrapped: GenerationalValue, - callbacks: Vec>>, + callbacks: Arc>>>, windows: AHashSet, widgets: AHashSet<(WindowHandle, WidgetId)>, wakers: Vec, @@ -731,12 +724,9 @@ struct State { } impl State { - fn note_changed(&mut self) { + fn note_changed(&mut self) -> ChangeCallbacks { self.wrapped.generation = self.wrapped.generation.next(); - for callback in &mut self.callbacks { - callback.update(&self.wrapped); - } for (window, widget) in self.widgets.drain() { window.invalidate(widget); } @@ -746,6 +736,8 @@ impl State { for waker in self.wakers.drain(..) { waker.wake(); } + + ChangeCallbacks(self.callbacks.clone()) } } @@ -761,16 +753,28 @@ where } } -trait ValueCallback: Send { - fn update(&mut self, value: &GenerationalValue); +struct ChangeCallbacks(Arc>>>); + +impl Drop for ChangeCallbacks { + fn drop(&mut self) { + if let Ok(mut callbacks) = self.0.lock() { + for callback in &mut *callbacks { + callback.changed(); + } + } + } } -impl ValueCallback for F +trait ValueCallback: Send { + fn changed(&mut self); +} + +impl ValueCallback for F where - F: for<'a> FnMut(&'a GenerationalValue) + Send + 'static, + F: for<'a> FnMut() + Send + 'static, { - fn update(&mut self, value: &GenerationalValue) { - self(value); + fn changed(&mut self) { + self(); } } @@ -808,7 +812,10 @@ impl<'a, T> DerefMut for DynamicGuard<'a, T> { impl Drop for DynamicGuard<'_, T> { fn drop(&mut self) { if self.accessed_mut { - self.guard.note_changed(); + let mut callbacks = Some(self.guard.note_changed()); + Duration::ZERO + .on_complete(move || drop(callbacks.take())) + .launch(); } } } @@ -1086,7 +1093,7 @@ impl IntoDynamic for Dynamic { impl IntoDynamic for F where F: FnMut(&T) + Send + 'static, - T: Default, + T: Default + Send + 'static, { /// Returns [`Dynamic::default()`] with `self` installed as a for-each /// callback. @@ -1182,8 +1189,9 @@ impl Value { #[must_use] pub fn map_each(&self, mut map: F) -> Value where + T: Send + 'static, F: for<'a> FnMut(&'a T) -> R + Send + 'static, - R: Send + 'static, + R: PartialEq + Send + 'static, { match self { Value::Constant(value) => Value::Constant(map(value)), @@ -1423,7 +1431,7 @@ macro_rules! impl_tuple_map_each { ($($type:ident $field:tt $var:ident),+) => { impl MapEach<($($type,)+), U> for ($(&Dynamic<$type>,)+) where - U: Send + 'static, + U: PartialEq + Send + 'static, $($type: Send + 'static),+ { type Ref<'a> = ($(&'a $type,)+); @@ -1451,3 +1459,144 @@ macro_rules! impl_tuple_map_each { } impl_all_tuples!(impl_tuple_map_each); + +/// A type that can have a `for_each` operation applied to it. +pub trait ForEachCloned { + /// Apply `for_each` to each value contained within `self`. + fn for_each_cloned(&self, for_each: F) + where + F: for<'a> FnMut(T) + Send + 'static; +} + +macro_rules! impl_tuple_for_each_cloned { + ($($type:ident $field:tt $var:ident),+) => { + impl<$($type,)+> ForEachCloned<($($type,)+)> for ($(&Dynamic<$type>,)+) + where + $($type: Clone + Send + 'static,)+ + { + + #[allow(unused_mut)] + fn for_each_cloned(&self, mut for_each: F) + where + F: for<'a> FnMut(($($type,)+)) + Send + 'static, + { + impl_tuple_for_each_cloned!(self for_each [] [$($type $field $var),+]); + } + } + }; + ($self:ident $for_each:ident [] [$type:ident $field:tt $var:ident]) => { + $self.$field.for_each(move |field: &$type| $for_each((field.clone(),))); + }; + ($self:ident $for_each:ident [] [$($type:ident $field:tt $var:ident),+]) => { + let $for_each = Arc::new(Mutex::new($for_each)); + $(let $var = $self.$field.clone();)* + + + impl_tuple_for_each_cloned!(invoke $self $for_each [] [$($type $field $var),+]); + }; + ( + invoke + // Identifiers used from the outer method + $self:ident $for_each:ident + // List of all tuple fields that have already been positioned as the focused call + [$($ltype:ident $lfield:tt $lvar:ident),*] + // + [$type:ident $field:tt $var:ident, $($rtype:ident $rfield:tt $rvar:ident),+] + ) => { + impl_tuple_for_each_cloned!( + invoke + $self $for_each + $type $field $var + [$($ltype $lfield $lvar,)* $type $field $var, $($rtype $rfield $rvar),+] + [$($ltype $lfield $lvar,)* $($rtype $rfield $rvar),+] + ); + impl_tuple_for_each_cloned!( + invoke + $self $for_each + [$($ltype $lfield $lvar,)* $type $field $var] + [$($rtype $rfield $rvar),+] + ); + }; + ( + invoke + // Identifiers used from the outer method + $self:ident $for_each:ident + // List of all tuple fields that have already been positioned as the focused call + [$($ltype:ident $lfield:tt $lvar:ident),+] + // + [$type:ident $field:tt $var:ident] + ) => { + impl_tuple_for_each_cloned!( + invoke + $self $for_each + $type $field $var + [$($ltype $lfield $lvar,)+ $type $field $var] + [$($ltype $lfield $lvar),+] + ); + }; + ( + invoke + // Identifiers used from the outer method + $self:ident $for_each:ident + // Tuple field that for_each is being invoked on + $type:ident $field:tt $var:ident + // The list of all tuple fields in this invocation, in the correct order. + [$($atype:ident $afield:tt $avar:ident),+] + // The list of tuple fields excluding the one being invoked. + [$($rtype:ident $rfield:tt $rvar:ident),+] + ) => { + $var.for_each_cloned((&$for_each, $(&$rvar,)+).with_clone(|(for_each, $($rvar,)+)| { + move |$var: $type| { + $(let $rvar = $rvar.get();)+ + if let Ok(mut for_each) = + for_each.try_lock() { + (for_each)(($($avar,)+)); + } + } + })); + }; +} + +impl_all_tuples!(impl_tuple_for_each_cloned); + +/// A type that can create a `Dynamic` from a `T` passed into a mapping +/// function. +pub trait MapEachCloned { + /// Apply `map_each` to each value in `self`, storing the result in the + /// returned dynamic. + fn map_each_cloned(&self, map_each: F) -> Dynamic + where + F: for<'a> FnMut(T) -> U + Send + 'static; +} + +macro_rules! impl_tuple_map_each_cloned { + ($($type:ident $field:tt $var:ident),+) => { + impl MapEachCloned<($($type,)+), U> for ($(&Dynamic<$type>,)+) + where + U: PartialEq + Send + 'static, + $($type: Clone + Send + 'static),+ + { + + fn map_each_cloned(&self, mut map_each: F) -> Dynamic + where + F: for<'a> FnMut(($($type,)+)) -> U + Send + 'static, + { + let dynamic = { + $(let $var = self.$field.get();)+ + + Dynamic::new(map_each(($($var,)+))) + }; + self.for_each_cloned({ + let dynamic = dynamic.clone(); + + move |tuple| { + dynamic.set(map_each(tuple)); + } + }); + dynamic + } + } + }; +} + +impl_all_tuples!(impl_tuple_map_each_cloned); diff --git a/src/widgets/button.rs b/src/widgets/button.rs index 4f79c45..2c71fed 100644 --- a/src/widgets/button.rs +++ b/src/widgets/button.rs @@ -243,7 +243,7 @@ impl Button { .spawn(); } (true, Some(style)) => { - style.update(new_style); + style.set(new_style); self.color_animation.clear(); } _ => { diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs index d3f9a1f..75c3c24 100644 --- a/src/widgets/progress.rs +++ b/src/widgets/progress.rs @@ -118,8 +118,8 @@ fn update_progress_bar( } Progress::Percent(value) => { let _stopped_animation = indeterminant_animation.take(); - start.update(ZeroToOne::ZERO); - end.update(value); + start.set(ZeroToOne::ZERO); + end.set(value); } } } @@ -127,7 +127,7 @@ fn update_progress_bar( /// A value that can be used in a progress indicator. pub trait Progressable: IntoDynamic + Sized where - T: ProgressValue, + T: ProgressValue + Send, { /// Returns a new progress bar that displays progress from `T::MIN` to /// `T::MAX`. @@ -145,7 +145,7 @@ where fn progress_bar_to(self, max: impl IntoValue) -> ProgressBar where T: Send, - T::Value: Ranged + Send + Clone, + T::Value: PartialEq + Ranged + Send + Clone, { let max = max.into_value(); match max { @@ -181,7 +181,7 @@ where } } -impl Progressable for Dynamic where U: ProgressValue {} +impl Progressable for Dynamic where U: ProgressValue + Send {} /// A value that can be used in a progress indicator. pub trait ProgressValue: 'static { diff --git a/src/widgets/radio.rs b/src/widgets/radio.rs index 277f836..4fe02ec 100644 --- a/src/widgets/radio.rs +++ b/src/widgets/radio.rs @@ -63,7 +63,7 @@ where .into_columns() .into_button() .on_click(move |()| { - self.state.update(self.value.clone()); + self.state.set(self.value.clone()); }) .kind(self.kind) .make_widget() diff --git a/src/widgets/scroll.rs b/src/widgets/scroll.rs index d19f986..2526e64 100644 --- a/src/widgets/scroll.rs +++ b/src/widgets/scroll.rs @@ -225,7 +225,7 @@ impl Widget for Scroll { let scroll_pct = scroll.y.into_float() / current_max_scroll.y.into_float(); scroll.y = max_scroll_y * scroll_pct; } - self.scroll.update(scroll); + self.scroll.set(scroll); self.control_size = control_size; self.content_size = new_content_size; diff --git a/src/widgets/slider.rs b/src/widgets/slider.rs index fd5cedc..d89eeae 100644 --- a/src/widgets/slider.rs +++ b/src/widgets/slider.rs @@ -343,7 +343,7 @@ where start = value; self.focused_knob = Some(Knob::Start); } - self.value.update(T::from_parts(start, opt_end)); + self.value.set(T::from_parts(start, opt_end)); } fn step(&mut self, forwards: bool, factor: f32) { @@ -391,7 +391,7 @@ where (Knob::Start, Some(end)) => (new_value, Some(end)), (Knob::End, Some(start)) => (start, Some(new_value)), }; - self.value.update(T::from_parts(start, end)); + self.value.set(T::from_parts(start, end)); } } diff --git a/src/widgets/space.rs b/src/widgets/space.rs index a5931a0..b0de0fb 100644 --- a/src/widgets/space.rs +++ b/src/widgets/space.rs @@ -39,8 +39,7 @@ impl Space { impl Widget for Space { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { - self.color.redraw_when_changed(context); - let color = self.color.get(); + let color = self.color.get_tracked(context); context.fill(color); } diff --git a/src/window.rs b/src/window.rs index 4a52625..43ee9b8 100644 --- a/src/window.rs +++ b/src/window.rs @@ -172,7 +172,7 @@ impl Window { /// of `false`. pub fn focused(mut self, focused: impl IntoDynamic) -> Self { let focused = focused.into_dynamic(); - focused.update(false); + focused.set(false); self.focused = Some(focused); self } @@ -187,7 +187,7 @@ impl Window { /// `occluded` will be initialized with an initial state of `false`. pub fn occluded(mut self, occluded: impl IntoDynamic) -> Self { let occluded = occluded.into_dynamic(); - occluded.update(false); + occluded.set(false); self.occluded = Some(occluded); self } @@ -528,7 +528,7 @@ where let theme_mode = match settings.theme_mode.take() { Some(Value::Dynamic(dynamic)) => { - dynamic.update(window.theme().into()); + dynamic.set(window.theme().into()); Value::Dynamic(dynamic) } Some(Value::Constant(mode)) => Value::Constant(mode), @@ -660,7 +660,7 @@ where window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { - self.focused.update(window.focused()); + self.focused.set(window.focused()); } fn occlusion_changed( @@ -668,7 +668,7 @@ where window: kludgine::app::Window<'_, WindowCommand>, _kludgine: &mut Kludgine, ) { - self.occluded.update(window.ocluded()); + self.occluded.set(window.ocluded()); } fn render<'pass>( @@ -1104,7 +1104,7 @@ where _kludgine: &mut Kludgine, ) { if let Value::Dynamic(theme_mode) = &self.theme_mode { - theme_mode.update(window.theme().into()); + theme_mode.set(window.theme().into()); } }