diff --git a/crates/op-editor-ui/src/widgets/canvas_agent_overlay.rs b/crates/op-editor-ui/src/widgets/canvas_agent_overlay.rs index 035aa9d0f..9a3d14393 100644 --- a/crates/op-editor-ui/src/widgets/canvas_agent_overlay.rs +++ b/crates/op-editor-ui/src/widgets/canvas_agent_overlay.rs @@ -9,6 +9,8 @@ use crate::widgets::PaintCx; use crate::{Color, Point2D, Rect, TextLayout}; use op_editor_core::agent_indicators::AgentIndicators; use std::collections::HashMap; +#[cfg(test)] +use std::sync::atomic::{AtomicUsize, Ordering}; /// One full breathe (0 → 1 → 0) per this many ms. const GLOW_PERIOD_MS: u64 = 1200; @@ -19,6 +21,24 @@ const REVEAL_ACCENT: Color = Color { a: 1.0, }; +#[cfg(test)] +static REVEAL_WALK_VISITS: AtomicUsize = AtomicUsize::new(0); + +#[cfg(test)] +fn reset_reveal_walk_visits() { + REVEAL_WALK_VISITS.store(0, Ordering::Relaxed); +} + +#[cfg(test)] +fn reveal_walk_visits() -> usize { + REVEAL_WALK_VISITS.load(Ordering::Relaxed) +} + +#[cfg(test)] +fn record_reveal_walk_visit() { + REVEAL_WALK_VISITS.fetch_add(1, Ordering::Relaxed); +} + /// Paint a breathing border around every active-page root frame that an /// agent currently owns. `roots` are the page's top-level scene nodes; /// `viewport_origin` already folds in the pan offset. @@ -49,14 +69,16 @@ pub(crate) fn paint_agent_frame_indicators_with_snapshot( now_ms: u64, indicators: &AgentIndicators, ) { - paint_node_reveal_indicators( - cx, - roots, - viewport_origin, - zoom, - now_ms, - &indicators.reveals, - ); + if !indicators.reveals.is_empty() { + paint_node_reveal_indicators( + cx, + roots, + viewport_origin, + zoom, + now_ms, + &indicators.reveals, + ); + } if indicators.frames.is_empty() { return; } @@ -102,6 +124,9 @@ fn paint_node_reveal_indicators( now_ms: u64, reveals: &HashMap, ) { + if reveals.is_empty() { + return; + } for root in roots { paint_node_reveal_indicator(cx, root, viewport_origin, zoom, now_ms, reveals, false); } @@ -116,6 +141,9 @@ fn paint_node_reveal_indicator( reveals: &HashMap, ancestor_revealing: bool, ) { + #[cfg(test)] + record_reveal_walk_visit(); + let reveal = reveals .get(&node.id) .and_then(|started_at| reveal_phase(*started_at, now_ms)); @@ -349,6 +377,45 @@ mod tests { vec![node] } + fn reveal_tree_with_many_nodes(count: usize) -> Vec { + let mut root = SceneNode::leaf("root", NodeKind::Frame); + root.bounds = Rect::xywh(0.0, 0.0, 100.0, 100.0); + root.children = (0..count) + .map(|i| { + let mut child = SceneNode::leaf(format!("child-{i}"), NodeKind::Rect); + child.bounds = Rect::xywh(i as f32, 0.0, 10.0, 10.0); + child + }) + .collect(); + vec![root] + } + + #[test] + fn empty_reveal_snapshot_skips_reveal_tree_walk() { + let roots = reveal_tree_with_many_nodes(128); + let indicators = AgentIndicators::default(); + let mut backend = RevealCaptureBackend::default(); + let mut cx = PaintCx { + backend: &mut backend, + }; + + reset_reveal_walk_visits(); + paint_agent_frame_indicators_with_snapshot( + &mut cx, + &roots, + Point2D::ZERO, + 1.0, + 1_000, + &indicators, + ); + + assert_eq!( + reveal_walk_visits(), + 0, + "empty reveal snapshots should not walk the scene tree" + ); + } + #[test] fn reveal_overlay_paints_lifted_highlight_sweep_and_prunes_after_window() { let _guard = crate::agent_indicator_test_support::lock();