From 7a30644bb07a4f8268035a75201bfe1900fcf6a7 Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Mon, 15 Jun 2026 04:17:45 +0800 Subject: [PATCH] perf(canvas): skip idle reveal node checks --- .../src/widgets/canvas_viewport.rs | 43 +++++++++++++------ .../src/widgets/canvas_viewport_tests.rs | 15 +++++++ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport.rs b/crates/op-editor-ui/src/widgets/canvas_viewport.rs index aebaec0b9..e59a1f0d1 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport.rs @@ -481,19 +481,28 @@ impl<'a> Widget for CanvasViewport<'a> { rect.size.y + CULL_MARGIN * 2.0, ), }; + let reveal_schedule = reveal_schedule_for_paint(&indicators.reveals, self.now_ms); for child in page.children.iter().rev() { - super::canvas_viewport_paint::paint_node_with_reveals( - cx, - child, - viewport_origin, - viewport.zoom, - edit_caret.clone(), - cull, - super::canvas_viewport_paint::RevealSchedule { - starts: &indicators.reveals, - now_ms: self.now_ms, - }, - ); + if let Some(reveals) = reveal_schedule { + super::canvas_viewport_paint::paint_node_with_reveals( + cx, + child, + viewport_origin, + viewport.zoom, + edit_caret.clone(), + cull, + reveals, + ); + } else { + super::canvas_viewport_paint::paint_node( + cx, + child, + viewport_origin, + viewport.zoom, + edit_caret.clone(), + cull, + ); + } } super::canvas_agent_overlay::paint_agent_frame_indicators_with_snapshot( cx, @@ -710,6 +719,16 @@ impl<'a> Widget for CanvasViewport<'a> { } } +pub(crate) fn reveal_schedule_for_paint<'a>( + reveals: &'a std::collections::HashMap, + now_ms: u64, +) -> Option> { + (!reveals.is_empty()).then_some(super::canvas_viewport_paint::RevealSchedule { + starts: reveals, + now_ms, + }) +} + /// Stroke a dashed rectangle as 4 dashed edges (4 px on / 4 px off, /// screen-space) — the `RenderBackend` trait has no path-effect /// surface, so the dash is segmented by hand; backend-agnostic. diff --git a/crates/op-editor-ui/src/widgets/canvas_viewport_tests.rs b/crates/op-editor-ui/src/widgets/canvas_viewport_tests.rs index 097452f31..e7fb42611 100644 --- a/crates/op-editor-ui/src/widgets/canvas_viewport_tests.rs +++ b/crates/op-editor-ui/src/widgets/canvas_viewport_tests.rs @@ -6,6 +6,7 @@ use super::*; use crate::layout_scene::{LayoutScene, SceneFillType, SceneNode, ScenePage, SceneStroke}; use crate::{Color, Point2D, Rect, TextLayout}; +use std::collections::HashMap; /// Records op order; clip-isolated paint = `Save, Clip, Fill, …, Restore`. #[derive(Debug, PartialEq, Eq)] @@ -217,6 +218,20 @@ fn grid_dot_count_matches_painted_dot_batch() { ); } +#[test] +fn empty_reveals_use_plain_node_paint_path() { + let empty = HashMap::new(); + assert!( + reveal_schedule_for_paint(&empty, 1_000).is_none(), + "idle canvas paint should not give every node an empty reveal lookup" + ); + + let active = HashMap::from([("n1".to_string(), 1_000)]); + let schedule = reveal_schedule_for_paint(&active, 1_250).expect("active reveal schedule"); + assert_eq!(schedule.now_ms, 1_250); + assert!(std::ptr::eq(schedule.starts, &active)); +} + #[test] fn unselected_scene_skips_overlay_stroke() { let state = sample_state();