//! 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, } } }