mirror of
https://github.com/emilk/egui.git
synced 2026-08-29 04:40:03 -04:00
Small improvements
This commit is contained in:
11
Cargo.lock
11
Cargo.lock
@@ -1792,6 +1792,16 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs4"
|
||||||
|
version = "0.13.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4"
|
||||||
|
dependencies = [
|
||||||
|
"rustix 1.1.4",
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
@@ -2526,6 +2536,7 @@ dependencies = [
|
|||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui_extras",
|
"egui_extras",
|
||||||
|
"fs4",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ ehttp = { version = "0.7.1", default-features = false }
|
|||||||
enum-map = "2.7.3"
|
enum-map = "2.7.3"
|
||||||
env_logger = { version = "0.11.8", default-features = false }
|
env_logger = { version = "0.11.8", default-features = false }
|
||||||
font-types = { version = "0.11.0", default-features = false, features = ["std"] }
|
font-types = { version = "0.11.0", default-features = false, features = ["std"] }
|
||||||
|
fs4 = { version = "0.13.1", default-features = false, features = ["sync"] }
|
||||||
glow = "0.17.0"
|
glow = "0.17.0"
|
||||||
glutin = { version = "0.32.3", default-features = false }
|
glutin = { version = "0.32.3", default-features = false }
|
||||||
glutin-winit = { version = "0.5.0", default-features = false }
|
glutin-winit = { version = "0.5.0", default-features = false }
|
||||||
|
|||||||
@@ -892,11 +892,12 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
/// Block at the inspector until it tells us to resume, re-rendering after each batch of
|
/// Block at the inspector until it tells us to resume, re-rendering after each batch of
|
||||||
/// events it sends. Events drive an internal `_step_inner` (and recording capture), but
|
/// events it sends. Events drive an internal `_step_inner` (and recording capture), but
|
||||||
/// do NOT return control to the outer test — the test advances only when the inspector
|
/// do NOT return control to the outer test — the test advances only when the inspector
|
||||||
/// replies with no events *and* egui has no pending repaint (i.e. user hit Next/Play with
|
/// replies with no events (i.e. user hit Next/Play/Step, or Control mode is off).
|
||||||
/// a settled UI, or Control mode is off).
|
|
||||||
///
|
///
|
||||||
/// While paused in Control mode, any `request_repaint` from the app (animations, async
|
/// We only loop while the inspector is *feeding events back* (Control mode) — animation
|
||||||
/// image loads, etc.) also drives another internal step so the user sees the UI tick.
|
/// frames driven by `request_repaint` are handled by the outer `try_run` loop calling
|
||||||
|
/// `step()` again, so we don't need to drive them here. Doing so would send extra "no
|
||||||
|
/// event highlighted" frames between each event and confuse the Step UX.
|
||||||
#[cfg(feature = "inspector")]
|
#[cfg(feature = "inspector")]
|
||||||
fn drive_inspector(&mut self) {
|
fn drive_inspector(&mut self) {
|
||||||
if self.inspector.is_none() {
|
if self.inspector.is_none() {
|
||||||
@@ -922,8 +923,7 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let wants_repaint = self.root_viewport_output().repaint_delay == Duration::ZERO;
|
if events.is_empty() {
|
||||||
if events.is_empty() && !wants_repaint {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for event in events {
|
for event in events {
|
||||||
@@ -934,6 +934,13 @@ impl<'a, State> Harness<'a, State> {
|
|||||||
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);
|
||||||
|
|
||||||
|
// Run one more step so effects of the just-delivered events are visible in the
|
||||||
|
// next frame we send (e.g. a clicked button's state change). Without this we'd
|
||||||
|
// show the frame *during* the click but not *after*.
|
||||||
|
self._step_inner(false);
|
||||||
|
#[cfg(feature = "recording")]
|
||||||
|
self.capture_frame_if_recording(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ required-features = ["app"]
|
|||||||
default = ["app"]
|
default = ["app"]
|
||||||
|
|
||||||
## Build the eframe inspector binary.
|
## Build the eframe inspector binary.
|
||||||
app = ["dep:eframe", "dep:egui_extras"]
|
app = ["dep:eframe", "dep:egui_extras", "dep:fs4"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
accesskit = { workspace = true, features = ["serde"] }
|
accesskit = { workspace = true, features = ["serde"] }
|
||||||
@@ -36,6 +36,7 @@ serde = { workspace = true }
|
|||||||
# `app` feature dependencies:
|
# `app` feature dependencies:
|
||||||
eframe = { workspace = true, features = ["default_fonts", "wgpu"], optional = true }
|
eframe = { workspace = true, features = ["default_fonts", "wgpu"], optional = true }
|
||||||
egui_extras = { workspace = true, features = ["image"], optional = true }
|
egui_extras = { workspace = true, features = ["image"], optional = true }
|
||||||
|
fs4 = { workspace = true, optional = true }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -34,7 +34,26 @@ enum PlayState {
|
|||||||
Paused,
|
Paused,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fast-forward state for the ⏭ Next button.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
enum SkipState {
|
||||||
|
Inactive,
|
||||||
|
/// Auto-release every incoming frame until `call_site_line` differs from this value.
|
||||||
|
UntilNewCallLine(Option<u32>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SkipState {
|
||||||
|
fn is_active(self) -> bool {
|
||||||
|
matches!(self, Self::UntilNewCallLine(_))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> eframe::Result<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
|
// Cross-process single-instance guard. If another inspector is already running, block
|
||||||
|
// here until that window closes. Held for the lifetime of `_lock`; the OS releases the
|
||||||
|
// flock when the file descriptor is dropped on exit.
|
||||||
|
let _lock = acquire_single_instance_lock();
|
||||||
|
|
||||||
let (worker_tx, worker_rx) = mpsc::channel::<WorkerEvent>();
|
let (worker_tx, worker_rx) = mpsc::channel::<WorkerEvent>();
|
||||||
let (release_tx, release_rx) = mpsc::channel::<Vec<egui::Event>>();
|
let (release_tx, release_rx) = mpsc::channel::<Vec<egui::Event>>();
|
||||||
|
|
||||||
@@ -115,6 +134,9 @@ struct InspectorApp {
|
|||||||
queued_events: Vec<egui::Event>,
|
queued_events: Vec<egui::Event>,
|
||||||
/// Set when a new frame arrives; the Source section consumes it to scroll once per frame.
|
/// Set when a new frame arrives; the Source section consumes it to scroll once per frame.
|
||||||
scroll_pending: bool,
|
scroll_pending: bool,
|
||||||
|
/// While `UntilNewCallLine`, auto-release every incoming frame until we see one with a
|
||||||
|
/// different `call_site_line` — i.e. until the test moves past the current runner call.
|
||||||
|
skip: SkipState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InspectorApp {
|
impl InspectorApp {
|
||||||
@@ -137,6 +159,7 @@ impl InspectorApp {
|
|||||||
control_enabled: false,
|
control_enabled: false,
|
||||||
queued_events: Vec::new(),
|
queued_events: Vec::new(),
|
||||||
scroll_pending: false,
|
scroll_pending: false,
|
||||||
|
skip: SkipState::Inactive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,14 +169,32 @@ impl InspectorApp {
|
|||||||
WorkerEvent::Frame(frame) => {
|
WorkerEvent::Frame(frame) => {
|
||||||
self.received_count += 1;
|
self.received_count += 1;
|
||||||
self.upload_frame(ctx, &frame);
|
self.upload_frame(ctx, &frame);
|
||||||
|
let new_call_line = frame
|
||||||
|
.source
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.call_site_line);
|
||||||
// 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;
|
||||||
self.scroll_pending = true;
|
|
||||||
|
// If we're fast-forwarding to the next `run()` call, stop once the
|
||||||
|
// call_site line differs from the one we started from.
|
||||||
|
let still_skipping = matches!(
|
||||||
|
self.skip,
|
||||||
|
SkipState::UntilNewCallLine(from) if new_call_line == from
|
||||||
|
);
|
||||||
|
if still_skipping {
|
||||||
|
// Don't auto-scroll / flash for in-between frames we're about to blow
|
||||||
|
// past; the user will see the first settled frame at the new call.
|
||||||
|
} else {
|
||||||
|
self.skip = SkipState::Inactive;
|
||||||
|
self.scroll_pending = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
WorkerEvent::Disconnected => {
|
WorkerEvent::Disconnected => {
|
||||||
self.connected = false;
|
self.connected = false;
|
||||||
self.worker_waiting = false;
|
self.worker_waiting = false;
|
||||||
|
self.skip = SkipState::Inactive;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -189,10 +230,13 @@ impl eframe::App for InspectorApp {
|
|||||||
central_panel(self, ui);
|
central_panel(self, ui);
|
||||||
|
|
||||||
// End-of-frame auto-release policy:
|
// End-of-frame auto-release policy:
|
||||||
|
// - Fast-forwarding to the next `run()` call: always release.
|
||||||
// - Control mode: stay blocked, but advance one step whenever the user generates events
|
// - Control mode: stay blocked, but advance one step whenever the user generates events
|
||||||
// (each click / keypress = one harness step).
|
// (each click / keypress = one harness step).
|
||||||
// - Otherwise, Playing mode runs freely; Paused mode waits for Next/Play.
|
// - Otherwise, Playing mode runs freely; Paused mode waits for Next/Play/Step.
|
||||||
let auto_release = if self.control_enabled {
|
let auto_release = if self.skip.is_active() {
|
||||||
|
true
|
||||||
|
} else if self.control_enabled {
|
||||||
!self.queued_events.is_empty()
|
!self.queued_events.is_empty()
|
||||||
} else {
|
} else {
|
||||||
self.play_state == PlayState::Playing
|
self.play_state == PlayState::Playing
|
||||||
@@ -228,12 +272,27 @@ fn controls_panel(app: &mut InspectorApp, ui: &mut egui::Ui) {
|
|||||||
}
|
}
|
||||||
let can_step = app.play_state == PlayState::Paused && app.worker_waiting;
|
let can_step = app.play_state == PlayState::Paused && app.worker_waiting;
|
||||||
if ui
|
if ui
|
||||||
.add_enabled(can_step, egui::Button::new("⏭ Next"))
|
.add_enabled(can_step, egui::Button::new("⏩ Step"))
|
||||||
.on_hover_text("Advance one harness step")
|
.on_hover_text("Advance one harness internal step")
|
||||||
.clicked()
|
.clicked()
|
||||||
{
|
{
|
||||||
app.send_release();
|
app.send_release();
|
||||||
}
|
}
|
||||||
|
if ui
|
||||||
|
.add_enabled(can_step, egui::Button::new("⏭ Next"))
|
||||||
|
.on_hover_text(
|
||||||
|
"Fast-forward until the test reaches the next `run()` / `step()` call",
|
||||||
|
)
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
let current_line = app
|
||||||
|
.current_frame
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|f| f.source.as_ref())
|
||||||
|
.and_then(|s| s.call_site_line);
|
||||||
|
app.skip = SkipState::UntilNewCallLine(current_line);
|
||||||
|
app.send_release();
|
||||||
|
}
|
||||||
|
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
@@ -732,3 +791,40 @@ fn widget_details(ui: &mut egui::Ui, id: NodeId, node: &Node) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Try to acquire a cross-process exclusive lock on a well-known file so that only one
|
||||||
|
/// inspector window can be open on the machine at a time. Blocks here (before we open any
|
||||||
|
/// windows or touch stdio beyond this stderr line) if another inspector is already running.
|
||||||
|
fn acquire_single_instance_lock() -> Option<std::fs::File> {
|
||||||
|
use fs4::fs_std::FileExt;
|
||||||
|
|
||||||
|
// We specifically need a stable, cross-process path here — tempfile's per-process dir
|
||||||
|
// can't serve as a system-wide mutex.
|
||||||
|
#[expect(clippy::disallowed_methods)]
|
||||||
|
let path = std::env::temp_dir().join("kittest_inspector.lock");
|
||||||
|
|
||||||
|
let file = match std::fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.create(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(&path)
|
||||||
|
{
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"kittest_inspector: couldn't open lock file {}: {err} (running without single-instance guard)",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match FileExt::lock_exclusive(&file) {
|
||||||
|
Ok(()) => Some(file),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("kittest_inspector: failed to acquire lock: {err}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user