Dynamic now requires PartialEq

This reduces the complexity of operations capable with Dynamic, and also
makes it easier to shortcut deadlocking operations.
This commit is contained in:
Jonathan Johnson 2023-11-23 11:53:59 -08:00
parent b63e4d66d2
commit b2fdf06e60
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
17 changed files with 543 additions and 234 deletions

View file

@ -3,9 +3,16 @@
[![Documentation for `main` branch](https://img.shields.io/badge/docs-main-informational)]($docs$) [![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 Gooey is an experimental Graphical User Interface (GUI) crate for the Rust
programming language. It is built using [`Kludgine`][kludgine], which is powered programming language. It is powered by:
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. - [`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 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 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$ $../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$ [widget]: $widget$
[kludgine]: https://github.com/khonsulabs/kludgine [kludgine]: https://github.com/khonsulabs/kludgine
[wgpu]: https://github.com/gfx-rs/wgpu [wgpu]: https://github.com/gfx-rs/wgpu
[winit]: https://github.com/rust-windowing/winit [winit]: https://github.com/rust-windowing/winit
[widgets]: $widgets$ [widgets]: $widgets$
[button-example]: https://github.com/khonsulabs/gooey/tree/$ref-name$/examples/basic-button.rs [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

View file

@ -11,7 +11,7 @@ fn main() -> gooey::Result {
.clone() .clone()
.into_checkbox(label) .into_checkbox(label)
.and("Maybe".into_button().on_click(move |()| { .and("Maybe".into_button().on_click(move |()| {
checkbox_state.update(CheckboxState::Indeterminant); checkbox_state.set(CheckboxState::Indeterminant);
})) }))
.into_columns() .into_columns()
.centered() .centered()

View file

@ -46,12 +46,12 @@ fn u8_slider() -> impl MakeWidget {
fn u8_range_slider() -> impl MakeWidget { fn u8_range_slider() -> impl MakeWidget {
let range = Dynamic::new(42..=127); let range = Dynamic::new(42..=127);
let start = range.map_each_unique(|range| *range.start()); let start = range.map_each(|range| *range.start());
let end = range.map_each_unique(|range| *range.end()); let end = range.map_each(|range| *range.end());
(&start, &end).for_each({ (&start, &end).for_each({
let range = range.clone(); let range = range.clone();
move |(start, end)| { move |(start, end)| {
let _result = range.try_update(*start..=*end); range.set(*start..=*end);
} }
}); });

View file

@ -2,7 +2,7 @@ use gooey::value::{Dynamic, Switchable};
use gooey::widget::{MakeWidget, WidgetInstance}; use gooey::widget::{MakeWidget, WidgetInstance};
use gooey::Run; use gooey::Run;
#[derive(Debug)] #[derive(Debug, Eq, PartialEq)]
enum ActiveContent { enum ActiveContent {
Intro, Intro,
Success, Success,

View file

@ -1,73 +1,153 @@
use gooey::styles::components::{TextColor, WidgetBackground}; use gooey::styles::components::{TextColor, WidgetBackground};
use gooey::styles::{ 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::widget::MakeWidget;
use gooey::widgets::checkbox::Checkable;
use gooey::widgets::input::InputValue; use gooey::widgets::input::InputValue;
use gooey::widgets::slider::Slidable; use gooey::widgets::slider::Slidable;
use gooey::widgets::Space;
use gooey::window::ThemeMode; use gooey::window::ThemeMode;
use gooey::Run; use gooey::Run;
use kludgine::figures::units::Lp;
use kludgine::Color; use kludgine::Color;
use palette::OklabHue;
struct Scheme<Primary, Other = Primary> {
primary: Primary,
secondary: Other,
tertiary: Other,
error: Other,
neutral: Other,
neutral_variant: Other,
}
impl From<ColorScheme> for Scheme<ColorSource> {
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<T> Scheme<T> {
pub fn map<R>(&self, mut map: impl FnMut(T) -> R) -> Scheme<R>
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<Primary, Other> Scheme<Primary, Other> {
pub fn map_labeled<NewPrimary, NewOther>(
&self,
primary: impl FnOnce(Primary) -> NewPrimary,
mut map: impl FnMut(&str, Other) -> NewOther,
) -> Scheme<NewPrimary, NewOther>
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 { fn main() -> gooey::Result {
let scheme = ColorScheme::default(); let (theme_mode, theme_switcher) = dark_mode_picker();
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 default_theme = ( let scheme = Scheme::from(ColorScheme::default());
&primary, let sources = scheme.map(Dynamic::new);
&secondary, let editors = sources.map_labeled(
&tertiary, |primary| {
&error, swatch_label("Primary", &primary)
&neutral, .and(color_editor(&primary))
&neutral_variant, .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( .map_each_cloned(
|(primary, secondary, tertiary, error, neutral, neutral_variant)| { move |(primary, secondary, tertiary, error, neutral, neutral_variant)| {
ThemePair::from(ColorScheme { let mut scheme = ColorSchemeBuilder::new(primary);
primary: *primary, scheme.secondary = secondary;
secondary: *secondary, scheme.tertiary = tertiary;
tertiary: *tertiary, scheme.error = error;
error: *error, scheme.neutral = neutral;
neutral: *neutral, scheme.neutral_variant = neutral_variant;
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 let editors = theme_switcher
.and(primary_editor) .and(editors.primary)
.and(secondary_editor) .and(editors.secondary.1)
.and(tertiary_editor) .and(editors.tertiary.1)
.and(error_editor) .and(editors.error.1)
.and(neutral_editor) .and(editors.neutral.1)
.and(neutral_variant_editor) .and(editors.neutral_variant.1)
.into_rows() .into_rows()
.vertical_scroll(); .vertical_scroll();
editors editors
.and(fixed_themes( .and(fixed_themes(
default_theme.map_each(|theme| theme.primary_fixed), theme.map_each(|theme| theme.primary_fixed),
default_theme.map_each(|theme| theme.secondary_fixed), theme.map_each(|theme| theme.secondary_fixed),
default_theme.map_each(|theme| theme.tertiary_fixed), theme.map_each(|theme| theme.tertiary_fixed),
)) ))
.and(theme( .and(theme_preview(
default_theme.map_each(|theme| theme.dark), theme.map_each(|theme| theme.dark),
ThemeMode::Dark, ThemeMode::Dark,
)) ))
.and(theme( .and(theme_preview(
default_theme.map_each(|theme| theme.light), theme.map_each(|theme| theme.light),
ThemeMode::Light, ThemeMode::Light,
)) ))
.into_columns() .into_columns()
.themed(default_theme) .themed(theme)
.pad() .pad()
.expand() .expand()
.into_window() .into_window()
@ -75,36 +155,68 @@ fn main() -> gooey::Result {
.run() .run()
} }
fn dark_mode_slider() -> (Dynamic<ThemeMode>, impl MakeWidget) { fn dark_mode_picker() -> (Dynamic<ThemeMode>, impl MakeWidget) {
let theme_mode = Dynamic::default(); 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<ColorSource>) -> 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<ColorSource>) -> (Dynamic<bool>, impl MakeWidget) {
let enabled = Dynamic::new(false);
let hide_editor = enabled.map_each(|enabled| !enabled);
( (
theme_mode.clone(), enabled.clone(),
"Theme Mode".and(theme_mode.slider()).into_rows(), enabled
.clone()
.into_checkbox(swatch_label(label, color))
.and(color_editor(color).collapse_vertically(hide_editor))
.into_rows(),
) )
} }
fn color_editor( fn color_editor(color: &Dynamic<ColorSource>) -> impl MakeWidget {
initial_color: ColorSource, let hue = color.map_each(|color| color.hue.into_positive_degrees());
label: &str, hue.for_each_cloned({
) -> (Dynamic<ColorSource>, impl MakeWidget) { let color = color.clone();
let hue = Dynamic::new(initial_color.hue.into_degrees()); move |hue| {
let mut source = color.get();
source.hue = OklabHue::new(hue);
color.set(source);
}
});
let hue_text = hue.linked_string(); 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 saturation_text = saturation.linked_string();
let color = hue.slider_between(0., 359.99)
(&hue, &saturation).map_each(|(hue, saturation)| ColorSource::new(*hue, *saturation)); .and(hue_text.into_input())
.and(saturation.slider())
( .and(saturation_text.into_input())
color, .into_rows()
label
.and(hue.slider_between(0., 360.))
.and(hue_text.into_input())
.and(saturation.slider())
.and(saturation_text.into_input())
.into_rows(),
)
} }
fn fixed_themes( fn fixed_themes(
@ -146,7 +258,7 @@ fn fixed_theme(theme: Dynamic<FixedTheme>, label: &str) -> impl MakeWidget {
.expand() .expand()
} }
fn theme(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget { fn theme_preview(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget {
match mode { match mode {
ThemeMode::Light => "Light", ThemeMode::Light => "Light",
ThemeMode::Dark => "Dark", ThemeMode::Dark => "Dark",

View file

@ -26,7 +26,7 @@ fn main() -> gooey::Result {
.run() .run()
} }
#[derive(Default, Debug)] #[derive(Default, Debug, Eq, PartialEq)]
enum AppState { enum AppState {
#[default] #[default]
Playing, Playing,
@ -187,8 +187,8 @@ fn square(row: usize, column: usize, game: &Dynamic<GameState>) -> impl MakeWidg
return; return;
}; };
if enabled.update(false) { if enabled.replace(false).is_some() {
label.update(player.to_string()); label.set(player.to_string());
} }
}); });
}); });

View file

@ -215,11 +215,11 @@ where
fn update(&self, percent: f32) { fn update(&self, percent: f32) {
self.change self.change
.dynamic .dynamic
.update(self.start.lerp(&self.change.new_value, percent)); .set(self.start.lerp(&self.change.new_value, percent));
} }
fn finish(&self) { 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<T, Easing> {
/// A handle to a spawned animation. When dropped, the associated animation will /// A handle to a spawned animation. When dropped, the associated animation will
/// be stopped. /// be stopped.
#[derive(Default, Debug)] #[derive(Default, Debug, PartialEq, Eq)]
#[must_use] #[must_use]
pub struct AnimationHandle(Option<LotId>); pub struct AnimationHandle(Option<LotId>);
@ -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. /// Performs easing for value interpolation.
pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static { pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static {
/// Eases a value ranging between zero and one. The resulting value does not /// Eases a value ranging between zero and one. The resulting value does not

View file

@ -137,7 +137,7 @@ where
impl<T> IntoComponentValue for Value<T> impl<T> IntoComponentValue for Value<T>
where where
T: Clone, T: Clone + Send + 'static,
Component: From<T>, Component: From<T>,
{ {
fn into_component_value(self) -> Value<Component> { fn into_component_value(self) -> Value<Component> {
@ -147,7 +147,7 @@ where
impl<T> IntoComponentValue for Dynamic<T> impl<T> IntoComponentValue for Dynamic<T>
where where
T: Clone, T: Clone + Send + 'static,
Component: From<T>, Component: From<T>,
{ {
fn into_component_value(self) -> Value<Component> { fn into_component_value(self) -> Value<Component> {
@ -201,7 +201,7 @@ impl IntoIterator for Styles {
// } // }
/// A value of a style component. /// A value of a style component.
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq)]
pub enum Component { pub enum Component {
/// A color. /// A color.
Color(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 { impl RequireInvalidation for CustomComponent {
fn requires_invalidation(&self) -> bool { fn requires_invalidation(&self) -> bool {
self.0.requires_invalidation() self.0.requires_invalidation()
@ -1112,7 +1118,7 @@ impl IntoValue<Dimension> for Lp {
} }
/// A set of light and dark [`Theme`]s. /// A set of light and dark [`Theme`]s.
#[derive(Clone, Debug)] #[derive(Clone, Debug, PartialEq)]
pub struct ThemePair { pub struct ThemePair {
/// The theme to use when the user interface is in light mode. /// The theme to use when the user interface is in light mode.
pub light: Theme, 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<ZeroToOne> {
Some(self.saturation)
}
}
impl ProtoColor for f32 { impl ProtoColor for f32 {
fn hue(&self) -> OklabHue { fn hue(&self) -> OklabHue {
(*self).into() (*self).into()

View file

@ -94,7 +94,7 @@ define_components! {
/// The [`Dimension`] to use as the size to render text. /// The [`Dimension`] to use as the size to render text.
TextSize(Dimension, "text_size", Dimension::Lp(Lp::points(12))) TextSize(Dimension, "text_size", Dimension::Lp(Lp::points(12)))
/// The [`Dimension`] to use to space multiple lines of text. /// 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. /// The [`Color`] of the surface for the user interface to draw upon.
SurfaceColor(Color, "surface_color", .surface.color) SurfaceColor(Color, "surface_color", .surface.color)
/// The [`Color`] to use when rendering text. /// The [`Color`] to use when rendering text.

View file

@ -8,11 +8,12 @@ use std::str::FromStr;
use std::sync::{Arc, Mutex, MutexGuard, TryLockError}; use std::sync::{Arc, Mutex, MutexGuard, TryLockError};
use std::task::{Poll, Waker}; use std::task::{Poll, Waker};
use std::thread::ThreadId; use std::thread::ThreadId;
use std::time::Duration;
use ahash::AHashSet; use ahash::AHashSet;
use intentional::Assert; use intentional::Assert;
use crate::animation::{DynamicTransition, LinearInterpolate}; use crate::animation::{DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
use crate::context::sealed::WindowHandle; use crate::context::sealed::WindowHandle;
use crate::context::{self, WidgetContext}; use crate::context::{self, WidgetContext};
use crate::utils::{IgnorePoison, UnwindsafeCondvar, WithClone}; use crate::utils::{IgnorePoison, UnwindsafeCondvar, WithClone};
@ -33,7 +34,7 @@ impl<T> Dynamic<T> {
value, value,
generation: Generation::default(), generation: Generation::default(),
}, },
callbacks: Vec::new(), callbacks: Arc::default(),
windows: AHashSet::new(), windows: AHashSet::new(),
readers: 0, readers: 0,
wakers: Vec::new(), wakers: Vec::new(),
@ -82,7 +83,7 @@ impl<T> Dynamic<T> {
r.with_clone(move |r| { r.with_clone(move |r| {
self.for_each(move |t| { self.for_each(move |t| {
if let Some(update) = t_into_r(t).into() { 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<T> Dynamic<T> {
self.with_clone(|t| { self.with_clone(|t| {
r.with_for_each(move |r| { r.with_for_each(move |r| {
if let Some(update) = r_into_t(r).into() { 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<T> Dynamic<T> {
#[must_use] #[must_use]
pub fn map_each_into<U>(&self) -> Dynamic<U> pub fn map_each_into<U>(&self) -> Dynamic<U>
where where
U: From<T> + Send + 'static, U: PartialEq + From<T> + Send + 'static,
T: Clone, T: Clone + Send + 'static,
{ {
self.map_each(|value| U::from(value.clone())) self.map_each(|value| U::from(value.clone()))
} }
@ -164,8 +165,8 @@ impl<T> Dynamic<T> {
#[must_use] #[must_use]
pub fn map_each_to<U>(&self) -> Dynamic<U> pub fn map_each_to<U>(&self) -> Dynamic<U>
where where
U: for<'a> From<&'a T> + Send + 'static, U: PartialEq + for<'a> From<&'a T> + Send + 'static,
T: Clone, T: Clone + Send + 'static,
{ {
self.map_each(|value| U::from(value)) self.map_each(|value| U::from(value))
} }
@ -174,19 +175,37 @@ impl<T> Dynamic<T> {
/// value's contents are updated. /// value's contents are updated.
pub fn for_each<F>(&self, mut for_each: F) pub fn for_each<F>(&self, mut for_each: F)
where where
T: Send + 'static,
F: for<'a> FnMut(&'a 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<F>(&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 /// Attaches `for_each` to this value so that it is invoked each time the
/// value's contents are updated. This function returns `self`. /// value's contents are updated. This function returns `self`.
#[must_use] #[must_use]
pub fn with_for_each<F>(self, mut for_each: F) -> Self pub fn with_for_each<F>(self, for_each: F) -> Self
where where
T: Send + 'static,
F: for<'a> FnMut(&'a 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 self
} }
@ -194,23 +213,24 @@ impl<T> Dynamic<T> {
/// each time this value is changed. /// each time this value is changed.
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R> pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
where where
T: Send + 'static,
F: for<'a> FnMut(&'a T) -> R + 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` /// Creates a new dynamic value that contains the result of invoking `map`
/// each time this value is changed. /// each time this value is changed.
/// pub fn map_each_cloned<R, F>(&self, mut map: F) -> Dynamic<R>
/// This version of `map_each` uses [`Dynamic::try_update`] to prevent
/// deadlocks and debounce dependent values.
pub fn map_each_unique<R, F>(&self, mut map: F) -> Dynamic<R>
where where
F: for<'a> FnMut(&'a T) -> R + Send + 'static, T: Clone + Send + 'static,
R: Send + PartialEq + '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 /// A helper function that invokes `with_clone` with a clone of self. This
@ -336,77 +356,62 @@ impl<T> Dynamic<T> {
/// Before returning from this function, all observers will be notified that /// Before returning from this function, all observers will be notified that
/// the contents have been updated. /// 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<T>
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. /// Before returning from this function, all observers will be notified that
#[must_use] /// the contents have been updated.
pub fn replace(&self, new_value: T) -> T { ///
self.0 /// # Errors
.map_mut(|value, _| std::mem::replace(value, new_value)) ///
.expect("deadlocked") /// - [`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<T, ReplaceError<T>>
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, /// Stores `new_value` in this dynamic. Before returning from this function,
/// all observers will be notified that the contents have been updated. /// 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.
/// This function panics if this value is already locked by the current /// If detecting this is important, use [`Self::try_replace()`].
/// thread. pub fn set(&self, new_value: T)
pub fn set(&self, new_value: T) { where
T: PartialEq,
{
let _old = self.replace(new_value); 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<bool, T>
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. /// Returns a new reference-based reader for this dynamic value.
/// ///
/// # Panics /// # Panics
@ -644,17 +649,16 @@ impl<T> DynamicData<T> {
pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result<R, DeadlockError> { pub fn map_mut<R>(&self, map: impl FnOnce(&mut T, &mut bool) -> R) -> Result<R, DeadlockError> {
let mut state = self.state()?; let mut state = self.state()?;
let old = { let (old, callbacks) = {
let state = &mut *state; let state = &mut *state;
let mut changed = true; let mut changed = true;
let result = map(&mut state.wrapped.value, &mut changed); let result = map(&mut state.wrapped.value, &mut changed);
if changed { let callbacks = changed.then(|| state.note_changed());
state.note_changed();
}
result (result, callbacks)
}; };
drop(state); drop(state);
drop(callbacks);
self.sync.notify_all(); self.sync.notify_all();
@ -663,55 +667,44 @@ impl<T> DynamicData<T> {
pub fn for_each<F>(&self, map: F) pub fn for_each<F>(&self, map: F)
where where
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static, F: for<'a> FnMut() + Send + 'static,
{ {
let mut state = self.state().expect("deadlocked"); let state = self.state().expect("deadlocked");
state.callbacks.push(Box::new(map)); let mut callbacks = state.callbacks.lock().ignore_poison();
callbacks.push(Box::new(map));
} }
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R> pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
where where
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static, F: for<'a> FnMut() -> 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<T>| {
mapped_value.set(map(updated));
}));
returned
}
pub fn map_each_unique<R, F>(&self, mut map: F) -> Dynamic<R>
where
F: for<'a> FnMut(&'a GenerationalValue<T>) -> R + Send + 'static,
R: PartialEq + Send + 'static, R: PartialEq + Send + 'static,
{ {
let mut state = self.state().expect("deadlocked"); let initial_value = map();
let initial_value = map(&state.wrapped);
let mapped_value = Dynamic::new(initial_value); let mapped_value = Dynamic::new(initial_value);
let returned = mapped_value.clone(); let returned = mapped_value.clone();
state
.callbacks self.for_each(move || {
.push(Box::new(move |updated: &GenerationalValue<T>| { mapped_value.set(map());
let _deadlock = mapped_value.try_update(map(updated)); });
}));
returned returned
} }
} }
/// An error occurred while updating a value in a [`Dynamic`].
pub enum ReplaceError<T> {
/// 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`]. /// A deadlock occurred accessing a [`Dynamic`].
/// ///
/// Currently Gooey is only able to detect deadlocks where a single thread tries /// Currently Gooey is only able to detect deadlocks where a single thread tries
/// to lock the same [`Dynamic`] multiple times. /// to lock the same [`Dynamic`] multiple times.
#[derive(Debug)] #[derive(Debug)]
pub struct DeadlockError; struct DeadlockError;
impl std::error::Error for DeadlockError {} impl std::error::Error for DeadlockError {}
@ -723,7 +716,7 @@ impl Display for DeadlockError {
struct State<T> { struct State<T> {
wrapped: GenerationalValue<T>, wrapped: GenerationalValue<T>,
callbacks: Vec<Box<dyn ValueCallback<T>>>, callbacks: Arc<Mutex<Vec<Box<dyn ValueCallback>>>>,
windows: AHashSet<WindowHandle>, windows: AHashSet<WindowHandle>,
widgets: AHashSet<(WindowHandle, WidgetId)>, widgets: AHashSet<(WindowHandle, WidgetId)>,
wakers: Vec<Waker>, wakers: Vec<Waker>,
@ -731,12 +724,9 @@ struct State<T> {
} }
impl<T> State<T> { impl<T> State<T> {
fn note_changed(&mut self) { fn note_changed(&mut self) -> ChangeCallbacks {
self.wrapped.generation = self.wrapped.generation.next(); self.wrapped.generation = self.wrapped.generation.next();
for callback in &mut self.callbacks {
callback.update(&self.wrapped);
}
for (window, widget) in self.widgets.drain() { for (window, widget) in self.widgets.drain() {
window.invalidate(widget); window.invalidate(widget);
} }
@ -746,6 +736,8 @@ impl<T> State<T> {
for waker in self.wakers.drain(..) { for waker in self.wakers.drain(..) {
waker.wake(); waker.wake();
} }
ChangeCallbacks(self.callbacks.clone())
} }
} }
@ -761,16 +753,28 @@ where
} }
} }
trait ValueCallback<T>: Send { struct ChangeCallbacks(Arc<Mutex<Vec<Box<dyn ValueCallback>>>>);
fn update(&mut self, value: &GenerationalValue<T>);
impl Drop for ChangeCallbacks {
fn drop(&mut self) {
if let Ok(mut callbacks) = self.0.lock() {
for callback in &mut *callbacks {
callback.changed();
}
}
}
} }
impl<T, F> ValueCallback<T> for F trait ValueCallback: Send {
fn changed(&mut self);
}
impl<F> ValueCallback for F
where where
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static, F: for<'a> FnMut() + Send + 'static,
{ {
fn update(&mut self, value: &GenerationalValue<T>) { fn changed(&mut self) {
self(value); self();
} }
} }
@ -808,7 +812,10 @@ impl<'a, T> DerefMut for DynamicGuard<'a, T> {
impl<T> Drop for DynamicGuard<'_, T> { impl<T> Drop for DynamicGuard<'_, T> {
fn drop(&mut self) { fn drop(&mut self) {
if self.accessed_mut { 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<T> IntoDynamic<T> for Dynamic<T> {
impl<T, F> IntoDynamic<T> for F impl<T, F> IntoDynamic<T> for F
where where
F: FnMut(&T) + Send + 'static, F: FnMut(&T) + Send + 'static,
T: Default, T: Default + Send + 'static,
{ {
/// Returns [`Dynamic::default()`] with `self` installed as a for-each /// Returns [`Dynamic::default()`] with `self` installed as a for-each
/// callback. /// callback.
@ -1182,8 +1189,9 @@ impl<T> Value<T> {
#[must_use] #[must_use]
pub fn map_each<R, F>(&self, mut map: F) -> Value<R> pub fn map_each<R, F>(&self, mut map: F) -> Value<R>
where where
T: Send + 'static,
F: for<'a> FnMut(&'a T) -> R + Send + 'static, F: for<'a> FnMut(&'a T) -> R + Send + 'static,
R: Send + 'static, R: PartialEq + Send + 'static,
{ {
match self { match self {
Value::Constant(value) => Value::Constant(map(value)), Value::Constant(value) => Value::Constant(map(value)),
@ -1423,7 +1431,7 @@ macro_rules! impl_tuple_map_each {
($($type:ident $field:tt $var:ident),+) => { ($($type:ident $field:tt $var:ident),+) => {
impl<U, $($type),+> MapEach<($($type,)+), U> for ($(&Dynamic<$type>,)+) impl<U, $($type),+> MapEach<($($type,)+), U> for ($(&Dynamic<$type>,)+)
where where
U: Send + 'static, U: PartialEq + Send + 'static,
$($type: Send + 'static),+ $($type: Send + 'static),+
{ {
type Ref<'a> = ($(&'a $type,)+); type Ref<'a> = ($(&'a $type,)+);
@ -1451,3 +1459,144 @@ macro_rules! impl_tuple_map_each {
} }
impl_all_tuples!(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<T> {
/// Apply `for_each` to each value contained within `self`.
fn for_each_cloned<F>(&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<F>(&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<U>` from a `T` passed into a mapping
/// function.
pub trait MapEachCloned<T, U> {
/// Apply `map_each` to each value in `self`, storing the result in the
/// returned dynamic.
fn map_each_cloned<F>(&self, map_each: F) -> Dynamic<U>
where
F: for<'a> FnMut(T) -> U + Send + 'static;
}
macro_rules! impl_tuple_map_each_cloned {
($($type:ident $field:tt $var:ident),+) => {
impl<U, $($type),+> MapEachCloned<($($type,)+), U> for ($(&Dynamic<$type>,)+)
where
U: PartialEq + Send + 'static,
$($type: Clone + Send + 'static),+
{
fn map_each_cloned<F>(&self, mut map_each: F) -> Dynamic<U>
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);

View file

@ -243,7 +243,7 @@ impl Button {
.spawn(); .spawn();
} }
(true, Some(style)) => { (true, Some(style)) => {
style.update(new_style); style.set(new_style);
self.color_animation.clear(); self.color_animation.clear();
} }
_ => { _ => {

View file

@ -118,8 +118,8 @@ fn update_progress_bar(
} }
Progress::Percent(value) => { Progress::Percent(value) => {
let _stopped_animation = indeterminant_animation.take(); let _stopped_animation = indeterminant_animation.take();
start.update(ZeroToOne::ZERO); start.set(ZeroToOne::ZERO);
end.update(value); end.set(value);
} }
} }
} }
@ -127,7 +127,7 @@ fn update_progress_bar(
/// A value that can be used in a progress indicator. /// A value that can be used in a progress indicator.
pub trait Progressable<T>: IntoDynamic<T> + Sized pub trait Progressable<T>: IntoDynamic<T> + Sized
where where
T: ProgressValue, T: ProgressValue + Send,
{ {
/// Returns a new progress bar that displays progress from `T::MIN` to /// Returns a new progress bar that displays progress from `T::MIN` to
/// `T::MAX`. /// `T::MAX`.
@ -145,7 +145,7 @@ where
fn progress_bar_to(self, max: impl IntoValue<T::Value>) -> ProgressBar fn progress_bar_to(self, max: impl IntoValue<T::Value>) -> ProgressBar
where where
T: Send, T: Send,
T::Value: Ranged + Send + Clone, T::Value: PartialEq + Ranged + Send + Clone,
{ {
let max = max.into_value(); let max = max.into_value();
match max { match max {
@ -181,7 +181,7 @@ where
} }
} }
impl<U> Progressable<U> for Dynamic<U> where U: ProgressValue {} impl<U> Progressable<U> for Dynamic<U> where U: ProgressValue + Send {}
/// A value that can be used in a progress indicator. /// A value that can be used in a progress indicator.
pub trait ProgressValue: 'static { pub trait ProgressValue: 'static {

View file

@ -63,7 +63,7 @@ where
.into_columns() .into_columns()
.into_button() .into_button()
.on_click(move |()| { .on_click(move |()| {
self.state.update(self.value.clone()); self.state.set(self.value.clone());
}) })
.kind(self.kind) .kind(self.kind)
.make_widget() .make_widget()

View file

@ -225,7 +225,7 @@ impl Widget for Scroll {
let scroll_pct = scroll.y.into_float() / current_max_scroll.y.into_float(); let scroll_pct = scroll.y.into_float() / current_max_scroll.y.into_float();
scroll.y = max_scroll_y * scroll_pct; scroll.y = max_scroll_y * scroll_pct;
} }
self.scroll.update(scroll); self.scroll.set(scroll);
self.control_size = control_size; self.control_size = control_size;
self.content_size = new_content_size; self.content_size = new_content_size;

View file

@ -343,7 +343,7 @@ where
start = value; start = value;
self.focused_knob = Some(Knob::Start); 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) { fn step(&mut self, forwards: bool, factor: f32) {
@ -391,7 +391,7 @@ where
(Knob::Start, Some(end)) => (new_value, Some(end)), (Knob::Start, Some(end)) => (new_value, Some(end)),
(Knob::End, Some(start)) => (start, Some(new_value)), (Knob::End, Some(start)) => (start, Some(new_value)),
}; };
self.value.update(T::from_parts(start, end)); self.value.set(T::from_parts(start, end));
} }
} }

View file

@ -39,8 +39,7 @@ impl Space {
impl Widget for Space { impl Widget for Space {
fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) {
self.color.redraw_when_changed(context); let color = self.color.get_tracked(context);
let color = self.color.get();
context.fill(color); context.fill(color);
} }

View file

@ -172,7 +172,7 @@ impl Window<WidgetInstance> {
/// of `false`. /// of `false`.
pub fn focused(mut self, focused: impl IntoDynamic<bool>) -> Self { pub fn focused(mut self, focused: impl IntoDynamic<bool>) -> Self {
let focused = focused.into_dynamic(); let focused = focused.into_dynamic();
focused.update(false); focused.set(false);
self.focused = Some(focused); self.focused = Some(focused);
self self
} }
@ -187,7 +187,7 @@ impl Window<WidgetInstance> {
/// `occluded` will be initialized with an initial state of `false`. /// `occluded` will be initialized with an initial state of `false`.
pub fn occluded(mut self, occluded: impl IntoDynamic<bool>) -> Self { pub fn occluded(mut self, occluded: impl IntoDynamic<bool>) -> Self {
let occluded = occluded.into_dynamic(); let occluded = occluded.into_dynamic();
occluded.update(false); occluded.set(false);
self.occluded = Some(occluded); self.occluded = Some(occluded);
self self
} }
@ -528,7 +528,7 @@ where
let theme_mode = match settings.theme_mode.take() { let theme_mode = match settings.theme_mode.take() {
Some(Value::Dynamic(dynamic)) => { Some(Value::Dynamic(dynamic)) => {
dynamic.update(window.theme().into()); dynamic.set(window.theme().into());
Value::Dynamic(dynamic) Value::Dynamic(dynamic)
} }
Some(Value::Constant(mode)) => Value::Constant(mode), Some(Value::Constant(mode)) => Value::Constant(mode),
@ -660,7 +660,7 @@ where
window: kludgine::app::Window<'_, WindowCommand>, window: kludgine::app::Window<'_, WindowCommand>,
_kludgine: &mut Kludgine, _kludgine: &mut Kludgine,
) { ) {
self.focused.update(window.focused()); self.focused.set(window.focused());
} }
fn occlusion_changed( fn occlusion_changed(
@ -668,7 +668,7 @@ where
window: kludgine::app::Window<'_, WindowCommand>, window: kludgine::app::Window<'_, WindowCommand>,
_kludgine: &mut Kludgine, _kludgine: &mut Kludgine,
) { ) {
self.occluded.update(window.ocluded()); self.occluded.set(window.ocluded());
} }
fn render<'pass>( fn render<'pass>(
@ -1104,7 +1104,7 @@ where
_kludgine: &mut Kludgine, _kludgine: &mut Kludgine,
) { ) {
if let Value::Dynamic(theme_mode) = &self.theme_mode { if let Value::Dynamic(theme_mode) = &self.theme_mode {
theme_mode.update(window.theme().into()); theme_mode.set(window.theme().into());
} }
} }