diff --git a/Cargo.lock b/Cargo.lock index 61b3c8b..d269d2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -106,8 +106,7 @@ dependencies = [ [[package]] name = "appit" version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f4127675fb55fa12ce1f3eaaf24826db1a00a4b5f108c0698bafbefab32c9b" +source = "git+https://github.com/khonsulabs/appit#0fa6a4b3a512eb3ef3c844fcbb03d2efbecec863" dependencies = [ "winit", ] @@ -1180,8 +1179,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "kludgine" version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33370975064272061b94d15ef0eb94ab1379c8da6eb585ce03fe5ac13494aab6" +source = "git+https://github.com/khonsulabs/kludgine#a38466b021ce55c26c6d533191a87a22109016b2" dependencies = [ "ahash", "alot", @@ -1381,9 +1379,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memmap2" diff --git a/Cargo.toml b/Cargo.toml index c6b3574..b84491e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,11 +18,11 @@ tracing-output = ["dep:tracing-subscriber"] roboto-flex = [] [dependencies] -kludgine = { version = "0.7.0", features = ["app"] } +# kludgine = { version = "0.7.0", features = ["app"] } +kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [ + "app", +] } figures = "0.2.1" -# kludgine = { git = "https://github.com/khonsulabs/kludgine", features = [ -# "app", -# ] } alot = "0.3" interner = "0.2.1" kempt = "0.2.1" diff --git a/examples/debug-window.rs b/examples/debug-window.rs new file mode 100644 index 0000000..db1bc3b --- /dev/null +++ b/examples/debug-window.rs @@ -0,0 +1,58 @@ +use cushy::debug::DebugContext; +use cushy::value::Dynamic; +use cushy::widget::MakeWidget; +use cushy::widgets::slider::Slidable; +use cushy::{Application, Open, PendingApp}; + +const INTRO: &str = "This example demonstrates the DebugContext, which allows observing values easily throughout GUI"; + +fn main() -> cushy::Result { + let app = PendingApp::default(); + let dbg = DebugContext::default(); + let window_count = Dynamic::new(0_usize); + let total_windows = Dynamic::new(0_usize); + + dbg.observe("Open Windows", &window_count); + dbg.observe("Total Windows", &total_windows); + dbg.clone().open(&app)?; + + INTRO + .and("Open a Window".into_button().on_click({ + let app = app.as_app(); + + move |()| open_a_window(&window_count, &total_windows, &dbg, &app) + })) + .into_rows() + .centered() + .run_in(app) +} + +fn open_a_window( + window_count: &Dynamic, + total_windows: &Dynamic, + dbg: &DebugContext, + app: &dyn Application, +) { + *window_count.lock() += 1; + let window_number = total_windows.map_mut(|total| { + *total += 1; + *total + }); + let window_title = format!("Window #{window_number}"); + let dbg = dbg.section(&window_title); + + let value = Dynamic::new(0_u8); + dbg.observe("Slider", &value); + + let window_count = window_count.clone(); + let _ = format!("This is window {window_number}.") + .and(value.slider()) + .into_rows() + .centered() + .into_window() + .titled(window_title) + .on_close(move || { + *window_count.lock() -= 1; + }) + .open(app); +} diff --git a/src/app.rs b/src/app.rs index b952837..4b8742b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -119,7 +119,7 @@ pub trait Open: Sized { /// Opens the provided type as a window inside of `app`. fn open(self, app: &App) -> crate::Result> where - App: Application; + App: Application + ?Sized; /// Runs the provided type inside of the pending `app`, returning `Ok(())` /// upon successful execution and program exit. Note that this function may diff --git a/src/debug.rs b/src/debug.rs new file mode 100644 index 0000000..243fda9 --- /dev/null +++ b/src/debug.rs @@ -0,0 +1,209 @@ +//! Utililies to help debug Cushy apps. + +use std::fmt::Debug; + +use alot::OrderedLots; + +use crate::value::{Dynamic, DynamicReader, ForEach}; +use crate::widget::{Children, MakeWidget, WidgetInstance}; +use crate::widgets::grid::{Grid, GridWidgets}; +use crate::window::Window; +use crate::Open; + +/// A widget that can provide extra information when debugging. +#[derive(Clone, Default)] +pub struct DebugContext { + section: Dynamic, +} + +impl DebugContext { + /// Observes `value` using `label` in this debug context. + /// + /// When the final reference to `value` is dropped, this observation will + /// automatically be removed. + pub fn observe(&self, label: impl Into, value: &Dynamic) + where + T: PartialEq + Clone + Debug + Send + Sync + 'static, + { + let reader = value.create_reader(); + let id = self.section.map_ref(|section| { + section.values.lock().push(Box::new(RegisteredValue { + label: label.into(), + value: reader.clone(), + widget: value + .weak_clone() + .map_each(|value| format!("{value:?}")) + .make_widget(), + })) + }); + let this = self.clone(); + reader.on_disconnect(move || { + this.section + .map_ref(|section| section.values.lock().remove(id)); + }); + } + + /// Returns a new child context with the given `label`. + /// + /// This creates a nested hierarchy of debug contexts. If a section with the + /// name already exists, a context for the existing section will be + /// returned. + #[must_use] + pub fn section(&self, label: impl Into) -> Self { + let label = label.into(); + let this = self.section.lock(); + let mut children = this.children.lock(); + let section = if let Some(existing) = children.iter().find_map(|child| { + child + .map_ref(|child| child.label == label) + .then(|| child.clone()) + }) { + existing + } else { + let new_section = Dynamic::new(DebugSection::new(Some(&self.section), label.clone())); + let mut insert_at = children.len(); + for index in 0..children.len() { + if children[index].map_ref(|child| label < child.label) { + insert_at = index; + break; + } + } + + children.insert(insert_at, new_section.clone()); + new_section + }; + + Self { section } + } + + fn into_window(self) -> Window { + self.section + .map_ref(|section| section.widget.clone()) + .vertical_scroll() + .into_window() + .titled("Cushy Debugger") + } +} + +impl Open for DebugContext { + fn open(self, app: &App) -> crate::Result> + where + App: crate::Application + ?Sized, + { + self.into_window().open(app) + } + + fn run_in(self, app: crate::PendingApp) -> crate::Result { + self.into_window().run_in(app) + } +} + +trait Observable: Send { + fn label(&self) -> &str; + fn alive(&self) -> bool; + fn widget(&self) -> &WidgetInstance; +} + +struct RegisteredValue { + label: String, + value: DynamicReader, + widget: WidgetInstance, +} + +impl Observable for RegisteredValue +where + T: Send, +{ + fn label(&self) -> &str { + &self.label + } + + fn alive(&self) -> bool { + self.value.connected() + } + + fn widget(&self) -> &WidgetInstance { + &self.widget + } +} + +struct DebugSection { + label: String, + children: Dynamic>>, + values: Dynamic>>, + widget: WidgetInstance, +} + +impl Default for DebugSection { + fn default() -> Self { + Self::new(None, String::default()) + } +} + +impl DebugSection { + fn new(parent: Option<&Dynamic>, label: String) -> Self { + // Create the grid of observed values + let values = Dynamic::>>::default(); + let value_grid = Grid::from_rows(values.map_each(|values| { + values + .iter() + .map(|o| [o.label().make_widget(), o.widget().clone()]) + .collect::>() + })); + + // Create the list of collapsable sub contexts + let children = Dynamic::>>::default(); + let child_widgets = children.map_each(|children| { + children + .iter() + .map(|section| section.map_ref(|section| section.widget.clone())) + .collect::() + }); + + let parent = parent.map(Dynamic::downgrade); + // Create a cleanup task to remove this section once it becomes empty. + if let Some(parent) = parent { + 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); + } + } + } + } + }) + .persist(); + } + + let contents = value_grid + .and(child_widgets.into_rows()) + .into_rows() + .make_widget(); + let widget = if label.is_empty() { + contents + } else { + contents + .disclose() + .labelled_by(label.as_str()) + .collapsed(false) + .make_widget() + }; + + Self { + label, + children, + values, + widget, + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 3db086b..eba9c36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ mod names; #[macro_use] pub mod styles; mod app; +pub mod debug; mod tick; mod tree; pub mod value; diff --git a/src/value.rs b/src/value.rs index b761d92..305820d 100644 --- a/src/value.rs +++ b/src/value.rs @@ -781,15 +781,25 @@ impl Clone for Dynamic { impl Drop for Dynamic { fn drop(&mut self) { - 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); + // Ignoring deadlocks here allows complex flows to work properly, and + // the only issue is that `on_disconnect` will not fire if during a map + // callback on a `DynamicReader` the final reference to the source + // `Dynamic`. + if let Ok(mut state) = self.state() { + 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(()); + for on_disconnect in on_disconnect { + on_disconnect.invoke(()); + } + + self.0.sync.notify_all(); } - + } else { + // In the event that this is the rare edge case and a reader is + // blocking, we want to signal that we've dropped the final + // reference. self.0.sync.notify_all(); } } diff --git a/src/widget.rs b/src/widget.rs index 015dbe6..c2296b1 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -468,7 +468,7 @@ where { fn open(self, app: &App) -> crate::Result> where - App: Application, + App: Application + ?Sized, { Window::::new(self.make_widget()).open(app) } @@ -1950,6 +1950,15 @@ impl Children { self.ordered.insert(index, widget.make_widget()); } + /// Extends this collection with the contents of `iter`. + pub fn extend(&mut self, iter: Iter) + where + Iter: IntoIterator, + T: MakeWidget, + { + self.ordered.extend(iter.into_iter().map(T::make_widget)); + } + /// Adds `widget` to self and returns the updated list. pub fn and(mut self, widget: W) -> Self where diff --git a/src/window.rs b/src/window.rs index e1b1822..b7eeb25 100644 --- a/src/window.rs +++ b/src/window.rs @@ -359,7 +359,7 @@ where { fn open(self, app: &App) -> crate::Result> where - App: Application, + App: Application + ?Sized, { let cushy = app.cushy().clone();