diff --git a/frontend/src/app/main/data/workspace/modifiers.cljs b/frontend/src/app/main/data/workspace/modifiers.cljs index a5fb08e92..b7ade7f6c 100644 --- a/frontend/src/app/main/data/workspace/modifiers.cljs +++ b/frontend/src/app/main/data/workspace/modifiers.cljs @@ -456,9 +456,9 @@ (mapcat (fn [[parent-id data]] (when (ctm/has-structure? (:modifiers data)) - (->> data - :modifiers - :structure-parent + (->> (concat + (get-in data [:modifiers :structure-parent]) + (get-in data [:modifiers :structure-child])) (mapcat (fn [modifier] (case (:type modifier) @@ -468,7 +468,8 @@ {:type :remove-children :parent parent-id :id child-id - :index 0}))) + :index 0 + :value 0}))) :add-children (->> (:value modifier) @@ -476,7 +477,15 @@ {:type :add-children :parent parent-id :id child-id - :index (:index modifier)}))) + :index (:index modifier) + :value 0}))) + + :scale-content + [{:type :scale-content + :parent parent-id + :id parent-id + :index 0 + :value (:value modifier)}] nil))))))) modif-tree)) @@ -554,6 +563,29 @@ (rx/of (set-temporary-selrect selrect) (set-temporary-modifiers modifiers))))))))) +(defn propagate-structure-modifiers + [modif-tree objects] + (letfn [(propagate-children + [modif-tree parent-id modifiers] + (let [new-modifiers (ctm/select-child-structre-modifiers modifiers)] + (->> (get-in objects [parent-id :shapes]) + (reduce + #(update-in %1 [%2 :modifiers] ctm/add-modifiers new-modifiers) + modif-tree))))] + (loop [pending (into [] (keys modif-tree)) + modif-tree modif-tree] + (if-let [next (first pending)] + (let [pending (rest pending) + modifiers (get-in modif-tree [next :modifiers]) + + [pending modif-tree] + (if (ctm/has-structure-child? modifiers) + [(into pending (get-in objects [next :shapes])) + (propagate-children modif-tree next modifiers)] + [pending modif-tree])] + (recur pending modif-tree)) + modif-tree)))) + #_:clj-kondo/ignore (defn apply-wasm-modifiers [modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis undo-group] @@ -574,6 +606,9 @@ (map (fn [{:keys [id transform]}] [id transform])) (wasm.api/propagate-modifiers geometry-entries snap-pixel?)) + modif-tree + (propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state)) + ids (into (set (keys modif-tree)) (keys transforms)) @@ -585,7 +620,6 @@ (-> shape (gsh/apply-transform transform) (ctm/apply-structure-modifiers modifiers))))] - (rx/of (clear-local-transform) (dwsh/update-shapes ids update-shape)))))) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 8a28f1913..e89ba466d 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -793,17 +793,19 @@ (defn set-structure-modifiers [entries] (when-not (empty? entries) - (let [offset (mem/alloc-bytes-32 (mem/get-list-size entries 40)) - heapu32 (mem/get-heap-u32)] + (let [offset (mem/alloc-bytes-32 (mem/get-list-size entries 44)) + heapu32 (mem/get-heap-u32) + heapf32 (mem/get-heap-f32)] (loop [entries (seq entries) current-offset offset] (when-not (empty? entries) - (let [{:keys [type parent id index] :as entry} (first entries)] + (let [{:keys [type parent id index value] :as entry} (first entries)] (sr/heapu32-set-u32 (sr/translate-structure-modifier-type type) heapu32 (+ current-offset 0)) (sr/heapu32-set-u32 (or index 0) heapu32 (+ current-offset 1)) (sr/heapu32-set-uuid parent heapu32 (+ current-offset 2)) (sr/heapu32-set-uuid id heapu32 (+ current-offset 6)) - (recur (rest entries) (+ current-offset 10))))) + (aset heapf32 (+ current-offset 10) value) + (recur (rest entries) (+ current-offset 11))))) (h/call wasm/internal-module "_set_structure_modifiers")))) (defn propagate-modifiers diff --git a/frontend/src/app/render_wasm/serializers.cljs b/frontend/src/app/render_wasm/serializers.cljs index a7989c81b..f7dd449e0 100644 --- a/frontend/src/app/render_wasm/serializers.cljs +++ b/frontend/src/app/render_wasm/serializers.cljs @@ -281,7 +281,8 @@ [type] (case type :remove-children 1 - :add-children 2)) + :add-children 2 + :scale-content 3)) (defn translate-grow-type [grow-type] diff --git a/frontend/src/app/render_wasm/shape.cljs b/frontend/src/app/render_wasm/shape.cljs index 7c879b9c8..d6e5483cb 100644 --- a/frontend/src/app/render_wasm/shape.cljs +++ b/frontend/src/app/render_wasm/shape.cljs @@ -6,6 +6,7 @@ (ns app.render-wasm.shape (:require + [app.common.data.macros :as dm] [app.common.transit :as t] [app.common.types.shape :as shape] [app.common.types.shape.layout :as ctl] @@ -132,6 +133,12 @@ :constraints-h (api/set-constraints-h v) :constraints-v (api/set-constraints-v v) + (:r1 :r2 :r3 :r4) + (api/set-shape-corners [(dm/get-prop shape :r1) + (dm/get-prop shape :r2) + (dm/get-prop shape :r3) + (dm/get-prop shape :r4)]) + :svg-attrs (when (= (:type shape) :path) (api/set-shape-path-attrs v)) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index a68c5f9b8..92b383b1a 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5115,9 +5115,14 @@ msgstr "File" msgid "workspace.header.menu.option.help-info" msgstr "Help & info" +#: src/app/main/ui/workspace/main_menu.cljs:881 +msgid "workspace.header.menu.option.preferences" +msgstr "Preferences" + #: src/app/main/ui/workspace/main_menu.cljs:916 msgid "workspace.header.menu.option.power-up" msgstr "Power up your plan" + msgid "workspace.header.menu.option.view" msgstr "View" diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 6eeac81e5..1ceb638a0 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -17,7 +17,9 @@ mod wasm; use indexmap::IndexSet; use math::{Bounds, Matrix}; use mem::SerializableResult; -use shapes::{BoolType, ConstraintH, ConstraintV, StructureEntry, TransformEntry, Type}; +use shapes::{ + BoolType, ConstraintH, ConstraintV, StructureEntry, StructureEntryType, TransformEntry, Type, +}; use skia_safe as skia; use state::State; use utils::uuid_from_u32_quartet; @@ -424,18 +426,30 @@ pub extern "C" fn set_structure_modifiers() { let bytes = mem::bytes(); let entries: Vec<_> = bytes - .chunks(40) + .chunks(44) .map(|data| StructureEntry::from_bytes(data.try_into().unwrap())) .collect(); with_state!(state, { for entry in entries { - state.structure.entry(entry.parent).or_insert_with(Vec::new); - state - .structure - .get_mut(&entry.parent) - .expect("Parent not found for entry") - .push(entry); + match entry.entry_type { + StructureEntryType::ScaleContent => { + let Some(shape) = state.shapes.get(&entry.id) else { + continue; + }; + for id in shape.all_children_with_self(&state.shapes) { + state.scale_content.insert(id, entry.value); + } + } + _ => { + state.structure.entry(entry.parent).or_insert_with(Vec::new); + state + .structure + .get_mut(&entry.parent) + .expect("Parent not found for entry") + .push(entry); + } + } } }); @@ -446,6 +460,7 @@ pub extern "C" fn set_structure_modifiers() { pub extern "C" fn clean_modifiers() { with_state!(state, { state.structure.clear(); + state.scale_content.clear(); state.modifiers.clear(); }); } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 5d9709e9d..92a81d9e2 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -327,10 +327,17 @@ impl RenderState { pub fn render_shape( &mut self, - shape: &mut Shape, + shape: &Shape, modifiers: Option<&Matrix>, + scale_content: Option<&f32>, clip_bounds: Option<(Rect, Option, Matrix)>, ) { + let shape = if let Some(scale_content) = scale_content { + &shape.scale_content(*scale_content) + } else { + shape + }; + let surface_ids = &[ SurfaceId::Fills, SurfaceId::Strokes, @@ -542,6 +549,7 @@ impl RenderState { tree: &mut HashMap, modifiers: &HashMap, structure: &HashMap>, + scale_content: &HashMap, timestamp: i32, ) -> Result<(), String> { let scale = self.get_scale(); @@ -585,7 +593,7 @@ impl RenderState { self.current_tile = None; self.render_in_progress = true; self.apply_drawing_to_render_canvas(None); - self.process_animation_frame(tree, modifiers, structure, timestamp)?; + self.process_animation_frame(tree, modifiers, structure, scale_content, timestamp)?; performance::end_measure!("start_render_loop"); Ok(()) } @@ -595,11 +603,12 @@ impl RenderState { tree: &mut HashMap, modifiers: &HashMap, structure: &HashMap>, + scale_content: &HashMap, timestamp: i32, ) -> Result<(), String> { performance::begin_measure!("process_animation_frame"); if self.render_in_progress { - self.render_shape_tree(tree, modifiers, structure, timestamp)?; + self.render_shape_tree(tree, modifiers, structure, scale_content, timestamp)?; self.flush_and_submit(); if self.render_in_progress { @@ -709,6 +718,7 @@ impl RenderState { tree: &mut HashMap, modifiers: &HashMap, structure: &HashMap>, + scale_content: &HashMap, timestamp: i32, ) -> Result<(), String> { if !self.render_in_progress { @@ -815,7 +825,12 @@ impl RenderState { self.render_shape_enter(element, mask); if !node_render_state.id.is_nil() { - self.render_shape(element, modifiers.get(&element.id), clip_bounds); + self.render_shape( + element, + modifiers.get(&element.id), + scale_content.get(&element.id), + clip_bounds, + ); } else { self.apply_drawing_to_render_canvas(Some(element)); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 7db114109..614b8515a 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -4,6 +4,7 @@ use crate::render::BlendMode; use crate::uuid::Uuid; use std::cell::OnceCell; use std::collections::{HashMap, HashSet}; +use std::iter::once; mod blurs; mod bools; @@ -108,6 +109,29 @@ impl Type { _ => None, } } + + pub fn scale_content(&mut self, value: f32) { + match self { + Type::Rect(Rect { + corners: Some(corners), + .. + }) => { + corners::scale_corners(corners, value); + } + Type::Frame(Frame { corners, layout }) => { + if let Some(corners) = corners { + corners::scale_corners(corners, value); + } + if let Some(layout) = layout { + layout.scale_content(value); + } + } + Type::Text(TextContent { paragraphs, .. }) => { + paragraphs.iter_mut().for_each(|p| p.scale_content(value)); + } + _ => {} + } + } } #[derive(Debug, Clone, PartialEq, Copy)] @@ -208,6 +232,25 @@ impl Shape { } } + pub fn scale_content(&self, value: f32) -> Self { + let mut result = self.clone(); + result.shape_type.scale_content(value); + result + .strokes + .iter_mut() + .for_each(|s| s.scale_content(value)); + result + .shadows + .iter_mut() + .for_each(|s| s.scale_content(value)); + result.blur.scale_content(value); + result + .layout_item + .iter_mut() + .for_each(|i| i.scale_content(value)); + result + } + fn invalidate_extrect(&mut self) { self.extrect = OnceCell::new(); } @@ -364,7 +407,7 @@ impl Shape { } } - // FIXME: These arguments could be grouped or simplified + // FIXME: These argumoents could be grouped or simplified #[allow(clippy::too_many_arguments)] pub fn set_grid_layout_data( &mut self, @@ -687,6 +730,17 @@ impl Shape { } } + pub fn all_children_with_self(&self, shapes: &HashMap) -> IndexSet { + once(self.id) + .chain(self.children_ids().into_iter().flat_map(|id| { + shapes + .get(&id) + .map(|s| s.all_children_with_self(shapes)) + .unwrap_or_default() + })) + .collect() + } + pub fn image_filter(&self, scale: f32) -> Option { if !self.blur.hidden { match self.blur.blur_type { @@ -873,6 +927,7 @@ pub fn modified_children_ids( StructureEntryType::RemoveChild => { to_remove.insert(&st.id); } + _ => {} } } diff --git a/render-wasm/src/shapes/blurs.rs b/render-wasm/src/shapes/blurs.rs index 7bc857cf0..36d3b588f 100644 --- a/render-wasm/src/shapes/blurs.rs +++ b/render-wasm/src/shapes/blurs.rs @@ -35,4 +35,8 @@ impl Blur { value, } } + + pub fn scale_content(&mut self, value: f32) { + self.value *= value; + } } diff --git a/render-wasm/src/shapes/corners.rs b/render-wasm/src/shapes/corners.rs index 91f3508b3..e4b530765 100644 --- a/render-wasm/src/shapes/corners.rs +++ b/render-wasm/src/shapes/corners.rs @@ -21,3 +21,14 @@ pub fn make_corners(raw_corners: (f32, f32, f32, f32)) -> Option { ]) } } + +pub fn scale_corners(corners: &mut Corners, value: f32) { + corners[0].x *= value; + corners[0].y *= value; + corners[1].x *= value; + corners[1].y *= value; + corners[2].x *= value; + corners[2].y *= value; + corners[3].x *= value; + corners[3].y *= value; +} diff --git a/render-wasm/src/shapes/layouts.rs b/render-wasm/src/shapes/layouts.rs index fd9fc9a84..1019ee79d 100644 --- a/render-wasm/src/shapes/layouts.rs +++ b/render-wasm/src/shapes/layouts.rs @@ -8,6 +8,20 @@ pub enum Layout { GridLayout(LayoutData, GridData), } +impl Layout { + pub fn scale_content(&mut self, value: f32) { + match self { + Layout::FlexLayout(layout_data, _) => { + layout_data.scale_content(value); + } + Layout::GridLayout(layout_data, grid_data) => { + layout_data.scale_content(value); + grid_data.scale_content(value); + } + } + } +} + #[derive(Debug, Clone, PartialEq)] pub enum FlexDirection { Row, @@ -184,6 +198,12 @@ impl GridTrack { value: f32::from_le_bytes(raw.value), } } + + pub fn scale_content(&mut self, value: f32) { + if self.track_type == GridTrackType::Fixed { + self.value *= value; + } + } } #[derive(Debug, Clone, PartialEq)] @@ -260,6 +280,17 @@ pub struct LayoutData { pub column_gap: f32, } +impl LayoutData { + pub fn scale_content(&mut self, value: f32) { + self.padding_top *= value; + self.padding_right *= value; + self.padding_bottom *= value; + self.padding_left *= value; + self.row_gap *= value; + self.column_gap *= value; + } +} + #[derive(Debug, Copy, Clone, PartialEq)] pub enum AlignSelf { Auto, @@ -324,9 +355,7 @@ impl FlexData { FlexDirection::RowReverse | FlexDirection::Row ) } -} -impl FlexData { pub fn is_wrap(&self) -> bool { matches!(self.wrap_type, WrapType::Wrap) } @@ -349,6 +378,11 @@ impl GridData { cells: vec![], } } + + pub fn scale_content(&mut self, value: f32) { + self.rows.iter_mut().for_each(|t| t.scale_content(value)); + self.columns.iter_mut().for_each(|t| t.scale_content(value)); + } } #[derive(Debug)] @@ -424,3 +458,16 @@ pub struct LayoutItem { pub z_index: i32, pub align_self: Option, } + +impl LayoutItem { + pub fn scale_content(&mut self, value: f32) { + self.margin_top *= value; + self.margin_right *= value; + self.margin_bottom *= value; + self.margin_left *= value; + self.max_h = self.max_h.map(|max_h| max_h * value); + self.min_h = self.max_h.map(|max_h| max_h * value); + self.max_w = self.max_h.map(|max_h| max_h * value); + self.min_w = self.max_h.map(|max_h| max_h * value); + } +} diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 4039c8581..bfd1cba47 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -14,6 +14,7 @@ use crate::shapes::{ use crate::state::State; use crate::uuid::Uuid; +#[allow(clippy::too_many_arguments)] fn propagate_children( shape: &Shape, shapes: &HashMap, @@ -22,6 +23,7 @@ fn propagate_children( transform: Matrix, bounds: &HashMap, structure: &HashMap>, + scale_content: &HashMap, ) -> VecDeque { let children_ids = modified_children_ids(shape, structure.get(&shape.id)); @@ -36,6 +38,8 @@ fn propagate_children( continue; }; + let ignore_constraints = scale_content.contains_key(child_id); + let child_bounds = bounds.find(child); let constraint_h = match &shape.shape_type { @@ -73,6 +77,7 @@ fn propagate_children( constraint_h, constraint_v, transform, + ignore_constraints, ); result.push_back(Modifier::transform(*child_id, transform)); @@ -200,6 +205,7 @@ pub fn propagate_modifiers( transform, &bounds, &state.structure, + &state.scale_content, ); entries.append(&mut children); } @@ -309,6 +315,12 @@ pub fn propagate_modifiers( continue; }; + let shape = if let Some(scale_content) = state.scale_content.get(id) { + &shape.scale_content(*scale_content) + } else { + shape + }; + let Type::Frame(frame_data) = &shape.shape_type else { continue; }; @@ -388,6 +400,7 @@ mod tests { transform, &HashMap::new(), &HashMap::new(), + &HashMap::new(), ); assert_eq!(result.len(), 1); diff --git a/render-wasm/src/shapes/modifiers/constraints.rs b/render-wasm/src/shapes/modifiers/constraints.rs index f38b5daf8..c76420100 100644 --- a/render-wasm/src/shapes/modifiers/constraints.rs +++ b/render-wasm/src/shapes/modifiers/constraints.rs @@ -104,10 +104,12 @@ pub fn propagate_shape_constraints( constraint_h: ConstraintH, constraint_v: ConstraintV, transform: Matrix, + ignore_constrainst: bool, ) -> Matrix { // if the constrains are scale & scale or the transform has only moves we // can propagate as is - if (constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale) + if (ignore_constrainst + || constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale) || transform.is_translate() { return transform; diff --git a/render-wasm/src/shapes/shadows.rs b/render-wasm/src/shapes/shadows.rs index a8f226c41..324368182 100644 --- a/render-wasm/src/shapes/shadows.rs +++ b/render-wasm/src/shapes/shadows.rs @@ -125,4 +125,11 @@ impl Shadow { filter } + + pub fn scale_content(&mut self, value: f32) { + self.blur *= value; + self.spread *= value; + self.offset.0 *= value; + self.offset.1 *= value; + } } diff --git a/render-wasm/src/shapes/strokes.rs b/render-wasm/src/shapes/strokes.rs index e389b627c..2336e0617 100644 --- a/render-wasm/src/shapes/strokes.rs +++ b/render-wasm/src/shapes/strokes.rs @@ -110,6 +110,10 @@ impl Stroke { } } + pub fn scale_content(&mut self, value: f32) { + self.width *= value; + } + pub fn delta(&self) -> f32 { match self.kind { StrokeKind::Inner => 0., diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index e63b1df29..abffc902f 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -34,9 +34,9 @@ impl GrowType { #[derive(Debug, PartialEq, Clone)] pub struct TextContent { - paragraphs: Vec, - bounds: Rect, - grow_type: GrowType, + pub paragraphs: Vec, + pub bounds: Rect, + pub grow_type: GrowType, } pub fn set_paragraphs_width(width: f32, paragraphs: &mut Vec>) { @@ -284,6 +284,13 @@ impl Paragraph { }); style } + + pub fn scale_content(&mut self, value: f32) { + self.letter_spacing *= value; + self.children + .iter_mut() + .for_each(|l| l.scale_content(value)); + } } #[derive(Debug, PartialEq, Clone)] @@ -388,6 +395,10 @@ impl TextLeaf { _ => self.text.clone(), } } + + pub fn scale_content(&mut self, value: f32) { + self.font_size *= value; + } } const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::(); diff --git a/render-wasm/src/shapes/transform.rs b/render-wasm/src/shapes/transform.rs index 7c7be13ec..7abf938b3 100644 --- a/render-wasm/src/shapes/transform.rs +++ b/render-wasm/src/shapes/transform.rs @@ -102,6 +102,7 @@ impl SerializableResult for TransformEntry { pub enum StructureEntryType { RemoveChild, AddChild, + ScaleContent, } impl StructureEntryType { @@ -109,6 +110,7 @@ impl StructureEntryType { match value { 1 => Self::RemoveChild, 2 => Self::AddChild, + 3 => Self::ScaleContent, _ => unreachable!(), } } @@ -121,19 +123,27 @@ pub struct StructureEntry { pub index: u32, pub parent: Uuid, pub id: Uuid, + pub value: f32, } impl StructureEntry { - pub fn new(entry_type: StructureEntryType, index: u32, parent: Uuid, id: Uuid) -> Self { + pub fn new( + entry_type: StructureEntryType, + index: u32, + parent: Uuid, + id: Uuid, + value: f32, + ) -> Self { StructureEntry { entry_type, index, parent, id, + value, } } - pub fn from_bytes(bytes: [u8; 40]) -> Self { + pub fn from_bytes(bytes: [u8; 44]) -> Self { let entry_type = StructureEntryType::from_u32(u32::from_le_bytes([ bytes[0], bytes[1], bytes[2], bytes[3], ])); @@ -154,7 +164,9 @@ impl StructureEntry { u32::from_le_bytes([bytes[36], bytes[37], bytes[38], bytes[39]]), ); - StructureEntry::new(entry_type, index, parent, id) + let value = f32::from_le_bytes([bytes[40], bytes[41], bytes[42], bytes[43]]); + + StructureEntry::new(entry_type, index, parent, id, value) } } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 4392c4d56..94f8f9973 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -69,6 +69,7 @@ pub(crate) struct State<'a> { pub current_shape: Option<&'a mut Shape>, pub shapes: HashMap, pub modifiers: HashMap, + pub scale_content: HashMap, pub structure: HashMap>, pub shapes_pool: ShapesPool, } @@ -81,6 +82,7 @@ impl<'a> State<'a> { current_shape: None, shapes: HashMap::with_capacity(capacity), modifiers: HashMap::new(), + scale_content: HashMap::new(), structure: HashMap::new(), shapes_pool: ShapesPool::new(), } @@ -99,6 +101,7 @@ impl<'a> State<'a> { &mut self.shapes, &self.modifiers, &self.structure, + &self.scale_content, timestamp, )?; Ok(()) @@ -109,6 +112,7 @@ impl<'a> State<'a> { &mut self.shapes, &self.modifiers, &self.structure, + &self.scale_content, timestamp, )?; Ok(())