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:
1 parent
5f951ea581
commit
517d23f78f
6 files changed
+182
-40
No files matched your search
+27
-12
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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),
|
||||
|
||||
Reference in new issue
Block a user