From 0be8a6e0e628424dba7f781065ee49f34b8d5972 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 10 Jun 2025 16:46:19 +0200 Subject: [PATCH] :sparkles: Add grid helpers to wasm --- .../app/main/ui/workspace/viewport_wasm.cljs | 22 ++---- frontend/src/app/render_wasm/api.cljs | 15 ++++ render-wasm/src/main.rs | 18 ++++- render-wasm/src/render.rs | 16 +++- render-wasm/src/render/grid_layout.rs | 77 +++++++++++++++++++ render-wasm/src/render/surfaces.rs | 30 +++++--- render-wasm/src/render/ui.rs | 43 +++++++++++ render-wasm/src/shapes.rs | 47 +++++++---- render-wasm/src/shapes/modifiers.rs | 2 +- .../src/shapes/modifiers/grid_layout.rs | 52 +++++++------ render-wasm/src/state.rs | 5 ++ 11 files changed, 259 insertions(+), 68 deletions(-) create mode 100644 render-wasm/src/render/grid_layout.rs create mode 100644 render-wasm/src/render/ui.rs diff --git a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs index 8af9d6ab32..adfb7bf88c 100644 --- a/frontend/src/app/main/ui/workspace/viewport_wasm.cljs +++ b/frontend/src/app/main/ui/workspace/viewport_wasm.cljs @@ -13,7 +13,6 @@ [app.common.files.helpers :as cfh] [app.common.geom.shapes :as gsh] [app.common.types.shape :as cts] - [app.common.types.shape-tree :as ctt] [app.common.types.shape.layout :as ctl] [app.main.data.workspace.transforms :as dwt] [app.main.features :as features] @@ -329,6 +328,12 @@ (when (and @canvas-init? initialized?) (wasm.api/set-canvas-background background))) + (mf/with-effect [@canvas-init? hover-grid? @hover-top-frame-id] + (when @canvas-init? + (if hover-grid? + (wasm.api/show-grid @hover-top-frame-id) + (wasm.api/clear-grid)))) + (hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?) (hooks/setup-viewport-size vport viewport-ref) (hooks/setup-cursor cursor alt? mod? space? panning drawing-tool drawing-path? node-editing? z? read-only?) @@ -639,25 +644,14 @@ :zoom zoom}]) [:g.grid-layout-editor {:clipPath "url(#clip-handlers)"} - (when (or show-grid-editor? hover-grid?) + (when show-grid-editor? [:& grid-layout/editor {:zoom zoom :objects objects-modified :shape (or (get base-objects edition) (get base-objects @hover-top-frame-id)) - :view-only (not show-grid-editor?)}]) + :view-only (not show-grid-editor?)}])] - (for [frame (ctt/get-frames objects)] - (when (and (ctl/grid-layout? frame) - (empty? (:shapes frame)) - (not= edition (:id frame)) - (not= @hover-top-frame-id (:id frame))) - [:& grid-layout/editor - {:zoom zoom - :key (dm/str (:id frame)) - :objects objects-modified - :shape frame - :view-only true}]))] [:g.scrollbar-wrapper {:clipPath "url(#clip-handlers)"} [:& scroll-bars/viewport-scrollbars {:objects base-objects diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index c3c762c016..be87a809e9 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -995,6 +995,21 @@ ;; TODO: perform corresponding cleaning (h/call wasm/internal-module "_clean_up")) +(defn show-grid + [id] + (let [buffer (uuid/get-u32 id)] + (h/call wasm/internal-module "_show_grid" + (aget buffer 0) + (aget buffer 1) + (aget buffer 2) + (aget buffer 3))) + (request-render "show-grid")) + +(defn clear-grid + [] + (h/call wasm/internal-module "_hide_grid") + (request-render "clear-grid")) + (defonce module (delay (if (exists? js/dynamicImport) diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 1ceb638a05..5995dd047c 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -104,8 +104,7 @@ pub extern "C" fn render(_: i32) { #[no_mangle] pub extern "C" fn render_from_cache(_: i32) { with_state!(state, { - let render_state = state.render_state(); - render_state.render_from_cache(); + state.render_from_cache(); }); } @@ -691,6 +690,21 @@ pub extern "C" fn set_grid_cells() { mem::free_bytes(); } +#[no_mangle] +pub extern "C" fn show_grid(a: u32, b: u32, c: u32, d: u32) { + with_state!(state, { + let id = uuid_from_u32_quartet(a, b, c, d); + state.render_state.show_grid = Some(id); + }); +} + +#[no_mangle] +pub extern "C" fn hide_grid() { + with_state!(state, { + state.render_state.show_grid = None; + }); +} + fn main() { #[cfg(target_arch = "wasm32")] init_gl!(); diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 0769c2926c..70f8906ee0 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -3,12 +3,14 @@ mod debug; mod fills; mod fonts; mod gpu_state; +pub mod grid_layout; mod images; mod options; mod shadows; mod strokes; mod surfaces; mod text; +mod ui; use skia_safe::{self as skia, Matrix, RRect, Rect}; use std::borrow::Cow; @@ -104,6 +106,7 @@ pub(crate) struct RenderState { // can affect its child elements if they don't specify one themselves. If the planned // migration to remove group-level fills is completed, this code should be removed. pub nested_fills: Vec>, + pub show_grid: Option, } pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { @@ -170,6 +173,7 @@ impl RenderState { ), pending_tiles: PendingTiles::new_empty(), nested_fills: vec![], + show_grid: None, } } @@ -492,7 +496,12 @@ impl RenderState { } } - pub fn render_from_cache(&mut self) { + pub fn render_from_cache( + &mut self, + shapes: &HashMap, + modifiers: &HashMap, + structure: &HashMap>, + ) { let scale = self.get_cached_scale(); if let Some(snapshot) = &self.cached_target_snapshot { let canvas = self.surfaces.canvas(SurfaceId::Target); @@ -523,6 +532,10 @@ impl RenderState { canvas.clear(self.background_color); canvas.draw_image(snapshot, (0, 0), Some(&skia::Paint::default())); canvas.restore(); + + ui::render(self, shapes, modifiers, structure); + debug::render_wasm_label(self); + self.flush_and_submit(); } } @@ -947,6 +960,7 @@ impl RenderState { debug::render(self); } + ui::render(self, tree, modifiers, structure); debug::render_wasm_label(self); Ok(()) diff --git a/render-wasm/src/render/grid_layout.rs b/render-wasm/src/render/grid_layout.rs new file mode 100644 index 0000000000..c617854287 --- /dev/null +++ b/render-wasm/src/render/grid_layout.rs @@ -0,0 +1,77 @@ +use skia_safe::{self as skia}; +use std::collections::HashMap; + +use crate::math::{Bounds, Matrix, Rect}; +use crate::shapes::modifiers::grid_layout::{calculate_tracks, create_cell_data}; +use crate::shapes::{modified_children_ids, Frame, Layout, Shape, StructureEntry, Type}; +use crate::uuid::Uuid; + +pub fn render_overlay( + zoom: f32, + canvas: &skia::Canvas, + shape: &Shape, + shapes: &HashMap, + modifiers: &HashMap, + structure: &HashMap>, +) { + let Type::Frame(Frame { + layout: Some(Layout::GridLayout(layout_data, grid_data)), + .. + }) = &shape.shape_type + else { + return; + }; + + let bounds = &HashMap::::new(); + + let shape = &mut shape.clone(); + if let Some(modifiers) = modifiers.get(&shape.id) { + shape.apply_transform(modifiers); + } + + let layout_bounds = shape.bounds(); + let children = modified_children_ids(shape, structure.get(&shape.id)); + + let column_tracks = calculate_tracks( + true, + shape, + layout_data, + grid_data, + &layout_bounds, + &grid_data.cells, + shapes, + bounds, + ); + + let row_tracks = calculate_tracks( + false, + shape, + layout_data, + grid_data, + &layout_bounds, + &grid_data.cells, + shapes, + bounds, + ); + + let cells = create_cell_data( + &layout_bounds, + &children, + shapes, + &grid_data.cells, + &column_tracks, + &row_tracks, + true, + ); + + let mut paint = skia::Paint::default(); + paint.set_style(skia::PaintStyle::Stroke); + paint.set_color(skia::Color::from_rgb(255, 111, 224)); + + paint.set_stroke_width(1.0 / zoom); + + for cell in cells.iter() { + let rect = Rect::from_xywh(cell.anchor.x, cell.anchor.y, cell.width, cell.height); + canvas.draw_rect(rect, &paint); + } +} diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index ef479d9d9b..a3188f4860 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -17,14 +17,15 @@ const TILE_SIZE_MULTIPLIER: i32 = 2; #[repr(u32)] #[derive(Debug, PartialEq, Clone, Copy)] pub enum SurfaceId { - Target = 0b0000_0001, - Cache = 0b0000_0010, - Current = 0b0000_0100, - Fills = 0b0000_1000, - Strokes = 0b0001_0000, - DropShadows = 0b0010_0000, - InnerShadows = 0b0100_0000, - Debug = 0b1000_0000, + Target = 0b0_0000_0001, + Cache = 0b0_0000_0010, + Current = 0b0_0000_0100, + Fills = 0b0_0000_1000, + Strokes = 0b0_0001_0000, + DropShadows = 0b0_0010_0000, + InnerShadows = 0b0_0100_0000, + UI = 0b0_1000_0000, + Debug = 0b1_0000_0000, } pub struct Surfaces { @@ -39,8 +40,10 @@ pub struct Surfaces { shape_strokes: skia::Surface, // used for rendering shadows drop_shadows: skia::Surface, - // used fo rendering over shadows. + // used for rendering over shadows. inner_shadows: skia::Surface, + // used for displaying auxiliary workspace elements + ui: skia::Surface, // for drawing debug info. debug: skia::Surface, // for drawing tiles. @@ -74,6 +77,8 @@ impl Surfaces { gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims); let shape_strokes = gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims); + + let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height); let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height); let tiles = TileTextureCache::new(); @@ -85,6 +90,7 @@ impl Surfaces { inner_shadows, shape_fills, shape_strokes, + ui, debug, tiles, sampling_options, @@ -198,6 +204,7 @@ impl Surfaces { SurfaceId::Fills => &mut self.shape_fills, SurfaceId::Strokes => &mut self.shape_strokes, SurfaceId::Debug => &mut self.debug, + SurfaceId::UI => &mut self.ui, } } @@ -205,6 +212,7 @@ impl Surfaces { let dim = (target.width(), target.height()); self.target = target; self.debug = self.target.new_surface_with_dimensions(dim).unwrap(); + self.ui = self.target.new_surface_with_dimensions(dim).unwrap(); // The rest are tile size surfaces } @@ -261,6 +269,10 @@ impl Surfaces { self.canvas(SurfaceId::Debug) .clear(skia::Color::TRANSPARENT) .reset_matrix(); + + self.canvas(SurfaceId::UI) + .clear(skia::Color::TRANSPARENT) + .reset_matrix(); } pub fn cache_current_tile_texture( diff --git a/render-wasm/src/render/ui.rs b/render-wasm/src/render/ui.rs new file mode 100644 index 0000000000..dd7ef9b811 --- /dev/null +++ b/render-wasm/src/render/ui.rs @@ -0,0 +1,43 @@ +use skia_safe::{self as skia, Color4f}; +use std::collections::HashMap; + +use crate::math::Matrix; +use crate::render::grid_layout; +use crate::shapes::{Shape, StructureEntry}; +use crate::uuid::Uuid; + +use super::{RenderState, SurfaceId}; + +pub fn render( + render_state: &mut RenderState, + shapes: &HashMap, + modifiers: &HashMap, + structure: &HashMap>, +) { + let canvas = render_state.surfaces.canvas(SurfaceId::UI); + + canvas.clear(Color4f::new(0.0, 0.0, 0.0, 0.0)); + canvas.save(); + + let viewbox = render_state.viewbox; + let zoom = viewbox.zoom * render_state.options.dpr(); + + canvas.scale((zoom, zoom)); + + canvas.translate((-viewbox.area.left, -viewbox.area.top)); + + let canvas = render_state.surfaces.canvas(SurfaceId::UI); + + if let Some(id) = render_state.show_grid { + if let Some(shape) = shapes.get(&id) { + grid_layout::render_overlay(zoom, canvas, shape, shapes, modifiers, structure); + } + } + + canvas.restore(); + render_state.surfaces.draw_into( + SurfaceId::UI, + SurfaceId::Target, + Some(&skia::Paint::default()), + ); +} diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 91bf209d85..e00257e577 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -14,7 +14,7 @@ mod fonts; mod frames; mod groups; mod layouts; -mod modifiers; +pub mod modifiers; mod paths; mod rects; mod shadows; @@ -424,22 +424,35 @@ impl Shape { padding_left: f32, ) { if let Type::Frame(data) = &mut self.shape_type { - let layout_data = LayoutData { - align_items, - align_content, - justify_items, - justify_content, - padding_top, - padding_right, - padding_bottom, - padding_left, - row_gap, - column_gap, - }; - - let mut grid_data = GridData::default(); - grid_data.direction = direction; - data.layout = Some(Layout::GridLayout(layout_data, grid_data)); + if let Some(Layout::GridLayout(layout_data, grid_data)) = &mut data.layout { + layout_data.align_items = align_items; + layout_data.align_content = align_content; + layout_data.justify_items = justify_items; + layout_data.justify_content = justify_content; + layout_data.padding_top = padding_top; + layout_data.padding_right = padding_right; + layout_data.padding_bottom = padding_bottom; + layout_data.padding_left = padding_left; + layout_data.row_gap = row_gap; + layout_data.column_gap = column_gap; + grid_data.direction = direction; + } else { + let layout_data = LayoutData { + align_items, + align_content, + justify_items, + justify_content, + padding_top, + padding_right, + padding_bottom, + padding_left, + row_gap, + column_gap, + }; + let mut grid_data = GridData::default(); + grid_data.direction = direction; + data.layout = Some(Layout::GridLayout(layout_data, grid_data)); + } } } diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index bfd1cba473..eaf5bbd220 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; mod common; mod constraints; mod flex_layout; -mod grid_layout; +pub mod grid_layout; use common::GetBounds; diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 04fd59baf8..5ac85911d9 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -14,28 +14,28 @@ const MIN_SIZE: f32 = 0.01; const MAX_SIZE: f32 = f32::INFINITY; #[derive(Debug)] -struct CellData<'a> { - shape: &'a Shape, - anchor: Point, - width: f32, - height: f32, - align_self: Option, - justify_self: Option, +pub struct CellData<'a> { + pub shape: Option<&'a Shape>, + pub anchor: Point, + pub width: f32, + pub height: f32, + pub align_self: Option, + pub justify_self: Option, } #[derive(Debug)] -struct TrackData { - track_type: GridTrackType, - value: f32, - size: f32, - max_size: f32, - anchor_start: Point, - anchor_end: Point, +pub struct TrackData { + pub track_type: GridTrackType, + pub value: f32, + pub size: f32, + pub max_size: f32, + pub anchor_start: Point, + pub anchor_end: Point, } // FIXME: We might be able to simplify these arguments #[allow(clippy::too_many_arguments)] -fn calculate_tracks( +pub fn calculate_tracks( is_column: bool, shape: &Shape, layout_data: &LayoutData, @@ -513,29 +513,32 @@ fn cell_bounds( Some(Bounds::new(nw, ne, se, sw)) } -fn create_cell_data<'a>( +pub fn create_cell_data<'a>( layout_bounds: &Bounds, children: &IndexSet, shapes: &'a HashMap, cells: &Vec, column_tracks: &[TrackData], row_tracks: &[TrackData], + allow_empty: bool, ) -> Vec> { let mut result = Vec::>::new(); for cell in cells { - let Some(shape_id) = cell.shape else { - continue; + let shape: Option<&Shape> = if let Some(shape_id) = cell.shape { + if !children.contains(&shape_id) { + None + } else { + shapes.get(&shape_id).map(|v| &**v) + } + } else { + None }; - if !children.contains(&shape_id) { + if !allow_empty && shape.is_none() { continue; } - let Some(shape) = shapes.get(&shape_id) else { - continue; - }; - let column_start = (cell.column - 1) as usize; let column_end = (cell.column + cell.column_span - 2) as usize; let row_start = (cell.row - 1) as usize; @@ -662,10 +665,11 @@ pub fn reflow_grid_layout( &grid_data.cells, &column_tracks, &row_tracks, + false, ); for cell in cells.iter() { - let child = cell.shape; + let Some(child) = cell.shape else { continue }; let child_bounds = bounds.find(child); let mut new_width = child_bounds.width(); diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 4607bea8e9..d9aaab2d7a 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -108,6 +108,11 @@ impl<'a> State<'a> { &mut self.render_state } + pub fn render_from_cache(&mut self) { + self.render_state + .render_from_cache(&self.shapes, &self.modifiers, &self.structure); + } + pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> { self.render_state.start_render_loop( &mut self.shapes,