From e839c40770edc8b78e03b1ea54edbda2b22b29a3 Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sat, 20 Jun 2026 00:44:06 +0800 Subject: [PATCH] perf(canvas): patch scene fill/stroke in place on colour-picker drag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A colour-picker drag rewrites doc fill on every mouse-move, so the scene build cache (which compares the doc) rebuilt the whole layout — taffy + a full SceneNode reshape — per drag frame, even though a solid colour change touches no layout. Mirror the node-drag fast path: patch the anchor's resolved scene fill / stroke via LayoutScene::set_node_fill / set_node_stroke_color and invalidate the cache, skipping mark_dirty so the patch survives the frame. Variable-mode, instance redirects, gradient-stop / effect targets, and brand-new strokes fall back to the full rebuild. Bumps vendor/jian to a86832d (the scene-patch methods). Native only; the web host's live-sync push gate needs the same wiring once its spine settles. --- .../op-host-native/src/widget_host/input.rs | 59 ++++++++++++++++++- vendor/jian | 2 +- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/op-host-native/src/widget_host/input.rs b/crates/op-host-native/src/widget_host/input.rs index 9e39e80d5..51a1fa391 100644 --- a/crates/op-host-native/src/widget_host/input.rs +++ b/crates/op-host-native/src/widget_host/input.rs @@ -371,6 +371,54 @@ impl WidgetHostNative { true } + /// Patch the live scene's resolved fill / stroke for the active + /// colour-picker drag without a layout rebuild. Returns `true` when the + /// patch applied (caller skips `mark_dirty`); `false` when the edit is + /// not in the patchable set — variable-mode, an instance redirect, a + /// gradient-stop / effect target, or a node not solid-patchable — and + /// the caller must rebuild for correctness. + fn try_patch_color_drag(&mut self, is_instance: bool) -> bool { + use op_editor_core::ui_draft::ColorTarget; + use op_editor_ui::widgets::color_picker::hsv_to_rgb; + if is_instance || self.editor_state_dirty { + return false; + } + let Some(state) = self.editor_state.ui.color_picker.as_ref() else { + return false; + }; + // Variable-mode writes fan out to every node referencing the + // variable — far beyond the anchor — so let the rebuild repaint. + if state.variable.is_some() { + return false; + } + let is_fill = match state.target { + ColorTarget::Fill => true, + ColorTarget::Stroke => false, + // GradientStop / EffectColor touch gradient bodies / effects + // the scene patch does not model — rebuild instead. + _ => return false, + }; + let color = hsv_to_rgb(state.hue, state.sat, state.val); + let anchor = self.editor_state.selection.anchor.clone(); + if !anchor.is_real() || !self.editor_state.is_editable(&anchor) { + return false; + } + let ids = [anchor.as_str().to_string()]; + let patched = if is_fill { + self.layout_scene.set_node_fill(&ids, color) + } else { + self.layout_scene.set_node_stroke_color(&ids, color) + }; + if patched { + // The scene now drifts from the cached build inputs; invalidate + // so a later refresh always rebuilds from the canonical doc + // (mirrors the node-drag patch path — otherwise an undo back to + // the cached colour would skip the rebuild and leave the patch). + self.scene_cache.invalidate(); + } + patched + } + pub fn apply_cursor_move(&mut self, x: f32, y: f32) -> bool { // In-flight VariablesPanel edge resize — owns the cursor. if self.variables_resize.is_some() @@ -425,6 +473,7 @@ impl WidgetHostNative { // Instance-write redirect (GAP #10) — picker drags on // a Ref anchor route the live colour into descendants. let instance_scope = self.editor_state.begin_instance_write_for_anchor(); + let is_instance = instance_scope.is_some(); match kind { ColorPickerDrag::SvBox => { let (s, v) = picker.sv_at(panel, point); @@ -440,7 +489,15 @@ impl WidgetHostNative { if let Some(scope) = instance_scope { self.editor_state.finish_instance_write(scope); } - self.mark_dirty(); + // A solid Fill/Stroke change on a concrete anchor touches + // no layout, so patch the resolved scene paint in place + // rather than re-running taffy + reshape on every drag + // frame (mirrors the node-drag `translate_nodes` fast + // path). Variable-mode, instance redirects, gradient-stop / + // effect targets, and new strokes fall back to a rebuild. + if !self.try_patch_color_drag(is_instance) { + self.mark_dirty(); + } return true; } } diff --git a/vendor/jian b/vendor/jian index 27086661d..1565f66f0 160000 --- a/vendor/jian +++ b/vendor/jian @@ -1 +1 @@ -Subproject commit 27086661dbd76ef4b932ee382431d6dc2d55ffa1 +Subproject commit 1565f66f024a12c3020a43848d0312aa94c51aa9