Callback handles are now managed

Installing a callback now returns a CallbackHandle. All map-style APIs
install this handle automatically on the created dynamic, which keeps
the callback installed until the dynamic is freed. All other APIs
return the handle for the caller to either call persist() or store
somewhere.

Now, the dynamic system can be used for application-long data with
almost no fear of leaking data due to how callbacks are being installed.
Technically cycles are still possible by moving clones into the
callbacks, so a WeakDynamic type might be worth exposing.
This commit is contained in:
Jonathan Johnson 2023-12-01 13:31:42 -08:00
parent a3e45d1d86
commit 3f8885efbe
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
7 changed files with 113 additions and 47 deletions

3
Cargo.lock generated
View file

@ -48,8 +48,7 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
[[package]] [[package]]
name = "alot" name = "alot"
version = "0.3.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/khonsulabs/alot#8868932a271587bdabcd2aeb18aa960fa1b5c79e"
checksum = "0f1063fbee2ac6d371ffcb55cb443f4d5b469f1b8eace60042878242bb534169"
[[package]] [[package]]
name = "android-activity" name = "android-activity"

View file

@ -39,7 +39,8 @@ unicode-segmentation = "1.10.1"
# [patch."https://github.com/khonsulabs/figures"] # [patch."https://github.com/khonsulabs/figures"]
# figures = { path = "../figures" } # figures = { path = "../figures" }
# [patch.crates-io] [patch.crates-io]
alot = { git = "https://github.com/khonsulabs/alot" }
# kempt = { path = "../objectmap" } # kempt = { path = "../objectmap" }
[profile.dev.package."*"] [profile.dev.package."*"]

View file

@ -112,14 +112,16 @@ fn main() -> gooey::Result {
scheme.build() scheme.build()
}, },
); );
color_scheme.for_each_cloned(move |scheme| { color_scheme
sources.primary.set(scheme.primary); .for_each_cloned(move |scheme| {
sources.secondary.set(scheme.secondary); sources.primary.set(scheme.primary);
sources.tertiary.set(scheme.tertiary); sources.secondary.set(scheme.secondary);
sources.error.set(scheme.error); sources.tertiary.set(scheme.tertiary);
sources.neutral.set(scheme.neutral); sources.error.set(scheme.error);
sources.neutral_variant.set(scheme.neutral_variant); sources.neutral.set(scheme.neutral);
}); sources.neutral_variant.set(scheme.neutral_variant);
})
.persist();
let theme = color_scheme.map_each_cloned(ThemePair::from); let theme = color_scheme.map_each_cloned(ThemePair::from);
let editors = theme_switcher let editors = theme_switcher
@ -198,18 +200,21 @@ fn color_editor(color: &Dynamic<ColorSource>) -> impl MakeWidget {
source.hue = OklabHue::new(hue); source.hue = OklabHue::new(hue);
color.set(source); color.set(source);
} }
}); })
.persist();
let hue_text = hue.linked_string(); let hue_text = hue.linked_string();
let saturation = color.map_each(|color| color.saturation); let saturation = color.map_each(|color| color.saturation);
saturation.for_each_cloned({ saturation
let color = color.clone(); .for_each_cloned({
move |saturation| { let color = color.clone();
let mut source = color.get(); move |saturation| {
source.saturation = saturation; let mut source = color.get();
color.set(source); source.saturation = saturation;
} color.set(source);
}); }
})
.persist();
let saturation_text = saturation.linked_string(); let saturation_text = saturation.linked_string();
hue.slider_between(0., 359.99) hue.slider_between(0., 359.99)

View file

@ -189,7 +189,8 @@ fn square(row: usize, column: usize, game: &Dynamic<GameState>) -> impl MakeWidg
if enabled.replace(false).is_some() { if enabled.replace(false).is_some() {
label.set(player.to_string()); label.set(player.to_string());
} }
}); })
.persist();
}); });
label label

View file

@ -7,11 +7,13 @@ use gooey::Run;
fn main() -> gooey::Result { fn main() -> gooey::Result {
let tasks = Dynamic::default(); let tasks = Dynamic::default();
let children = Dynamic::default(); let children = Dynamic::default();
tasks.for_each(children.with_clone(|children| { tasks
move |tasks: &Vec<Task>| { .for_each(children.with_clone(|children| {
update_task_widgets(tasks, &mut children.lock()); move |tasks: &Vec<Task>| {
} update_task_widgets(tasks, &mut children.lock());
})); }
}))
.persist();
let task_text = Dynamic::default(); let task_text = Dynamic::default();
let valid = task_text.map_each(|text: &String| !text.is_empty()); let valid = task_text.map_each(|text: &String| !text.is_empty());

View file

@ -14,6 +14,8 @@ use std::thread::ThreadId;
use std::time::Duration; use std::time::Duration;
use ahash::AHashSet; use ahash::AHashSet;
use alot::{LotId, Lots};
use intentional::Assert;
use kempt::{Map, Sort}; use kempt::{Map, Sort};
use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn}; use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
@ -41,6 +43,7 @@ impl<T> Dynamic<T> {
readers: 0, readers: 0,
wakers: Vec::new(), wakers: Vec::new(),
widgets: AHashSet::new(), widgets: AHashSet::new(),
source_callback: None,
}), }),
during_callback_state: Mutex::default(), during_callback_state: Mutex::default(),
sync: UnwindsafeCondvar::default(), sync: UnwindsafeCondvar::default(),
@ -87,7 +90,8 @@ impl<T> Dynamic<T> {
if let Some(update) = t_into_r(t).into() { if let Some(update) = t_into_r(t).into() {
let _result = r.replace(update); let _result = r.replace(update);
} }
}); })
.persist();
}); });
self.with_clone(|t| { self.with_clone(|t| {
@ -162,6 +166,12 @@ impl<T> Dynamic<T> {
}) })
} }
fn set_source(&self, source: CallbackHandle) {
self.state()
.assert("called during initialization")
.source_callback = Some(source);
}
/// Returns a new dynamic that contains the updated contents of this dynamic /// Returns a new dynamic that contains the updated contents of this dynamic
/// at most once every `period`. /// at most once every `period`.
#[must_use] #[must_use]
@ -171,7 +181,8 @@ impl<T> Dynamic<T> {
{ {
let debounced = Dynamic::new(self.get()); let debounced = Dynamic::new(self.get());
let mut debounce = Debounce::new(debounced.clone(), period); let mut debounce = Debounce::new(debounced.clone(), period);
self.for_each_cloned(move |value| debounce.update(value)); let callback = self.for_each_cloned(move |value| debounce.update(value));
debounced.set_source(callback);
debounced debounced
} }
@ -185,7 +196,8 @@ impl<T> Dynamic<T> {
{ {
let debounced = Dynamic::new(self.get()); let debounced = Dynamic::new(self.get());
let mut debounce = Debounce::new(debounced.clone(), period).extending(); let mut debounce = Debounce::new(debounced.clone(), period).extending();
self.for_each_cloned(move |value| debounce.update(value)); let callback = self.for_each_cloned(move |value| debounce.update(value));
debounced.set_source(callback);
debounced debounced
} }
@ -213,7 +225,7 @@ impl<T> Dynamic<T> {
/// 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. /// 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) -> CallbackHandle
where where
T: Send + 'static, T: Send + 'static,
F: for<'a> FnMut(&'a T) + Send + 'static, F: for<'a> FnMut(&'a T) + Send + 'static,
@ -221,12 +233,12 @@ impl<T> Dynamic<T> {
let this = self.clone(); let this = self.clone();
self.0.for_each(move || { self.0.for_each(move || {
this.map_ref(&mut for_each); this.map_ref(&mut for_each);
}); })
} }
/// Attaches `for_each` to this value and its [`Generation`] so that it is /// Attaches `for_each` to this value and its [`Generation`] so that it is
/// invoked each time the value's contents are updated. /// invoked each time the value's contents are updated.
pub fn for_each_generational<F>(&self, mut for_each: F) pub fn for_each_generational<F>(&self, mut for_each: F) -> CallbackHandle
where where
T: Send + 'static, T: Send + 'static,
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static, F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
@ -234,12 +246,12 @@ impl<T> Dynamic<T> {
let this = self.clone(); let this = self.clone();
self.0.for_each(move || { self.0.for_each(move || {
this.map_generational(&mut for_each); this.map_generational(&mut for_each);
}); })
} }
/// 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. /// value's contents are updated.
pub fn for_each_cloned<F>(&self, mut for_each: F) pub fn for_each_cloned<F>(&self, mut for_each: F) -> CallbackHandle
where where
T: Clone + Send + 'static, T: Clone + Send + 'static,
F: FnMut(T) + Send + 'static, F: FnMut(T) + Send + 'static,
@ -247,7 +259,7 @@ impl<T> Dynamic<T> {
let this = self.clone(); let this = self.clone();
self.0.for_each(move || { self.0.for_each(move || {
for_each(this.get()); 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
@ -258,7 +270,7 @@ impl<T> Dynamic<T> {
T: Send + 'static, T: Send + 'static,
F: for<'a> FnMut(&'a T) + Send + 'static, F: for<'a> FnMut(&'a T) + Send + 'static,
{ {
self.for_each(for_each); self.for_each(for_each).persist();
self self
} }
@ -584,7 +596,7 @@ impl<T> Dynamic<T> {
E: Display, E: Display,
{ {
let validation = Dynamic::new(Validation::None); let validation = Dynamic::new(Validation::None);
self.for_each({ let callback = self.for_each({
let validation = validation.clone(); let validation = validation.clone();
move |value| { move |value| {
validation.set(match check(value) { validation.set(match check(value) {
@ -593,6 +605,7 @@ impl<T> Dynamic<T> {
}); });
} }
}); });
validation.set_source(callback);
validation validation
} }
} }
@ -806,13 +819,16 @@ impl<T> DynamicData<T> {
Ok(old) Ok(old)
} }
pub fn for_each<F>(&self, map: F) pub fn for_each<F>(&self, map: F) -> CallbackHandle
where where
F: for<'a> FnMut() + Send + 'static, F: for<'a> FnMut() + Send + 'static,
{ {
let state = self.state().expect("deadlocked"); let state = self.state().expect("deadlocked");
let mut callbacks = state.callbacks.callbacks.lock().ignore_poison(); let mut callbacks = state.callbacks.callbacks.lock().ignore_poison();
callbacks.push(Box::new(map)); CallbackHandle {
id: Some(callbacks.push(Box::new(map))),
callbacks: state.callbacks.clone(),
}
} }
pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R> pub fn map_each<R, F>(&self, mut map: F) -> Dynamic<R>
@ -824,10 +840,12 @@ impl<T> DynamicData<T> {
let mapped_value = Dynamic::new(initial_value); let mapped_value = Dynamic::new(initial_value);
let returned = mapped_value.clone(); let returned = mapped_value.clone();
self.for_each(move || { let callback = self.for_each(move || {
mapped_value.set(map()); mapped_value.set(map());
}); });
returned.set_source(callback);
returned returned
} }
} }
@ -855,8 +873,46 @@ impl Display for DeadlockError {
} }
} }
/// A handle to a callback installed on a [`Dynamic`]. When dropped, the
/// callback will be uninstalled.
///
/// To prevent the callback from ever being uninstalled, use
/// [`Self::persist()`].
#[must_use]
pub struct CallbackHandle {
id: Option<LotId>,
callbacks: Arc<ChangeCallbacksData>,
}
impl Debug for CallbackHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CallbackHandle")
.field("id", &self.id)
.finish_non_exhaustive()
}
}
impl CallbackHandle {
/// Persists the callback so that it will always be invoked until the
/// dynamic is freed.
pub fn persist(mut self) {
let _id = self.id.take();
drop(self);
}
}
impl Drop for CallbackHandle {
fn drop(&mut self) {
if let Some(id) = self.id {
let mut callbacks = self.callbacks.callbacks.lock().ignore_poison();
callbacks.remove(id);
}
}
}
struct State<T> { struct State<T> {
wrapped: GenerationalValue<T>, wrapped: GenerationalValue<T>,
source_callback: Option<CallbackHandle>,
callbacks: Arc<ChangeCallbacksData>, callbacks: Arc<ChangeCallbacksData>,
windows: AHashSet<WindowHandle>, windows: AHashSet<WindowHandle>,
widgets: AHashSet<(WindowHandle, WidgetId)>, widgets: AHashSet<(WindowHandle, WidgetId)>,
@ -896,7 +952,7 @@ where
#[derive(Default)] #[derive(Default)]
struct ChangeCallbacksData { struct ChangeCallbacksData {
callbacks: Mutex<Vec<Box<dyn ValueCallback>>>, callbacks: Mutex<Lots<Box<dyn ValueCallback>>>,
currently_executing: AtomicBool, currently_executing: AtomicBool,
} }
@ -1617,7 +1673,7 @@ macro_rules! impl_tuple_for_each {
} }
}; };
($self:ident $for_each:ident [] [$type:ident $field:tt $var:ident]) => { ($self:ident $for_each:ident [] [$type:ident $field:tt $var:ident]) => {
$self.$field.for_each(move |field: &$type| $for_each((field,))); $self.$field.for_each(move |field: &$type| $for_each((field,))).persist();
}; };
($self:ident $for_each:ident [] [$($type:ident $field:tt $var:ident),+]) => { ($self:ident $for_each:ident [] [$($type:ident $field:tt $var:ident),+]) => {
let $for_each = Arc::new(Mutex::new($for_each)); let $for_each = Arc::new(Mutex::new($for_each));
@ -1684,7 +1740,7 @@ macro_rules! impl_tuple_for_each {
for_each.lock().ignore_poison(); for_each.lock().ignore_poison();
(for_each)(($(&$avar,)+)); (for_each)(($(&$avar,)+));
} }
})); })).persist();
}; };
} }
@ -1761,7 +1817,7 @@ macro_rules! impl_tuple_for_each_cloned {
} }
}; };
($self:ident $for_each:ident [] [$type:ident $field:tt $var:ident]) => { ($self:ident $for_each:ident [] [$type:ident $field:tt $var:ident]) => {
$self.$field.for_each(move |field: &$type| $for_each((field.clone(),))); $self.$field.for_each(move |field: &$type| $for_each((field.clone(),))).persist();
}; };
($self:ident $for_each:ident [] [$($type:ident $field:tt $var:ident),+]) => { ($self:ident $for_each:ident [] [$($type:ident $field:tt $var:ident),+]) => {
let $for_each = Arc::new(Mutex::new($for_each)); let $for_each = Arc::new(Mutex::new($for_each));
@ -1829,7 +1885,7 @@ macro_rules! impl_tuple_for_each_cloned {
(for_each)(($($avar,)+)); (for_each)(($($avar,)+));
} }
} }
})); })).persist();
}; };
} }
@ -2203,6 +2259,7 @@ struct Debounce<T> {
delay: Option<AnimationHandle>, delay: Option<AnimationHandle>,
buffer: Dynamic<T>, buffer: Dynamic<T>,
extend: bool, extend: bool,
_callback: Option<CallbackHandle>,
} }
impl<T> Debounce<T> impl<T> Debounce<T>
@ -2216,6 +2273,7 @@ where
period, period,
delay: None, delay: None,
extend: false, extend: false,
_callback: None,
} }
} }

View file

@ -64,10 +64,10 @@ impl MakeWidgetWithId for ProgressBar {
let slider = value.slider().knobless().non_interactive().make_with_id(id); let slider = value.slider().knobless().non_interactive().make_with_id(id);
match self.progress { match self.progress {
Value::Dynamic(progress) => { Value::Dynamic(progress) => {
progress.for_each(move |progress| { let callback = progress.for_each(move |progress| {
update_progress_bar(*progress, &mut indeterminant_animation, &start, &end); update_progress_bar(*progress, &mut indeterminant_animation, &start, &end);
}); });
Data::new_wrapping(progress, slider).make_widget() Data::new_wrapping((callback, progress), slider).make_widget()
} }
Value::Constant(_) => Data::new_wrapping(indeterminant_animation, slider).make_widget(), Value::Constant(_) => Data::new_wrapping(indeterminant_animation, slider).make_widget(),
} }