Initial DebugContext implementation

This commit is contained in:
Jonathan Johnson 2023-12-28 21:30:25 -08:00
parent c6d66a3166
commit 3fc49d2424
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
9 changed files with 305 additions and 20 deletions

10
Cargo.lock generated
View file

@ -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"

View file

@ -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"

58
examples/debug-window.rs Normal file
View file

@ -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<usize>,
total_windows: &Dynamic<usize>,
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);
}

View file

@ -119,7 +119,7 @@ pub trait Open: Sized {
/// Opens the provided type as a window inside of `app`.
fn open<App>(self, app: &App) -> crate::Result<Option<WindowHandle>>
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

209
src/debug.rs Normal file
View file

@ -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<DebugSection>,
}
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<T>(&self, label: impl Into<String>, value: &Dynamic<T>)
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<String>) -> 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<App>(self, app: &App) -> crate::Result<Option<crate::window::WindowHandle>>
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<T> {
label: String,
value: DynamicReader<T>,
widget: WidgetInstance,
}
impl<T> Observable for RegisteredValue<T>
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<OrderedLots<Dynamic<DebugSection>>>,
values: Dynamic<OrderedLots<Box<dyn Observable>>>,
widget: WidgetInstance,
}
impl Default for DebugSection {
fn default() -> Self {
Self::new(None, String::default())
}
}
impl DebugSection {
fn new(parent: Option<&Dynamic<Self>>, label: String) -> Self {
// Create the grid of observed values
let values = Dynamic::<OrderedLots<Box<dyn Observable>>>::default();
let value_grid = Grid::from_rows(values.map_each(|values| {
values
.iter()
.map(|o| [o.label().make_widget(), o.widget().clone()])
.collect::<GridWidgets<2>>()
}));
// Create the list of collapsable sub contexts
let children = Dynamic::<OrderedLots<Dynamic<DebugSection>>>::default();
let child_widgets = children.map_each(|children| {
children
.iter()
.map(|section| section.map_ref(|section| section.widget.clone()))
.collect::<Children>()
});
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,
}
}
}

View file

@ -15,6 +15,7 @@ mod names;
#[macro_use]
pub mod styles;
mod app;
pub mod debug;
mod tick;
mod tree;
pub mod value;

View file

@ -781,15 +781,25 @@ impl<T> Clone for Dynamic<T> {
impl<T> Drop for Dynamic<T> {
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();
}
}

View file

@ -468,7 +468,7 @@ where
{
fn open<App>(self, app: &App) -> crate::Result<Option<crate::window::WindowHandle>>
where
App: Application,
App: Application + ?Sized,
{
Window::<WidgetInstance>::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<T, Iter>(&mut self, iter: Iter)
where
Iter: IntoIterator<Item = T>,
T: MakeWidget,
{
self.ordered.extend(iter.into_iter().map(T::make_widget));
}
/// Adds `widget` to self and returns the updated list.
pub fn and<W>(mut self, widget: W) -> Self
where

View file

@ -359,7 +359,7 @@ where
{
fn open<App>(self, app: &App) -> crate::Result<Option<WindowHandle>>
where
App: Application,
App: Application + ?Sized,
{
let cushy = app.cushy().clone();