mirror of
https://github.com/emilk/egui.git
synced 2026-09-03 07:10:04 -04:00
Show source code location in inspector
This commit is contained in:
@@ -1392,6 +1392,7 @@ dependencies = [
|
|||||||
name = "egui_kittest"
|
name = "egui_kittest"
|
||||||
version = "0.34.1"
|
version = "0.34.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
"dify",
|
"dify",
|
||||||
"document-features",
|
"document-features",
|
||||||
"eframe",
|
"eframe",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ snapshot = ["dep:dify", "dep:image", "dep:open", "dep:tempfile", "image/png"]
|
|||||||
recording = ["dep:image", "image/gif", "image/png"]
|
recording = ["dep:image", "image/gif", "image/png"]
|
||||||
|
|
||||||
## Stream frames + accesskit tree to a `kittest_inspector` window for live debugging.
|
## Stream frames + accesskit tree to a `kittest_inspector` window for live debugging.
|
||||||
inspector = ["dep:image", "dep:kittest_inspector"]
|
inspector = ["dep:image", "dep:kittest_inspector", "dep:backtrace"]
|
||||||
|
|
||||||
## Allows testing eframe::App
|
## Allows testing eframe::App
|
||||||
eframe = ["dep:eframe", "eframe/accesskit"]
|
eframe = ["dep:eframe", "eframe/accesskit"]
|
||||||
@@ -58,6 +58,7 @@ dify = { workspace = true, optional = true }
|
|||||||
|
|
||||||
# inspector dependencies
|
# inspector dependencies
|
||||||
kittest_inspector = { workspace = true, default-features = false, optional = true }
|
kittest_inspector = { workspace = true, default-features = false, optional = true }
|
||||||
|
backtrace = { workspace = true, optional = true }
|
||||||
|
|
||||||
# Enable this when generating docs.
|
# Enable this when generating docs.
|
||||||
document-features = { workspace = true, optional = true }
|
document-features = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -4,15 +4,20 @@
|
|||||||
//! step the harness writes a frame + accesskit tree update to the child's stdin and reads a
|
//! step the harness writes a frame + accesskit tree update to the child's stdin and reads a
|
||||||
//! reply from its stdout, blocking until the user resumes (when paused).
|
//! reply from its stdout, blocking until the user resumes (when paused).
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::io::{BufReader, BufWriter, Write as _};
|
use std::io::{BufReader, BufWriter, Write as _};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
|
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use egui::accesskit;
|
use egui::accesskit;
|
||||||
|
use egui::mutex::Mutex;
|
||||||
use kittest_inspector::{
|
use kittest_inspector::{
|
||||||
Frame, HarnessMessage, InspectorReply, read_message, write_message,
|
Frame, HarnessMessage, InspectorReply, SourceView, read_message, write_message,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::node::EventSite;
|
||||||
|
|
||||||
/// Environment variable: when set to a truthy value, every harness auto-launches an inspector.
|
/// Environment variable: when set to a truthy value, every harness auto-launches an inspector.
|
||||||
pub const INSPECTOR_ENV_VAR: &str = "KITTEST_INSPECTOR";
|
pub const INSPECTOR_ENV_VAR: &str = "KITTEST_INSPECTOR";
|
||||||
|
|
||||||
@@ -98,6 +103,8 @@ impl Inspector {
|
|||||||
image: &image::RgbaImage,
|
image: &image::RgbaImage,
|
||||||
pixels_per_point: f32,
|
pixels_per_point: f32,
|
||||||
accesskit: Option<accesskit::TreeUpdate>,
|
accesskit: Option<accesskit::TreeUpdate>,
|
||||||
|
call_site: &EventSite,
|
||||||
|
event_sites: &[EventSite],
|
||||||
) -> Vec<egui::Event> {
|
) -> Vec<egui::Event> {
|
||||||
if self.broken {
|
if self.broken {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -111,8 +118,11 @@ impl Inspector {
|
|||||||
rgba: image.as_raw().clone(),
|
rgba: image.as_raw().clone(),
|
||||||
accesskit,
|
accesskit,
|
||||||
label: self.label.clone(),
|
label: self.label.clone(),
|
||||||
|
source: build_source_view(call_site, event_sites),
|
||||||
};
|
};
|
||||||
if let Err(err) = write_message(&mut self.writer, &HarnessMessage::Frame(frame)) {
|
if let Err(err) =
|
||||||
|
write_message(&mut self.writer, &HarnessMessage::Frame(Box::new(frame)))
|
||||||
|
{
|
||||||
#[expect(clippy::print_stderr)]
|
#[expect(clippy::print_stderr)]
|
||||||
{
|
{
|
||||||
eprintln!("egui_kittest inspector: send failed: {err}");
|
eprintln!("egui_kittest inspector: send failed: {err}");
|
||||||
@@ -148,6 +158,118 @@ impl Drop for Inspector {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the [`SourceView`] payload for a frame: find the topmost test-source file common to
|
||||||
|
/// the runner call (`call_site`) and all consumed events, then read that file once and record
|
||||||
|
/// each event's line inside it.
|
||||||
|
fn build_source_view(call_site: &EventSite, event_sites: &[EventSite]) -> Option<SourceView> {
|
||||||
|
let call_frames = call_site.as_deref().map(user_frames);
|
||||||
|
let event_frames: Vec<Vec<UserFrame>> = event_sites
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.as_deref().map(user_frames).unwrap_or_default())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Build the list of candidate-source frame vecs. The call site is required — without it
|
||||||
|
// there's nothing anchoring the "frame producer".
|
||||||
|
let call_frames = call_frames?;
|
||||||
|
if call_frames.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick the topmost file (latest in outer-most order) that appears in every non-empty
|
||||||
|
// event stack as well as the call-site stack. Ignore completely-empty event stacks
|
||||||
|
// (e.g. events driven by the inspector itself).
|
||||||
|
let non_empty_events: Vec<&Vec<UserFrame>> =
|
||||||
|
event_frames.iter().filter(|v| !v.is_empty()).collect();
|
||||||
|
let path = pick_common_file(&call_frames, &non_empty_events)?;
|
||||||
|
|
||||||
|
let call_site_line = innermost_line_for(&call_frames, &path);
|
||||||
|
let event_lines: Vec<u32> = event_frames
|
||||||
|
.iter()
|
||||||
|
.filter_map(|frames| innermost_line_for(frames, &path))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Some(SourceView {
|
||||||
|
path: path.clone(),
|
||||||
|
contents: read_source_file(&path),
|
||||||
|
call_site_line,
|
||||||
|
event_lines,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One resolved user-code frame (file + line).
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct UserFrame {
|
||||||
|
file: String,
|
||||||
|
line: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a backtrace and return its user-code frames, innermost first.
|
||||||
|
fn user_frames(bt: &backtrace::Backtrace) -> Vec<UserFrame> {
|
||||||
|
let mut bt = bt.clone();
|
||||||
|
bt.resolve();
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for frame in bt.frames() {
|
||||||
|
for symbol in frame.symbols() {
|
||||||
|
let Some(path) = symbol.filename() else { continue };
|
||||||
|
let Some(line) = symbol.lineno() else { continue };
|
||||||
|
let path = path.to_string_lossy().into_owned();
|
||||||
|
if !is_user_code(&path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(UserFrame { file: path, line });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A frame's file is "user code" if it isn't inside the Rust toolchain, a cargo registry
|
||||||
|
/// dependency, or the `egui_kittest` / `kittest_inspector` crates themselves. This keeps the
|
||||||
|
/// common-file search honest: we skip past the harness's own plumbing.
|
||||||
|
fn is_user_code(path: &str) -> bool {
|
||||||
|
const EXCLUDE: &[&str] = &[
|
||||||
|
"/rustc/",
|
||||||
|
"/toolchains/",
|
||||||
|
"/.cargo/registry/",
|
||||||
|
"/.cargo/git/",
|
||||||
|
"egui_kittest/src/",
|
||||||
|
"kittest_inspector/src/",
|
||||||
|
];
|
||||||
|
!EXCLUDE.iter().any(|needle| path.contains(needle))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Among files common to `call_frames` and every stack in `event_frames`, pick the one that
|
||||||
|
/// is **outermost** (furthest from the event origin) in the call-site stack. Intuition: the
|
||||||
|
/// outermost common file is the test function itself; inner ones are helpers.
|
||||||
|
fn pick_common_file(call_frames: &[UserFrame], event_frames: &[&Vec<UserFrame>]) -> Option<String> {
|
||||||
|
// Walk the call-site stack outermost-first.
|
||||||
|
for frame in call_frames.iter().rev() {
|
||||||
|
if event_frames
|
||||||
|
.iter()
|
||||||
|
.all(|frames| frames.iter().any(|f| f.file == frame.file))
|
||||||
|
{
|
||||||
|
return Some(frame.file.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the line number of the innermost frame in `frames` whose file matches `path`.
|
||||||
|
fn innermost_line_for(frames: &[UserFrame], path: &str) -> Option<u32> {
|
||||||
|
frames.iter().find(|f| f.file == path).map(|f| f.line)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read the full contents of a source file, cached per path (including negative results).
|
||||||
|
fn read_source_file(path: &str) -> Option<String> {
|
||||||
|
static CACHE: LazyLock<Mutex<HashMap<String, Option<String>>>> =
|
||||||
|
LazyLock::new(|| Mutex::new(HashMap::new()));
|
||||||
|
let mut cache = CACHE.lock();
|
||||||
|
cache
|
||||||
|
.entry(path.to_owned())
|
||||||
|
.or_insert_with(|| std::fs::read_to_string(path).ok())
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Read [`INSPECTOR_ENV_VAR`] once and cache.
|
/// Read [`INSPECTOR_ENV_VAR`] once and cache.
|
||||||
pub(crate) fn env_enabled() -> bool {
|
pub(crate) fn env_enabled() -> bool {
|
||||||
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||||
|
|||||||
@@ -105,6 +105,13 @@ pub struct Harness<'a, State = ()> {
|
|||||||
inspector: Option<inspector::Inspector>,
|
inspector: Option<inspector::Inspector>,
|
||||||
#[cfg(feature = "inspector")]
|
#[cfg(feature = "inspector")]
|
||||||
last_accesskit_update: Option<egui::accesskit::TreeUpdate>,
|
last_accesskit_update: Option<egui::accesskit::TreeUpdate>,
|
||||||
|
/// Backtrace captured at the most recent public runner call (e.g. `.run()` / `.step()`).
|
||||||
|
/// Used to find the topmost common test-source file across the call and its events.
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
current_call_site: node::EventSite,
|
||||||
|
/// Backtraces of events consumed in the step that produced the current frame.
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
consumed_event_sites: Vec<node::EventSite>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<State> Debug for Harness<'_, State> {
|
impl<State> Debug for Harness<'_, State> {
|
||||||
@@ -200,6 +207,10 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
inspector: None,
|
inspector: None,
|
||||||
#[cfg(feature = "inspector")]
|
#[cfg(feature = "inspector")]
|
||||||
last_accesskit_update: None,
|
last_accesskit_update: None,
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
current_call_site: node::empty_site(),
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
consumed_event_sites: Vec::new(),
|
||||||
};
|
};
|
||||||
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
|
// Run the harness until it is stable, ensuring that all Areas are shown and animations are done
|
||||||
harness.run_ok();
|
harness.run_ok();
|
||||||
@@ -296,17 +307,30 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// Run a frame for each queued event (or a single frame if there are no events).
|
/// Run a frame for each queued event (or a single frame if there are no events).
|
||||||
/// This will call the app closure with each queued event and
|
/// This will call the app closure with each queued event and
|
||||||
/// update the Harness.
|
/// update the Harness.
|
||||||
|
#[track_caller]
|
||||||
pub fn step(&mut self) {
|
pub fn step(&mut self) {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
{
|
||||||
|
self.current_call_site = node::capture_site();
|
||||||
|
}
|
||||||
let events = std::mem::take(&mut *self.queued_events.lock());
|
let events = std::mem::take(&mut *self.queued_events.lock());
|
||||||
if events.is_empty() {
|
if events.is_empty() {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
self.consumed_event_sites.clear();
|
||||||
self._step(false);
|
self._step(false);
|
||||||
}
|
}
|
||||||
for event in events {
|
for event in events {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
self.consumed_event_sites.clear();
|
||||||
match event {
|
match event {
|
||||||
EventType::Event(event) => {
|
EventType::Event(event, _site) => {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
self.consumed_event_sites.push(_site);
|
||||||
self.input.events.push(event);
|
self.input.events.push(event);
|
||||||
}
|
}
|
||||||
EventType::Modifiers(modifiers) => {
|
EventType::Modifiers(modifiers, _site) => {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
self.consumed_event_sites.push(_site);
|
||||||
self.input.modifiers = modifiers;
|
self.input.modifiers = modifiers;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -376,7 +400,12 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// Resize the test harness to fit the contents. This only works when creating the Harness via
|
/// Resize the test harness to fit the contents. This only works when creating the Harness via
|
||||||
/// [`Harness::new_ui`] / [`Harness::new_ui_state`] or
|
/// [`Harness::new_ui`] / [`Harness::new_ui_state`] or
|
||||||
/// [`HarnessBuilder::build_ui`] / [`HarnessBuilder::build_ui_state`].
|
/// [`HarnessBuilder::build_ui`] / [`HarnessBuilder::build_ui_state`].
|
||||||
|
#[track_caller]
|
||||||
pub fn fit_contents(&mut self) {
|
pub fn fit_contents(&mut self) {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
{
|
||||||
|
self.current_call_site = node::capture_site();
|
||||||
|
}
|
||||||
self._step(true);
|
self._step(true);
|
||||||
|
|
||||||
// Calculate size including all content (main UI + popups + tooltips)
|
// Calculate size including all content (main UI + popups + tooltips)
|
||||||
@@ -405,6 +434,10 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// - [`Harness::run_steps`].
|
/// - [`Harness::run_steps`].
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn run(&mut self) -> u64 {
|
pub fn run(&mut self) -> u64 {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
{
|
||||||
|
self.current_call_site = node::capture_site();
|
||||||
|
}
|
||||||
match self.try_run() {
|
match self.try_run() {
|
||||||
Ok(steps) => steps,
|
Ok(steps) => steps,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@@ -457,7 +490,12 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// - [`Harness::step`].
|
/// - [`Harness::step`].
|
||||||
/// - [`Harness::run_steps`].
|
/// - [`Harness::run_steps`].
|
||||||
/// - [`Harness::try_run_realtime`].
|
/// - [`Harness::try_run_realtime`].
|
||||||
|
#[track_caller]
|
||||||
pub fn try_run(&mut self) -> Result<u64, ExceededMaxStepsError> {
|
pub fn try_run(&mut self) -> Result<u64, ExceededMaxStepsError> {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
{
|
||||||
|
self.current_call_site = node::capture_site();
|
||||||
|
}
|
||||||
self._try_run(false)
|
self._try_run(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,7 +512,12 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// - [`Harness::step`].
|
/// - [`Harness::step`].
|
||||||
/// - [`Harness::run_steps`].
|
/// - [`Harness::run_steps`].
|
||||||
/// - [`Harness::try_run_realtime`].
|
/// - [`Harness::try_run_realtime`].
|
||||||
|
#[track_caller]
|
||||||
pub fn run_ok(&mut self) -> Option<u64> {
|
pub fn run_ok(&mut self) -> Option<u64> {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
{
|
||||||
|
self.current_call_site = node::capture_site();
|
||||||
|
}
|
||||||
self.try_run().ok()
|
self.try_run().ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,13 +540,23 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// - [`Harness::step`].
|
/// - [`Harness::step`].
|
||||||
/// - [`Harness::run_steps`].
|
/// - [`Harness::run_steps`].
|
||||||
/// - [`Harness::try_run`].
|
/// - [`Harness::try_run`].
|
||||||
|
#[track_caller]
|
||||||
pub fn try_run_realtime(&mut self) -> Result<u64, ExceededMaxStepsError> {
|
pub fn try_run_realtime(&mut self) -> Result<u64, ExceededMaxStepsError> {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
{
|
||||||
|
self.current_call_site = node::capture_site();
|
||||||
|
}
|
||||||
self._try_run(true)
|
self._try_run(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run a number of steps.
|
/// Run a number of steps.
|
||||||
/// Equivalent to calling [`Harness::step`] x times.
|
/// Equivalent to calling [`Harness::step`] x times.
|
||||||
|
#[track_caller]
|
||||||
pub fn run_steps(&mut self, steps: usize) {
|
pub fn run_steps(&mut self, steps: usize) {
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
{
|
||||||
|
self.current_call_site = node::capture_site();
|
||||||
|
}
|
||||||
for _ in 0..steps {
|
for _ in 0..steps {
|
||||||
self.step();
|
self.step();
|
||||||
}
|
}
|
||||||
@@ -541,7 +594,9 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
|
|
||||||
/// Queue an event to be processed in the next frame.
|
/// Queue an event to be processed in the next frame.
|
||||||
pub fn event(&self, event: egui::Event) {
|
pub fn event(&self, event: egui::Event) {
|
||||||
self.queued_events.lock().push(EventType::Event(event));
|
self.queued_events
|
||||||
|
.lock()
|
||||||
|
.push(EventType::Event(event, node::capture_site()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue an event with modifiers.
|
/// Queue an event with modifiers.
|
||||||
@@ -549,17 +604,18 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// Queues the modifiers to be pressed, then the event, then the modifiers to be released.
|
/// Queues the modifiers to be pressed, then the event, then the modifiers to be released.
|
||||||
pub fn event_modifiers(&self, event: egui::Event, modifiers: Modifiers) {
|
pub fn event_modifiers(&self, event: egui::Event, modifiers: Modifiers) {
|
||||||
let mut queue = self.queued_events.lock();
|
let mut queue = self.queued_events.lock();
|
||||||
queue.push(EventType::Modifiers(modifiers));
|
queue.push(EventType::Modifiers(modifiers, node::capture_site()));
|
||||||
queue.push(EventType::Event(event));
|
queue.push(EventType::Event(event, node::capture_site()));
|
||||||
queue.push(EventType::Modifiers(Modifiers::default()));
|
queue.push(EventType::Modifiers(Modifiers::default(), node::capture_site()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modifiers(&self, modifiers: Modifiers) {
|
fn modifiers(&self, modifiers: Modifiers) {
|
||||||
self.queued_events
|
self.queued_events
|
||||||
.lock()
|
.lock()
|
||||||
.push(EventType::Modifiers(modifiers));
|
.push(EventType::Modifiers(modifiers, node::capture_site()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn key_down(&self, key: egui::Key) {
|
pub fn key_down(&self, key: egui::Key) {
|
||||||
self.event(egui::Event::Key {
|
self.event(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
@@ -570,6 +626,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn key_down_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
|
pub fn key_down_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
|
||||||
self.event_modifiers(
|
self.event_modifiers(
|
||||||
egui::Event::Key {
|
egui::Event::Key {
|
||||||
@@ -583,6 +640,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn key_up(&self, key: egui::Key) {
|
pub fn key_up(&self, key: egui::Key) {
|
||||||
self.event(egui::Event::Key {
|
self.event(egui::Event::Key {
|
||||||
key,
|
key,
|
||||||
@@ -593,6 +651,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn key_up_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
|
pub fn key_up_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
|
||||||
self.event_modifiers(
|
self.event_modifiers(
|
||||||
egui::Event::Key {
|
egui::Event::Key {
|
||||||
@@ -613,6 +672,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// - Press [`Key::B`]
|
/// - Press [`Key::B`]
|
||||||
/// - Release [`Key::B`]
|
/// - Release [`Key::B`]
|
||||||
/// - Release [`Key::A`]
|
/// - Release [`Key::A`]
|
||||||
|
#[track_caller]
|
||||||
pub fn key_combination(&self, keys: &[Key]) {
|
pub fn key_combination(&self, keys: &[Key]) {
|
||||||
for key in keys {
|
for key in keys {
|
||||||
self.key_down(*key);
|
self.key_down(*key);
|
||||||
@@ -631,6 +691,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// - Release [`Key::B`]
|
/// - Release [`Key::B`]
|
||||||
/// - Release [`Key::A`]
|
/// - Release [`Key::A`]
|
||||||
/// - Release [`Modifiers::COMMAND`]
|
/// - Release [`Modifiers::COMMAND`]
|
||||||
|
#[track_caller]
|
||||||
pub fn key_combination_modifiers(&self, modifiers: Modifiers, keys: &[Key]) {
|
pub fn key_combination_modifiers(&self, modifiers: Modifiers, keys: &[Key]) {
|
||||||
self.modifiers(modifiers);
|
self.modifiers(modifiers);
|
||||||
|
|
||||||
@@ -652,6 +713,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// Press a key.
|
/// Press a key.
|
||||||
///
|
///
|
||||||
/// This will create a key down event and a key up event.
|
/// This will create a key down event and a key up event.
|
||||||
|
#[track_caller]
|
||||||
pub fn key_press(&self, key: egui::Key) {
|
pub fn key_press(&self, key: egui::Key) {
|
||||||
self.key_combination(&[key]);
|
self.key_combination(&[key]);
|
||||||
}
|
}
|
||||||
@@ -663,16 +725,19 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// - create a key down event
|
/// - create a key down event
|
||||||
/// - create a key up event
|
/// - create a key up event
|
||||||
/// - reset the modifiers
|
/// - reset the modifiers
|
||||||
|
#[track_caller]
|
||||||
pub fn key_press_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
|
pub fn key_press_modifiers(&self, modifiers: Modifiers, key: egui::Key) {
|
||||||
self.key_combination_modifiers(modifiers, &[key]);
|
self.key_combination_modifiers(modifiers, &[key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Move mouse cursor to this position.
|
/// Move mouse cursor to this position.
|
||||||
|
#[track_caller]
|
||||||
pub fn hover_at(&self, pos: egui::Pos2) {
|
pub fn hover_at(&self, pos: egui::Pos2) {
|
||||||
self.event(egui::Event::PointerMoved(pos));
|
self.event(egui::Event::PointerMoved(pos));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start dragging from a position.
|
/// Start dragging from a position.
|
||||||
|
#[track_caller]
|
||||||
pub fn drag_at(&self, pos: egui::Pos2) {
|
pub fn drag_at(&self, pos: egui::Pos2) {
|
||||||
self.event(egui::Event::PointerButton {
|
self.event(egui::Event::PointerButton {
|
||||||
pos,
|
pos,
|
||||||
@@ -683,6 +748,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Stop dragging and remove cursor.
|
/// Stop dragging and remove cursor.
|
||||||
|
#[track_caller]
|
||||||
pub fn drop_at(&self, pos: egui::Pos2) {
|
pub fn drop_at(&self, pos: egui::Pos2) {
|
||||||
self.event(egui::Event::PointerButton {
|
self.event(egui::Event::PointerButton {
|
||||||
pos,
|
pos,
|
||||||
@@ -699,6 +765,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
///
|
///
|
||||||
/// If you click a button and then take a snapshot, the button will be shown as hovered.
|
/// If you click a button and then take a snapshot, the button will be shown as hovered.
|
||||||
/// If you don't want that, you can call this method after clicking.
|
/// If you don't want that, you can call this method after clicking.
|
||||||
|
#[track_caller]
|
||||||
pub fn remove_cursor(&self) {
|
pub fn remove_cursor(&self) {
|
||||||
self.event(egui::Event::PointerGone);
|
self.event(egui::Event::PointerGone);
|
||||||
}
|
}
|
||||||
@@ -848,8 +915,10 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
};
|
};
|
||||||
let tree = self.last_accesskit_update.clone();
|
let tree = self.last_accesskit_update.clone();
|
||||||
let ppp = self.ctx.pixels_per_point();
|
let ppp = self.ctx.pixels_per_point();
|
||||||
|
let call_site = self.current_call_site.clone();
|
||||||
|
let event_sites: Vec<_> = self.consumed_event_sites.clone();
|
||||||
let events = if let Some(inspector) = self.inspector.as_mut() {
|
let events = if let Some(inspector) = self.inspector.as_mut() {
|
||||||
inspector.send_step(&image, ppp, tree)
|
inspector.send_step(&image, ppp, tree, &call_site, &event_sites)
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -860,6 +929,8 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
for event in events {
|
for event in events {
|
||||||
self.input.events.push(event);
|
self.input.events.push(event);
|
||||||
}
|
}
|
||||||
|
// Events driven by the inspector itself don't have a test-source location.
|
||||||
|
self.consumed_event_sites.clear();
|
||||||
self._step_inner(false);
|
self._step_inner(false);
|
||||||
#[cfg(feature = "recording")]
|
#[cfg(feature = "recording")]
|
||||||
self.capture_frame_if_recording(false);
|
self.capture_frame_if_recording(false);
|
||||||
|
|||||||
@@ -4,9 +4,36 @@ use egui::{Modifiers, PointerButton, Pos2, accesskit};
|
|||||||
use kittest::{AccessKitNode, NodeT, debug_fmt_node};
|
use kittest::{AccessKitNode, NodeT, debug_fmt_node};
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
|
/// Source-location info stashed alongside queued events. We store a runtime backtrace so the
|
||||||
|
/// inspector can walk *past* non-`#[track_caller]` helper functions and find the common
|
||||||
|
/// test-source file that all events came from. Zero-cost when the `inspector` feature is off.
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
pub(crate) type EventSite = Option<Box<backtrace::Backtrace>>;
|
||||||
|
#[cfg(not(feature = "inspector"))]
|
||||||
|
pub(crate) type EventSite = ();
|
||||||
|
|
||||||
|
/// Capture a backtrace at the call site. Unresolved so capture is cheap (~microseconds);
|
||||||
|
/// resolution happens lazily when we actually ship the frame to the inspector.
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
#[expect(clippy::unnecessary_wraps)] // Option<_> is the shape of EventSite by design.
|
||||||
|
pub(crate) fn capture_site() -> EventSite {
|
||||||
|
Some(Box::new(backtrace::Backtrace::new_unresolved()))
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "inspector"))]
|
||||||
|
pub(crate) fn capture_site() -> EventSite {}
|
||||||
|
|
||||||
|
/// The "empty" value for an [`EventSite`] — used as a default when no location has been
|
||||||
|
/// captured yet (e.g. `Harness` construction). Zero-cost when the feature is off.
|
||||||
|
#[cfg(feature = "inspector")]
|
||||||
|
pub(crate) fn empty_site() -> EventSite {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "inspector"))]
|
||||||
|
pub(crate) fn empty_site() -> EventSite {}
|
||||||
|
|
||||||
pub(crate) enum EventType {
|
pub(crate) enum EventType {
|
||||||
Event(egui::Event),
|
Event(egui::Event, EventSite),
|
||||||
Modifiers(Modifiers),
|
Modifiers(Modifiers, EventSite),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) type EventQueue = Mutex<Vec<EventType>>;
|
pub(crate) type EventQueue = Mutex<Vec<EventType>>;
|
||||||
@@ -38,26 +65,34 @@ impl<'tree> NodeT<'tree> for Node<'tree> {
|
|||||||
|
|
||||||
impl Node<'_> {
|
impl Node<'_> {
|
||||||
fn event(&self, event: egui::Event) {
|
fn event(&self, event: egui::Event) {
|
||||||
self.queue.lock().push(EventType::Event(event));
|
self.queue
|
||||||
|
.lock()
|
||||||
|
.push(EventType::Event(event, capture_site()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn modifiers(&self, modifiers: Modifiers) {
|
fn modifiers(&self, modifiers: Modifiers) {
|
||||||
self.queue.lock().push(EventType::Modifiers(modifiers));
|
self.queue
|
||||||
|
.lock()
|
||||||
|
.push(EventType::Modifiers(modifiers, capture_site()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn hover(&self) {
|
pub fn hover(&self) {
|
||||||
self.event(egui::Event::PointerMoved(self.rect().center()));
|
self.event(egui::Event::PointerMoved(self.rect().center()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Click at the node center with the primary button.
|
/// Click at the node center with the primary button.
|
||||||
|
#[track_caller]
|
||||||
pub fn click(&self) {
|
pub fn click(&self) {
|
||||||
self.click_button(PointerButton::Primary);
|
self.click_button(PointerButton::Primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn click_secondary(&self) {
|
pub fn click_secondary(&self) {
|
||||||
self.click_button(PointerButton::Secondary);
|
self.click_button(PointerButton::Secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn click_button(&self, button: PointerButton) {
|
pub fn click_button(&self, button: PointerButton) {
|
||||||
self.hover();
|
self.hover();
|
||||||
for pressed in [true, false] {
|
for pressed in [true, false] {
|
||||||
@@ -70,10 +105,12 @@ impl Node<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn click_modifiers(&self, modifiers: Modifiers) {
|
pub fn click_modifiers(&self, modifiers: Modifiers) {
|
||||||
self.click_button_modifiers(PointerButton::Primary, modifiers);
|
self.click_button_modifiers(PointerButton::Primary, modifiers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn click_button_modifiers(&self, button: PointerButton, modifiers: Modifiers) {
|
pub fn click_button_modifiers(&self, button: PointerButton, modifiers: Modifiers) {
|
||||||
self.hover();
|
self.hover();
|
||||||
self.modifiers(modifiers);
|
self.modifiers(modifiers);
|
||||||
@@ -92,6 +129,7 @@ impl Node<'_> {
|
|||||||
///
|
///
|
||||||
/// This will trigger a [`accesskit::Action::Click`] action.
|
/// This will trigger a [`accesskit::Action::Click`] action.
|
||||||
/// In contrast to `click()`, this can also click widgets that are not currently visible.
|
/// In contrast to `click()`, this can also click widgets that are not currently visible.
|
||||||
|
#[track_caller]
|
||||||
pub fn click_accesskit(&self) {
|
pub fn click_accesskit(&self) {
|
||||||
let (target_node, target_tree) = self.accesskit_node.locate();
|
let (target_node, target_tree) = self.accesskit_node.locate();
|
||||||
self.event(egui::Event::AccessKitActionRequest(
|
self.event(egui::Event::AccessKitActionRequest(
|
||||||
@@ -115,6 +153,7 @@ impl Node<'_> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn focus(&self) {
|
pub fn focus(&self) {
|
||||||
let (target_node, target_tree) = self.accesskit_node.locate();
|
let (target_node, target_tree) = self.accesskit_node.locate();
|
||||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||||
@@ -125,6 +164,7 @@ impl Node<'_> {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
pub fn type_text(&self, text: &str) {
|
pub fn type_text(&self, text: &str) {
|
||||||
self.event(egui::Event::Text(text.to_owned()));
|
self.event(egui::Event::Text(text.to_owned()));
|
||||||
}
|
}
|
||||||
@@ -138,6 +178,7 @@ impl Node<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll the node into view.
|
/// Scroll the node into view.
|
||||||
|
#[track_caller]
|
||||||
pub fn scroll_to_me(&self) {
|
pub fn scroll_to_me(&self) {
|
||||||
let (target_node, target_tree) = self.accesskit_node.locate();
|
let (target_node, target_tree) = self.accesskit_node.locate();
|
||||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||||
@@ -149,6 +190,7 @@ impl Node<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll the [`egui::ScrollArea`] containing this node down (100px).
|
/// Scroll the [`egui::ScrollArea`] containing this node down (100px).
|
||||||
|
#[track_caller]
|
||||||
pub fn scroll_down(&self) {
|
pub fn scroll_down(&self) {
|
||||||
let (target_node, target_tree) = self.accesskit_node.locate();
|
let (target_node, target_tree) = self.accesskit_node.locate();
|
||||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||||
@@ -160,6 +202,7 @@ impl Node<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll the [`egui::ScrollArea`] containing this node up (100px).
|
/// Scroll the [`egui::ScrollArea`] containing this node up (100px).
|
||||||
|
#[track_caller]
|
||||||
pub fn scroll_up(&self) {
|
pub fn scroll_up(&self) {
|
||||||
let (target_node, target_tree) = self.accesskit_node.locate();
|
let (target_node, target_tree) = self.accesskit_node.locate();
|
||||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||||
@@ -171,6 +214,7 @@ impl Node<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll the [`egui::ScrollArea`] containing this node left (100px).
|
/// Scroll the [`egui::ScrollArea`] containing this node left (100px).
|
||||||
|
#[track_caller]
|
||||||
pub fn scroll_left(&self) {
|
pub fn scroll_left(&self) {
|
||||||
let (target_node, target_tree) = self.accesskit_node.locate();
|
let (target_node, target_tree) = self.accesskit_node.locate();
|
||||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||||
@@ -182,6 +226,7 @@ impl Node<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Scroll the [`egui::ScrollArea`] containing this node right (100px).
|
/// Scroll the [`egui::ScrollArea`] containing this node right (100px).
|
||||||
|
#[track_caller]
|
||||||
pub fn scroll_right(&self) {
|
pub fn scroll_right(&self) {
|
||||||
let (target_node, target_tree) = self.accesskit_node.locate();
|
let (target_node, target_tree) = self.accesskit_node.locate();
|
||||||
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
self.event(egui::Event::AccessKitActionRequest(ActionRequest {
|
||||||
|
|||||||
@@ -11,6 +11,24 @@
|
|||||||
|
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{self, Read, Write};
|
||||||
|
|
||||||
|
/// One source file plus the test-source lines the inspector should highlight inside it.
|
||||||
|
///
|
||||||
|
/// The harness walks each captured backtrace (for the `.run()` call that produced the frame
|
||||||
|
/// and each event consumed by it), finds the topmost common test-source file across all of
|
||||||
|
/// them, reads that file, and emits its contents here. Highlights are line numbers within
|
||||||
|
/// that file: [`call_site_line`] for the runner call, [`event_lines`] for each event.
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct SourceView {
|
||||||
|
/// Absolute or crate-relative path as reported by the backtrace resolver.
|
||||||
|
pub path: String,
|
||||||
|
/// Entire file contents, lines separated by `\n`. `None` if the file couldn't be read.
|
||||||
|
pub contents: Option<String>,
|
||||||
|
/// Line number of the `.run()` / `.step()` call that produced this frame.
|
||||||
|
pub call_site_line: Option<u32>,
|
||||||
|
/// Line numbers of events consumed by this frame's step, in queue order.
|
||||||
|
pub event_lines: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
/// A single rendered frame plus the accesskit tree update produced by the harness step.
|
/// A single rendered frame plus the accesskit tree update produced by the harness step.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Frame {
|
pub struct Frame {
|
||||||
@@ -29,13 +47,15 @@ pub struct Frame {
|
|||||||
pub accesskit: Option<accesskit::TreeUpdate>,
|
pub accesskit: Option<accesskit::TreeUpdate>,
|
||||||
/// Optional human-readable label (e.g. test name).
|
/// Optional human-readable label (e.g. test name).
|
||||||
pub label: Option<String>,
|
pub label: Option<String>,
|
||||||
|
/// The test source file associated with this frame + the lines to highlight inside it.
|
||||||
|
pub source: Option<SourceView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sent harness → inspector after every step, and once when the harness disconnects.
|
/// Sent harness → inspector after every step, and once when the harness disconnects.
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub enum HarnessMessage {
|
pub enum HarnessMessage {
|
||||||
/// A new frame is available.
|
/// A new frame is available.
|
||||||
Frame(Frame),
|
Frame(Box<Frame>),
|
||||||
/// The harness is shutting down (e.g. `Drop`).
|
/// The harness is shutting down (e.g. `Drop`).
|
||||||
Goodbye,
|
Goodbye,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use accesskit::{Node, NodeId, Rect as AkRect};
|
|||||||
|
|
||||||
/// Internal worker → UI message.
|
/// Internal worker → UI message.
|
||||||
enum WorkerEvent {
|
enum WorkerEvent {
|
||||||
Frame(Frame),
|
Frame(Box<Frame>),
|
||||||
Disconnected,
|
Disconnected,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ impl InspectorApp {
|
|||||||
self.received_count += 1;
|
self.received_count += 1;
|
||||||
self.upload_frame(ctx, &frame);
|
self.upload_frame(ctx, &frame);
|
||||||
// Keep the selection sticky across frames (same NodeId may still exist).
|
// Keep the selection sticky across frames (same NodeId may still exist).
|
||||||
self.current_frame = Some(frame);
|
self.current_frame = Some(*frame);
|
||||||
self.worker_waiting = true;
|
self.worker_waiting = true;
|
||||||
}
|
}
|
||||||
WorkerEvent::Disconnected => {
|
WorkerEvent::Disconnected => {
|
||||||
@@ -272,6 +272,12 @@ fn details_panel(app: &mut InspectorApp, ui: &mut egui::Ui) {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
egui::CollapsingHeader::new("Source")
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
source_section(ui, &frame);
|
||||||
|
});
|
||||||
|
|
||||||
egui::CollapsingHeader::new("Frame")
|
egui::CollapsingHeader::new("Frame")
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
@@ -524,6 +530,89 @@ fn kv_grid(ui: &mut egui::Ui, id: &str, body: impl FnOnce(&mut egui::Ui)) {
|
|||||||
.show(ui, body);
|
.show(ui, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render the "Source" section: the test file (topmost common ancestor across the call and
|
||||||
|
/// its events), with the relevant lines highlighted and the view scrolled to them.
|
||||||
|
fn source_section(ui: &mut egui::Ui, frame: &kittest_inspector::Frame) {
|
||||||
|
let Some(source) = &frame.source else {
|
||||||
|
ui.weak("No source location for this frame.");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.monospace(shorten_path(&source.path));
|
||||||
|
if let Some(line) = source.call_site_line {
|
||||||
|
ui.weak(format!("(producer: line {line})"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let Some(contents) = source.contents.as_deref() else {
|
||||||
|
ui.weak(format!("(couldn't read {})", source.path));
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let call_site_line = source.call_site_line;
|
||||||
|
let event_lines: std::collections::HashSet<u32> = source.event_lines.iter().copied().collect();
|
||||||
|
let focus_line = call_site_line.or_else(|| source.event_lines.first().copied());
|
||||||
|
|
||||||
|
// Fixed-height viewport with auto-scroll to the focused line.
|
||||||
|
let row_height = ui.text_style_height(&egui::TextStyle::Monospace);
|
||||||
|
let scroll_area = egui::ScrollArea::both()
|
||||||
|
.auto_shrink([false, false])
|
||||||
|
.max_height(320.0);
|
||||||
|
let output = scroll_area.show_rows(ui, row_height, contents.lines().count(), |ui, range| {
|
||||||
|
for (idx, line) in contents.lines().enumerate().skip(range.start).take(range.len()) {
|
||||||
|
let line_no = idx as u32 + 1;
|
||||||
|
let is_call = Some(line_no) == call_site_line;
|
||||||
|
let is_event = event_lines.contains(&line_no);
|
||||||
|
let bg = if is_call {
|
||||||
|
Some(egui::Color32::from_rgb(30, 70, 120))
|
||||||
|
} else if is_event {
|
||||||
|
Some(egui::Color32::from_rgb(90, 60, 20))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
source_line_row(ui, line_no, line, bg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scroll the focused line into view on the first render of each new frame.
|
||||||
|
if let Some(focus) = focus_line {
|
||||||
|
let target_y = output.inner_rect.min.y + (focus.saturating_sub(1) as f32) * row_height;
|
||||||
|
let target = egui::Rect::from_min_size(
|
||||||
|
egui::pos2(output.inner_rect.min.x, target_y),
|
||||||
|
egui::vec2(1.0, row_height),
|
||||||
|
);
|
||||||
|
ui.scroll_to_rect(target, Some(egui::Align::Center));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_line_row(ui: &mut egui::Ui, line_no: u32, text: &str, bg: Option<egui::Color32>) {
|
||||||
|
let row = ui.horizontal(|ui| {
|
||||||
|
ui.set_min_width(ui.available_width());
|
||||||
|
ui.add(egui::Label::new(
|
||||||
|
egui::RichText::new(format!("{line_no:>4} "))
|
||||||
|
.monospace()
|
||||||
|
.weak(),
|
||||||
|
));
|
||||||
|
ui.add(egui::Label::new(egui::RichText::new(text).monospace()).wrap_mode(egui::TextWrapMode::Extend));
|
||||||
|
});
|
||||||
|
if let Some(color) = bg {
|
||||||
|
ui.painter().rect_filled(row.response.rect, 2.0, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorten a `rustc`-reported path for display — keep the last two components so we show
|
||||||
|
/// `tests/menu.rs` instead of a long absolute path, while still disambiguating.
|
||||||
|
fn shorten_path(path: &str) -> String {
|
||||||
|
let components: Vec<&str> = path.split(['/', '\\']).collect();
|
||||||
|
if components.len() <= 2 {
|
||||||
|
path.to_owned()
|
||||||
|
} else {
|
||||||
|
let n = components.len();
|
||||||
|
format!("{}/{}", components[n - 2], components[n - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Render the inspector grid for a single accesskit node, mimicking egui's `inspection_ui`.
|
/// Render the inspector grid for a single accesskit node, mimicking egui's `inspection_ui`.
|
||||||
fn widget_details(ui: &mut egui::Ui, id: NodeId, node: &Node) {
|
fn widget_details(ui: &mut egui::Ui, id: NodeId, node: &Node) {
|
||||||
kv_grid(ui, "widget_grid", |ui| {
|
kv_grid(ui, "widget_grid", |ui| {
|
||||||
|
|||||||
Reference in New Issue
Block a user