Tooling · Inspection: inspector, FPS meter, render counter and event logger
install_inspector draws gpui's debug inspector in Ely's look: a panel with the picked box's source, ids and box model, and its margin, border, padding and size drawn over the page through raise, since the panel scrolls. FpsMeter counts frames on the executor's clock and colors each by the frames it missed. RenderCounter counts the renders of the view that draws it. EventLogger lists the input that reaches what it holds, its canvas laid first so a child's stop hides nothing.
This commit is contained in:
1 parent
274089a8f6
commit
462e47b854
14 files changed
+1378
-7
No files matched your search
@@ -0,0 +1,286 @@
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
fmt,
|
||||
rc::Rc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{
|
||||
layout::on_axis,
|
||||
theme::{ActiveTheme, TextSize},
|
||||
typography::{Ellipsis, tabular},
|
||||
};
|
||||
|
||||
/// Events a logger keeps.
|
||||
const KEPT: usize = 50;
|
||||
|
||||
/// An input event and what it carried; pointer places are in the logger's box.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Seen {
|
||||
Press(MouseButton, usize, Point<Pixels>),
|
||||
Release(MouseButton, Point<Pixels>),
|
||||
/// Where the pointer is, and how many moves ran together to get there.
|
||||
Move(Point<Pixels>, usize),
|
||||
Wheel(ScrollDelta),
|
||||
KeyDown(String, bool),
|
||||
KeyUp(String),
|
||||
Modifiers(Modifiers),
|
||||
}
|
||||
|
||||
fn at(point: Point<Pixels>) -> String {
|
||||
format!("{:.0}, {:.0}", f32::from(point.x), f32::from(point.y))
|
||||
}
|
||||
|
||||
fn button(button: MouseButton) -> String {
|
||||
format!("{button:?}").to_lowercase()
|
||||
}
|
||||
|
||||
impl fmt::Display for Seen {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Seen::Press(which, 1, place) => write!(f, "press {} at {}", button(*which), at(*place)),
|
||||
Seen::Press(which, clicks, place) => {
|
||||
write!(f, "press {} ×{clicks} at {}", button(*which), at(*place))
|
||||
}
|
||||
Seen::Release(which, place) => {
|
||||
write!(f, "release {} at {}", button(*which), at(*place))
|
||||
}
|
||||
Seen::Move(place, 1) => write!(f, "move to {}", at(*place)),
|
||||
Seen::Move(place, moves) => write!(f, "move to {} ×{moves}", at(*place)),
|
||||
Seen::Wheel(ScrollDelta::Pixels(delta)) => write!(f, "wheel {} px", at(*delta)),
|
||||
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::KeyUp(key) => write!(f, "key up {key}"),
|
||||
Seen::Modifiers(held) => {
|
||||
let names = [
|
||||
(held.control, "ctrl"),
|
||||
(held.alt, "alt"),
|
||||
(held.shift, "shift"),
|
||||
(held.platform, "cmd"),
|
||||
(held.function, "fn"),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(on, name)| on.then_some(name))
|
||||
.collect::<Vec<_>>();
|
||||
match names.is_empty() {
|
||||
true => write!(f, "modifiers none"),
|
||||
false => write!(f, "modifiers {}", names.join(" ")),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The events seen, oldest first, each timed from the first.
|
||||
#[derive(Default)]
|
||||
struct Log {
|
||||
start: Option<Instant>,
|
||||
entries: VecDeque<(Duration, Seen)>,
|
||||
}
|
||||
|
||||
impl Log {
|
||||
/// Adds an event; a move after a move joins it.
|
||||
fn push(&mut self, now: Instant, seen: Seen) {
|
||||
let start = *self.start.get_or_insert(now);
|
||||
let when = now.duration_since(start);
|
||||
if let (Seen::Move(place, _), Some((last, Seen::Move(was, moves)))) =
|
||||
(&seen, self.entries.back_mut())
|
||||
{
|
||||
(*last, *was, *moves) = (when, *place, *moves + 1);
|
||||
return;
|
||||
}
|
||||
self.entries.push_back((when, seen));
|
||||
if self.entries.len() > KEPT {
|
||||
self.entries.pop_front();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
#[derive(IntoElement)]
|
||||
pub struct EventLogger {
|
||||
id: ElementId,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
||||
impl EventLogger {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for EventLogger {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for EventLogger {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let id = self.id;
|
||||
let log = window.use_keyed_state((id.clone(), "log"), cx, |_, _| Log::default());
|
||||
let entries: Vec<(Duration, Seen)> = log.read(cx).entries.iter().rev().cloned().collect();
|
||||
let record: Record = Rc::new({
|
||||
let id = id.clone();
|
||||
move |seen, cx| {
|
||||
if !matches!(seen, Seen::Move(..)) {
|
||||
log::debug!("event logger {id}: {seen}");
|
||||
}
|
||||
let now = cx.background_executor().now();
|
||||
log.update(cx, |log, cx| {
|
||||
log.push(now, seen);
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
});
|
||||
let theme = cx.theme();
|
||||
let colors = &theme.colors;
|
||||
let (down, up, held) = (record.clone(), record.clone(), record.clone());
|
||||
let watched = div()
|
||||
.relative()
|
||||
.capture_key_down(move |event, _, cx| {
|
||||
down(Seen::KeyDown(event.keystroke.unparse(), event.is_held), cx)
|
||||
})
|
||||
.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.
|
||||
.child(
|
||||
canvas(
|
||||
|_, _, _| {},
|
||||
move |bounds, _, window, _| {
|
||||
let inside = move |place: Point<Pixels>, phase: DispatchPhase| {
|
||||
(phase == DispatchPhase::Capture && bounds.contains(&place))
|
||||
.then(|| place - bounds.origin)
|
||||
};
|
||||
let pressed = record.clone();
|
||||
window.on_mouse_event(move |event: &MouseDownEvent, phase, _, cx| {
|
||||
if let Some(place) = inside(event.position, phase) {
|
||||
pressed(Seen::Press(event.button, event.click_count, place), cx)
|
||||
}
|
||||
});
|
||||
let released = record.clone();
|
||||
window.on_mouse_event(move |event: &MouseUpEvent, phase, _, cx| {
|
||||
if let Some(place) = inside(event.position, phase) {
|
||||
released(Seen::Release(event.button, place), cx)
|
||||
}
|
||||
});
|
||||
let moved = record.clone();
|
||||
window.on_mouse_event(move |event: &MouseMoveEvent, phase, _, cx| {
|
||||
if let Some(place) = inside(event.position, phase) {
|
||||
moved(Seen::Move(place, 1), cx)
|
||||
}
|
||||
});
|
||||
let wheeled = record.clone();
|
||||
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, _, cx| {
|
||||
if inside(event.position, phase).is_some() {
|
||||
wheeled(Seen::Wheel(event.delta), cx)
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
.absolute()
|
||||
.inset_0(),
|
||||
)
|
||||
.children(self.children);
|
||||
let rows = entries.into_iter().map(|(when, seen)| {
|
||||
let text = seen.to_string();
|
||||
div()
|
||||
.debug_selector({
|
||||
let text = text.clone();
|
||||
move || format!("event-{text}")
|
||||
})
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_3()
|
||||
.child(
|
||||
tabular(div())
|
||||
.flex_none()
|
||||
.text_color(colors.fg_muted)
|
||||
.child(format!("+{:.2} s", when.as_secs_f64())),
|
||||
)
|
||||
.child(div().flex_1().min_w_0().child(Ellipsis::new(text)))
|
||||
});
|
||||
let empty = rows.len() == 0;
|
||||
div().flex().flex_col().gap_3().child(watched).child(
|
||||
on_axis(
|
||||
div()
|
||||
.id((id, "log"))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_1()
|
||||
.max_h(theme.tooling().log)
|
||||
.overflow_y_scroll()
|
||||
.text_size(theme.text_size(TextSize::Xs)),
|
||||
)
|
||||
.when(empty, |log| {
|
||||
log.child(
|
||||
div()
|
||||
.debug_selector(|| "event-logger-empty".into())
|
||||
.text_color(colors.fg_muted)
|
||||
.child("Events inside the box show here."),
|
||||
)
|
||||
})
|
||||
.children(rows),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{Modifiers, MouseButton, point, px};
|
||||
|
||||
use super::{KEPT, Log, Seen};
|
||||
|
||||
#[test]
|
||||
fn moves_run_together_and_the_log_keeps_fifty() {
|
||||
let (start, mut log) = (Instant::now(), Log::default());
|
||||
log.push(start, Seen::Move(point(px(1.0), px(2.0)), 1));
|
||||
log.push(
|
||||
start + Duration::from_millis(250),
|
||||
Seen::Move(point(px(5.0), px(6.0)), 1),
|
||||
);
|
||||
let (when, seen) = log.entries.back().expect("an entry");
|
||||
assert_eq!(log.entries.len(), 1);
|
||||
assert_eq!(*when, Duration::from_millis(250));
|
||||
assert_eq!(seen.to_string(), "move to 5, 6 ×2");
|
||||
for _ in 0..KEPT {
|
||||
log.push(
|
||||
start,
|
||||
Seen::Press(MouseButton::Left, 2, point(px(0.0), px(0.0))),
|
||||
);
|
||||
}
|
||||
assert_eq!(log.entries.len(), KEPT);
|
||||
assert_eq!(
|
||||
log.entries.front().expect("an entry").1.to_string(),
|
||||
"press left ×2 at 0, 0"
|
||||
);
|
||||
let held = Modifiers {
|
||||
shift: true,
|
||||
platform: true,
|
||||
..Modifiers::default()
|
||||
};
|
||||
assert_eq!(Seen::Modifiers(held).to_string(), "modifiers shift cmd");
|
||||
assert_eq!(
|
||||
Seen::Modifiers(Modifiers::default()).to_string(),
|
||||
"modifiers none"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user