diff --git a/masonry/README.md b/masonry/README.md index a1f65a995..d4df2e475 100644 --- a/masonry/README.md +++ b/masonry/README.md @@ -108,6 +108,12 @@ The following feature flags are available: - `tracy`: Enables creating output for the [Tracy](https://github.com/wolfpld/tracy) profiler using [`tracing-tracy`][tracing_tracy]. This can be used by installing Tracy and connecting to a Masonry with this feature enabled. +### Debugging features + +Masonry apps currently ship with two debugging features built in: +- A rudimentary widget inspector - toggled by F11 key. +- A debug mode painting widget layout rectangles - toggled by F12 key. + [winit]: https://crates.io/crates/winit [Druid]: https://crates.io/crates/druid [Xilem]: https://crates.io/crates/xilem diff --git a/masonry/src/event_loop_runner.rs b/masonry/src/event_loop_runner.rs index ffafdf3e3..72e62c68f 100644 --- a/masonry/src/event_loop_runner.rs +++ b/masonry/src/event_loop_runner.rs @@ -8,7 +8,7 @@ use std::num::NonZeroUsize; use std::sync::Arc; use accesskit_winit::Adapter; -use tracing::{debug, info_span, warn}; +use tracing::{debug, info, info_span, warn}; use vello::kurbo::Affine; use vello::util::{RenderContext, RenderSurface}; use vello::{AaSupport, RenderParams, Renderer, RendererOptions, Scene}; @@ -737,6 +737,17 @@ impl MasonryState<'_> { render_root::RenderRootSignal::ShowWindowMenu(position) => { window.show_window_menu(position); } + render_root::RenderRootSignal::WidgetSelectedInInspector(widget_id) => { + let (widget, state) = self.render_root.widget_arena.get_pair(widget_id); + let widget_name = widget.item.short_type_name(); + let display_name = if let Some(debug_text) = widget.item.get_debug_text() { + format!("{widget_name}<{debug_text}>") + } else { + widget_name.into() + }; + info!("Widget selected in inspector: {widget_id} - {display_name}"); + info!("{:#?}", state.item); + } } } diff --git a/masonry/src/lib.rs b/masonry/src/lib.rs index 3846c41c7..0cf807ca3 100644 --- a/masonry/src/lib.rs +++ b/masonry/src/lib.rs @@ -86,6 +86,12 @@ //! - `tracy`: Enables creating output for the [Tracy](https://github.com/wolfpld/tracy) profiler using [`tracing-tracy`][tracing_tracy]. //! This can be used by installing Tracy and connecting to a Masonry with this feature enabled. //! +//! ### Debugging features +//! +//! Masonry apps currently ship with two debugging features built in: +//! - A rudimentary widget inspector - toggled by F11 key. +//! - A debug mode painting widget layout rectangles - toggled by F12 key. +//! //! [winit]: https://crates.io/crates/winit //! [Druid]: https://crates.io/crates/druid //! [Xilem]: https://crates.io/crates/xilem diff --git a/masonry/src/passes/event.rs b/masonry/src/passes/event.rs index 983a6e7d6..fbddcdb67 100644 --- a/masonry/src/passes/event.rs +++ b/masonry/src/passes/event.rs @@ -8,7 +8,9 @@ use winit::keyboard::{KeyCode, PhysicalKey}; use crate::passes::{enter_span, merge_state_up}; use crate::render_root::RenderRoot; -use crate::{AccessEvent, EventCtx, Handled, PointerEvent, TextEvent, Widget, WidgetId}; +use crate::{ + AccessEvent, EventCtx, Handled, PointerEvent, RenderRootSignal, TextEvent, Widget, WidgetId, +}; // --- MARK: HELPERS --- fn get_pointer_target( @@ -103,6 +105,32 @@ pub(crate) fn run_on_pointer_event_pass(root: &mut RenderRoot, event: &PointerEv root.last_mouse_pos = event.position(); } + if root.global_state.inspector_state.is_picking_widget + && matches!(event, PointerEvent::PointerMove(..)) + { + root.global_state.needs_pointer_pass = true; + return Handled::Yes; + } + + // If the widget picker is active and this is a click event, + // we select the widget under the mouse and short-circuit the event pass. + if root.global_state.inspector_state.is_picking_widget + && matches!(event, PointerEvent::PointerDown(..)) + { + let target_widget_id = get_pointer_target(root, event.position()); + if let Some(target_widget_id) = target_widget_id { + root.global_state + .emit_signal(RenderRootSignal::WidgetSelectedInInspector( + target_widget_id, + )); + } + root.global_state.inspector_state.is_picking_widget = false; + root.global_state.inspector_state.hovered_widget = None; + root.global_state.needs_pointer_pass = true; + root.root_state_mut().needs_paint = true; + return Handled::Yes; + } + let target_widget_id = get_pointer_target(root, event.position()); if matches!(event, PointerEvent::PointerDown(..)) { @@ -188,8 +216,8 @@ pub(crate) fn run_on_text_event_pass(root: &mut RenderRoot, event: &TextEvent) - !event.is_high_density(), ); - // Handle Tab focus if let TextEvent::KeyboardKey(key, mods) = event { + // Handle Tab focus if key.physical_key == PhysicalKey::Code(KeyCode::Tab) && key.state == ElementState::Pressed && handled == Handled::No @@ -199,6 +227,27 @@ pub(crate) fn run_on_text_event_pass(root: &mut RenderRoot, event: &TextEvent) - root.global_state.next_focused_widget = next_focused_widget; handled = Handled::Yes; } + + if key.physical_key == PhysicalKey::Code(KeyCode::F11) + && key.state == ElementState::Pressed + && handled == Handled::No + { + root.global_state.inspector_state.is_picking_widget = + !root.global_state.inspector_state.is_picking_widget; + root.global_state.inspector_state.hovered_widget = None; + root.global_state.needs_pointer_pass = true; + root.root_state_mut().needs_paint = true; + handled = Handled::Yes; + } + + if key.physical_key == PhysicalKey::Code(KeyCode::F12) + && key.state == ElementState::Pressed + && handled == Handled::No + { + root.debug_paint = !root.debug_paint; + root.root_state_mut().needs_paint = true; + handled = Handled::Yes; + } } if !event.is_high_density() { diff --git a/masonry/src/passes/paint.rs b/masonry/src/passes/paint.rs index 1b8cf53f5..9333e43c3 100644 --- a/masonry/src/passes/paint.rs +++ b/masonry/src/passes/paint.rs @@ -5,14 +5,15 @@ use std::collections::HashMap; use tracing::{info_span, trace}; use tree_arena::ArenaMut; -use vello::peniko::Mix; +use vello::kurbo::Affine; +use vello::peniko::{Color, Fill, Mix}; use vello::Scene; use crate::paint_scene_helpers::stroke; use crate::passes::{enter_span_if, recurse_on_children}; use crate::render_root::{RenderRoot, RenderRootState}; use crate::theme::get_debug_color; -use crate::{PaintCtx, Widget, WidgetId, WidgetState}; +use crate::{PaintCtx, Rect, Widget, WidgetId, WidgetState}; // --- MARK: PAINT WIDGET --- fn paint_widget( @@ -110,8 +111,6 @@ fn paint_widget( pub(crate) fn run_paint_pass(root: &mut RenderRoot) -> Scene { let _span = info_span!("paint").entered(); - let debug_paint = std::env::var("MASONRY_DEBUG_PAINT").is_ok_and(|it| !it.is_empty()); - // TODO - Reserve scene // https://github.com/linebender/xilem/issues/524 let mut complete_scene = Scene::new(); @@ -141,9 +140,24 @@ pub(crate) fn run_paint_pass(root: &mut RenderRoot) -> Scene { &mut scenes, root_widget, root_state, - debug_paint, + root.debug_paint, ); root.global_state.scenes = scenes; + // Display a rectangle over the hovered widget + if let Some(hovered_widget) = root.global_state.inspector_state.hovered_widget { + const HOVER_FILL_COLOR: Color = Color::from_rgba8(60, 60, 250, 100); + let state = root.widget_arena.get_state(hovered_widget).item; + let rect = Rect::from_origin_size(state.window_origin(), state.size); + + complete_scene.fill( + Fill::NonZero, + Affine::IDENTITY, + HOVER_FILL_COLOR, + None, + &rect, + ); + } + complete_scene } diff --git a/masonry/src/passes/update.rs b/masonry/src/passes/update.rs index b0b9a6f49..d7188baba 100644 --- a/masonry/src/passes/update.rs +++ b/masonry/src/passes/update.rs @@ -584,6 +584,17 @@ pub(crate) fn run_update_pointer_pass(root: &mut RenderRoot) { let pointer_pos = root.last_mouse_pos.map(|pos| (pos.x, pos.y).into()); + if root.global_state.inspector_state.is_picking_widget { + if let Some(pos) = pointer_pos { + root.global_state.inspector_state.hovered_widget = root + .get_root_widget() + .find_widget_at_pos(pos) + .map(|widget| widget.id()); + } + root.root_state_mut().needs_paint = true; + return; + } + // Release pointer capture if target can no longer hold it. if let Some(id) = root.global_state.pointer_capture_target { if !root.is_still_interactive(id) { diff --git a/masonry/src/render_root.rs b/masonry/src/render_root.rs index ece1a2706..57fa26d2c 100644 --- a/masonry/src/render_root.rs +++ b/masonry/src/render_root.rs @@ -82,6 +82,7 @@ pub struct RenderRoot { /// The widget tree; stores widgets and their states. pub(crate) widget_arena: WidgetArena, + pub(crate) debug_paint: bool, } /// State shared between passes. @@ -138,6 +139,7 @@ pub(crate) struct RenderRootState { /// Pass tracing configuration, used to skip tracing to limit overhead. pub(crate) trace: PassTracing, + pub(crate) inspector_state: InspectorState, } pub(crate) struct MutateCallback { @@ -218,6 +220,16 @@ pub enum RenderRootSignal { Exit, /// The window menu is being shown. ShowWindowMenu(LogicalPosition), + /// The widget picker has selected this widget. + WidgetSelectedInInspector(WidgetId), +} + +/// State of the widget inspector. Useful for debugging. +/// +/// Widget inspector is WIP. It should get its own standalone documentation. +pub(crate) struct InspectorState { + pub(crate) is_picking_widget: bool, + pub(crate) hovered_widget: Option, } impl RenderRoot { @@ -233,6 +245,8 @@ impl RenderRoot { scale_factor, test_font, } = options; + let debug_paint = std::env::var("MASONRY_DEBUG_PAINT").is_ok_and(|it| !it.is_empty()); + let mut root = Self { root: WidgetPod::new(root_widget).boxed(), size_policy, @@ -264,12 +278,17 @@ impl RenderRoot { scenes: HashMap::new(), needs_pointer_pass: false, trace: PassTracing::from_env(), + inspector_state: InspectorState { + is_picking_widget: false, + hovered_widget: None, + }, }, widget_arena: WidgetArena { widgets: TreeArena::new(), states: TreeArena::new(), }, rebuild_access_tree: true, + debug_paint, }; if let Some(test_font_data) = test_font { diff --git a/masonry/src/testing/harness.rs b/masonry/src/testing/harness.rs index ae8fe384b..5e9238d77 100644 --- a/masonry/src/testing/harness.rs +++ b/masonry/src/testing/harness.rs @@ -296,6 +296,7 @@ impl TestHarness { RenderRootSignal::Minimize => (), RenderRootSignal::Exit => (), RenderRootSignal::ShowWindowMenu(_) => (), + RenderRootSignal::WidgetSelectedInInspector(_) => (), } } } diff --git a/masonry/src/tracing_backend.rs b/masonry/src/tracing_backend.rs index 7b7eaa32b..e3a4c26ea 100644 --- a/masonry/src/tracing_backend.rs +++ b/masonry/src/tracing_backend.rs @@ -150,6 +150,7 @@ pub(crate) fn try_init_tracing() -> Result<(), SetGlobalDefaultError> { } else { LevelFilter::INFO }; + #[cfg(not(target_arch = "wasm32"))] { try_init_layered_tracing(default_level)