diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index adfb7bf88c..06e7434ebe 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -320,6 +320,12 @@ (wasm.api/initialize base-objects zoom vbox background) (reset! initialized? true))) + (mf/with-effect [focus] + (when (and @canvas-init? @initialized?) + (if (empty? focus) + (wasm.api/clear-focus-mode) + (wasm.api/set-focus-mode focus)))) + (mf/with-effect [vbox zoom] (when (and @canvas-init? initialized?) (wasm.api/set-view-box zoom vbox))) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index c49bd05f6d..d4d637899a 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -248,7 +248,7 @@ heap (mem/get-heap-u8) dview (js/DataView. (.-buffer heap))] - ;; write fill data to heap + ;; write fill data to heap (loop [fills (seq fills) current-offset offset] (when-not (empty? fills) @@ -256,10 +256,10 @@ new-offset (sr-fills/write-fill! current-offset dview fill)] (recur (rest fills) new-offset)))) - ;; send fills to wasm + ;; send fills to wasm (h/call wasm/internal-module "_set_shape_fills") - ;; load images for image fills if not cached + ;; load images for image fills if not cached (keep (fn [fill] (let [image (:fill-image fill) id (dm/get-prop image :id) @@ -802,6 +802,28 @@ (request-render "set-objects") (process-pending pending))) +(defn clear-focus-mode + [] + (h/call wasm/internal-module "_clear_focus_mode") + (clear-drawing-cache) + (request-render "clear-focus-mode")) + +(defn set-focus-mode + [entries] + (let [offset (mem/alloc-bytes-32 (* (count entries) 16)) + heapu32 (mem/get-heap-u32)] + + (loop [entries (seq entries) + current-offset offset] + (when-not (empty? entries) + (let [id (first entries)] + (sr/heapu32-set-uuid id heapu32 current-offset) + (recur (rest entries) (+ current-offset (mem/ptr8->ptr32 16)))))) + + (h/call wasm/internal-module "_set_focus_mode") + (clear-drawing-cache) + (request-render "set-focus-mode"))) + (defn set-structure-modifiers [entries] (when-not (empty? entries) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 5995dd047c..91fd754491 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -161,6 +161,27 @@ pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) { }); } +#[no_mangle] +pub extern "C" fn clear_focus_mode() { + with_state!(state, { + state.clear_focus_mode(); + }); +} + +#[no_mangle] +pub extern "C" fn set_focus_mode() { + let bytes = mem::bytes(); + + let entries: Vec = bytes + .chunks(size_of::<::BytesType>()) + .map(|data| Uuid::from_bytes(data.try_into().unwrap())) + .collect(); + + with_state!(state, { + state.set_focus_mode(entries); + }); +} + #[no_mangle] pub extern "C" fn init_shapes_pool(capacity: usize) { with_state!(state, { diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index ccfe71c8cf..135225c1a3 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -81,6 +81,73 @@ impl NodeRenderState { } } +/// Represents the "focus mode" state used during rendering. +/// +/// Focus mode allows selectively highlighting or isolating specific shapes (UUIDs) +/// during the render pass. It maintains a list of shapes to focus and tracks +/// whether the current rendering context is inside a focused element. +/// +/// # Focus Propagation +/// If a shape is in focus, all its nested content +/// is also considered to be in focus for the duration of the render traversal. Focus +/// state propagates *downward* through the tree while rendering. +/// +/// # Usage +/// - `set_shapes(...)` to activate focus mode for specific elements and their anidated content. +/// - `clear()` to disable focus mode. +/// - `reset()` should be called at the beginning of the render loop. +/// - `enter(...)` / `exit(...)` should be called when entering and leaving shape +/// render contexts. +/// - `is_active()` returns whether the current shape is being rendered in focus. +pub struct FocusMode { + shapes: Vec, + active: bool, +} + +impl FocusMode { + pub fn new() -> Self { + FocusMode { + shapes: Vec::new(), + active: false, + } + } + + pub fn clear(&mut self) { + self.shapes.clear(); + self.active = false; + } + + pub fn set_shapes(&mut self, shapes: Vec) { + self.shapes = shapes; + } + + /// Returns `true` if the given shape ID should be focused. + /// If the `shapes` list is empty, focus applies to all shapes. + pub fn should_focus(&self, id: &Uuid) -> bool { + self.shapes.is_empty() || self.shapes.contains(id) + } + + pub fn enter(&mut self, id: &Uuid) { + if !self.active && self.should_focus(id) { + self.active = true; + } + } + + pub fn exit(&mut self, id: &Uuid) { + if self.active && self.should_focus(id) { + self.active = false; + } + } + + pub fn is_active(&self) -> bool { + self.active + } + + pub fn reset(&mut self) { + self.active = false; + } +} + pub(crate) struct RenderState { gpu_state: GpuState, pub options: RenderOptions, @@ -109,6 +176,7 @@ pub(crate) struct RenderState { // migration to remove group-level fills is completed, this code should be removed. pub nested_fills: Vec>, pub show_grid: Option, + pub focus_mode: FocusMode, } pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { @@ -176,6 +244,7 @@ impl RenderState { pending_tiles: PendingTiles::new_empty(), nested_fills: vec![], show_grid: None, + focus_mode: FocusMode::new(), } } @@ -311,6 +380,14 @@ impl RenderState { }); } + pub fn clear_focus_mode(&mut self) { + self.focus_mode.clear(); + } + + pub fn set_focus_mode(&mut self, shapes: Vec) { + self.focus_mode.set_shapes(shapes); + } + pub fn render_shape( &mut self, shape: &Shape, @@ -569,6 +646,8 @@ impl RenderState { let scale = self.get_scale(); self.tile_viewbox.update(self.viewbox, scale); + self.focus_mode.reset(); + performance::begin_measure!("render"); performance::begin_measure!("start_render_loop"); @@ -683,6 +762,8 @@ impl RenderState { self.surfaces .canvas(SurfaceId::Current) .save_layer(&layer_rec); + + self.focus_mode.enter(&element.id); } #[inline] @@ -728,6 +809,8 @@ impl RenderState { self.nested_fills.pop(); } self.surfaces.canvas(SurfaceId::Current).restore(); + + self.focus_mode.exit(&element.id); } pub fn get_current_tile_bounds(&mut self) -> Rect { @@ -823,7 +906,7 @@ impl RenderState { } self.render_shape_enter(element, mask); - if !node_render_state.is_root() { + if !node_render_state.is_root() && self.focus_mode.is_active() { self.render_shape( element, modifiers.get(&element.id), diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index d9aaab2d7a..f8d617a26d 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -1,5 +1,5 @@ use std::collections::{hash_map::Entry, HashMap}; -use std::iter; +use std::{iter, vec}; use skia_safe as skia; @@ -135,6 +135,14 @@ impl<'a> State<'a> { Ok(()) } + pub fn clear_focus_mode(&mut self) { + self.render_state.clear_focus_mode(); + } + + pub fn set_focus_mode(&mut self, shapes: Vec) { + self.render_state.set_focus_mode(shapes); + } + pub fn init_shapes_pool(&mut self, capacity: usize) { self.shapes_pool.initialize(capacity); }