Tooling · Inspection: stalls count whole, the panel owns Tab, bound keys log

FpsMeter keeps the stamp before the last second, so a frame longer
than the span shows as the worst and a bar. The inspector's panel, a
second root out of the app's scope, sits in a FocusScope of its own,
so Tab goes on from Pick. EventLogger takes key downs through an
interceptor, which runs before bindings, and marks its box with a key
context to know focus is inside, so a field's Backspace and Enter log.
Tests hold the stall, Tab both ways through Pick, bound keys in a
field, a key while focus is outside, newest first and a bare border.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-28 07:54:54 -04:00
1 parent 5f951ea581
commit 517d23f78f
6 files changed
+182 -40

No files matched your search

+27 -12
View File
@@ -6,10 +6,10 @@ use std::{
};
use gpui::{
AnyElement, App, DispatchPhase, ElementId, InteractiveElement, IntoElement, Modifiers,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
RenderOnce, ScrollDelta, ScrollWheelEvent, StatefulInteractiveElement, Styled, Window, canvas,
div, prelude::FluentBuilder,
AnyElement, App, DispatchPhase, ElementId, InteractiveElement, IntoElement, KeyContext,
Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels,
Point, RenderOnce, ScrollDelta, ScrollWheelEvent, StatefulInteractiveElement, Styled, Window,
canvas, div, prelude::FluentBuilder,
};
use smallvec::SmallVec;
@@ -30,7 +30,7 @@ enum Seen {
/// Where the pointer is, and how many moves ran together to get there.
Move(Point<Pixels>, usize),
Wheel(ScrollDelta),
KeyDown(String, bool),
KeyDown(String),
KeyUp(String),
Modifiers(Modifiers),
}
@@ -59,8 +59,7 @@ impl fmt::Display for Seen {
Seen::Wheel(ScrollDelta::Lines(delta)) => {
write!(f, "wheel {}, {} lines", delta.x, delta.y)
}
Seen::KeyDown(key, false) => write!(f, "key down {key}"),
Seen::KeyDown(key, true) => write!(f, "key held {key}"),
Seen::KeyDown(key) => write!(f, "key down {key}"),
Seen::KeyUp(key) => write!(f, "key up {key}"),
Seen::Modifiers(held) => {
let names = [
@@ -109,7 +108,7 @@ impl Log {
type Record = Rc<dyn Fn(Seen, &mut App)>;
/// The input events that reach what it holds, newest first below it: presses, releases, moves run together while the pointer travels, wheels, keys and modifiers, each timed from the first. Pointer events count inside its box, even where a child stops them; keys count while focus is inside. It keeps the last fifty.
/// The input events that reach what it holds, newest first below it: presses, releases, moves run together while the pointer travels, wheels, keys and modifiers, each timed from the first. Pointer events count inside its box, even where a child stops them; keys count while focus is inside, a key a binding takes too. It keeps the last fifty.
#[derive(IntoElement)]
pub struct EventLogger {
id: ElementId,
@@ -149,14 +148,30 @@ impl RenderOnce for EventLogger {
});
}
});
// Bindings run before key listeners, so key downs come through an interceptor.
let mark = id.to_string();
window.use_keyed_state((id.clone(), "keys"), cx, {
let (down, mark) = (record.clone(), mark.clone());
move |_, cx| {
cx.intercept_keystrokes(move |event, _, cx| {
let inside = event
.context_stack
.iter()
.any(|context| context.get("logger").is_some_and(|logger| *logger == mark));
if inside {
down(Seen::KeyDown(event.keystroke.unparse()), cx)
}
})
}
});
let mut context = KeyContext::default();
context.set("logger", mark);
let theme = cx.theme();
let colors = &theme.colors;
let (down, up, held) = (record.clone(), record.clone(), record.clone());
let (up, held) = (record.clone(), record.clone());
let watched = div()
.relative()
.capture_key_down(move |event, _, cx| {
down(Seen::KeyDown(event.keystroke.unparse(), event.is_held), cx)
})
.key_context(context)
.capture_key_up(move |event, _, cx| up(Seen::KeyUp(event.keystroke.unparse()), cx))
.on_modifiers_changed(move |event, _, cx| held(Seen::Modifiers(event.modifiers), cx))
// Laid first, so its capture listeners run before any child's.
+17 -3
View File
@@ -9,7 +9,7 @@ use gpui::{
use crate::{
buttons::{Button, ButtonVariant},
primitives::raise,
primitives::{FocusScope, raise},
theme::{ActiveTheme, ControlSize, Radius, TextSize},
typography::{Ellipsis, LEADING, literal, tabular},
};
@@ -305,6 +305,10 @@ fn panel(
)
});
let states = inspector.render_inspector_states(window, cx);
let focus = window
.use_keyed_state("ely-inspector-focus", cx, |_, cx| cx.focus_handle())
.read(cx)
.clone();
let owner = cx.entity();
let theme = cx.theme();
let colors = &theme.colors;
@@ -314,7 +318,7 @@ fn panel(
}
false => "Held. Pick chooses another box.",
};
div()
let body = div()
.id("ely-inspector")
.size_full()
.flex()
@@ -365,7 +369,11 @@ fn panel(
.child(Ellipsis::new(ids)),
)
}))
.children(states)
.children(states);
// A second root, out of the app's scope: it owns Tab itself.
FocusScope::new(&focus)
.size_full()
.child(body)
.into_any_element()
}
@@ -394,6 +402,12 @@ mod tests {
);
assert_eq!(written.padding, [Side::Px(8.0); 4]);
assert_eq!(written.border, [Side::Px(2.0); 4]);
let bare = layers(&StyleRefinement::default(), px(16.0));
assert_eq!(
bare.border,
[Side::Px(0.0); 4],
"no border written, none read"
);
let mut share = StyleRefinement::default();
share.padding.left = Some(relative(0.25));
let shown = layers(&share, px(16.0))
+14 -4
View File
@@ -25,7 +25,7 @@ fn missed(interval: Duration) -> u32 {
(interval.as_secs_f64() / BUDGET.as_secs_f64() - 0.5).max(0.0) as u32
}
/// Frame stamps over the last second, oldest first.
/// Frame stamps over the last second and the one before it, oldest first, so a frame that ran into the second counts whole.
#[derive(Default)]
struct Frames(VecDeque<Instant>);
@@ -34,8 +34,8 @@ impl Frames {
self.0.push_back(now);
while self
.0
.front()
.is_some_and(|first| now.duration_since(*first) > SPAN)
.get(1)
.is_some_and(|second| now.duration_since(*second) >= SPAN)
{
self.0.pop_front();
}
@@ -60,7 +60,7 @@ impl Frames {
}
}
/// Frames per second over the last second, the worst frame, and a bar per recent frame against the 60 Hz budget: success on time, warning a frame late, danger later. While shown it keeps its view drawing, so it counts the display's frames and holds the window awake; show it only while measuring. Its numbers are readings, so reduced motion leaves them live.
/// Frames per second and the worst frame over the last second, a frame that ran into it counted whole, and a bar per recent frame against the 60 Hz budget: success on time, warning a frame late, danger later. While shown it keeps its view drawing, so it counts the display's frames and holds the window awake; show it only while measuring. Its numbers are readings, so reduced motion leaves them live.
#[derive(IntoElement)]
pub struct FpsMeter {
id: ElementId,
@@ -204,6 +204,16 @@ mod tests {
assert_eq!(frames.worst(), Some(Duration::from_millis(100)));
}
#[test]
fn a_stall_longer_than_the_span_counts_whole() {
let (start, mut frames) = (Instant::now(), Frames::default());
for ms in [0, 500, 2000] {
frames.record(start + Duration::from_millis(ms));
}
assert_eq!(frames.worst(), Some(Duration::from_millis(1500)));
assert_eq!(frames.fps().map(|fps| (fps * 100.0).round()), Some(67.0));
}
#[test]
fn a_frame_is_late_by_whole_frames() {
let tenths = |tenths: u64| Duration::from_micros(tenths * 100);
+121 -19
View File
@@ -1,13 +1,15 @@
use std::time::Duration;
use gpui::{
Context, FocusHandle, InteractiveElement, IntoElement, KeyUpEvent, Keystroke, Modifiers,
ParentElement, Render, ScrollDelta, ScrollWheelEvent, Styled, TestAppContext, TouchPhase,
VisualTestContext, Window, div, point, px,
AppContext, Context, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement,
KeyBinding, KeyUpEvent, Keystroke, Modifiers, ParentElement, Render, ScrollDelta,
ScrollWheelEvent, Styled, TestAppContext, TouchPhase, VisualTestContext, Window, div, point,
px,
};
use crate::{
primitives::FocusScope,
forms::{self, TextInput},
primitives::{FocusNext, FocusPrev, FocusScope},
theme::Theme,
tooling::{EventLogger, FpsMeter, RenderCounter, install_inspector},
};
@@ -73,6 +75,75 @@ fn the_inspector_shows_the_picked_box(cx: &mut TestAppContext) {
);
}
struct Stops {
root: FocusHandle,
first: FocusHandle,
second: FocusHandle,
}
impl Render for Stops {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
FocusScope::new(&self.root)
.root()
.size_full()
.child(div().track_focus(&self.first).size(px(40.0)))
.child(div().track_focus(&self.second).size(px(40.0)))
}
}
fn press(key: &str, cx: &mut VisualTestContext) {
cx.simulate_keystrokes(key);
cx.run_until_parked();
}
/// Tab reaches the panel's Pick and goes on from it, both ways.
#[gpui::test]
fn tab_passes_through_the_panel(cx: &mut TestAppContext) {
cx.update(|cx| {
Theme::init(cx);
install_inspector(cx);
cx.bind_keys([
KeyBinding::new("tab", FocusNext, None),
KeyBinding::new("shift-tab", FocusPrev, None),
]);
});
let (view, cx) = cx.add_window_view(|_, cx| Stops {
root: cx.focus_handle(),
first: cx.focus_handle().tab_stop(true),
second: cx.focus_handle().tab_stop(true),
});
let (root, first, second) = view.read_with(cx, |stops, _| {
(
stops.root.clone(),
stops.first.clone(),
stops.second.clone(),
)
});
cx.update(|window, cx| {
window.focus(&root);
window.toggle_inspector(cx);
});
cx.run_until_parked();
cx.simulate_click(point(px(20.0), px(20.0)), Modifiers::none());
redraw(cx);
assert!(shown("inspector-pick", cx), "a press holds the box");
let focused = |cx: &mut VisualTestContext| cx.update(|window, cx| window.focused(cx));
for _ in 0..3 {
press("tab", cx);
}
let pick = focused(cx).expect("Pick holds focus");
assert!(
![&root, &first, &second].contains(&&pick),
"Tab reached Pick"
);
press("tab", cx);
assert_eq!(focused(cx), Some(first.clone()), "Tab went on from Pick");
press("shift-tab", cx);
assert_eq!(focused(cx), Some(pick), "Shift-Tab came back");
press("shift-tab", cx);
assert_eq!(focused(cx), Some(second));
}
struct Meter;
impl Render for Meter {
@@ -120,6 +191,7 @@ fn the_counter_counts_renders(cx: &mut TestAppContext) {
struct Logged {
root: FocusHandle,
target: FocusHandle,
field: Entity<TextInput>,
}
impl Render for Logged {
@@ -129,24 +201,30 @@ impl Render for Logged {
.size_full()
.p(px(10.0))
.child(
EventLogger::new("logger").child(
div()
.id("target")
.track_focus(&self.target)
.h(px(100.0))
.capture_any_mouse_down(|_, _, cx| cx.stop_propagation()),
),
EventLogger::new("logger")
.child(
div()
.id("target")
.track_focus(&self.target)
.h(px(100.0))
.capture_any_mouse_down(|_, _, cx| cx.stop_propagation()),
)
.child(self.field.clone()),
)
}
}
/// Pointer events inside the box, even ones a child stops as they arrive, and keys while focus is inside, list with what they carried, placed in the box.
/// Pointer events inside the box, even ones a child stops as they arrive, and keys while focus is inside, bound ones too, list newest first with what they carried, placed in the box.
#[gpui::test]
fn the_logger_lists_events(cx: &mut TestAppContext) {
cx.update(Theme::init);
let (view, cx) = cx.add_window_view(|_, cx| Logged {
cx.update(|cx| {
Theme::init(cx);
forms::bind_keys(cx);
});
let (view, cx) = cx.add_window_view(|window, cx| Logged {
root: cx.focus_handle(),
target: cx.focus_handle(),
field: cx.new(|cx| TextInput::new(window, cx)),
});
let target = view.read_with(cx, |logged, _| logged.target.clone());
cx.update(|window, _| window.focus(&target));
@@ -157,11 +235,13 @@ fn the_logger_lists_events(cx: &mut TestAppContext) {
cx.simulate_mouse_move(point(px(30.0), px(40.0)), None, Modifiers::none());
assert!(shown("event-move to 20, 30 ×2", cx), "moves run together");
cx.simulate_click(point(px(30.0), px(40.0)), Modifiers::none());
assert!(
shown("event-press left at 20, 30", cx),
"a press the child stops"
);
assert!(shown("event-release left at 20, 30", cx));
let pressed = cx
.debug_bounds("event-press left at 20, 30")
.expect("a press the child stops");
let released = cx
.debug_bounds("event-release left at 20, 30")
.expect("its release");
assert!(released.top() < pressed.top(), "newest first");
cx.simulate_event(ScrollWheelEvent {
position: point(px(30.0), px(40.0)),
delta: ScrollDelta::Pixels(point(px(0.0), px(-24.0))),
@@ -177,6 +257,28 @@ fn the_logger_lists_events(cx: &mut TestAppContext) {
assert!(shown("event-key up a", cx));
cx.simulate_modifiers_change(Modifiers::shift());
assert!(shown("event-modifiers shift", cx));
let (field, root) = view.read_with(cx, |logged, cx| {
(logged.field.focus_handle(cx), logged.root.clone())
});
cx.update(|window, _| window.focus(&field));
for key in ["b", "c", "backspace", "left", "enter"] {
press(key, cx);
}
let text = view.read_with(cx, |logged, cx| logged.field.read(cx).text().to_string());
assert_eq!(text, "b", "the field took Backspace");
for row in [
"event-key down backspace",
"event-key down left",
"event-key down enter",
] {
assert!(shown(row, cx), "{row}: a key the field's bindings take");
}
cx.update(|window, _| window.focus(&root));
press("z", cx);
assert!(
!shown("event-key down z", cx),
"a key while focus is outside"
);
cx.simulate_click(point(px(30.0), px(410.0)), Modifiers::none());
assert!(
!shown("event-press left at 20, 400", cx),