From 0597eef75059cad58fd320c4487390455670c6ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 18 Mar 2026 12:44:10 +0100 Subject: [PATCH 01/22] :sparkles: Show/hide wasm info label via config flag --- common/src/app/common/flags.cljc | 2 ++ frontend/src/app/render_wasm/api.cljs | 4 +++- render-wasm/src/options.rs | 3 ++- render-wasm/src/render/debug.rs | 4 ++++ render-wasm/src/render/options.rs | 6 +++++- 5 files changed, 16 insertions(+), 3 deletions(-) diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 23ef653592..d5e80d4381 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -136,6 +136,8 @@ :webhooks ;; TODO: deprecate this flag and consolidate the code :render-wasm-dpr + ;; Show WASM renderer info label (hidden by default). + :render-wasm-info :hide-release-modal :subscriptions :subscriptions-old diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index e4d097cc06..ddbd3d0085 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1401,7 +1401,9 @@ (dbg/enabled? :wasm-viewbox) (bit-or 2r00000000000000000000000000000001) (text-editor-wasm?) - (bit-or 2r00000000000000000000000000001000))) + (bit-or 2r00000000000000000000000000001000) + (contains? cf/flags :render-wasm-info) + (bit-or 2r00000000000000000000000000010000))) (defn set-canvas-size [canvas] diff --git a/render-wasm/src/options.rs b/render-wasm/src/options.rs index beeeec5a30..56fa7b0ae1 100644 --- a/render-wasm/src/options.rs +++ b/render-wasm/src/options.rs @@ -1,4 +1,5 @@ pub const DEBUG_VISIBLE: u32 = 0x01; pub const PROFILE_REBUILD_TILES: u32 = 0x02; pub const FAST_MODE: u32 = 0x04; -pub const INFO_TEXT: u32 = 0x08; +pub const TEXT_EDITOR_V3: u32 = 0x08; +pub const SHOW_WASM_INFO: u32 = 0x10; diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 41f68a663e..9bfa690441 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -41,6 +41,10 @@ pub fn render_debug_cache_surface(render_state: &mut RenderState) { } pub fn render_wasm_label(render_state: &mut RenderState) { + if !render_state.options.show_wasm_info() { + return; + } + let canvas = render_state.surfaces.canvas(SurfaceId::Target); let skia::ISize { width, height } = canvas.base_layer_size(); let mut paint = skia::Paint::default(); diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index d6f1d1396f..dfa8f7ec86 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -33,6 +33,10 @@ impl RenderOptions { } pub fn show_info_text(&self) -> bool { - self.flags & options::INFO_TEXT == options::INFO_TEXT + self.flags & options::TEXT_EDITOR_V3 == options::TEXT_EDITOR_V3 + } + + pub fn show_wasm_info(&self) -> bool { + self.flags & options::SHOW_WASM_INFO == options::SHOW_WASM_INFO } } From 619842152d62cd29d2a4387a8a18802e2c6e5352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 18 Mar 2026 13:16:13 +0100 Subject: [PATCH 02/22] :recycle: Refactor render options (wasm) --- frontend/src/app/render_wasm/api.cljs | 4 ++-- render-wasm/src/main.rs | 1 - render-wasm/src/options.rs | 5 ----- render-wasm/src/render/debug.rs | 2 +- render-wasm/src/render/options.rs | 25 +++++++++++++------------ 5 files changed, 16 insertions(+), 21 deletions(-) delete mode 100644 render-wasm/src/options.rs diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index ddbd3d0085..04546d9ef1 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -1401,9 +1401,9 @@ (dbg/enabled? :wasm-viewbox) (bit-or 2r00000000000000000000000000000001) (text-editor-wasm?) - (bit-or 2r00000000000000000000000000001000) + (bit-or 2r00000000000000000000000000000100) (contains? cf/flags :render-wasm-info) - (bit-or 2r00000000000000000000000000010000))) + (bit-or 2r00000000000000000000000000001000))) (defn set-canvas-size [canvas] diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 6e519cc249..ed0ba87f10 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -3,7 +3,6 @@ mod emscripten; mod error; mod math; mod mem; -mod options; mod performance; mod render; mod shapes; diff --git a/render-wasm/src/options.rs b/render-wasm/src/options.rs deleted file mode 100644 index 56fa7b0ae1..0000000000 --- a/render-wasm/src/options.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub const DEBUG_VISIBLE: u32 = 0x01; -pub const PROFILE_REBUILD_TILES: u32 = 0x02; -pub const FAST_MODE: u32 = 0x04; -pub const TEXT_EDITOR_V3: u32 = 0x08; -pub const SHOW_WASM_INFO: u32 = 0x10; diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 9bfa690441..4ff244020b 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -61,7 +61,7 @@ pub fn render_wasm_label(render_state: &mut RenderState) { let debug_font = render_state.fonts.debug_font(); canvas.draw_str(str, p, debug_font, &paint); - if render_state.options.show_info_text() { + if render_state.options.is_text_editor_v3() { str = "TEXT EDITOR / V3"; let (scalar, _) = render_state.fonts.debug_font().measure_str(str, None); diff --git a/render-wasm/src/render/options.rs b/render-wasm/src/render/options.rs index dfa8f7ec86..9505d0b254 100644 --- a/render-wasm/src/render/options.rs +++ b/render-wasm/src/render/options.rs @@ -1,42 +1,43 @@ -use crate::options; +// Render options flags +const DEBUG_VISIBLE: u32 = 0x01; +const PROFILE_REBUILD_TILES: u32 = 0x02; +const TEXT_EDITOR_V3: u32 = 0x04; +const SHOW_WASM_INFO: u32 = 0x08; #[derive(Debug, Copy, Clone, PartialEq, Default)] pub struct RenderOptions { pub flags: u32, pub dpr: Option, + fast_mode: bool, } impl RenderOptions { pub fn is_debug_visible(&self) -> bool { - self.flags & options::DEBUG_VISIBLE == options::DEBUG_VISIBLE + self.flags & DEBUG_VISIBLE == DEBUG_VISIBLE } pub fn is_profile_rebuild_tiles(&self) -> bool { - self.flags & options::PROFILE_REBUILD_TILES == options::PROFILE_REBUILD_TILES + self.flags & PROFILE_REBUILD_TILES == PROFILE_REBUILD_TILES } /// Use fast mode to enable / disable expensive operations pub fn is_fast_mode(&self) -> bool { - self.flags & options::FAST_MODE == options::FAST_MODE + self.fast_mode } pub fn set_fast_mode(&mut self, enabled: bool) { - if enabled { - self.flags |= options::FAST_MODE; - } else { - self.flags &= !options::FAST_MODE; - } + self.fast_mode = enabled; } pub fn dpr(&self) -> f32 { self.dpr.unwrap_or(1.0) } - pub fn show_info_text(&self) -> bool { - self.flags & options::TEXT_EDITOR_V3 == options::TEXT_EDITOR_V3 + pub fn is_text_editor_v3(&self) -> bool { + self.flags & TEXT_EDITOR_V3 == TEXT_EDITOR_V3 } pub fn show_wasm_info(&self) -> bool { - self.flags & options::SHOW_WASM_INFO == options::SHOW_WASM_INFO + self.flags & SHOW_WASM_INFO == SHOW_WASM_INFO } } From 66ba097ba20388c953fe5216ac357e900ef4c9d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Wed, 18 Mar 2026 13:27:05 +0100 Subject: [PATCH 03/22] :bug: Fix not being able to enable wasm text editor via config flag --- frontend/src/app/render_wasm/api.cljs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index 04546d9ef1..5ffc60a0bd 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -55,10 +55,11 @@ (defn text-editor-wasm? [] - (let [runtime-features (get @st/state :features-runtime) - enabled-features (get @st/state :features)] - (or (contains? runtime-features "text-editor-wasm/v1") - (contains? enabled-features "text-editor-wasm/v1")))) + (or (contains? cf/flags :feature-text-editor-wasm) + (let [runtime-features (get @st/state :features-runtime) + enabled-features (get @st/state :features)] + (or (contains? runtime-features "text-editor-wasm/v1") + (contains? enabled-features "text-editor-wasm/v1"))))) (def ^:const UUID-U8-SIZE 16) (def ^:const UUID-U32-SIZE (/ UUID-U8-SIZE 4)) From 72f5ecfe56f80aeb7b1334f181735d0b5833b92f Mon Sep 17 00:00:00 2001 From: Aitor Moreno Date: Wed, 11 Mar 2026 15:42:28 +0100 Subject: [PATCH 04/22] :tada: Feat add text editor composition update --- .../ui/workspace/shapes/text/v3_editor.cljs | 20 ++- frontend/src/app/render_wasm/text_editor.cljs | 41 +++++- render-wasm/src/state/text_editor.rs | 60 ++++++++ render-wasm/src/wasm/text_editor.rs | 129 ++++++++++++++++++ 4 files changed, 243 insertions(+), 7 deletions(-) diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index 576854c163..bfc2e07947 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -75,7 +75,22 @@ on-composition-start (mf/use-fn (fn [_event] - (reset! composing? true))) + (reset! composing? true) + (text-editor/text-editor-composition-start))) + + on-composition-update + (mf/use-fn + (fn [event] + (when-not composing? + (reset! composing? true)) + + (let [data (.-data event)] + (when data + (text-editor/text-editor-composition-update data) + (sync-wasm-text-editor-content!) + (wasm.api/request-render "text-composition")) + (when-let [node (mf/ref-val contenteditable-ref)] + (set! (.-textContent node) ""))))) on-composition-end (mf/use-fn @@ -83,7 +98,7 @@ (reset! composing? false) (let [data (.-data event)] (when data - (text-editor/text-editor-insert-text data) + (text-editor/text-editor-composition-end data) (sync-wasm-text-editor-content!) (wasm.api/request-render "text-composition")) (when-let [node (mf/ref-val contenteditable-ref)] @@ -326,6 +341,7 @@ :contentEditable true :suppressContentEditableWarning true :on-composition-start on-composition-start + :on-composition-update on-composition-update :on-composition-end on-composition-end :on-key-down on-key-down :on-input on-input diff --git a/frontend/src/app/render_wasm/text_editor.cljs b/frontend/src/app/render_wasm/text_editor.cljs index e8e540b8f1..70c225ced2 100644 --- a/frontend/src/app/render_wasm/text_editor.cljs +++ b/frontend/src/app/render_wasm/text_editor.cljs @@ -66,17 +66,48 @@ (let [res (h/call wasm/internal-module "_text_editor_poll_event")] res))) -(defn text-editor-insert-text +(defn text-editor-encode-text-pre [text] - (when wasm/context-initialized? + (when (and (not (empty? text)) + wasm/context-initialized?) (let [encoder (js/TextEncoder.) buf (.encode encoder text) heapu8 (mem/get-heap-u8) size (mem/size buf) offset (mem/alloc size)] - (mem/write-buffer offset heapu8 buf) - (h/call wasm/internal-module "_text_editor_insert_text") - (mem/free)))) + (mem/write-buffer offset heapu8 buf)))) + +(defn text-editor-encode-text-post + [text] + (when (and (not (empty? text)) + wasm/context-initialized?) + (mem/free))) + +(defn text-editor-composition-start + [] + (when wasm/context-initialized? + (h/call wasm/internal-module "_text_editor_composition_start"))) + +(defn text-editor-composition-update + [text] + (when wasm/context-initialized? + (text-editor-encode-text-pre text) + (h/call wasm/internal-module "_text_editor_composition_update") + (text-editor-encode-text-post text))) + +(defn text-editor-composition-end + [text] + (when wasm/context-initialized? + (text-editor-encode-text-pre text) + (h/call wasm/internal-module "_text_editor_composition_end") + (text-editor-encode-text-post text))) + +(defn text-editor-insert-text + [text] + (when wasm/context-initialized? + (text-editor-encode-text-pre text) + (h/call wasm/internal-module "_text_editor_insert_text") + (text-editor-encode-text-post text))) (defn text-editor-delete-backward ([] diff --git a/render-wasm/src/state/text_editor.rs b/render-wasm/src/state/text_editor.rs index 65ffbbc19a..1c40c25053 100644 --- a/render-wasm/src/state/text_editor.rs +++ b/render-wasm/src/state/text_editor.rs @@ -103,9 +103,68 @@ pub struct TextEditorTheme { pub cursor_color: Color, } +pub struct TextComposition { + pub previous: String, + pub current: String, + pub is_composing: bool, +} + +impl TextComposition { + pub fn new() -> Self { + Self { + previous: String::new(), + current: String::new(), + is_composing: false, + } + } + + pub fn start(&mut self) -> bool { + if self.is_composing { + return false; + } + self.is_composing = true; + self.previous = String::new(); + self.current = String::new(); + true + } + + pub fn update(&mut self, text: &str) -> bool { + if !self.is_composing { + self.is_composing = true; + } + self.previous = self.current.clone(); + self.current = text.to_owned(); + true + } + + pub fn end(&mut self) -> bool { + if !self.is_composing { + return false; + } + self.is_composing = false; + true + } + + pub fn get_selection(&self, selection: &TextSelection) -> TextSelection { + if self.previous.is_empty() { + return *selection; + } + + let focus = selection.focus; + let previous_len = self.previous.chars().count(); + let anchor = TextPositionWithAffinity::new_without_affinity( + focus.paragraph, + focus.offset + previous_len, + ); + + TextSelection { anchor, focus } + } +} + pub struct TextEditorState { pub theme: TextEditorTheme, pub selection: TextSelection, + pub composition: TextComposition, pub is_active: bool, // This property indicates that we've started // selecting something with the pointer. @@ -125,6 +184,7 @@ impl TextEditorState { cursor_color: CURSOR_COLOR, }, selection: TextSelection::new(), + composition: TextComposition::new(), is_active: false, is_pointer_selection_active: false, active_shape_id: None, diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 25182c5050..eeac88ce43 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -293,6 +293,135 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) { // TEXT OPERATIONS // ============================================================================ +#[no_mangle] +#[wasm_error] +pub extern "C" fn text_editor_composition_start() -> Result<()> { + with_state_mut!(state, { + if !state.text_editor_state.is_active { + return Ok(()); + } + state.text_editor_state.composition.start(); + }); + + Ok(()) +} + +#[no_mangle] +#[wasm_error] +pub extern "C" fn text_editor_composition_end() -> Result<()> { + let bytes = crate::mem::bytes(); + let text = match String::from_utf8(bytes) { + Ok(text) => text, + Err(_) => return Ok(()), + }; + + with_state_mut!(state, { + if !state.text_editor_state.is_active { + return Ok(()); + } + + let Some(shape_id) = state.text_editor_state.active_shape_id else { + return Ok(()); + }; + + let Some(shape) = state.shapes.get_mut(&shape_id) else { + return Ok(()); + }; + + let Type::Text(text_content) = &mut shape.shape_type else { + return Ok(()); + }; + + state.text_editor_state.composition.update(&text); + + let selection = state + .text_editor_state + .composition + .get_selection(&state.text_editor_state.selection); + text_helpers::delete_selection_range(text_content, &selection); + + let cursor = state.text_editor_state.selection.focus; + if let Some(new_cursor) = + text_helpers::insert_text_with_newlines(text_content, &cursor, &text) + { + state.text_editor_state.selection.set_caret(new_cursor); + } + + text_content.layout.paragraphs.clear(); + text_content.layout.paragraph_builders.clear(); + + state.text_editor_state.reset_blink(); + state + .text_editor_state + .push_event(crate::state::TextEditorEvent::ContentChanged); + state + .text_editor_state + .push_event(crate::state::TextEditorEvent::NeedsLayout); + + state.render_state.mark_touched(shape_id); + + state.text_editor_state.composition.end(); + }); + + crate::mem::free_bytes()?; + Ok(()) +} + +#[no_mangle] +#[wasm_error] +pub extern "C" fn text_editor_composition_update() -> Result<()> { + let bytes = crate::mem::bytes(); + let text = match String::from_utf8(bytes) { + Ok(text) => text, + Err(_) => return Ok(()), + }; + + with_state_mut!(state, { + if !state.text_editor_state.is_active { + return Ok(()); + } + + let Some(shape_id) = state.text_editor_state.active_shape_id else { + return Ok(()); + }; + + let Some(shape) = state.shapes.get_mut(&shape_id) else { + return Ok(()); + }; + + let Type::Text(text_content) = &mut shape.shape_type else { + return Ok(()); + }; + + state.text_editor_state.composition.update(&text); + + let selection = state + .text_editor_state + .composition + .get_selection(&state.text_editor_state.selection); + text_helpers::delete_selection_range(text_content, &selection); + + let cursor = state.text_editor_state.selection.focus; + text_helpers::insert_text_with_newlines(text_content, &cursor, &text); + + text_content.layout.paragraphs.clear(); + text_content.layout.paragraph_builders.clear(); + + state.text_editor_state.reset_blink(); + state + .text_editor_state + .push_event(crate::state::TextEditorEvent::ContentChanged); + state + .text_editor_state + .push_event(crate::state::TextEditorEvent::NeedsLayout); + + state.render_state.mark_touched(shape_id); + }); + + crate::mem::free_bytes()?; + Ok(()) +} + // FIXME: Review if all the return Ok(()) should be Err instead. #[no_mangle] #[wasm_error] From 4e1b940e0438e491f3f7674b2b1091f3ce7fe4cf Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 12 Mar 2026 12:14:25 +0100 Subject: [PATCH 05/22] :sparkles: Remap token properties for usability --- CHANGES.md | 2 + .../data/workspace/tokens/application.cljs | 12 ++--- .../tokens/management/context_menu.cljs | 2 +- frontend/src/app/plugins/shape.cljs | 11 ++-- frontend/src/app/plugins/tokens.cljs | 52 +++++++++++++++++-- .../logic/components_and_tokens.cljs | 8 +-- .../tokens/logic/token_actions_test.cljs | 14 ++--- plugins/libs/plugin-types/index.d.ts | 22 ++++---- 8 files changed, 86 insertions(+), 37 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 538c8994f4..86c535adcc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,8 @@ ### :sparkles: New features & Enhancements - Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) +- Access Tokens look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114) +- Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) ### :bug: Bugs fixed diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 686cd866d0..6f52e92df9 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -49,14 +49,14 @@ ;; (note that dwsh/update-shapes function returns an event) -(defn update-shape-radius-all - ([value shape-ids attributes] (update-shape-radius-all value shape-ids attributes nil)) - ([value shape-ids _attributes page-id] ; The attributes param is needed to have the same arity that other update functions +(defn update-shape-radius + ([value shape-ids attributes] (update-shape-radius value shape-ids attributes nil)) + ([value shape-ids attributes page-id] ; The attributes param is needed to have the same arity that other update functions (when (number? value) (let [value (max 0 value)] (dwsh/update-shapes shape-ids (fn [shape] - (ctsr/set-radius-to-all-corners shape value)) + (ctsr/set-radius-for-corners shape attributes value)) {:reg-objects? true :ignore-touched true :page-id page-id @@ -531,7 +531,7 @@ (some attributes #{:r1 :r2 :r3 :r4}) (conj #(if (= attributes #{:r1 :r2 :r3 :r4}) - (update-shape-radius-all value shape-ids attributes page-id) + (update-shape-radius value shape-ids attributes page-id) (update-shape-radius-for-corners value shape-ids (set (filter attributes #{:r1 :r2 :r3 :r4})) @@ -862,7 +862,7 @@ :border-radius {:title "Border Radius" :attributes ctt/border-radius-keys - :on-update-shape update-shape-radius-all + :on-update-shape update-shape-radius :modal {:key :tokens/border-radius :fields [{:label "Border Radius" :key :border-radius}]}} diff --git a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs index bcb44b83c5..eb43f4a23b 100644 --- a/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/management/context_menu.cljs @@ -290,7 +290,7 @@ :r4 "Bottom Left" :r3 "Bottom Right"} :hint (tr "workspace.tokens.radius") - :on-update-shape-all dwta/update-shape-radius-all + :on-update-shape-all dwta/update-shape-radius :on-update-shape update-shape-radius-for-corners}) shadow (partial generic-attribute-actions #{:shadow} "Shadow")] {:border-radius border-radius diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index c7730d8a36..9075448ca8 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -31,7 +31,6 @@ [app.common.types.shape.radius :as ctsr] [app.common.types.shape.shadow :as ctss] [app.common.types.text :as txt] - [app.common.types.token :as cto] [app.common.uuid :as uuid] [app.main.data.plugins :as dp] [app.main.data.workspace :as dw] @@ -55,6 +54,7 @@ [app.plugins.register :as r] [app.plugins.ruler-guides :as rg] [app.plugins.text :as text] + [app.plugins.tokens :refer [resolve-tokens translate-prop token-attr?]] [app.plugins.utils :as u] [app.util.http :as http] [app.util.object :as obj] @@ -1300,7 +1300,8 @@ (fn [_] (let [tokens (-> (u/locate-shape file-id page-id id) - (get :applied-tokens))] + (get :applied-tokens) + (resolve-tokens))] (reduce (fn [acc [prop name]] (obj/set! acc (json/write-camel-key prop) name)) @@ -1311,11 +1312,11 @@ {:enumerable false :schema [:tuple [:fn token-proxy?] - [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]] + [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [token attrs] (let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id")) - kw-attrs (into #{} (map keyword attrs))] - (if (some #(not (cto/token-attr? %)) kw-attrs) + kw-attrs (into #{} (map (comp translate-prop keyword) attrs))] + (if (some #(not (token-attr? %)) kw-attrs) (u/display-not-valid :applyToken attrs) (st/emit! (dwta/toggle-token {:token token diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index 268652e334..281b82fbc9 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -19,18 +19,53 @@ [app.main.store :as st] [app.plugins.utils :as u] [app.util.object :as obj] - [clojure.datafy :refer [datafy]])) + [clojure.datafy :refer [datafy]] + [clojure.set :refer [map-invert]] + [cuerdas.core :as str])) ;; === Token +(def token-name-mapping + {:r1 :borderRadiusTopLeft + :r2 :borderRadiusTopRight + :r3 :borderRadiusBottomRight + :r4 :borderRadiusBottomLeft + + :p1 :paddingTopLeft + :p2 :paddingTopRight + :p3 :paddingBottomRight + :p4 :paddingBottomLeft + + :m1 :marginTopLeft + :m2 :marginTopRight + :m3 :marginBottomRight + :m4 :marginBottomLeft}) + +(def name-token-mapping + (map-invert token-name-mapping)) + +(defn resolve-prop + [k] + (get token-name-mapping k k)) + +(defn translate-prop + [k] + (let [k (-> (str/camel k) keyword)] + (get name-token-mapping k k))) + +(defn token-attr? + [attr] + (cto/token-attr? (translate-prop attr))) + (defn- apply-token-to-shapes [file-id set-id id shape-ids attrs] + (let [token (u/locate-token file-id set-id id)] - (if (some #(not (cto/token-attr? %)) attrs) + (if (some #(not (token-attr? %)) attrs) (u/display-not-valid :applyToSelected attrs) (st/emit! (dwta/toggle-token {:token token - :attrs attrs + :attrs (into #{} (map translate-prop) attrs) :shape-ids shape-ids :expand-with-children false}))))) @@ -42,6 +77,13 @@ (ts/tokenscript-symbols->penpot-unit))] resolved-value)) + +(defn resolve-tokens + [value] + (into {} + (map (fn [[k v]] [(resolve-prop k) v])) + value)) + (defn token-proxy? [p] (obj/type-of? p "TokenProxy")) @@ -146,13 +188,13 @@ {:enumerable false :schema [:tuple [:vector [:fn shape-proxy?]] - [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]] + [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [shapes attrs] (apply-token-to-shapes file-id set-id id (map #(obj/get % "$id") shapes) attrs))} :applyToSelected {:enumerable false - :schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn cto/token-attr?]]]]] + :schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [attrs] (let [selected (get-in @st/state [:workspace-local :selected])] (apply-token-to-shapes file-id set-id id selected attrs)))})) diff --git a/frontend/test/frontend_tests/logic/components_and_tokens.cljs b/frontend/test/frontend_tests/logic/components_and_tokens.cljs index 689ac76b63..9e830c9a76 100644 --- a/frontend/test/frontend_tests/logic/components_and_tokens.cljs +++ b/frontend/test/frontend_tests/logic/components_and_tokens.cljs @@ -141,7 +141,7 @@ events [(dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-2") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] @@ -249,11 +249,11 @@ events [(dwta/apply-token {:shape-ids [(cthi/id :c-frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-2") - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) (dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-3") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] @@ -293,7 +293,7 @@ (dwta/apply-token {:shape-ids [(cthi/id :frame1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "test-token-3") - :on-update-shape dwta/update-shape-radius-all})] + :on-update-shape dwta/update-shape-radius})] step2 (fn [_] (let [events2 [(dwl/sync-file (:id file) (:id file))]] diff --git a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs index cb6f2e39d8..e468e420bb 100644 --- a/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs +++ b/frontend/test/frontend_tests/tokens/logic/token_actions_test.cljs @@ -64,7 +64,7 @@ events [(dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.md") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -89,11 +89,11 @@ events [(dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) (dwta/apply-token {:shape-ids [(:id rect-1)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.md") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -117,14 +117,14 @@ (dwta/apply-token {:attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") :shape-ids [(:id rect-1)] - :on-update-shape dwta/update-shape-radius-all}) + :on-update-shape dwta/update-shape-radius}) ;; Apply single `:r1` attribute to same shape ;; while removing other attributes from the border-radius set ;; but keep `:r4` for testing purposes (dwta/apply-token {:attributes #{:r1 :r2 :r3} :token (toht/get-token file "borderRadius.md") :shape-ids [(:id rect-1)] - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -153,7 +153,7 @@ (dwta/apply-token {:shape-ids [(:id rect-2)] :attributes #{:r1 :r2 :r3 :r4} :token (toht/get-token file "borderRadius.sm") - :on-update-shape dwta/update-shape-radius-all})]] + :on-update-shape dwta/update-shape-radius})]] (tohs/run-store-async store done events (fn [new-state] @@ -762,7 +762,7 @@ rect-2 (cths/get-shape file :rect-2) events [(dwta/toggle-token {:shape-ids [(:id rect-1) (:id rect-2)] :token-type-props {:attributes #{:r1 :r2 :r3 :r4} - :on-update-shape dwta/update-shape-radius-all} + :on-update-shape dwta/update-shape-radius} :token (toht/get-token file "borderRadius.md")})]] (tohs/run-store-async store done events diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 381de47fc7..84cff58440 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -5231,7 +5231,11 @@ export interface TokenTheme { /** * The properties that a BorderRadius token can be applied to. */ -type TokenBorderRadiusProps = 'r1' | 'r2' | 'r3' | 'r4'; +type TokenBorderRadiusProps = + | 'borderRadiusTopLeft' + | 'borderRadiusTopRight' + | 'borderRadiusBottomRight' + | 'borderRadiusBottomLeft'; /** * The properties that a Shadow token can be applied to. @@ -5307,16 +5311,16 @@ type TokenSpacingProps = | 'columnGap' // Spacing / Padding - | 'p1' - | 'p2' - | 'p3' - | 'p4' + | 'paddingLeft' + | 'paddingTop' + | 'paddingRight' + | 'paddingBottom' // Spacing / Margin - | 'm1' - | 'm2' - | 'm3' - | 'm4'; + | 'marginLeft' + | 'marginTop' + | 'marginRight' + | 'marginBottom'; /** * The properties that a BorderWidth token can be applied to. From 8e2a52af50406159ec8a85cdfe2ef06b932dd533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Tue, 17 Mar 2026 12:46:22 +0100 Subject: [PATCH 06/22] :lipstick: Change function names --- .../data/workspace/tokens/application.cljs | 2 +- frontend/src/app/plugins/shape.cljs | 10 ++-- frontend/src/app/plugins/tokens.cljs | 60 +++++++++---------- .../src/app/app.component.ts | 2 +- 4 files changed, 36 insertions(+), 38 deletions(-) diff --git a/frontend/src/app/main/data/workspace/tokens/application.cljs b/frontend/src/app/main/data/workspace/tokens/application.cljs index 6f52e92df9..3ee7758284 100644 --- a/frontend/src/app/main/data/workspace/tokens/application.cljs +++ b/frontend/src/app/main/data/workspace/tokens/application.cljs @@ -51,7 +51,7 @@ (defn update-shape-radius ([value shape-ids attributes] (update-shape-radius value shape-ids attributes nil)) - ([value shape-ids attributes page-id] ; The attributes param is needed to have the same arity that other update functions + ([value shape-ids attributes page-id] (when (number? value) (let [value (max 0 value)] (dwsh/update-shapes shape-ids diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 9075448ca8..3692267fd4 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -54,7 +54,7 @@ [app.plugins.register :as r] [app.plugins.ruler-guides :as rg] [app.plugins.text :as text] - [app.plugins.tokens :refer [resolve-tokens translate-prop token-attr?]] + [app.plugins.tokens :refer [applied-tokens-plugin->applied-tokens token-attr-plugin->token-attr token-attr?]] [app.plugins.utils :as u] [app.util.http :as http] [app.util.object :as obj] @@ -1298,15 +1298,15 @@ {:this true :get (fn [_] - (let [tokens + (let [applied-tokens (-> (u/locate-shape file-id page-id id) (get :applied-tokens) - (resolve-tokens))] + (applied-tokens-plugin->applied-tokens))] (reduce (fn [acc [prop name]] (obj/set! acc (json/write-camel-key prop) name)) #js {} - tokens)))} + applied-tokens)))} :applyToken {:enumerable false @@ -1315,7 +1315,7 @@ [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [token attrs] (let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id")) - kw-attrs (into #{} (map (comp translate-prop keyword) attrs))] + kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))] (if (some #(not (token-attr? %)) kw-attrs) (u/display-not-valid :applyToken attrs) (st/emit! diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index 281b82fbc9..6c3a4f5eff 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -20,42 +20,47 @@ [app.plugins.utils :as u] [app.util.object :as obj] [clojure.datafy :refer [datafy]] - [clojure.set :refer [map-invert]] - [cuerdas.core :as str])) + [clojure.set :refer [map-invert]])) ;; === Token -(def token-name-mapping - {:r1 :borderRadiusTopLeft - :r2 :borderRadiusTopRight - :r3 :borderRadiusBottomRight - :r4 :borderRadiusBottomLeft +;; Give more semantic names to the shape attributes that tokens can be applied to +(def ^:private map:token-attr->token-attr-plugin + {:r1 :border-radius-top-left + :r2 :border-radius-top-right + :r3 :border-radius-bottom-right + :r4 :border-radius-bottom-left - :p1 :paddingTopLeft - :p2 :paddingTopRight - :p3 :paddingBottomRight - :p4 :paddingBottomLeft + :p1 :padding-top-left + :p2 :padding-top-right + :p3 :padding-bottom-right + :p4 :padding-bottom-left - :m1 :marginTopLeft - :m2 :marginTopRight - :m3 :marginBottomRight - :m4 :marginBottomLeft}) + :m1 :margin-top-left + :m2 :margin-top-right + :m3 :margin-bottom-right + :m4 :margin-bottom-left}) -(def name-token-mapping - (map-invert token-name-mapping)) +(def ^:private map:token-attr-plugin->token-attr + (map-invert map:token-attr->token-attr-plugin)) -(defn resolve-prop +(defn token-attr->token-attr-plugin [k] - (get token-name-mapping k k)) + (get map:token-attr->token-attr-plugin k k)) -(defn translate-prop +(defn token-attr-plugin->token-attr [k] - (let [k (-> (str/camel k) keyword)] - (get name-token-mapping k k))) + (get map:token-attr-plugin->token-attr k k)) + +(defn applied-tokens-plugin->applied-tokens + [value] + (into {} + (map (fn [[k v]] [(token-attr->token-attr-plugin k) v])) + value)) (defn token-attr? [attr] - (cto/token-attr? (translate-prop attr))) + (cto/token-attr? (token-attr-plugin->token-attr attr))) (defn- apply-token-to-shapes [file-id set-id id shape-ids attrs] @@ -65,7 +70,7 @@ (u/display-not-valid :applyToSelected attrs) (st/emit! (dwta/toggle-token {:token token - :attrs (into #{} (map translate-prop) attrs) + :attrs (into #{} (map token-attr-plugin->token-attr) attrs) :shape-ids shape-ids :expand-with-children false}))))) @@ -77,13 +82,6 @@ (ts/tokenscript-symbols->penpot-unit))] resolved-value)) - -(defn resolve-tokens - [value] - (into {} - (map (fn [[k v]] [(resolve-prop k) v])) - value)) - (defn token-proxy? [p] (obj/type-of? p "TokenProxy")) diff --git a/plugins/apps/poc-tokens-plugin/src/app/app.component.ts b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts index 0b5584fbe8..8b60e40d8d 100644 --- a/plugins/apps/poc-tokens-plugin/src/app/app.component.ts +++ b/plugins/apps/poc-tokens-plugin/src/app/app.component.ts @@ -264,7 +264,7 @@ export class AppComponent { type: 'apply-token', setId: this.currentSetId, tokenId, - // properties: ['strokeColor'] // Uncomment to choose attribute to apply + // properties: ['borderRadiusTopRight'] // Uncomment to choose attribute to apply }); // (incompatible attributes will have no effect) } } From 1b91bbe64d2eb0448a513b300a8f158ea6783730 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 18 Mar 2026 11:07:46 +0100 Subject: [PATCH 07/22] :sparkles: Update MCP server to account for API updates Update instructions and API documentation to account for * updated token property names; resolves #8512 * improved variant container creation; resolves #8564 --- mcp/packages/server/data/api_types.yml | 754 ++++++++++-------- .../server/data/initial_instructions.md | 16 +- 2 files changed, 425 insertions(+), 345 deletions(-) diff --git a/mcp/packages/server/data/api_types.yml b/mcp/packages/server/data/api_types.yml index 943cc62021..901037c249 100644 --- a/mcp/packages/server/data/api_types.yml +++ b/mcp/packages/server/data/api_types.yml @@ -84,6 +84,7 @@ Penpot: distributeHorizontal(shapes: Shape[]): void; distributeVertical(shapes: Shape[]): void; flatten(shapes: Shape[]): Path[]; + createVariantFromComponents(shapes: Board[]): VariantContainer; } ``` @@ -823,6 +824,24 @@ Penpot: to flatten Returns Path[] + createVariantFromComponents: |- + ``` + createVariantFromComponents(shapes: Board[]): VariantContainer + ``` + + Combine several standard Components into a VariantComponent. Similar to doing it + with the contextual menu on the Penpot interface. + All the shapes passed as arguments should be main instances. + + Parameters + + * shapes: Board[] + + A list of main instances of the components to combine. + + Returns VariantContainer + + The variant container created ActiveUser: overview: |- Interface ActiveUser @@ -1071,10 +1090,10 @@ Board: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -1090,14 +1109,14 @@ Board: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -1114,7 +1133,7 @@ Board: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -1193,12 +1212,20 @@ Board: ``` The horizontal sizing behavior of the board. + It can be one of the following values: + + * 'fix': The containers has its own intrinsic fixed size. + * 'auto': The container fits the content. verticalSizing: |- ``` verticalSizing?: "auto" | "fix" ``` The vertical sizing behavior of the board. + It can be one of the following values: + + * 'fix': The containers has its own intrinsic fixed size. + * 'auto': The container fits the content. fills: |- ``` fills: Fill[] @@ -1469,10 +1496,10 @@ Board: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -1488,14 +1515,14 @@ Board: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -1881,7 +1908,7 @@ Board: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -1894,7 +1921,9 @@ Board: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -2180,10 +2209,10 @@ VariantContainer: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -2199,14 +2228,14 @@ VariantContainer: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -2223,7 +2252,7 @@ VariantContainer: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -2250,7 +2279,7 @@ VariantContainer: * Board + VariantContainer - Referenced by: ContextTypesUtils + Referenced by: Board, Boolean, Context, ContextTypesUtils, Ellipse, Group, Image, Path, Penpot, Rectangle, ShapeBase, SvgRaw, Text, VariantContainer members: Properties: type: |- @@ -2301,12 +2330,20 @@ VariantContainer: ``` The horizontal sizing behavior of the board. + It can be one of the following values: + + * 'fix': The containers has its own intrinsic fixed size. + * 'auto': The container fits the content. verticalSizing: |- ``` verticalSizing?: "auto" | "fix" ``` The vertical sizing behavior of the board. + It can be one of the following values: + + * 'fix': The containers has its own intrinsic fixed size. + * 'auto': The container fits the content. fills: |- ``` fills: Fill[] @@ -2581,10 +2618,10 @@ VariantContainer: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -2600,14 +2637,14 @@ VariantContainer: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -2993,7 +3030,7 @@ VariantContainer: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -3006,7 +3043,9 @@ VariantContainer: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -3279,10 +3318,10 @@ Boolean: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -3298,14 +3337,14 @@ Boolean: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -3322,7 +3361,7 @@ Boolean: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -3642,10 +3681,10 @@ Boolean: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -3661,14 +3700,14 @@ Boolean: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -4005,7 +4044,7 @@ Boolean: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -4018,7 +4057,9 @@ Boolean: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -4575,8 +4616,8 @@ CommonLayout: leftPadding: number; horizontalSizing: "fill" | "auto" - | "fit-content"; - verticalSizing: "fill" | "auto" | "fit-content"; + | "fix"; + verticalSizing: "fill" | "auto" | "fix"; remove(): void; } ``` @@ -4706,26 +4747,26 @@ CommonLayout: The `leftPadding` property specifies the padding at the left of the container. horizontalSizing: |- ``` - horizontalSizing: "fill" | "auto" | "fit-content" + horizontalSizing: "fill" | "auto" | "fix" ``` The `horizontalSizing` property specifies the horizontal sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. verticalSizing: |- ``` - verticalSizing: "fill" | "auto" | "fit-content" + verticalSizing: "fill" | "auto" | "fix" ``` The `verticalSizing` property specifies the vertical sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. Methods: remove: |- ``` @@ -4808,6 +4849,7 @@ Context: distributeHorizontal(shapes: Shape[]): void; distributeVertical(shapes: Shape[]): void; flatten(shapes: Shape[]): Path[]; + createVariantFromComponents(shapes: Board[]): VariantContainer; } ``` members: @@ -5449,6 +5491,24 @@ Context: to flatten Returns Path[] + createVariantFromComponents: |- + ``` + createVariantFromComponents(shapes: Board[]): VariantContainer + ``` + + Combine several standard Components into a VariantComponent. Similar to doing it + with the contextual menu on the Penpot interface. + All the shapes passed as arguments should be main instances. + + Parameters + + * shapes: Board[] + + A list of main instances of the components to combine. + + Returns VariantContainer + + The variant container created ContextGeometryUtils: overview: |- Interface ContextGeometryUtils @@ -5859,10 +5919,10 @@ Ellipse: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -5878,14 +5938,14 @@ Ellipse: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -5902,7 +5962,7 @@ Ellipse: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -6192,10 +6252,10 @@ Ellipse: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -6211,14 +6271,14 @@ Ellipse: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -6500,7 +6560,7 @@ Ellipse: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -6513,7 +6573,9 @@ Ellipse: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -7244,8 +7306,8 @@ FlexLayout: leftPadding: number; horizontalSizing: "fill" | "auto" - | "fit-content"; - verticalSizing: "fill" | "auto" | "fit-content"; + | "fix"; + verticalSizing: "fill" | "auto" | "fix"; remove(): void; dir: "row" | "row-reverse" | "column" | "column-reverse"; wrap?: "wrap" | "nowrap"; @@ -7379,26 +7441,26 @@ FlexLayout: The `leftPadding` property specifies the padding at the left of the container. horizontalSizing: |- ``` - horizontalSizing: "fill" | "auto" | "fit-content" + horizontalSizing: "fill" | "auto" | "fix" ``` The `horizontalSizing` property specifies the horizontal sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. verticalSizing: |- ``` - verticalSizing: "fill" | "auto" | "fit-content" + verticalSizing: "fill" | "auto" | "fix" ``` The `verticalSizing` property specifies the vertical sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. dir: |- ``` dir: "row" | "row-reverse" | "column" | "column-reverse" @@ -7802,8 +7864,8 @@ GridLayout: leftPadding: number; horizontalSizing: "fill" | "auto" - | "fit-content"; - verticalSizing: "fill" | "auto" | "fit-content"; + | "fix"; + verticalSizing: "fill" | "auto" | "fix"; remove(): void; dir: "row" | "column"; rows: Track[]; @@ -7946,26 +8008,26 @@ GridLayout: The `leftPadding` property specifies the padding at the left of the container. horizontalSizing: |- ``` - horizontalSizing: "fill" | "auto" | "fit-content" + horizontalSizing: "fill" | "auto" | "fix" ``` The `horizontalSizing` property specifies the horizontal sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. verticalSizing: |- ``` - verticalSizing: "fill" | "auto" | "fit-content" + verticalSizing: "fill" | "auto" | "fix" ``` The `verticalSizing` property specifies the vertical sizing behavior of the container. It can be one of the following values: - * 'fit-content': The container fits the content. - * 'fill': The container fills the available space. - * 'auto': The container size is determined automatically. + * 'fix': The containers has its own intrinsic fixed size. + * 'fill': The container fills the available space. Only can be set if it's inside another layout. + * 'auto': The container fits the content. dir: |- ``` dir: "row" | "column" @@ -8288,10 +8350,10 @@ Group: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -8307,14 +8369,14 @@ Group: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -8331,7 +8393,7 @@ Group: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -8627,10 +8689,10 @@ Group: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -8646,14 +8708,14 @@ Group: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -9001,7 +9063,7 @@ Group: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -9014,7 +9076,9 @@ Group: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -9532,10 +9596,10 @@ Image: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -9551,14 +9615,14 @@ Image: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -9575,7 +9639,7 @@ Image: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -9865,10 +9929,10 @@ Image: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -9884,14 +9948,14 @@ Image: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -10173,7 +10237,7 @@ Image: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -10186,7 +10250,9 @@ Image: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -12997,10 +13063,10 @@ Path: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -13016,14 +13082,14 @@ Path: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -13040,7 +13106,7 @@ Path: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -13354,10 +13420,10 @@ Path: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -13373,14 +13439,14 @@ Path: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -13676,7 +13742,7 @@ Path: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -13689,7 +13755,9 @@ Path: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -14324,10 +14392,10 @@ Rectangle: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -14343,14 +14411,14 @@ Rectangle: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -14367,7 +14435,7 @@ Rectangle: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -14659,10 +14727,10 @@ Rectangle: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -14678,14 +14746,14 @@ Rectangle: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -14967,7 +15035,7 @@ Rectangle: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -14980,7 +15048,9 @@ Rectangle: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -15360,10 +15430,10 @@ ShapeBase: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -15379,14 +15449,14 @@ ShapeBase: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -15403,7 +15473,7 @@ ShapeBase: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -15694,10 +15764,10 @@ ShapeBase: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -15713,14 +15783,14 @@ ShapeBase: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -16002,7 +16072,7 @@ ShapeBase: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -16015,7 +16085,9 @@ ShapeBase: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -16426,10 +16498,10 @@ SvgRaw: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -16445,14 +16517,14 @@ SvgRaw: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -16469,7 +16541,7 @@ SvgRaw: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -16754,10 +16826,10 @@ SvgRaw: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -16773,14 +16845,14 @@ SvgRaw: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -17066,7 +17138,7 @@ SvgRaw: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -17079,7 +17151,9 @@ SvgRaw: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -17345,10 +17419,10 @@ Text: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -17364,14 +17438,14 @@ Text: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -17388,7 +17462,7 @@ Text: detach(): void; swapComponent(component: LibraryComponent): void; switchVariant(pos: number, value: string): void; - combineAsVariants(ids: string[]): void; + combineAsVariants(ids: string[]): VariantContainer; isVariantHead(): boolean; resize(width: number, height: number): void; rotate(angle: number, center?: { x: number; y: number } | null): void; @@ -17691,10 +17765,10 @@ Text: x: string; y: string; all: string; - r1: string; - r2: string; - r3: string; - r4: string; + borderRadiusTopLeft: string; + borderRadiusTopRight: string; + borderRadiusBottomRight: string; + borderRadiusBottomLeft: string; shadow: string; strokeColor: string; strokeWidth: string; @@ -17710,14 +17784,14 @@ Text: layoutItemMaxH: string; rowGap: string; columnGap: string; - p1: string; - p2: string; - p3: string; - p4: string; - m1: string; - m2: string; - m3: string; - m4: string; + paddingLeft: string; + paddingTop: string; + paddingRight: string; + paddingBottom: string; + marginLeft: string; + marginTop: string; + marginRight: string; + marginBottom: string; textCase: string; textDecoration: string; typography: string; @@ -18107,7 +18181,7 @@ Text: Returns void combineAsVariants: |- ``` - combineAsVariants(ids: string[]): void + combineAsVariants(ids: string[]): VariantContainer ``` Combine several standard Components into a VariantComponent. Similar to doing it with the contextual menu @@ -18120,7 +18194,9 @@ Text: A list of ids of the main instances of the components to combine with this one. - Returns void + Returns VariantContainer + + The variant container created isVariantHead: |- ``` isVariantHead(): boolean @@ -22618,7 +22694,11 @@ TokenBorderRadiusProps: ================================= ``` - TokenBorderRadiusProps: "r1" | "r2" | "r3" | "r4" + TokenBorderRadiusProps: + | "borderRadiusTopLeft" + | "borderRadiusTopRight" + | "borderRadiusBottomRight" + | "borderRadiusBottomLeft" ``` The properties that a BorderRadius token can be applied to. @@ -22770,14 +22850,14 @@ TokenSpacingProps: TokenSpacingProps: | "rowGap" | "columnGap" - | "p1" - | "p2" - | "p3" - | "p4" - | "m1" - | "m2" - | "m3" - | "m4" + | "paddingLeft" + | "paddingTop" + | "paddingRight" + | "paddingBottom" + | "marginLeft" + | "marginTop" + | "marginRight" + | "marginBottom" ``` The properties that a Spacing token can be applied to. diff --git a/mcp/packages/server/data/initial_instructions.md b/mcp/packages/server/data/initial_instructions.md index c212bb408b..c85441a977 100644 --- a/mcp/packages/server/data/initial_instructions.md +++ b/mcp/packages/server/data/initial_instructions.md @@ -282,7 +282,7 @@ Variants are a system for grouping related component versions along named proper - check with `isVariantContainer()` - property `variants: Variants`. * `Variants`: Defines the combinations of property values for which component variants can exist and manages the concrete component variants. - - `properties: string[]` (ordered list of property names); `addProperty()`, `renameProperty(pos, name)`, `currentValues(property)` + - `properties: string[]` (ordered list of property names); `addProperty(): void`, `renameProperty(pos, name)`, `currentValues(property)` - `variantComponents(): LibraryVariantComponent[]` * `LibraryVariantComponent` (extends `LibraryComponent`): full library component with metadata, for which `isVariant()` returns true. - `variantProps: { [property: string]: string }` (this component's value for each property) @@ -292,11 +292,11 @@ Variants are a system for grouping related component versions along named proper Properties are often addressed positionally: `pos` parameter in various methods = index in `Variants.properties`. **Creating a variant group**: -- `component.transformInVariant(): null`: Converts a standard component into a variant group, creating a `VariantContainer` and a second duplicate variant. - Both start with a default property `Property 1` with values `Value 1` / `Value 2`; there is no name-based auto-parsing. -- `board.combineAsVariants(ids: string[]): null`: Combines the board (a main component instance) with other main components (referenced via IDs) into a new variant group. - All components end up inside a single new `VariantContainer` on the canvas. -- In both cases, look for the created `VariantContainer` on the page, and then edit properties using `variants.renameProperty(pos, name)`, `variants.addProperty()`, and `comp.setVariantProperty(pos, value)`. +- `penpot.createVariantFromComponents(mainInstances: Board[]): VariantContainer`: Combines several main component instances into a new variant group. + All components end up inside a single new container on the canvas. + NOTE: The returned instance `variantContainer` is not usable but has an usable id; use `penpot.findShapeById(variantContainer.id)` to get the actual instance you can work with. + The container's `Variants` instance is initialised with one property `Property 1`, with the property values set to the respective component's name. +- After creation, edit properties using `variants.renameProperty(pos, name)`, `variants.addProperty()`, and `comp.setVariantProperty(pos, value)`. **Adding a variant to an existing group**: Use `variantContainer.appendChild(mainInstance)` to move a component's main instance into the container, then set its position manually and assign property values via `setVariantProperty`. @@ -342,7 +342,7 @@ Applying tokens: (if properties is undefined, use a default property based on the token type - not usually recommended). `TokenProperty` is a union type; possible values are: - "all": applies the token to all properties it can control - - TokenBorderRadiusProps: "r1", "r2", "r3", "r4" + - TokenBorderRadiusProps: "borderRadiusTopLeft", "borderRadiusTopRight", "borderRadiusBottomRight", "borderRadiusBottomLeft" - TokenShadowProps: "shadow" - TokenColorProps: "fill", "strokeColor" - TokenDimensionProps: "x", "y", "strokeWidth" @@ -353,7 +353,7 @@ Applying tokens: - TokenNumberProps: "rotation" - TokenOpacityProps: "opacity" - TokenSizingProps: "width", "height", "layoutItemMinW", "layoutItemMaxW", "layoutItemMinH", "layoutItemMaxH" - - TokenSpacingProps: "rowGap", "columnGap", "p1", "p2", "p3", "p4", "m1", "m2", "m3", "m4" + - TokenSpacingProps: "rowGap", "columnGap", "paddingLeft", "paddingTop", "paddingRight", "paddingBottom", "marginLeft", "marginTop", "marginRight", "marginBottom" - TokenBorderWidthProps: "strokeWidth" - TokenTextCaseProps: "textCase" - TokenTextDecorationProps: "textDecoration" From a4ad94017736a1853e58d06527925d2c37767f62 Mon Sep 17 00:00:00 2001 From: Alonso Torres Date: Wed, 18 Mar 2026 16:03:17 +0100 Subject: [PATCH 08/22] :sparkles: Add version property to plugins API (#8676) --- frontend/src/app/plugins/api.cljs | 5 +++++ plugins/CHANGELOG.md | 5 +++++ plugins/libs/plugin-types/index.d.ts | 5 +++++ plugins/libs/plugins-runtime/src/lib/api/index.ts | 4 ++++ 4 files changed, 19 insertions(+) diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 4c982db774..59bd81caae 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -18,6 +18,7 @@ [app.common.types.shape :as cts] [app.common.types.text :as txt] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.changes :as ch] [app.main.data.common :as dcm] [app.main.data.helpers :as dsh] @@ -84,6 +85,10 @@ :$plugin {:enumerable false :get (fn [] plugin-id)} ;; Public properties + :version + {:this true + :get (constantly (:base cf/version))} + :root {:this true :get #(.getRoot ^js %)} diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index a5d5faff84..4ba7745a4f 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -1,6 +1,11 @@ ## 1.5.0 (Unreleased) - **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering` +- **plugins-runtime**: Added `version` field that returns the current version +- **plugin-types**: Fix penpot.openPage() to navigate in same tab by default +- **plugin-types**: Added `createVariantFromComponents` +- **plugin-types**: Change return type of `combineAsVariants` +- **plugin-types**: Added `textBounds` property for text shapes ## 1.4.2 (2026-01-21) diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 84cff58440..91dbb38b84 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -768,6 +768,11 @@ export interface CommonLayout { * Represents the context of Penpot, providing access to various Penpot functionalities and data. */ export interface Context { + /** + * Returns the current penpot version. + */ + readonly version: string; + /** * The root shape in the current Penpot context. Requires `content:read` permission. * diff --git a/plugins/libs/plugins-runtime/src/lib/api/index.ts b/plugins/libs/plugins-runtime/src/lib/api/index.ts index 2f8e9e58d9..00d64e58ed 100644 --- a/plugins/libs/plugins-runtime/src/lib/api/index.ts +++ b/plugins/libs/plugins-runtime/src/lib/api/index.ts @@ -165,6 +165,10 @@ export function createApi( // Penpot State API + get version(): string { + return plugin.context.version; + }, + get root(): Shape | null { checkPermission('content:read'); return plugin.context.root; From 81d90be4c9ade21decb27d49eb09faa14aa31690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Torr=C3=B3?= Date: Wed, 18 Mar 2026 16:04:04 +0100 Subject: [PATCH 09/22] :bug: Fix rasterizer initialization to only run when render-wasm/v1 is active (#8669) --- frontend/src/app/main.cljs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/src/app/main.cljs b/frontend/src/app/main.cljs index 7bf4afecc7..68dfb71e51 100644 --- a/frontend/src/app/main.cljs +++ b/frontend/src/app/main.cljs @@ -58,6 +58,14 @@ [] (mf/render! app-root (mf/element ui/app))) +(defn- initialize-rasterizer + [] + (ptk/reify ::initialize-rasterizer + ptk/EffectEvent + (effect [_ state _] + (when (feat/active-feature? state "render-wasm/v1") + (thr/init!))))) + (defn initialize [] (ptk/reify ::initialize @@ -93,12 +101,12 @@ (rx/map deref) (rx/filter dp/is-authenticated?) (rx/take 1) - (rx/map #(ws/initialize))))) + (rx/map #(ws/initialize))) - ptk/EffectEvent - (effect [_ state _] - (when-not (feat/active-feature? state "render-wasm/v1") - (thr/init!))))) + (->> stream + (rx/filter (ptk/type? ::feat/initialize)) + (rx/take 1) + (rx/map #(initialize-rasterizer))))))) (defn ^:export init [options] From 5ba53f7296ea49962884628a095889df1fbe7c68 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 17 Mar 2026 09:16:46 +0100 Subject: [PATCH 10/22] :tada: Add background blur for wasm render --- common/src/app/common/flags.cljc | 3 +- .../render-wasm/get-file-background-blur.json | 160 ++++++++++++++++++ frontend/playwright/ui/pages/WorkspacePage.js | 7 +- .../ui/render-wasm-specs/shapes.spec.js | 17 ++ ...r-on-shapes-overlapping-other-shapes-1.png | Bin 0 -> 145504 bytes .../playwright/ui/specs/design-tab.spec.js | 84 +++++++++ .../app/main/ui/inspect/attributes/blur.cljs | 26 ++- frontend/src/app/main/ui/shapes/filters.cljs | 4 +- .../workspace/sidebar/options/menus/blur.cljs | 35 +++- .../workspace/sidebar/options/menus/blur.scss | 4 + frontend/src/app/util/code_gen/style_css.cljs | 1 + .../app/util/code_gen/style_css_formats.cljs | 1 + .../app/util/code_gen/style_css_values.cljs | 10 +- frontend/translations/en.po | 8 + frontend/translations/es.po | 8 + render-wasm/src/render.rs | 159 ++++++++++++++++- render-wasm/src/render/surfaces.rs | 4 + render-wasm/src/shapes.rs | 2 + render-wasm/src/shapes/blurs.rs | 1 + render-wasm/src/wasm/blurs.rs | 4 +- 20 files changed, 511 insertions(+), 27 deletions(-) create mode 100644 frontend/playwright/data/render-wasm/get-file-background-blur.json create mode 100644 frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-background-blur-on-shapes-overlapping-other-shapes-1.png diff --git a/common/src/app/common/flags.cljc b/common/src/app/common/flags.cljc index 23ef653592..9b64aca37b 100644 --- a/common/src/app/common/flags.cljc +++ b/common/src/app/common/flags.cljc @@ -162,7 +162,8 @@ ;; Activates the nitrate module :nitrate - :mcp}) + :mcp + :background-blur}) (def all-flags (set/union email login varia)) diff --git a/frontend/playwright/data/render-wasm/get-file-background-blur.json b/frontend/playwright/data/render-wasm/get-file-background-blur.json new file mode 100644 index 0000000000..34bbba7ad8 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-file-background-blur.json @@ -0,0 +1,160 @@ +{ + "~:features": { + "~#set": [ + "fdata/path-data", + "plugins/runtime", + "design-tokens/v1", + "variants/v1", + "layout/grid", + "styles/v2", + "fdata/objects-map", + "text-editor/v2", + "render-wasm/v1", + "text-editor-wasm/v1", + "components/v2", + "fdata/shape-data-type" + ] + }, + "~:team-id": "~ud7430f09-4f59-8049-8007-6277bb7586f6", + "~:permissions": { + "~:type": "~:membership", + "~:is-owner": true, + "~:is-admin": true, + "~:can-edit": true, + "~:can-read": true, + "~:is-logged": true + }, + "~:has-media-trimmed": false, + "~:comment-thread-seqn": 0, + "~:name": "test_feature", + "~:revn": 39, + "~:modified-at": "~m1773253429056", + "~:vern": 0, + "~:id": "~u93bfc923-66b2-813c-8007-b2725507ba08", + "~:is-shared": false, + "~:migrations": { + "~#ordered-set": [ + "legacy-2", + "legacy-3", + "legacy-5", + "legacy-6", + "legacy-7", + "legacy-8", + "legacy-9", + "legacy-10", + "legacy-11", + "legacy-12", + "legacy-13", + "legacy-14", + "legacy-16", + "legacy-17", + "legacy-18", + "legacy-19", + "legacy-25", + "legacy-26", + "legacy-27", + "legacy-28", + "legacy-29", + "legacy-31", + "legacy-32", + "legacy-33", + "legacy-34", + "legacy-36", + "legacy-37", + "legacy-38", + "legacy-39", + "legacy-40", + "legacy-41", + "legacy-42", + "legacy-43", + "legacy-44", + "legacy-45", + "legacy-46", + "legacy-47", + "legacy-48", + "legacy-49", + "legacy-50", + "legacy-51", + "legacy-52", + "legacy-53", + "legacy-54", + "legacy-55", + "legacy-56", + "legacy-57", + "legacy-59", + "legacy-62", + "legacy-65", + "legacy-66", + "legacy-67", + "0001-remove-tokens-from-groups", + "0002-normalize-bool-content-v2", + "0002-clean-shape-interactions", + "0003-fix-root-shape", + "0003-convert-path-content-v2", + "0005-deprecate-image-type", + "0006-fix-old-texts-fills", + "0008-fix-library-colors-v4", + "0009-clean-library-colors", + "0009-add-partial-text-touched-flags", + "0010-fix-swap-slots-pointing-non-existent-shapes", + "0011-fix-invalid-text-touched-flags", + "0012-fix-position-data", + "0013-fix-component-path", + "0013-clear-invalid-strokes-and-fills", + "0014-fix-tokens-lib-duplicate-ids", + "0014-clear-components-nil-objects", + "0015-fix-text-attrs-blank-strings", + "0015-clean-shadow-color", + "0016-copy-fills-from-position-data-to-text-node" + ] + }, + "~:version": 67, + "~:project-id": "~ud7430f09-4f59-8049-8007-6277bb765abd", + "~:created-at": "~m1773229633566", + "~:backend": "legacy-db", + "~:data": { + "~:pages": [ + "~u93bfc923-66b2-813c-8007-b2725507ba09" + ], + "~:pages-index": { + "~u93bfc923-66b2-813c-8007-b2725507ba09": { + "~:objects": { + "~#penpot/objects-map/v2": { + "~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b2804a521773\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ac\",\"~u11813dac-4dc3-80d8-8007-b28053db9865\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a36\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a4\",\"~u5b58e018-fa5e-805d-8007-b27294ad5af1\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a7\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14af\",\"~u11813dac-4dc3-80d8-8007-b28053db9864\",\"~u11813dac-4dc3-80d8-8007-b2806dfde19d\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14aa\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ab\"]]]", + "~u11813dac-4dc3-80d8-8007-b2806dfd8a36": "[\"~#shape\",[\"^ \",\"~:y\",93.99999856948853,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",93.99999856948853]],[\"^:\",[\"^ \",\"~:x\",1610.9999368190765,\"~:y\",93.99999856948853]],[\"^:\",[\"^ \",\"~:x\",1610.9999368190765,\"~:y\",396.9999985694885]],[\"^:\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",396.9999985694885]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a36\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1307.9999368190765,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",93.99999856948853,\"^6\",303,\"~:height\",303,\"~:x1\",1307.9999368190765,\"~:y1\",93.99999856948853,\"~:x2\",1610.9999368190765,\"~:y2\",396.9999985694885]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b2806dfd8a37\",\"~u11813dac-4dc3-80d8-8007-b2806dfde19c\"]]]", + "~u11813dac-4dc3-80d8-8007-b2806dfd8a37": "[\"~#shape\",[\"^ \",\"~:y\",93.99999856948853,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1416.9999368190765,\"~:y\",93.99999856948853]],[\"^<\",[\"^ \",\"~:x\",1610.9999368190765,\"~:y\",93.99999856948853]],[\"^<\",[\"^ \",\"~:x\",1610.9999368190765,\"~:y\",293.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1416.9999368190765,\"~:y\",293.9999985694885]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a37\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a36\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1416.9999368190765,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1416.9999368190765,\"~:y\",93.99999856948853,\"^8\",194,\"~:height\",200,\"~:x1\",1416.9999368190765,\"~:y1\",93.99999856948853,\"~:x2\",1610.9999368190765,\"~:y2\",293.9999985694885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]", + "~u5b58e018-fa5e-805d-8007-b27294ad5af1": "[\"~#shape\",[\"^ \",\"~:y\",-22.00000122055286,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",445.0000023918125,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",162.9999992063131,\"~:y\",-22.000001220552832]],[\"^<\",[\"^ \",\"~:x\",608.0000015981256,\"~:y\",-22.000001220552832]],[\"^<\",[\"^ \",\"~:x\",608.0000015981256,\"~:y\",423.0000182011825]],[\"^<\",[\"^ \",\"~:x\",162.9999992063131,\"~:y\",423.0000182011825]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",4,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u5b58e018-fa5e-805d-8007-b27294ad5af1\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",162.99999920631308,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",162.99999920631308,\"~:y\",-22.00000122055286,\"^8\",445.0000023918125,\"~:height\",445.0000194217354,\"~:x1\",162.99999920631308,\"~:y1\",-22.00000122055286,\"~:x2\",608.0000015981256,\"~:y2\",423.0000182011825]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",445.0000194217354,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b2804a521773": "[\"~#shape\",[\"^ \",\"~:y\",49.00000846385956,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",49.00000846385956]],[\"^:\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",49.00000846385956]],[\"^:\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",352.00000846385956]],[\"^:\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",352.00000846385956]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b2804a521773\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",234.00000047683716,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",49.00000846385956,\"^6\",303,\"~:height\",303,\"~:x1\",234.00000047683716,\"~:y1\",49.00000846385956,\"~:x2\",537.0000004768372,\"~:y2\",352.00000846385956]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u5b58e018-fa5e-805d-8007-b27288b0cd80\",\"~u5b58e018-fa5e-805d-8007-b2728b102fe6\"]]]", + "~u11813dac-4dc3-80d8-8007-b2806dfde19c": "[\"~#shape\",[\"^ \",\"~:y\",189.99999856948853,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",189.99999856948853]],[\"^<\",[\"^ \",\"~:x\",1513.9999368190765,\"~:y\",189.99999856948853]],[\"^<\",[\"^ \",\"~:x\",1513.9999368190765,\"~:y\",396.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",396.9999985694885]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b2806dfde19c\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b2806dfd8a36\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1307.9999368190765,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1307.9999368190765,\"~:y\",189.99999856948853,\"^8\",206,\"~:height\",207,\"~:x1\",1307.9999368190765,\"~:y1\",189.99999856948853,\"~:x2\",1513.9999368190765,\"~:y2\",396.9999985694885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b2806dfde19d": "[\"~#shape\",[\"^ \",\"~:y\",-32.00000238488809,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",302.9999596227117,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1307.9999330492521,\"~:y\",-32.00000238488809]],[\"^<\",[\"^ \",\"~:x\",1610.9998926719638,\"~:y\",-32.00000238488809]],[\"^<\",[\"^ \",\"~:x\",1610.9998926719638,\"~:y\",268.00000735984116]],[\"^<\",[\"^ \",\"~:x\",1307.9999330492521,\"~:y\",268.00000735984116]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",4,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b2806dfde19d\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1307.9999330492521,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1307.9999330492521,\"~:y\",-32.00000238488809,\"^8\",302.9999596227117,\"~:height\",300.00000974472925,\"~:x1\",1307.9999330492521,\"~:y1\",-32.00000238488809,\"~:x2\",1610.9998926719638,\"~:y2\",268.00000735984116]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",300.00000974472925,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14a4": "[\"~#shape\",[\"^ \",\"~:y\",592.0000023841858,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",592.0000023841858]],[\"^:\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",592.0000023841858]],[\"^:\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",895.0000023841858]],[\"^:\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",895.0000023841858]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a4\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817.9999825954437,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",592.0000023841858,\"^6\",303,\"~:height\",303,\"~:x1\",817.9999825954437,\"~:y1\",592.0000023841858,\"~:x2\",1120.9999825954437,\"~:y2\",895.0000023841858]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b280a6cd14a5\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a6\"]]]", + "~u11813dac-4dc3-80d8-8007-b28053db9864": "[\"~#shape\",[\"^ \",\"~:y\",97.00000524450644,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",299.9999982638824,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",770.9999871603877,\"~:y\",97.00000524450644]],[\"^<\",[\"^ \",\"~:x\",1070.99998542427,\"~:y\",97.00000524450644]],[\"^<\",[\"^ \",\"~:x\",1070.99998542427,\"~:y\",397.0000149892357]],[\"^<\",[\"^ \",\"~:x\",770.9999871603877,\"~:y\",397.0000149892357]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",4,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b28053db9864\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",770.9999871603877,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",770.9999871603877,\"~:y\",97.00000524450644,\"^8\",299.9999982638824,\"~:height\",300.00000974472925,\"~:x1\",770.9999871603877,\"~:y1\",97.00000524450644,\"~:x2\",1070.99998542427,\"~:y2\",397.0000149892357]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",300.00000974472925,\"~:flip-y\",null]]", + "~u5b58e018-fa5e-805d-8007-b2728b102fe6": "[\"~#shape\",[\"^ \",\"~:y\",145.00000846385956,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",145.00000846385956]],[\"^<\",[\"^ \",\"~:x\",440.00000047683716,\"~:y\",145.00000846385956]],[\"^<\",[\"^ \",\"~:x\",440.00000047683716,\"~:y\",352.00000846385956]],[\"^<\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",352.00000846385956]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2728b102fe6\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b2804a521773\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",234.00000047683716,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",145.00000846385956,\"^8\",206,\"~:height\",207,\"~:x1\",234.00000047683716,\"~:y1\",145.00000846385956,\"~:x2\",440.00000047683716,\"~:y2\",352.00000846385956]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14a5": "[\"~#shape\",[\"^ \",\"~:y\",592.0000023841858,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",592.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",592.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",792.0000023841858]],[\"^<\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",792.0000023841858]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a5\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a4\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",926.9999825954437,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",592.0000023841858,\"^8\",194,\"~:height\",200,\"~:x1\",926.9999825954437,\"~:y1\",592.0000023841858,\"~:x2\",1120.9999825954437,\"~:y2\",792.0000023841858]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b28053db9865": "[\"~#shape\",[\"^ \",\"~:y\",48.00000238418579,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",48.00000238418579]],[\"^:\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",48.00000238418579]],[\"^:\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",351.0000023841858]],[\"^:\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",351.0000023841858]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b28053db9865\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817.9999825954437,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",48.00000238418579,\"^6\",303,\"~:height\",303,\"~:x1\",817.9999825954437,\"~:y1\",48.00000238418579,\"~:x2\",1120.9999825954437,\"~:y2\",351.0000023841858]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b28053db9866\",\"~u11813dac-4dc3-80d8-8007-b28053db9867\"]]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14a6": "[\"~#shape\",[\"^ \",\"~:y\",688.0000023841858,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",688.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1023.9999825954437,\"~:y\",688.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1023.9999825954437,\"~:y\",895.0000023841858]],[\"^<\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",895.0000023841858]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a6\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a4\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817.9999825954437,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",688.0000023841858,\"^8\",206,\"~:height\",207,\"~:x1\",817.9999825954437,\"~:y1\",688.0000023841858,\"~:x2\",1023.9999825954437,\"~:y2\",895.0000023841858]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b28053db9866": "[\"~#shape\",[\"^ \",\"~:y\",48.00000238418579,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",48.00000238418579]],[\"^<\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",48.00000238418579]],[\"^<\",[\"^ \",\"~:x\",1120.9999825954437,\"~:y\",248.0000023841858]],[\"^<\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",248.0000023841858]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b28053db9866\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b28053db9865\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",926.9999825954437,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",926.9999825954437,\"~:y\",48.00000238418579,\"^8\",194,\"~:height\",200,\"~:x1\",926.9999825954437,\"~:y1\",48.00000238418579,\"~:x2\",1120.9999825954437,\"~:y2\",248.0000023841858]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14a7": "[\"~#shape\",[\"^ \",\"~:y\",637.9999985694885,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",637.9999985694885]],[\"^:\",[\"^ \",\"~:x\",1611.000058889389,\"~:y\",637.9999985694885]],[\"^:\",[\"^ \",\"~:x\",1611.000058889389,\"~:y\",940.9999985694885]],[\"^:\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",940.9999985694885]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a7\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1308.000058889389,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",637.9999985694885,\"^6\",303,\"~:height\",303,\"~:x1\",1308.000058889389,\"~:y1\",637.9999985694885,\"~:x2\",1611.000058889389,\"~:y2\",940.9999985694885]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b280a6cd14a8\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a9\"]]]", + "~u11813dac-4dc3-80d8-8007-b28053db9867": "[\"~#shape\",[\"^ \",\"~:y\",144.0000023841858,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",144.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1023.9999825954437,\"~:y\",144.0000023841858]],[\"^<\",[\"^ \",\"~:x\",1023.9999825954437,\"~:y\",351.0000023841858]],[\"^<\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",351.0000023841858]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b28053db9867\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b28053db9865\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817.9999825954437,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817.9999825954437,\"~:y\",144.0000023841858,\"^8\",206,\"~:height\",207,\"~:x1\",817.9999825954437,\"~:y1\",144.0000023841858,\"~:x2\",1023.9999825954437,\"~:y2\",351.0000023841858]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]", + "~u5b58e018-fa5e-805d-8007-b27288b0cd80": "[\"~#shape\",[\"^ \",\"~:y\",49.00000846385956,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",49.00000846385956]],[\"^<\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",49.00000846385956]],[\"^<\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",249.00000846385956]],[\"^<\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",249.00000846385956]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u5b58e018-fa5e-805d-8007-b27288b0cd80\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b2804a521773\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",343.00000047683716,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",49.00000846385956,\"^8\",194,\"~:height\",200,\"~:x1\",343.00000047683716,\"~:y1\",49.00000846385956,\"~:x2\",537.0000004768372,\"~:y2\",249.00000846385956]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14ac": "[\"~#shape\",[\"^ \",\"~:y\",593.0000084638596,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:index\",2,\"~:name\",\"Group\",\"~:width\",303,\"~:type\",\"~:group\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",593.0000084638596]],[\"^:\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",593.0000084638596]],[\"^:\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",896.0000084638596]],[\"^:\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",896.0000084638596]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ac\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",234.00000047683716,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",593.0000084638596,\"^6\",303,\"~:height\",303,\"~:x1\",234.00000047683716,\"~:y1\",593.0000084638596,\"~:x2\",537.0000004768372,\"~:y2\",896.0000084638596]],\"~:fills\",[],\"~:flip-x\",null,\"^D\",303,\"~:flip-y\",null,\"~:shapes\",[\"~u11813dac-4dc3-80d8-8007-b280a6cd14ad\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ae\"]]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14ad": "[\"~#shape\",[\"^ \",\"~:y\",593.0000084638596,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",593.0000084638596]],[\"^<\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",593.0000084638596]],[\"^<\",[\"^ \",\"~:x\",537.0000004768372,\"~:y\",793.0000084638596]],[\"^<\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",793.0000084638596]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ad\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ac\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",343.00000047683716,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",343.00000047683716,\"~:y\",593.0000084638596,\"^8\",194,\"~:height\",200,\"~:x1\",343.00000047683716,\"~:y1\",593.0000084638596,\"~:x2\",537.0000004768372,\"~:y2\",793.0000084638596]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14ae": "[\"~#shape\",[\"^ \",\"~:y\",689.0000084638596,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",689.0000084638596]],[\"^<\",[\"^ \",\"~:x\",440.00000047683716,\"~:y\",689.0000084638596]],[\"^<\",[\"^ \",\"~:x\",440.00000047683716,\"~:y\",896.0000084638596]],[\"^<\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",896.0000084638596]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ae\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ac\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",234.00000047683716,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",234.00000047683716,\"~:y\",689.0000084638596,\"^8\",206,\"~:height\",207,\"~:x1\",234.00000047683716,\"~:y1\",689.0000084638596,\"~:x2\",440.00000047683716,\"~:y2\",896.0000084638596]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14af": "[\"~#shape\",[\"^ \",\"~:y\",512.0000283168957,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",445.0000023918125,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",157.99998871589563,\"~:y\",512.0000283168957]],[\"^<\",[\"^ \",\"~:x\",602.9999911077081,\"~:y\",512.0000283168957]],[\"^<\",[\"^ \",\"~:x\",602.9999911077081,\"~:y\",957.000047738631]],[\"^<\",[\"^ \",\"~:x\",157.99998871589563,\"~:y\",957.000047738631]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",50,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14af\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",157.9999887158956,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",157.9999887158956,\"~:y\",512.0000283168957,\"^8\",445.0000023918125,\"~:height\",445.00001942173526,\"~:x1\",157.9999887158956,\"~:y1\",512.0000283168957,\"~:x2\",602.9999911077081,\"~:y2\",957.000047738631]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",445.00001942173526,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14a8": "[\"~#shape\",[\"^ \",\"~:y\",637.9999985694885,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",194,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1417.000058889389,\"~:y\",637.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1611.000058889389,\"~:y\",637.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1611.000058889389,\"~:y\",837.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1417.000058889389,\"~:y\",837.9999985694885]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a8\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a7\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1417.000058889389,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1417.000058889389,\"~:y\",637.9999985694885,\"^8\",194,\"~:height\",200,\"~:x1\",1417.000058889389,\"~:y1\",637.9999985694885,\"~:x2\",1611.000058889389,\"~:y2\",837.9999985694885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#174be9\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",200,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14a9": "[\"~#shape\",[\"^ \",\"~:y\",733.9999985694885,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",206,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",733.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1514.000058889389,\"~:y\",733.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1514.000058889389,\"~:y\",940.9999985694885]],[\"^<\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",940.9999985694885]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a9\",\"~:parent-id\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14a7\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1308.000058889389,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1308.000058889389,\"~:y\",733.9999985694885,\"^8\",206,\"~:height\",207,\"~:x1\",1308.000058889389,\"~:y1\",733.9999985694885,\"~:x2\",1514.000058889389,\"~:y2\",940.9999985694885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#c8f00d\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",207,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14aa": "[\"~#shape\",[\"^ \",\"~:y\",511.9999976151119,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",302.9999596227117,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1308.0000551195646,\"~:y\",511.9999976151119]],[\"^<\",[\"^ \",\"~:x\",1611.0000147422763,\"~:y\",511.9999976151119]],[\"^<\",[\"^ \",\"~:x\",1611.0000147422763,\"~:y\",812.0000073598412]],[\"^<\",[\"^ \",\"~:x\",1308.0000551195646,\"~:y\",812.0000073598412]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",50,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14aa\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",1308.0000551195646,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1308.0000551195646,\"~:y\",511.9999976151119,\"^8\",302.9999596227117,\"~:height\",300.0000097447293,\"~:x1\",1308.0000551195646,\"~:y1\",511.9999976151119,\"~:x2\",1611.0000147422763,\"~:y2\",812.0000073598412]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",300.0000097447293,\"~:flip-y\",null]]", + "~u11813dac-4dc3-80d8-8007-b280a6cd14ab": "[\"~#shape\",[\"^ \",\"~:y\",512.00002813269,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",299.9999982638824,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",743.9999566428096,\"~:y\",512.00002813269]],[\"^<\",[\"^ \",\"~:x\",1043.999954906692,\"~:y\",512.00002813269]],[\"^<\",[\"^ \",\"~:x\",1043.999954906692,\"~:y\",812.0000378774193]],[\"^<\",[\"^ \",\"~:x\",743.9999566428096,\"~:y\",812.0000378774193]]],\"~:r2\",20,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",20,\"~:blur\",[\"^ \",\"~:id\",\"~u5b58e018-fa5e-805d-8007-b2729af5ed71\",\"^9\",\"~:background-blur\",\"~:value\",50,\"~:hidden\",false],\"~:r1\",20,\"^B\",\"~u11813dac-4dc3-80d8-8007-b280a6cd14ab\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",743.9999566428096,\"~:proportion\",1,\"~:r4\",20,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",743.9999566428096,\"~:y\",512.00002813269,\"^8\",299.9999982638824,\"~:height\",300.00000974472937,\"~:x1\",743.9999566428096,\"~:y1\",512.00002813269,\"~:x2\",1043.999954906692,\"~:y2\",812.0000378774193]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffffff\",\"~:fill-opacity\",0.30392156862745096]],\"~:flip-x\",null,\"^N\",300.00000974472937,\"~:flip-y\",null]]" + } + }, + "~:id": "~u93bfc923-66b2-813c-8007-b2725507ba09", + "~:name": "Page 1" + } + }, + "~:id": "~u93bfc923-66b2-813c-8007-b2725507ba08", + "~:options": { + "~:components-v2": true, + "~:base-font-size": "16px" + } + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 92f9b9bef2..1dcac5c4ca 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -212,6 +212,7 @@ export class WorkspacePage extends BaseWebSocketPage { async goToWorkspace({ fileId = this.fileId ?? WorkspacePage.anyFileId, pageId = this.pageId ?? WorkspacePage.anyPageId, + pageName = "Page 1", } = {}) { await this.page.goto( `/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`, @@ -219,12 +220,12 @@ export class WorkspacePage extends BaseWebSocketPage { this.#ws = await this.waitForNotificationsWebSocket(); await this.#ws.mockOpen(); - await this.#waitForWebSocketReadiness(); + await this.#waitForWebSocketReadiness(pageName); } - async #waitForWebSocketReadiness() { + async #waitForWebSocketReadiness(pageName) { // TODO: find a better event to settle whether the app is ready to receive notifications via ws - await expect(this.pageName).toHaveText("Page 1", { timeout: 30000 }) + await expect(this.pageName).toHaveText(pageName, { timeout: 30000 }) } async sendPresenceMessage(fixture) { diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index 4d0597b9aa..21fb267806 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -545,5 +545,22 @@ test("BUG 13610 - Huge inner strokes", async ({ pageId: "effcbebc-b8c8-802f-8007-b11dd34fe191", }); await workspace.waitForFirstRenderWithoutUI(); + + await expect(workspace.canvas).toHaveScreenshot(); +}); + +test("Renders background blur on shapes overlapping other shapes", async ({ + page, +}) => { + const workspace = new WasmWorkspacePage(page); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-background-blur.json"); + + await workspace.goToWorkspace({ + id: "93bfc923-66b2-813c-8007-b2725507ba08", + pageId: "93bfc923-66b2-813c-8007-b2725507ba09", + }); + await workspace.waitForFirstRenderWithoutUI(); + await expect(workspace.canvas).toHaveScreenshot(); }); \ No newline at end of file diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-background-blur-on-shapes-overlapping-other-shapes-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-background-blur-on-shapes-overlapping-other-shapes-1.png new file mode 100644 index 0000000000000000000000000000000000000000..69f126eeaf52b8a456e598b7c423b02081a0bb61 GIT binary patch literal 145504 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 literal 0 HcmV?d00001 diff --git a/frontend/playwright/ui/specs/design-tab.spec.js b/frontend/playwright/ui/specs/design-tab.spec.js index 489579b844..8fe67b9d3a 100644 --- a/frontend/playwright/ui/specs/design-tab.spec.js +++ b/frontend/playwright/ui/specs/design-tab.spec.js @@ -231,6 +231,90 @@ test("BUG 9061 - Group blur visibility toggle icon not updating", async ({ await expect(blurIcon).toHaveAttribute("href", "#icon-hide"); }); +test.describe("Background blur", () => { + test("Shows background blur option in blur type select when both render-wasm and background-blur flags are active", async ({ + page, + }) => { + const workspace = new WasmWorkspacePage(page); + await workspace.mockConfigFlags(["enable-background-blur"]); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-background-blur.json"); + + await workspace.goToWorkspace({ + fileId: "93bfc923-66b2-813c-8007-b2725507ba08", + pageId: "93bfc923-66b2-813c-8007-b2725507ba09", + }); + + // Click the first Rectangle (which has background-blur type) + await workspace.clickLeafLayer("Rectangle"); + + // The blur type select should show "Background blur" as the current value + const blurTypeSelect = workspace.page + .getByTestId("blur-info") + .getByRole("combobox"); + await expect(blurTypeSelect).toBeVisible(); + await expect(blurTypeSelect).toContainText("Background blur"); + }); + + test("Shows both layer-blur and background-blur options in the blur type dropdown", async ({ + page, + }) => { + const workspace = new WasmWorkspacePage(page); + await workspace.mockConfigFlags(["enable-background-blur"]); + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-background-blur.json"); + + await workspace.goToWorkspace({ + fileId: "93bfc923-66b2-813c-8007-b2725507ba08", + pageId: "93bfc923-66b2-813c-8007-b2725507ba09", + }); + + await workspace.clickLeafLayer("Rectangle"); + + // Open the blur type dropdown + const blurTypeSelect = workspace.page + .getByTestId("blur-info") + .getByRole("combobox"); + await blurTypeSelect.click(); + + // Both options should be visible + const layerBlurOption = workspace.page.getByRole("option", { + name: "Layer blur", + }); + const backgroundBlurOption = workspace.page.getByRole("option", { + name: "Background blur", + }); + await expect(layerBlurOption).toBeVisible(); + await expect(backgroundBlurOption).toBeVisible(); + }); + + test("Does not show background blur option when background-blur flag is not active", async ({ + page, + }) => { + const workspace = new WasmWorkspacePage(page); + // No enable-background-blur flag + await workspace.setupEmptyFile(); + await workspace.mockGetFile("render-wasm/get-file-background-blur.json"); + + await workspace.goToWorkspace({ + fileId: "93bfc923-66b2-813c-8007-b2725507ba08", + pageId: "93bfc923-66b2-813c-8007-b2725507ba09", + }); + + await workspace.clickLeafLayer("Rectangle"); + + // Without the background-blur flag, no blur type dropdown should appear. + // Instead, a plain "Blur" label is shown. + const blurTypeSelect = workspace.page + .getByTestId("blur-info") + .getByRole("combobox"); + await expect(blurTypeSelect).not.toBeVisible(); + + const blurLabel = workspace.page.getByTestId("blur-info").getByText("Blur"); + await expect(blurLabel).toBeVisible(); + }); +}); + test("BUG 9543 - Layout padding inputs not showing 'mixed' when needed", async ({ page, }) => { diff --git a/frontend/src/app/main/ui/inspect/attributes/blur.cljs b/frontend/src/app/main/ui/inspect/attributes/blur.cljs index 21a21a6b4b..553178483d 100644 --- a/frontend/src/app/main/ui/inspect/attributes/blur.cljs +++ b/frontend/src/app/main/ui/inspect/attributes/blur.cljs @@ -17,6 +17,11 @@ (defn- has-blur? [shape] (:blur shape)) +(defn- blur-css-property [shape] + (if (= :background-blur (get-in shape [:blur :type])) + :backdrop-filter + :filter)) + (mf/defc blur-panel [{:keys [objects shapes]}] (let [shapes (->> shapes (filter has-blur?))] @@ -27,15 +32,18 @@ :class (stl/css :title-wrapper) :title-class (stl/css :blur-attr-title)} (when (= (count shapes) 1) - [:> copy-button* {:data (css/get-css-property objects (first shapes) :filter) - :class (stl/css :copy-btn-title)}])] + (let [prop (blur-css-property (first shapes))] + [:> copy-button* {:data (css/get-css-property objects (first shapes) prop) + :class (stl/css :copy-btn-title)}]))] [:div {:class (stl/css :attributes-content)} (for [shape shapes] - [:div {:class (stl/css :blur-row) - :key (dm/str "block-" (:id shape) "-blur")} - [:div {:class (stl/css :global/attr-label)} "Filter"] - [:div {:class (stl/css :global/attr-value)} - [:> copy-button* {:data (css/get-css-property objects shape :filter)} - [:div {:class (stl/css :button-children)} - (css/get-css-value objects shape :filter)]]]])]]))) + (let [prop (blur-css-property shape)] + [:div {:class (stl/css :blur-row) + :key (dm/str "block-" (:id shape) "-blur")} + [:div {:class (stl/css :global/attr-label)} + (if (= prop :backdrop-filter) "Backdrop Filter" "Filter")] + [:div {:class (stl/css :global/attr-value)} + [:> copy-button* {:data (css/get-css-property objects shape prop)} + [:div {:class (stl/css :button-children)} + (css/get-css-value objects shape prop)]]]]))]]))) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 9dcbd0672e..6e0adbebea 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -21,7 +21,9 @@ (defn filter-str [filter-id shape] (when (or (seq (->> (:shadow shape) (remove :hidden))) - (and (:blur shape) (-> shape :blur :hidden not))) + (and (:blur shape) + (-> shape :blur :hidden not) + (= :layer-blur (-> shape :blur :type)))) (str/ffmt "url(#%)" filter-id))) (mf/defc color-matrix diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs index 52a5570614..f1beba6eb0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs @@ -7,11 +7,15 @@ (ns app.main.ui.workspace.sidebar.options.menus.blur (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.workspace :as udw] [app.main.data.workspace.shapes :as dwsh] + [app.main.features :as features] [app.main.store :as st] [app.main.ui.components.numeric-input :refer [numeric-input*]] + [app.main.ui.components.select :refer [select]] [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :as i] @@ -31,6 +35,9 @@ (mf/defc blur-menu [{:keys [ids type values]}] (let [blur (:blur values) has-value? (not (nil? blur)) + render-wasm? (features/use-feature "render-wasm/v1") + bg-blur? (and render-wasm? + (contains? cf/flags :background-blur)) state* (mf/use-state {:show-content true :show-more-options false}) @@ -75,12 +82,25 @@ :always (assoc-in [:blur :value] value))))) + handle-type-change + (mf/use-fn + (mf/deps change! ids) + (fn [value] + (st/emit! (udw/trigger-bounding-box-cloaking ids)) + (change! #(assoc-in % [:blur :type] (keyword value))))) + handle-toggle-visibility (mf/use-fn (mf/deps change! ids) (fn [] (st/emit! (udw/trigger-bounding-box-cloaking ids)) - (change! #(update-in % [:blur :hidden] not))))] + (change! #(update-in % [:blur :hidden] not)))) + + type-options + (mf/with-memo [bg-blur?] + (cond-> [{:value "layer-blur" :label (tr "workspace.options.blur-options.layer-blur")}] + bg-blur? + (conj {:value "background-blur" :label (tr "workspace.options.blur-options.background-blur")})))] [:div {:class (stl/css :element-set)} [:div {:class (stl/css :element-title)} @@ -102,13 +122,20 @@ [:div {:class (stl/css :element-set-content)} [:div {:class (stl/css-case :first-row true :hidden hidden?)} - [:div {:class (stl/css :blur-info)} + [:div {:class (stl/css :blur-info) + :data-testid "blur-info"} [:button {:class (stl/css-case :show-more true :selected more-options?) :on-click toggle-more-options} deprecated-icon/menu] - [:span {:class (stl/css :label)} - (tr "workspace.options.blur-options.title")]] + (if bg-blur? + [:& select {:class (stl/css :blur-type-select) + :default-value (d/name (:type blur)) + :options type-options + :disabled hidden? + :on-change handle-type-change}] + [:span {:class (stl/css :label)} + (tr "workspace.options.blur-options.title")])] [:div {:class (stl/css :actions)} [:> icon-button* {:variant "ghost" :aria-label (tr "workspace.options.blur-options.toggle-blur") diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss index e15a50c83e..c80e57e1ec 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.scss @@ -66,6 +66,10 @@ box-sizing: border-box; border: deprecated.$s-1 solid var(--input-border-color); } + .blur-type-select { + flex-grow: 1; + border-radius: 0 deprecated.$br-8 deprecated.$br-8 0; + } } .actions { @include deprecated.flexRow; diff --git a/frontend/src/app/util/code_gen/style_css.cljs b/frontend/src/app/util/code_gen/style_css.cljs index e72fc9a9ee..8354a73682 100644 --- a/frontend/src/app/util/code_gen/style_css.cljs +++ b/frontend/src/app/util/code_gen/style_css.cljs @@ -79,6 +79,7 @@ body { :border-end-end-radius :box-shadow :filter + :backdrop-filter :opacity :overflow :blend-mode diff --git a/frontend/src/app/util/code_gen/style_css_formats.cljs b/frontend/src/app/util/code_gen/style_css_formats.cljs index 74c074d2bc..547e022dc6 100644 --- a/frontend/src/app/util/code_gen/style_css_formats.cljs +++ b/frontend/src/app/util/code_gen/style_css_formats.cljs @@ -38,6 +38,7 @@ :border-color :border-color :box-shadow :shadows :filter :blur + :backdrop-filter :blur :gap :size-array :row-gap :size-array :column-gap :size-array diff --git a/frontend/src/app/util/code_gen/style_css_values.cljs b/frontend/src/app/util/code_gen/style_css_values.cljs index cba1991599..83b6bd7a98 100644 --- a/frontend/src/app/util/code_gen/style_css_values.cljs +++ b/frontend/src/app/util/code_gen/style_css_values.cljs @@ -256,7 +256,14 @@ (defn- get-filter [shape] (when-not (cgc/svg-markup? shape) - (get-in shape [:blur :value]))) + (when (= :layer-blur (get-in shape [:blur :type])) + (get-in shape [:blur :value])))) + +(defn- get-backdrop-filter + [shape] + (when-not (cgc/svg-markup? shape) + (when (= :background-blur (get-in shape [:blur :type])) + (get-in shape [:blur :value])))) (defn- get-display [shape] @@ -546,6 +553,7 @@ :opacity (get-opacity shape) :box-shadow (get-box-shadow shape) :filter (get-filter shape) + :backdrop-filter (get-backdrop-filter shape) :overflow (get-overflow shape) ;; Display diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ebe68d176b..c36e440412 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -6097,6 +6097,14 @@ msgstr "Remove blur" msgid "workspace.options.blur-options.title" msgstr "Blur" +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.layer-blur" +msgstr "Layer blur" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.background-blur" +msgstr "Background blur" + #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:91 msgid "workspace.options.blur-options.title.group" msgstr "Group blur" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 2721f60649..4d58a6dc7f 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -6059,6 +6059,14 @@ msgstr "Eliminar desenfoque" msgid "workspace.options.blur-options.title" msgstr "Desenfoque" +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.layer-blur" +msgstr "Desenfoque de capa" + +#: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +msgid "workspace.options.blur-options.background-blur" +msgstr "Desenfoque de fondo" + #: src/app/main/ui/workspace/sidebar/options/menus/blur.cljs:91 msgid "workspace.options.blur-options.title.group" msgstr "Desenfoque del grupo" diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 271a7bc38d..68748effe0 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -281,6 +281,9 @@ pub(crate) struct RenderState { pub current_tile: Option, pub sampling_options: skia::SamplingOptions, pub render_area: Rect, + // render_area expanded by surface margins — used for visibility checks so that + // shapes in the margin zone are rendered (needed for background blur sampling). + pub render_area_with_margins: Rect, pub tile_viewbox: tiles::TileViewbox, pub tiles: tiles::TileHashMap, pub pending_tiles: PendingTiles, @@ -358,6 +361,7 @@ impl RenderState { current_tile: None, sampling_options, render_area: Rect::new_empty(), + render_area_with_margins: Rect::new_empty(), tiles, tile_viewbox: tiles::TileViewbox::new_with_interest( viewbox, @@ -426,6 +430,104 @@ impl RenderState { shape.frame_clip_layer_blur() } + /// Renders background blur effect directly to the Current surface. + /// Must be called BEFORE any save_layer for the shape's own opacity/blend, + /// so that the backdrop blur is independent of the shape's visual properties. + fn render_background_blur(&mut self, shape: &Shape) { + if self.options.is_fast_mode() { + return; + } + if matches!(shape.shape_type, Type::Text(_)) || matches!(shape.shape_type, Type::SVGRaw(_)) + { + return; + } + let blur = match shape + .blur + .filter(|b| !b.hidden && b.blur_type == BlurType::BackgroundBlur) + { + Some(blur) => blur, + None => return, + }; + + let scale = self.get_scale(); + let scaled_sigma = radius_to_sigma(blur.value * scale); + // Cap sigma so the blur kernel (≈3σ) stays within the tile margin. + // This prevents visible seams at tile boundaries when zoomed in. + let margin = self.surfaces.margins().width as f32; + let max_sigma = margin / 3.0; + let capped_sigma = scaled_sigma.min(max_sigma); + + let blur_filter = match skia::image_filters::blur( + (capped_sigma, capped_sigma), + skia::TileMode::Clamp, + None, + None, + ) { + Some(filter) => filter, + None => return, + }; + + let snapshot = self.surfaces.snapshot(SurfaceId::Current); + let translation = self + .surfaces + .get_render_context_translation(self.render_area, scale); + + let center = shape.center(); + let mut matrix = shape.transform; + matrix.post_translate(center); + matrix.pre_translate(-center); + + let canvas = self.surfaces.canvas(SurfaceId::Current); + canvas.save(); + + // Current has no render context transform (identity canvas). + // Apply scale + translate + shape transform so the clip maps + // from shape-local coords to device pixels correctly. + canvas.scale((scale, scale)); + canvas.translate(translation); + canvas.concat(&matrix); + + // Clip to shape's path based on shape type + match &shape.shape_type { + Type::Rect(data) if data.corners.is_some() => { + let rrect = RRect::new_rect_radii(shape.selrect, data.corners.as_ref().unwrap()); + canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true); + } + Type::Frame(data) if data.corners.is_some() => { + let rrect = RRect::new_rect_radii(shape.selrect, data.corners.as_ref().unwrap()); + canvas.clip_rrect(rrect, skia::ClipOp::Intersect, true); + } + Type::Rect(_) | Type::Frame(_) => { + canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true); + } + Type::Circle => { + let mut pb = skia::PathBuilder::new(); + pb.add_oval(shape.selrect, None, None); + canvas.clip_path(&pb.detach(), skia::ClipOp::Intersect, true); + } + _ => { + if let Some(path) = shape.get_skia_path() { + canvas.clip_path(&path, skia::ClipOp::Intersect, true); + } else { + canvas.clip_rect(shape.selrect, skia::ClipOp::Intersect, true); + } + } + } + + // Reset matrix so snapshot draws pixel-for-pixel on the surface. + // Clips survive reset_matrix (stored in device coords). + canvas.reset_matrix(); + + // Use Src blend to replace content within the clip with the + // blurred version (not SrcOver which would double-render). + let mut paint = skia::Paint::default(); + paint.set_image_filter(blur_filter); + paint.set_blend_mode(skia::BlendMode::Src); + canvas.draw_image(&snapshot, (0, 0), Some(&paint)); + + canvas.restore(); + } + /// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`. /// Certain off-screen passes (e.g. shadow masks) must render shapes without /// inheriting ancestor blur. This helper guarantees the flag is restored. @@ -788,6 +890,22 @@ impl RenderState { // We don't want to change the value in the global state let mut shape: Cow = Cow::Borrowed(shape); + + // Remove background blur from the shape so it doesn't get processed + // as a layer blur. The actual rendering is done before the save_layer + // in render_background_blur() so it's independent of shape opacity. + if !fast_mode + && apply_to_current_surface + && fills_surface_id == SurfaceId::Fills + && !matches!(shape.shape_type, Type::Text(_)) + && !matches!(shape.shape_type, Type::SVGRaw(_)) + && shape + .blur + .is_some_and(|b| !b.hidden && b.blur_type == BlurType::BackgroundBlur) + { + shape.to_mut().set_blur(None); + } + let frame_has_blur = Self::frame_clip_layer_blur(&shape).is_some(); let shape_has_blur = shape.blur.is_some(); @@ -1181,9 +1299,18 @@ impl RenderState { pub fn update_render_context(&mut self, tile: tiles::Tile) { self.current_tile = Some(tile); - self.render_area = tiles::get_tile_rect(tile, self.get_scale()); - self.surfaces - .update_render_context(self.render_area, self.get_scale()); + let scale = self.get_scale(); + self.render_area = tiles::get_tile_rect(tile, scale); + let margins = self.surfaces.margins(); + let margin_w = margins.width as f32 / scale; + let margin_h = margins.height as f32 / scale; + self.render_area_with_margins = skia::Rect::from_ltrb( + self.render_area.left - margin_w, + self.render_area.top - margin_h, + self.render_area.right + margin_w, + self.render_area.bottom + margin_h, + ); + self.surfaces.update_render_context(self.render_area, scale); } pub fn cancel_animation_frame(&mut self) { @@ -1685,7 +1812,7 @@ impl RenderState { // Account for the shadow offset so the temporary surface fully contains the shifted blur. bounds.offset(world_offset); // Early cull if the shadow bounds are outside the render area. - if !bounds.intersects(self.render_area) { + if !bounds.intersects(self.render_area_with_margins) { return; } @@ -2051,11 +2178,11 @@ impl RenderState { let is_visible = if is_container || has_effects { let element_extrect = extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale)); - element_extrect.intersects(self.render_area) + element_extrect.intersects(self.render_area_with_margins) && !transformed_element.visually_insignificant(scale, tree) } else { let selrect = transformed_element.selrect(); - selrect.intersects(self.render_area) + selrect.intersects(self.render_area_with_margins) && !transformed_element.visually_insignificant(scale, tree) }; @@ -2102,6 +2229,12 @@ impl RenderState { ); } + // Render background blur BEFORE save_layer so it modifies + // the backdrop independently of the shape's opacity. + if !node_render_state.is_root() && self.focus_mode.is_active() { + self.render_background_blur(element); + } + self.render_shape_enter(element, mask); } @@ -2320,10 +2453,22 @@ impl RenderState { if !self.surfaces.has_cached_tile_surface(next_tile) { if let Some(ids) = self.tiles.get_shapes_at(next_tile) { + // Check if any shape on this tile has a background blur. + // If so, we need ALL root shapes rendered (not just those + // assigned to this tile) because the blur snapshots Current + // which must contain the shapes behind it. + let tile_has_bg_blur = ids.iter().any(|id| { + tree.get(id).is_some_and(|s| { + s.blur.is_some_and(|b| { + !b.hidden && b.blur_type == BlurType::BackgroundBlur + }) + }) + }); + // We only need first level shapes, in the same order as the parent node let mut valid_ids = Vec::with_capacity(ids.len()); for root_id in root_ids.iter() { - if ids.contains(root_id) { + if tile_has_bg_blur || ids.contains(root_id) { valid_ids.push(*root_id); } } diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 11239c2007..536737daba 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -115,6 +115,10 @@ impl Surfaces { self.tiles.clear(); } + pub fn margins(&self) -> skia::ISize { + self.margins + } + pub fn resize(&mut self, gpu_state: &mut GpuState, new_width: i32, new_height: i32) { self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)); } diff --git a/render-wasm/src/shapes.rs b/render-wasm/src/shapes.rs index 390391e11b..dedf9ab770 100644 --- a/render-wasm/src/shapes.rs +++ b/render-wasm/src/shapes.rs @@ -1241,6 +1241,7 @@ impl Shape { let sigma = radius_to_sigma(blur.value * scale); skia::image_filters::blur((sigma, sigma), None, None, None) } + BlurType::BackgroundBlur => None, }) } @@ -1253,6 +1254,7 @@ impl Shape { let sigma = radius_to_sigma(blur.value * scale); skia::MaskFilter::blur(skia::BlurStyle::Normal, sigma, Some(true)) } + BlurType::BackgroundBlur => None, }) } diff --git a/render-wasm/src/shapes/blurs.rs b/render-wasm/src/shapes/blurs.rs index 543e11efa8..828dcb10d3 100644 --- a/render-wasm/src/shapes/blurs.rs +++ b/render-wasm/src/shapes/blurs.rs @@ -15,6 +15,7 @@ pub fn radius_to_sigma(radius: f32) -> f32 { #[derive(Debug, Clone, Copy, PartialEq)] pub enum BlurType { LayerBlur, + BackgroundBlur, } #[derive(Debug, Clone, Copy, PartialEq)] diff --git a/render-wasm/src/wasm/blurs.rs b/render-wasm/src/wasm/blurs.rs index 700ac053bc..e8e1a4242c 100644 --- a/render-wasm/src/wasm/blurs.rs +++ b/render-wasm/src/wasm/blurs.rs @@ -7,7 +7,8 @@ use crate::{with_current_shape_mut, STATE}; #[repr(u8)] #[allow(dead_code)] pub enum RawBlurType { - LayerBlur = 0, // odd naming to comply with cljs value + LayerBlur = 0, + BackgroundBlur = 1, } impl From for RawBlurType { @@ -20,6 +21,7 @@ impl From for BlurType { fn from(value: RawBlurType) -> Self { match value { RawBlurType::LayerBlur => BlurType::LayerBlur, + RawBlurType::BackgroundBlur => BlurType::BackgroundBlur, } } } From e630be1509424c6ac62676d34a969550f09838d1 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Tue, 17 Mar 2026 09:16:46 +0100 Subject: [PATCH 11/22] :tada: Add background blur for wasm render --- render-wasm/macros/src/lib.rs | 2 + render-wasm/src/main.rs | 76 +++---- render-wasm/src/render.rs | 203 ++++++++++-------- render-wasm/src/render/debug.rs | 12 +- render-wasm/src/render/fills.rs | 46 ++-- render-wasm/src/render/filters.rs | 21 +- render-wasm/src/render/fonts.rs | 17 +- render-wasm/src/render/gpu_state.rs | 45 ++-- render-wasm/src/render/images.rs | 20 +- render-wasm/src/render/shadows.rs | 15 +- render-wasm/src/render/strokes.rs | 84 ++++---- render-wasm/src/render/surfaces.rs | 96 ++++++--- render-wasm/src/render/text.rs | 17 +- render-wasm/src/shapes/modifiers.rs | 42 ++-- .../src/shapes/modifiers/constraints.rs | 11 +- .../src/shapes/modifiers/flex_layout.rs | 10 +- .../src/shapes/modifiers/grid_layout.rs | 10 +- render-wasm/src/state.rs | 56 ++--- render-wasm/src/tiles.rs | 8 +- render-wasm/src/wasm/fills/image.rs | 51 +++-- render-wasm/src/wasm/layouts/grid.rs | 32 ++- render-wasm/src/wasm/paths.rs | 57 +++-- render-wasm/src/wasm/shapes/base_props.rs | 15 +- render-wasm/src/wasm/text.rs | 5 +- render-wasm/src/wasm/transforms.rs | 21 +- 25 files changed, 584 insertions(+), 388 deletions(-) diff --git a/render-wasm/macros/src/lib.rs b/render-wasm/macros/src/lib.rs index 2d3536d3d1..876af71254 100644 --- a/render-wasm/macros/src/lib.rs +++ b/render-wasm/macros/src/lib.rs @@ -61,11 +61,13 @@ pub fn wasm_error(_attr: TokenStream, item: TokenStream) -> TokenStream { let _: &dyn std::error::Error = &__e; let __msg = __e.to_string(); crate::mem::set_error_code(__e.into()); + crate::mem::free_bytes().expect("Failed to free bytes"); panic!("WASM error: {}",__msg); } }, Err(__payload) => { crate::mem::set_error_code(0x02); // critical, same as Error::Critical + crate::mem::free_bytes().expect("Failed to free bytes"); std::panic::resume_unwind(__payload); } } diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 6e519cc249..017540a11b 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -30,6 +30,9 @@ use uuid::Uuid; pub(crate) static mut STATE: Option> = None; +// FIXME: These with_state* macros should be using our CriticalError instead of expect. +// But to do that, we need to not use them at domain-level (i.e. in business logic), just +// in the context of the wasm call. #[macro_export] macro_rules! with_state_mut { ($state:ident, $block:block) => {{ @@ -102,7 +105,7 @@ macro_rules! with_state_mut_current_shape { #[no_mangle] #[wasm_error] pub extern "C" fn init(width: i32, height: i32) -> Result<()> { - let state_box = Box::new(State::new(width, height)); + let state_box = Box::new(State::try_new(width, height)?); unsafe { STATE = Some(state_box); } @@ -138,7 +141,7 @@ pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> { with_state_mut!(state, { let render_state = state.render_state_mut(); render_state.set_debug_flags(debug); - render_state.set_dpr(dpr); + render_state.set_dpr(dpr)?; }); Ok(()) } @@ -162,7 +165,7 @@ pub extern "C" fn render(_: i32) -> Result<()> { state.rebuild_touched_tiles(); state .start_render_loop(performance::get_time()) - .expect("Error rendering"); + .map_err(|_| Error::RecoverableError("Error rendering".to_string()))?; }); Ok(()) } @@ -174,7 +177,7 @@ pub extern "C" fn render_sync() -> Result<()> { state.rebuild_tiles(); state .render_sync(performance::get_time()) - .expect("Error rendering"); + .map_err(|_| Error::RecoverableError("Error rendering".to_string()))?; }); Ok(()) } @@ -236,24 +239,12 @@ pub extern "C" fn render_preview() -> Result<()> { #[no_mangle] #[wasm_error] pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> { - let result = std::panic::catch_unwind(|| { - with_state_mut!(state, { - state - .process_animation_frame(timestamp) - .expect("Error processing animation frame"); - }); - }); + let result = with_state_mut!(state, { state.process_animation_frame(timestamp) }); - match result { - Ok(_) => {} - Err(err) => { - match err.downcast_ref::() { - Some(message) => println!("process_animation_frame error: {}", message), - None => println!("process_animation_frame error: {:?}", err), - } - std::panic::resume_unwind(err); - } + if let Err(err) = result { + eprintln!("process_animation_frame error: {}", err); } + Ok(()) } @@ -270,7 +261,7 @@ pub extern "C" fn reset_canvas() -> Result<()> { #[wasm_error] pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> { with_state_mut!(state, { - state.resize(width, height); + state.resize(width, height)?; }); Ok(()) } @@ -362,8 +353,8 @@ pub extern "C" fn set_focus_mode() -> Result<()> { let entries: Vec = bytes .chunks(size_of::<::BytesType>()) - .map(|data| Uuid::try_from(data).unwrap()) - .collect(); + .map(|data| Uuid::try_from(data).map_err(|e| Error::RecoverableError(e.to_string()))) + .collect::>>()?; with_state_mut!(state, { state.set_focus_mode(entries); @@ -637,8 +628,8 @@ pub extern "C" fn set_children() -> Result<()> { let entries: Vec = bytes .chunks(size_of::<::BytesType>()) - .map(|data| Uuid::try_from(data).unwrap()) - .collect(); + .map(|data| Uuid::try_from(data).map_err(|e| Error::CriticalError(e.to_string()))) + .collect::>>()?; set_children_set(entries)?; @@ -761,10 +752,15 @@ pub extern "C" fn get_selection_rect() -> Result<*mut u8> { pub extern "C" fn set_structure_modifiers() -> Result<()> { let bytes = mem::bytes(); - let entries: Vec<_> = bytes + let entries: Vec = bytes .chunks(44) - .map(|data| StructureEntry::from_bytes(data.try_into().unwrap())) - .collect(); + .map(|chunk| { + let data = chunk + .try_into() + .map_err(|_| Error::CriticalError("Invalid StructureEntry bytes".to_string()))?; + Ok(StructureEntry::from_bytes(data)) + }) + .collect::>>()?; with_state_mut!(state, { let mut structure = HashMap::new(); @@ -783,7 +779,9 @@ pub extern "C" fn set_structure_modifiers() -> Result<()> { structure.entry(entry.parent).or_insert_with(Vec::new); structure .get_mut(&entry.parent) - .expect("Parent not found for entry") + .ok_or(Error::CriticalError( + "Parent not found for entry".to_string(), + ))? .push(entry); } } @@ -814,10 +812,10 @@ pub extern "C" fn clean_modifiers() -> Result<()> { pub extern "C" fn set_modifiers() -> Result<()> { let bytes = mem::bytes(); - let entries: Vec<_> = bytes + let entries: Vec = bytes .chunks(size_of::<::BytesType>()) - .map(|data| TransformEntry::try_from(data).unwrap()) - .collect(); + .map(|data| TransformEntry::try_from(data).map_err(|e| Error::CriticalError(e.to_string()))) + .collect::>>()?; let mut modifiers = HashMap::new(); let mut ids = Vec::::new(); @@ -828,7 +826,7 @@ pub extern "C" fn set_modifiers() -> Result<()> { with_state_mut!(state, { state.set_modifiers(modifiers); - state.rebuild_modifier_tiles(ids); + state.rebuild_modifier_tiles(ids)?; }); Ok(()) } @@ -838,8 +836,10 @@ pub extern "C" fn set_modifiers() -> Result<()> { pub extern "C" fn start_temp_objects() -> Result<()> { unsafe { #[allow(static_mut_refs)] - let mut state = STATE.take().expect("Got an invalid state pointer"); - state = Box::new(state.start_temp_objects()); + let mut state = STATE.take().ok_or(Error::CriticalError( + "Got an invalid state pointer".to_string(), + ))?; + state = Box::new(state.start_temp_objects()?); STATE = Some(state); } Ok(()) @@ -850,8 +850,10 @@ pub extern "C" fn start_temp_objects() -> Result<()> { pub extern "C" fn end_temp_objects() -> Result<()> { unsafe { #[allow(static_mut_refs)] - let mut state = STATE.take().expect("Got an invalid state pointer"); - state = Box::new(state.end_temp_objects()); + let mut state = STATE.take().ok_or(Error::CriticalError( + "Got an invalid state pointer".to_string(), + ))?; + state = Box::new(state.end_temp_objects()?); STATE = Some(state); } Ok(()) diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 68748effe0..bb0427ac78 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -21,6 +21,7 @@ use gpu_state::GpuState; use options::RenderOptions; pub use surfaces::{SurfaceId, Surfaces}; +use crate::error::{Error, Result}; use crate::performance; use crate::shapes::{ all_with_ancestors, radius_to_sigma, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, @@ -326,19 +327,19 @@ pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize { } impl RenderState { - pub fn new(width: i32, height: i32) -> RenderState { + pub fn try_new(width: i32, height: i32) -> Result { // This needs to be done once per WebGL context. - let mut gpu_state = GpuState::new(); + let mut gpu_state = GpuState::try_new()?; let sampling_options = skia::SamplingOptions::new(skia::FilterMode::Linear, skia::MipmapMode::Nearest); - let fonts = FontStore::new(); - let surfaces = Surfaces::new( + let fonts = FontStore::try_new()?; + let surfaces = Surfaces::try_new( &mut gpu_state, (width, height), sampling_options, tiles::get_tile_dimensions(), - ); + )?; // This is used multiple times everywhere so instead of creating new instances every // time we reuse this one. @@ -346,7 +347,7 @@ impl RenderState { let viewbox = Viewbox::new(width as f32, height as f32); let tiles = tiles::TileHashMap::new(); - RenderState { + Ok(RenderState { gpu_state: gpu_state.clone(), options: RenderOptions::default(), surfaces, @@ -377,7 +378,7 @@ impl RenderState { touched_ids: HashSet::default(), ignore_nested_blurs: false, preview_mode: false, - } + }) } /// Combines every visible layer blur currently active (ancestors + shape) @@ -531,15 +532,15 @@ impl RenderState { /// Runs `f` with `ignore_nested_blurs` temporarily forced to `true`. /// Certain off-screen passes (e.g. shadow masks) must render shapes without /// inheriting ancestor blur. This helper guarantees the flag is restored. - fn with_nested_blurs_suppressed(&mut self, f: F) -> R + fn with_nested_blurs_suppressed(&mut self, f: F) -> Result where - F: FnOnce(&mut RenderState) -> R, + F: FnOnce(&mut RenderState) -> Result, { let previous = self.ignore_nested_blurs; self.ignore_nested_blurs = true; - let result = f(self); + let result = f(self)?; self.ignore_nested_blurs = previous; - result + Ok(result) } pub fn fonts(&self) -> &FontStore { @@ -550,12 +551,7 @@ impl RenderState { &mut self.fonts } - pub fn add_image( - &mut self, - id: Uuid, - is_thumbnail: bool, - image_data: &[u8], - ) -> Result<(), String> { + pub fn add_image(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<()> { self.images.add(id, is_thumbnail, image_data) } @@ -567,7 +563,7 @@ impl RenderState { texture_id: u32, width: i32, height: i32, - ) -> Result<(), String> { + ) -> Result<()> { self.images .add_image_from_gl_texture(id, is_thumbnail, texture_id, width, height) } @@ -580,15 +576,16 @@ impl RenderState { self.options.flags = debug; } - pub fn set_dpr(&mut self, dpr: f32) { + pub fn set_dpr(&mut self, dpr: f32) -> Result<()> { if Some(dpr) != self.options.dpr { self.options.dpr = Some(dpr); self.resize( self.viewbox.width.floor() as i32, self.viewbox.height.floor() as i32, - ); + )?; self.fonts.set_scale_debug_font(dpr); } + Ok(()) } pub fn set_background_color(&mut self, color: skia::Color) { @@ -599,13 +596,15 @@ impl RenderState { self.preview_mode = enabled; } - pub fn resize(&mut self, width: i32, height: i32) { + pub fn resize(&mut self, width: i32, height: i32) -> Result<()> { let dpr_width = (width as f32 * self.options.dpr()).floor() as i32; let dpr_height = (height as f32 * self.options.dpr()).floor() as i32; self.surfaces - .resize(&mut self.gpu_state, dpr_width, dpr_height); + .resize(&mut self.gpu_state, dpr_width, dpr_height)?; self.viewbox.set_wh(width as f32, height as f32); self.tile_viewbox.update(self.viewbox, self.get_scale()); + + Ok(()) } pub fn flush_and_submit(&mut self) { @@ -627,19 +626,23 @@ impl RenderState { self.surfaces.canvas(surface_id).restore(); } - pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) { - let tile_rect = self.get_current_aligned_tile_bounds(); + pub fn apply_render_to_final_canvas(&mut self, rect: skia::Rect) -> Result<()> { + let tile_rect = self.get_current_aligned_tile_bounds()?; self.surfaces.cache_current_tile_texture( &self.tile_viewbox, - &self.current_tile.unwrap(), + &self + .current_tile + .ok_or(Error::CriticalError("Current tile not found".to_string()))?, &tile_rect, ); self.surfaces.draw_cached_tile_surface( - self.current_tile.unwrap(), + self.current_tile + .ok_or(Error::CriticalError("Current tile not found".to_string()))?, rect, self.background_color, ); + Ok(()) } pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) { @@ -748,7 +751,7 @@ impl RenderState { offset: Option<(f32, f32)>, parent_shadows: Option>, outset: Option, - ) { + ) -> Result<()> { let surface_ids = fills_surface_id as u32 | strokes_surface_id as u32 | innershadows_surface_id as u32 @@ -813,7 +816,7 @@ impl RenderState { antialias, SurfaceId::Current, None, - ); + )?; // Pass strokes in natural order; stroke merging handles top-most ordering internally. let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect(); @@ -824,7 +827,7 @@ impl RenderState { Some(SurfaceId::Current), antialias, outset, - ); + )?; self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { s.canvas().restore(); @@ -840,7 +843,7 @@ impl RenderState { s.canvas().restore(); }); } - return; + return Ok(()); } // set clipping @@ -1017,7 +1020,7 @@ impl RenderState { None, text_fill_inset, None, - ); + )?; for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list .iter_mut() @@ -1034,7 +1037,7 @@ impl RenderState { text_stroke_blur_outset, None, *layer_opacity, - ); + )?; } } else { let mut drop_shadows = shape.drop_shadow_paints(); @@ -1077,7 +1080,7 @@ impl RenderState { blur_filter.as_ref(), None, None, - ); + )?; } } else { shadows::render_text_shadows( @@ -1088,7 +1091,7 @@ impl RenderState { text_drop_shadows_surface_id.into(), &parent_shadows, &blur_filter, - ); + )?; } } else { // 1. Text drop shadows @@ -1104,7 +1107,7 @@ impl RenderState { blur_filter.as_ref(), None, None, - ); + )?; } } @@ -1119,7 +1122,7 @@ impl RenderState { blur_filter.as_ref(), text_fill_inset, None, - ); + )?; // 3. Stroke drop shadows shadows::render_text_shadows( @@ -1130,7 +1133,7 @@ impl RenderState { text_drop_shadows_surface_id.into(), &drop_shadows, &blur_filter, - ); + )?; // 4. Stroke fills for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list @@ -1148,7 +1151,7 @@ impl RenderState { text_stroke_blur_outset, None, *layer_opacity, - ); + )?; } // 5. Stroke inner shadows @@ -1160,7 +1163,7 @@ impl RenderState { Some(innershadows_surface_id), &inner_shadows, &blur_filter, - ); + )?; // 6. Fill Inner shadows if !shape.has_visible_strokes() { @@ -1175,7 +1178,7 @@ impl RenderState { blur_filter.as_ref(), None, None, - ); + )?; } } } @@ -1223,7 +1226,7 @@ impl RenderState { antialias, fills_surface_id, outset, - ); + )?; } } else { fills::render( @@ -1233,7 +1236,7 @@ impl RenderState { antialias, fills_surface_id, outset, - ); + )?; } // Skip stroke rendering for clipped frames - they are drawn in render_shape_exit @@ -1249,7 +1252,7 @@ impl RenderState { Some(strokes_surface_id), antialias, outset, - ); + )?; if !fast_mode { for stroke in &visible_strokes { shadows::render_stroke_inner_shadows( @@ -1258,7 +1261,7 @@ impl RenderState { stroke, antialias, innershadows_surface_id, - ); + )?; } } } @@ -1295,6 +1298,7 @@ impl RenderState { s.canvas().restore(); }); } + Ok(()) } pub fn update_render_context(&mut self, tile: tiles::Tile) { @@ -1373,7 +1377,7 @@ impl RenderState { /// Render a preview of the shapes during loading. /// This rebuilds tiles for touched shapes and renders synchronously. - pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> { + pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<()> { let _start = performance::begin_timed_log!("render_preview"); performance::begin_measure!("render_preview"); @@ -1403,7 +1407,7 @@ impl RenderState { tree: ShapesPoolRef, timestamp: i32, sync_render: bool, - ) -> Result<(), String> { + ) -> Result<()> { let _start = performance::begin_timed_log!("start_render_loop"); let scale = self.get_scale(); @@ -1430,7 +1434,7 @@ impl RenderState { || viewbox_cache_size.height > cached_viewbox_cache_size.height { self.surfaces - .resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD); + .resize_cache(viewbox_cache_size, VIEWPORT_INTEREST_AREA_THRESHOLD)?; } // FIXME - review debug @@ -1475,7 +1479,7 @@ impl RenderState { base_object: Option<&Uuid>, tree: ShapesPoolRef, timestamp: i32, - ) -> Result<(), String> { + ) -> Result<()> { performance::begin_measure!("process_animation_frame"); if self.render_in_progress { if tree.len() != 0 { @@ -1499,7 +1503,7 @@ impl RenderState { base_object: Option<&Uuid>, tree: ShapesPoolRef, timestamp: i32, - ) -> Result<(), String> { + ) -> Result<()> { if tree.len() != 0 { self.render_shape_tree_partial(base_object, tree, timestamp, false)?; } @@ -1589,7 +1593,7 @@ impl RenderState { element: &Shape, visited_mask: bool, clip_bounds: Option, - ) { + ) -> Result<()> { if visited_mask { // Because masked groups needs two rendering passes (first drawing // the content and then drawing the mask), we need to do an @@ -1660,7 +1664,7 @@ impl RenderState { None, None, None, - ); + )?; } // Only restore if we created a layer (optimization for simple shapes) @@ -1672,19 +1676,22 @@ impl RenderState { } self.focus_mode.exit(&element.id); + Ok(()) } - pub fn get_current_tile_bounds(&mut self) -> Rect { - let tiles::Tile(tile_x, tile_y) = self.current_tile.unwrap(); + pub fn get_current_tile_bounds(&mut self) -> Result { + let tiles::Tile(tile_x, tile_y) = self + .current_tile + .ok_or(Error::CriticalError("Current tile not found".to_string()))?; let scale = self.get_scale(); let offset_x = self.viewbox.area.left * scale; let offset_y = self.viewbox.area.top * scale; - Rect::from_xywh( + Ok(Rect::from_xywh( (tile_x as f32 * tiles::TILE_SIZE) - offset_x, (tile_y as f32 * tiles::TILE_SIZE) - offset_y, tiles::TILE_SIZE, tiles::TILE_SIZE, - ) + )) } pub fn get_rect_bounds(&mut self, rect: skia::Rect) -> Rect { @@ -1732,8 +1739,11 @@ impl RenderState { // lower multiple of `TILE_SIZE`. This ensures the tile bounds are aligned // with the global tile grid, which is useful for rendering tiles in a /// consistent and predictable layout. - pub fn get_current_aligned_tile_bounds(&mut self) -> Rect { - self.get_aligned_tile_bounds(self.current_tile.unwrap()) + pub fn get_current_aligned_tile_bounds(&mut self) -> Result { + Ok(self.get_aligned_tile_bounds( + self.current_tile + .ok_or(Error::CriticalError("Current tile not found".to_string()))?, + )) } /// Renders a drop shadow effect for the given shape. @@ -1750,7 +1760,7 @@ impl RenderState { scale: f32, translation: (f32, f32), extra_layer_blur: Option, - ) { + ) -> Result<()> { let mut transformed_shadow: Cow = Cow::Borrowed(shadow); transformed_shadow.to_mut().offset = (0.0, 0.0); transformed_shadow.to_mut().color = skia::Color::BLACK; @@ -1805,7 +1815,7 @@ impl RenderState { plain_shape_mut.clip_content = false; let Some(drop_filter) = transformed_shadow.get_drop_shadow_filter() else { - return; + return Ok(()); }; let mut bounds = drop_filter.compute_fast_bounds(shape_bounds); @@ -1813,7 +1823,7 @@ impl RenderState { bounds.offset(world_offset); // Early cull if the shadow bounds are outside the render area. if !bounds.intersects(self.render_area_with_margins) { - return; + return Ok(()); } // blur=0 at high zoom: draw directly on DropShadows with geometric spread (no filter). @@ -1835,11 +1845,11 @@ impl RenderState { Some(shadow.offset), None, Some(shadow.spread), - ); - }); + ) + })?; self.surfaces.canvas(SurfaceId::DropShadows).restore(); - return; + return Ok(()); } // Create filter with blur only (no offset, no spread - handled geometrically) @@ -1877,11 +1887,11 @@ impl RenderState { Some(shadow.offset), // Offset is geometric None, Some(shadow.spread), - ); - }); + ) + })?; self.surfaces.canvas(SurfaceId::DropShadows).restore(); - return; + return Ok(()); } // Adaptive downscale for large blur values (lossless GPU optimization). @@ -1918,12 +1928,13 @@ impl RenderState { Some(shadow.offset), // Offset is geometric None, Some(shadow.spread), - ); - }); + ) + })?; state.surfaces.canvas(temp_surface).restore(); + Ok(()) }, - ); + )?; if let Some((mut surface, filter_scale)) = filter_result { let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows); @@ -1958,6 +1969,7 @@ impl RenderState { } drop_canvas.restore(); } + Ok(()) } /// Renders element drop shadows to DropShadows surface and composites to Current. @@ -1972,7 +1984,7 @@ impl RenderState { scale: f32, translation: (f32, f32), node_render_state: &NodeRenderState, - ) { + ) -> Result<()> { let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale)); let inherited_layer_blur = match element.shape_type { Type::Frame(_) | Type::Group(_) => element.blur, @@ -1994,7 +2006,7 @@ impl RenderState { scale, translation, None, - ); + )?; if !matches!(element.shape_type, Type::Bool(_)) { let shadow_children = if element.is_recursive() { @@ -2023,7 +2035,7 @@ impl RenderState { scale, translation, inherited_layer_blur, - ); + )?; } else { let paint = skia::Paint::default(); let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); @@ -2059,8 +2071,8 @@ impl RenderState { None, Some(vec![new_shadow_paint.clone()]), None, - ); - }); + ) + })?; self.surfaces.canvas(SurfaceId::DropShadows).restore(); } } @@ -2117,6 +2129,7 @@ impl RenderState { self.surfaces .canvas(SurfaceId::DropShadows) .clear(skia::Color::TRANSPARENT); + Ok(()) } pub fn render_shape_tree_partial_uncached( @@ -2124,7 +2137,7 @@ impl RenderState { tree: ShapesPoolRef, timestamp: i32, allow_stop: bool, - ) -> Result<(bool, bool), String> { + ) -> Result<(bool, bool)> { let mut iteration = 0; let mut is_empty = true; @@ -2152,7 +2165,7 @@ impl RenderState { if visited_children { if !node_render_state.flattened { - self.render_shape_exit(element, visited_mask, clip_bounds); + self.render_shape_exit(element, visited_mask, clip_bounds)?; } continue; } @@ -2226,7 +2239,13 @@ impl RenderState { scale, translation, &node_render_state, - ); + )?; + } + + // Render background blur BEFORE save_layer so it modifies + // the backdrop independently of the shape's opacity. + if !node_render_state.is_root() && self.focus_mode.is_active() { + self.render_background_blur(element); } // Render background blur BEFORE save_layer so it modifies @@ -2262,7 +2281,7 @@ impl RenderState { scale, translation, &node_render_state, - ); + )?; } self.render_shape( @@ -2276,7 +2295,7 @@ impl RenderState { None, None, None, - ); + )?; self.surfaces .canvas(SurfaceId::DropShadows) @@ -2373,14 +2392,14 @@ impl RenderState { tree: ShapesPoolRef, timestamp: i32, allow_stop: bool, - ) -> Result<(), String> { + ) -> Result<()> { let mut should_stop = false; let root_ids = { if let Some(shape_id) = base_object { vec![*shape_id] } else { let Some(root) = tree.get(&Uuid::nil()) else { - return Err(String::from("Root shape not found")); + return Err(Error::CriticalError("Root shape not found".to_string())); }; root.children_ids(false) } @@ -2390,7 +2409,7 @@ impl RenderState { if let Some(current_tile) = self.current_tile { if self.surfaces.has_cached_tile_surface(current_tile) { performance::begin_measure!("render_shape_tree::cached"); - let tile_rect = self.get_current_tile_bounds(); + let tile_rect = self.get_current_tile_bounds()?; self.surfaces.draw_cached_tile_surface( current_tile, tile_rect, @@ -2420,9 +2439,9 @@ impl RenderState { return Ok(()); } performance::end_measure!("render_shape_tree::uncached"); - let tile_rect = self.get_current_tile_bounds(); + let tile_rect = self.get_current_tile_bounds()?; if !is_empty { - self.apply_render_to_final_canvas(tile_rect); + self.apply_render_to_final_canvas(tile_rect)?; if self.options.is_debug_visible() { debug::render_workspace_current_tile( @@ -2760,7 +2779,11 @@ impl RenderState { /// /// This is useful when you have a pre-computed set of shape IDs that need to be refreshed, /// regardless of their relationship to other shapes (e.g., ancestors, descendants, or any other collection). - pub fn update_tiles_shapes(&mut self, shape_ids: &[Uuid], tree: ShapesPoolMutRef<'_>) { + pub fn update_tiles_shapes( + &mut self, + shape_ids: &[Uuid], + tree: ShapesPoolMutRef<'_>, + ) -> Result<()> { performance::begin_measure!("invalidate_and_update_tiles"); let mut all_tiles = HashSet::::new(); for shape_id in shape_ids { @@ -2772,6 +2795,7 @@ impl RenderState { self.remove_cached_tile(tile); } performance::end_measure!("invalidate_and_update_tiles"); + Ok(()) } /// Rebuilds tiles for shapes with modifiers and processes their ancestors @@ -2780,9 +2804,14 @@ impl RenderState { /// Additionally, it processes all ancestors of modified shapes to ensure their /// extended rectangles are properly recalculated and their tiles are updated. /// This is crucial for frames and groups that contain transformed children. - pub fn rebuild_modifier_tiles(&mut self, tree: ShapesPoolMutRef<'_>, ids: Vec) { + pub fn rebuild_modifier_tiles( + &mut self, + tree: ShapesPoolMutRef<'_>, + ids: Vec, + ) -> Result<()> { let ancestors = all_with_ancestors(&ids, tree, false); - self.update_tiles_shapes(&ancestors, tree); + self.update_tiles_shapes(&ancestors, tree)?; + Ok(()) } pub fn get_scale(&self) -> f32 { diff --git a/render-wasm/src/render/debug.rs b/render-wasm/src/render/debug.rs index 41f68a663e..27f8da07a8 100644 --- a/render-wasm/src/render/debug.rs +++ b/render-wasm/src/render/debug.rs @@ -179,9 +179,12 @@ pub fn render_debug_shape( #[cfg(target_arch = "wasm32")] #[allow(dead_code)] pub fn console_debug_surface(render_state: &mut RenderState, id: SurfaceId) { - let base64_image = render_state.surfaces.base64_snapshot(id); + let base64_image = render_state + .surfaces + .base64_snapshot(id) + .expect("Failed to get base64 image"); - run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')")) + run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')")); } #[allow(dead_code)] @@ -194,7 +197,10 @@ pub fn console_debug_surface_rect(render_state: &mut RenderState, id: SurfaceId, rect.bottom as i32, ); - let base64_image = render_state.surfaces.base64_snapshot_rect(id, int_rect); + let base64_image = render_state + .surfaces + .base64_snapshot_rect(id, int_rect) + .expect("Failed to get base64 image"); if let Some(base64_image) = base64_image { run_script!(format!("console.log('%c ', 'font-size: 1px; background: url(data:image/png;base64,{base64_image}) no-repeat; padding: 100px; background-size: contain;')")) diff --git a/render-wasm/src/render/fills.rs b/render-wasm/src/render/fills.rs index 6e098c9752..2a1968a93c 100644 --- a/render-wasm/src/render/fills.rs +++ b/render-wasm/src/render/fills.rs @@ -1,6 +1,7 @@ use skia_safe::{self as skia, Paint, RRect}; use super::{filters, RenderState, SurfaceId}; +use crate::error::Result; use crate::render::get_source_rect; use crate::shapes::{merge_fills, Fill, Frame, ImageFill, Rect, Shape, StrokeKind, Type}; @@ -20,12 +21,11 @@ fn draw_image_fill( antialias: bool, surface_id: SurfaceId, ) { - let image = render_state.images.get(&image_fill.id()); - if image.is_none() { + let Some(image) = render_state.images.get(&image_fill.id()) else { return; - } + }; - let size = image.unwrap().dimensions(); + let size = image.dimensions(); let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id); let container = &shape.selrect; let path_transform = shape.to_path_transform(); @@ -85,15 +85,13 @@ fn draw_image_fill( } // Draw the image with the calculated destination rectangle - if let Some(image) = image { - canvas.draw_image_rect_with_sampling_options( - image, - Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), - dest_rect, - render_state.sampling_options, - paint, - ); - } + canvas.draw_image_rect_with_sampling_options( + image, + Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), + dest_rect, + render_state.sampling_options, + paint, + ); // Restore the canvas to remove the clipping canvas.restore(); @@ -109,9 +107,9 @@ pub fn render( antialias: bool, surface_id: SurfaceId, outset: Option, -) { +) -> Result<()> { if fills.is_empty() { - return; + return Ok(()); } let scale = render_state.get_scale().max(1e-6); @@ -134,9 +132,9 @@ pub fn render( surface_id, outset, inset, - ); + )?; } - return; + return Ok(()); } let mut paint = merge_fills(fills, shape.selrect); @@ -152,15 +150,17 @@ pub fn render( let mut filtered_paint = paint.clone(); filtered_paint.set_image_filter(image_filter.clone()); draw_fill_to_surface(state, shape, temp_surface, &filtered_paint, outset, inset); + Ok(()) }, - ) { - return; + )? { + return Ok(()); } else { paint.set_image_filter(image_filter); } } draw_fill_to_surface(render_state, shape, surface_id, &paint, outset, inset); + Ok(()) } /// Draws a single paint (with a merged shader) to the appropriate surface @@ -203,7 +203,7 @@ fn render_single_fill( surface_id: SurfaceId, outset: Option, inset: Option, -) { +) -> Result<()> { let mut paint = fill.to_paint(&shape.selrect, antialias); if let Some(image_filter) = shape.image_filter(1.) { let bounds = image_filter.compute_fast_bounds(shape.selrect); @@ -224,9 +224,10 @@ fn render_single_fill( outset, inset, ); + Ok(()) }, - ) { - return; + )? { + return Ok(()); } else { paint.set_image_filter(image_filter); } @@ -242,6 +243,7 @@ fn render_single_fill( outset, inset, ); + Ok(()) } #[allow(clippy::too_many_arguments)] diff --git a/render-wasm/src/render/filters.rs b/render-wasm/src/render/filters.rs index 149c598e94..34b33f403d 100644 --- a/render-wasm/src/render/filters.rs +++ b/render-wasm/src/render/filters.rs @@ -1,6 +1,7 @@ use skia_safe::{self as skia, ImageFilter, Rect}; use super::{RenderState, SurfaceId}; +use crate::error::Result; /// Composes two image filters, returning a combined filter if both are present, /// or the individual filter if only one is present, or None if neither is present. @@ -36,12 +37,12 @@ pub fn render_with_filter_surface( bounds: Rect, target_surface: SurfaceId, draw_fn: F, -) -> bool +) -> Result where - F: FnOnce(&mut RenderState, SurfaceId), + F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>, { if let Some((mut surface, scale)) = - render_into_filter_surface(render_state, bounds, 1.0, draw_fn) + render_into_filter_surface(render_state, bounds, 1.0, draw_fn)? { let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); @@ -58,9 +59,9 @@ where surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None); canvas.restore(); } - true + Ok(true) } else { - false + Ok(false) } } @@ -81,12 +82,12 @@ pub fn render_into_filter_surface( bounds: Rect, extra_downscale: f32, draw_fn: F, -) -> Option<(skia::Surface, f32)> +) -> Result> where - F: FnOnce(&mut RenderState, SurfaceId), + F: FnOnce(&mut RenderState, SurfaceId) -> Result<()>, { if !bounds.is_finite() || bounds.width() <= 0.0 || bounds.height() <= 0.0 { - return None; + return Ok(None); } let filter_id = SurfaceId::Filter; @@ -125,10 +126,10 @@ where canvas.translate((-bounds.left, -bounds.top)); } - draw_fn(render_state, filter_id); + draw_fn(render_state, filter_id)?; render_state.surfaces.canvas(filter_id).restore(); let filter_surface = render_state.surfaces.surface_clone(filter_id); - Some((filter_surface, scale)) + Ok(Some((filter_surface, scale))) } diff --git a/render-wasm/src/render/fonts.rs b/render-wasm/src/render/fonts.rs index 917fe134a8..d528d7b691 100644 --- a/render-wasm/src/render/fonts.rs +++ b/render-wasm/src/render/fonts.rs @@ -1,6 +1,7 @@ use skia_safe::{self as skia, textlayout, Font, FontMgr}; use std::collections::HashSet; +use crate::error::{Error, Result}; use crate::shapes::{FontFamily, FontStyle}; use crate::uuid::Uuid; @@ -26,7 +27,7 @@ pub struct FontStore { } impl FontStore { - pub fn new() -> Self { + pub fn try_new() -> Result { let font_mgr = FontMgr::new(); let font_provider = load_default_provider(&font_mgr); let mut font_collection = skia::textlayout::FontCollection::new(); @@ -34,17 +35,19 @@ impl FontStore { let debug_typeface = font_provider .match_family_style(default_font().as_str(), skia::FontStyle::default()) - .unwrap(); + .ok_or(Error::CriticalError( + "Failed to match default font".to_string(), + ))?; let debug_font = skia::Font::new(debug_typeface, 10.0); - Self { + Ok(Self { font_mgr, font_provider, font_collection, debug_font, fallback_fonts: HashSet::new(), - } + }) } pub fn set_scale_debug_font(&mut self, dpr: f32) { @@ -70,7 +73,7 @@ impl FontStore { font_data: &[u8], is_emoji: bool, is_fallback: bool, - ) -> Result<(), String> { + ) -> Result<()> { if self.has_family(&family, is_emoji) { return Ok(()); } @@ -78,7 +81,9 @@ impl FontStore { let typeface = self .font_mgr .new_from_data(font_data, None) - .ok_or("Failed to create typeface")?; + .ok_or(Error::CriticalError( + "Failed to create typeface".to_string(), + ))?; let alias = format!("{}", family); let font_name = if is_emoji { diff --git a/render-wasm/src/render/gpu_state.rs b/render-wasm/src/render/gpu_state.rs index d4d90faaec..910c5beda2 100644 --- a/render-wasm/src/render/gpu_state.rs +++ b/render-wasm/src/render/gpu_state.rs @@ -1,3 +1,4 @@ +use crate::error::{Error, Result}; use skia_safe::gpu::{self, gl::FramebufferInfo, gl::TextureInfo, DirectContext}; use skia_safe::{self as skia, ISize}; @@ -8,24 +9,30 @@ pub struct GpuState { } impl GpuState { - pub fn new() -> Self { - let interface = gpu::gl::Interface::new_native().unwrap(); - let context = gpu::direct_contexts::make_gl(interface, None).unwrap(); + pub fn try_new() -> Result { + let interface = gpu::gl::Interface::new_native().ok_or(Error::CriticalError( + "Failed to create GL interface".to_string(), + ))?; + let context = gpu::direct_contexts::make_gl(interface, None).ok_or( + Error::CriticalError("Failed to create GL context".to_string()), + )?; let framebuffer_info = { let mut fboid: gl::types::GLint = 0; unsafe { gl::GetIntegerv(gl::FRAMEBUFFER_BINDING, &mut fboid) }; FramebufferInfo { - fboid: fboid.try_into().unwrap(), + fboid: fboid.try_into().map_err(|_| { + Error::CriticalError("Failed to convert GL framebuffer ID to u32".to_string()) + })?, format: gpu::gl::Format::RGBA8.into(), protected: gpu::Protected::No, } }; - GpuState { + Ok(GpuState { context, framebuffer_info, - } + }) } fn create_webgl_texture(&mut self, width: i32, height: i32) -> gl::types::GLuint { @@ -56,7 +63,11 @@ impl GpuState { texture_id } - pub fn create_surface_with_isize(&mut self, label: String, size: ISize) -> skia::Surface { + pub fn create_surface_with_isize( + &mut self, + label: String, + size: ISize, + ) -> Result { self.create_surface_with_dimensions(label, size.width, size.height) } @@ -65,7 +76,7 @@ impl GpuState { label: String, width: i32, height: i32, - ) -> skia::Surface { + ) -> Result { let backend_texture = unsafe { let texture_id = self.create_webgl_texture(width, height); let texture_info = TextureInfo { @@ -77,7 +88,7 @@ impl GpuState { gpu::backend_textures::make_gl((width, height), gpu::Mipmapped::No, texture_info, label) }; - gpu::surfaces::wrap_backend_texture( + let surface = gpu::surfaces::wrap_backend_texture( &mut self.context, &backend_texture, gpu::SurfaceOrigin::BottomLeft, @@ -86,15 +97,19 @@ impl GpuState { None, None, ) - .unwrap() + .ok_or(Error::CriticalError( + "Failed to create Skia surface".to_string(), + ))?; + + Ok(surface) } /// Create a Skia surface that will be used for rendering. - pub fn create_target_surface(&mut self, width: i32, height: i32) -> skia::Surface { + pub fn create_target_surface(&mut self, width: i32, height: i32) -> Result { let backend_render_target = gpu::backend_render_targets::make_gl((width, height), 1, 8, self.framebuffer_info); - gpu::surfaces::wrap_backend_render_target( + let surface = gpu::surfaces::wrap_backend_render_target( &mut self.context, &backend_render_target, gpu::SurfaceOrigin::BottomLeft, @@ -102,6 +117,10 @@ impl GpuState { None, None, ) - .unwrap() + .ok_or(Error::CriticalError( + "Failed to create Skia surface".to_string(), + ))?; + + Ok(surface) } } diff --git a/render-wasm/src/render/images.rs b/render-wasm/src/render/images.rs index 4faf393895..51bf9dbbe0 100644 --- a/render-wasm/src/render/images.rs +++ b/render-wasm/src/render/images.rs @@ -2,6 +2,7 @@ use crate::math::Rect as MathRect; use crate::shapes::ImageFill; use crate::uuid::Uuid; +use crate::error::{Error, Result}; use skia_safe::gpu::{surfaces, Budgeted, DirectContext}; use skia_safe::{self as skia, Codec, ISize}; use std::collections::HashMap; @@ -70,7 +71,7 @@ fn create_image_from_gl_texture( texture_id: u32, width: i32, height: i32, -) -> Result { +) -> Result { use skia_safe::gpu; use skia_safe::gpu::gl::TextureInfo; @@ -99,7 +100,9 @@ fn create_image_from_gl_texture( skia::AlphaType::Premul, None, ) - .ok_or("Failed to create Skia image from GL texture")?; + .ok_or(crate::error::Error::CriticalError( + "Failed to create Skia image from GL texture".to_string(), + ))?; Ok(image) } @@ -147,11 +150,16 @@ impl ImageStore { } } - pub fn add(&mut self, id: Uuid, is_thumbnail: bool, image_data: &[u8]) -> Result<(), String> { + pub fn add( + &mut self, + id: Uuid, + is_thumbnail: bool, + image_data: &[u8], + ) -> crate::error::Result<()> { let key = (id, is_thumbnail); if self.images.contains_key(&key) { - return Err("Image already exists".to_string()); + return Err(Error::RecoverableError("Image already exists".to_string())); } let raw_data = image_data.to_vec(); @@ -174,11 +182,11 @@ impl ImageStore { texture_id: u32, width: i32, height: i32, - ) -> Result<(), String> { + ) -> Result<()> { let key = (id, is_thumbnail); if self.images.contains_key(&key) { - return Err("Image already exists".to_string()); + return Err(Error::RecoverableError("Image already exists".to_string())); } // Create a Skia image from the existing GL texture diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index ea43322b70..d392305327 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -3,6 +3,7 @@ use crate::render::strokes; use crate::shapes::{ParagraphBuilderGroup, Shadow, Shape, Stroke, Type}; use skia_safe::{canvas::SaveLayerRec, Paint, Path}; +use crate::error::Result; use crate::render::text; // Fill Shadows @@ -36,7 +37,7 @@ pub fn render_stroke_inner_shadows( stroke: &Stroke, antialias: bool, surface_id: SurfaceId, -) { +) -> Result<()> { if !shape.has_fills() { for shadow in shape.inner_shadows_visible() { let filter = shadow.get_inner_shadow_filter(); @@ -48,9 +49,10 @@ pub fn render_stroke_inner_shadows( filter.as_ref(), antialias, None, // Inner shadows don't use spread - ) + )?; } } + Ok(()) } // Render text paths (unused) @@ -133,9 +135,9 @@ pub fn render_text_shadows( surface_id: Option, shadows: &[Paint], blur_filter: &Option, -) { +) -> Result<()> { if stroke_paragraphs_group.is_empty() { - return; + return Ok(()); } let canvas = render_state @@ -156,7 +158,7 @@ pub fn render_text_shadows( blur_filter.as_ref(), None, None, - ); + )?; for stroke_paragraphs in stroke_paragraphs_group.iter_mut() { text::render( @@ -169,9 +171,10 @@ pub fn render_text_shadows( blur_filter.as_ref(), None, None, - ); + )?; } canvas.restore(); } + Ok(()) } diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 319b921ac5..c5a3a26bf1 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -6,6 +6,7 @@ use crate::shapes::{ use skia_safe::{self as skia, ImageFilter, RRect}; use super::{filters, RenderState, SurfaceId}; +use crate::error::{Error, Result}; use crate::render::filters::compose_filters; use crate::render::{get_dest_rect, get_source_rect}; @@ -294,16 +295,16 @@ fn handle_stroke_caps( blur: Option<&ImageFilter>, _antialias: bool, ) { - let mut points = path.points().to_vec(); - // Curves can have duplicated points, so let's remove consecutive duplicated points - points.dedup(); - let c_points = points.len(); - // Closed shapes don't have caps - if c_points >= 2 && is_open { - let first_point = points.first().unwrap(); - let last_point = points.last().unwrap(); + if !is_open { + return; + } + // Curves can have duplicated points, so let's remove consecutive duplicated points + let mut points = path.points().to_vec(); + points.dedup(); + + if let [first_point, .., last_point] = points.as_slice() { let mut paint_stroke = paint.clone(); if let Some(filter) = blur { @@ -328,7 +329,7 @@ fn handle_stroke_caps( stroke.width, &mut paint_stroke, last_point, - &points[c_points - 2], + &points[points.len() - 2], ); } } @@ -456,14 +457,13 @@ fn draw_image_stroke_in_container( image_fill: &ImageFill, antialias: bool, surface_id: SurfaceId, -) { +) -> Result<()> { let scale = render_state.get_scale(); - let image = render_state.images.get(&image_fill.id()); - if image.is_none() { - return; - } + let Some(image) = render_state.images.get(&image_fill.id()) else { + return Ok(()); + }; - let size = image.unwrap().dimensions(); + let size = image.dimensions(); let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id); let container = &shape.selrect; let path_transform = shape.to_path_transform(); @@ -509,7 +509,10 @@ fn draw_image_stroke_in_container( shape_type @ (Type::Path(_) | Type::Bool(_)) => { if let Some(p) = shape_type.path() { canvas.save(); - let path = p.to_skia_path().make_transform(&path_transform.unwrap()); + + let path = p.to_skia_path().make_transform( + &path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?, + ); let stroke_kind = stroke.render_kind(p.is_open()); match stroke_kind { StrokeKind::Inner => { @@ -561,7 +564,7 @@ fn draw_image_stroke_in_container( canvas.clip_rect(dest_rect, skia::ClipOp::Intersect, antialias); canvas.draw_image_rect_with_sampling_options( - image.unwrap(), + image, Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)), dest_rect, render_state.sampling_options, @@ -571,7 +574,9 @@ fn draw_image_stroke_in_container( // Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area. if let Type::Path(p) = &shape.shape_type { if stroke.render_kind(p.is_open()) == StrokeKind::Outer { - let path = p.to_skia_path().make_transform(&path_transform.unwrap()); + let path = p.to_skia_path().make_transform( + &path_transform.ok_or(Error::CriticalError("No path transform".to_string()))?, + ); let mut clear_paint = skia::Paint::default(); clear_paint.set_blend_mode(skia::BlendMode::Clear); clear_paint.set_anti_alias(antialias); @@ -581,6 +586,7 @@ fn draw_image_stroke_in_container( // Restore canvas state canvas.restore(); + Ok(()) } /// Renders all strokes for a shape. Merges strokes that share the same @@ -593,9 +599,9 @@ pub fn render( surface_id: Option, antialias: bool, outset: Option, -) { +) -> Result<()> { if strokes.is_empty() { - return; + return Ok(()); } let has_image_fills = strokes.iter().any(|s| matches!(s.fill, Fill::Image(_))); @@ -655,13 +661,14 @@ pub fn render( true, true, outset, - ); + )?; } state.surfaces.canvas(temp_surface).restore(); + Ok(()) }, - ) { - return; + )? { + return Ok(()); } } @@ -675,9 +682,9 @@ pub fn render( None, antialias, outset, - ); + )?; } - return; + return Ok(()); } render_merged( @@ -688,7 +695,7 @@ pub fn render( antialias, false, outset, - ); + ) } fn strokes_share_geometry(strokes: &[&Stroke]) -> bool { @@ -709,7 +716,7 @@ fn render_merged( antialias: bool, bypass_filter: bool, outset: Option, -) { +) -> Result<()> { let representative = *strokes .last() .expect("render_merged expects at least one stroke"); @@ -761,14 +768,15 @@ fn render_merged( antialias, true, outset, - ); + )?; state.surfaces.apply_mut(temp_surface as u32, |surface| { surface.canvas().restore(); }); + Ok(()) }, - ) { - return; + )? { + return Ok(()); } } } @@ -844,6 +852,7 @@ fn render_merged( } _ => unreachable!("This shape should not have strokes"), } + Ok(()) } /// Renders a single stroke. Used by the shadow module which needs per-stroke @@ -857,7 +866,7 @@ pub fn render_single( shadow: Option<&ImageFilter>, antialias: bool, outset: Option, -) { +) -> Result<()> { render_single_internal( render_state, shape, @@ -868,7 +877,7 @@ pub fn render_single( false, false, outset, - ); + ) } #[allow(clippy::too_many_arguments)] @@ -882,7 +891,7 @@ fn render_single_internal( bypass_filter: bool, skip_blur: bool, outset: Option, -) { +) -> Result<()> { if !bypass_filter { if let Some(image_filter) = shape.image_filter(1.) { let mut content_bounds = shape.selrect; @@ -916,10 +925,10 @@ fn render_single_internal( true, true, outset, - ); + ) }, - ) { - return; + )? { + return Ok(()); } } } @@ -949,7 +958,7 @@ fn render_single_internal( image_fill, antialias, target_surface, - ); + )?; } } else { match &shape.shape_type { @@ -1014,6 +1023,7 @@ fn render_single_internal( _ => unreachable!("This shape should not have strokes"), } } + Ok(()) } // Render text paths (unused) diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 536737daba..857ccfda6d 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -1,3 +1,4 @@ +use crate::error::{Error, Result}; use crate::performance; use crate::shapes::Shape; @@ -61,38 +62,39 @@ pub struct Surfaces { #[allow(dead_code)] impl Surfaces { - pub fn new( + pub fn try_new( gpu_state: &mut GpuState, (width, height): (i32, i32), sampling_options: skia::SamplingOptions, tile_dims: skia::ISize, - ) -> Self { + ) -> Result { let extra_tile_dims = skia::ISize::new( tile_dims.width * TILE_SIZE_MULTIPLIER, tile_dims.height * TILE_SIZE_MULTIPLIER, ); let margins = skia::ISize::new(extra_tile_dims.width / 4, extra_tile_dims.height / 4); - let target = gpu_state.create_target_surface(width, height); - let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims); - let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height); - let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims); + let target = gpu_state.create_target_surface(width, height)?; + let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims)?; + let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height)?; + let current = + gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims)?; let drop_shadows = - gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims); + gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims)?; let inner_shadows = - gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims); - let text_drop_shadows = - gpu_state.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims); + gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims)?; + let text_drop_shadows = gpu_state + .create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims)?; let shape_fills = - gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims); + 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); + 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 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(); - Surfaces { + Ok(Surfaces { target, filter, cache, @@ -108,7 +110,7 @@ impl Surfaces { sampling_options, margins, dirty_surfaces: 0, - } + }) } pub fn clear_tiles(&mut self) { @@ -119,8 +121,14 @@ impl Surfaces { self.margins } - pub fn resize(&mut self, gpu_state: &mut GpuState, new_width: i32, new_height: i32) { - self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)); + pub fn resize( + &mut self, + gpu_state: &mut GpuState, + new_width: i32, + new_height: i32, + ) -> Result<()> { + self.reset_from_target(gpu_state.create_target_surface(new_width, new_height)?)?; + Ok(()) } pub fn snapshot(&mut self, id: SurfaceId) -> skia::Image { @@ -132,26 +140,33 @@ impl Surfaces { (self.filter.width(), self.filter.height()) } - pub fn base64_snapshot(&mut self, id: SurfaceId) -> String { + pub fn base64_snapshot(&mut self, id: SurfaceId) -> Result { let surface = self.get_mut(id); let image = surface.image_snapshot(); let mut context = surface.direct_context(); let encoded_image = image .encode(context.as_mut(), skia::EncodedImageFormat::PNG, None) - .unwrap(); - general_purpose::STANDARD.encode(encoded_image.as_bytes()) + .ok_or(Error::CriticalError("Failed to encode image".to_string()))?; + Ok(general_purpose::STANDARD.encode(encoded_image.as_bytes())) } - pub fn base64_snapshot_rect(&mut self, id: SurfaceId, irect: skia::IRect) -> Option { + pub fn base64_snapshot_rect( + &mut self, + id: SurfaceId, + irect: skia::IRect, + ) -> Result> { let surface = self.get_mut(id); if let Some(image) = surface.image_snapshot_with_bounds(irect) { let mut context = surface.direct_context(); let encoded_image = image .encode(context.as_mut(), skia::EncodedImageFormat::PNG, None) - .unwrap(); - return Some(general_purpose::STANDARD.encode(encoded_image.as_bytes())); + .ok_or(Error::CriticalError("Failed to encode image".to_string()))?; + Ok(Some( + general_purpose::STANDARD.encode(encoded_image.as_bytes()), + )) + } else { + Ok(None) } - None } /// Returns a mutable reference to the canvas and automatically marks @@ -341,22 +356,41 @@ impl Surfaces { } } - fn reset_from_target(&mut self, target: skia::Surface) { + fn reset_from_target(&mut self, target: skia::Surface) -> Result<()> { let dim = (target.width(), target.height()); self.target = target; - self.filter = self.target.new_surface_with_dimensions(dim).unwrap(); - self.debug = self.target.new_surface_with_dimensions(dim).unwrap(); - self.ui = self.target.new_surface_with_dimensions(dim).unwrap(); + self.filter = self + .target + .new_surface_with_dimensions(dim) + .ok_or(Error::CriticalError("Failed to create surface".to_string()))?; + self.debug = self + .target + .new_surface_with_dimensions(dim) + .ok_or(Error::CriticalError("Failed to create surface".to_string()))?; + self.ui = self + .target + .new_surface_with_dimensions(dim) + .ok_or(Error::CriticalError("Failed to create surface".to_string()))?; // The rest are tile size surfaces + + Ok(()) } - pub fn resize_cache(&mut self, cache_dims: skia::ISize, interest_area_threshold: i32) { - self.cache = self.target.new_surface_with_dimensions(cache_dims).unwrap(); + pub fn resize_cache( + &mut self, + cache_dims: skia::ISize, + interest_area_threshold: i32, + ) -> Result<()> { + self.cache = self + .target + .new_surface_with_dimensions(cache_dims) + .ok_or(Error::CriticalError("Failed to create surface".to_string()))?; self.cache.canvas().reset_matrix(); self.cache.canvas().translate(( (interest_area_threshold as f32 * TILE_SIZE), (interest_area_threshold as f32 * TILE_SIZE), )); + Ok(()) } pub fn draw_rect_to( diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 832503505d..85a150284b 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -1,5 +1,6 @@ use super::{filters, RenderState, Shape, SurfaceId}; use crate::{ + error::Result, math::Rect, shapes::{ calculate_position_data, calculate_text_layout_data, merge_fills, set_paint_fill, @@ -66,7 +67,7 @@ pub fn stroke_paragraph_builder_group_from_text( } let stroke_paragraphs: Vec = (0..stroke_paragraphs_map.len()) - .map(|i| stroke_paragraphs_map.remove(&i).unwrap()) + .filter_map(|i| stroke_paragraphs_map.remove(&i)) .collect(); paragraph_group.push(stroke_paragraphs); @@ -195,7 +196,7 @@ pub fn render_with_bounds_outset( stroke_bounds_outset: f32, fill_inset: Option, layer_opacity: Option, -) { +) -> Result<()> { if let Some(render_state) = render_state { let target_surface = surface_id.unwrap_or(SurfaceId::Fills); @@ -225,9 +226,10 @@ pub fn render_with_bounds_outset( fill_inset, layer_opacity, ); + Ok(()) }, - ) { - return; + )? { + return Ok(()); } } } @@ -242,7 +244,7 @@ pub fn render_with_bounds_outset( fill_inset, layer_opacity, ); - return; + return Ok(()); } if let Some(canvas) = canvas { @@ -256,6 +258,7 @@ pub fn render_with_bounds_outset( layer_opacity, ); } + Ok(()) } #[allow(clippy::too_many_arguments)] @@ -269,7 +272,7 @@ pub fn render( blur: Option<&ImageFilter>, fill_inset: Option, layer_opacity: Option, -) { +) -> Result<()> { render_with_bounds_outset( render_state, canvas, @@ -281,7 +284,7 @@ pub fn render( 0.0, fill_inset, layer_opacity, - ); + ) } fn render_text_on_canvas( diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs index 9713c066a9..4c7f6d69d4 100644 --- a/render-wasm/src/shapes/modifiers.rs +++ b/render-wasm/src/shapes/modifiers.rs @@ -9,6 +9,7 @@ pub mod grid_layout; use crate::math::{self as math, bools, identitish, is_close_to, Bounds, Matrix, Point}; use common::GetBounds; +use crate::error::Result; use crate::shapes::{ ConstraintH, ConstraintV, Frame, Group, GrowType, Layout, Modifier, Shape, TransformEntry, TransformEntrySource, Type, @@ -24,9 +25,9 @@ fn propagate_children( parent_bounds_after: &Bounds, transform: Matrix, bounds: &HashMap, -) -> VecDeque { +) -> Result> { if identitish(&transform) { - return VecDeque::new(); + return Ok(VecDeque::new()); } let mut result = VecDeque::new(); @@ -74,12 +75,12 @@ fn propagate_children( constraint_v, transform, child.ignore_constraints, - ); + )?; result.push_back(Modifier::transform_propagate(*child_id, transform)); } - result + Ok(result) } fn calculate_group_bounds( @@ -172,9 +173,9 @@ fn propagate_transform( entries: &mut VecDeque, bounds: &mut HashMap, modifiers: &mut HashMap, -) { +) -> Result<()> { let Some(shape) = state.shapes.get(&entry.id) else { - return; + return Ok(()); }; let shapes = &state.shapes; @@ -249,7 +250,7 @@ fn propagate_transform( &shape_bounds_after, transform, bounds, - ); + )?; entries.append(&mut children); } @@ -275,6 +276,7 @@ fn propagate_transform( entries.push_back(Modifier::reflow(parent.id, false)); } } + Ok(()) } fn propagate_reflow( @@ -338,34 +340,35 @@ fn reflow_shape( reflown: &mut HashSet, entries: &mut VecDeque, bounds: &mut HashMap, -) { +) -> Result<()> { let Some(shape) = state.shapes.get(id) else { - return; + return Ok(()); }; let shapes = &state.shapes; let Type::Frame(frame_data) = &shape.shape_type else { - return; + return Ok(()); }; if let Some(Layout::FlexLayout(layout_data, flex_data)) = &frame_data.layout { let mut children = - flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds); + flex_layout::reflow_flex_layout(shape, layout_data, flex_data, shapes, bounds)?; entries.append(&mut children); } else if let Some(Layout::GridLayout(layout_data, grid_data)) = &frame_data.layout { let mut children = - grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds); + grid_layout::reflow_grid_layout(shape, layout_data, grid_data, shapes, bounds)?; entries.append(&mut children); } reflown.insert(*id); + Ok(()) } pub fn propagate_modifiers( state: &State, modifiers: &[TransformEntry], pixel_precision: bool, -) -> Vec { +) -> Result> { let mut entries: VecDeque<_> = modifiers .iter() .map(|entry| { @@ -399,7 +402,7 @@ pub fn propagate_modifiers( &mut entries, &mut bounds, &mut modifiers, - ), + )?, Modifier::Reflow(id, force_reflow) => { if force_reflow { reflown.remove(&id); @@ -437,16 +440,16 @@ pub fn propagate_modifiers( if reflown.contains(id) { continue; } - reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp); + reflow_shape(id, state, &mut reflown, &mut entries, &mut bounds_temp)?; } layout_reflows = HashSet::new(); } - #[allow(dead_code)] - modifiers + // #[allow(dead_code)] + Ok(modifiers .iter() .map(|(key, val)| TransformEntry::from_input(*key, *val)) - .collect() + .collect()) } #[cfg(test)] @@ -494,7 +497,8 @@ mod tests { &bounds_after, transform, &HashMap::new(), - ); + ) + .unwrap(); assert_eq!(result.len(), 1); } diff --git a/render-wasm/src/shapes/modifiers/constraints.rs b/render-wasm/src/shapes/modifiers/constraints.rs index 190fd32734..4f5a9cb228 100644 --- a/render-wasm/src/shapes/modifiers/constraints.rs +++ b/render-wasm/src/shapes/modifiers/constraints.rs @@ -1,3 +1,4 @@ +use crate::error::{Error, Result}; use crate::math::{is_move_only_matrix, Bounds, Matrix}; use crate::shapes::{ConstraintH, ConstraintV}; @@ -105,14 +106,14 @@ pub fn propagate_shape_constraints( constraint_v: ConstraintV, transform: Matrix, ignore_constrainst: bool, -) -> Matrix { +) -> Result { // if the constrains are scale & scale or the transform has only moves we // can propagate as is if (ignore_constrainst || constraint_h == ConstraintH::Scale && constraint_v == ConstraintV::Scale) || is_move_only_matrix(&transform) { - return transform; + return Ok(transform); } let mut transform = transform; @@ -133,7 +134,9 @@ pub fn propagate_shape_constraints( parent_transform.post_translate(center); parent_transform.pre_translate(-center); - let parent_transform_inv = &parent_transform.invert().unwrap(); + let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError( + "Failed to invert parent transform".to_string(), + ))?; let origin = parent_transform_inv.map_point(child_bounds_after.nw); let mut scale = Matrix::scale((scale_width, scale_height)); @@ -160,5 +163,5 @@ pub fn propagate_shape_constraints( transform.post_concat(&Matrix::translate(th + tv)); } - transform + Ok(transform) } diff --git a/render-wasm/src/shapes/modifiers/flex_layout.rs b/render-wasm/src/shapes/modifiers/flex_layout.rs index 72191a32a2..7661d93088 100644 --- a/render-wasm/src/shapes/modifiers/flex_layout.rs +++ b/render-wasm/src/shapes/modifiers/flex_layout.rs @@ -1,4 +1,6 @@ #![allow(dead_code)] + +use crate::error::{Error, Result}; use crate::math::{self as math, Bounds, Matrix, Point, Vector, VectorExt}; use crate::shapes::{ AlignContent, AlignItems, AlignSelf, FlexData, JustifyContent, LayoutData, LayoutItem, @@ -588,7 +590,7 @@ pub fn reflow_flex_layout( flex_data: &FlexData, shapes: ShapesPoolRef, bounds: &mut HashMap, -) -> VecDeque { +) -> Result> { let mut result = VecDeque::new(); let layout_bounds = &bounds.find(shape); let layout_axis = LayoutAxis::new(shape, layout_bounds, layout_data, flex_data); @@ -724,7 +726,9 @@ pub fn reflow_flex_layout( let parent_transform = layout_bounds.transform_matrix().unwrap_or_default(); - let parent_transform_inv = &parent_transform.invert().unwrap(); + let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError( + "Failed to invert parent transform".to_string(), + ))?; let origin = parent_transform_inv.map_point(layout_bounds.nw); let mut scale = Matrix::scale((scale_width, scale_height)); @@ -737,5 +741,5 @@ pub fn reflow_flex_layout( result.push_back(Modifier::parent(shape.id, scale)); bounds.insert(shape.id, layout_bounds_after); } - result + Ok(result) } diff --git a/render-wasm/src/shapes/modifiers/grid_layout.rs b/render-wasm/src/shapes/modifiers/grid_layout.rs index 7b2a314989..7ef2cb447b 100644 --- a/render-wasm/src/shapes/modifiers/grid_layout.rs +++ b/render-wasm/src/shapes/modifiers/grid_layout.rs @@ -1,3 +1,4 @@ +use crate::error::{Error, Result}; use crate::math::{self as math, intersect_rays, Bounds, Matrix, Point, Ray, Vector, VectorExt}; use crate::shapes::{ AlignContent, AlignItems, AlignSelf, Frame, GridCell, GridData, GridTrack, GridTrackType, @@ -6,6 +7,7 @@ use crate::shapes::{ }; use crate::state::ShapesPoolRef; use crate::uuid::Uuid; + use std::collections::{HashMap, HashSet, VecDeque}; use super::common::GetBounds; @@ -704,7 +706,7 @@ pub fn reflow_grid_layout( grid_data: &GridData, shapes: ShapesPoolRef, bounds: &mut HashMap, -) -> VecDeque { +) -> Result> { let mut result = VecDeque::new(); let layout_bounds = bounds.find(shape); let children: HashSet = shape.children_ids_iter(true).copied().collect(); @@ -825,7 +827,9 @@ pub fn reflow_grid_layout( let parent_transform = layout_bounds.transform_matrix().unwrap_or_default(); - let parent_transform_inv = &parent_transform.invert().unwrap(); + let parent_transform_inv = &parent_transform.invert().ok_or(Error::CriticalError( + "Failed to invert parent transform".to_string(), + ))?; let origin = parent_transform_inv.map_point(layout_bounds.nw); let mut scale = Matrix::scale((scale_width, scale_height)); @@ -839,5 +843,5 @@ pub fn reflow_grid_layout( bounds.insert(shape.id, layout_bounds_after); } - result + Ok(result) } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index b99b768334..3509b529aa 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -6,6 +6,7 @@ mod text_editor; pub use shapes_pool::{ShapesPool, ShapesPoolMutRef, ShapesPoolRef}; pub use text_editor::*; +use crate::error::{Error, Result}; use crate::render::RenderState; use crate::shapes::Shape; use crate::tiles; @@ -28,41 +29,44 @@ pub(crate) struct State { } impl State { - pub fn new(width: i32, height: i32) -> Self { - State { - render_state: RenderState::new(width, height), + pub fn try_new(width: i32, height: i32) -> Result { + Ok(State { + render_state: RenderState::try_new(width, height)?, text_editor_state: TextEditorState::new(), current_id: None, current_browser: 0, shapes: ShapesPool::new(), // TODO: Maybe this can be moved to a different object saved_shapes: None, - } + }) } // Creates a new temporary shapes pool. // Will panic if a previous temporary pool exists. - pub fn start_temp_objects(mut self) -> Self { + pub fn start_temp_objects(mut self) -> Result { if self.saved_shapes.is_some() { - panic!("Tried to start a temp objects while the previous have not been restored"); + return Err(Error::CriticalError( + "Tried to start a temp objects while the previous have not been restored" + .to_string(), + )); } self.saved_shapes = Some(self.shapes); self.shapes = ShapesPool::new(); - self + Ok(self) } // Disposes of the temporary shapes pool restoring the normal pool // Will panic if a there is no temporary pool. - pub fn end_temp_objects(mut self) -> Self { - self.shapes = self - .saved_shapes - .expect("Tried to end temp objects but not content to be restored is present"); + pub fn end_temp_objects(mut self) -> Result { + self.shapes = self.saved_shapes.ok_or(Error::CriticalError( + "Tried to end temp objects but not content to be restored is present".to_string(), + ))?; self.saved_shapes = None; - self + Ok(self) } - pub fn resize(&mut self, width: i32, height: i32) { - self.render_state.resize(width, height); + pub fn resize(&mut self, width: i32, height: i32) -> Result<()> { + self.render_state.resize(width, height) } pub fn render_state_mut(&mut self) -> &mut RenderState { @@ -87,19 +91,17 @@ impl State { self.render_state.render_from_cache(&self.shapes); } - pub fn render_sync(&mut self, timestamp: i32) -> Result<(), String> { + pub fn render_sync(&mut self, timestamp: i32) -> Result<()> { self.render_state - .start_render_loop(None, &self.shapes, timestamp, true)?; - Ok(()) + .start_render_loop(None, &self.shapes, timestamp, true) } - pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<(), String> { + pub fn render_sync_shape(&mut self, id: &Uuid, timestamp: i32) -> Result<()> { self.render_state - .start_render_loop(Some(id), &self.shapes, timestamp, true)?; - Ok(()) + .start_render_loop(Some(id), &self.shapes, timestamp, true) } - pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> { + pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> { // If zoom changed, we MUST rebuild the tile index before using it. // Otherwise, the index will have tiles from the old zoom level, causing visible // tiles to appear empty. This can happen if start_render_loop() is called before @@ -111,14 +113,12 @@ impl State { } self.render_state - .start_render_loop(None, &self.shapes, timestamp, false)?; - Ok(()) + .start_render_loop(None, &self.shapes, timestamp, false) } - pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<(), String> { + pub fn process_animation_frame(&mut self, timestamp: i32) -> Result<()> { self.render_state - .process_animation_frame(None, &self.shapes, timestamp)?; - Ok(()) + .process_animation_frame(None, &self.shapes, timestamp) } pub fn clear_focus_mode(&mut self) { @@ -227,10 +227,10 @@ impl State { let _ = self.render_state.render_preview(&self.shapes, timestamp); } - pub fn rebuild_modifier_tiles(&mut self, ids: Vec) { + pub fn rebuild_modifier_tiles(&mut self, ids: Vec) -> Result<()> { // Index-based storage is safe self.render_state - .rebuild_modifier_tiles(&mut self.shapes, ids); + .rebuild_modifier_tiles(&mut self.shapes, ids) } pub fn font_collection(&self) -> &FontCollection { diff --git a/render-wasm/src/tiles.rs b/render-wasm/src/tiles.rs index 02bd5c5eb5..13ed4c1aeb 100644 --- a/render-wasm/src/tiles.rs +++ b/render-wasm/src/tiles.rs @@ -3,7 +3,6 @@ use crate::uuid::Uuid; use crate::view::Viewbox; use skia_safe as skia; use std::collections::{HashMap, HashSet}; - #[derive(PartialEq, Eq, Hash, Clone, Copy, Debug)] pub struct Tile(pub i32, pub i32); @@ -178,13 +177,10 @@ impl TileHashMap { } pub fn add_shape_at(&mut self, tile: Tile, shape_id: Uuid) { - self.grid.entry(tile).or_default(); - self.index.entry(shape_id).or_default(); - - let tile_set = self.grid.get_mut(&tile).unwrap(); + let tile_set = self.grid.entry(tile).or_default(); tile_set.insert(shape_id); - let index_set = self.index.get_mut(&shape_id).unwrap(); + let index_set = self.index.entry(shape_id).or_default(); index_set.insert(tile); } diff --git a/render-wasm/src/wasm/fills/image.rs b/render-wasm/src/wasm/fills/image.rs index f0e5b36526..b122de4cc8 100644 --- a/render-wasm/src/wasm/fills/image.rs +++ b/render-wasm/src/wasm/fills/image.rs @@ -1,11 +1,10 @@ +use crate::error::{Error, Result}; use crate::mem; -use macros::wasm_error; -// use crate::mem::SerializableResult; -use crate::error::Error; use crate::uuid::Uuid; use crate::with_state_mut; use crate::STATE; use crate::{shapes::ImageFill, utils::uuid_from_u32_quartet}; +use macros::wasm_error; const FLAG_KEEP_ASPECT_RATIO: u8 = 1 << 0; const IMAGE_IDS_SIZE: usize = 32; @@ -50,6 +49,7 @@ pub struct ShapeImageIds { impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds { fn from(bytes: [u8; IMAGE_IDS_SIZE]) -> Self { + // FIXME: this should probably be a try_from instead let shape_id = Uuid::try_from(&bytes[0..16]).unwrap(); let image_id = Uuid::try_from(&bytes[16..32]).unwrap(); ShapeImageIds { shape_id, image_id } @@ -57,9 +57,9 @@ impl From<[u8; IMAGE_IDS_SIZE]> for ShapeImageIds { } impl TryFrom> for ShapeImageIds { - type Error = &'static str; + type Error = Error; - fn try_from(value: Vec) -> Result { + fn try_from(value: Vec) -> Result { let mut arr = [0u8; IMAGE_IDS_SIZE]; arr.copy_from_slice(&value); Ok(ShapeImageIds::from(arr)) @@ -68,13 +68,16 @@ impl TryFrom> for ShapeImageIds { #[no_mangle] #[wasm_error] -pub extern "C" fn store_image() -> crate::error::Result<()> { +pub extern "C" fn store_image() -> Result<()> { let bytes = mem::bytes(); - let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap(); + let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec())?; // Read is_thumbnail flag (4 bytes as u32) let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE]; - let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap()); + let is_thumbnail_value = + u32::from_le_bytes(is_thumbnail_bytes.try_into().map_err(|_| { + Error::CriticalError("Invalid bytes for is_thumbnail flag".to_string()) + })?); let is_thumbnail = is_thumbnail_value != 0; let image_bytes = &bytes[IMAGE_HEADER_SIZE..]; @@ -104,9 +107,10 @@ pub extern "C" fn store_image() -> crate::error::Result<()> { /// - bytes 44-47: height (i32) #[no_mangle] #[wasm_error] -pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> { +pub extern "C" fn store_image_from_texture() -> Result<()> { let bytes = mem::bytes(); + // FIXME: where does this 48 come from? if bytes.len() < 48 { // FIXME: Review if this should be an critical or a recoverable error. eprintln!("store_image_from_texture: insufficient data"); @@ -116,23 +120,41 @@ pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> { )); } - let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap(); + let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()) + .map_err(|_| Error::CriticalError("Invalid image ids".to_string()))?; + + // FIXME: read bytes in a safe way // Read is_thumbnail flag (4 bytes as u32) let is_thumbnail_bytes = &bytes[IMAGE_IDS_SIZE..IMAGE_HEADER_SIZE]; - let is_thumbnail_value = u32::from_le_bytes(is_thumbnail_bytes.try_into().unwrap()); + let is_thumbnail_value = + u32::from_le_bytes(is_thumbnail_bytes.try_into().map_err(|_| { + Error::CriticalError("Invalid bytes for is_thumbnail flag".to_string()) + })?); let is_thumbnail = is_thumbnail_value != 0; // Read GL texture ID (4 bytes as u32) let texture_id_bytes = &bytes[36..40]; - let texture_id = u32::from_le_bytes(texture_id_bytes.try_into().unwrap()); + let texture_id = u32::from_le_bytes( + texture_id_bytes + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for texture id".to_string()))?, + ); // Read width and height (8 bytes as two i32s) let width_bytes = &bytes[40..44]; - let width = i32::from_le_bytes(width_bytes.try_into().unwrap()); + let width = i32::from_le_bytes( + width_bytes + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for width".to_string()))?, + ); let height_bytes = &bytes[44..48]; - let height = i32::from_le_bytes(height_bytes.try_into().unwrap()); + let height = i32::from_le_bytes( + height_bytes + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for height".to_string()))?, + ); with_state_mut!(state, { if let Err(msg) = state.render_state_mut().add_image_from_gl_texture( @@ -142,6 +164,7 @@ pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> { width, height, ) { + // FIXME: Review if we should return a RecoverableError eprintln!("store_image_from_texture error: {}", msg); } state.touch_shape(ids.shape_id); diff --git a/render-wasm/src/wasm/layouts/grid.rs b/render-wasm/src/wasm/layouts/grid.rs index d1a0476814..aac2fd1928 100644 --- a/render-wasm/src/wasm/layouts/grid.rs +++ b/render-wasm/src/wasm/layouts/grid.rs @@ -8,7 +8,7 @@ use crate::{uuid_from_u32_quartet, with_current_shape_mut, with_state, with_stat use super::align; #[allow(unused_imports)] -use crate::error::Result; +use crate::error::{Error, Result}; #[derive(Debug)] #[repr(C, align(1))] @@ -177,9 +177,13 @@ pub extern "C" fn set_grid_columns() -> Result<()> { let entries: Vec = bytes .chunks(size_of::()) - .map(|data| data.try_into().unwrap()) - .map(|data: [u8; size_of::()]| RawGridTrack::from(data).into()) - .collect(); + .map(|data| { + let track_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?; + Ok(RawGridTrack::from(track_bytes).into()) + }) + .collect::>>()?; with_current_shape_mut!(state, |shape: &mut Shape| { shape.set_grid_columns(entries); @@ -196,9 +200,13 @@ pub extern "C" fn set_grid_rows() -> Result<()> { let entries: Vec = bytes .chunks(size_of::()) - .map(|data| data.try_into().unwrap()) - .map(|data: [u8; size_of::()]| RawGridTrack::from(data).into()) - .collect(); + .map(|data| { + let track_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid track".to_string()))?; + Ok(RawGridTrack::from(track_bytes).into()) + }) + .collect::>>()?; with_current_shape_mut!(state, |shape: &mut Shape| { shape.set_grid_rows(entries); @@ -215,9 +223,13 @@ pub extern "C" fn set_grid_cells() -> Result<()> { let cells: Vec = bytes .chunks(size_of::()) - .map(|data| data.try_into().expect("Invalid grid cell data")) - .map(|data: [u8; size_of::()]| RawGridCell::from(data)) - .collect(); + .map(|data| { + let cell_bytes: [u8; size_of::()] = data + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for grid cell".to_string()))?; + Ok(RawGridCell::from(cell_bytes)) + }) + .collect::>>()?; with_current_shape_mut!(state, |shape: &mut Shape| { shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect()); diff --git a/render-wasm/src/wasm/paths.rs b/render-wasm/src/wasm/paths.rs index 0748111533..f700317633 100644 --- a/render-wasm/src/wasm/paths.rs +++ b/render-wasm/src/wasm/paths.rs @@ -4,6 +4,7 @@ use mem::SerializableResult; use std::mem::size_of; use std::sync::{Mutex, OnceLock}; +use crate::error::{Error, Result}; use crate::shapes::{Path, Segment, ToPath}; use crate::{mem, with_current_shape, with_current_shape_mut, STATE}; @@ -41,12 +42,12 @@ impl From<[u8; size_of::()]> for RawSegmentData { } impl TryFrom<&[u8]> for RawSegmentData { - type Error = String; - fn try_from(bytes: &[u8]) -> Result { + type Error = Error; + fn try_from(bytes: &[u8]) -> Result { let data: [u8; RAW_SEGMENT_DATA_SIZE] = bytes .get(0..RAW_SEGMENT_DATA_SIZE) .and_then(|slice| slice.try_into().ok()) - .ok_or("Invalid path data".to_string())?; + .ok_or(Error::CriticalError("Invalid path data".to_string()))?; Ok(RawSegmentData::from(data)) } } @@ -154,10 +155,14 @@ fn get_path_upload_buffer() -> &'static Mutex> { } #[no_mangle] -pub extern "C" fn start_shape_path_buffer() { +#[wasm_error] +pub extern "C" fn start_shape_path_buffer() -> Result<()> { let buffer = get_path_upload_buffer(); - let mut buffer = buffer.lock().unwrap(); + let mut buffer = buffer + .lock() + .map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?; buffer.clear(); + Ok(()) } #[no_mangle] @@ -165,32 +170,40 @@ pub extern "C" fn start_shape_path_buffer() { pub extern "C" fn set_shape_path_chunk_buffer() -> Result<()> { let bytes = mem::bytes(); let buffer = get_path_upload_buffer(); - let mut buffer = buffer.lock().unwrap(); + let mut buffer = buffer + .lock() + .map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?; buffer.extend_from_slice(&bytes); mem::free_bytes()?; Ok(()) } #[no_mangle] -pub extern "C" fn set_shape_path_buffer() { +#[wasm_error] +pub extern "C" fn set_shape_path_buffer() -> Result<()> { + let buffer = get_path_upload_buffer(); + let mut buffer = buffer + .lock() + .map_err(|_| Error::CriticalError("Failed to lock path buffer".to_string()))?; + let chunk_size = size_of::(); + if !buffer.len().is_multiple_of(chunk_size) { + // FIXME + println!("Warning: buffer length is not a multiple of chunk size!"); + } + let mut segments = Vec::new(); + for (i, chunk) in buffer.chunks(chunk_size).enumerate() { + match RawSegmentData::try_from(chunk) { + Ok(seg) => segments.push(Segment::from(seg)), + Err(e) => println!("Error at segment {}: {}", i, e), + } + } + with_current_shape_mut!(state, |shape: &mut Shape| { - let buffer = get_path_upload_buffer(); - let mut buffer = buffer.lock().unwrap(); - let chunk_size = size_of::(); - if !buffer.len().is_multiple_of(chunk_size) { - // FIXME - println!("Warning: buffer length is not a multiple of chunk size!"); - } - let mut segments = Vec::new(); - for (i, chunk) in buffer.chunks(chunk_size).enumerate() { - match RawSegmentData::try_from(chunk) { - Ok(seg) => segments.push(Segment::from(seg)), - Err(e) => println!("Error at segment {}: {}", i, e), - } - } shape.set_path_segments(segments); - buffer.clear(); }); + buffer.clear(); + + Ok(()) } #[no_mangle] diff --git a/render-wasm/src/wasm/shapes/base_props.rs b/render-wasm/src/wasm/shapes/base_props.rs index 265e4f7841..5e0146f276 100644 --- a/render-wasm/src/wasm/shapes/base_props.rs +++ b/render-wasm/src/wasm/shapes/base_props.rs @@ -6,6 +6,10 @@ use crate::wasm::blend::RawBlendMode; use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV}; use crate::{with_state_mut, STATE}; +#[allow(unused_imports)] +use crate::error::{Error, Result}; +use macros::wasm_error; + use super::RawShapeType; const FLAG_CLIP_CONTENT: u8 = 0b0000_0001; @@ -106,14 +110,18 @@ impl From<[u8; RAW_BASE_PROPS_SIZE]> for RawBasePropsData { } #[no_mangle] -pub extern "C" fn set_shape_base_props() { +#[wasm_error] +pub extern "C" fn set_shape_base_props() -> Result<()> { let bytes = mem::bytes(); if bytes.len() < RAW_BASE_PROPS_SIZE { - return; + return Ok(()); } - let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE].try_into().unwrap(); + // FIXME: this should just be a try_from + let data: [u8; RAW_BASE_PROPS_SIZE] = bytes[..RAW_BASE_PROPS_SIZE] + .try_into() + .map_err(|_| Error::CriticalError("Invalid bytes for base props".to_string()))?; let raw = RawBasePropsData::from(data); let id = raw.id(); @@ -151,6 +159,7 @@ pub extern "C" fn set_shape_base_props() { shape.set_corners((raw.corner_r1, raw.corner_r2, raw.corner_r3, raw.corner_r4)); } }); + Ok(()) } #[cfg(test)] diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs index 3635e0949f..f34aa10cf2 100644 --- a/render-wasm/src/wasm/text.rs +++ b/render-wasm/src/wasm/text.rs @@ -292,9 +292,10 @@ pub extern "C" fn clear_shape_text() { #[wasm_error] pub extern "C" fn set_shape_text_content() -> crate::error::Result<()> { let bytes = mem::bytes(); - with_current_shape_mut!(state, |shape: &mut Shape| { - let raw_text_data = RawParagraph::try_from(&bytes).unwrap(); + let raw_text_data = RawParagraph::try_from(&bytes) + .map_err(|_| Error::CriticalError("Invalid text data".to_string()))?; + with_current_shape_mut!(state, |shape: &mut Shape| { shape.add_paragraph(raw_text_data.into()).map_err(|_| { Error::RecoverableError(format!( "Error with set_shape_text_content on {:?}", diff --git a/render-wasm/src/wasm/transforms.rs b/render-wasm/src/wasm/transforms.rs index 88b888d1ef..b0e0a2d84d 100644 --- a/render-wasm/src/wasm/transforms.rs +++ b/render-wasm/src/wasm/transforms.rs @@ -1,4 +1,6 @@ -use macros::ToJs; +#[allow(unused_imports)] +use crate::error::{Error, Result}; +use macros::{wasm_error, ToJs}; use skia_safe as skia; @@ -39,11 +41,11 @@ impl From<[u8; RAW_TRANSFORM_ENTRY_SIZE]> for RawTransformEntry { } impl TryFrom<&[u8]> for RawTransformEntry { - type Error = String; - fn try_from(bytes: &[u8]) -> Result { + type Error = Error; + fn try_from(bytes: &[u8]) -> Result { let bytes: [u8; RAW_TRANSFORM_ENTRY_SIZE] = bytes .try_into() - .map_err(|_| "Invalid transform entry bytes".to_string())?; + .map_err(|_| Error::CriticalError("Invalid transform entry bytes".to_string()))?; Ok(RawTransformEntry::from(bytes)) } } @@ -73,16 +75,17 @@ impl From for TransformEntry { } #[no_mangle] -pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> *mut u8 { +#[wasm_error] +pub extern "C" fn propagate_modifiers(pixel_precision: bool) -> Result<*mut u8> { let bytes = mem::bytes(); let entries: Vec = bytes .chunks(RAW_TRANSFORM_ENTRY_SIZE) - .map(|data| RawTransformEntry::try_from(data).unwrap().into()) - .collect(); + .map(|data| RawTransformEntry::try_from(data).map(|entry| entry.into())) + .collect::>>()?; with_state!(state, { - let result = shapes::propagate_modifiers(state, &entries, pixel_precision); - mem::write_vec(result) + let result = shapes::propagate_modifiers(state, &entries, pixel_precision)?; + Ok(mem::write_vec(result)) }) } From a28d47f437824b4b62624de6192970f7d678198f Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 19 Mar 2026 11:05:29 +0100 Subject: [PATCH 12/22] :bug: Fix embedded editor pasting text --- .../app/main/data/workspace/clipboard.cljs | 37 ++-- .../src/app/main/data/workspace/shapes.cljs | 70 ++++---- .../app/main/data/workspace/wasm_text.cljs | 162 +++++++++++------- 3 files changed, 161 insertions(+), 108 deletions(-) diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs index 279666db33..8c1b8021a0 100644 --- a/frontend/src/app/main/data/workspace/clipboard.cljs +++ b/frontend/src/app/main/data/workspace/clipboard.cljs @@ -41,6 +41,7 @@ [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.wasm-text :as dwwt] [app.main.errors] [app.main.features :as features] [app.main.refs :as refs] @@ -970,10 +971,11 @@ text (.-textContent root) content (tc/dom->cljs root)] (when (types.text/valid-content? content) - (let [id (uuid/next) - width (max 8 (min (* 7 (count text)) 700)) - height 16 + (let [id (uuid/next) + width (max 8 (min (* 7 (count text)) 700)) + height 16 {:keys [x y]} (calculate-paste-position state) + skip-edition? (features/active-feature? state "text-editor-wasm/v1") shape {:id id :type :text @@ -985,9 +987,14 @@ :grow-type (if (> (count text) 100) :auto-height :auto-width) :content content} undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dwsh/create-and-add-shape :text x y shape) - (dwu/commit-undo-transaction undo-id)))))))) + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id) + (dwsh/create-and-add-shape :text x y shape + (when skip-edition? {:skip-edition? true}))) + (if skip-edition? + (rx/of (dwwt/resize-wasm-text-debounce id {:undo-group id + :undo-id undo-id})) + (rx/of (dwu/commit-undo-transaction undo-id)))))))))) (defn- paste-text [text] @@ -995,10 +1002,11 @@ (ptk/reify ::paste-text ptk/WatchEvent (watch [_ state _] - (let [id (uuid/next) - width (max 8 (min (* 7 (count text)) 700)) - height 16 + (let [id (uuid/next) + width (max 8 (min (* 7 (count text)) 700)) + height 16 {:keys [x y]} (calculate-paste-position state) + skip-edition? (features/active-feature? state "text-editor-wasm/v1") shape {:id id :type :text @@ -1011,9 +1019,14 @@ :content (as-content text)} undo-id (js/Symbol)] - (rx/of (dwu/start-undo-transaction undo-id) - (dwsh/create-and-add-shape :text x y shape) - (dwu/commit-undo-transaction undo-id)))))) + (rx/concat + (rx/of (dwu/start-undo-transaction undo-id) + (dwsh/create-and-add-shape :text x y shape + (when skip-edition? {:skip-edition? true}))) + (if skip-edition? + (rx/of (dwwt/resize-wasm-text-debounce id {:undo-group id + :undo-id undo-id})) + (rx/of (dwu/commit-undo-transaction undo-id)))))))) ;; TODO: why not implement it in terms of upload-media-workspace? (defn- paste-svg-text diff --git a/frontend/src/app/main/data/workspace/shapes.cljs b/frontend/src/app/main/data/workspace/shapes.cljs index 989e85eb38..022dd11d21 100644 --- a/frontend/src/app/main/data/workspace/shapes.cljs +++ b/frontend/src/app/main/data/workspace/shapes.cljs @@ -109,7 +109,7 @@ (defn add-shape ([shape] (add-shape shape {})) - ([shape {:keys [no-select? no-update-layout?]}] + ([shape {:keys [no-select? no-update-layout? skip-edition?]}] (cts/check-shape shape) @@ -139,7 +139,11 @@ (js/Symbol) parent-type - (cfh/get-shape-type objects (:parent-id shape))] + (cfh/get-shape-type objects (:parent-id shape)) + + ;; Skip edition when using embedded editor (v3) and shape already has content (e.g. paste) + start-edition? (and (cfh/text-shape? shape) + (not (and skip-edition? (some? (:content shape)))))] (rx/concat (rx/of (dwu/start-undo-transaction undo-id) @@ -149,7 +153,7 @@ (when-not no-select? (dws/select-shapes (d/ordered-set (:id shape)))) (dwu/commit-undo-transaction undo-id)) - (when (cfh/text-shape? shape) + (when start-edition? (->> (rx/of (dwe/start-edition-mode (:id shape))) (rx/observe-on :async))) @@ -217,40 +221,42 @@ (dwu/commit-undo-transaction undo-id))))))) (defn create-and-add-shape - [type frame-x frame-y {:keys [width height] :as attrs}] - (ptk/reify ::create-and-add-shape - ptk/WatchEvent - (watch [_ state _] - (let [vbc (dsh/get-viewport-center state) - x (:x attrs (- (:x vbc) (/ width 2))) - y (:y attrs (- (:y vbc) (/ height 2))) - page-id (:current-page-id state) - objects (dsh/lookup-page-objects state page-id) - frame-id (-> (dsh/lookup-page-objects state page-id) - (ctst/top-nested-frame {:x frame-x :y frame-y})) + ([type frame-x frame-y attrs] + (create-and-add-shape type frame-x frame-y attrs nil)) + ([type frame-x frame-y {:keys [width height] :as attrs} {:keys [skip-edition?]}] + (ptk/reify ::create-and-add-shape + ptk/WatchEvent + (watch [_ state _] + (let [vbc (dsh/get-viewport-center state) + x (:x attrs (- (:x vbc) (/ width 2))) + y (:y attrs (- (:y vbc) (/ height 2))) + page-id (:current-page-id state) + objects (dsh/lookup-page-objects state page-id) + frame-id (-> (dsh/lookup-page-objects state page-id) + (ctst/top-nested-frame {:x frame-x :y frame-y})) - selected (dsh/lookup-selected state) - base (cfh/get-base-shape objects selected) + selected (dsh/lookup-selected state) + base (cfh/get-base-shape objects selected) - parent-id (if (or (and (= 1 (count selected)) - (cfh/frame-shape? (get objects (first selected)))) - (empty? selected)) - frame-id - (:parent-id base)) + parent-id (if (or (and (= 1 (count selected)) + (cfh/frame-shape? (get objects (first selected)))) + (empty? selected)) + frame-id + (:parent-id base)) - ;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent - parent-id (:id (ctn/get-first-valid-parent objects parent-id)) ;; We don't want to change the structure of component copies - frame-id (:id (ctn/get-first-valid-parent objects frame-id)) + ;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent + parent-id (:id (ctn/get-first-valid-parent objects parent-id)) ;; We don't want to change the structure of component copies + frame-id (:id (ctn/get-first-valid-parent objects frame-id)) - shape (cts/setup-shape - (-> attrs - (assoc :type type) - (assoc :x x) - (assoc :y y) - (assoc :frame-id frame-id) - (assoc :parent-id parent-id)))] + shape (cts/setup-shape + (-> attrs + (assoc :type type) + (assoc :x x) + (assoc :y y) + (assoc :frame-id frame-id) + (assoc :parent-id parent-id)))] - (rx/of (add-shape shape)))))) + (rx/of (add-shape shape {:skip-edition? skip-edition?}))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Artboard diff --git a/frontend/src/app/main/data/workspace/wasm_text.cljs b/frontend/src/app/main/data/workspace/wasm_text.cljs index c43cee567f..c175b46bdf 100644 --- a/frontend/src/app/main/data/workspace/wasm_text.cljs +++ b/frontend/src/app/main/data/workspace/wasm_text.cljs @@ -17,6 +17,7 @@ [app.common.types.modifiers :as ctm] [app.main.data.helpers :as dsh] [app.main.data.workspace.modifiers :as dwm] + [app.main.data.workspace.undo :as dwu] [app.render-wasm.api :as wasm.api] [app.render-wasm.api.fonts :as wasm.fonts] [beicon.v2.core :as rx] @@ -76,82 +77,115 @@ (rx/empty)))))) (defn resize-wasm-text-debounce-commit - [] - (ptk/reify ::resize-wasm-text-debounce-commit - ptk/WatchEvent - (watch [_ state _] - (let [ids (get state ::resize-wasm-text-debounce-ids) - objects (dsh/lookup-page-objects state) + ([] + (resize-wasm-text-debounce-commit nil nil)) + ([undo-group undo-id] + (ptk/reify ::resize-wasm-text-debounce-commit + ptk/WatchEvent + (watch [_ state _] + (let [ids (get state ::resize-wasm-text-debounce-ids) + objects (dsh/lookup-page-objects state) - modifiers - (reduce - (fn [modifiers id] - (let [shape (get objects id)] - (cond-> modifiers - (and (some? shape) - (cfh/text-shape? shape) - (not= :fixed (:grow-type shape))) - (merge (resize-wasm-text-modifiers shape))))) - {} - ids)] - (if (not (empty? modifiers)) - (rx/of (dwm/apply-wasm-modifiers modifiers)) - (rx/empty)))))) + modifiers + (reduce + (fn [modifiers id] + (let [shape (get objects id)] + (cond-> modifiers + (and (some? shape) + (cfh/text-shape? shape) + (not= :fixed (:grow-type shape))) + (merge (resize-wasm-text-modifiers shape))))) + {} + ids) + + ;; When undo-id is present, extend the current undo transaction instead of + ;; creating a new one, and commit it after the resize (single undo action). + extend-tx? (some? undo-id) + apply-opts (cond-> {} + (some? undo-group) (assoc :undo-group undo-group) + extend-tx? (assoc :undo-transation? false))] + (cond + (not (empty? modifiers)) + (if extend-tx? + (rx/concat + (rx/of (dwm/apply-wasm-modifiers modifiers apply-opts)) + (rx/of (dwu/commit-undo-transaction undo-id))) + (rx/of (dwm/apply-wasm-modifiers modifiers apply-opts))) + + extend-tx? + ;; No resize needed (e.g. :fixed grow-type) but we must commit the add + (rx/of (dwu/commit-undo-transaction undo-id)) + + :else + (rx/empty))))))) ;; This event will debounce the resize events so, if there are many, they ;; are processed at the same time and not one-by-one. This will improve ;; performance because it's better to make only one layout calculation instead ;; of (potentialy) hundreds. (defn resize-wasm-text-debounce-inner - [id] - (let [cur-event (js/Symbol)] - (ptk/reify ::resize-wasm-text-debounce-inner - ptk/UpdateEvent - (update [_ state] - (-> state - (update ::resize-wasm-text-debounce-ids (fnil conj []) id) - (cond-> (nil? (::resize-wasm-text-debounce-event state)) - (assoc ::resize-wasm-text-debounce-event cur-event)))) + ([id] + (resize-wasm-text-debounce-inner id nil)) + ([id {:keys [undo-group undo-id]}] + (let [cur-event (js/Symbol)] + (ptk/reify ::resize-wasm-text-debounce-inner + ptk/UpdateEvent + (update [_ state] + (-> state + (update ::resize-wasm-text-debounce-ids (fnil conj []) id) + (cond-> (nil? (::resize-wasm-text-debounce-event state)) + (assoc ::resize-wasm-text-debounce-event cur-event)))) - ptk/WatchEvent - (watch [_ state stream] - (if (= (::resize-wasm-text-debounce-event state) cur-event) - (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))] - (rx/concat - (rx/merge - (->> stream - (rx/filter (ptk/type? ::resize-wasm-text-debounce-inner)) - (rx/debounce 40) - (rx/take 1) - (rx/map #(resize-wasm-text-debounce-commit)) - (rx/take-until stopper)) - (rx/of (resize-wasm-text-debounce-inner id))) - (rx/of #(dissoc % - ::resize-wasm-text-debounce-ids - ::resize-wasm-text-debounce-event)))) - (rx/empty)))))) + ptk/WatchEvent + (watch [_ state stream] + (if (= (::resize-wasm-text-debounce-event state) cur-event) + (let [stopper (->> stream (rx/filter (ptk/type? :app.main.data.workspace/finalize)))] + (rx/concat + (rx/merge + (->> stream + (rx/filter (ptk/type? ::resize-wasm-text-debounce-inner)) + (rx/debounce 40) + (rx/take 1) + (rx/map (fn [evt] + (resize-wasm-text-debounce-commit + (some-> evt meta :undo-group) + (some-> evt meta :undo-id)))) + (rx/take-until stopper)) + (rx/of (with-meta + (resize-wasm-text-debounce-inner id) + {:undo-group undo-group :undo-id undo-id}))) + (rx/of #(dissoc % + ::resize-wasm-text-debounce-ids + ::resize-wasm-text-debounce-event)))) + (rx/empty))))))) (defn resize-wasm-text-debounce - [id] - (ptk/reify ::resize-wasm-text-debounce - ptk/WatchEvent - (watch [_ state _] - (let [page-id (:current-page-id state) - objects (dsh/lookup-page-objects state page-id) - content (dm/get-in objects [id :content]) - fonts (wasm.fonts/get-content-fonts content) + ([id] + (resize-wasm-text-debounce id nil)) + ([id {:keys [undo-group undo-id] :as opts}] + (ptk/reify ::resize-wasm-text-debounce + ptk/WatchEvent + (watch [_ state _] + (let [page-id (:current-page-id state) + objects (dsh/lookup-page-objects state page-id) + content (dm/get-in objects [id :content]) + fonts (wasm.fonts/get-content-fonts content) - fonts-loaded? - (->> fonts - (every? - (fn [font] - (let [font-data (wasm.fonts/make-font-data font)] - (wasm.fonts/font-stored? font-data (:emoji? font-data))))))] + fonts-loaded? + (->> fonts + (every? + (fn [font] + (let [font-data (wasm.fonts/make-font-data font)] + (wasm.fonts/font-stored? font-data (:emoji? font-data))))))] - (if (not fonts-loaded?) - (->> (rx/of (resize-wasm-text-debounce id)) - (rx/delay 20)) - (rx/of (resize-wasm-text-debounce-inner id))))))) + (if (not fonts-loaded?) + (->> (rx/of (resize-wasm-text-debounce id opts)) + (rx/delay 20)) + (let [pass-opts (when (or (some? undo-group) (some? undo-id)) + (cond-> {} + (some? undo-group) (assoc :undo-group undo-group) + (some? undo-id) (assoc :undo-id undo-id)))] + (rx/of (resize-wasm-text-debounce-inner id pass-opts))))))))) (defn resize-wasm-text-all "Resize all text shapes (auto-width/auto-height) from a collection of ids." From de04896266362522ff24fb7b1ab7f61df3ae7c47 Mon Sep 17 00:00:00 2001 From: Elena Torro Date: Mon, 16 Mar 2026 13:05:05 +0100 Subject: [PATCH 13/22] :wrench: Preserve cache canvas during tile rebuild for smooth zoom preview --- frontend/src/app/render_wasm/api.cljs | 6 ++--- render-wasm/src/main.rs | 22 +++++------------- render-wasm/src/render.rs | 32 +++++++++++++-------------- render-wasm/src/render/surfaces.rs | 11 +++++++++ render-wasm/src/state.rs | 16 +++++++------- 5 files changed, 44 insertions(+), 43 deletions(-) diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs index e4d097cc06..91097d6e66 100644 --- a/frontend/src/app/render_wasm/api.cljs +++ b/frontend/src/app/render_wasm/api.cljs @@ -951,14 +951,14 @@ (= result 1))) (def render-finish - (letfn [(do-render [ts] + (letfn [(do-render [] ;; Check if context is still initialized before executing ;; to prevent errors when navigating quickly (when wasm/context-initialized? (perf/begin-measure "render-finish") (h/call wasm/internal-module "_set_view_end") - (render ts) - (perf/end-measure "render-finish")))] + (perf/end-measure "render-finish") + (render (js/performance.now))))] (fns/debounce do-render DEBOUNCE_DELAY_MS))) (def render-pan diff --git a/render-wasm/src/main.rs b/render-wasm/src/main.rs index 017540a11b..bdb7a6ba88 100644 --- a/render-wasm/src/main.rs +++ b/render-wasm/src/main.rs @@ -296,43 +296,33 @@ pub extern "C" fn set_view_start() -> Result<()> { Ok(()) } +/// Finishes a view interaction (zoom or pan). Rebuilds the tile index +/// and invalidates the tile texture cache so the subsequent render +/// re-draws all tiles at full quality (fast_mode is off at this point). #[no_mangle] #[wasm_error] pub extern "C" fn set_view_end() -> Result<()> { with_state_mut!(state, { - let _end_start = performance::begin_timed_log!("set_view_end"); performance::begin_measure!("set_view_end"); state.render_state.options.set_fast_mode(false); state.render_state.cancel_animation_frame(); - // Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area - // This is critical because we limit tiles to the interest area for optimization let scale = state.render_state.get_scale(); state .render_state .tile_viewbox .update(state.render_state.viewbox, scale); - // We rebuild the tile index on both pan and zoom because `get_tiles_for_shape` - // clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent). - let _rebuild_start = performance::begin_timed_log!("rebuild_tiles"); - performance::begin_measure!("set_view_end::rebuild_tiles"); if state.render_state.options.is_profile_rebuild_tiles() { state.rebuild_tiles(); } else { + // Rebuild tile index + invalidate tile texture cache. + // Cache canvas is preserved so render_from_cache can still + // show a scaled preview during zoom. state.rebuild_tiles_shallow(); } - performance::end_measure!("set_view_end::rebuild_tiles"); - performance::end_timed_log!("rebuild_tiles", _rebuild_start); - state.render_state.sync_cached_viewbox(); performance::end_measure!("set_view_end"); - performance::end_timed_log!("set_view_end", _end_start); - #[cfg(feature = "profile-macros")] - { - let total_time = performance::get_time() - unsafe { VIEW_INTERACTION_START }; - performance::console_log!("[PERF] view_interaction: {}ms", total_time); - } }); Ok(()) } diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index bb0427ac78..ed117bcb60 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -2675,24 +2675,20 @@ impl RenderState { self.surfaces.remove_cached_tile_surface(tile); } - pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) { - performance::begin_measure!("rebuild_tiles_shallow"); - - // Check if zoom changed - if so, we need full cache invalidation - // because tiles are rendered at specific zoom levels + /// Rebuild the tile index (shape→tile mapping) for all top-level shapes. + /// This does NOT invalidate the tile texture cache — cached tile images + /// survive so that fast-mode renders during pan still show shadows/blur. + pub fn rebuild_tile_index(&mut self, tree: ShapesPoolRef) { let zoom_changed = self.zoom_changed(); - let mut tiles_to_invalidate = HashSet::::new(); let mut nodes = vec![Uuid::nil()]; while let Some(shape_id) = nodes.pop() { if let Some(shape) = tree.get(&shape_id) { if shape_id != Uuid::nil() { if zoom_changed { - // Zoom changed: use full update that tracks all affected tiles - tiles_to_invalidate.extend(self.update_shape_tiles(shape, tree)); + let _ = self.update_shape_tiles(shape, tree); } else { - // Pan only: use incremental update that preserves valid cached tiles - self.update_shape_tiles_incremental(shape, tree); + let _ = self.update_shape_tiles_incremental(shape, tree); } } else { // We only need to rebuild tiles from the first level. @@ -2702,9 +2698,17 @@ impl RenderState { } } } + } - // Invalidate changed tiles - old content stays visible until new tiles render - self.surfaces.remove_cached_tiles(self.background_color); + pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) { + performance::begin_measure!("rebuild_tiles_shallow"); + + self.rebuild_tile_index(tree); + + // Invalidate the tile texture cache so all tiles are re-rendered, but + // preserve the cache canvas so render_from_cache can still show a scaled + // preview of old content while new tiles load progressively. + self.surfaces.invalidate_tile_cache(); performance::end_measure!("rebuild_tiles_shallow"); } @@ -2826,10 +2830,6 @@ impl RenderState { (self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON } - pub fn sync_cached_viewbox(&mut self) { - self.cached_viewbox = self.viewbox; - } - pub fn mark_touched(&mut self, uuid: Uuid) { self.touched_ids.insert(uuid); } diff --git a/render-wasm/src/render/surfaces.rs b/render-wasm/src/render/surfaces.rs index 857ccfda6d..7337409923 100644 --- a/render-wasm/src/render/surfaces.rs +++ b/render-wasm/src/render/surfaces.rs @@ -608,11 +608,22 @@ impl Surfaces { ); } + /// Full cache reset: clears both the tile texture cache and the cache canvas. + /// Used by `rebuild_tiles` (full rebuild). For shallow rebuilds that preserve + /// the cache canvas for scaled previews, use `invalidate_tile_cache` instead. pub fn remove_cached_tiles(&mut self, color: skia::Color) { self.tiles.clear(); self.cache.canvas().clear(color); } + /// Invalidate the tile texture cache without clearing the cache canvas. + /// This forces all tiles to be re-rendered, but preserves the cache canvas + /// so that `render_from_cache` can still show a scaled preview of the old + /// content while new tiles are being rendered. + pub fn invalidate_tile_cache(&mut self) { + self.tiles.clear(); + } + pub fn gc(&mut self) { self.tiles.gc(); } diff --git a/render-wasm/src/state.rs b/render-wasm/src/state.rs index 3509b529aa..a976ef331f 100644 --- a/render-wasm/src/state.rs +++ b/render-wasm/src/state.rs @@ -102,14 +102,14 @@ impl State { } pub fn start_render_loop(&mut self, timestamp: i32) -> Result<()> { - // If zoom changed, we MUST rebuild the tile index before using it. - // Otherwise, the index will have tiles from the old zoom level, causing visible - // tiles to appear empty. This can happen if start_render_loop() is called before - // set_view_end() finishes rebuilding the index, or if set_view_end() hasn't been - // called yet. - let zoom_changed = self.render_state.zoom_changed(); - if zoom_changed { - self.rebuild_tiles_shallow(); + // If zoom changed (e.g. interrupted zoom render followed by pan), the + // tile index may be stale for the new viewport position. Rebuild the + // index so shapes are mapped to the correct tiles. We use + // rebuild_tile_index (NOT rebuild_tiles_shallow) to preserve the tile + // texture cache — otherwise cached tiles with shadows/blur would be + // cleared and re-rendered in fast mode without effects. + if self.render_state.zoom_changed() { + self.render_state.rebuild_tile_index(&self.shapes); } self.render_state From 4abaae4f80f9e6f3f446eb30a259274aad322dae Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 19 Mar 2026 13:41:33 +0100 Subject: [PATCH 14/22] :bug: Fix open tooltip on tab change (#8680) --- CHANGES.md | 1 + .../src/app/main/ui/ds/tooltip/tooltip.cljs | 34 +++++++++++++------ 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 86c535adcc..c0d68a08b1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -16,6 +16,7 @@ - Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361) - Add token name on broken token pill on sidebar [Taiga #13527](https://tree.taiga.io/project/penpot/issue/13527) +- Fix tooltip activated when tab change [Taiga #13627](https://tree.taiga.io/project/penpot/issue/13627) ## 2.14.0 (Unreleased) diff --git a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs index 087649ca63..968361f865 100644 --- a/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs +++ b/frontend/src/app/main/ui/ds/tooltip/tooltip.cljs @@ -185,17 +185,18 @@ (mf/use-fn (mf/deps tooltip-id delay) (fn [_] - (let [trigger-el (mf/ref-val trigger-ref)] - (clear-schedule schedule-ref) - (add-schedule schedule-ref (d/nilv delay 300) - (fn [] - (when-let [active @active-tooltip] - (when (not= (:id active) tooltip-id) - (when-let [tooltip-el (dom/get-element (:id active))] - (dom/set-css-property! tooltip-el "display" "none")) - (reset! active-tooltip nil))) - (reset! active-tooltip {:id tooltip-id :trigger trigger-el}) - (reset! visible* true)))))) + (when-not (.-hidden js/document) + (let [trigger-el (mf/ref-val trigger-ref)] + (clear-schedule schedule-ref) + (add-schedule schedule-ref (d/nilv delay 300) + (fn [] + (when-let [active @active-tooltip] + (when (not= (:id active) tooltip-id) + (when-let [tooltip-el (dom/get-element (:id active))] + (dom/set-css-property! tooltip-el "display" "none")) + (reset! active-tooltip nil))) + (reset! active-tooltip {:id tooltip-id :trigger trigger-el}) + (reset! visible* true))))))) on-hide (mf/use-fn @@ -243,6 +244,17 @@ content aria-label)})] + (mf/use-effect + (mf/deps tooltip-id) + (fn [] + (let [handle-visibility-change + (fn [] + (when (.-hidden js/document) + (on-hide)))] + (js/document.addEventListener "visibilitychange" handle-visibility-change) + ;; cleanup + #(js/document.removeEventListener "visibilitychange" handle-visibility-change)))) + (mf/use-effect (mf/deps visible placement offset) (fn [] From a1a469449e3d397428412f231fd4dd3f265f878b Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 18 Mar 2026 16:45:54 +0100 Subject: [PATCH 15/22] :sparkles: Add `throwValidationErrors` flag for plugins --- frontend/src/app/plugins/api.cljs | 60 ++--- frontend/src/app/plugins/comments.cljs | 26 +- frontend/src/app/plugins/file.cljs | 28 +- frontend/src/app/plugins/flags.cljs | 26 +- frontend/src/app/plugins/flex.cljs | 129 +++++---- frontend/src/app/plugins/fonts.cljs | 28 +- frontend/src/app/plugins/grid.cljs | 162 ++++++------ frontend/src/app/plugins/history.cljs | 6 +- frontend/src/app/plugins/library.cljs | 216 +++++++-------- frontend/src/app/plugins/local_storage.cljs | 12 +- frontend/src/app/plugins/page.cljs | 70 ++--- frontend/src/app/plugins/public_utils.cljs | 4 +- frontend/src/app/plugins/ruler_guides.cljs | 10 +- frontend/src/app/plugins/shape.cljs | 278 ++++++++++---------- frontend/src/app/plugins/text.cljs | 112 ++++---- frontend/src/app/plugins/tokens.cljs | 18 +- frontend/src/app/plugins/utils.cljs | 49 ++-- frontend/src/app/plugins/viewport.cljs | 8 +- plugins/CHANGELOG.md | 1 + plugins/libs/plugin-types/index.d.ts | 7 + 20 files changed, 639 insertions(+), 611 deletions(-) diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 59bd81caae..5af6cb05be 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -117,7 +117,7 @@ (fn [_ shapes] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :selection shapes) + (u/not-valid plugin-id :selection shapes) :else (let [ids (into (d/ordered-set) (map #(obj/get % "$id")) shapes)] @@ -182,7 +182,7 @@ (fn [shapes] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :shapesColors-shapes shapes) + (u/not-valid plugin-id :shapesColors-shapes shapes) :else (let [objects (u/locate-objects) @@ -202,13 +202,13 @@ new-color (parser/parse-color-data new-color)] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :replaceColor-shapes shapes) + (u/not-valid plugin-id :replaceColor-shapes shapes) (not (sm/validate ctc/schema:color old-color)) - (u/display-not-valid :replaceColor-oldColor old-color) + (u/not-valid plugin-id :replaceColor-oldColor old-color) (not (sm/validate ctc/schema:color new-color)) - (u/display-not-valid :replaceColor-newColor new-color) + (u/not-valid plugin-id :replaceColor-newColor new-color) :else (let [file-id (:current-file-id @st/state) @@ -261,10 +261,10 @@ (fn [name url] (cond (not (string? name)) - (u/display-not-valid :uploadMedia-name name) + (u/not-valid plugin-id :uploadMedia-name name) (not (string? url)) - (u/display-not-valid :uploadMedia-url url) + (u/not-valid plugin-id :uploadMedia-url url) :else (let [file-id (:current-file-id @st/state)] @@ -295,7 +295,7 @@ (fn [shapes] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :group-shapes shapes) + (u/not-valid plugin-id :group-shapes shapes) :else (let [file-id (:current-file-id @st/state) @@ -310,10 +310,10 @@ (fn [group & rest] (cond (not (shape/shape-proxy? group)) - (u/display-not-valid :ungroup group) + (u/not-valid plugin-id :ungroup group) (and (some? rest) (not (every? shape/shape-proxy? rest))) - (u/display-not-valid :ungroup rest) + (u/not-valid plugin-id :ungroup rest) :else (let [shapes (concat [group] rest) @@ -353,7 +353,7 @@ (fn [text] (cond (or (not (string? text)) (empty? text)) - (u/display-not-valid :createText text) + (u/not-valid plugin-id :createText text) :else (let [page (dsh/lookup-page @st/state) @@ -384,7 +384,7 @@ (fn [svg-string] (cond (or (not (string? svg-string)) (empty? svg-string)) - (u/display-not-valid :createShapeFromSvg svg-string) + (u/not-valid plugin-id :createShapeFromSvg svg-string) :else (let [id (uuid/next) @@ -401,7 +401,7 @@ (cond (or (not (string? svg-string)) (empty? svg-string)) (do - (u/display-not-valid :createShapeFromSvg "Svg not valid") + (u/not-valid plugin-id :createShapeFromSvg "Svg not valid") (reject "Svg not valid")) :else @@ -419,10 +419,10 @@ (let [bool-type (keyword bool-type)] (cond (not (contains? cts/bool-types bool-type)) - (u/display-not-valid :createBoolean-boolType bool-type) + (u/not-valid plugin-id :createBoolean-boolType bool-type) (or (not (array? shapes)) (empty? shapes) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :createBoolean-shapes shapes) + (u/not-valid plugin-id :createBoolean-shapes shapes) :else (let [ids (into #{} (map #(obj/get % "$id")) shapes) @@ -436,10 +436,10 @@ (let [type (d/nilv (obj/get options "type") "html")] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :generateMarkup-shapes shapes) + (u/not-valid plugin-id :generateMarkup-shapes shapes) (and (some? type) (not (contains? #{"html" "svg"} type))) - (u/display-not-valid :generateMarkup-type type) + (u/not-valid plugin-id :generateMarkup-type type) :else (let [resolved-code @@ -471,16 +471,16 @@ children? (d/nilv (obj/get options "includeChildren") true)] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :generateStyle-shapes shapes) + (u/not-valid plugin-id :generateStyle-shapes shapes) (and (some? type) (not (contains? #{"css"} type))) - (u/display-not-valid :generateStyle-type type) + (u/not-valid plugin-id :generateStyle-type type) (and (some? prelude?) (not (boolean? prelude?))) - (u/display-not-valid :generateStyle-withPrelude prelude?) + (u/not-valid plugin-id :generateStyle-withPrelude prelude?) (and (some? children?) (not (boolean? children?))) - (u/display-not-valid :generateStyle-includeChildren children?) + (u/not-valid plugin-id :generateStyle-includeChildren children?) :else (let [resolved-styles @@ -553,7 +553,7 @@ :else nil) new-window (if (boolean? new-window) new-window false)] (if (nil? id) - (u/display-not-valid :openPage "Expected a Page object or a page UUID string") + (u/not-valid plugin-id :openPage "Expected a Page object or a page UUID string") (st/emit! (dcm/go-to-workspace :page-id id ::rt/new-window new-window))))) :alignHorizontal @@ -565,10 +565,10 @@ nil)] (cond (nil? dir) - (u/display-not-valid :alignHorizontal-direction "Direction not valid") + (u/not-valid plugin-id :alignHorizontal-direction "Direction not valid") (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :alignHorizontal-shapes "Not valid shapes") + (u/not-valid plugin-id :alignHorizontal-shapes "Not valid shapes") :else (let [ids (into #{} (map #(obj/get % "$id")) shapes)] @@ -583,10 +583,10 @@ nil)] (cond (nil? dir) - (u/display-not-valid :alignVertical-direction "Direction not valid") + (u/not-valid plugin-id :alignVertical-direction "Direction not valid") (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :alignVertical-shapes "Not valid shapes") + (u/not-valid plugin-id :alignVertical-shapes "Not valid shapes") :else (let [ids (into #{} (map #(obj/get % "$id")) shapes)] @@ -596,7 +596,7 @@ (fn [shapes] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :distributeHorizontal-shapes "Not valid shapes") + (u/not-valid plugin-id :distributeHorizontal-shapes "Not valid shapes") :else (let [ids (into #{} (map #(obj/get % "$id")) shapes)] @@ -606,7 +606,7 @@ (fn [shapes] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :distributeVertical-shapes "Not valid shapes") + (u/not-valid plugin-id :distributeVertical-shapes "Not valid shapes") :else (let [ids (into #{} (map #(obj/get % "$id")) shapes)] @@ -616,7 +616,7 @@ (fn [shapes] (cond (or (not (array? shapes)) (not (every? shape/shape-proxy? shapes))) - (u/display-not-valid :flatten-shapes "Not valid shapes") + (u/not-valid plugin-id :flatten-shapes "Not valid shapes") :else (let [ids (into #{} (map #(obj/get % "$id")) shapes)] @@ -627,7 +627,7 @@ (cond (or (not (seq shapes)) (not (every? u/is-main-component-proxy? shapes))) - (u/display-not-valid :shapes shapes) + (u/not-valid plugin-id :shapes shapes) :else (let [file-id (obj/get (first shapes) "$file") diff --git a/frontend/src/app/plugins/comments.cljs b/frontend/src/app/plugins/comments.cljs index f3cfdcf954..236074142d 100644 --- a/frontend/src/app/plugins/comments.cljs +++ b/frontend/src/app/plugins/comments.cljs @@ -60,13 +60,13 @@ (let [profile (:profile @st/state)] (cond (or (not (string? content)) (empty? content)) - (u/display-not-valid :content "Not valid") + (u/not-valid plugin-id :content "Not valid") (not= (:id profile) (:owner-id data)) - (u/display-not-valid :content "Cannot change content from another user's comments") + (u/not-valid plugin-id :content "Cannot change content from another user's comments") (not (r/check-permission plugin-id "comment:write")) - (u/display-not-valid :content "Plugin doesn't have 'comment:write' permission") + (u/not-valid plugin-id :content "Plugin doesn't have 'comment:write' permission") :else (->> (rp/cmd! :update-comment {:id (:id data) :content content}) @@ -81,7 +81,7 @@ (cond (not (r/check-permission plugin-id "comment:write")) (do - (u/display-not-valid :remove "Plugin doesn't have 'comment:write' permission") + (u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission") (reject "Plugin doesn't have 'comment:write' permission")) :else @@ -120,10 +120,10 @@ (cond (or (not (sm/valid-safe-number? (:x position))) (not (sm/valid-safe-number? (:y position)))) - (u/display-not-valid :position "Not valid point") + (u/not-valid plugin-id :position "Not valid point") (not (r/check-permission plugin-id "comment:write")) - (u/display-not-valid :position "Plugin doesn't have 'comment:write' permission") + (u/not-valid plugin-id :position "Plugin doesn't have 'comment:write' permission") :else (do (st/emit! (dwc/update-comment-thread-position @data* [(:x position) (:y position)])) @@ -137,10 +137,10 @@ (fn [is-resolved] (cond (not (boolean? is-resolved)) - (u/display-not-valid :resolved "Not a boolean type") + (u/not-valid plugin-id :resolved "Not a boolean type") (not (r/check-permission plugin-id "comment:write")) - (u/display-not-valid :resolved "Plugin doesn't have 'comment:write' permission") + (u/not-valid plugin-id :resolved "Plugin doesn't have 'comment:write' permission") :else (do (st/emit! (dc/update-comment-thread (assoc @data* :is-resolved is-resolved))) @@ -153,7 +153,7 @@ (cond (not (r/check-permission plugin-id "comment:read")) (do - (u/display-not-valid :findComments "Plugin doesn't have 'comment:read' permission") + (u/not-valid plugin-id :findComments "Plugin doesn't have 'comment:read' permission") (reject "Plugin doesn't have 'comment:read' permission")) :else @@ -169,10 +169,10 @@ (fn [content] (cond (not (r/check-permission plugin-id "comment:write")) - (u/display-not-valid :reply "Plugin doesn't have 'comment:write' permission") + (u/not-valid plugin-id :reply "Plugin doesn't have 'comment:write' permission") (or (not (string? content)) (empty? content)) - (u/display-not-valid :reply "Not valid") + (u/not-valid plugin-id :reply "Not valid") :else (js/Promise. @@ -186,10 +186,10 @@ owner (dsh/lookup-profile @st/state (:owner-id data))] (cond (not (r/check-permission plugin-id "comment:write")) - (u/display-not-valid :remove "Plugin doesn't have 'comment:write' permission") + (u/not-valid plugin-id :remove "Plugin doesn't have 'comment:write' permission") (not= (:id profile) owner) - (u/display-not-valid :remove "Cannot change content from another user's comments") + (u/not-valid plugin-id :remove "Cannot change content from another user's comments") :else (js/Promise. diff --git a/frontend/src/app/plugins/file.cljs b/frontend/src/app/plugins/file.cljs index 54fcb2f8c3..15c0bf7188 100644 --- a/frontend/src/app/plugins/file.cljs +++ b/frontend/src/app/plugins/file.cljs @@ -45,10 +45,10 @@ (fn [value] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :label "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :label "Plugin doesn't have 'content:write' permission") (or (not (string? value)) (empty? value)) - (u/display-not-valid :label value) + (u/not-valid plugin-id :label value) :else (do (swap! data assoc :label value :created-by "user") @@ -145,7 +145,7 @@ (fn [key] (cond (not (string? key)) - (u/display-not-valid :getPluginData-key key) + (u/not-valid plugin-id :getPluginData-key key) :else (let [file (u/locate-file id)] @@ -155,13 +155,13 @@ (fn [key value] (cond (or (not (string? key)) (empty? key)) - (u/display-not-valid :setPluginData-key key) + (u/not-valid plugin-id :setPluginData-key key) (not (string? value)) - (u/display-not-valid :setPluginData-value value) + (u/not-valid plugin-id :setPluginData-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission") :else (st/emit! (dp/set-plugin-data id :file (keyword "plugin" (str plugin-id)) key value)))) @@ -175,10 +175,10 @@ (fn [namespace key] (cond (not (string? namespace)) - (u/display-not-valid :getSharedPluginData-namespace namespace) + (u/not-valid plugin-id :getSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :getSharedPluginData-key key) + (u/not-valid plugin-id :getSharedPluginData-key key) :else (let [file (u/locate-file id)] @@ -188,16 +188,16 @@ (fn [namespace key value] (cond (or (not (string? namespace)) (empty? namespace)) - (u/display-not-valid :setSharedPluginData-namespace namespace) + (u/not-valid plugin-id :setSharedPluginData-namespace namespace) (or (not (string? key)) (empty? key)) - (u/display-not-valid :setSharedPluginData-key key) + (u/not-valid plugin-id :setSharedPluginData-key key) (not (string? value)) - (u/display-not-valid :setSharedPluginData-value value) + (u/not-valid plugin-id :setSharedPluginData-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission") :else (st/emit! (dp/set-plugin-data id :file (keyword "shared" namespace) key value)))) @@ -206,7 +206,7 @@ (fn [namespace] (cond (not (string? namespace)) - (u/display-not-valid :getSharedPluginDataKeys namespace) + (u/not-valid plugin-id :getSharedPluginDataKeys namespace) :else (let [file (u/locate-file id)] @@ -216,7 +216,7 @@ (fn [] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :createPage "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :createPage "Plugin doesn't have 'content:write' permission") :else (let [page-id (uuid/next)] diff --git a/frontend/src/app/plugins/flags.cljs b/frontend/src/app/plugins/flags.cljs index a9f1a6dce7..c28623ac62 100644 --- a/frontend/src/app/plugins/flags.cljs +++ b/frontend/src/app/plugins/flags.cljs @@ -6,17 +6,11 @@ (ns app.plugins.flags (:require - [app.common.data.macros :as dm] [app.main.store :as st] [app.plugins.utils :as u] [app.util.object :as obj] [potok.v2.core :as ptk])) -(defn natural-child-ordering? - [plugin-id] - (boolean - (dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering]))) - (defn clear [id] (ptk/reify ::reset @@ -37,13 +31,27 @@ :naturalChildOrdering {:this false :get - (fn [] (natural-child-ordering? plugin-id)) + (fn [] (u/natural-child-ordering? plugin-id)) :set (fn [value] (cond (not (boolean? value)) - (u/display-not-valid :naturalChildOrdering value) + (u/not-valid plugin-id :naturalChildOrdering value) :else - (st/emit! (set-flag plugin-id :natural-child-ordering value))))})) + (st/emit! (set-flag plugin-id :natural-child-ordering value))))} + + :throwValidationErrors + {:this false + :get + (fn [] (u/throw-validation-errors? plugin-id)) + + :set + (fn [value] + (cond + (not (boolean? value)) + (u/not-valid plugin-id :throwValidationErrors value) + + :else + (st/emit! (set-flag plugin-id :throw-validation-errors value))))})) diff --git a/frontend/src/app/plugins/flex.cljs b/frontend/src/app/plugins/flex.cljs index a1c7ef754c..ff6de68488 100644 --- a/frontend/src/app/plugins/flex.cljs +++ b/frontend/src/app/plugins/flex.cljs @@ -12,7 +12,6 @@ [app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shapes :as dwsh] [app.main.store :as st] - [app.plugins.flags :refer [natural-child-ordering?]] [app.plugins.register :as r] [app.plugins.utils :as u] [app.util.object :as obj])) @@ -39,10 +38,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/flex-direction-types value)) - (u/display-not-valid :dir value) + (u/not-valid plugin-id :dir value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :dir "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :dir "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-flex-dir value})))))} @@ -55,10 +54,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/wrap-types value)) - (u/display-not-valid :wrap value) + (u/not-valid plugin-id :wrap value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :wrap "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :wrap "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-wrap-type value})))))} @@ -71,10 +70,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/align-items-types value)) - (u/display-not-valid :alignItems value) + (u/not-valid plugin-id :alignItems value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :alignItems "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :alignItems "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))} @@ -87,10 +86,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/align-content-types value)) - (u/display-not-valid :alignContent value) + (u/not-valid plugin-id :alignContent value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :alignContent "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :alignContent "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))} @@ -103,10 +102,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/justify-items-types value)) - (u/display-not-valid :justifyItems value) + (u/not-valid plugin-id :justifyItems value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :justifyItems "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :justifyItems "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))} @@ -119,10 +118,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/justify-content-types value)) - (u/display-not-valid :justifyContent value) + (u/not-valid plugin-id :justifyContent value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :justifyContent "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :justifyContent "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))} @@ -134,10 +133,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :rowGap value) + (u/not-valid plugin-id :rowGap value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :rowGap "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :rowGap "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))} @@ -149,10 +148,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :columnGap value) + (u/not-valid plugin-id :columnGap value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :columnGap "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :columnGap "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))} @@ -164,10 +163,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :verticalPadding value) + (u/not-valid plugin-id :verticalPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :verticalPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :verticalPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))} @@ -179,10 +178,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :horizontalPadding value) + (u/not-valid plugin-id :horizontalPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))} @@ -195,10 +194,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :topPadding value) + (u/not-valid plugin-id :topPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :topPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :topPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))} @@ -210,10 +209,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :rightPadding value) + (u/not-valid plugin-id :rightPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :rightPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :rightPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))} @@ -225,10 +224,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :bottomPadding value) + (u/not-valid plugin-id :bottomPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :bottomPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :bottomPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))} @@ -240,10 +239,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :leftPadding value) + (u/not-valid plugin-id :leftPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :leftPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :leftPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))} @@ -256,13 +255,13 @@ (fn [child] (cond (not (shape-proxy? child)) - (u/display-not-valid :appendChild child) + (u/not-valid plugin-id :appendChild child) :else (let [child-id (obj/get child "$id") shape (u/locate-shape file-id page-id id) index - (if (and (natural-child-ordering? plugin-id) (not (ctl/reverse? shape))) + (if (and (u/natural-child-ordering? plugin-id) (not (ctl/reverse? shape))) 0 (count (:shapes shape)))] (st/emit! (dwsh/relocate-shapes #{child-id} id index))))) @@ -275,10 +274,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/item-h-sizing-types value)) - (u/display-not-valid :horizontalSizing value) + (u/not-valid plugin-id :horizontalSizing value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :horizontalSizing "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :horizontalSizing "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))} @@ -291,10 +290,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/item-v-sizing-types value)) - (u/display-not-valid :verticalSizing value) + (u/not-valid plugin-id :verticalSizing value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))})) @@ -317,10 +316,10 @@ (fn [_ value] (cond (not (boolean? value)) - (u/display-not-valid :absolute value) + (u/not-valid plugin-id :absolute value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :absolute "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :absolute "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-item-absolute value}))))} @@ -332,10 +331,10 @@ (fn [_ value] (cond (sm/valid-safe-int? value) - (u/display-not-valid :zIndex value) + (u/not-valid plugin-id :zIndex value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :zIndex "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :zIndex "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-z-index value}))))} @@ -348,10 +347,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/item-h-sizing-types value)) - (u/display-not-valid :horizontalPadding value) + (u/not-valid plugin-id :horizontalPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-h-sizing value})))))} @@ -364,10 +363,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/item-v-sizing-types value)) - (u/display-not-valid :verticalSizing value) + (u/not-valid plugin-id :verticalSizing value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-v-sizing value})))))} @@ -380,10 +379,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/item-align-self-types value)) - (u/display-not-valid :alignSelf value) + (u/not-valid plugin-id :alignSelf value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :alignSelf "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :alignSelf "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-align-self value})))))} @@ -395,10 +394,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :verticalMargin value) + (u/not-valid plugin-id :verticalMargin value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :verticalMargin "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :verticalMargin "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value :m3 value}}))))} @@ -410,10 +409,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :horizontalMargin value) + (u/not-valid plugin-id :horizontalMargin value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :horizontalMargin "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :horizontalMargin "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value :m4 value}}))))} @@ -425,10 +424,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :topMargin value) + (u/not-valid plugin-id :topMargin value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :topMargin "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :topMargin "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m1 value}}))))} @@ -440,10 +439,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :rightMargin value) + (u/not-valid plugin-id :rightMargin value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :rightMargin "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :rightMargin "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m2 value}}))))} @@ -455,10 +454,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :bottomMargin value) + (u/not-valid plugin-id :bottomMargin value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :bottomMargin "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :bottomMargin "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m3 value}}))))} @@ -470,10 +469,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :leftMargin value) + (u/not-valid plugin-id :leftMargin value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :leftMargin "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :leftMargin "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-margin {:m4 value}}))))} @@ -485,10 +484,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :maxWidth value) + (u/not-valid plugin-id :maxWidth value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :maxWidth "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :maxWidth "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-w value}))))} @@ -500,10 +499,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :minWidth value) + (u/not-valid plugin-id :minWidth value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :minWidth "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :minWidth "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-w value}))))} @@ -515,10 +514,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :maxHeight value) + (u/not-valid plugin-id :maxHeight value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :maxHeight "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :maxHeight "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-max-h value}))))} @@ -530,10 +529,10 @@ (fn [_ value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :minHeight value) + (u/not-valid plugin-id :minHeight value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :minHeight "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :minHeight "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout-child #{id} {:layout-item-min-h value}))))})) diff --git a/frontend/src/app/plugins/fonts.cljs b/frontend/src/app/plugins/fonts.cljs index 77602816f6..13996fabc2 100644 --- a/frontend/src/app/plugins/fonts.cljs +++ b/frontend/src/app/plugins/fonts.cljs @@ -32,7 +32,7 @@ (obj/type-of? p "FontProxy")) (defn font-proxy - [{:keys [id family name variants] :as font}] + [plugin-id {:keys [id family name variants] :as font}] (when (some? font) (let [default-variant (fonts/get-default-variant font)] (obj/reify {:name "FontProxy"} @@ -55,10 +55,10 @@ (fn [text variant] (cond (not (shape/shape-proxy? text)) - (u/display-not-valid :applyToText text) + (u/not-valid plugin-id :applyToText text) (not (r/check-permission (obj/get text "$plugin") "content:write")) - (u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get text "$id") @@ -73,10 +73,10 @@ (fn [range variant] (cond (not (text/text-range-proxy? range)) - (u/display-not-valid :applyToRange range) + (u/not-valid plugin-id :applyToRange range) (not (r/check-permission (obj/get range "$plugin") "content:write")) - (u/display-not-valid :applyToRange "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :applyToRange "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get range "$id") @@ -98,53 +98,53 @@ {:get (fn [] (format/format-array - font-proxy + (partial font-proxy plugin-id) (vals @fonts/fontsdb)))} :findById (fn [id] (cond (not (string? id)) - (u/display-not-valid :findbyId id) + (u/not-valid plugin-id :findbyId id) :else (->> (vals @fonts/fontsdb) (d/seek #(str/includes? (str/lower (:id %)) (str/lower id))) - (font-proxy)))) + (font-proxy plugin-id)))) :findByName (fn [name] (cond (not (string? name)) - (u/display-not-valid :findByName name) + (u/not-valid plugin-id :findByName name) :else (->> (vals @fonts/fontsdb) (d/seek #(str/includes? (str/lower (:name %)) (str/lower name))) - (font-proxy)))) + (font-proxy plugin-id)))) :findAllById (fn [id] (cond (not (string? id)) - (u/display-not-valid :findAllById name) + (u/not-valid plugin-id :findAllById name) :else (format/format-array (fn [font] (when (str/includes? (str/lower (:id font)) (str/lower id)) - (font-proxy font))) + (font-proxy plugin-id font))) (vals @fonts/fontsdb)))) :findAllByName (fn [name] (cond (not (string? name)) - (u/display-not-valid :findAllByName name) + (u/not-valid plugin-id :findAllByName name) :else (format/format-array (fn [font] (when (str/includes? (str/lower (:name font)) (str/lower name)) - (font-proxy font))) + (font-proxy plugin-id font))) (vals @fonts/fontsdb)))))) diff --git a/frontend/src/app/plugins/grid.cljs b/frontend/src/app/plugins/grid.cljs index f57873ec31..351b673baf 100644 --- a/frontend/src/app/plugins/grid.cljs +++ b/frontend/src/app/plugins/grid.cljs @@ -40,10 +40,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/grid-direction-types value)) - (u/display-not-valid :dir value) + (u/not-valid plugin-id :dir value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :dir "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :dir "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-grid-dir value})))))} @@ -64,10 +64,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/align-items-types value)) - (u/display-not-valid :alignItems value) + (u/not-valid plugin-id :alignItems value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :alignItems "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :alignItems "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-align-items value})))))} @@ -80,10 +80,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/align-content-types value)) - (u/display-not-valid :alignContent value) + (u/not-valid plugin-id :alignContent value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :alignContent "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :alignContent "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-align-content value})))))} @@ -96,10 +96,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/justify-items-types value)) - (u/display-not-valid :justifyItems value) + (u/not-valid plugin-id :justifyItems value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :justifyItems "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :justifyItems "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-justify-items value})))))} @@ -112,10 +112,10 @@ (let [value (keyword value)] (cond (not (contains? ctl/justify-content-types value)) - (u/display-not-valid :justifyContent value) + (u/not-valid plugin-id :justifyContent value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :justifyContent "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :justifyContent "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-justify-content value})))))} @@ -127,10 +127,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :rowGap value) + (u/not-valid plugin-id :rowGap value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :rowGap "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :rowGap "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-gap {:row-gap value}}))))} @@ -142,10 +142,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :columnGap value) + (u/not-valid plugin-id :columnGap value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :columnGap "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :columnGap "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-gap {:column-gap value}}))))} @@ -157,10 +157,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :verticalPadding value) + (u/not-valid plugin-id :verticalPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :verticalPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :verticalPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value :p3 value}}))))} @@ -172,10 +172,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :horizontalPadding value) + (u/not-valid plugin-id :horizontalPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :horizontalPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value :p4 value}}))))} @@ -187,10 +187,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :topPadding value) + (u/not-valid plugin-id :topPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :topPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :topPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p1 value}}))))} @@ -202,10 +202,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :rightPadding value) + (u/not-valid plugin-id :rightPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :righPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :righPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p2 value}}))))} @@ -217,10 +217,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :bottomPadding value) + (u/not-valid plugin-id :bottomPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :bottomPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :bottomPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p3 value}}))))} @@ -232,10 +232,10 @@ (fn [_ value] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :leftPadding value) + (u/not-valid plugin-id :leftPadding value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :leftPadding "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :leftPadding "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-padding {:p4 value}}))))} @@ -245,14 +245,14 @@ (let [type (keyword type)] (cond (not (contains? ctl/grid-track-types type)) - (u/display-not-valid :addRow-type type) + (u/not-valid plugin-id :addRow-type type) (and (or (= :percent type) (= :flex type) (= :fixed type)) (not (sm/valid-safe-number? value))) - (u/display-not-valid :addRow-value value) + (u/not-valid plugin-id :addRow-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :addRow "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :addRow "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value}))))) @@ -262,17 +262,17 @@ (let [type (keyword type)] (cond (not (sm/valid-safe-int? index)) - (u/display-not-valid :addRowAtIndex-index index) + (u/not-valid plugin-id :addRowAtIndex-index index) (not (contains? ctl/grid-track-types type)) - (u/display-not-valid :addRowAtIndex-type type) + (u/not-valid plugin-id :addRowAtIndex-type type) (and (or (= :percent type) (= :flex type) (= :fixed type)) (not (sm/valid-safe-number? value))) - (u/display-not-valid :addRowAtIndex-value value) + (u/not-valid plugin-id :addRowAtIndex-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :addRowAtIndex "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :addRowAtIndex "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/add-layout-track #{id} :row {:type type :value value} index))))) @@ -282,14 +282,14 @@ (let [type (keyword type)] (cond (not (contains? ctl/grid-track-types type)) - (u/display-not-valid :addColumn-type type) + (u/not-valid plugin-id :addColumn-type type) (and (or (= :percent type) (= :flex type) (= :lex type)) (not (sm/valid-safe-number? value))) - (u/display-not-valid :addColumn-value value) + (u/not-valid plugin-id :addColumn-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :addColumn "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :addColumn "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/add-layout-track #{id} :column {:type type :value value}))))) @@ -298,17 +298,17 @@ (fn [index type value] (cond (not (sm/valid-safe-int? index)) - (u/display-not-valid :addColumnAtIndex-index index) + (u/not-valid plugin-id :addColumnAtIndex-index index) (not (contains? ctl/grid-track-types type)) - (u/display-not-valid :addColumnAtIndex-type type) + (u/not-valid plugin-id :addColumnAtIndex-type type) (and (or (= :percent type) (= :flex type) (= :fixed type)) (not (sm/valid-safe-number? value))) - (u/display-not-valid :addColumnAtIndex-value value) + (u/not-valid plugin-id :addColumnAtIndex-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :addColumnAtIndex "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :addColumnAtIndex "Plugin doesn't have 'content:write' permission") :else (let [type (keyword type)] @@ -318,10 +318,10 @@ (fn [index] (cond (not (sm/valid-safe-int? index)) - (u/display-not-valid :removeRow index) + (u/not-valid plugin-id :removeRow index) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :removeRow "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :removeRow "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/remove-layout-track #{id} :row index)))) @@ -330,10 +330,10 @@ (fn [index] (cond (not (sm/valid-safe-int? index)) - (u/display-not-valid :removeColumn index) + (u/not-valid plugin-id :removeColumn index) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :removeColumn "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :removeColumn "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/remove-layout-track #{id} :column index)))) @@ -343,17 +343,17 @@ (let [type (keyword type)] (cond (not (sm/valid-safe-int? index)) - (u/display-not-valid :setColumn-index index) + (u/not-valid plugin-id :setColumn-index index) (not (contains? ctl/grid-track-types type)) - (u/display-not-valid :setColumn-type type) + (u/not-valid plugin-id :setColumn-type type) (and (or (= :percent type) (= :flex type) (= :fixed type)) (not (sm/valid-safe-number? value))) - (u/display-not-valid :setColumn-value value) + (u/not-valid plugin-id :setColumn-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setColumn "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setColumn "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/change-layout-track #{id} :column index (d/without-nils {:type type :value value})))))) @@ -363,17 +363,17 @@ (let [type (keyword type)] (cond (not (sm/valid-safe-int? index)) - (u/display-not-valid :setRow-index index) + (u/not-valid plugin-id :setRow-index index) (not (contains? ctl/grid-track-types type)) - (u/display-not-valid :setRow-type type) + (u/not-valid plugin-id :setRow-type type) (and (or (= :percent type) (= :flex type) (= :fixed type)) (not (sm/valid-safe-number? value))) - (u/display-not-valid :setRow-value value) + (u/not-valid plugin-id :setRow-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setRow "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setRow "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/change-layout-track #{id} :row index (d/without-nils {:type type :value value})))))) @@ -382,7 +382,7 @@ (fn [] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :remove "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/remove-layout #{id})))) @@ -391,16 +391,16 @@ (fn [child row column] (cond (not (shape-proxy? child)) - (u/display-not-valid :appendChild-child child) + (u/not-valid plugin-id :appendChild-child child) (or (< row 0) (not (sm/valid-safe-int? row))) - (u/display-not-valid :appendChild-row row) + (u/not-valid plugin-id :appendChild-row row) (or (< column 0) (not (sm/valid-safe-int? column))) - (u/display-not-valid :appendChild-column column) + (u/not-valid plugin-id :appendChild-column column) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :appendChild "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission") :else (let [child-id (obj/get child "$id")] @@ -432,13 +432,13 @@ shape (u/proxy->shape self)] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :row-value value) + (u/not-valid plugin-id :row-value value) (nil? cell) - (u/display-not-valid :row-cell "cell not found") + (u/not-valid plugin-id :row-cell "cell not found") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :row "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :row "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row value})))))} @@ -452,13 +452,13 @@ cell (locate-cell self)] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :rowSpan-value value) + (u/not-valid plugin-id :rowSpan-value value) (nil? cell) - (u/display-not-valid :rowSpan-cell "cell not found") + (u/not-valid plugin-id :rowSpan-cell "cell not found") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :rowSpan "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :rowSpan "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:row-span value})))))} @@ -472,13 +472,13 @@ cell (locate-cell self)] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :column-value value) + (u/not-valid plugin-id :column-value value) (nil? cell) - (u/display-not-valid :column-cell "cell not found") + (u/not-valid plugin-id :column-cell "cell not found") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :column "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :column "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column value})))))} @@ -492,13 +492,13 @@ cell (locate-cell self)] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :columnSpan-value value) + (u/not-valid plugin-id :columnSpan-value value) (nil? cell) - (u/display-not-valid :columnSpan-cell "cell not found") + (u/not-valid plugin-id :columnSpan-cell "cell not found") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :columnSpan "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :columnSpan "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-grid-cell-position (:parent-id shape) (:id cell) {:column-span value})))))} @@ -512,13 +512,13 @@ cell (locate-cell self)] (cond (not (string? value)) - (u/display-not-valid :areaName-value value) + (u/not-valid plugin-id :areaName-value value) (nil? cell) - (u/display-not-valid :areaName-cell "cell not found") + (u/not-valid plugin-id :areaName-cell "cell not found") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :areaName "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :areaName "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:area-name value})))))} @@ -533,13 +533,13 @@ value (keyword value)] (cond (not (contains? ctl/grid-position-types value)) - (u/display-not-valid :position-value value) + (u/not-valid plugin-id :position-value value) (nil? cell) - (u/display-not-valid :position-cell "cell not found") + (u/not-valid plugin-id :position-cell "cell not found") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :position "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :position "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/change-cells-mode (:parent-id shape) #{(:id cell)} value)))))} @@ -554,13 +554,13 @@ cell (locate-cell self)] (cond (not (contains? ctl/grid-cell-align-self-types value)) - (u/display-not-valid :alignSelf-value value) + (u/not-valid plugin-id :alignSelf-value value) (nil? cell) - (u/display-not-valid :alignSelf-cell "cell not found") + (u/not-valid plugin-id :alignSelf-cell "cell not found") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :alignSelf "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :alignSelf "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:align-self value})))))} @@ -575,13 +575,13 @@ cell (locate-cell self)] (cond (not (contains? ctl/grid-cell-justify-self-types value)) - (u/display-not-valid :justifySelf-value value) + (u/not-valid plugin-id :justifySelf-value value) (nil? cell) - (u/display-not-valid :justifySelf-cell "cell not found") + (u/not-valid plugin-id :justifySelf-cell "cell not found") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :justifySelf "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :justifySelf "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-grid-cells (:parent-id shape) #{(:id cell)} {:justify-self value})))))}))) diff --git a/frontend/src/app/plugins/history.cljs b/frontend/src/app/plugins/history.cljs index 191dcc0d7e..25756c47da 100644 --- a/frontend/src/app/plugins/history.cljs +++ b/frontend/src/app/plugins/history.cljs @@ -24,7 +24,7 @@ (fn [] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :resize "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission") :else (let [id (js/Symbol)] @@ -35,10 +35,10 @@ (fn [block-id] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :resize "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission") (not block-id) - (u/display-not-valid :undoBlockFinish block-id) + (u/not-valid plugin-id :undoBlockFinish block-id) :else (st/emit! (dwu/commit-undo-transaction block-id)))))) diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs index c3dcd9ef28..1022a1a65c 100644 --- a/frontend/src/app/plugins/library.cljs +++ b/frontend/src/app/plugins/library.cljs @@ -60,10 +60,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :name value) + (u/not-valid plugin-id :name value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :name "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission") :else (let [color (u/proxy->library-color self) @@ -77,10 +77,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :path value) + (u/not-valid plugin-id :path value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :path "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission") :else (let [color (-> (u/proxy->library-color self) @@ -94,10 +94,10 @@ (fn [self value] (cond (or (not (string? value)) (not (clr/valid-hex-color? value))) - (u/display-not-valid :color value) + (u/not-valid plugin-id :color value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :color "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :color "Plugin doesn't have 'library:write' permission") :else (let [color (-> (u/proxy->library-color self) @@ -111,10 +111,10 @@ (fn [self value] (cond (or (not (number? value)) (< value 0) (> value 1)) - (u/display-not-valid :opacity value) + (u/not-valid plugin-id :opacity value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :opacity "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :opacity "Plugin doesn't have 'library:write' permission") :else (let [color (-> (u/proxy->library-color self) @@ -129,10 +129,10 @@ (let [value (parser/parse-gradient value)] (cond (not (sm/validate clr/schema:gradient value)) - (u/display-not-valid :gradient value) + (u/not-valid plugin-id :gradient value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :gradient "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :gradient "Plugin doesn't have 'library:write' permission") :else (let [color (-> (u/proxy->library-color self) @@ -147,10 +147,10 @@ (let [value (parser/parse-image-data value)] (cond (not (sm/validate clr/schema:image value)) - (u/display-not-valid :image value) + (u/not-valid plugin-id :image value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :image "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :image "Plugin doesn't have 'library:write' permission") :else (let [color (-> (u/proxy->library-color self) @@ -161,7 +161,7 @@ (fn [] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :remove "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission") :else (st/emit! (dwl/delete-color {:id id})))) @@ -170,7 +170,7 @@ (fn [] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :clone "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :clone "Plugin doesn't have 'library:write' permission") :else (let [color-id (uuid/next) @@ -207,7 +207,7 @@ (fn [key] (cond (not (string? key)) - (u/display-not-valid :getPluginData-key key) + (u/not-valid plugin-id :getPluginData-key key) :else (let [color (u/locate-library-color file-id id)] @@ -217,16 +217,16 @@ (fn [key value] (cond (not= file-id (:current-file-id @st/state)) - (u/display-not-valid :setPluginData-non-local-library file-id) + (u/not-valid plugin-id :setPluginData-non-local-library file-id) (not (string? key)) - (u/display-not-valid :setPluginData-key key) + (u/not-valid plugin-id :setPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setPluginData-value value) + (u/not-valid plugin-id :setPluginData-value value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission") :else (st/emit! (dp/set-plugin-data file-id :color id (keyword "plugin" (str plugin-id)) key value)))) @@ -240,10 +240,10 @@ (fn [namespace key] (cond (not (string? namespace)) - (u/display-not-valid :getSharedPluginData-namespace namespace) + (u/not-valid plugin-id :getSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :getSharedPluginData-key key) + (u/not-valid plugin-id :getSharedPluginData-key key) :else (let [color (u/locate-library-color file-id id)] @@ -253,19 +253,19 @@ (fn [namespace key value] (cond (not= file-id (:current-file-id @st/state)) - (u/display-not-valid :setSharedPluginData-non-local-library file-id) + (u/not-valid plugin-id :setSharedPluginData-non-local-library file-id) (not (string? namespace)) - (u/display-not-valid :setSharedPluginData-namespace namespace) + (u/not-valid plugin-id :setSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :setSharedPluginData-key key) + (u/not-valid plugin-id :setSharedPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setSharedPluginData-value value) + (u/not-valid plugin-id :setSharedPluginData-value value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission") :else (st/emit! (dp/set-plugin-data file-id :color id (keyword "shared" namespace) key value)))) @@ -274,7 +274,7 @@ (fn [namespace] (cond (not (string? namespace)) - (u/display-not-valid :getSharedPluginDataKeys-namespace namespace) + (u/not-valid plugin-id :getSharedPluginDataKeys-namespace namespace) :else (let [color (u/locate-library-color file-id id)] @@ -301,10 +301,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :name value) + (u/not-valid plugin-id :name value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :name "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission") :else (let [typo (u/proxy->library-typography self) @@ -318,10 +318,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :path value) + (u/not-valid plugin-id :path value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :path "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -335,10 +335,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :fontId value) + (u/not-valid plugin-id :fontId value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :fontId "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :fontId "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -352,10 +352,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :fontFamily value) + (u/not-valid plugin-id :fontFamily value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :fontFamily "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :fontFamily "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -369,10 +369,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :fontVariantId value) + (u/not-valid plugin-id :fontVariantId value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :fontVariantId "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -386,10 +386,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :fontSize value) + (u/not-valid plugin-id :fontSize value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :fontSize "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :fontSize "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -403,10 +403,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :fontWeight value) + (u/not-valid plugin-id :fontWeight value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :fontWeight "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :fontWeight "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -420,10 +420,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :fontStyle value) + (u/not-valid plugin-id :fontStyle value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :fontStyle "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :fontStyle "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -437,10 +437,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :lineHeight value) + (u/not-valid plugin-id :lineHeight value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :lineHeight "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :lineHeight "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -454,10 +454,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :letterSpacing value) + (u/not-valid plugin-id :letterSpacing value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :letterSpacing "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -471,10 +471,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :textTransform value) + (u/not-valid plugin-id :textTransform value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :textTransform "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :textTransform "Plugin doesn't have 'library:write' permission") :else (let [typo (-> (u/proxy->library-typography self) @@ -485,7 +485,7 @@ (fn [] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :remove "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission") :else (st/emit! (dwl/delete-typography {:id id})))) @@ -494,7 +494,7 @@ (fn [] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :clone "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :clone "Plugin doesn't have 'library:write' permission") :else (let [typo-id (uuid/next) @@ -507,10 +507,10 @@ (fn [shape] (cond (not (shape/shape-proxy? shape)) - (u/display-not-valid :applyToText shape) + (u/not-valid plugin-id :applyToText shape) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission") :else (let [shape-id (obj/get shape "$id") @@ -521,10 +521,10 @@ (fn [range] (cond (not (text/text-range-proxy? range)) - (u/display-not-valid :applyToText range) + (u/not-valid plugin-id :applyToText range) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :applyToText "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :applyToText "Plugin doesn't have 'content:write' permission") :else (let [shape-id (obj/get range "$id") @@ -542,7 +542,7 @@ (fn [key] (cond (not (string? key)) - (u/display-not-valid :typography-plugin-data-key key) + (u/not-valid plugin-id :typography-plugin-data-key key) :else (let [typography (u/locate-library-typography file-id id)] @@ -552,16 +552,16 @@ (fn [key value] (cond (not= file-id (:current-file-id @st/state)) - (u/display-not-valid :setPluginData-non-local-library file-id) + (u/not-valid plugin-id :setPluginData-non-local-library file-id) (not (string? key)) - (u/display-not-valid :setPluginData-key key) + (u/not-valid plugin-id :setPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setPluginData-value value) + (u/not-valid plugin-id :setPluginData-value value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission") :else (st/emit! (dp/set-plugin-data file-id :typography id (keyword "plugin" (str plugin-id)) key value)))) @@ -575,10 +575,10 @@ (fn [namespace key] (cond (not (string? namespace)) - (u/display-not-valid :getSharedPluginData-namespace namespace) + (u/not-valid plugin-id :getSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :getSharedPluginData-key key) + (u/not-valid plugin-id :getSharedPluginData-key key) :else (let [typography (u/locate-library-typography file-id id)] @@ -588,19 +588,19 @@ (fn [namespace key value] (cond (not= file-id (:current-file-id @st/state)) - (u/display-not-valid :setSharedPluginData-non-local-library file-id) + (u/not-valid plugin-id :setSharedPluginData-non-local-library file-id) (not (string? namespace)) - (u/display-not-valid :setSharedPluginData-namespace namespace) + (u/not-valid plugin-id :setSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :setSharedPluginData-key key) + (u/not-valid plugin-id :setSharedPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setSharedPluginData-value value) + (u/not-valid plugin-id :setSharedPluginData-value value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission") :else (st/emit! (dp/set-plugin-data file-id :typography id (keyword "shared" namespace) key value)))) @@ -609,7 +609,7 @@ (fn [namespace] (cond (not (string? namespace)) - (u/display-not-valid :getSharedPluginDataKeys-namespace namespace) + (u/not-valid plugin-id :getSharedPluginDataKeys-namespace namespace) :else (let [typography (u/locate-library-typography file-id id)] @@ -674,7 +674,7 @@ :removeProperty (fn [pos] (if (not (nat-int? pos)) - (u/display-not-valid :pos pos) + (u/not-valid plugin-id :pos pos) (st/emit! (ev/event {::ev/name "remove-property" ::ev/origin "plugin:remove-property"}) (dwv/remove-property id pos)))) @@ -683,10 +683,10 @@ (fn [pos name] (cond (not (nat-int? pos)) - (u/display-not-valid :pos pos) + (u/not-valid plugin-id :pos pos) (not (string? name)) - (u/display-not-valid :name name) + (u/not-valid plugin-id :name name) :else (st/emit! @@ -715,10 +715,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :name value) + (u/not-valid plugin-id :name value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :name "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :name "Plugin doesn't have 'library:write' permission") :else (let [component (u/proxy->library-component self) @@ -732,10 +732,10 @@ (fn [self value] (cond (not (string? value)) - (u/display-not-valid :path value) + (u/not-valid plugin-id :path value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :path "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :path "Plugin doesn't have 'library:write' permission") :else (let [component (u/proxy->library-component self) @@ -746,7 +746,7 @@ (fn [] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :remove "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :remove "Plugin doesn't have 'library:write' permission") :else (st/emit! (dwl/delete-component {:id id})))) @@ -755,7 +755,7 @@ (fn [] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :instance "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :instance "Plugin doesn't have 'content:write' permission") :else (let [id-ref (atom nil)] @@ -766,7 +766,7 @@ (fn [key] (cond (not (string? key)) - (u/display-not-valid :component-plugin-data-key key) + (u/not-valid plugin-id :component-plugin-data-key key) :else (let [component (u/locate-library-component file-id id)] @@ -776,16 +776,16 @@ (fn [key value] (cond (not= file-id (:current-file-id @st/state)) - (u/display-not-valid :setPluginData-non-local-library file-id) + (u/not-valid plugin-id :setPluginData-non-local-library file-id) (not (string? key)) - (u/display-not-valid :setPluginData-key key) + (u/not-valid plugin-id :setPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setPluginData-value value) + (u/not-valid plugin-id :setPluginData-value value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission") :else (st/emit! (dp/set-plugin-data file-id :component id (keyword "plugin" (str plugin-id)) key value)))) @@ -799,10 +799,10 @@ (fn [namespace key] (cond (not (string? namespace)) - (u/display-not-valid :component-plugin-data-namespace namespace) + (u/not-valid plugin-id :component-plugin-data-namespace namespace) (not (string? key)) - (u/display-not-valid :component-plugin-data-key key) + (u/not-valid plugin-id :component-plugin-data-key key) :else (let [component (u/locate-library-component file-id id)] @@ -812,19 +812,19 @@ (fn [namespace key value] (cond (not= file-id (:current-file-id @st/state)) - (u/display-not-valid :setSharedPluginData-non-local-library file-id) + (u/not-valid plugin-id :setSharedPluginData-non-local-library file-id) (not (string? namespace)) - (u/display-not-valid :setSharedPluginData-namespace namespace) + (u/not-valid plugin-id :setSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :setSharedPluginData-key key) + (u/not-valid plugin-id :setSharedPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setSharedPluginData-value value) + (u/not-valid plugin-id :setSharedPluginData-value value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission") :else (st/emit! (dp/set-plugin-data file-id :component id (keyword "shared" namespace) key value)))) @@ -833,7 +833,7 @@ (fn [namespace] (cond (not (string? namespace)) - (u/display-not-valid :component-plugin-data-namespace namespace) + (u/not-valid plugin-id :component-plugin-data-namespace namespace) :else (let [component (u/locate-library-component file-id id)] @@ -901,10 +901,10 @@ (fn [pos value] (cond (not (nat-int? pos)) - (u/display-not-valid :pos (str pos)) + (u/not-valid plugin-id :pos (str pos)) (not (string? value)) - (u/display-not-valid :name value) + (u/not-valid plugin-id :name value) :else (st/emit! @@ -970,7 +970,7 @@ (fn [] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :createColor "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :createColor "Plugin doesn't have 'library:write' permission") :else (let [color-id (uuid/next)] @@ -981,7 +981,7 @@ (fn [] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :createTypography "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :createTypography "Plugin doesn't have 'library:write' permission") :else (let [typography-id (uuid/next)] @@ -992,7 +992,7 @@ (fn [shapes] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :createComponent "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :createComponent "Plugin doesn't have 'library:write' permission") :else (let [id-ref (atom nil) @@ -1005,7 +1005,7 @@ (fn [key] (cond (not (string? key)) - (u/display-not-valid :file-plugin-data-key key) + (u/not-valid plugin-id :file-plugin-data-key key) :else (let [file (u/locate-file file-id)] @@ -1015,13 +1015,13 @@ (fn [key value] (cond (not (string? key)) - (u/display-not-valid :setPluginData-key key) + (u/not-valid plugin-id :setPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setPluginData-value value) + (u/not-valid plugin-id :setPluginData-value value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :setPluginData "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :setPluginData "Plugin doesn't have 'library:write' permission") :else (st/emit! (dp/set-plugin-data file-id :file (keyword "plugin" (str plugin-id)) key value)))) @@ -1035,10 +1035,10 @@ (fn [namespace key] (cond (not (string? namespace)) - (u/display-not-valid :file-plugin-data-namespace namespace) + (u/not-valid plugin-id :file-plugin-data-namespace namespace) (not (string? key)) - (u/display-not-valid :file-plugin-data-key key) + (u/not-valid plugin-id :file-plugin-data-key key) :else (let [file (u/locate-file file-id)] @@ -1048,16 +1048,16 @@ (fn [namespace key value] (cond (not (string? namespace)) - (u/display-not-valid :setSharedPluginData-namespace namespace) + (u/not-valid plugin-id :setSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :setSharedPluginData-key key) + (u/not-valid plugin-id :setSharedPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setSharedPluginData-value value) + (u/not-valid plugin-id :setSharedPluginData-value value) (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :setSharedPluginData "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'library:write' permission") :else (st/emit! (dp/set-plugin-data file-id :file (keyword "shared" namespace) key value)))) @@ -1066,7 +1066,7 @@ (fn [namespace] (cond (not (string? namespace)) - (u/display-not-valid :namespace namespace) + (u/not-valid plugin-id :namespace namespace) :else (let [file (u/locate-file file-id)] @@ -1110,14 +1110,14 @@ (fn [library-id] (cond (not (r/check-permission plugin-id "library:write")) - (u/display-not-valid :connectLibrary "Plugin doesn't have 'library:write' permission") + (u/not-valid plugin-id :connectLibrary "Plugin doesn't have 'library:write' permission") :else (js/Promise. (fn [resolve reject] (cond (not (string? library-id)) - (do (u/display-not-valid :connectLibrary library-id) + (do (u/not-valid plugin-id :connectLibrary library-id) (reject nil)) :else diff --git a/frontend/src/app/plugins/local_storage.cljs b/frontend/src/app/plugins/local_storage.cljs index cac6529be1..80d9b66b6b 100644 --- a/frontend/src/app/plugins/local_storage.cljs +++ b/frontend/src/app/plugins/local_storage.cljs @@ -30,10 +30,10 @@ (fn [key] (cond (not (r/check-permission plugin-id "allow:localstorage")) - (u/display-not-valid :getItem "Plugin doesn't have 'allow:localstorage' permission") + (u/not-valid plugin-id :getItem "Plugin doesn't have 'allow:localstorage' permission") (not (string? key)) - (u/display-not-valid :getItem "The key must be a string") + (u/not-valid plugin-id :getItem "The key must be a string") :else (.getItem ^js local-storage (prefix-key plugin-id key)))) @@ -42,10 +42,10 @@ (fn [key value] (cond (not (r/check-permission plugin-id "allow:localstorage")) - (u/display-not-valid :setItem "Plugin doesn't have 'allow:localstorage' permission") + (u/not-valid plugin-id :setItem "Plugin doesn't have 'allow:localstorage' permission") (not (string? key)) - (u/display-not-valid :setItem "The key must be a string") + (u/not-valid plugin-id :setItem "The key must be a string") :else (.setItem ^js local-storage (prefix-key plugin-id key) value))) @@ -54,10 +54,10 @@ (fn [key] (cond (not (r/check-permission plugin-id "allow:localstorage")) - (u/display-not-valid :removeItem "Plugin doesn't have 'allow:localstorage' permission") + (u/not-valid plugin-id :removeItem "Plugin doesn't have 'allow:localstorage' permission") (not (string? key)) - (u/display-not-valid :removeItem "The key must be a string") + (u/not-valid plugin-id :removeItem "The key must be a string") :else (.getItem ^js local-storage (prefix-key plugin-id key)))) diff --git a/frontend/src/app/plugins/page.cljs b/frontend/src/app/plugins/page.cljs index b0302a1939..7bc5726a17 100644 --- a/frontend/src/app/plugins/page.cljs +++ b/frontend/src/app/plugins/page.cljs @@ -59,7 +59,7 @@ (fn [_ value] (cond (or (not (string? value)) (empty? value)) - (u/display-not-valid :name value) + (u/not-valid plugin-id :name value) :else (st/emit! (dwi/update-flow page-id id #(assoc % :name value)))))} @@ -74,7 +74,7 @@ (fn [_ value] (cond (not (shape/shape-proxy? value)) - (u/display-not-valid :startingBoard value) + (u/not-valid plugin-id :startingBoard value) :else (st/emit! (dwi/update-flow page-id id #(assoc % :starting-frame (obj/get value "$id"))))))} @@ -103,10 +103,10 @@ (fn [_ value] (cond (not (string? value)) - (u/display-not-valid :name value) + (u/not-valid plugin-id :name value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :name "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission") :else (st/emit! (dw/rename-page id value))))} @@ -127,10 +127,10 @@ (fn [_ value] (cond (or (not (string? value)) (not (cc/valid-hex-color? value))) - (u/display-not-valid :background value) + (u/not-valid plugin-id :background value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :background "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :background "Plugin doesn't have 'content:write' permission") :else (st/emit! (dw/change-canvas-color id {:color value}))))} @@ -158,7 +158,7 @@ (fn [shape-id] (cond (not (string? shape-id)) - (u/display-not-valid :getShapeById shape-id) + (u/not-valid plugin-id :getShapeById shape-id) :else (let [shape-id (uuid/parse shape-id) @@ -195,7 +195,7 @@ (fn [key] (cond (not (string? key)) - (u/display-not-valid :page-plugin-data-key key) + (u/not-valid plugin-id :page-plugin-data-key key) :else (let [page (u/locate-page file-id id)] @@ -205,13 +205,13 @@ (fn [key value] (cond (not (string? key)) - (u/display-not-valid :setPluginData-key key) + (u/not-valid plugin-id :setPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setPluginData-value value) + (u/not-valid plugin-id :setPluginData-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission") :else (st/emit! (dp/set-plugin-data file-id :page id (keyword "plugin" (str plugin-id)) key value)))) @@ -225,10 +225,10 @@ (fn [namespace key] (cond (not (string? namespace)) - (u/display-not-valid :page-plugin-data-namespace namespace) + (u/not-valid plugin-id :page-plugin-data-namespace namespace) (not (string? key)) - (u/display-not-valid :page-plugin-data-key key) + (u/not-valid plugin-id :page-plugin-data-key key) :else (let [page (u/locate-page file-id id)] @@ -238,16 +238,16 @@ (fn [namespace key value] (cond (not (string? namespace)) - (u/display-not-valid :setSharedPluginData-namespace namespace) + (u/not-valid plugin-id :setSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :setSharedPluginData-key key) + (u/not-valid plugin-id :setSharedPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setSharedPluginData-value value) + (u/not-valid plugin-id :setSharedPluginData-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission") :else (st/emit! (dp/set-plugin-data file-id :page id (keyword "shared" namespace) key value)))) @@ -256,7 +256,7 @@ (fn [self namespace] (cond (not (string? namespace)) - (u/display-not-valid :page-plugin-data-namespace namespace) + (u/not-valid plugin-id :page-plugin-data-namespace namespace) :else (let [page (u/proxy->page self)] @@ -266,7 +266,7 @@ (fn [new-window] (cond (not (r/check-permission plugin-id "content:read")) - (u/display-not-valid :openPage "Plugin doesn't have 'content:read' permission") + (u/not-valid plugin-id :openPage "Plugin doesn't have 'content:read' permission") :else (let [new-window (if (boolean? new-window) new-window false)] @@ -276,10 +276,10 @@ (fn [name frame] (cond (or (not (string? name)) (empty? name)) - (u/display-not-valid :createFlow-name name) + (u/not-valid plugin-id :createFlow-name name) (not (shape/shape-proxy? frame)) - (u/display-not-valid :createFlow-frame frame) + (u/not-valid plugin-id :createFlow-frame frame) :else (let [flow-id (uuid/next)] @@ -290,7 +290,7 @@ (fn [flow] (cond (not (flow-proxy? flow)) - (u/display-not-valid :removeFlow-flow flow) + (u/not-valid plugin-id :removeFlow-flow flow) :else (st/emit! (dwi/remove-flow id (obj/get flow "$id"))))) @@ -300,18 +300,18 @@ (let [shape (u/proxy->shape board)] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :addRulerGuide "Value not a safe number") + (u/not-valid plugin-id :addRulerGuide "Value not a safe number") (not (contains? #{"vertical" "horizontal"} orientation)) - (u/display-not-valid :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'") + (u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'") (and (some? shape) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape)))) - (u/display-not-valid :addRulerGuide "The shape is not a board") + (u/not-valid plugin-id :addRulerGuide "The shape is not a board") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission") :else (let [ruler-id (uuid/next)] @@ -328,10 +328,10 @@ (fn [value] (cond (not (rg/ruler-guide-proxy? value)) - (u/display-not-valid :removeRulerGuide "Guide not provided") + (u/not-valid plugin-id :removeRulerGuide "Guide not provided") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :removeRulerGuide "Plugin doesn't have 'comment:write' permission") + (u/not-valid plugin-id :removeRulerGuide "Plugin doesn't have 'comment:write' permission") :else (let [guide (u/proxy->ruler-guide value)] @@ -343,17 +343,17 @@ position (parser/parse-point position)] (cond (or (not (string? content)) (empty? content)) - (u/display-not-valid :addCommentThread "Content not valid") + (u/not-valid plugin-id :addCommentThread "Content not valid") (or (not (sm/valid-safe-number? (:x position))) (not (sm/valid-safe-number? (:y position)))) - (u/display-not-valid :addCommentThread "Position not valid") + (u/not-valid plugin-id :addCommentThread "Position not valid") (and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape)))) - (u/display-not-valid :addCommentThread "Board not valid") + (u/not-valid plugin-id :addCommentThread "Board not valid") (not (r/check-permission plugin-id "comment:write")) - (u/display-not-valid :addCommentThread "Plugin doesn't have 'comment:write' permission") + (u/not-valid plugin-id :addCommentThread "Plugin doesn't have 'comment:write' permission") :else (let [position @@ -378,10 +378,10 @@ (fn [thread] (cond (not (pc/comment-thread-proxy? thread)) - (u/display-not-valid :removeCommentThread "Comment thread not valid") + (u/not-valid plugin-id :removeCommentThread "Comment thread not valid") (not (r/check-permission plugin-id "comment:write")) - (u/display-not-valid :removeCommentThread "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :removeCommentThread "Plugin doesn't have 'content:write' permission") :else (js/Promise. @@ -400,7 +400,7 @@ (cond (not (r/check-permission plugin-id "comment:read")) (do - (u/display-not-valid :findCommentThreads "Plugin doesn't have 'comment:read' permission") + (u/not-valid plugin-id :findCommentThreads "Plugin doesn't have 'comment:read' permission") (reject "Plugin doesn't have 'comment:read' permission")) :else diff --git a/frontend/src/app/plugins/public_utils.cljs b/frontend/src/app/plugins/public_utils.cljs index d3ed6a46e2..0bfe911ed5 100644 --- a/frontend/src/app/plugins/public_utils.cljs +++ b/frontend/src/app/plugins/public_utils.cljs @@ -14,10 +14,10 @@ [app.plugins.utils :as u])) (defn ^:export centerShapes - [shapes] + [plugin-id shapes] (cond (not (every? shape/shape-proxy? shapes)) - (u/display-not-valid :centerShapes shapes) + (u/not-valid plugin-id :centerShapes shapes) :else (let [shapes (->> shapes (map u/proxy->shape))] diff --git a/frontend/src/app/plugins/ruler_guides.cljs b/frontend/src/app/plugins/ruler_guides.cljs index d9c8e7c6c4..696df23001 100644 --- a/frontend/src/app/plugins/ruler_guides.cljs +++ b/frontend/src/app/plugins/ruler_guides.cljs @@ -44,13 +44,13 @@ (let [shape (u/locate-shape file-id page-id (obj/get value "$id"))] (cond (not (shape-proxy? value)) - (u/display-not-valid :board "The board is not a shape proxy") + (u/not-valid plugin-id :board "The board is not a shape proxy") (not (cfh/frame-shape? shape)) - (u/display-not-valid :board "The shape is not a board") + (u/not-valid plugin-id :board "The shape is not a board") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :board "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :board "Plugin doesn't have 'content:write' permission") :else (let [board-id (when value (obj/get value "$id")) @@ -78,10 +78,10 @@ (fn [self value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :position "Not valid position") + (u/not-valid plugin-id :position "Not valid position") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :position "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :position "Plugin doesn't have 'content:write' permission") :else (let [guide (u/proxy->ruler-guide self) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index 3692267fd4..e3dcd65ec2 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -46,7 +46,6 @@ [app.main.data.workspace.variants :as dwv] [app.main.repo :as rp] [app.main.store :as st] - [app.plugins.flags :refer [natural-child-ordering?]] [app.plugins.flex :as flex] [app.plugins.format :as format] [app.plugins.grid :as grid] @@ -91,7 +90,7 @@ (let [value (parser/parse-keyword value)] (cond (not (contains? ctsi/event-types value)) - (u/display-not-valid :trigger value) + (u/not-valid plugin-id :trigger value) :else (st/emit! (dwi/update-interaction @@ -107,7 +106,7 @@ (fn [_ value] (cond (or (not (number? value)) (not (pos? value))) - (u/display-not-valid :delay value) + (u/not-valid plugin-id :delay value) :else (st/emit! (dwi/update-interaction @@ -127,7 +126,7 @@ (d/patch-object params))] (cond (not (sm/validate ctsi/schema:interaction interaction)) - (u/display-not-valid :action interaction) + (u/not-valid plugin-id :action interaction) :else (st/emit! (dwi/update-interaction @@ -192,7 +191,8 @@ (assert (uuid? id)) (let [data (u/locate-shape file-id page-id id)] - (-> (obj/reify {:name "ShapeProxy" :on-error u/handle-error} + (-> (obj/reify {:name "ShapeProxy" + :on-error (u/handle-error plugin-id)} :$plugin {:enumerable false :get (fn [] plugin-id)} :$id {:enumerable false :get (fn [] id)} :$file {:enumerable false :get (fn [] file-id)} @@ -218,10 +218,10 @@ (not (str/blank? value)))] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :name "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :name "Plugin doesn't have 'content:write' permission") (not valid?) - (u/display-not-valid :name value) + (u/not-valid plugin-id :name value) :else (st/emit! (dw/rename-shape-or-variant file-id page-id id value)))))} @@ -233,10 +233,10 @@ (fn [self value] (cond (not (boolean? value)) - (u/display-not-valid :blocked value) + (u/not-valid plugin-id :blocked value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :blocked "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :blocked "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id")] @@ -249,10 +249,10 @@ (fn [self value] (cond (not (boolean? value)) - (u/display-not-valid :hidden value) + (u/not-valid plugin-id :hidden value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :hidden "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :hidden "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id")] @@ -265,10 +265,10 @@ (fn [self value] (cond (not (boolean? value)) - (u/display-not-valid :visible value) + (u/not-valid plugin-id :visible value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :visible "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :visible "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id")] @@ -281,10 +281,10 @@ (fn [self value] (cond (not (boolean? value)) - (u/display-not-valid :proportionLock value) + (u/not-valid plugin-id :proportionLock value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :proportionLock "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :proportionLock "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id")] @@ -299,10 +299,10 @@ value (keyword value)] (cond (not (contains? cts/horizontal-constraint-types value)) - (u/display-not-valid :constraintsHorizontal value) + (u/not-valid plugin-id :constraintsHorizontal value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :constraintsHorizontal "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :constraintsHorizontal "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-h value))))))} @@ -316,10 +316,10 @@ value (keyword value)] (cond (not (contains? cts/vertical-constraint-types value)) - (u/display-not-valid :constraintsVertical value) + (u/not-valid plugin-id :constraintsVertical value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :constraintsVertical "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :constraintsVertical "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :constraints-v value))))))} @@ -332,10 +332,10 @@ (let [id (obj/get self "$id")] (cond (or (not (sm/valid-safe-int? value)) (< value 0)) - (u/display-not-valid :borderRadius value) + (u/not-valid plugin-id :borderRadius value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :borderRadius "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :borderRadius "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-all-corners % value))))))} @@ -348,10 +348,10 @@ (let [id (obj/get self "$id")] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :borderRadiusTopLeft value) + (u/not-valid plugin-id :borderRadiusTopLeft value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :borderRadiusTopLeft "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :borderRadiusTopLeft "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r1 value))))))} @@ -364,10 +364,10 @@ (let [id (obj/get self "$id")] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :borderRadiusTopRight value) + (u/not-valid plugin-id :borderRadiusTopRight value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :borderRadiusTopRight "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :borderRadiusTopRight "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r2 value))))))} @@ -380,10 +380,10 @@ (let [id (obj/get self "$id")] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :borderRadiusBottomRight value) + (u/not-valid plugin-id :borderRadiusBottomRight value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :borderRadiusBottomRight "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :borderRadiusBottomRight "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r3 value))))))} @@ -396,10 +396,10 @@ (let [id (obj/get self "$id")] (cond (not (sm/valid-safe-int? value)) - (u/display-not-valid :borderRadiusBottomLeft value) + (u/not-valid plugin-id :borderRadiusBottomLeft value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :borderRadiusBottomLeft "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :borderRadiusBottomLeft "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(ctsr/set-radius-to-single-corner % :r4 value))))))} @@ -412,10 +412,10 @@ (let [id (obj/get self "$id")] (cond (or (not (sm/valid-safe-number? value)) (< value 0) (> value 1)) - (u/display-not-valid :opacity value) + (u/not-valid plugin-id :opacity value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :opacity "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :opacity "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :opacity value))))))} @@ -429,10 +429,10 @@ value (keyword value)] (cond (not (contains? cts/blend-modes value)) - (u/display-not-valid :blendMode value) + (u/not-valid plugin-id :blendMode value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :blendMode "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :blendMode "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :blend-mode value))))))} @@ -446,10 +446,10 @@ value (mapv #(shadow-defaults (parser/parse-shadow %)) value)] (cond (not (sm/validate [:vector ctss/schema:shadow] value)) - (u/display-not-valid :shadows value) + (u/not-valid plugin-id :shadows value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :shadows "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :shadows "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :shadow value))))))} @@ -465,10 +465,10 @@ value (blur-defaults (parser/parse-blur value))] (cond (not (sm/validate ctsb/schema:blur value)) - (u/display-not-valid :blur value) + (u/not-valid plugin-id :blur value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :blur "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :blur "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :blur value)))))))} @@ -482,10 +482,10 @@ value (parser/parse-exports value)] (cond (not (sm/validate [:vector ctse/schema:export] value)) - (u/display-not-valid :exports value) + (u/not-valid plugin-id :exports value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :exports "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :exports "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :exports value))))))} @@ -499,10 +499,10 @@ (let [id (obj/get self "$id")] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :x value) + (u/not-valid plugin-id :x value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :x "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :x "Plugin doesn't have 'content:write' permission") :else (st/emit! (dw/update-position id @@ -517,10 +517,10 @@ (let [id (obj/get self "$id")] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :y value) + (u/not-valid plugin-id :y value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :y "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :y "Plugin doesn't have 'content:write' permission") :else (st/emit! (dw/update-position id @@ -562,10 +562,10 @@ (fn [self value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :parentX value) + (u/not-valid plugin-id :parentX value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :parentX "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :parentX "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id") @@ -589,10 +589,10 @@ (fn [self value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :parentY value) + (u/not-valid plugin-id :parentY value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :parentY "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :parentY "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id") @@ -616,10 +616,10 @@ (fn [self value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :frameX value) + (u/not-valid plugin-id :frameX value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :frameX "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :frameX "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id") @@ -643,10 +643,10 @@ (fn [self value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :frameY value) + (u/not-valid plugin-id :frameY value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :frameY "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :frameY "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id") @@ -680,10 +680,10 @@ (fn [self value] (cond (not (number? value)) - (u/display-not-valid :rotation value) + (u/not-valid plugin-id :rotation value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :rotation "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :rotation "Plugin doesn't have 'content:write' permission") :else (let [shape (u/proxy->shape self)] @@ -696,10 +696,10 @@ (fn [self value] (cond (not (boolean? value)) - (u/display-not-valid :flipX value) + (u/not-valid plugin-id :flipX value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :flipX "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :flipX "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id")] @@ -712,10 +712,10 @@ (fn [self value] (cond (not (boolean? value)) - (u/display-not-valid :flipY value) + (u/not-valid plugin-id :flipY value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :flipY "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :flipY "Plugin doesn't have 'content:write' permission") :else (let [id (obj/get self "$id")] @@ -734,13 +734,13 @@ value (parser/parse-fills value)] (cond (not (sm/validate [:vector types.fills/schema:fill] value)) - (u/display-not-valid :fills value) + (u/not-valid plugin-id :fills value) (cfh/text-shape? shape) (st/emit! (dwt/update-attrs id {:fills value})) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fills "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fills "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :fills value))))))} @@ -754,10 +754,10 @@ value (parser/parse-strokes value)] (cond (not (sm/validate [:vector cts/schema:stroke] value)) - (u/display-not-valid :strokes value) + (u/not-valid plugin-id :strokes value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :strokes "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :strokes "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :strokes value))))))} @@ -802,13 +802,13 @@ (fn [width height] (cond (or (not (sm/valid-safe-number? width)) (<= width 0)) - (u/display-not-valid :resize width) + (u/not-valid plugin-id :resize width) (or (not (sm/valid-safe-number? height)) (<= height 0)) - (u/display-not-valid :resize height) + (u/not-valid plugin-id :resize height) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :resize "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :resize "Plugin doesn't have 'content:write' permission") :else (st/emit! (dw/update-dimensions [id] :width width) @@ -819,13 +819,13 @@ (let [center (when center {:x (obj/get center "x") :y (obj/get center "y")})] (cond (not (number? angle)) - (u/display-not-valid :rotate-angle angle) + (u/not-valid plugin-id :rotate-angle angle) (and (some? center) (or (not (number? (:x center))) (not (number? (:y center))))) - (u/display-not-valid :rotate-center center) + (u/not-valid plugin-id :rotate-center center) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :rotate "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :rotate "Plugin doesn't have 'content:write' permission") :else (st/emit! (dw/increase-rotation [id] angle {:center center :delta? true}))))) @@ -835,7 +835,7 @@ (let [ret-v (atom nil)] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :clone "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :clone "Plugin doesn't have 'content:write' permission") :else (do (st/emit! (dws/duplicate-shapes #{id} :change-selection? false :return-ref ret-v)) @@ -845,7 +845,7 @@ (fn [] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :remove "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :remove "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/delete-shapes #{id})))) @@ -855,7 +855,7 @@ (fn [key] (cond (not (string? key)) - (u/display-not-valid :getPluginData key) + (u/not-valid plugin-id :getPluginData key) :else (let [shape (u/locate-shape file-id page-id id)] @@ -865,13 +865,13 @@ (fn [key value] (cond (not (string? key)) - (u/display-not-valid :setPluginData-key key) + (u/not-valid plugin-id :setPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setPluginData-value value) + (u/not-valid plugin-id :setPluginData-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setPluginData "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setPluginData "Plugin doesn't have 'content:write' permission") :else (st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "plugin" (str plugin-id)) key value)))) @@ -885,10 +885,10 @@ (fn [namespace key] (cond (not (string? namespace)) - (u/display-not-valid :getSharedPluginData-namespace namespace) + (u/not-valid plugin-id :getSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :getSharedPluginData-key key) + (u/not-valid plugin-id :getSharedPluginData-key key) :else (let [shape (u/locate-shape file-id page-id id)] @@ -898,16 +898,16 @@ (fn [namespace key value] (cond (not (string? namespace)) - (u/display-not-valid :setSharedPluginData-namespace namespace) + (u/not-valid plugin-id :setSharedPluginData-namespace namespace) (not (string? key)) - (u/display-not-valid :setSharedPluginData-key key) + (u/not-valid plugin-id :setSharedPluginData-key key) (and (some? value) (not (string? value))) - (u/display-not-valid :setSharedPluginData-value value) + (u/not-valid plugin-id :setSharedPluginData-value value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setSharedPluginData "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setSharedPluginData "Plugin doesn't have 'content:write' permission") :else (st/emit! (dp/set-plugin-data file-id :shape id page-id (keyword "shared" namespace) key value)))) @@ -916,7 +916,7 @@ (fn [namespace] (cond (not (string? namespace)) - (u/display-not-valid :getSharedPluginDataKeys namespace) + (u/not-valid plugin-id :getSharedPluginDataKeys namespace) :else (let [shape (u/locate-shape file-id page-id id)] @@ -931,12 +931,12 @@ (not (cfh/group-shape? shape)) (not (cfh/svg-raw-shape? shape)) (not (cfh/bool-shape? shape))) - (u/display-not-valid :getChildren (:type shape)) + (u/not-valid plugin-id :getChildren (:type shape)) :else (let [is-reversed? (ctl/flex-layout? shape) reverse-fn - (if (and (natural-child-ordering? plugin-id) is-reversed?) + (if (and (u/natural-child-ordering? plugin-id) is-reversed?) reverse identity)] (->> (u/locate-shape file-id page-id id) (:shapes) @@ -951,19 +951,19 @@ (not (cfh/group-shape? shape)) (not (cfh/svg-raw-shape? shape)) (not (cfh/bool-shape? shape))) - (u/display-not-valid :appendChild (:type shape)) + (u/not-valid plugin-id :appendChild (:type shape)) (not (shape-proxy? child)) - (u/display-not-valid :appendChild-child child) + (u/not-valid plugin-id :appendChild-child child) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :appendChild "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :appendChild "Plugin doesn't have 'content:write' permission") :else (let [child-id (obj/get child "$id") is-reversed? (ctl/flex-layout? shape) index - (if (or (not (natural-child-ordering? plugin-id)) is-reversed?) + (if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?) 0 (count (:shapes shape)))] (st/emit! (dwsh/relocate-shapes #{child-id} id index)))))) @@ -976,19 +976,19 @@ (not (cfh/group-shape? shape)) (not (cfh/svg-raw-shape? shape)) (not (cfh/bool-shape? shape))) - (u/display-not-valid :insertChild (:type shape)) + (u/not-valid plugin-id :insertChild (:type shape)) (not (shape-proxy? child)) - (u/display-not-valid :insertChild-child child) + (u/not-valid plugin-id :insertChild-child child) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :insertChild "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :insertChild "Plugin doesn't have 'content:write' permission") :else (let [child-id (obj/get child "$id") is-reversed? (ctl/flex-layout? shape) index - (if (or (not (natural-child-ordering? plugin-id)) is-reversed?) + (if (or (not (u/natural-child-ordering? plugin-id)) is-reversed?) (- (count (:shapes shape)) index) index)] (st/emit! (dwsh/relocate-shapes #{child-id} id index)))))) @@ -999,10 +999,10 @@ (let [shape (u/locate-shape file-id page-id id)] (cond (not (cfh/frame-shape? shape)) - (u/display-not-valid :addFlexLayout (:type shape)) + (u/not-valid plugin-id :addFlexLayout (:type shape)) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :addFlexLayout "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :addFlexLayout "Plugin doesn't have 'content:write' permission") :else (do (st/emit! (dwsl/create-layout-from-id id :flex :from-frame? true :calculate-params? false)) @@ -1013,10 +1013,10 @@ (let [shape (u/locate-shape file-id page-id id)] (cond (not (cfh/frame-shape? shape)) - (u/display-not-valid :addGridLayout (:type shape)) + (u/not-valid plugin-id :addGridLayout (:type shape)) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :addGridLayout "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :addGridLayout "Plugin doesn't have 'content:write' permission") :else (do (st/emit! (dwsl/create-layout-from-id id :grid :from-frame? true :calculate-params? false)) @@ -1028,10 +1028,10 @@ (let [shape (u/locate-shape file-id page-id id)] (cond (not (cfh/group-shape? shape)) - (u/display-not-valid :makeMask (:type shape)) + (u/not-valid plugin-id :makeMask (:type shape)) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :makeMask "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :makeMask "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwg/mask-group #{id}))))) @@ -1041,10 +1041,10 @@ (let [shape (u/locate-shape file-id page-id id)] (cond (not (cfh/mask-shape? shape)) - (u/display-not-valid :removeMask (:type shape)) + (u/not-valid plugin-id :removeMask (:type shape)) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :removeMask "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :removeMask "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwg/unmask-group #{id}))))) @@ -1055,7 +1055,7 @@ (let [shape (u/locate-shape file-id page-id id)] (cond (and (not (cfh/path-shape? shape)) (not (cfh/bool-shape? shape))) - (u/display-not-valid :toD (:type shape)) + (u/not-valid plugin-id :toD (:type shape)) :else (.toString (:content shape))))) @@ -1066,13 +1066,13 @@ (let [shape (u/locate-shape file-id page-id id)] (cond (not (cfh/text-shape? shape)) - (u/display-not-valid :getRange-shape "shape is not text") + (u/not-valid plugin-id :getRange-shape "shape is not text") (or (not (sm/valid-safe-int? start)) (< start 0) (> start end)) - (u/display-not-valid :getRange-start start) + (u/not-valid plugin-id :getRange-start start) (not (sm/valid-safe-int? end)) - (u/display-not-valid :getRange-end end) + (u/not-valid plugin-id :getRange-end end) :else (text/text-range-proxy plugin-id file-id page-id id start end)))) @@ -1082,13 +1082,13 @@ (let [shape (u/locate-shape file-id page-id id)] (cond (not (lib-typography-proxy? typography)) - (u/display-not-valid :applyTypography-typography typography) + (u/not-valid plugin-id :applyTypography-typography typography) (not (cfh/text-shape? shape)) - (u/display-not-valid :applyTypography-shape (:type shape)) + (u/not-valid plugin-id :applyTypography-shape (:type shape)) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :applyTypography "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :applyTypography "Plugin doesn't have 'content:write' permission") :else (let [typography (u/proxy->library-typography typography)] @@ -1099,10 +1099,10 @@ (fn [index] (cond (not (sm/valid-safe-int? index)) - (u/display-not-valid :setParentIndex index) + (u/not-valid plugin-id :setParentIndex index) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :setParentIndex "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :setParentIndex "Plugin doesn't have 'content:write' permission") :else (st/emit! (dw/set-shape-index file-id page-id id index)))) @@ -1197,7 +1197,7 @@ (let [value (parser/parse-export value)] (cond (not (sm/validate ctse/schema:export value)) - (u/display-not-valid :export value) + (u/not-valid plugin-id :export value) :else (let [shape (u/locate-shape file-id page-id id) @@ -1233,7 +1233,7 @@ (d/patch-object (parser/parse-interaction trigger action delay)))] (cond (not (sm/validate ctsi/schema:interaction interaction)) - (u/display-not-valid :addInteraction interaction) + (u/not-valid plugin-id :addInteraction interaction) :else (let [index (-> (u/locate-shape file-id page-id id) (:interactions []) count)] @@ -1244,7 +1244,7 @@ (fn [interaction] (cond (not (interaction-proxy? interaction)) - (u/display-not-valid :removeInteraction interaction) + (u/not-valid plugin-id :removeInteraction interaction) :else (st/emit! (dwi/remove-interaction {:id id} (obj/get interaction "$index"))))) @@ -1255,16 +1255,16 @@ (let [shape (u/locate-shape file-id page-id id)] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :addRulerGuide "Value not a safe number") + (u/not-valid plugin-id :addRulerGuide "Value not a safe number") (not (contains? #{"vertical" "horizontal"} orientation)) - (u/display-not-valid :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'") + (u/not-valid plugin-id :addRulerGuide "Orientation should be either 'vertical' or 'horizontal'") (not (cfh/frame-shape? shape)) - (u/display-not-valid :addRulerGuide "The shape is not a board") + (u/not-valid plugin-id :addRulerGuide "The shape is not a board") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :addRulerGuide "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :addRulerGuide "Plugin doesn't have 'content:write' permission") :else (let [id (uuid/next) @@ -1285,10 +1285,10 @@ (fn [_ value] (cond (not (rg/ruler-guide-proxy? value)) - (u/display-not-valid :removeRulerGuide "Guide not provided") + (u/not-valid plugin-id :removeRulerGuide "Guide not provided") (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :removeRulerGuide "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :removeRulerGuide "Plugin doesn't have 'content:write' permission") :else (let [guide (u/proxy->ruler-guide value)] @@ -1317,7 +1317,7 @@ (let [token (u/locate-token file-id (obj/get token "$set-id") (obj/get token "$id")) kw-attrs (into #{} (map token-attr-plugin->token-attr attrs))] (if (some #(not (token-attr? %)) kw-attrs) - (u/display-not-valid :applyToken attrs) + (u/not-valid plugin-id :applyToken attrs) (st/emit! (dwta/toggle-token {:token token :attrs kw-attrs @@ -1339,10 +1339,10 @@ (fn [pos value] (cond (not (nat-int? pos)) - (u/display-not-valid :pos pos) + (u/not-valid plugin-id :pos pos) (not (string? value)) - (u/display-not-valid :value value) + (u/not-valid plugin-id :value value) :else (let [shape (u/locate-shape file-id page-id id) @@ -1354,7 +1354,7 @@ (fn [ids] (cond (or (not (seq ids)) (not (every? uuid/parse* ids))) - (u/display-not-valid :ids ids) + (u/not-valid plugin-id :ids ids) :else (let [shape (u/locate-shape file-id page-id id) @@ -1382,21 +1382,21 @@ (fn [^js self children] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :children "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :children "Plugin doesn't have 'content:write' permission") (not (every? shape-proxy? children)) - (u/display-not-valid :children "Every children needs to be shape proxies") + (u/not-valid plugin-id :children "Every children needs to be shape proxies") :else (let [shape (u/proxy->shape self) file-id (obj/get self "$file") page-id (obj/get self "$page") - reverse-fn (if (natural-child-ordering? plugin-id) reverse identity) + reverse-fn (if (u/natural-child-ordering? plugin-id) reverse identity) ids (->> children reverse-fn (map #(obj/get % "$id")))] (cond (not= (set ids) (set (:shapes shape))) - (u/display-not-valid :children "Not all children are present in the input") + (u/not-valid plugin-id :children "Not all children are present in the input") :else (st/emit! (dw/reorder-children file-id page-id (:id shape) ids))))))})) @@ -1412,10 +1412,10 @@ (fn [_ value] (cond (not (boolean? value)) - (u/display-not-valid :clipContent value) + (u/not-valid plugin-id :clipContent value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :clipContent "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :clipContent "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :show-content (not value))))))} @@ -1428,10 +1428,10 @@ (fn [_ value] (cond (not (boolean? value)) - (u/display-not-valid :showInViewMode value) + (u/not-valid plugin-id :showInViewMode value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :showInViewMode "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :showInViewMode "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :hide-in-viewer (not value))))))} @@ -1463,10 +1463,10 @@ value (parser/parse-frame-guides value)] (cond (not (sm/validate [:vector ::ctg/grid] value)) - (u/display-not-valid :guides value) + (u/not-valid plugin-id :guides value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :guides "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :guides "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :grids value))))))} @@ -1488,10 +1488,10 @@ value (keyword value)] (cond (not (contains? #{:fix :auto} value)) - (u/display-not-valid :horizontalSizing value) + (u/not-valid plugin-id :horizontalSizing value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :horizontalSizing "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :horizontalSizing "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-item-h-sizing value})))))} @@ -1504,10 +1504,10 @@ value (keyword value)] (cond (not (contains? #{:fix :auto} value)) - (u/display-not-valid :verticalSizing value) + (u/not-valid plugin-id :verticalSizing value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :verticalSizing "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsl/update-layout #{id} {:layout-item-v-sizing value})))))} @@ -1531,10 +1531,10 @@ (let [segments (parser/parse-commands value)] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :content "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :content "Plugin doesn't have 'content:write' permission") (not (sm/validate path/schema:segments segments)) - (u/display-not-valid :content segments) + (u/not-valid plugin-id :content segments) :else (let [selrect (path/calc-selrect segments) @@ -1557,13 +1557,13 @@ value)] (cond (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :content "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :content "Plugin doesn't have 'content:write' permission") (not (cfh/path-shape? data)) - (u/display-not-valid :content-type type) + (u/not-valid plugin-id :content-type type) (not (sm/validate path/schema:segments segments)) - (u/display-not-valid :content segments) + (u/not-valid plugin-id :content segments) :else (let [selrect (path/calc-selrect segments) diff --git a/frontend/src/app/plugins/text.cljs b/frontend/src/app/plugins/text.cljs index 36828324b2..08dc64db12 100644 --- a/frontend/src/app/plugins/text.cljs +++ b/frontend/src/app/plugins/text.cljs @@ -119,10 +119,10 @@ variant (fonts/get-default-variant font)] (cond (not font) - (u/display-not-valid :fontId value) + (u/not-valid plugin-id :fontId value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontId "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontId "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end (font-data font variant))))))} @@ -141,10 +141,10 @@ variant (fonts/get-default-variant font)] (cond (not (string? value)) - (u/display-not-valid :fontFamily value) + (u/not-valid plugin-id :fontFamily value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontFamily "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontFamily "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end (font-data font variant))))))} @@ -162,10 +162,10 @@ variant (fonts/get-variant font value)] (cond (not (string? value)) - (u/display-not-valid :fontVariantId value) + (u/not-valid plugin-id :fontVariantId value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontVariantId "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end (variant-data variant))))))} @@ -182,10 +182,10 @@ (let [value (str/trim (dm/str value))] (cond (or (empty? value) (not (re-matches font-size-re value))) - (u/display-not-valid :fontSize value) + (u/not-valid plugin-id :fontSize value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontSize "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontSize "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end {:font-size value})))))} @@ -209,10 +209,10 @@ (fonts/find-variant font {:weight weight}))] (cond (nil? variant) - (u/display-not-valid :fontWeight (dm/str "Font weight '" value "' not supported for the current font")) + (u/not-valid plugin-id :fontWeight (dm/str "Font weight '" value "' not supported for the current font")) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontWeight "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontWeight "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end (variant-data variant))))))} @@ -235,10 +235,10 @@ (fonts/find-variant font {:style style}))] (cond (nil? variant) - (u/display-not-valid :fontStyle (dm/str "Font style '" value "' not supported for the current font")) + (u/not-valid plugin-id :fontStyle (dm/str "Font style '" value "' not supported for the current font")) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontStyle "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontStyle "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end (variant-data variant))))))} @@ -255,10 +255,10 @@ (let [value (str/trim (dm/str value))] (cond (or (empty? value) (not (re-matches line-height-re value))) - (u/display-not-valid :lineHeight value) + (u/not-valid plugin-id :lineHeight value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :lineHeight "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :lineHeight "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end {:line-height value})))))} @@ -275,10 +275,10 @@ (let [value (str/trim (dm/str value))] (cond (or (empty? value) (re-matches letter-spacing-re value)) - (u/display-not-valid :letterSpacing value) + (u/not-valid plugin-id :letterSpacing value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :letterSpacing "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end {:letter-spacing value})))))} @@ -294,10 +294,10 @@ (fn [_ value] (cond (and (string? value) (not (re-matches text-transform-re value))) - (u/display-not-valid :textTransform value) + (u/not-valid plugin-id :textTransform value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :textTransform "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :textTransform "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end {:text-transform value}))))} @@ -313,10 +313,10 @@ (fn [_ value] (cond (and (string? value) (re-matches text-decoration-re value)) - (u/display-not-valid :textDecoration value) + (u/not-valid plugin-id :textDecoration value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :textDecoration "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :textDecoration "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end {:text-decoration value}))))} @@ -332,10 +332,10 @@ (fn [_ value] (cond (and (string? value) (re-matches text-direction-re value)) - (u/display-not-valid :direction value) + (u/not-valid plugin-id :direction value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :direction "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :direction "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end {:direction value}))))} @@ -351,10 +351,10 @@ (fn [_ value] (cond (and (string? value) (re-matches text-align-re value)) - (u/display-not-valid :align value) + (u/not-valid plugin-id :align value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :align "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :align "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end {:text-align value}))))} @@ -371,10 +371,10 @@ (let [value (parser/parse-fills value)] (cond (not (sm/validate [:vector ::cts/fill] value)) - (u/display-not-valid :fills value) + (u/not-valid plugin-id :fills value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fills "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fills "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-text-range id start end {:fills value})))))} @@ -401,10 +401,10 @@ ;; editor as well (cond (or (not (string? value)) (empty? value)) - (u/display-not-valid :characters value) + (u/not-valid plugin-id :characters value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :characters "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :characters "Plugin doesn't have 'content:write' permission") (contains? (:workspace-editor-state @st/state) id) (let [shape (u/proxy->shape self) @@ -428,10 +428,10 @@ value (keyword value)] (cond (not (contains? #{:auto-width :auto-height :fixed} value)) - (u/display-not-valid :growType value) + (u/not-valid plugin-id :growType value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :growType "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :growType "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwsh/update-shapes [id] #(assoc % :grow-type value))))))} @@ -445,10 +445,10 @@ variant (fonts/get-default-variant font)] (cond (not font) - (u/display-not-valid :fontId value) + (u/not-valid plugin-id :fontId value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontId "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontId "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id (font-data font variant))))))} @@ -462,10 +462,10 @@ variant (fonts/get-default-variant font)] (cond (not font) - (u/display-not-valid :fontFamily value) + (u/not-valid plugin-id :fontFamily value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontFamily "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontFamily "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id (font-data font variant))))))} @@ -479,10 +479,10 @@ variant (fonts/get-variant font value)] (cond (not variant) - (u/display-not-valid :fontVariantId value) + (u/not-valid plugin-id :fontVariantId value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontVariantId "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontVariantId "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id (variant-data variant))))))} @@ -495,10 +495,10 @@ value (str/trim (dm/str value))] (cond (or (empty? value) (not (re-matches font-size-re value))) - (u/display-not-valid :fontSize value) + (u/not-valid plugin-id :fontSize value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontSize "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontSize "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id {:font-size value})))))} @@ -517,10 +517,10 @@ (fonts/find-variant font {:weight weight}))] (cond (nil? variant) - (u/display-not-valid :fontWeight (dm/str "Font weight '" value "' not supported for the current font")) + (u/not-valid plugin-id :fontWeight (dm/str "Font weight '" value "' not supported for the current font")) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontWeight "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontWeight "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id (variant-data variant))))))} @@ -539,10 +539,10 @@ (fonts/find-variant font {:style style}))] (cond (nil? variant) - (u/display-not-valid :fontStyle (dm/str "Font style '" value "' not supported for the current font")) + (u/not-valid plugin-id :fontStyle (dm/str "Font style '" value "' not supported for the current font")) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :fontStyle "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :fontStyle "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id (variant-data variant))))))} @@ -555,10 +555,10 @@ value (str/trim (dm/str value))] (cond (or (empty? value) (not (re-matches line-height-re value))) - (u/display-not-valid :lineHeight value) + (u/not-valid plugin-id :lineHeight value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :lineHeight "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :lineHeight "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id {:line-height value})))))} @@ -571,10 +571,10 @@ value (str/trim (dm/str value))] (cond (or (not (string? value)) (not (re-matches letter-spacing-re value))) - (u/display-not-valid :letterSpacing value) + (u/not-valid plugin-id :letterSpacing value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :letterSpacing "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :letterSpacing "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id {:letter-spacing value})))))} @@ -586,10 +586,10 @@ (let [id (obj/get self "$id")] (cond (or (not (string? value)) (not (re-matches text-transform-re value))) - (u/display-not-valid :textTransform value) + (u/not-valid plugin-id :textTransform value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :textTransform "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :textTransform "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id {:text-transform value})))))} @@ -601,10 +601,10 @@ (let [id (obj/get self "$id")] (cond (or (not (string? value)) (not (re-matches text-decoration-re value))) - (u/display-not-valid :textDecoration value) + (u/not-valid plugin-id :textDecoration value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :textDecoration "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :textDecoration "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id {:text-decoration value})))))} @@ -616,10 +616,10 @@ (let [id (obj/get self "$id")] (cond (or (not (string? value)) (not (re-matches text-direction-re value))) - (u/display-not-valid :textDirection value) + (u/not-valid plugin-id :textDirection value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :textDirection "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :textDirection "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id {:text-direction value})))))} @@ -631,10 +631,10 @@ (let [id (obj/get self "$id")] (cond (or (not (string? value)) (not (re-matches text-align-re value))) - (u/display-not-valid :align value) + (u/not-valid plugin-id :align value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :align "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :align "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id {:text-align value})))))} @@ -646,10 +646,10 @@ (let [id (obj/get self "$id")] (cond (or (not (string? value)) (not (re-matches vertical-align-re value))) - (u/display-not-valid :verticalAlign value) + (u/not-valid plugin-id :verticalAlign value) (not (r/check-permission plugin-id "content:write")) - (u/display-not-valid :verticalAlign "Plugin doesn't have 'content:write' permission") + (u/not-valid plugin-id :verticalAlign "Plugin doesn't have 'content:write' permission") :else (st/emit! (dwt/update-attrs id {:vertical-align value})))))} diff --git a/frontend/src/app/plugins/tokens.cljs b/frontend/src/app/plugins/tokens.cljs index 6c3a4f5eff..ec1ebf6b71 100644 --- a/frontend/src/app/plugins/tokens.cljs +++ b/frontend/src/app/plugins/tokens.cljs @@ -63,11 +63,11 @@ (cto/token-attr? (token-attr-plugin->token-attr attr))) (defn- apply-token-to-shapes - [file-id set-id id shape-ids attrs] + [plugin-id file-id set-id id shape-ids attrs] (let [token (u/locate-token file-id set-id id)] (if (some #(not (token-attr? %)) attrs) - (u/display-not-valid :applyToSelected attrs) + (u/not-valid plugin-id :applyToSelected attrs) (st/emit! (dwta/toggle-token {:token token :attrs (into #{} (map token-attr-plugin->token-attr) attrs) @@ -92,7 +92,7 @@ (defn token-proxy [plugin-id file-id set-id id] (obj/reify {:name "TokenProxy" - :on-error u/handle-error} + :on-error (u/handle-error plugin-id)} :$plugin {:enumerable false :get (constantly plugin-id)} :$file-id {:enumerable false :get (constantly file-id)} :$set-id {:enumerable false :get (constantly set-id)} @@ -188,14 +188,14 @@ [:vector [:fn shape-proxy?]] [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [shapes attrs] - (apply-token-to-shapes file-id set-id id (map #(obj/get % "$id") shapes) attrs))} + (apply-token-to-shapes plugin-id file-id set-id id (map #(obj/get % "$id") shapes) attrs))} :applyToSelected {:enumerable false :schema [:tuple [:maybe [:set [:and ::sm/keyword [:fn token-attr?]]]]] :fn (fn [attrs] (let [selected (get-in @st/state [:workspace-local :selected])] - (apply-token-to-shapes file-id set-id id selected attrs)))})) + (apply-token-to-shapes plugin-id file-id set-id id selected attrs)))})) ;; === Token Set @@ -205,7 +205,7 @@ (defn token-set-proxy [plugin-id file-id id] (obj/reify {:name "TokenSetProxy" - :on-error u/handle-error} + :on-error (u/handle-error plugin-id)} :$plugin {:enumerable false :get (constantly plugin-id)} :$file-id {:enumerable false :get (constantly file-id)} :$id {:enumerable false :get (constantly id)} @@ -310,7 +310,7 @@ (if resolved-value (do (st/emit! (dwtl/create-token id token)) (token-proxy plugin-id file-id id (:id token))) - (do (u/display-not-valid :addToken (str errors)) + (do (u/not-valid plugin-id :addToken (str errors)) nil))))} :duplicate @@ -327,7 +327,7 @@ (defn token-theme-proxy [plugin-id file-id id] (obj/reify {:name "TokenThemeProxy" - :on-error u/handle-error} + :on-error (u/handle-error plugin-id)} :$plugin {:enumerable false :get (constantly plugin-id)} :$file-id {:enumerable false :get (constantly file-id)} :$id {:enumerable false :get (constantly id)} @@ -434,7 +434,7 @@ (defn tokens-catalog [plugin-id file-id] (obj/reify {:name "TokensCatalog" - :on-error u/handle-error} + :on-error (u/handle-error plugin-id)} :$plugin {:enumerable false :get (constantly plugin-id)} :$id {:enumerable false :get (constantly file-id)} diff --git a/frontend/src/app/plugins/utils.cljs b/frontend/src/app/plugins/utils.cljs index 0e94c1543d..cbfc512323 100644 --- a/frontend/src/app/plugins/utils.cljs +++ b/frontend/src/app/plugins/utils.cljs @@ -9,7 +9,6 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.json :as json] [app.common.schema :as sm] [app.common.types.component :as ctk] [app.common.types.container :as ctn] @@ -222,6 +221,16 @@ (resolve value)))))] [ret-v ret-p])) +(defn natural-child-ordering? + [plugin-id] + (boolean + (dm/get-in @st/state [:plugins :flags plugin-id :natural-child-ordering]))) + +(defn throw-validation-errors? + [plugin-id] + (boolean + (dm/get-in @st/state [:plugins :flags plugin-id :throw-validation-errors]))) + (defn display-not-valid [code value] (if (some? value) @@ -229,23 +238,25 @@ (.error js/console (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code))) nil) +(defn throw-not-valid + [code value] + (if (some? value) + (throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code))) + (throw (js/Error. (dm/str "[PENPOT PLUGIN] Value not valid. Code: " code)))) + nil) + +(defn not-valid + [plugin-id code value] + (if (throw-validation-errors? plugin-id) + (throw-not-valid code value) + (display-not-valid code value))) + (defn reject-not-valid [reject code value] (let [msg (dm/str "[PENPOT PLUGIN] Value not valid: " value ". Code: " code)] (.error js/console msg) (reject msg))) -(defn coerce - "Decodes a javascript object into clj and check against schema. If schema validation fails, - displays a not-valid message with the code and hint provided and returns nil." - [attrs schema code hint] - (let [decoder (sm/decoder schema sm/json-transformer) - explainer (sm/explainer schema) - attrs (-> attrs json/->clj decoder)] - (if-let [explain (explainer attrs)] - (display-not-valid code (str hint " " (sm/humanize-explain explain))) - attrs))) - (defn mixed-value [values] (let [s (set values)] @@ -254,12 +265,14 @@ (defn handle-error "Function to be used in plugin proxies methods to handle errors and print a readable message to the console." - [cause] - (display-not-valid (ex-message cause) nil) - (if-let [explain (-> cause ex-data ::sm/explain)] - (println (sm/humanize-explain explain)) - (js/console.log (ex-data cause))) - (js/console.log (.-stack cause))) + [plugin-id] + (fn [cause] + (let [message + (if-let [explain (-> cause ex-data ::sm/explain)] + (sm/humanize-explain explain) + (ex-data cause))] + (js/console.log (.-stack cause)) + (not-valid plugin-id :error message)))) (defn is-main-component-proxy? [p] diff --git a/frontend/src/app/plugins/viewport.cljs b/frontend/src/app/plugins/viewport.cljs index a581e3ba60..8b5b73ac57 100644 --- a/frontend/src/app/plugins/viewport.cljs +++ b/frontend/src/app/plugins/viewport.cljs @@ -38,10 +38,10 @@ new-y (obj/get value "y")] (cond (not (sm/valid-safe-number? new-x)) - (u/display-not-valid :center-x new-x) + (u/not-valid plugin-id :center-x new-x) (not (sm/valid-safe-number? new-y)) - (u/display-not-valid :center-y new-y) + (u/not-valid plugin-id :center-y new-y) :else (let [vb (dm/get-in @st/state [:workspace-local :vbox]) @@ -63,7 +63,7 @@ (fn [value] (cond (not (sm/valid-safe-number? value)) - (u/display-not-valid :zoom value) + (u/not-valid plugin-id :zoom value) :else (let [z (dm/get-in @st/state [:workspace-local :zoom])] @@ -87,7 +87,7 @@ (fn [shapes] (cond (not (every? ps/shape-proxy? shapes)) - (u/display-not-valid :zoomIntoView "Argument should be valid shapes") + (u/not-valid plugin-id :zoomIntoView "Argument should be valid shapes") :else (let [ids (->> shapes diff --git a/plugins/CHANGELOG.md b/plugins/CHANGELOG.md index 4ba7745a4f..203e5e0e81 100644 --- a/plugins/CHANGELOG.md +++ b/plugins/CHANGELOG.md @@ -6,6 +6,7 @@ - **plugin-types**: Added `createVariantFromComponents` - **plugin-types**: Change return type of `combineAsVariants` - **plugin-types**: Added `textBounds` property for text shapes +- **plugin-types**: Added flag `throwValidationErrors` to enable exceptions on validation ## 1.4.2 (2026-01-21) diff --git a/plugins/libs/plugin-types/index.d.ts b/plugins/libs/plugin-types/index.d.ts index 91dbb38b84..1ebface05c 100644 --- a/plugins/libs/plugin-types/index.d.ts +++ b/plugins/libs/plugin-types/index.d.ts @@ -1715,6 +1715,13 @@ export interface Flags { * Defaults to false */ naturalChildOrdering: boolean; + + /** + * If `true` the validation errors will throw an exception instead of displaying an + * error in the debugger console. + * Defaults to false + */ + throwValidationErrors: boolean; } /** From 3270d6549111467bfd0caa9d42ef0567de8fc16d Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 19 Mar 2026 14:35:48 +0100 Subject: [PATCH 16/22] :bug: Fix problem with token retrieval --- backend/src/app/rpc/commands/access_token.clj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index 393f824599..40a27ee763 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -88,7 +88,6 @@ :columns [:id :name :perms :type :created-at :updated-at :expires-at]}) (mapv decode-row))) - (def ^:private schema:get-current-mcp-token [:map {:title "get-current-mcp-token"}]) @@ -101,6 +100,6 @@ :type "mcp"} {:order-by [[:expires-at :asc] [:created-at :asc]] :columns [:token :expires-at]}) - (remove #(ct/is-after? (:expires-at %) request-at)) + (remove #(ct/is-after? request-at (:expires-at %))) (map decode-row) (first))) From 93de83c4273953cf197992f60c1b5d9cf2d9970f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 19 Mar 2026 14:36:00 +0100 Subject: [PATCH 17/22] :bug: Fix problem with error message --- backend/src/app/rpc/commands/access_token.clj | 3 ++- mcp/packages/plugin/src/main.ts | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/src/app/rpc/commands/access_token.clj b/backend/src/app/rpc/commands/access_token.clj index 40a27ee763..eedb119d06 100644 --- a/backend/src/app/rpc/commands/access_token.clj +++ b/backend/src/app/rpc/commands/access_token.clj @@ -100,6 +100,7 @@ :type "mcp"} {:order-by [[:expires-at :asc] [:created-at :asc]] :columns [:token :expires-at]}) - (remove #(ct/is-after? request-at (:expires-at %))) + (remove #(and (some? (:expires-at %)) + (ct/is-after? request-at (:expires-at %)))) (map decode-row) (first))) diff --git a/mcp/packages/plugin/src/main.ts b/mcp/packages/plugin/src/main.ts index 1d6c7edf3b..da87fa025f 100644 --- a/mcp/packages/plugin/src/main.ts +++ b/mcp/packages/plugin/src/main.ts @@ -55,6 +55,8 @@ function connectToMcpServer(baseUrl?: string, token?: string): void { try { let wsUrl = baseUrl || PENPOT_MCP_WEBSOCKET_URL; + let wsError: unknown | undefined; + if (token) { wsUrl += `?userToken=${encodeURIComponent(token)}`; } @@ -79,14 +81,18 @@ function connectToMcpServer(baseUrl?: string, token?: string): void { }; ws.onclose = (event: CloseEvent) => { - console.log("Disconnected from MCP server"); - const message = event.reason || undefined; - updateConnectionStatus("disconnected", "Disconnected", false, message); + // If we've send the error update we don't send the disconnect as well + if (!wsError) { + console.log("Disconnected from MCP server"); + const message = event.reason || undefined; + updateConnectionStatus("disconnected", "Disconnected", false, message); + } ws = null; }; ws.onerror = (error) => { console.error("WebSocket error:", error); + wsError = error; // note: WebSocket error events typically don't contain detailed error messages updateConnectionStatus("error", "Connection error", false); }; From 9e9c28fe3c5b08b7560194e8bf78040a65efeb18 Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Wed, 18 Mar 2026 15:06:06 +0100 Subject: [PATCH 18/22] :bug: Fix MCP notifications when there is only one tab --- frontend/src/app/main/data/workspace/mcp.cljs | 64 ++++++++++--------- .../main/data/workspace/notifications.cljs | 7 +- .../app/main/ui/settings/integrations.cljs | 3 +- .../src/app/main/ui/workspace/main_menu.cljs | 14 ++-- .../src/app/main/ui/workspace/main_menu.scss | 4 ++ frontend/translations/en.po | 5 +- frontend/translations/es.po | 5 +- 7 files changed, 62 insertions(+), 40 deletions(-) diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index 63e7de7516..d8ce546c85 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -53,21 +53,28 @@ (rx/take 1) (rx/map #(ptk/data-event ::connect)))))) -(defn manage-notification - [mcp-enabled? mcp-connected?] - (if mcp-enabled? - (if mcp-connected? - (rx/of (ntf/hide)) - (rx/of (ntf/dialog :content (tr "notifications.mcp.active-tab-switching.text") - :cancel {:label (tr "labels.dismiss") - :callback #(st/emit! (ntf/hide) - (ptk/event ::ev/event {::ev/name "confirm-mcp-tab-switch" - ::ev/origin "workspace-notification"}))} - :accept {:label (tr "labels.switch") - :callback #(st/emit! (connect-mcp) - (ptk/event ::ev/event {::ev/name "dismiss-mcp-tab-switch" - ::ev/origin "workspace-notification"}))}))) - (rx/of (ntf/hide)))) +(defn manage-mcp-notification + [] + (ptk/reify ::manage-mcp-notification + ptk/WatchEvent + (watch [_ state _] + (let [mcp-connected? (true? (-> state :workspace-local :mcp :connection)) + mcp-enabled? (true? (-> state :profile :props :mcp-enabled)) + num-sessions (-> state :workspace-presence vals count) + multi-session? (> num-sessions 1)] + (if (and mcp-enabled? multi-session?) + (if mcp-connected? + (rx/of (ntf/hide)) + (rx/of (ntf/dialog :content (tr "notifications.mcp.active-in-another-tab") + :cancel {:label (tr "labels.dismiss") + :callback #(st/emit! (ntf/hide) + (ptk/event ::ev/event {::ev/name "confirm-mcp-tab-switch" + ::ev/origin "workspace-notification"}))} + :accept {:label (tr "labels.switch") + :callback #(st/emit! (connect-mcp) + (ptk/event ::ev/event {::ev/name "dismiss-mcp-tab-switch" + ::ev/origin "workspace-notification"}))}))) + (rx/of (ntf/hide))))))) (defn update-mcp-status [value] @@ -77,26 +84,24 @@ (update-in state [:profile :props] assoc :mcp-enabled value)) ptk/WatchEvent - (watch [_ state _] + (watch [_ _ _] (rx/merge - (let [mcp-connected? (-> state :workspace-local :mcp :connected)] - (manage-notification value mcp-connected?)) - (case value - true (rx/of (ptk/data-event ::connect)) - false (rx/of (ptk/data-event ::disconnect)) - nil))))) + (rx/of (manage-mcp-notification))) + (case value + true (rx/of (ptk/data-event ::connect)) + false (rx/of (ptk/data-event ::disconnect)) + nil)))) (defn update-mcp-connection [value] (ptk/reify ::update-mcp-plugin-connection ptk/UpdateEvent (update [_ state] - (update-in state [:workspace-local :mcp] assoc :connected value)) + (update-in state [:workspace-local :mcp] assoc :connection value)) ptk/WatchEvent - (watch [_ state _] - (let [mcp-enabled? (-> state :profile :props :mcp-enabled)] - (manage-notification mcp-enabled? value))))) + (watch [_ _ _] + (rx/of (manage-mcp-notification))))) (defn init-mcp! [stream] @@ -116,11 +121,12 @@ :getServerUrl #(str cf/mcp-ws-uri) :setMcpStatus (fn [status] - (let [mcp-connected? (case status + (let [mcp-connection (case status "connected" true "disconnected" false - nil)] - (st/emit! (update-mcp-connection mcp-connected?)) + "error" nil + "")] + (st/emit! (update-mcp-connection mcp-connection)) (log/info :hint "MCP STATUS" :status status))) :on diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 9bfc7ac8a2..743fecd850 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -23,6 +23,7 @@ [app.main.data.workspace.edition :as dwe] [app.main.data.workspace.layout :as dwly] [app.main.data.workspace.libraries :as dwl] + [app.main.data.workspace.mcp :as mcp] [app.main.data.workspace.texts :as dwt] [app.main.router :as rt] [app.util.globals :refer [global]] @@ -212,7 +213,11 @@ (update [_ state] (if (or (= :disconnect type) (= :leave-file type)) (update state :workspace-presence dissoc session-id) - (update state :workspace-presence update-presence)))))) + (update state :workspace-presence update-presence))) + + ptk/WatchEvent + (watch [_ _ _] + (rx/of (mcp/manage-mcp-notification)))))) (defn handle-pointer-update [{:keys [page-id session-id position zoom zoom-inverse vbox vport] :as msg}] diff --git a/frontend/src/app/main/ui/settings/integrations.cljs b/frontend/src/app/main/ui/settings/integrations.cljs index f4b820e0b3..a54b43d2b9 100644 --- a/frontend/src/app/main/ui/settings/integrations.cljs +++ b/frontend/src/app/main/ui/settings/integrations.cljs @@ -7,7 +7,6 @@ (ns app.main.ui.settings.integrations (:require-macros [app.main.style :as stl]) (:require - [app.common.data :as d] [app.common.data.macros :as dm] [app.common.schema :as sm] [app.common.time :as ct] @@ -410,7 +409,7 @@ profile (mf/deref refs/profile) mcp-key (some #(when (= (:type %) "mcp") %) tokens) - mcp-enabled? (d/nilv (-> profile :props :mcp-enabled) false) + mcp-enabled? (true? (-> profile :props :mcp-enabled)) expires-at (:expires-at mcp-key) expired? (and (some? expires-at) (> (ct/now) expires-at)) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index aa320694c7..223e30ce50 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -749,8 +749,8 @@ profile (mf/deref refs/profile) workspace-local (mf/deref refs/workspace-local) - mcp-enabled? (-> profile :props :mcp-enabled) - mcp-connected? (-> workspace-local :mcp :connected) + mcp-enabled? (true? (-> profile :props :mcp-enabled)) + mcp-connected? (true? (-> workspace-local :mcp :connection)) on-nav-to-integrations (mf/use-fn @@ -978,9 +978,10 @@ :class (stl/css :item-arrow)}]]) (when (contains? cf/flags :mcp) - (let [mcp-enabled? (-> profile :props :mcp-enabled) - mcp-connected? (-> workspace-local :mcp :connected) - mcp-active? (and mcp-enabled? mcp-connected?)] + (let [mcp-enabled? (true? (-> profile :props :mcp-enabled)) + mcp-connection (-> workspace-local :mcp :connection) + mcp-connected? (true? mcp-connection) + mcp-error? (nil? mcp-connection)] [:> dropdown-menu-item* {:class (stl/css :base-menu-item :menu-item) :on-click on-menu-click :on-key-down (fn [event] @@ -992,7 +993,8 @@ [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.mcp")] [:span {:class (stl/css-case :item-indicator true - :active mcp-active?)}] + :active (and mcp-enabled? mcp-connected?) + :failed (and mcp-enabled? mcp-error?))}] [:> icon* {:icon-id i/arrow-right :class (stl/css :item-arrow)}]])) diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss index d69b09e61a..1b12e2cdbf 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.scss +++ b/frontend/src/app/main/ui/workspace/main_menu.scss @@ -134,6 +134,10 @@ &.active { --menu-indicator-color: var(--color-accent-primary); } + + &.failed { + --menu-indicator-color: var(--color-foreground-error); + } } .item-arrow { diff --git a/frontend/translations/en.po b/frontend/translations/en.po index c36e440412..4e52ddd5ee 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3833,9 +3833,12 @@ msgstr "Invitation sent successfully" msgid "notifications.invitation-link-copied" msgstr "Invitation link copied" -msgid "notifications.mcp.active-tab-switching.text" +msgid "notifications.mcp.active-in-another-tab" msgstr "MCP is active in another tab. Switch here?" +msgid "notifications.mcp.active-in-this-tab" +msgstr "MCP is now active in this tab." + #: src/app/main/ui/settings/delete_account.cljs:24 msgid "notifications.profile-deletion-not-allowed" msgstr "You can't delete your profile. Reassign your teams before proceed." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 4d58a6dc7f..a2d6277640 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3790,9 +3790,12 @@ msgstr "Invitación enviada con éxito" msgid "notifications.invitation-link-copied" msgstr "Enlace de invitacion copiado" -msgid "notifications.mcp.active-tab-switching.text" +msgid "notifications.mcp.active-in-another-tab" msgstr "MCP está activo en otra pestaña. ¿Cambiar a esta?" +msgid "notifications.mcp.active-in-this-tab" +msgstr "MCP está ahora activo en esta pestaña." + #: src/app/main/ui/settings/delete_account.cljs:24 msgid "notifications.profile-deletion-not-allowed" msgstr "No puedes borrar tu perfil. Reasigna tus equipos antes de seguir." From e870497ae113220c8d58ee81f4e81a92986c982e Mon Sep 17 00:00:00 2001 From: Luis de Dios Date: Thu, 19 Mar 2026 16:27:44 +0100 Subject: [PATCH 19/22] :paperclip: PR changes --- frontend/src/app/main/data/workspace/mcp.cljs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/main/data/workspace/mcp.cljs b/frontend/src/app/main/data/workspace/mcp.cljs index d8ce546c85..a9198750c7 100644 --- a/frontend/src/app/main/data/workspace/mcp.cljs +++ b/frontend/src/app/main/data/workspace/mcp.cljs @@ -60,7 +60,7 @@ (watch [_ state _] (let [mcp-connected? (true? (-> state :workspace-local :mcp :connection)) mcp-enabled? (true? (-> state :profile :props :mcp-enabled)) - num-sessions (-> state :workspace-presence vals count) + num-sessions (-> state :workspace-presence count) multi-session? (> num-sessions 1)] (if (and mcp-enabled? multi-session?) (if mcp-connected? From e8ce2a43f2425bbee779b753bd333ec41e245e5b Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 19 Mar 2026 17:45:58 +0100 Subject: [PATCH 20/22] :bug: Fix line breaks not rendering in text shapes --- render-wasm/src/shapes/text.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index f9619eaf74..9ef4da3d07 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -1139,7 +1139,11 @@ impl TextSpan { fn process_ignored_chars(text: &str, browser: u8) -> String { text.chars() .filter_map(|c| { - if c < '\u{0020}' || c == '\u{2028}' || c == '\u{2029}' { + // Preserve line breaks: \n (U+000A), \r (U+000D), and Unicode separators + if c == '\n' || c == '\r' || c == '\u{2028}' || c == '\u{2029}' { + return Some(c); + } + if c < '\u{0020}' { if browser == Browser::Firefox as u8 { None } else { From 353d8677b054cf51330205952eccc1cb4cfc2b0e Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 20 Mar 2026 09:28:28 +0100 Subject: [PATCH 21/22] :bug: Fix WASM text auto-width geometry on finalize --- frontend/src/app/main/data/workspace/texts.cljs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index b1903ec5c5..440c8da3d7 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -978,8 +978,7 @@ (select-keys shape [:selrect :points :width :height])) content-has-text? (v2-content-has-text? content) prev-content-has-text? (v2-content-has-text? prev-content) - new-size (when (and (not= :fixed (:grow-type shape)) - content-has-text?) + new-size (when (not= :fixed (:grow-type shape)) (dwwt/get-wasm-text-new-size shape content)) ;; New shapes: single undo on finalize only (no per-keystroke undo) effective-save-undo? (if new-shape? finalize? save-undo?) From 1126ed37f14c71f4a9a843aa7b2c7aa2216d6939 Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Fri, 20 Mar 2026 09:43:00 +0100 Subject: [PATCH 22/22] :bug: Coerce finalize? in WASM text updates for valid undo flags --- frontend/src/app/main/data/workspace/texts.cljs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 440c8da3d7..6dc5618f66 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -966,7 +966,11 @@ ptk/WatchEvent (watch [it state _] (if (features/active-feature? state "render-wasm/v1") - (let [objects (dsh/lookup-page-objects state) + (let [;; v3 editor always passes :finalize? from keyword opts; when absent + ;; that binds nil and :or defaults do not apply — coerce so undo flags + ;; stay strict booleans for changes-builder schema validation. + finalize? (boolean finalize?) + objects (dsh/lookup-page-objects state) shape (get objects id) new-shape? (contains? (:workspace-new-text-shapes state) id) prev-content (:content shape)