From 9512224eb4afd49a98aa7b93c96eef841b3fca5b Mon Sep 17 00:00:00 2001 From: Lucas Meurer Date: Mon, 17 Aug 2026 17:09:35 +0200 Subject: [PATCH] Add `RepaintCauses` and `TreeUpdate` to `egui_inspection::Settle` response --- crates/egui_inspection/src/plugin.rs | 22 +++++++++++-- crates/egui_inspection/src/protocol.rs | 43 +++++++++++++++++--------- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/crates/egui_inspection/src/plugin.rs b/crates/egui_inspection/src/plugin.rs index 76ad8893a..ed0497590 100644 --- a/crates/egui_inspection/src/plugin.rs +++ b/crates/egui_inspection/src/plugin.rs @@ -38,7 +38,7 @@ use std::sync::mpsc; use egui::{Context, FullOutput, RawInput}; -use crate::protocol::{EncodedPng, Request, Response}; +use crate::protocol::{EncodedPng, Request, Response, Tree, TreeResponse}; /// How long [`serve`]'s connection threads wait for the UI thread before giving up. Generous: /// a backgrounded window may not paint (and thus not service requests) for a while. @@ -274,11 +274,11 @@ impl egui::Plugin for InspectionPlugin { .retain_mut(|item| match (&mut item.phase, &item.req) { (Phase::AwaitOutput, Request::GetTree) => { if let Some(reply) = item.reply.take() { - reply(Response::Tree { + reply(Response::Tree(TreeResponse { step, pixels_per_point: output.pixels_per_point, accesskit: output.platform_output.accesskit_update.clone(), - }); + })); } false } @@ -299,9 +299,25 @@ impl egui::Plugin for InspectionPlugin { let steps_exceeded = *steps_taken >= *max_steps; if !immediate_repaint || steps_exceeded { if let Some(reply) = item.reply.take() { + // Report what keeps the app busy, so the inspector can say *why* it + // never settled instead of only that it didn't. + let repaint_causes = if immediate_repaint { + ctx.repaint_causes() + .iter() + .map(ToString::to_string) + .collect() + } else { + Vec::new() + }; reply(Response::Settled { settled: !immediate_repaint, steps: *steps_taken, + repaint_causes, + tree: TreeResponse { + step, + pixels_per_point: output.pixels_per_point, + accesskit: output.platform_output.accesskit_update.clone(), + }, }); } false diff --git a/crates/egui_inspection/src/protocol.rs b/crates/egui_inspection/src/protocol.rs index e95ce5cf5..1a8e011e2 100644 --- a/crates/egui_inspection/src/protocol.rs +++ b/crates/egui_inspection/src/protocol.rs @@ -23,7 +23,7 @@ use egui::accesskit; /// Wire-protocol version, sent in the connection handshake (see [`write_handshake`]). /// /// Bump on any non-additive change to [`Request`] / [`Response`]. -pub const PROTOCOL_VERSION: u32 = 1; +pub const PROTOCOL_VERSION: u32 = 2; /// Magic bytes that open every connection, identifying the egui inspection protocol. pub const PROTOCOL_MAGIC: [u8; 4] = *b"eins"; @@ -66,10 +66,26 @@ pub enum Request { /// Wait until the app goes idle, then reply [`Response::Settled`]. /// - /// Will wait for at most `max_steps`. + /// Will wait for at most `max_steps`. If the app never goes idle, the reply lists the + /// repaint causes that kept it busy. Settle { max_steps: u64 }, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct TreeResponse { + /// Monotonically increasing frame counter. + pub step: u64, + + /// `physical_pixel = logical_point * pixels_per_point`. AccessKit bounds are in + /// logical coords; a screenshot is in physical pixels — multiply to align them. + pub pixels_per_point: f32, + + /// The current full AccessKit tree. egui rebuilds the complete node set every pass, + /// so this is a full snapshot, not an incremental update. `None` if AccessKit hasn't + /// produced a tree yet. + pub accesskit: Option, +} + /// Sent peer → inspector, exactly one per [`Request`]. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub enum Response { @@ -83,19 +99,7 @@ pub enum Response { }, /// Reply to [`Request::GetTree`]. - Tree { - /// Monotonically increasing frame counter. - step: u64, - - /// `physical_pixel = logical_point * pixels_per_point`. AccessKit bounds are in - /// logical coords; a screenshot is in physical pixels — multiply to align them. - pixels_per_point: f32, - - /// The current full AccessKit tree. egui rebuilds the complete node set every pass, - /// so this is a full snapshot, not an incremental update. `None` if AccessKit hasn't - /// produced a tree yet. - accesskit: Option, - }, + Tree(TreeResponse), /// Reply to [`Request::GetScreenshot`]. Screenshot(EncodedPng), @@ -111,6 +115,15 @@ pub enum Response { /// How many frames did we run until we settled? steps: u64, + + /// Why did the app keep repainting? One formatted `egui::RepaintCause` + /// (`file:line reason`) per entry. + /// + /// Empty when `settled` is true. + repaint_causes: Vec, + + /// The last frames tree. + tree: TreeResponse, }, /// The peer failed to service the request (recoverable; the connection stays open).