mirror of
https://github.com/danbulant/cushy
synced 2026-06-20 15:01:11 +00:00
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:
parent
a3e45d1d86
commit
3f8885efbe
7 changed files with 113 additions and 47 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -48,8 +48,7 @@ checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5"
|
|||
[[package]]
|
||||
name = "alot"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f1063fbee2ac6d371ffcb55cb443f4d5b469f1b8eace60042878242bb534169"
|
||||
source = "git+https://github.com/khonsulabs/alot#8868932a271587bdabcd2aeb18aa960fa1b5c79e"
|
||||
|
||||
[[package]]
|
||||
name = "android-activity"
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ unicode-segmentation = "1.10.1"
|
|||
# [patch."https://github.com/khonsulabs/figures"]
|
||||
# figures = { path = "../figures" }
|
||||
|
||||
# [patch.crates-io]
|
||||
[patch.crates-io]
|
||||
alot = { git = "https://github.com/khonsulabs/alot" }
|
||||
# kempt = { path = "../objectmap" }
|
||||
|
||||
[profile.dev.package."*"]
|
||||
|
|
|
|||
|
|
@ -112,14 +112,16 @@ fn main() -> gooey::Result {
|
|||
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);
|
||||
});
|
||||
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);
|
||||
})
|
||||
.persist();
|
||||
let theme = color_scheme.map_each_cloned(ThemePair::from);
|
||||
|
||||
let editors = theme_switcher
|
||||
|
|
@ -198,18 +200,21 @@ fn color_editor(color: &Dynamic<ColorSource>) -> impl MakeWidget {
|
|||
source.hue = OklabHue::new(hue);
|
||||
color.set(source);
|
||||
}
|
||||
});
|
||||
})
|
||||
.persist();
|
||||
|
||||
let hue_text = hue.linked_string();
|
||||
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);
|
||||
}
|
||||
});
|
||||
saturation
|
||||
.for_each_cloned({
|
||||
let color = color.clone();
|
||||
move |saturation| {
|
||||
let mut source = color.get();
|
||||
source.saturation = saturation;
|
||||
color.set(source);
|
||||
}
|
||||
})
|
||||
.persist();
|
||||
let saturation_text = saturation.linked_string();
|
||||
|
||||
hue.slider_between(0., 359.99)
|
||||
|
|
|
|||
|
|
@ -189,7 +189,8 @@ fn square(row: usize, column: usize, game: &Dynamic<GameState>) -> impl MakeWidg
|
|||
if enabled.replace(false).is_some() {
|
||||
label.set(player.to_string());
|
||||
}
|
||||
});
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
label
|
||||
|
|
|
|||
|
|
@ -7,11 +7,13 @@ use gooey::Run;
|
|||
fn main() -> gooey::Result {
|
||||
let tasks = Dynamic::default();
|
||||
let children = Dynamic::default();
|
||||
tasks.for_each(children.with_clone(|children| {
|
||||
move |tasks: &Vec<Task>| {
|
||||
update_task_widgets(tasks, &mut children.lock());
|
||||
}
|
||||
}));
|
||||
tasks
|
||||
.for_each(children.with_clone(|children| {
|
||||
move |tasks: &Vec<Task>| {
|
||||
update_task_widgets(tasks, &mut children.lock());
|
||||
}
|
||||
}))
|
||||
.persist();
|
||||
|
||||
let task_text = Dynamic::default();
|
||||
let valid = task_text.map_each(|text: &String| !text.is_empty());
|
||||
|
|
|
|||
96
src/value.rs
96
src/value.rs
|
|
@ -14,6 +14,8 @@ use std::thread::ThreadId;
|
|||
use std::time::Duration;
|
||||
|
||||
use ahash::AHashSet;
|
||||
use alot::{LotId, Lots};
|
||||
use intentional::Assert;
|
||||
use kempt::{Map, Sort};
|
||||
|
||||
use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn};
|
||||
|
|
@ -41,6 +43,7 @@ impl<T> Dynamic<T> {
|
|||
readers: 0,
|
||||
wakers: Vec::new(),
|
||||
widgets: AHashSet::new(),
|
||||
source_callback: None,
|
||||
}),
|
||||
during_callback_state: Mutex::default(),
|
||||
sync: UnwindsafeCondvar::default(),
|
||||
|
|
@ -87,7 +90,8 @@ impl<T> Dynamic<T> {
|
|||
if let Some(update) = t_into_r(t).into() {
|
||||
let _result = r.replace(update);
|
||||
}
|
||||
});
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
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
|
||||
/// at most once every `period`.
|
||||
#[must_use]
|
||||
|
|
@ -171,7 +181,8 @@ impl<T> Dynamic<T> {
|
|||
{
|
||||
let debounced = Dynamic::new(self.get());
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +196,8 @@ impl<T> Dynamic<T> {
|
|||
{
|
||||
let debounced = Dynamic::new(self.get());
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +225,7 @@ impl<T> Dynamic<T> {
|
|||
|
||||
/// Attaches `for_each` to this value so that it is invoked each time the
|
||||
/// 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
|
||||
T: Send + 'static,
|
||||
F: for<'a> FnMut(&'a T) + Send + 'static,
|
||||
|
|
@ -221,12 +233,12 @@ impl<T> Dynamic<T> {
|
|||
let this = self.clone();
|
||||
self.0.for_each(move || {
|
||||
this.map_ref(&mut for_each);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/// Attaches `for_each` to this value and its [`Generation`] so that it is
|
||||
/// invoked each time the value's contents are updated.
|
||||
pub fn for_each_generational<F>(&self, mut for_each: F)
|
||||
pub fn for_each_generational<F>(&self, mut for_each: F) -> CallbackHandle
|
||||
where
|
||||
T: Send + 'static,
|
||||
F: for<'a> FnMut(&'a GenerationalValue<T>) + Send + 'static,
|
||||
|
|
@ -234,12 +246,12 @@ impl<T> Dynamic<T> {
|
|||
let this = self.clone();
|
||||
self.0.for_each(move || {
|
||||
this.map_generational(&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)
|
||||
pub fn for_each_cloned<F>(&self, mut for_each: F) -> CallbackHandle
|
||||
where
|
||||
T: Clone + Send + 'static,
|
||||
F: FnMut(T) + Send + 'static,
|
||||
|
|
@ -247,7 +259,7 @@ impl<T> Dynamic<T> {
|
|||
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
|
||||
|
|
@ -258,7 +270,7 @@ impl<T> Dynamic<T> {
|
|||
T: Send + 'static,
|
||||
F: for<'a> FnMut(&'a T) + Send + 'static,
|
||||
{
|
||||
self.for_each(for_each);
|
||||
self.for_each(for_each).persist();
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -584,7 +596,7 @@ impl<T> Dynamic<T> {
|
|||
E: Display,
|
||||
{
|
||||
let validation = Dynamic::new(Validation::None);
|
||||
self.for_each({
|
||||
let callback = self.for_each({
|
||||
let validation = validation.clone();
|
||||
move |value| {
|
||||
validation.set(match check(value) {
|
||||
|
|
@ -593,6 +605,7 @@ impl<T> Dynamic<T> {
|
|||
});
|
||||
}
|
||||
});
|
||||
validation.set_source(callback);
|
||||
validation
|
||||
}
|
||||
}
|
||||
|
|
@ -806,13 +819,16 @@ impl<T> DynamicData<T> {
|
|||
Ok(old)
|
||||
}
|
||||
|
||||
pub fn for_each<F>(&self, map: F)
|
||||
pub fn for_each<F>(&self, map: F) -> CallbackHandle
|
||||
where
|
||||
F: for<'a> FnMut() + Send + 'static,
|
||||
{
|
||||
let state = self.state().expect("deadlocked");
|
||||
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>
|
||||
|
|
@ -824,10 +840,12 @@ impl<T> DynamicData<T> {
|
|||
let mapped_value = Dynamic::new(initial_value);
|
||||
let returned = mapped_value.clone();
|
||||
|
||||
self.for_each(move || {
|
||||
let callback = self.for_each(move || {
|
||||
mapped_value.set(map());
|
||||
});
|
||||
|
||||
returned.set_source(callback);
|
||||
|
||||
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> {
|
||||
wrapped: GenerationalValue<T>,
|
||||
source_callback: Option<CallbackHandle>,
|
||||
callbacks: Arc<ChangeCallbacksData>,
|
||||
windows: AHashSet<WindowHandle>,
|
||||
widgets: AHashSet<(WindowHandle, WidgetId)>,
|
||||
|
|
@ -896,7 +952,7 @@ where
|
|||
|
||||
#[derive(Default)]
|
||||
struct ChangeCallbacksData {
|
||||
callbacks: Mutex<Vec<Box<dyn ValueCallback>>>,
|
||||
callbacks: Mutex<Lots<Box<dyn ValueCallback>>>,
|
||||
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.$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),+]) => {
|
||||
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)(($(&$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.$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),+]) => {
|
||||
let $for_each = Arc::new(Mutex::new($for_each));
|
||||
|
|
@ -1829,7 +1885,7 @@ macro_rules! impl_tuple_for_each_cloned {
|
|||
(for_each)(($($avar,)+));
|
||||
}
|
||||
}
|
||||
}));
|
||||
})).persist();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -2203,6 +2259,7 @@ struct Debounce<T> {
|
|||
delay: Option<AnimationHandle>,
|
||||
buffer: Dynamic<T>,
|
||||
extend: bool,
|
||||
_callback: Option<CallbackHandle>,
|
||||
}
|
||||
|
||||
impl<T> Debounce<T>
|
||||
|
|
@ -2216,6 +2273,7 @@ where
|
|||
period,
|
||||
delay: None,
|
||||
extend: false,
|
||||
_callback: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -64,10 +64,10 @@ impl MakeWidgetWithId for ProgressBar {
|
|||
let slider = value.slider().knobless().non_interactive().make_with_id(id);
|
||||
match self.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);
|
||||
});
|
||||
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(),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue