From 313af5e2cbc3be460dbf9edd609763801ab9190c Mon Sep 17 00:00:00 2001 From: Olivier FAURE Date: Sun, 6 Jun 2021 19:02:11 +0200 Subject: [PATCH] Add DebugState --- druid/src/core.rs | 22 ++++++++++-- druid/src/debug_state.rs | 48 ++++++++++++++++++++++++++ druid/src/event.rs | 50 ++++++++++++++++++++++++++-- druid/src/lib.rs | 5 +-- druid/src/tests/harness.rs | 36 ++++++++++++++++++++ druid/src/widget/align.rs | 9 +++++ druid/src/widget/aspect_ratio_box.rs | 10 ++++++ druid/src/widget/button.rs | 9 +++++ druid/src/widget/checkbox.rs | 14 ++++++++ druid/src/widget/clip_box.rs | 9 +++++ druid/src/widget/container.rs | 9 +++++ druid/src/widget/controller.rs | 9 +++++ druid/src/widget/disable_if.rs | 9 +++++ druid/src/widget/either.rs | 14 ++++++++ druid/src/widget/env_scope.rs | 9 +++++ druid/src/widget/flex.rs | 18 ++++++++++ druid/src/widget/identity_wrapper.rs | 9 +++++ druid/src/widget/invalidation.rs | 9 +++++ druid/src/widget/label.rs | 9 +++++ druid/src/widget/lens_wrap.rs | 10 ++++++ druid/src/widget/list.rs | 17 ++++++++++ druid/src/widget/maybe.rs | 15 +++++++++ druid/src/widget/padding.rs | 9 +++++ druid/src/widget/parse.rs | 9 +++++ druid/src/widget/progress_bar.rs | 9 +++++ druid/src/widget/radio.rs | 14 ++++++++ druid/src/widget/scope.rs | 3 ++ druid/src/widget/scroll.rs | 9 +++++ druid/src/widget/sized_box.rs | 14 ++++++++ druid/src/widget/slider.rs | 9 +++++ druid/src/widget/split.rs | 12 +++++++ druid/src/widget/stepper.rs | 9 +++++ druid/src/widget/switch.rs | 9 +++++ druid/src/widget/textbox.rs | 10 ++++++ druid/src/widget/value_textbox.rs | 9 +++++ druid/src/widget/widget.rs | 29 ++++++++++++++++ druid/src/window.rs | 6 ++++ 37 files changed, 503 insertions(+), 6 deletions(-) create mode 100644 druid/src/debug_state.rs diff --git a/druid/src/core.rs b/druid/src/core.rs index ea019c70bc..149ab82832 100644 --- a/druid/src/core.rs +++ b/druid/src/core.rs @@ -402,6 +402,11 @@ impl> WidgetPod { Some(pos) => rect.winding(pos) != 0, None => false, }; + trace!( + "Widget {:?}: set hot state to {}", + child_state.id, + child_state.is_hot + ); if had_hot != child_state.is_hot { let hot_changed_event = LifeCycle::HotChanged(child_state.is_hot); let mut child_ctx = LifeCycleCtx { @@ -990,6 +995,18 @@ impl> WidgetPod { self.state.children.may_contain(widget) } } + InternalLifeCycle::DebugRequestDebugState { widget, state_cell } => { + if *widget == self.id() { + if let Some(data) = &self.old_data { + state_cell.set(self.inner.debug_state(data)); + } + false + } else { + // Recurse when the target widget could be our descendant. + // The bloom filter we're checking can return false positives. + self.state.children.may_contain(widget) + } + } InternalLifeCycle::DebugInspectState(f) => { f.call(&self.state); true @@ -1327,11 +1344,12 @@ impl WidgetState { /// For more information, see [`WidgetPod::paint_rect`]. /// /// [`WidgetPod::paint_rect`]: struct.WidgetPod.html#method.paint_rect - pub(crate) fn paint_rect(&self) -> Rect { + pub fn paint_rect(&self) -> Rect { self.layout_rect() + self.paint_insets } - pub(crate) fn layout_rect(&self) -> Rect { + /// The rectangle used when calculating layout with other widgets + pub fn layout_rect(&self) -> Rect { Rect::from_origin_size(self.origin, self.size) } diff --git a/druid/src/debug_state.rs b/druid/src/debug_state.rs new file mode 100644 index 0000000000..8d45587e5f --- /dev/null +++ b/druid/src/debug_state.rs @@ -0,0 +1,48 @@ +//! A data structure for representing widget trees. + +use std::collections::HashMap; + +/// A description widget and its children, clonable and comparable, meant +/// for testing and debugging. This is extremely not optimized. +#[derive(Default, Clone, PartialEq, Eq)] +pub struct DebugState { + /// The widget's type as a human-readable string. + pub display_name: String, + /// If a widget has a "central" value (for instance, a textbox's contents), + /// it is stored here. + pub main_value: String, + /// Untyped values that reveal useful information about the widget. + pub other_values: HashMap, + /// Debug info of child widgets. + pub children: Vec, +} + +impl std::fmt::Debug for DebugState { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + if self.other_values.is_empty() && self.children.is_empty() && self.main_value.is_empty() { + f.write_str(&self.display_name) + } else if self.other_values.is_empty() && self.children.is_empty() { + f.debug_tuple(&self.display_name) + .field(&self.main_value) + .finish() + } else if self.other_values.is_empty() && self.main_value.is_empty() { + let mut f_tuple = f.debug_tuple(&self.display_name); + for child in &self.children { + f_tuple.field(child); + } + f_tuple.finish() + } else { + let mut f_struct = f.debug_struct(&self.display_name); + if !self.main_value.is_empty() { + f_struct.field("_main_value_", &self.main_value); + } + for (key, value) in self.other_values.iter() { + f_struct.field(key, &value); + } + if !self.children.is_empty() { + f_struct.field("children", &self.children); + } + f_struct.finish() + } + } +} diff --git a/druid/src/event.rs b/druid/src/event.rs index d927f30b76..c060e083af 100644 --- a/druid/src/event.rs +++ b/druid/src/event.rs @@ -345,6 +345,18 @@ pub enum InternalLifeCycle { /// a cell used to store the a widget's state state_cell: StateCell, }, + /// For testing: request the `DebugState` of a specific widget. + /// + /// This is useful if you need to get a best-effort description of the + /// state of this widget and its children. You can dispatch this event, + /// specifying the widget in question, and that widget will + /// set its state in the provided `Cell`, if it exists. + DebugRequestDebugState { + /// the widget whose state is requested + widget: WidgetId, + /// a cell used to store the a widget's state + state_cell: DebugStateCell, + }, /// For testing: apply the given function on every widget. DebugInspectState(StateCheckFn), } @@ -475,22 +487,28 @@ impl InternalLifeCycle { | InternalLifeCycle::RouteDisabledChanged => true, InternalLifeCycle::ParentWindowOrigin => false, InternalLifeCycle::DebugRequestState { .. } + | InternalLifeCycle::DebugRequestDebugState { .. } | InternalLifeCycle::DebugInspectState(_) => true, } } } -pub(crate) use state_cell::{StateCell, StateCheckFn}; +pub(crate) use state_cell::{DebugStateCell, StateCell, StateCheckFn}; mod state_cell { use crate::core::WidgetState; + use crate::debug_state::DebugState; use crate::WidgetId; use std::{cell::RefCell, rc::Rc}; - /// An interior-mutable struct for fetching BasteState. + /// An interior-mutable struct for fetching WidgetState. #[derive(Clone, Default)] pub struct StateCell(Rc>>); + /// An interior-mutable struct for fetching DebugState. + #[derive(Clone, Default)] + pub struct DebugStateCell(Rc>>); + #[derive(Clone)] pub struct StateCheckFn(Rc); @@ -520,6 +538,21 @@ mod state_cell { } } + impl DebugStateCell { + /// Set the state. This will panic if it is called twice. + pub(crate) fn set(&self, state: DebugState) { + assert!( + self.0.borrow_mut().replace(state).is_none(), + "DebugStateCell already set" + ) + } + + #[allow(dead_code)] + pub(crate) fn take(&self) -> Option { + self.0.borrow_mut().take() + } + } + impl StateCheckFn { #[cfg(not(target_arch = "wasm32"))] pub(crate) fn new(f: impl Fn(&WidgetState) + 'static) -> Self { @@ -533,6 +566,8 @@ mod state_cell { } } + // TODO - Use fmt.debug_tuple? + impl std::fmt::Debug for StateCell { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { let inner = if self.0.borrow().is_some() { @@ -544,6 +579,17 @@ mod state_cell { } } + impl std::fmt::Debug for DebugStateCell { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let inner = if self.0.borrow().is_some() { + "Some" + } else { + "None" + }; + write!(f, "DebugStateCell({})", inner) + } + } + impl std::fmt::Debug for StateCheckFn { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "StateCheckFn") diff --git a/druid/src/lib.rs b/druid/src/lib.rs index 9769cb7799..0d233b36e0 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -165,6 +165,7 @@ mod command; mod contexts; mod core; mod data; +pub mod debug_state; mod dialog; pub mod env; mod event; @@ -200,7 +201,7 @@ pub use shell::{ #[cfg(feature = "raw-win-handle")] pub use crate::shell::raw_window_handle::{HasRawWindowHandle, RawWindowHandle}; -pub use crate::core::WidgetPod; +pub use crate::core::{WidgetPod, WidgetState}; pub use app::{AppLauncher, WindowConfig, WindowDesc, WindowSizePolicy}; pub use app_delegate::{AppDelegate, DelegateCtx}; pub use box_constraints::BoxConstraints; @@ -221,7 +222,7 @@ pub use win_handler::DruidHandler; pub use window::{Window, WindowId}; #[cfg(not(target_arch = "wasm32"))] -pub(crate) use event::{StateCell, StateCheckFn}; +pub(crate) use event::{DebugStateCell, StateCell, StateCheckFn}; #[deprecated(since = "0.8.0", note = "import from druid::text module instead")] pub use piet::{FontFamily, FontStyle, FontWeight, TextAlignment}; diff --git a/druid/src/tests/harness.rs b/druid/src/tests/harness.rs index 8aea8ab5a7..4fc584520c 100644 --- a/druid/src/tests/harness.rs +++ b/druid/src/tests/harness.rs @@ -23,6 +23,8 @@ use crate::ext_event::ExtEventHost; use crate::piet::{BitmapTarget, Device, Error, ImageFormat, Piet}; use crate::*; +use crate::debug_state::DebugState; + pub(crate) const DEFAULT_SIZE: Size = Size::new(400., 400.); /// A type that tries very hard to provide a comforting and safe environment @@ -208,6 +210,32 @@ impl Harness<'_, T> { cell.take() } + /// Retrieve a copy of the root widget's `DebugState` (and by recursion, all others) + pub fn get_root_debug_state(&self) -> DebugState { + self.mock_app.root_debug_state() + } + + /// Retrieve a copy of this widget's `DebugState`, or die trying. + pub fn get_debug_state(&mut self, widget_id: WidgetId) -> DebugState { + match self.try_get_debug_state(widget_id) { + Some(thing) => thing, + None => panic!("get_debug_state failed for widget {:?}", widget_id), + } + } + + /// Attempt to retrieve a copy of this widget's `DebugState`. + pub fn try_get_debug_state(&mut self, widget_id: WidgetId) -> Option { + let cell = DebugStateCell::default(); + let state_cell = cell.clone(); + self.lifecycle(LifeCycle::Internal( + InternalLifeCycle::DebugRequestDebugState { + widget: widget_id, + state_cell, + }, + )); + cell.take() + } + /// Inspect the `WidgetState` of each widget in the tree. /// /// The provided closure will be called on each widget. @@ -285,6 +313,10 @@ impl Harness<'_, T> { self.mock_app .paint_region(&mut self.piet, &self.window_size.to_rect().into()); } + + pub fn root_debug_state(&self) -> DebugState { + self.mock_app.root_debug_state() + } } impl MockAppState { @@ -312,6 +344,10 @@ impl MockAppState { self.window .do_paint(piet, invalid, &mut self.cmds, &self.data, &self.env); } + + pub fn root_debug_state(&self) -> DebugState { + self.window.root_debug_state(&self.data) + } } impl Drop for Harness<'_, T> { diff --git a/druid/src/widget/align.rs b/druid/src/widget/align.rs index 13e2814dd9..ae13c3a797 100644 --- a/druid/src/widget/align.rs +++ b/druid/src/widget/align.rs @@ -14,6 +14,7 @@ //! A widget that aligns its child (for example, centering it). +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::{Data, Rect, Size, UnitPoint, WidgetPod}; use tracing::{instrument, trace}; @@ -141,6 +142,14 @@ impl Widget for Align { fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { self.child.paint(ctx, data, env); } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.widget().debug_state(data)], + ..Default::default() + } + } } fn log_size_warnings(size: Size) { diff --git a/druid/src/widget/aspect_ratio_box.rs b/druid/src/widget/aspect_ratio_box.rs index 4047addcff..3dbacec85d 100644 --- a/druid/src/widget/aspect_ratio_box.rs +++ b/druid/src/widget/aspect_ratio_box.rs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::debug_state::DebugState; + use druid::widget::prelude::*; use druid::Data; use tracing::{instrument, warn}; @@ -161,4 +163,12 @@ impl Widget for AspectRatioBox { fn id(&self) -> Option { self.child.id() } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.debug_state(data)], + ..Default::default() + } + } } diff --git a/druid/src/widget/button.rs b/druid/src/widget/button.rs index e82cfde657..52294ebc0a 100644 --- a/druid/src/widget/button.rs +++ b/druid/src/widget/button.rs @@ -14,6 +14,7 @@ //! A button widget. +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::widget::{Click, ControllerHost, Label, LabelText}; use crate::{theme, Affine, Data, Insets, LinearGradient, UnitPoint}; @@ -217,4 +218,12 @@ impl Widget for Button { self.label.paint(ctx, data, env); }); } + + fn debug_state(&self, _data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + main_value: self.label.text().to_string(), + ..Default::default() + } + } } diff --git a/druid/src/widget/checkbox.rs b/druid/src/widget/checkbox.rs index c9aeefeff6..a6e767b674 100644 --- a/druid/src/widget/checkbox.rs +++ b/druid/src/widget/checkbox.rs @@ -14,6 +14,7 @@ //! A checkbox widget. +use crate::debug_state::DebugState; use crate::kurbo::{BezPath, Size}; use crate::piet::{LineCap, LineJoin, LinearGradient, RenderContext, StrokeStyle, UnitPoint}; use crate::theme; @@ -159,4 +160,17 @@ impl Widget for Checkbox { // Paint the text label self.child_label.draw_at(ctx, (size + x_padding, 0.0)); } + + fn debug_state(&self, data: &bool) -> DebugState { + let display_value = if *data { + format!("[X] {}", self.child_label.text()) + } else { + format!("[_] {}", self.child_label.text()) + }; + DebugState { + display_name: self.short_type_name().to_string(), + main_value: display_value, + ..Default::default() + } + } } diff --git a/druid/src/widget/clip_box.rs b/druid/src/widget/clip_box.rs index 8a1e4f1043..df2a73d49e 100644 --- a/druid/src/widget/clip_box.rs +++ b/druid/src/widget/clip_box.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::debug_state::DebugState; use crate::kurbo::{Affine, Point, Rect, Size, Vec2}; use crate::widget::prelude::*; use crate::widget::Axis; @@ -377,6 +378,14 @@ impl> Widget for ClipBox { ctx.with_child_ctx(visible, |ctx| self.child.paint_raw(ctx, data, env)); }); } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.widget().debug_state(data)], + ..Default::default() + } + } } #[cfg(test)] diff --git a/druid/src/widget/container.rs b/druid/src/widget/container.rs index 0ba2b053a5..12c9cba34b 100644 --- a/druid/src/widget/container.rs +++ b/druid/src/widget/container.rs @@ -15,6 +15,7 @@ //! A widget that provides simple visual styling options to a child. use super::BackgroundBrush; +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::{Color, Data, KeyOrValue, Point, WidgetPod}; use tracing::{instrument, trace, trace_span}; @@ -227,4 +228,12 @@ impl Widget for Container { self.child.paint(ctx, data, env); } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.widget().debug_state(data)], + ..Default::default() + } + } } diff --git a/druid/src/widget/controller.rs b/druid/src/widget/controller.rs index f200fa0e23..d66a6ff0a5 100644 --- a/druid/src/widget/controller.rs +++ b/druid/src/widget/controller.rs @@ -14,6 +14,7 @@ //! A widget-controlling widget. +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::widget::WidgetWrapper; @@ -133,6 +134,14 @@ impl, C: Controller> Widget for ControllerHost { fn id(&self) -> Option { self.widget.id() } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.widget.debug_state(data)], + ..Default::default() + } + } } impl WidgetWrapper for ControllerHost { diff --git a/druid/src/widget/disable_if.rs b/druid/src/widget/disable_if.rs index 502de02bb4..d2b5c5a50b 100644 --- a/druid/src/widget/disable_if.rs +++ b/druid/src/widget/disable_if.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::debug_state::DebugState; use crate::{ BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, Size, UpdateCtx, Widget, WidgetPod, @@ -68,4 +69,12 @@ impl> Widget for DisabledIf { fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { self.child.paint(ctx, data, env); } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.widget().debug_state(data)], + ..Default::default() + } + } } diff --git a/druid/src/widget/either.rs b/druid/src/widget/either.rs index 3fa03bc48f..83f9886ee0 100644 --- a/druid/src/widget/either.rs +++ b/druid/src/widget/either.rs @@ -14,6 +14,7 @@ //! A widget that switches dynamically between two child views. +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::{Data, Point, WidgetPod}; use tracing::instrument; @@ -93,6 +94,19 @@ impl Widget for Either { fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { self.current_widget().paint(ctx, data, env) } + + fn debug_state(&self, data: &T) -> DebugState { + let current_widget = if self.current { + &self.true_branch + } else { + &self.false_branch + }; + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![current_widget.widget().debug_state(data)], + ..Default::default() + } + } } impl Either { diff --git a/druid/src/widget/env_scope.rs b/druid/src/widget/env_scope.rs index 33094903cc..1b3bcbaad7 100644 --- a/druid/src/widget/env_scope.rs +++ b/druid/src/widget/env_scope.rs @@ -14,6 +14,7 @@ //! A widget that accepts a closure to update the environment for its child. +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::widget::WidgetWrapper; use crate::{Data, Point, WidgetPod}; @@ -104,6 +105,14 @@ impl> Widget for EnvScope { self.child.paint(ctx, data, &new_env); } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.widget().debug_state(data)], + ..Default::default() + } + } } impl> WidgetWrapper for EnvScope { diff --git a/druid/src/widget/flex.rs b/druid/src/widget/flex.rs index a7ffaf23c1..05303a244f 100644 --- a/druid/src/widget/flex.rs +++ b/druid/src/widget/flex.rs @@ -14,6 +14,7 @@ //! A widget that arranges its children in a one-dimensional array. +use crate::debug_state::DebugState; use crate::kurbo::{common::FloatExt, Vec2}; use crate::widget::prelude::*; use crate::{Data, KeyOrValue, Point, Rect, WidgetPod}; @@ -876,6 +877,23 @@ impl Widget for Flex { ctx.stroke_styled(line, &color, 1.0, &stroke_style); } } + + fn debug_state(&self, data: &T) -> DebugState { + let children_state = self + .children + .iter() + .map(|child| { + let child_widget_pod = child.widget()?; + Some(child_widget_pod.widget().debug_state(data)) + }) + .flatten() + .collect(); + DebugState { + display_name: self.short_type_name().to_string(), + children: children_state, + ..Default::default() + } + } } impl CrossAxisAlignment { diff --git a/druid/src/widget/identity_wrapper.rs b/druid/src/widget/identity_wrapper.rs index 2d8b9fd431..bdc67ee2c1 100644 --- a/druid/src/widget/identity_wrapper.rs +++ b/druid/src/widget/identity_wrapper.rs @@ -14,6 +14,7 @@ //! A widget that provides an explicit identity to a child. +use crate::debug_state::DebugState; use crate::kurbo::Size; use crate::widget::prelude::*; use crate::widget::WidgetWrapper; @@ -78,6 +79,14 @@ impl> Widget for IdentityWrapper { fn id(&self) -> Option { Some(self.id) } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.debug_state(data)], + ..Default::default() + } + } } impl WidgetWrapper for IdentityWrapper { diff --git a/druid/src/widget/invalidation.rs b/druid/src/widget/invalidation.rs index a85276493b..f99062e390 100644 --- a/druid/src/widget/invalidation.rs +++ b/druid/src/widget/invalidation.rs @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::Data; use tracing::instrument; @@ -93,4 +94,12 @@ impl> Widget for DebugInvalidation { fn id(&self) -> Option { self.child.id() } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.debug_state(data)], + ..Default::default() + } + } } diff --git a/druid/src/widget/label.rs b/druid/src/widget/label.rs index 6873d2ed59..96ce5c11b1 100644 --- a/druid/src/widget/label.rs +++ b/druid/src/widget/label.rs @@ -18,6 +18,7 @@ use std::ops::{Deref, DerefMut}; use druid_shell::Cursor; +use crate::debug_state::DebugState; use crate::kurbo::Vec2; use crate::text::TextStorage; use crate::widget::prelude::*; @@ -524,6 +525,14 @@ impl Widget for Label { } self.label.paint(ctx, &self.current_text, env) } + + fn debug_state(&self, _data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + main_value: self.current_text.to_string(), + ..Default::default() + } + } } impl Widget for RawLabel { diff --git a/druid/src/widget/lens_wrap.rs b/druid/src/widget/lens_wrap.rs index 296d534946..9949e4ae55 100644 --- a/druid/src/widget/lens_wrap.rs +++ b/druid/src/widget/lens_wrap.rs @@ -20,6 +20,7 @@ use std::marker::PhantomData; +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::widget::WidgetWrapper; use crate::{Data, Lens}; @@ -137,6 +138,15 @@ where fn id(&self) -> Option { self.child.id() } + + fn debug_state(&self, data: &T) -> DebugState { + let child_state = self.lens.with(data, |data| self.child.debug_state(data)); + DebugState { + display_name: "LensWrap".to_string(), + children: vec![child_state], + ..Default::default() + } + } } impl WidgetWrapper for LensWrap { diff --git a/druid/src/widget/list.rs b/druid/src/widget/list.rs index cedb44603e..f7b381102d 100644 --- a/druid/src/widget/list.rs +++ b/druid/src/widget/list.rs @@ -26,6 +26,7 @@ use crate::im::{OrdMap, Vector}; use crate::kurbo::{Point, Rect, Size}; +use crate::debug_state::DebugState; use crate::{ widget::Axis, BoxConstraints, Data, Env, Event, EventCtx, KeyOrValue, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, UpdateCtx, Widget, WidgetPod, @@ -411,4 +412,20 @@ impl> Widget for List { } }); } + + fn debug_state(&self, data: &T) -> DebugState { + let mut children = self.children.iter(); + let mut children_state = Vec::with_capacity(data.data_len()); + data.for_each(|child_data, _| { + if let Some(child) = children.next() { + children_state.push(child.widget().debug_state(child_data)); + } + }); + + DebugState { + display_name: "List".to_string(), + children: children_state, + ..Default::default() + } + } } diff --git a/druid/src/widget/maybe.rs b/druid/src/widget/maybe.rs index aa6efe400c..d43024878d 100644 --- a/druid/src/widget/maybe.rs +++ b/druid/src/widget/maybe.rs @@ -14,6 +14,8 @@ //! A widget for optional data, with different `Some` and `None` children. +use crate::debug_state::DebugState; + use druid::{ BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Point, Size, UpdateCtx, Widget, WidgetExt, WidgetPod, @@ -141,6 +143,19 @@ impl Widget> for Maybe { None => self.widget.with_none(|w| w.paint(ctx, &(), env)), }; } + + fn debug_state(&self, data: &Option) -> DebugState { + let child_state = match (&self.widget, data.as_ref()) { + (MaybeWidget::Some(widget_pod), Some(d)) => vec![widget_pod.widget().debug_state(d)], + (MaybeWidget::None(widget_pod), None) => vec![widget_pod.widget().debug_state(&())], + _ => vec![], + }; + DebugState { + display_name: self.short_type_name().to_string(), + children: child_state, + ..Default::default() + } + } } impl MaybeWidget { diff --git a/druid/src/widget/padding.rs b/druid/src/widget/padding.rs index a075ee9d2a..63d84e14e7 100644 --- a/druid/src/widget/padding.rs +++ b/druid/src/widget/padding.rs @@ -14,6 +14,7 @@ //! A widget that just adds padding during layout. +use crate::debug_state::DebugState; use crate::widget::{prelude::*, WidgetWrapper}; use crate::{Data, Insets, KeyOrValue, Point, WidgetPod}; @@ -113,4 +114,12 @@ impl> Widget for Padding { fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { self.child.paint(ctx, data, env); } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.child.widget().debug_state(data)], + ..Default::default() + } + } } diff --git a/druid/src/widget/parse.rs b/druid/src/widget/parse.rs index cfb668beaf..41c75a4abf 100644 --- a/druid/src/widget/parse.rs +++ b/druid/src/widget/parse.rs @@ -17,6 +17,7 @@ use std::mem; use std::str::FromStr; use tracing::instrument; +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::Data; @@ -87,4 +88,12 @@ impl> Widget> for Parse fn id(&self) -> Option { self.widget.id() } + + fn debug_state(&self, _data: &Option) -> DebugState { + DebugState { + display_name: "Parse".to_string(), + main_value: self.state.clone(), + ..Default::default() + } + } } diff --git a/druid/src/widget/progress_bar.rs b/druid/src/widget/progress_bar.rs index 573df3feeb..beae85ceb6 100644 --- a/druid/src/widget/progress_bar.rs +++ b/druid/src/widget/progress_bar.rs @@ -14,6 +14,7 @@ //! A progress bar widget. +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::{theme, LinearGradient, Point, Rect, UnitPoint}; use tracing::instrument; @@ -118,4 +119,12 @@ impl Widget for ProgressBar { ); ctx.fill(rounded_rect, &bar_gradient); } + + fn debug_state(&self, data: &f64) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + main_value: data.to_string(), + ..Default::default() + } + } } diff --git a/druid/src/widget/radio.rs b/druid/src/widget/radio.rs index a3436d8fe6..97aca78b11 100644 --- a/druid/src/widget/radio.rs +++ b/druid/src/widget/radio.rs @@ -14,6 +14,7 @@ //! A radio button widget. +use crate::debug_state::DebugState; use crate::kurbo::Circle; use crate::widget::prelude::*; use crate::widget::{CrossAxisAlignment, Flex, Label, LabelText}; @@ -162,4 +163,17 @@ impl Widget for Radio { // Paint the text label self.child_label.draw_at(ctx, (size + x_padding, 0.0)); } + + fn debug_state(&self, data: &T) -> DebugState { + let value_text = if *data == self.variant { + format!("[X] {}", self.child_label.text()) + } else { + self.child_label.text().to_string() + }; + DebugState { + display_name: self.short_type_name().to_string(), + main_value: value_text, + ..Default::default() + } + } } diff --git a/druid/src/widget/scope.rs b/druid/src/widget/scope.rs index 4f29f80d00..37fa472071 100644 --- a/druid/src/widget/scope.rs +++ b/druid/src/widget/scope.rs @@ -307,6 +307,9 @@ impl> Widget for Scope { fn paint(&mut self, ctx: &mut PaintCtx, data: &SP::In, env: &Env) { self.with_state(data, |state, inner| inner.paint_raw(ctx, state, env)); } + + // TODO + // fn debug_state(&self, data: &SP::In) -> DebugState; } impl> WidgetWrapper for Scope { diff --git a/druid/src/widget/scroll.rs b/druid/src/widget/scroll.rs index d2e7f012aa..50e333b084 100644 --- a/druid/src/widget/scroll.rs +++ b/druid/src/widget/scroll.rs @@ -14,6 +14,7 @@ //! A container that scrolls its contents. +use crate::debug_state::DebugState; use crate::widget::prelude::*; use crate::widget::{Axis, ClipBox}; use crate::{scroll_component::*, Data, Rect, Vec2}; @@ -226,6 +227,14 @@ impl> Widget for Scroll { self.scroll_component .draw_bars(ctx, &self.clip.viewport(), env); } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![self.clip.debug_state(data)], + ..Default::default() + } + } } fn log_size_warnings(size: Size) { diff --git a/druid/src/widget/sized_box.rs b/druid/src/widget/sized_box.rs index 70ec522f84..7800b0fc41 100644 --- a/druid/src/widget/sized_box.rs +++ b/druid/src/widget/sized_box.rs @@ -14,6 +14,7 @@ //! A widget with predefined size. +use crate::debug_state::DebugState; use std::f64::INFINITY; use tracing::{instrument, trace, warn}; @@ -190,6 +191,19 @@ impl Widget for SizedBox { fn id(&self) -> Option { self.child.as_ref().and_then(|child| child.id()) } + + fn debug_state(&self, data: &T) -> DebugState { + let children = if let Some(child) = &self.child { + vec![child.debug_state(data)] + } else { + vec![] + }; + DebugState { + display_name: self.short_type_name().to_string(), + children, + ..Default::default() + } + } } #[cfg(test)] diff --git a/druid/src/widget/slider.rs b/druid/src/widget/slider.rs index 5897046696..4c3d98e1dd 100644 --- a/druid/src/widget/slider.rs +++ b/druid/src/widget/slider.rs @@ -14,6 +14,7 @@ //! A slider widget. +use crate::debug_state::DebugState; use crate::kurbo::{Circle, Shape}; use crate::widget::prelude::*; use crate::{theme, LinearGradient, Point, Rect, UnitPoint}; @@ -282,4 +283,12 @@ impl Widget for Slider { //Actually paint the knob ctx.fill(knob_circle, &knob_gradient); } + + fn debug_state(&self, data: &f64) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + main_value: data.to_string(), + ..Default::default() + } + } } diff --git a/druid/src/widget/split.rs b/druid/src/widget/split.rs index 8e81c85384..21270d90ab 100644 --- a/druid/src/widget/split.rs +++ b/druid/src/widget/split.rs @@ -14,6 +14,7 @@ //! A widget which splits an area in two, with a settable ratio, and optional draggable resizing. +use crate::debug_state::DebugState; use crate::kurbo::Line; use crate::widget::flex::Axis; use crate::widget::prelude::*; @@ -501,4 +502,15 @@ impl Widget for Split { self.child1.paint(ctx, data, env); self.child2.paint(ctx, data, env); } + + fn debug_state(&self, data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + children: vec![ + self.child1.widget().debug_state(data), + self.child2.widget().debug_state(data), + ], + ..Default::default() + } + } } diff --git a/druid/src/widget/stepper.rs b/druid/src/widget/stepper.rs index e11b570caa..55ae97f0b2 100644 --- a/druid/src/widget/stepper.rs +++ b/druid/src/widget/stepper.rs @@ -18,6 +18,7 @@ use std::f64::EPSILON; use std::time::Duration; use tracing::{instrument, trace}; +use crate::debug_state::DebugState; use crate::kurbo::BezPath; use crate::piet::{LinearGradient, RenderContext, UnitPoint}; use crate::widget::prelude::*; @@ -282,4 +283,12 @@ impl Widget for Stepper { ctx.request_paint(); } } + + fn debug_state(&self, data: &f64) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + main_value: data.to_string(), + ..Default::default() + } + } } diff --git a/druid/src/widget/switch.rs b/druid/src/widget/switch.rs index aeb8635085..7186292b84 100644 --- a/druid/src/widget/switch.rs +++ b/druid/src/widget/switch.rs @@ -17,6 +17,7 @@ use std::time::Duration; use tracing::{instrument, trace}; +use crate::debug_state::DebugState; use crate::kurbo::{Circle, Shape}; use crate::piet::{LinearGradient, RenderContext, UnitPoint}; use crate::widget::prelude::*; @@ -334,4 +335,12 @@ impl Widget for Switch { // paint on/off label self.paint_labels(ctx, env, switch_width); } + + fn debug_state(&self, data: &bool) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + main_value: data.to_string(), + ..Default::default() + } + } } diff --git a/druid/src/widget/textbox.rs b/druid/src/widget/textbox.rs index 038771aaa7..54a1ba2966 100644 --- a/druid/src/widget/textbox.rs +++ b/druid/src/widget/textbox.rs @@ -17,6 +17,7 @@ use std::time::Duration; use tracing::{instrument, trace}; +use crate::debug_state::DebugState; use crate::kurbo::Insets; use crate::piet::TextLayout as _; use crate::text::{ @@ -671,6 +672,15 @@ impl Widget for TextBox { // Paint the border ctx.stroke(clip_rect, &border_color, border_width); } + + fn debug_state(&self, data: &T) -> DebugState { + let text = data.slice(0..data.len()).unwrap_or_default(); + DebugState { + display_name: self.short_type_name().to_string(), + main_value: text.to_string(), + ..Default::default() + } + } } impl Default for TextBox { diff --git a/druid/src/widget/value_textbox.rs b/druid/src/widget/value_textbox.rs index 34699bd6bd..a63e55cb4f 100644 --- a/druid/src/widget/value_textbox.rs +++ b/druid/src/widget/value_textbox.rs @@ -17,6 +17,7 @@ use tracing::instrument; use super::TextBox; +use crate::debug_state::DebugState; use crate::text::{Formatter, Selection, TextComponent, ValidationError}; use crate::widget::prelude::*; use crate::{Data, Selector}; @@ -414,4 +415,12 @@ impl Widget for ValueTextBox { fn paint(&mut self, ctx: &mut PaintCtx, _data: &T, env: &Env) { self.child.paint(ctx, &self.buffer, env); } + + fn debug_state(&self, _data: &T) -> DebugState { + DebugState { + display_name: self.short_type_name().to_string(), + main_value: self.buffer.clone(), + ..Default::default() + } + } } diff --git a/druid/src/widget/widget.rs b/druid/src/widget/widget.rs index 4b6843069c..a1da95aa1b 100644 --- a/druid/src/widget/widget.rs +++ b/druid/src/widget/widget.rs @@ -16,6 +16,7 @@ use std::num::NonZeroU64; use std::ops::{Deref, DerefMut}; use super::prelude::*; +use crate::debug_state::DebugState; /// A unique identifier for a single [`Widget`]. /// @@ -197,6 +198,30 @@ pub trait Widget { fn type_name(&self) -> &'static str { std::any::type_name::() } + + #[doc(hidden)] + /// Get the (abridged) type name of the widget for debugging purposes. + /// You should not override this method. + fn short_type_name(&self) -> &'static str { + let name = self.type_name(); + name.split('<') + .next() + .unwrap_or(name) + .split("::") + .last() + .unwrap_or(name) + } + + #[doc(hidden)] + /// From the current data, get a best-effort description of the state of + /// this widget and its children for debugging purposes. + fn debug_state(&self, data: &T) -> DebugState { + #![allow(unused_variables)] + DebugState { + display_name: self.short_type_name().to_string(), + ..Default::default() + } + } } impl WidgetId { @@ -262,4 +287,8 @@ impl Widget for Box> { fn type_name(&self) -> &'static str { self.deref().type_name() } + + fn debug_state(&self, data: &T) -> DebugState { + self.deref().debug_state(data) + } } diff --git a/druid/src/window.rs b/druid/src/window.rs index c0fac55ecc..715ffd176d 100644 --- a/druid/src/window.rs +++ b/druid/src/window.rs @@ -27,6 +27,7 @@ use crate::shell::{text::InputHandler, Counter, Cursor, Region, TextFieldToken, use crate::app::{PendingWindow, WindowSizePolicy}; use crate::contexts::ContextState; use crate::core::{CommandQueue, FocusChange, WidgetState}; +use crate::debug_state::DebugState; use crate::menu::{MenuItemId, MenuManager}; use crate::text::TextFieldRegistration; use crate::util::ExtendDrain; @@ -523,6 +524,11 @@ impl Window { } } + /// Get a best-effort representation of the entire widget tree for debug purposes. + pub fn root_debug_state(&self, data: &T) -> DebugState { + self.root.widget().debug_state(data) + } + pub(crate) fn update_title(&mut self, data: &T, env: &Env) { if self.title.resolve(data, env) { self.handle.set_title(&self.title.display_text());