diff --git a/CHANGELOG.md b/CHANGELOG.md index fa03145..ed4b410 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The root widget is now included in the search for widgets to accept focus. - Widgets that have been laid out with a 0px width or height no longer have their `redraw` functions called nor can they receive focus. +- `Grid` now synchronizes removal of widgets from `GridWidgets` correctly. ### Added @@ -27,6 +28,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 focused widget will be activated and deactived by the events. This previously was a `Button`-specific behavior that has been refactored into an automatic behavior for all widgets. +- `GridWidgets` now implements `FromIterator` for types that implement + `Into>`. +- `Window::titled` allows setting a window's title, and can be provided a + string-type or a `Dynamic` to allow updating the title while the + window is open. +- `Dynamic::weak_clone` returns a new dynamic that is updated from the original + dynamic, but is careful to not add any strong references to the source + `Dynamic`. This allows breaking dynamic graphs into independent "sections" + that can be deallocated independently of other graphs it is connected with. +- `DynamicReader::on_disconnect` allows attaching a callback that is invoked + once the final source `Dynamic` is dropped. [99]: https://github.com/khonsulabs/cushy/issues/99 diff --git a/src/value.rs b/src/value.rs index e5f3a4e..b761d92 100644 --- a/src/value.rs +++ b/src/value.rs @@ -19,7 +19,9 @@ use kempt::{Map, Sort}; use crate::animation::{AnimationHandle, DynamicTransition, IntoAnimate, LinearInterpolate, Spawn}; use crate::context::{self, WidgetContext}; use crate::utils::{run_in_bg, IgnorePoison, WithClone}; -use crate::widget::{Children, MakeWidget, MakeWidgetWithTag, WidgetId, WidgetInstance}; +use crate::widget::{ + Children, MakeWidget, MakeWidgetWithTag, OnceCallback, WidgetId, WidgetInstance, +}; use crate::widgets::{Radio, Select, Space, Switcher}; use crate::window::WindowHandle; @@ -41,6 +43,7 @@ impl Dynamic { readers: 0, wakers: Vec::new(), widgets: AHashSet::new(), + on_disconnect: Vec::new(), source_callback: CallbackHandle::default(), }), during_callback_state: Mutex::default(), @@ -234,6 +237,28 @@ impl Dynamic { self.map_each(|value| U::from(value)) } + /// Returns a new dynamic that contains a weak clone of `self`'s contents. + /// + /// The returned `Dynamic` does not use any strong references, ensuring the + /// returned dynamic does not extend the lifetime of `self`. + #[must_use] + pub fn weak_clone(&self) -> Self + where + T: Clone + PartialEq + Send + 'static, + { + let weak_source = self.downgrade(); + let weak_out = Dynamic::new(self.get()); + weak_out.set_source(self.0.for_each({ + let weak_out = weak_out.clone(); + move || { + if let Some(source) = weak_source.upgrade() { + weak_out.set(source.get()); + } + } + })); + weak_out + } + /// Attaches `for_each` to this value so that it is invoked each time the /// value's contents are updated. pub fn for_each(&self, mut for_each: F) -> CallbackHandle @@ -756,9 +781,15 @@ impl Clone for Dynamic { impl Drop for Dynamic { fn drop(&mut self) { - let state = self.state().expect("deadlocked"); - if state.readers == 0 { + let mut state = self.state().expect("deadlocked"); + if Arc::strong_count(&self.0) == state.readers + 1 { + let on_disconnect = std::mem::take(&mut state.on_disconnect); drop(state); + + for on_disconnect in on_disconnect { + on_disconnect.invoke(()); + } + self.0.sync.notify_all(); } } @@ -1117,6 +1148,7 @@ struct State { windows: AHashSet, widgets: AHashSet<(WindowHandle, WidgetId)>, wakers: Vec, + on_disconnect: Vec, readers: usize, } @@ -1550,6 +1582,12 @@ impl DynamicReader { } } + /// Returns true if this reader still has any writers connected to it. + #[must_use] + pub fn connected(&self) -> bool { + self.source.state.lock().ignore_poison().readers < Arc::strong_count(&self.source) + } + /// Suspends the current async task until the contained value has been /// updated or there are no remaining writers for the value. /// @@ -1557,6 +1595,26 @@ impl DynamicReader { pub fn wait_until_updated(&mut self) -> BlockUntilUpdatedFuture<'_, T> { BlockUntilUpdatedFuture(self) } + + /// Invokes `on_disconnect` when no instances of `Dynamic` exist. + /// + /// This callback will be invoked even if this `DynamicReader` has been + /// dropped. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + pub fn on_disconnect(&self, on_disconnect: OnDisconnect) + where + OnDisconnect: FnOnce() + Send + 'static, + { + self.source + .state() + .expect("deadlocked") + .on_disconnect + .push(OnceCallback::new(|()| on_disconnect())); + } } impl context::sealed::Trackable for DynamicReader { diff --git a/src/widgets/grid.rs b/src/widgets/grid.rs index fcbd19c..18bf7c0 100644 --- a/src/widgets/grid.rs +++ b/src/widgets/grid.rs @@ -69,10 +69,10 @@ impl Grid { fn synchronize_specs(&mut self, context: &mut EventContext<'_, '_>) { let current_generation = self.columns.generation(); - if current_generation.map_or_else( - || self.layout.children.len() != ELEMENTS, - |gen| Some(gen) != self.spec_generation, - ) { + let count_changed = self.layout.children.len() != ELEMENTS; + if count_changed + || current_generation.map_or_else(|| true, |gen| Some(gen) != self.spec_generation) + { self.spec_generation = current_generation; self.columns.map(|columns| { self.layout.truncate(0); @@ -822,6 +822,15 @@ where } } +impl FromIterator for GridWidgets +where + A: Into>, +{ + fn from_iter>(iter: T) -> Self { + Self(iter.into_iter().map(A::into).collect()) + } +} + impl Deref for GridWidgets { type Target = Vec>; diff --git a/src/window.rs b/src/window.rs index a62e7fb..e1b1822 100644 --- a/src/window.rs +++ b/src/window.rs @@ -139,7 +139,7 @@ pub type WindowAttributes = kludgine::app::WindowAttributes; /// A Cushy window that is not yet running. #[must_use] -pub struct Window +pub struct Window where Behavior: WindowBehavior, { @@ -147,6 +147,8 @@ where pending: PendingWindow, /// The attributes of this window. pub attributes: WindowAttributes, + /// The title to display in the title bar of the window. + pub title: Value, /// The colors to use to theme the user interface. pub theme: Value, /// When true, the system fonts will be loaded into the font database. This @@ -280,6 +282,12 @@ impl Window { self.on_closed = Some(OnceCallback::new(|()| on_close())); self } + + /// Sets the window's title. + pub fn titled(mut self, title: impl IntoValue) -> Self { + self.title = title.into_value(); + self + } } impl Window @@ -310,10 +318,8 @@ where .clone(); Self { pending, - attributes: WindowAttributes { - title, - ..WindowAttributes::default() - }, + title: Value::Constant(title), + attributes: WindowAttributes::default(), on_closed: None, context, load_system_fonts: true, @@ -363,6 +369,7 @@ where user: self.context, settings: RefCell::new(sealed::WindowSettings { cushy, + title: self.title, redraw_status: self.pending.0.redraw_status.clone(), on_closed: self.on_closed, transparent: self.attributes.transparent, @@ -781,6 +788,14 @@ where context: Self::Context, ) -> Self { let mut settings = context.settings.borrow_mut(); + if let Value::Dynamic(title) = &settings.title { + let handle = window.handle(); + title + .for_each_cloned(move |title| { + let _result = handle.send(WindowCommand::SetTitle(title)); + }) + .persist(); + } let cushy = settings.cushy.clone(); let occluded = settings.occluded.take().unwrap_or_default(); let focused = settings.focused.take().unwrap_or_default(); @@ -985,15 +1000,12 @@ where } fn initial_window_attributes(context: &Self::Context) -> kludgine::app::WindowAttributes { - let mut attrs = context - .settings - .borrow_mut() - .attributes - .take() - .expect("called more than once"); - if let Some(Value::Constant(theme_mode)) = &context.settings.borrow().theme_mode { + let mut settings = context.settings.borrow_mut(); + let mut attrs = settings.attributes.take().expect("called more than once"); + if let Some(Value::Constant(theme_mode)) = &settings.theme_mode { attrs.preferred_theme = Some((*theme_mode).into()); } + attrs.title = settings.title.get(); attrs } @@ -1398,6 +1410,9 @@ where window.close(); } } + WindowCommand::SetTitle(new_title) => { + window.set_title(&new_title); + } } } } @@ -1449,6 +1464,7 @@ pub(crate) mod sealed { pub struct WindowSettings { pub cushy: Cushy, pub redraw_status: InvalidationStatus, + pub title: Value, pub attributes: Option, pub occluded: Option>, pub focused: Option>, @@ -1469,6 +1485,7 @@ pub(crate) mod sealed { pub enum WindowCommand { Redraw, RequestClose, + SetTitle(String), } }