mirror of
https://github.com/danbulant/cushy
synced 2026-06-15 12:31:11 +00:00
Initial DebugContext implementation
This commit is contained in:
parent
c6d66a3166
commit
3fc49d2424
9 changed files with 305 additions and 20 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
58
examples/debug-window.rs
Normal 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);
|
||||
}
|
||||
|
|
@ -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
209
src/debug.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,7 @@ mod names;
|
|||
#[macro_use]
|
||||
pub mod styles;
|
||||
mod app;
|
||||
pub mod debug;
|
||||
mod tick;
|
||||
mod tree;
|
||||
pub mod value;
|
||||
|
|
|
|||
24
src/value.rs
24
src/value.rs
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue