Skip to content

Commit 0d56631

Browse files
authored
Add debug option to display widget ids (#876)
* Add debug option to display widget ids This will paint the id of each widget of some subtree in the bottom right of that widget's bounds; useful for debugging various bits of event flow and widget behaviour. * Paint debug widget ids with z-order This ensures that the ids of children will not be obscured by the ids of their parents. This also adds caching of the textlayout object. * Update changelog
1 parent 8ea0279 commit 0d56631

File tree

6 files changed

+122
-10
lines changed

6 files changed

+122
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ While some features like the clipboard, menus or file dialogs are not yet availa
3333
- `Env` and `Key` gained methods for inspecting an `Env` at runtime ([#880] by [@Zarenor])
3434
- `UpdateCtx::request_timer` and `UpdateCtx::request_anim_frame`. ([#898] by [@finnerale])
3535
- `UpdateCtx::size` and `LifeCycleCtx::size`. ([#917] by [@jneem])
36+
- `WidgetExt::debug_widget_id`, for displaying widget ids on hover. ([#876] by [@cmyr])
3637

3738
### Changed
3839

@@ -150,6 +151,7 @@ While some features like the clipboard, menus or file dialogs are not yet availa
150151
[#857]: https://github.com/xi-editor/druid/pull/857
151152
[#861]: https://github.com/xi-editor/druid/pull/861
152153
[#869]: https://github.com/xi-editor/druid/pull/869
154+
[#876]: https://github.com/xi-editor/druid/pull/876
153155
[#878]: https://github.com/xi-editor/druid/pull/878
154156
[#880]: https://github.com/xi-editor/druid/pull/880
155157
[#889]: https://github.com/xi-editor/druid/pull/889

druid/src/contexts.rs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ pub struct PaintCtx<'a, 'b: 'a> {
114114
pub(crate) region: Region,
115115
pub(crate) base_state: &'a BaseState,
116116
pub(crate) focus_widget: Option<WidgetId>,
117+
/// The approximate depth in the tree at the time of painting.
118+
pub(crate) depth: u32,
117119
}
118120

119121
/// A region of a widget, generally used to describe what needs to be drawn.
@@ -341,7 +343,10 @@ impl<'a> EventCtx<'a> {
341343
if self.is_focused() {
342344
self.base_state.request_focus = Some(FocusChange::Resign);
343345
} else {
344-
log::warn!("resign_focus can only be called by the currently focused widget");
346+
log::warn!(
347+
"resign_focus can only be called by the currently focused widget ({:?})",
348+
self.widget_id()
349+
);
345350
}
346351
}
347352

@@ -705,6 +710,20 @@ impl<'a, 'b: 'a> PaintCtx<'a, 'b> {
705710
self.base_state.has_focus
706711
}
707712

713+
/// The depth in the tree of the currently painting widget.
714+
///
715+
/// This may be used in combination with [`paint_with_z_index`] in order
716+
/// to correctly order painting operations.
717+
///
718+
/// The `depth` here may not be exact; it is only guaranteed that a child will
719+
/// have a greater depth than its parent.
720+
///
721+
/// [`paint_with_z_index`]: #method.paint_with_z_index
722+
#[inline]
723+
pub fn depth(&self) -> u32 {
724+
self.depth
725+
}
726+
708727
/// Returns the currently visible [`Region`].
709728
///
710729
/// [`Region`]: struct.Region.html
@@ -726,6 +745,7 @@ impl<'a, 'b: 'a> PaintCtx<'a, 'b> {
726745
window_id: self.window_id,
727746
focus_widget: self.focus_widget,
728747
region: region.into(),
748+
depth: self.depth + 1,
729749
};
730750
f(&mut child_ctx);
731751
self.z_ops.append(&mut child_ctx.z_ops);

druid/src/core.rs

Lines changed: 79 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ use std::collections::{HashMap, VecDeque};
1818

1919
use crate::bloom::Bloom;
2020
use crate::kurbo::{Affine, Insets, Point, Rect, Shape, Size, Vec2};
21-
use crate::piet::RenderContext;
21+
use crate::piet::{
22+
FontBuilder, PietTextLayout, RenderContext, Text, TextLayout, TextLayoutBuilder,
23+
};
2224
use crate::{
23-
BoxConstraints, Command, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle,
25+
BoxConstraints, Color, Command, Data, Env, Event, EventCtx, InternalEvent, InternalLifeCycle,
2426
LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Region, Target, TimerToken, UpdateCtx, Widget,
2527
WidgetId, WindowId,
2628
};
@@ -45,6 +47,8 @@ pub struct WidgetPod<T, W> {
4547
old_data: Option<T>,
4648
env: Option<Env>,
4749
inner: W,
50+
// stashed layout so we don't recompute this when debugging
51+
debug_widget_text: Option<PietTextLayout>,
4852
}
4953

5054
/// Generic state for all widgets in the hierarchy.
@@ -141,6 +145,7 @@ impl<T, W: Widget<T>> WidgetPod<T, W> {
141145
old_data: None,
142146
env: None,
143147
inner,
148+
debug_widget_text: None,
144149
}
145150
}
146151

@@ -316,6 +321,10 @@ impl<T, W: Widget<T>> WidgetPod<T, W> {
316321
window_id,
317322
};
318323
child.lifecycle(&mut child_ctx, &hot_changed_event, data, env);
324+
// if hot changes and we're showing widget ids, always repaint
325+
if env.get(Env::DEBUG_WIDGET_ID) {
326+
child_ctx.request_paint();
327+
}
319328
return true;
320329
}
321330
false
@@ -335,25 +344,33 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
335344
/// [`paint`]: trait.Widget.html#tymethod.paint
336345
/// [`paint_with_offset`]: #method.paint_with_offset
337346
pub fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) {
347+
// we need to do this before we borrow from self
348+
if env.get(Env::DEBUG_WIDGET_ID) {
349+
self.make_widget_id_layout_if_needed(self.state.id, ctx, env);
350+
}
351+
338352
let mut inner_ctx = PaintCtx {
339353
render_ctx: ctx.render_ctx,
340354
window_id: ctx.window_id,
341355
z_ops: Vec::new(),
342356
region: ctx.region.clone(),
343357
base_state: &self.state,
344358
focus_widget: ctx.focus_widget,
359+
depth: ctx.depth,
345360
};
346361
self.inner.paint(&mut inner_ctx, data, env);
347-
ctx.z_ops.append(&mut inner_ctx.z_ops);
348362

349-
if env.get(Env::DEBUG_PAINT) {
350-
const BORDER_WIDTH: f64 = 1.0;
351-
let rect = inner_ctx.size().to_rect().inset(BORDER_WIDTH / -2.0);
352-
let id = self.id().to_raw();
353-
let color = env.get_debug_color(id);
354-
inner_ctx.stroke(rect, &color, BORDER_WIDTH);
363+
let debug_ids = inner_ctx.is_hot() && env.get(Env::DEBUG_WIDGET_ID);
364+
if debug_ids {
365+
// this also draws layout bounds
366+
self.debug_paint_widget_ids(&mut inner_ctx, env);
367+
}
368+
369+
if !debug_ids && env.get(Env::DEBUG_PAINT) {
370+
self.debug_paint_layout_bounds(&mut inner_ctx, env);
355371
}
356372

373+
ctx.z_ops.append(&mut inner_ctx.z_ops);
357374
self.state.invalid = Region::EMPTY;
358375
}
359376

@@ -392,6 +409,58 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
392409
});
393410
}
394411

412+
fn make_widget_id_layout_if_needed(&mut self, id: WidgetId, ctx: &mut PaintCtx, env: &Env) {
413+
if self.debug_widget_text.is_none() {
414+
let font = ctx
415+
.text()
416+
.new_font_by_name(env.get(crate::theme::FONT_NAME), 10.0)
417+
.build()
418+
.unwrap();
419+
let id_string = id.to_raw().to_string();
420+
self.debug_widget_text = ctx
421+
.text()
422+
.new_text_layout(&font, &id_string, f64::INFINITY)
423+
.build()
424+
.ok();
425+
}
426+
}
427+
428+
fn debug_paint_widget_ids(&self, ctx: &mut PaintCtx, env: &Env) {
429+
// we clone because we need to move it for paint_with_z_index
430+
let text = self.debug_widget_text.clone();
431+
if let Some(text) = text {
432+
let text_size = Size::new(text.width(), 10.0);
433+
let origin = ctx.size().to_vec2() - text_size.to_vec2();
434+
let border_color = env.get_debug_color(ctx.widget_id().to_raw());
435+
self.debug_paint_layout_bounds(ctx, env);
436+
437+
ctx.paint_with_z_index(ctx.depth(), move |ctx| {
438+
let origin = Point::new(origin.x.max(0.0), origin.y.max(0.0));
439+
440+
let text_pos = origin + Vec2::new(0., 8.0);
441+
let text_rect = Rect::from_origin_size(origin, text_size);
442+
443+
ctx.fill(text_rect, &border_color);
444+
let (r, g, b, _) = border_color.as_rgba_u8();
445+
let avg = (r as u32 + g as u32 + b as u32) / 3;
446+
let text_color = if avg < 128 {
447+
Color::WHITE
448+
} else {
449+
Color::BLACK
450+
};
451+
ctx.draw_text(&text, text_pos, &text_color);
452+
})
453+
}
454+
}
455+
456+
fn debug_paint_layout_bounds(&self, ctx: &mut PaintCtx, env: &Env) {
457+
const BORDER_WIDTH: f64 = 1.0;
458+
let rect = ctx.size().to_rect().inset(BORDER_WIDTH / -2.0);
459+
let id = self.id().to_raw();
460+
let color = env.get_debug_color(id);
461+
ctx.stroke(rect, &color, BORDER_WIDTH);
462+
}
463+
395464
/// Compute layout of a widget.
396465
///
397466
/// Generally called by container widgets as part of their [`layout`]
@@ -712,6 +781,7 @@ impl<T: Data, W: Widget<T>> WidgetPod<T, W> {
712781

713782
true
714783
}
784+
//NOTE: this is not sent here, but from the special set_hot_state method
715785
LifeCycle::HotChanged(_) => false,
716786
LifeCycle::FocusChanged(_) => {
717787
// We are a descendant of a widget that has/had focus.

druid/src/env.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ impl Env {
155155
/// [`WidgetExt`]: trait.WidgetExt.html
156156
pub(crate) const DEBUG_PAINT: Key<bool> = Key::new("druid.built-in.debug-paint");
157157

158+
/// State for whether or not to paint `WidgetId`s, for event debugging.
159+
///
160+
/// Set by the `debug_widget_id()` method on [`WidgetExt`].
161+
///
162+
/// [`WidgetExt`]: trait.WidgetExt.html
163+
pub(crate) const DEBUG_WIDGET_ID: Key<bool> = Key::new("druid.built-in.debug-widget-id");
164+
158165
/// A key used to tell widgets to print additional debug information.
159166
///
160167
/// This does nothing by default; however you can check this key while
@@ -441,6 +448,7 @@ impl Default for Env {
441448

442449
Env(Arc::new(inner))
443450
.adding(Env::DEBUG_PAINT, false)
451+
.adding(Env::DEBUG_WIDGET_ID, false)
444452
.adding(Env::DEBUG_WIDGET, false)
445453
}
446454
}

druid/src/widget/widget_ext.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,17 @@ pub trait WidgetExt<T: Data>: Widget<T> + Sized + 'static {
182182
EnvScope::new(|env, _| env.set(Env::DEBUG_PAINT, true), self)
183183
}
184184

185+
/// Display the `WidgetId`s for this widget and its children, when hot.
186+
///
187+
/// When this is `true`, widgets that are `hot` (are under the mouse cursor)
188+
/// will display their ids in their bottom right corner.
189+
///
190+
/// These ids may overlap; in this case the id of a child will obscure
191+
/// the id of its parent.
192+
fn debug_widget_id(self) -> EnvScope<T, Self> {
193+
EnvScope::new(|env, _| env.set(Env::DEBUG_WIDGET_ID, true), self)
194+
}
195+
185196
/// Draw a color-changing rectangle over this widget, allowing you to see the
186197
/// invalidation regions.
187198
fn debug_invalidation(self) -> DebugInvalidation<T, Self> {

druid/src/window.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,7 @@ impl<T: Data> Window<T> {
373373
z_ops: Vec::new(),
374374
focus_widget: self.focus,
375375
region: invalid_rect.into(),
376+
depth: 0,
376377
};
377378
ctx.with_child_ctx(invalid_rect, |ctx| self.root.paint(ctx, data, env));
378379

0 commit comments

Comments
 (0)