diff --git a/CHANGELOG.md b/CHANGELOG.md index ed4b410..4a7887e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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. +- `Dynamic::instances()` returns the number of clones the dynamic has in + existence. +- `Dynamic::readers()` returns the number of `DynamicReader`s for the dynamic in + existence. [99]: https://github.com/khonsulabs/cushy/issues/99 diff --git a/src/debug.rs b/src/debug.rs index 243fda9..2ce59ba 100644 --- a/src/debug.rs +++ b/src/debug.rs @@ -4,7 +4,7 @@ use std::fmt::Debug; use alot::OrderedLots; -use crate::value::{Dynamic, DynamicReader, ForEach}; +use crate::value::{Dynamic, DynamicReader, ForEach, WeakDynamic}; use crate::widget::{Children, MakeWidget, WidgetInstance}; use crate::widgets::grid::{Grid, GridWidgets}; use crate::window::Window; @@ -83,6 +83,15 @@ impl DebugContext { .into_window() .titled("Cushy Debugger") } + + /// Returns true if this debug context has no child sections or observed + /// values. + #[must_use] + pub fn is_empty(&self) -> bool { + self.section.map_ref(|section| { + section.children.map_ref(OrderedLots::len) + section.values.map_ref(OrderedLots::len) + }) == 0 + } } impl Open for DebugContext { @@ -98,6 +107,22 @@ impl Open for DebugContext { } } +impl Drop for DebugContext { + fn drop(&mut self) { + // If the only two references are this context and the parent owning the + // child section, then we want to remove the section if nothing was + // added to it. + if self.section.instances() == 2 { + let section = self.section.lock(); + if let Some(parent) = section.parent.clone() { + let label = section.label.clone(); + drop(section); + DebugSection::remove_child_section(&parent, &label); + } + } + } +} + trait Observable: Send { fn label(&self) -> &str; fn alive(&self) -> bool; @@ -132,6 +157,7 @@ struct DebugSection { children: Dynamic>>, values: Dynamic>>, widget: WidgetInstance, + parent: Option>, } impl Default for DebugSection { @@ -162,23 +188,13 @@ impl DebugSection { let parent = parent.map(Dynamic::downgrade); // Create a cleanup task to remove this section once it becomes empty. - if let Some(parent) = parent { + if let Some(parent) = parent.clone() { let label = label.clone(); (&children, &values) .for_each({ move |(children, values)| { if children.is_empty() && values.is_empty() { - if let Some(parent) = parent.upgrade() { - let parent = parent.lock(); - let mut children = parent.children.lock(); - if let Some(index) = - children.iter().enumerate().find_map(|(index, child)| { - child.map_ref(|child| child.label == label).then_some(index) - }) - { - children.remove_by_index(index); - } - } + Self::remove_child_section(&parent, &label); } } }) @@ -204,6 +220,26 @@ impl DebugSection { children, values, widget, + parent, + } + } + + fn remove_child_section(parent: &WeakDynamic, label: &str) { + if let Some(parent) = parent.upgrade() { + let parent = parent.lock(); + let mut children = parent.children.lock(); + if let Some(index) = children.iter().enumerate().find_map(|(index, child)| { + child.map_ref(|child| child.label == label).then_some(index) + }) { + children.remove_by_index(index); + } } } } + +#[test] +fn empty_child_clears_on_drop() { + let root = DebugContext::default(); + drop(root.section("child")); + assert!(root.is_empty()); +} diff --git a/src/value.rs b/src/value.rs index 305820d..b902d64 100644 --- a/src/value.rs +++ b/src/value.rs @@ -60,6 +60,30 @@ impl Dynamic { WeakDynamic::from(self) } + /// Returns the number [`Dynamic`]s that point to this same value. + /// + /// The returned count includes `self`. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + pub fn instances(&self) -> usize { + Arc::strong_count(&self.0) - self.readers() + } + + /// Returns the number of [`DynamicReader`]s for this value. + /// + /// # Panics + /// + /// This function panics if this value is already locked by the current + /// thread. + #[must_use] + pub fn readers(&self) -> usize { + self.state().expect("deadlocked").readers + } + /// Returns a new dynamic that has its contents linked with `self` by the /// pair of mapping functions provided. /// @@ -2801,3 +2825,32 @@ fn compare_swap() { assert_eq!(dynamic.compare_swap(&2, 0), Ok(2)); assert_eq!(dynamic.get(), 0); } + +#[test] +fn ref_counts() { + let dynamic = Dynamic::new(1); + assert_eq!(dynamic.instances(), 1); + + let second = dynamic.clone(); + assert_eq!(dynamic.instances(), 2); + + assert_eq!(dynamic.readers(), 0); + let reader = second.into_reader(); + assert_eq!(dynamic.instances(), 1); + assert_eq!(dynamic.readers(), 1); + + // Test that once the last instance is dropped that the reader is no longer + // connected and that on_disconnect gets invoked. + assert!(reader.connected()); + let invoked = Dynamic::new(false); + reader.on_disconnect({ + let invoked = invoked.clone(); + move || { + invoked.set(true); + } + }); + drop(dynamic); + + assert!(invoked.get()); + assert!(!reader.connected()); +}