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$)
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

View file

@ -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()

View file

@ -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);
}
});

View file

@ -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,

View file

@ -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, 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 {
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<ThemeMode>, impl MakeWidget) {
let theme_mode = Dynamic::default();
fn dark_mode_picker() -> (Dynamic<ThemeMode>, 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<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(),
"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<ColorSource>, impl MakeWidget) {
let hue = Dynamic::new(initial_color.hue.into_degrees());
fn color_editor(color: &Dynamic<ColorSource>) -> 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<FixedTheme>, label: &str) -> impl MakeWidget {
.expand()
}
fn theme(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget {
fn theme_preview(theme: Dynamic<Theme>, mode: ThemeMode) -> impl MakeWidget {
match mode {
ThemeMode::Light => "Light",
ThemeMode::Dark => "Dark",

View file

@ -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<GameState>) -> impl MakeWidg
return;
};
if enabled.update(false) {
label.update(player.to_string());
if enabled.replace(false).is_some() {
label.set(player.to_string());
}
});
});

View file

@ -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<T, Easing> {
/// 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<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.
pub trait Easing: Debug + Send + Sync + RefUnwindSafe + UnwindSafe + 'static {
/// 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>
where
T: Clone,
T: Clone + Send + 'static,
Component: From<T>,
{
fn into_component_value(self) -> Value<Component> {
@ -147,7 +147,7 @@ where
impl<T> IntoComponentValue for Dynamic<T>
where
T: Clone,
T: Clone + Send + 'static,
Component: From<T>,
{
fn into_component_value(self) -> Value<Component> {
@ -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<Dimension> 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<ZeroToOne> {
Some(self.saturation)
}
}
impl ProtoColor for f32 {
fn hue(&self) -> OklabHue {
(*self).into()

View file

@ -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.

View file

@ -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<T> Dynamic<T> {
value,
generation: Generation::default(),
},
callbacks: Vec::new(),
callbacks: Arc::default(),
windows: AHashSet::new(),
readers: 0,
wakers: Vec::new(),
@ -82,7 +83,7 @@ impl<T> Dynamic<T> {
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<T> Dynamic<T> {
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<T> Dynamic<T> {
#[must_use]
pub fn map_each_into<U>(&self) -> Dynamic<U>
where
U: From<T> + Send + 'static,
T: Clone,
U: PartialEq + From<T> + Send + 'static,
T: Clone + Send + 'static,
{
self.map_each(|value| U::from(value.clone()))
}
@ -164,8 +165,8 @@ impl<T> Dynamic<T> {
#[must_use]
pub fn map_each_to<U>(&self) -> Dynamic<U>
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<T> Dynamic<T> {
/// value's contents are updated.
pub fn for_each<F>(&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<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
/// value's contents are updated. This function returns `self`.
#[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
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<T> Dynamic<T> {
/// each time this value is changed.
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
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<R, F>(&self, mut map: F) -> Dynamic<R>
pub fn map_each_cloned<R, F>(&self, mut map: F) -> Dynamic<R>
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<T> Dynamic<T> {
/// 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<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.
#[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<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,
/// 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<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.
///
/// # 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> {
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<T> DynamicData<T> {
pub fn for_each<F>(&self, map: F)
where
F: for<'a> FnMut(&'a GenerationalValue<T>) + 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<R, F>(&self, mut map: F) -> Dynamic<R>
where
F: for<'a> FnMut(&'a GenerationalValue<T>) -> 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,
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<T>| {
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<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`].
///
/// 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<T> {
wrapped: GenerationalValue<T>,
callbacks: Vec<Box<dyn ValueCallback<T>>>,
callbacks: Arc<Mutex<Vec<Box<dyn ValueCallback>>>>,
windows: AHashSet<WindowHandle>,
widgets: AHashSet<(WindowHandle, WidgetId)>,
wakers: Vec<Waker>,
@ -731,12 +724,9 @@ struct State<T> {
}
impl<T> State<T> {
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<T> State<T> {
for waker in self.wakers.drain(..) {
waker.wake();
}
ChangeCallbacks(self.callbacks.clone())
}
}
@ -761,16 +753,28 @@ where
}
}
trait ValueCallback<T>: Send {
fn update(&mut self, value: &GenerationalValue<T>);
struct ChangeCallbacks(Arc<Mutex<Vec<Box<dyn ValueCallback>>>>);
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
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
F: for<'a> FnMut() + Send + 'static,
{
fn update(&mut self, value: &GenerationalValue<T>) {
self(value);
fn changed(&mut self) {
self();
}
}
@ -808,7 +812,10 @@ impl<'a, T> DerefMut for DynamicGuard<'a, T> {
impl<T> 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<T> IntoDynamic<T> for Dynamic<T> {
impl<T, F> IntoDynamic<T> 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<T> Value<T> {
#[must_use]
pub fn map_each<R, F>(&self, mut map: F) -> Value<R>
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<U, $($type),+> 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<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();
}
(true, Some(style)) => {
style.update(new_style);
style.set(new_style);
self.color_animation.clear();
}
_ => {

View file

@ -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<T>: IntoDynamic<T> + 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<T::Value>) -> 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<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.
pub trait ProgressValue: 'static {

View file

@ -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()

View file

@ -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;

View file

@ -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));
}
}

View file

@ -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);
}

View file

@ -172,7 +172,7 @@ impl Window<WidgetInstance> {
/// of `false`.
pub fn focused(mut self, focused: impl IntoDynamic<bool>) -> Self {
let focused = focused.into_dynamic();
focused.update(false);
focused.set(false);
self.focused = Some(focused);
self
}
@ -187,7 +187,7 @@ impl Window<WidgetInstance> {
/// `occluded` will be initialized with an initial state of `false`.
pub fn occluded(mut self, occluded: impl IntoDynamic<bool>) -> 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());
}
}