mirror of
https://github.com/penpot/penpot.git
synced 2026-02-04 03:32:08 -05:00
Compare commits
36 Commits
ladybenko-
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6831e9b48 | ||
|
|
138df7c958 | ||
|
|
ef2bdf86d8 | ||
|
|
512a31d375 | ||
|
|
717a048b73 | ||
|
|
cbd90ff970 | ||
|
|
c99fac000a | ||
|
|
1325584e1a | ||
|
|
0d9b7ca696 | ||
|
|
d215a5c402 | ||
|
|
7681231d8f | ||
|
|
07b9ef0fd6 | ||
|
|
2ae68d5752 | ||
|
|
913672e5c5 | ||
|
|
8c25fb00ac | ||
|
|
6a84215911 | ||
|
|
f65292a13c | ||
|
|
94722fdec2 | ||
|
|
28509e0418 | ||
|
|
9569fa2bcb | ||
|
|
852b31c3a0 | ||
|
|
84b3f5d7c6 | ||
|
|
76bd31fe7d | ||
|
|
77bbf30ae4 | ||
|
|
693b52bf45 | ||
|
|
0f51b23ce7 | ||
|
|
ec61aa6b6d | ||
|
|
abc1773f65 | ||
|
|
93f5e74bb0 | ||
|
|
38179ba11e | ||
|
|
719a95246a | ||
|
|
e590cd852d | ||
|
|
a9741073e5 | ||
|
|
599656c31e | ||
|
|
16f22a7b5c | ||
|
|
a1460115e8 |
@@ -32,6 +32,7 @@
|
||||
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
|
||||
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
|
||||
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
|
||||
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
|
||||
|
||||
## 2.13.0 (Unreleased)
|
||||
|
||||
@@ -73,6 +74,9 @@
|
||||
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
|
||||
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
|
||||
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
|
||||
- Fix import a file with shadow tokens [Taiga #13229](https://tree.taiga.io/project/penpot/issue/13229)
|
||||
- Fix allow spaces on token description [Taiga #13184](https://tree.taiga.io/project/penpot/issue/13184)
|
||||
- Fix error when creating a token with an invalid name [Taiga #13219](https://tree.taiga.io/project/penpot/issue/13219)
|
||||
|
||||
## 2.12.1
|
||||
|
||||
|
||||
@@ -2016,7 +2016,9 @@
|
||||
(let [;; We need to sync only the position relative to the origin of the component.
|
||||
;; (see update-attrs for a full explanation)
|
||||
previous-shape (reposition-shape previous-shape prev-root current-root)
|
||||
touched (get previous-shape :touched #{})]
|
||||
touched (get previous-shape :touched #{})
|
||||
text-auto? (and (cfh/text-shape? current-shape)
|
||||
(contains? #{:auto-height :auto-width} (:grow-type current-shape)))]
|
||||
|
||||
(loop [attrs updatable-attrs
|
||||
roperations [{:type :set-touched :touched (:touched previous-shape)}]
|
||||
@@ -2025,6 +2027,10 @@
|
||||
(let [attr-group (get ctk/sync-attrs attr)
|
||||
skip-operations?
|
||||
(or
|
||||
;; For auto text, avoid copying geometry-driven attrs on switch.
|
||||
(and text-auto?
|
||||
(contains? #{:points :selrect :width :height :position-data} attr))
|
||||
|
||||
;; If the attribute is not valid for the destiny, don't copy it
|
||||
(not (cts/is-allowed-switch-keep-attr? attr (:type current-shape)))
|
||||
|
||||
|
||||
@@ -109,9 +109,12 @@
|
||||
(def token-types
|
||||
(into #{} (keys token-type->dtcg-token-type)))
|
||||
|
||||
(def token-name-validation-regex
|
||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$")
|
||||
|
||||
(def token-name-ref
|
||||
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$"])
|
||||
token-name-validation-regex])
|
||||
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
|
||||
@@ -1467,11 +1467,12 @@ Will return a value that matches this schema:
|
||||
(def ^:private schema:dtcg-node
|
||||
[:schema {:registry
|
||||
{::simple-value
|
||||
[:or :string :int :double]
|
||||
[:or :string :int :double ::sm/boolean]
|
||||
::value
|
||||
[:or
|
||||
[:ref ::simple-value]
|
||||
[:vector ::simple-value]
|
||||
[:vector [:map-of :string ::simple-value]]
|
||||
[:map-of :string [:or
|
||||
[:ref ::simple-value]
|
||||
[:vector ::simple-value]]]]}}
|
||||
|
||||
@@ -41,9 +41,4 @@ tmux select-window -t penpot:3
|
||||
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
|
||||
tmux send-keys -t penpot './scripts/start-dev' enter
|
||||
|
||||
tmux new-window -t penpot:4 -n 'rust-wasm'
|
||||
tmux select-window -t penpot:4
|
||||
tmux send-keys -t penpot 'cd penpot/render-wasm' enter C-l
|
||||
tmux send-keys -t penpot './build' enter
|
||||
|
||||
tmux -2 attach-session -t penpot
|
||||
|
||||
@@ -110,7 +110,7 @@ test("Update an already created text shape by prepending text", async ({
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by inserting text in between", async ({
|
||||
test.skip("Update an already created text shape by inserting text in between", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
@@ -151,7 +151,7 @@ test("Update a new text shape appending text by pasting text", async ({
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape prepending text by pasting text", async ({
|
||||
test.skip("Update a new text shape prepending text by pasting text", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
|
||||
@@ -44,6 +44,20 @@
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :open-plugins] (fnil conj #{}) id))))
|
||||
|
||||
(defn reset-plugin-flags
|
||||
[id]
|
||||
(ptk/reify ::reset-plugin-flags
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :plugin-flags] assoc id {}))))
|
||||
|
||||
(defn set-plugin-flag
|
||||
[id key value]
|
||||
(ptk/reify ::set-plugin-flag
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(update-in state [:workspace-local :plugin-flags id] assoc key value))))
|
||||
|
||||
(defn remove-current-plugin
|
||||
[id]
|
||||
(ptk/reify ::remove-current-plugin
|
||||
@@ -54,7 +68,9 @@
|
||||
(defn- load-plugin!
|
||||
[{:keys [plugin-id name description host code icon permissions]}]
|
||||
(try
|
||||
(st/emit! (save-current-plugin plugin-id))
|
||||
(st/emit! (save-current-plugin plugin-id)
|
||||
(reset-plugin-flags plugin-id))
|
||||
|
||||
(.ɵloadPlugin
|
||||
^js ug/global
|
||||
#js {:pluginId plugin-id
|
||||
|
||||
@@ -46,7 +46,9 @@
|
||||
[app.main.data.workspace.thumbnails :as dwt]
|
||||
[app.main.data.workspace.transforms :as dwtr]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.wasm-text :as dwwt]
|
||||
[app.main.data.workspace.zoom :as dwz]
|
||||
[app.main.features :as features]
|
||||
[app.main.features.pointer-map :as fpmap]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
@@ -1012,6 +1014,13 @@
|
||||
|
||||
updated-objects (pcb/get-objects changes)
|
||||
new-children-ids (cfh/get-children-ids-with-self updated-objects (:id new-shape))
|
||||
new-text-ids (->> new-children-ids
|
||||
(keep (fn [id]
|
||||
(when-let [child (get updated-objects id)]
|
||||
(when (and (cfh/text-shape? child)
|
||||
(not= :fixed (:grow-type child)))
|
||||
id))))
|
||||
(vec))
|
||||
|
||||
[changes parents-of-swapped]
|
||||
(if keep-touched?
|
||||
@@ -1021,6 +1030,9 @@
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
(when (and (features/active-feature? state "render-wasm/v1")
|
||||
(seq new-text-ids))
|
||||
(dwwt/resize-wasm-text-all new-text-ids))
|
||||
(ptk/data-event :layout/update {:ids update-layout-ids :undo-group undo-group})
|
||||
(dwu/commit-undo-transaction undo-id)
|
||||
(dws/select-shape (:id new-shape) false))))))
|
||||
|
||||
@@ -712,8 +712,7 @@
|
||||
(ctm/rotation-modifiers shape center angle))
|
||||
|
||||
modif-tree
|
||||
(-> (build-modif-tree ids objects get-modifier)
|
||||
(gm/set-objects-modifiers objects))
|
||||
(build-modif-tree ids objects get-modifier)
|
||||
|
||||
modifiers
|
||||
(mapv (fn [[id {:keys [modifiers]}]]
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
@@ -29,10 +28,10 @@
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.wasm-text :as dwwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.router :as rt]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.text-editor :as ted]
|
||||
[app.util.text.content.styles :as styles]
|
||||
[app.util.timers :as ts]
|
||||
@@ -52,50 +51,6 @@
|
||||
(declare v2-update-text-shape-content)
|
||||
(declare v2-update-text-editor-styles)
|
||||
|
||||
(defn resize-wasm-text-modifiers
|
||||
([shape]
|
||||
(resize-wasm-text-modifiers shape (:content shape)))
|
||||
|
||||
([{:keys [id points selrect grow-type] :as shape} content]
|
||||
(wasm.api/use-shape id)
|
||||
(wasm.api/set-shape-text-content id content)
|
||||
(wasm.api/set-shape-text-images id content)
|
||||
|
||||
(let [dimension (wasm.api/get-text-dimensions)
|
||||
width-scale (if (#{:fixed :auto-height} grow-type)
|
||||
1.0
|
||||
(/ (:width dimension) (:width selrect)))
|
||||
height-scale (if (= :fixed grow-type)
|
||||
1.0
|
||||
(/ (:height dimension) (:height selrect)))
|
||||
resize-v (gpt/point width-scale height-scale)
|
||||
origin (first points)]
|
||||
|
||||
{id
|
||||
{:modifiers
|
||||
(ctm/resize-modifiers
|
||||
resize-v
|
||||
origin
|
||||
(:transform shape (gmt/matrix))
|
||||
(:transform-inverse shape (gmt/matrix)))}})))
|
||||
|
||||
(defn resize-wasm-text
|
||||
[id]
|
||||
(ptk/reify ::resize-wasm-text
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
shape (get objects id)]
|
||||
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))))))
|
||||
|
||||
(defn resize-wasm-text-all
|
||||
[ids]
|
||||
(ptk/reify ::resize-wasm-text-all
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rx/from ids)
|
||||
(rx/map resize-wasm-text)))))
|
||||
|
||||
;; -- Content helpers
|
||||
|
||||
(defn- v2-content-has-text?
|
||||
@@ -178,7 +133,7 @@
|
||||
{:undo-group (when new-shape? id)})
|
||||
|
||||
(dwm/apply-wasm-modifiers
|
||||
(resize-wasm-text-modifiers shape content)
|
||||
(dwwt/resize-wasm-text-modifiers shape content)
|
||||
{:undo-group (when new-shape? id)})))))
|
||||
|
||||
(let [content (d/merge (ted/export-content content)
|
||||
@@ -823,7 +778,7 @@
|
||||
(when (features/active-feature? state "render-wasm/v1")
|
||||
;; This delay is to give time for the font to be correctly rendered
|
||||
;; in wasm.
|
||||
(cond->> (rx/of (resize-wasm-text id))
|
||||
(cond->> (rx/of (dwwt/resize-wasm-text id))
|
||||
(contains? attrs :font-id)
|
||||
(rx/delay 200)))))))
|
||||
|
||||
@@ -973,11 +928,11 @@
|
||||
|
||||
(if (and (not= :fixed (:grow-type shape)) finalize?)
|
||||
(dwm/apply-wasm-modifiers
|
||||
(resize-wasm-text-modifiers shape content)
|
||||
(dwwt/resize-wasm-text-modifiers shape content)
|
||||
{:undo-group (when new-shape? id)})
|
||||
|
||||
(dwm/set-wasm-modifiers
|
||||
(resize-wasm-text-modifiers shape content)
|
||||
(dwwt/resize-wasm-text-modifiers shape content)
|
||||
{:undo-group (when new-shape? id)})))
|
||||
|
||||
(when finalize?
|
||||
|
||||
@@ -27,9 +27,9 @@
|
||||
[app.main.data.workspace.colors :as wdc]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.transforms :as dwtr]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.wasm-text :as dwwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
@@ -304,7 +304,7 @@
|
||||
(and affects-layout?
|
||||
(features/active-feature? state "render-wasm/v1"))
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn update-line-height
|
||||
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
|
||||
@@ -363,7 +363,7 @@
|
||||
:page-id page-id}))
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn- create-font-family-text-attrs
|
||||
[value]
|
||||
@@ -440,7 +440,7 @@
|
||||
:page-id page-id}))
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
(rx/of (dwwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn update-font-weight
|
||||
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
|
||||
|
||||
@@ -406,13 +406,13 @@
|
||||
(ctm/change-property :grow-type new-grow-type)))
|
||||
modifiers)))
|
||||
|
||||
modif-tree
|
||||
(-> (dwm/build-modif-tree ids objects get-modifier)
|
||||
(gm/set-objects-modifiers objects))]
|
||||
modif-tree (dwm/build-modif-tree ids objects get-modifier)]
|
||||
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree {:ignore-snap-pixel true}))
|
||||
(rx/of (dwm/apply-modifiers* objects modif-tree nil options))))))))
|
||||
|
||||
(let [modif-tree (gm/set-objects-modifiers modif-tree objects)]
|
||||
(rx/of (dwm/apply-modifiers* objects modif-tree nil options)))))))))
|
||||
|
||||
(defn change-orientation
|
||||
"Change orientation of shapes, from the sidebar options form.
|
||||
|
||||
72
frontend/src/app/main/data/workspace/wasm_text.cljs
Normal file
72
frontend/src/app/main/data/workspace/wasm_text.cljs
Normal file
@@ -0,0 +1,72 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.data.workspace.wasm-text
|
||||
"Helpers/events to resize wasm text shapes without depending on workspace.texts.
|
||||
|
||||
This exists to avoid circular deps:
|
||||
workspace.texts -> workspace.libraries -> workspace.texts"
|
||||
(:require
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.matrix :as gmt]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn resize-wasm-text-modifiers
|
||||
([shape]
|
||||
(resize-wasm-text-modifiers shape (:content shape)))
|
||||
|
||||
([{:keys [id points selrect grow-type] :as shape} content]
|
||||
(wasm.api/use-shape id)
|
||||
(wasm.api/set-shape-text-content id content)
|
||||
(wasm.api/set-shape-text-images id content)
|
||||
|
||||
(let [dimension (wasm.api/get-text-dimensions)
|
||||
width-scale (if (#{:fixed :auto-height} grow-type)
|
||||
1.0
|
||||
(/ (:width dimension) (:width selrect)))
|
||||
height-scale (if (= :fixed grow-type)
|
||||
1.0
|
||||
(/ (:height dimension) (:height selrect)))
|
||||
resize-v (gpt/point width-scale height-scale)
|
||||
origin (first points)]
|
||||
|
||||
{id
|
||||
{:modifiers
|
||||
(ctm/resize-modifiers
|
||||
resize-v
|
||||
origin
|
||||
(:transform shape (gmt/matrix))
|
||||
(:transform-inverse shape (gmt/matrix)))}})))
|
||||
|
||||
(defn resize-wasm-text
|
||||
"Resize a single text shape (auto-width/auto-height) by id.
|
||||
No-op if the id is not a text shape or is :fixed."
|
||||
[id]
|
||||
(ptk/reify ::resize-wasm-text
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
shape (get objects id)]
|
||||
(if (and (some? shape)
|
||||
(cfh/text-shape? shape)
|
||||
(not= :fixed (:grow-type shape)))
|
||||
(rx/of (dwm/apply-wasm-modifiers (resize-wasm-text-modifiers shape)))
|
||||
(rx/empty))))))
|
||||
|
||||
(defn resize-wasm-text-all
|
||||
"Resize all text shapes (auto-width/auto-height) from a collection of ids."
|
||||
[ids]
|
||||
(ptk/reify ::resize-wasm-text-all
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(->> (rx/from ids)
|
||||
(rx/map resize-wasm-text)))))
|
||||
@@ -36,10 +36,12 @@
|
||||
|
||||
(defn- hide-popover
|
||||
[node]
|
||||
(dom/unset-css-property! node "block-size")
|
||||
(dom/unset-css-property! node "inset-block-start")
|
||||
(dom/unset-css-property! node "inset-inline-start")
|
||||
(.hidePopover ^js node))
|
||||
(when (and (some? node)
|
||||
(fn? (.-hidePopover node)))
|
||||
(dom/unset-css-property! node "block-size")
|
||||
(dom/unset-css-property! node "inset-block-start")
|
||||
(dom/unset-css-property! node "inset-inline-start")
|
||||
(.hidePopover ^js node)))
|
||||
|
||||
(defn- calculate-placement-bounding-rect
|
||||
"Given a placement, calcultates the bounding rect for it taking in
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
(def context (mf/create-context nil))
|
||||
|
||||
(mf/defc form-input*
|
||||
[{:keys [name] :rest props}]
|
||||
[{:keys [name trim] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx context)
|
||||
input-name name
|
||||
@@ -33,7 +33,7 @@
|
||||
(mf/deps input-name)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-input-value)]
|
||||
(fm/on-input-change form input-name value true))))
|
||||
(fm/on-input-change form input-name value trim))))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-change on-change
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
[app.main.data.workspace.shortcuts :as sc]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.data.workspace.wasm-text :as dwwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -141,7 +142,7 @@
|
||||
(dwsh/update-shapes ids #(assoc % :grow-type grow-type)))
|
||||
|
||||
(when (features/active-feature? @st/state "render-wasm/v1")
|
||||
(st/emit! (dwt/resize-wasm-text-all ids)))
|
||||
(st/emit! (dwwt/resize-wasm-text-all ids)))
|
||||
;; We asynchronously commit so every sychronous event is resolved first and inside the transaction
|
||||
(ts/schedule #(st/emit! (dwu/commit-undo-transaction uid))))
|
||||
(when (some? on-blur)
|
||||
|
||||
@@ -309,7 +309,7 @@
|
||||
|
||||
on-remove'
|
||||
(mf/use-fn
|
||||
(mf/deps index)
|
||||
(mf/deps index on-remove)
|
||||
(fn [_]
|
||||
(when on-remove
|
||||
(on-remove index))))
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.color :as cl]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tinycolor :as tinycolor]
|
||||
@@ -51,12 +52,15 @@
|
||||
;; Both variants provide identical color-picker and text-input behavior, but
|
||||
;; differ in how they persist the value within the form’s nested structure.
|
||||
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
(let [token
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
{:value value
|
||||
:name (if (str/blank? token-name)
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
|
||||
|
||||
@@ -50,9 +50,13 @@
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
(let [token
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
{:value (cto/split-font-family value)
|
||||
:name (if (str/blank? token-name)
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
@@ -140,9 +141,13 @@
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
(let [token
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
{:value value
|
||||
:name (if (str/blank? token-name)
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
tokens
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-token]]
|
||||
@@ -97,12 +98,12 @@
|
||||
value-subfield
|
||||
input-value-placeholder] :as props}]
|
||||
|
||||
(let [make-schema (or make-schema default-make-schema)
|
||||
(let [make-schema (or make-schema default-make-schema)
|
||||
input-component (or input-component token.controls/input*)
|
||||
validate-token (or validator default-validate-token)
|
||||
validate-token (or validator default-validate-token)
|
||||
|
||||
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
|
||||
active-tab (deref active-tab*)
|
||||
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
|
||||
active-tab (deref active-tab*)
|
||||
|
||||
token
|
||||
(mf/with-memo [token]
|
||||
@@ -142,6 +143,10 @@
|
||||
(fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
warning-name-change?
|
||||
(not= (get-in @form [:data :name])
|
||||
(:name initial))
|
||||
|
||||
on-toggle-tab
|
||||
(mf/use-fn
|
||||
(mf/deps form)
|
||||
@@ -270,7 +275,13 @@
|
||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:auto-focus true}]]
|
||||
:trim true
|
||||
:auto-focus true}]
|
||||
|
||||
(when (and warning-name-change? (= action "edit"))
|
||||
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
|
||||
[:> context-notification*
|
||||
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
(case type
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
[app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]]
|
||||
[app.plugins.events :as events]
|
||||
[app.plugins.file :as file]
|
||||
[app.plugins.flags :as flags]
|
||||
[app.plugins.fonts :as fonts]
|
||||
[app.plugins.format :as format]
|
||||
[app.plugins.history :as history]
|
||||
@@ -40,6 +41,7 @@
|
||||
[app.plugins.page :as page]
|
||||
[app.plugins.parser :as parser]
|
||||
[app.plugins.shape :as shape]
|
||||
[app.plugins.system-events :as se]
|
||||
[app.plugins.user :as user]
|
||||
[app.plugins.utils :as u]
|
||||
[app.plugins.viewport :as viewport]
|
||||
@@ -65,7 +67,10 @@
|
||||
(cb/with-objects (:objects page))
|
||||
(cb/add-object shape))]
|
||||
|
||||
(st/emit! (ch/commit-changes changes))
|
||||
(st/emit!
|
||||
(ch/commit-changes changes)
|
||||
(se/event plugin-id "create-shape" :type type))
|
||||
|
||||
(shape/shape-proxy plugin-id (:id shape))))
|
||||
|
||||
(defn create-context
|
||||
@@ -124,6 +129,9 @@
|
||||
:fonts
|
||||
{:get (fn [] (fonts/fonts-subcontext plugin-id))}
|
||||
|
||||
:flags
|
||||
{:get (fn [] (flags/flags-proxy plugin-id))}
|
||||
|
||||
:library
|
||||
{:get (fn [] (library/library-subcontext plugin-id))}
|
||||
|
||||
@@ -285,7 +293,8 @@
|
||||
page-id (:current-page-id @st/state)
|
||||
id (uuid/next)
|
||||
ids (into #{} (map #(obj/get % "$id")) shapes)]
|
||||
(st/emit! (dwg/group-shapes id ids))
|
||||
(st/emit! (dwg/group-shapes id ids)
|
||||
(se/event plugin-id "create-shape" :type type))
|
||||
(shape/shape-proxy plugin-id file-id page-id id))))
|
||||
|
||||
:ungroup
|
||||
@@ -327,7 +336,8 @@
|
||||
(cb/with-objects (:objects page))
|
||||
(cb/add-object shape))]
|
||||
|
||||
(st/emit! (ch/commit-changes changes))
|
||||
(st/emit! (ch/commit-changes changes)
|
||||
(se/event plugin-id "create-shape" :type :path))
|
||||
(shape/shape-proxy plugin-id (:id shape))))
|
||||
|
||||
:createText
|
||||
@@ -348,7 +358,9 @@
|
||||
(cb/with-objects (:objects page))
|
||||
(cb/add-object shape))]
|
||||
|
||||
(st/emit! (ch/commit-changes changes))
|
||||
(st/emit!
|
||||
(ch/commit-changes changes)
|
||||
(se/event plugin-id "create-shape" :type :text))
|
||||
(shape/shape-proxy plugin-id (:id shape)))))
|
||||
|
||||
:createShapeFromSvg
|
||||
@@ -361,7 +373,8 @@
|
||||
(let [id (uuid/next)
|
||||
file-id (:current-file-id @st/state)
|
||||
page-id (:current-page-id @st/state)]
|
||||
(st/emit! (dwm/create-svg-shape id "svg" svg-string (gpt/point 0 0)))
|
||||
(st/emit! (dwm/create-svg-shape id "svg" svg-string (gpt/point 0 0))
|
||||
(se/event plugin-id "create-shape" :type :svg))
|
||||
(shape/shape-proxy plugin-id file-id page-id id))))
|
||||
|
||||
:createShapeFromSvgWithImages
|
||||
@@ -381,7 +394,8 @@
|
||||
(st/emit! (dwm/create-svg-shape-with-images
|
||||
file-id id "svg" svg-string (gpt/point 0 0)
|
||||
#(resolve (shape/shape-proxy plugin-id file-id page-id id))
|
||||
reject)))))))
|
||||
reject)
|
||||
(se/event plugin-id "create-shape" :type :text)))))))
|
||||
|
||||
:createBoolean
|
||||
(fn [bool-type shapes]
|
||||
@@ -396,7 +410,8 @@
|
||||
:else
|
||||
(let [ids (into #{} (map #(obj/get % "$id")) shapes)
|
||||
shape-id (uuid/next)]
|
||||
(st/emit! (dwb/create-bool bool-type :ids ids :force-shape-id shape-id))
|
||||
(st/emit! (dwb/create-bool bool-type :ids ids :force-shape-id shape-id)
|
||||
(se/event plugin-id "create-shape" :type :boolean))
|
||||
(shape/shape-proxy plugin-id shape-id)))))
|
||||
|
||||
:generateMarkup
|
||||
|
||||
33
frontend/src/app/plugins/flags.cljs
Normal file
33
frontend/src/app/plugins/flags.cljs
Normal file
@@ -0,0 +1,33 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.plugins.flags
|
||||
(:require
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.store :as st]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.object :as obj]))
|
||||
|
||||
(defn flags-proxy
|
||||
[plugin-id]
|
||||
(obj/reify {:name "FlagProxy"}
|
||||
:naturalChildOrdering
|
||||
{:this false
|
||||
:get
|
||||
(fn []
|
||||
(boolean
|
||||
(get-in
|
||||
@st/state
|
||||
[:workspace-local :plugin-flags plugin-id :natural-child-ordering])))
|
||||
|
||||
:set
|
||||
(fn [value]
|
||||
(cond
|
||||
(not (boolean? value))
|
||||
(u/display-not-valid :naturalChildOrdering value)
|
||||
|
||||
:else
|
||||
(st/emit! (dp/set-plugin-flag plugin-id :natural-child-ordering value))))}))
|
||||
@@ -52,6 +52,7 @@
|
||||
[app.plugins.parser :as parser]
|
||||
[app.plugins.register :as r]
|
||||
[app.plugins.ruler-guides :as rg]
|
||||
[app.plugins.state :refer [natural-child-ordering?]]
|
||||
[app.plugins.text :as text]
|
||||
[app.plugins.utils :as u]
|
||||
[app.util.http :as http]
|
||||
@@ -921,7 +922,6 @@
|
||||
(fn []
|
||||
(let [shape (u/locate-shape file-id page-id id)]
|
||||
(cond
|
||||
|
||||
(and (not (cfh/frame-shape? shape))
|
||||
(not (cfh/group-shape? shape))
|
||||
(not (cfh/svg-raw-shape? shape))
|
||||
@@ -929,9 +929,14 @@
|
||||
(u/display-not-valid :getChildren (:type shape))
|
||||
|
||||
:else
|
||||
(->> (u/locate-shape file-id page-id id)
|
||||
(:shapes)
|
||||
(format/format-array #(shape-proxy plugin-id file-id page-id %))))))
|
||||
(let [is-reversed? (ctl/flex-layout? shape)
|
||||
reverse-fn
|
||||
(if (and (natural-child-ordering? plugin-id) is-reversed?)
|
||||
reverse identity)]
|
||||
(->> (u/locate-shape file-id page-id id)
|
||||
(:shapes)
|
||||
(reverse-fn)
|
||||
(format/format-array #(shape-proxy plugin-id file-id page-id %)))))))
|
||||
|
||||
:appendChild
|
||||
(fn [child]
|
||||
@@ -950,8 +955,10 @@
|
||||
(u/display-not-valid :appendChild "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(let [child-id (obj/get child "$id")]
|
||||
(st/emit! (dwsh/relocate-shapes #{child-id} id 0))))))
|
||||
(let [child-id (obj/get child "$id")
|
||||
is-reversed? (ctl/flex-layout? shape)
|
||||
index (if (and (natural-child-ordering? plugin-id) is-reversed?) 0 (count (:shapes shape)))]
|
||||
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
|
||||
|
||||
:insertChild
|
||||
(fn [index child]
|
||||
@@ -970,7 +977,12 @@
|
||||
(u/display-not-valid :insertChild "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(let [child-id (obj/get child "$id")]
|
||||
(let [child-id (obj/get child "$id")
|
||||
is-reversed? (ctl/flex-layout? shape)
|
||||
index
|
||||
(if (and (natural-child-ordering? plugin-id) is-reversed?)
|
||||
(- (count (:shapes shape)) index)
|
||||
index)]
|
||||
(st/emit! (dwsh/relocate-shapes #{child-id} id index))))))
|
||||
|
||||
;; Only for frames
|
||||
@@ -1360,7 +1372,8 @@
|
||||
(let [shape (u/proxy->shape self)
|
||||
file-id (obj/get self "$file")
|
||||
page-id (obj/get self "$page")
|
||||
ids (->> children (map #(obj/get % "$id")))]
|
||||
reverse-fn (if (natural-child-ordering? plugin-id) reverse identity)
|
||||
ids (->> children reverse-fn (map #(obj/get % "$id")))]
|
||||
|
||||
(cond
|
||||
(not= (set ids) (set (:shapes shape)))
|
||||
|
||||
16
frontend/src/app/plugins/state.cljs
Normal file
16
frontend/src/app/plugins/state.cljs
Normal file
@@ -0,0 +1,16 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.plugins.state
|
||||
(:require
|
||||
[app.main.store :as st]))
|
||||
|
||||
(defn natural-child-ordering?
|
||||
[plugin-id]
|
||||
(boolean
|
||||
(get-in
|
||||
@st/state
|
||||
[:workspace-local :plugin-flags plugin-id :natural-child-ordering])))
|
||||
23
frontend/src/app/plugins/system_events.cljs
Normal file
23
frontend/src/app/plugins/system_events.cljs
Normal file
@@ -0,0 +1,23 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.plugins.system-events
|
||||
(:require
|
||||
[app.main.data.event :as ev]
|
||||
[app.main.store :as st]))
|
||||
|
||||
;; Formats an event from the plugin system
|
||||
(defn event
|
||||
[plugin-id name & {:as props}]
|
||||
(let [plugin-data (get-in @st/state [:profile :props :plugins :data plugin-id])]
|
||||
(-> props
|
||||
(assoc ::ev/name name)
|
||||
(assoc ::ev/origin "plugin")
|
||||
(assoc ::ev/context
|
||||
{:plugin-name (:name plugin-data)
|
||||
:plugin-url (:url plugin-data)})
|
||||
(ev/event))))
|
||||
|
||||
@@ -106,17 +106,20 @@
|
||||
|
||||
(defn stop-propagation
|
||||
[^js event]
|
||||
(when event
|
||||
(when (and (some? event)
|
||||
(fn? (.-stopPropagation event)))
|
||||
(.stopPropagation event)))
|
||||
|
||||
(defn stop-immediate-propagation
|
||||
[^js event]
|
||||
(when event
|
||||
(when (and (some? event)
|
||||
(fn? (.-stopImmediatePropagation event)))
|
||||
(.stopImmediatePropagation event)))
|
||||
|
||||
(defn prevent-default
|
||||
[^js event]
|
||||
(when event
|
||||
(when (and (some? event)
|
||||
(fn? (.-preventDefault event)))
|
||||
(.preventDefault event)))
|
||||
|
||||
(defn get-target
|
||||
|
||||
@@ -7,15 +7,16 @@
|
||||
(ns app.util.keyboard
|
||||
(:require
|
||||
[app.config :as cfg]
|
||||
[app.util.dom :as dom]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defrecord KeyboardEvent [type key shift ctrl alt meta mod editing native-event]
|
||||
Object
|
||||
(preventDefault [_]
|
||||
(.preventDefault native-event))
|
||||
(dom/prevent-default native-event))
|
||||
|
||||
(stopPropagation [_]
|
||||
(.stopPropagation native-event)))
|
||||
(dom/stop-propagation native-event)))
|
||||
|
||||
(defn keyboard-event?
|
||||
[o]
|
||||
|
||||
@@ -405,12 +405,8 @@ export class TextEditor extends EventTarget {
|
||||
|
||||
if (e.inputType in commands) {
|
||||
const command = commands[e.inputType];
|
||||
if (!this.#selectionController.startMutation()) {
|
||||
return;
|
||||
}
|
||||
command(e, this, this.#selectionController);
|
||||
const mutations = this.#selectionController.endMutation();
|
||||
this.#notifyLayout(LayoutType.FULL, mutations);
|
||||
this.#notifyLayout(LayoutType.FULL);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -456,19 +452,12 @@ export class TextEditor extends EventTarget {
|
||||
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Backspace") {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.#selectionController.startMutation()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.#selectionController.isCollapsed) {
|
||||
this.#selectionController.removeWordBackward();
|
||||
} else {
|
||||
this.#selectionController.removeSelected();
|
||||
}
|
||||
|
||||
const mutations = this.#selectionController.endMutation();
|
||||
this.#notifyLayout(LayoutType.FULL, mutations);
|
||||
this.#notifyLayout(LayoutType.FULL);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -476,14 +465,12 @@ export class TextEditor extends EventTarget {
|
||||
* Notifies that the edited texts needs layout.
|
||||
*
|
||||
* @param {'full'|'partial'} type
|
||||
* @param {CommandMutations} mutations
|
||||
*/
|
||||
#notifyLayout(type = LayoutType.FULL, mutations) {
|
||||
#notifyLayout(type = LayoutType.FULL) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("needslayout", {
|
||||
detail: {
|
||||
type: type,
|
||||
mutations: mutations,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -630,10 +617,8 @@ export class TextEditor extends EventTarget {
|
||||
* @returns {TextEditor}
|
||||
*/
|
||||
applyStylesToSelection(styles) {
|
||||
this.#selectionController.startMutation();
|
||||
this.#selectionController.applyStyles(styles);
|
||||
const mutations = this.#selectionController.endMutation();
|
||||
this.#notifyLayout(LayoutType.FULL, mutations);
|
||||
this.#notifyLayout(LayoutType.FULL);
|
||||
this.#changeController.notifyImmediately();
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
/**
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*
|
||||
* Copyright (c) KALEIDOS INC
|
||||
*/
|
||||
|
||||
/**
|
||||
* Command mutations
|
||||
*/
|
||||
export class CommandMutations {
|
||||
#added = new Set();
|
||||
#removed = new Set();
|
||||
#updated = new Set();
|
||||
|
||||
constructor(added, updated, removed) {
|
||||
if (added && Array.isArray(added)) this.#added = new Set(added);
|
||||
if (updated && Array.isArray(updated)) this.#updated = new Set(updated);
|
||||
if (removed && Array.isArray(removed)) this.#removed = new Set(removed);
|
||||
}
|
||||
|
||||
get added() {
|
||||
return this.#added;
|
||||
}
|
||||
|
||||
get removed() {
|
||||
return this.#removed;
|
||||
}
|
||||
|
||||
get updated() {
|
||||
return this.#updated;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#added.clear();
|
||||
this.#removed.clear();
|
||||
this.#updated.clear();
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.#added.clear();
|
||||
this.#added = null;
|
||||
this.#removed.clear();
|
||||
this.#removed = null;
|
||||
this.#updated.clear();
|
||||
this.#updated = null;
|
||||
}
|
||||
|
||||
add(node) {
|
||||
this.#added.add(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
remove(node) {
|
||||
this.#removed.add(node);
|
||||
return this;
|
||||
}
|
||||
|
||||
update(node) {
|
||||
this.#updated.add(node);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default CommandMutations;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import CommandMutations from "./CommandMutations.js";
|
||||
|
||||
describe("CommandMutations", () => {
|
||||
test("should create a new CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
expect(mutations).toHaveProperty("added");
|
||||
expect(mutations).toHaveProperty("updated");
|
||||
expect(mutations).toHaveProperty("removed");
|
||||
});
|
||||
|
||||
test("should create an initialized new CommandMutations", () => {
|
||||
const mutations = new CommandMutations([1], [2], [3]);
|
||||
expect(mutations.added.size).toBe(1);
|
||||
expect(mutations.updated.size).toBe(1);
|
||||
expect(mutations.removed.size).toBe(1);
|
||||
expect(mutations.added.has(1)).toBe(true);
|
||||
expect(mutations.updated.has(2)).toBe(true);
|
||||
expect(mutations.removed.has(3)).toBe(true);
|
||||
});
|
||||
|
||||
test("should add an added node to a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.add(1);
|
||||
expect(mutations.added.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("should add an updated node to a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.update(1);
|
||||
expect(mutations.updated.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("should add an removed node to a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.remove(1);
|
||||
expect(mutations.removed.has(1)).toBe(true);
|
||||
});
|
||||
|
||||
test("should clear a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.add(1);
|
||||
mutations.update(2);
|
||||
mutations.remove(3);
|
||||
expect(mutations.added.has(1)).toBe(true);
|
||||
expect(mutations.added.size).toBe(1);
|
||||
expect(mutations.updated.has(2)).toBe(true);
|
||||
expect(mutations.updated.size).toBe(1);
|
||||
expect(mutations.removed.has(3)).toBe(true);
|
||||
expect(mutations.removed.size).toBe(1);
|
||||
|
||||
mutations.clear();
|
||||
expect(mutations.added.size).toBe(0);
|
||||
expect(mutations.added.has(1)).toBe(false);
|
||||
expect(mutations.updated.size).toBe(0);
|
||||
expect(mutations.updated.has(1)).toBe(false);
|
||||
expect(mutations.removed.size).toBe(0);
|
||||
expect(mutations.removed.has(1)).toBe(false);
|
||||
});
|
||||
|
||||
test("should dispose a CommandMutations", () => {
|
||||
const mutations = new CommandMutations();
|
||||
mutations.add(1);
|
||||
mutations.update(2);
|
||||
mutations.remove(3);
|
||||
mutations.dispose();
|
||||
expect(mutations.added).toBe(null);
|
||||
expect(mutations.updated).toBe(null);
|
||||
expect(mutations.removed).toBe(null);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { insertInto, removeBackward, removeForward, replaceWith } from "./Text";
|
||||
import { insertInto, removeSlice, removeBackward, removeForward, removeWordBackward, replaceWith, findPreviousWordBoundary } from "./Text";
|
||||
|
||||
describe("Text", () => {
|
||||
test("* should throw when passed wrong parameters", () => {
|
||||
@@ -51,4 +51,23 @@ describe("Text", () => {
|
||||
test("`removeForward` should remove string forward from offset 6", () => {
|
||||
expect(removeForward("Hello, World!", 6)).toBe("Hello,World!");
|
||||
});
|
||||
|
||||
test("`removeSlice` should remove a part of a text", () => {
|
||||
expect(removeSlice("Hello, World!", 7, 12)).toBe("Hello, !");
|
||||
});
|
||||
|
||||
test("`findPreviousWordBoundary` edge cases", () => {
|
||||
expect(findPreviousWordBoundary(null)).toBe(0);
|
||||
expect(findPreviousWordBoundary("Hello, World!", 0)).toBe(0);
|
||||
expect(findPreviousWordBoundary(" Hello, World!", 3)).toBe(0);
|
||||
})
|
||||
|
||||
test("`removeWordBackward` with no text should return an empty string", () => {
|
||||
expect(removeWordBackward(null, 0)).toBe("");
|
||||
});
|
||||
|
||||
test("`removeWordBackward` should remove a word backward", () => {
|
||||
expect(removeWordBackward("Hello, World!", 13)).toBe("Hello, World");
|
||||
expect(removeWordBackward("Hello, World", 12)).toBe("Hello, ");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest";
|
||||
import { getFills } from "./Color.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("Color", () => {
|
||||
describe.skip("Color", () => {
|
||||
test("getFills", () => {
|
||||
expect(getFills("#aa0000")).toBe(
|
||||
'[["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]',
|
||||
|
||||
@@ -49,7 +49,6 @@ import {
|
||||
} from "../content/dom/TextNode.js";
|
||||
import TextNodeIterator from "../content/dom/TextNodeIterator.js";
|
||||
import TextEditor from "../TextEditor.js";
|
||||
import CommandMutations from "../commands/CommandMutations.js";
|
||||
import { isRoot, setRootStyles } from "../content/dom/Root.js";
|
||||
import { SelectionDirection } from "./SelectionDirection.js";
|
||||
import { SafeGuard } from "./SafeGuard.js";
|
||||
@@ -145,13 +144,6 @@ export class SelectionController extends EventTarget {
|
||||
*/
|
||||
#debug = null;
|
||||
|
||||
/**
|
||||
* Command Mutations.
|
||||
*
|
||||
* @type {CommandMutations}
|
||||
*/
|
||||
#mutations = new CommandMutations();
|
||||
|
||||
/**
|
||||
* Style defaults.
|
||||
*
|
||||
@@ -449,14 +441,14 @@ export class SelectionController extends EventTarget {
|
||||
dispose() {
|
||||
document.removeEventListener("selectionchange", this.#onSelectionChange);
|
||||
this.#textEditor = null;
|
||||
this.#currentStyle = null;
|
||||
this.#options = null;
|
||||
this.#ranges.clear();
|
||||
this.#ranges = null;
|
||||
this.#range = null;
|
||||
this.#selection = null;
|
||||
this.#focusNode = null;
|
||||
this.#anchorNode = null;
|
||||
this.#mutations.dispose();
|
||||
this.#mutations = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,28 +514,6 @@ export class SelectionController extends EventTarget {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the start of a mutation.
|
||||
*
|
||||
* Clears all the mutations kept in CommandMutations.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
startMutation() {
|
||||
this.#mutations.clear();
|
||||
if (!this.#focusNode) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the end of a mutation.
|
||||
*
|
||||
* @returns {CommandMutations}
|
||||
*/
|
||||
endMutation() {
|
||||
return this.#mutations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects all content.
|
||||
*
|
||||
@@ -597,11 +567,18 @@ export class SelectionController extends EventTarget {
|
||||
* @returns {SelectionController}
|
||||
*/
|
||||
cursorToEnd() {
|
||||
const root = this.#textEditor.root;
|
||||
|
||||
const range = document.createRange(); //Create a range (a range is a like the selection but invisible)
|
||||
range.selectNodeContents(this.#textEditor.element);
|
||||
range.setStart(root.lastChild.firstChild.firstChild, root.lastChild.firstChild.firstChild?.nodeValue?.length ?? 0);
|
||||
range.setEnd(root.lastChild.firstChild.firstChild, root.lastChild.firstChild.firstChild?.nodeValue?.length ?? 0);
|
||||
range.collapse(false);
|
||||
|
||||
this.#selection.removeAllRanges();
|
||||
this.#selection.addRange(range);
|
||||
|
||||
this.#updateState();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1340,7 +1317,6 @@ export class SelectionController extends EventTarget {
|
||||
|
||||
if (this.focusNode.nodeValue !== removedData) {
|
||||
this.focusNode.nodeValue = removedData;
|
||||
this.#mutations.update(this.focusTextSpan);
|
||||
}
|
||||
|
||||
const paragraph = this.focusParagraph;
|
||||
@@ -1383,7 +1359,6 @@ export class SelectionController extends EventTarget {
|
||||
this.focusOffset,
|
||||
newText,
|
||||
);
|
||||
this.#mutations.update(this.focusTextSpan);
|
||||
return this.collapse(this.focusNode, this.focusOffset + newText.length);
|
||||
}
|
||||
|
||||
@@ -1447,7 +1422,6 @@ export class SelectionController extends EventTarget {
|
||||
this.#textEditor.root.replaceChildren(newParagraph);
|
||||
return this.collapse(newTextNode, newText.length + 1);
|
||||
}
|
||||
this.#mutations.update(this.focusTextSpan);
|
||||
return this.collapse(this.focusNode, startOffset + newText.length);
|
||||
}
|
||||
|
||||
@@ -1525,8 +1499,6 @@ export class SelectionController extends EventTarget {
|
||||
const currentParagraph = this.focusParagraph;
|
||||
const newParagraph = createEmptyParagraph(this.#currentStyle);
|
||||
currentParagraph.after(newParagraph);
|
||||
this.#mutations.update(currentParagraph);
|
||||
this.#mutations.add(newParagraph);
|
||||
return this.collapse(newParagraph.firstChild.firstChild, 0);
|
||||
}
|
||||
|
||||
@@ -1537,8 +1509,6 @@ export class SelectionController extends EventTarget {
|
||||
const currentParagraph = this.focusParagraph;
|
||||
const newParagraph = createEmptyParagraph(this.#currentStyle);
|
||||
currentParagraph.before(newParagraph);
|
||||
this.#mutations.update(currentParagraph);
|
||||
this.#mutations.add(newParagraph);
|
||||
return this.collapse(currentParagraph.firstChild.firstChild, 0);
|
||||
}
|
||||
|
||||
@@ -1553,8 +1523,6 @@ export class SelectionController extends EventTarget {
|
||||
this.#focusOffset,
|
||||
);
|
||||
this.focusParagraph.after(newParagraph);
|
||||
this.#mutations.update(currentParagraph);
|
||||
this.#mutations.add(newParagraph);
|
||||
return this.collapse(newParagraph.firstChild.firstChild, 0);
|
||||
}
|
||||
|
||||
@@ -1586,10 +1554,6 @@ export class SelectionController extends EventTarget {
|
||||
this.focusOffset,
|
||||
);
|
||||
currentParagraph.after(newParagraph);
|
||||
|
||||
this.#mutations.update(currentParagraph);
|
||||
this.#mutations.add(newParagraph);
|
||||
|
||||
// FIXME: Missing collapse?
|
||||
}
|
||||
|
||||
@@ -1610,7 +1574,6 @@ export class SelectionController extends EventTarget {
|
||||
const previousOffset = isLineBreak(previousTextSpan.firstChild)
|
||||
? 0
|
||||
: previousTextSpan.firstChild.nodeValue?.length || 0;
|
||||
this.#mutations.remove(paragraphToBeRemoved);
|
||||
return this.collapse(previousTextSpan.firstChild, previousOffset);
|
||||
}
|
||||
|
||||
@@ -1632,8 +1595,6 @@ export class SelectionController extends EventTarget {
|
||||
} else {
|
||||
mergeParagraphs(previousParagraph, currentParagraph);
|
||||
}
|
||||
this.#mutations.remove(currentParagraph);
|
||||
this.#mutations.update(previousParagraph);
|
||||
return this.collapse(previousTextSpan.firstChild, previousOffset);
|
||||
}
|
||||
|
||||
@@ -1647,8 +1608,6 @@ export class SelectionController extends EventTarget {
|
||||
return;
|
||||
}
|
||||
mergeParagraphs(this.focusParagraph, nextParagraph);
|
||||
this.#mutations.update(currentParagraph);
|
||||
this.#mutations.remove(nextParagraph);
|
||||
|
||||
// FIXME: Missing collapse?
|
||||
}
|
||||
@@ -1665,7 +1624,6 @@ export class SelectionController extends EventTarget {
|
||||
paragraphToBeRemoved.remove();
|
||||
const nextTextSpan = nextParagraph.firstChild;
|
||||
const nextOffset = this.focusOffset;
|
||||
this.#mutations.remove(paragraphToBeRemoved);
|
||||
return this.collapse(nextTextSpan.firstChild, nextOffset);
|
||||
}
|
||||
|
||||
@@ -1680,7 +1638,6 @@ export class SelectionController extends EventTarget {
|
||||
for (const textSpan of affectedTextSpans) {
|
||||
if (textSpan.textContent === "") {
|
||||
textSpan.remove();
|
||||
this.#mutations.remove(textSpan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1688,7 +1645,6 @@ export class SelectionController extends EventTarget {
|
||||
for (const paragraph of affectedParagraphs) {
|
||||
if (paragraph.children.length === 0) {
|
||||
paragraph.remove();
|
||||
this.#mutations.remove(paragraph);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +581,136 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.textContent).toBe("");
|
||||
});
|
||||
|
||||
test("`insertParagraph` should insert a new paragraph in an empty editor", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockEmpty();
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(textEditorMock, selection);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstChild.firstChild.firstChild,
|
||||
0,
|
||||
);
|
||||
selectionController.insertParagraph();
|
||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||
expect(textEditorMock.root.children.length).toBe(2);
|
||||
expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children.item(0).dataset.itype).toBe("paragraph");
|
||||
expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe("span");
|
||||
expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children.item(1).dataset.itype).toBe("paragraph");
|
||||
expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe(
|
||||
"span",
|
||||
);
|
||||
expect(textEditorMock.root.textContent).toBe("");
|
||||
});
|
||||
|
||||
test("`insertParagraph` should insert a new paragraph after a text", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, World!"]
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstChild.firstChild.firstChild,
|
||||
"Hello, World!".length
|
||||
);
|
||||
selectionController.insertParagraph();
|
||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||
expect(textEditorMock.root.children.length).toBe(2);
|
||||
expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children.item(0).dataset.itype).toBe(
|
||||
"paragraph",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe(
|
||||
"span",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(0).firstChild.textContent).toBe(
|
||||
"Hello, World!",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children.item(1).dataset.itype).toBe(
|
||||
"paragraph",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe(
|
||||
"span",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1).firstChild.firstChild).toBeInstanceOf(
|
||||
HTMLBRElement,
|
||||
);
|
||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
test("`insertParagraph` should insert a new paragraph before a text", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, World!"],
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstChild.firstChild.firstChild,
|
||||
0,
|
||||
);
|
||||
selectionController.insertParagraph();
|
||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||
expect(textEditorMock.root.children.length).toBe(2);
|
||||
expect(textEditorMock.root.children.item(0)).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children.item(0).dataset.itype).toBe(
|
||||
"paragraph",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(0).firstChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
expect(textEditorMock.root.children.item(0).firstChild.dataset.itype).toBe(
|
||||
"span",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(0).firstChild.firstChild).toBeInstanceOf(
|
||||
HTMLBRElement,
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1)).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children.item(1).dataset.itype).toBe(
|
||||
"paragraph",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1).firstChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1).firstChild.dataset.itype).toBe(
|
||||
"span",
|
||||
);
|
||||
expect(textEditorMock.root.children.item(1).firstChild.textContent).toBe(
|
||||
"Hello, World!",
|
||||
);
|
||||
expect(textEditorMock.root.textContent).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, "],
|
||||
@@ -1027,7 +1157,7 @@ describe("SelectionController", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test.skip("`removeSelected` multiple paragraphs", () => {
|
||||
test("`removeSelected` multiple paragraphs", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, "],
|
||||
["\n"],
|
||||
@@ -1392,7 +1522,10 @@ describe("SelectionController", () => {
|
||||
root.firstChild.lastChild.firstChild.nodeValue.length - 3,
|
||||
);
|
||||
selectionController.applyStyles({
|
||||
"font-family": "Montserrat, sans-serif",
|
||||
"font-weight": "bold",
|
||||
"--fills":
|
||||
'[["^ ","~:fill-color","#000000","~:fill-opacity",1],["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]',
|
||||
});
|
||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children.length).toBe(1);
|
||||
@@ -1492,4 +1625,68 @@ describe("SelectionController", () => {
|
||||
"ld!",
|
||||
);
|
||||
});
|
||||
|
||||
test("`selectAll` should select everything", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraphWith(["Hello, "], {
|
||||
"font-style": "italic",
|
||||
}),
|
||||
createParagraphWith(["World!"], {
|
||||
"font-style": "oblique",
|
||||
}),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(textEditorMock, selection);
|
||||
textEditorMock.element.focus();
|
||||
selectionController.selectAll();
|
||||
expect(selectionController.anchorNode).toBe(
|
||||
root.firstChild.firstChild.firstChild
|
||||
);
|
||||
expect(selectionController.focusNode).toBe(
|
||||
root.lastChild.firstChild.firstChild,
|
||||
);
|
||||
});
|
||||
|
||||
test("`cursorToEnd` should move cursor to the end", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraphWith(["Hello, "], {
|
||||
"font-style": "italic",
|
||||
}),
|
||||
createParagraphWith(["World!"], {
|
||||
"font-style": "oblique",
|
||||
}),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(textEditorMock, selection);
|
||||
textEditorMock.element.focus();
|
||||
selectionController.cursorToEnd();
|
||||
expect(selectionController.focusNode).toBe(root.lastChild.firstChild.firstChild);
|
||||
expect(selectionController.focusAtEnd).toBeTruthy();
|
||||
})
|
||||
|
||||
test("`dispose` should release every held reference", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraphWith(["Hello, "], {
|
||||
"font-style": "italic",
|
||||
}),
|
||||
createParagraphWith(["World!"], {
|
||||
"font-style": "oblique",
|
||||
}),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(textEditorMock, selection);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstChild.firstChild.firstChild,
|
||||
0
|
||||
);
|
||||
selectionController.dispose();
|
||||
expect(selectionController.selection).toBe(null);
|
||||
expect(selectionController.currentStyle).toBe(null);
|
||||
expect(selectionController.options).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
## 1.5.0 (Unreleased)
|
||||
|
||||
- **plugin-types**: Added a flags subcontexts with the flag `naturalChildrenOrdering`
|
||||
|
||||
## 1.4.2 (2026-01-21)
|
||||
|
||||
- **plugin-types:** fix atob/btoa functions
|
||||
- **plugin-runtime:** fix atob/btoa functions
|
||||
|
||||
## 1.4.0 (2026-01-21)
|
||||
|
||||
|
||||
18
plugins/libs/plugin-types/index.d.ts
vendored
18
plugins/libs/plugin-types/index.d.ts
vendored
@@ -804,6 +804,11 @@ export interface Context {
|
||||
*/
|
||||
readonly viewport: Viewport;
|
||||
|
||||
/**
|
||||
* Provides flags to customize the API behavior.
|
||||
*/
|
||||
readonly flags: Flags;
|
||||
|
||||
/**
|
||||
* Context encapsulating the history operations
|
||||
*
|
||||
@@ -1679,6 +1684,19 @@ export interface Fill {
|
||||
fillImage?: ImageData;
|
||||
}
|
||||
|
||||
/**
|
||||
* This subcontext allows the API o change certain defaults
|
||||
*/
|
||||
export interface Flags {
|
||||
/**
|
||||
* If `true` the .children property will be always sorted in the z-index ordering.
|
||||
* Also, appendChild method will be append the children in the top-most position.
|
||||
* The insertchild method is changed acordingly to respect this ordering.
|
||||
* Defaults to false
|
||||
*/
|
||||
naturalChildOrdering: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a flexible layout configuration in Penpot.
|
||||
* This interface extends `CommonLayout` and includes properties for defining the direction,
|
||||
|
||||
@@ -1,31 +1,32 @@
|
||||
import type {
|
||||
Penpot,
|
||||
EventsMap,
|
||||
Page,
|
||||
Shape,
|
||||
Rectangle,
|
||||
Board,
|
||||
Group,
|
||||
Viewport,
|
||||
Text,
|
||||
File,
|
||||
Theme,
|
||||
LibraryContext,
|
||||
Ellipse,
|
||||
Path,
|
||||
BooleanType,
|
||||
Boolean,
|
||||
User,
|
||||
ActiveUser,
|
||||
FontsContext,
|
||||
SvgRaw,
|
||||
Board,
|
||||
Boolean,
|
||||
BooleanType,
|
||||
Color,
|
||||
ColorShapeInfo,
|
||||
Ellipse,
|
||||
EventsMap,
|
||||
File,
|
||||
Flags,
|
||||
FontsContext,
|
||||
Group,
|
||||
HistoryContext,
|
||||
LocalStorage,
|
||||
VariantContainer,
|
||||
LibraryComponent,
|
||||
LibraryContext,
|
||||
LibraryVariantComponent,
|
||||
LocalStorage,
|
||||
Page,
|
||||
Path,
|
||||
Penpot,
|
||||
Rectangle,
|
||||
Shape,
|
||||
SvgRaw,
|
||||
Text,
|
||||
Theme,
|
||||
User,
|
||||
VariantContainer,
|
||||
Viewport,
|
||||
} from '@penpot/plugin-types';
|
||||
|
||||
import { Permissions } from '../models/manifest.model.js';
|
||||
@@ -193,6 +194,10 @@ export function createApi(
|
||||
return plugin.context.fonts;
|
||||
},
|
||||
|
||||
get flags(): Flags {
|
||||
return plugin.context.flags;
|
||||
},
|
||||
|
||||
get currentUser(): User {
|
||||
checkPermission('user:read');
|
||||
return plugin.context.currentUser;
|
||||
|
||||
@@ -27,8 +27,8 @@ fn draw_stroke_on_rect(
|
||||
// - The same rect if it's a center stroke
|
||||
// - A bigger rect if it's an outer stroke
|
||||
// - A smaller rect if it's an outer stroke
|
||||
let stroke_rect = stroke.outer_rect(rect);
|
||||
let mut paint = stroke.to_paint(selrect, svg_attrs, scale, antialias);
|
||||
let stroke_rect = stroke.aligned_rect(rect, scale);
|
||||
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
|
||||
|
||||
// Apply both blur and shadow filters if present, composing them if necessary.
|
||||
let filter = compose_filters(blur, shadow);
|
||||
@@ -63,8 +63,8 @@ fn draw_stroke_on_circle(
|
||||
// - The same oval if it's a center stroke
|
||||
// - A bigger oval if it's an outer stroke
|
||||
// - A smaller oval if it's an outer stroke
|
||||
let stroke_rect = stroke.outer_rect(rect);
|
||||
let mut paint = stroke.to_paint(selrect, svg_attrs, scale, antialias);
|
||||
let stroke_rect = stroke.aligned_rect(rect, scale);
|
||||
let mut paint = stroke.to_paint(selrect, svg_attrs, antialias);
|
||||
|
||||
// Apply both blur and shadow filters if present, composing them if necessary.
|
||||
let filter = compose_filters(blur, shadow);
|
||||
@@ -131,7 +131,6 @@ pub fn draw_stroke_on_path(
|
||||
selrect: &Rect,
|
||||
path_transform: Option<&Matrix>,
|
||||
svg_attrs: Option<&SvgAttrs>,
|
||||
scale: f32,
|
||||
shadow: Option<&ImageFilter>,
|
||||
blur: Option<&ImageFilter>,
|
||||
antialias: bool,
|
||||
@@ -142,7 +141,7 @@ pub fn draw_stroke_on_path(
|
||||
let is_open = path.is_open();
|
||||
|
||||
let mut paint: skia_safe::Handle<_> =
|
||||
stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale, antialias);
|
||||
stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias);
|
||||
|
||||
let filter = compose_filters(blur, shadow);
|
||||
paint.set_image_filter(filter);
|
||||
@@ -166,7 +165,6 @@ pub fn draw_stroke_on_path(
|
||||
canvas,
|
||||
is_open,
|
||||
svg_attrs,
|
||||
scale,
|
||||
blur,
|
||||
antialias,
|
||||
);
|
||||
@@ -218,7 +216,6 @@ fn handle_stroke_caps(
|
||||
canvas: &skia::Canvas,
|
||||
is_open: bool,
|
||||
svg_attrs: Option<&SvgAttrs>,
|
||||
scale: f32,
|
||||
blur: Option<&ImageFilter>,
|
||||
antialias: bool,
|
||||
) {
|
||||
@@ -233,8 +230,7 @@ fn handle_stroke_caps(
|
||||
let first_point = points.first().unwrap();
|
||||
let last_point = points.last().unwrap();
|
||||
|
||||
let mut paint_stroke =
|
||||
stroke.to_stroked_paint(is_open, selrect, svg_attrs, scale, antialias);
|
||||
let mut paint_stroke = stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias);
|
||||
|
||||
if let Some(filter) = blur {
|
||||
paint_stroke.set_image_filter(filter.clone());
|
||||
@@ -405,7 +401,7 @@ fn draw_image_stroke_in_container(
|
||||
|
||||
// Draw the stroke based on the shape type, we are using this stroke as
|
||||
// a "selector" of the area of the image we want to show.
|
||||
let outer_rect = stroke.outer_rect(container);
|
||||
let outer_rect = stroke.aligned_rect(container, scale);
|
||||
|
||||
match &shape.shape_type {
|
||||
shape_type @ (Type::Rect(_) | Type::Frame(_)) => {
|
||||
@@ -450,8 +446,7 @@ fn draw_image_stroke_in_container(
|
||||
}
|
||||
}
|
||||
let is_open = p.is_open();
|
||||
let mut paint =
|
||||
stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, scale, antialias);
|
||||
let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, antialias);
|
||||
canvas.draw_path(&path, &paint);
|
||||
if stroke.render_kind(is_open) == StrokeKind::Outer {
|
||||
// Small extra inner stroke to overlap with the fill
|
||||
@@ -466,7 +461,6 @@ fn draw_image_stroke_in_container(
|
||||
canvas,
|
||||
is_open,
|
||||
svg_attrs,
|
||||
scale,
|
||||
shape.image_filter(1.).as_ref(),
|
||||
antialias,
|
||||
);
|
||||
@@ -662,7 +656,6 @@ fn render_internal(
|
||||
&selrect,
|
||||
path_transform.as_ref(),
|
||||
svg_attrs,
|
||||
scale,
|
||||
shadow,
|
||||
shape.image_filter(1.).as_ref(),
|
||||
antialias,
|
||||
@@ -685,14 +678,13 @@ pub fn render_text_paths(
|
||||
shadow: Option<&ImageFilter>,
|
||||
antialias: bool,
|
||||
) {
|
||||
let scale = render_state.get_scale();
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::Strokes));
|
||||
let selrect = &shape.selrect;
|
||||
let svg_attrs = shape.svg_attrs.as_ref();
|
||||
let mut paint: skia_safe::Handle<_> =
|
||||
stroke.to_text_stroked_paint(false, selrect, svg_attrs, scale, antialias);
|
||||
stroke.to_text_stroked_paint(false, selrect, svg_attrs, antialias);
|
||||
|
||||
if let Some(filter) = shadow {
|
||||
paint.set_image_filter(filter.clone());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::math::is_close_to;
|
||||
use crate::shapes::fills::{Fill, SolidColor};
|
||||
use skia_safe::{self as skia, Rect};
|
||||
|
||||
@@ -144,6 +145,15 @@ impl Stroke {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn aligned_rect(&self, rect: &Rect, scale: f32) -> Rect {
|
||||
let stroke_rect = self.outer_rect(rect);
|
||||
if self.kind != StrokeKind::Center {
|
||||
return stroke_rect;
|
||||
}
|
||||
|
||||
align_rect_to_half_pixel(&stroke_rect, self.width, scale)
|
||||
}
|
||||
|
||||
pub fn outer_corners(&self, corners: &Corners) -> Corners {
|
||||
let offset = match self.kind {
|
||||
StrokeKind::Center => 0.0,
|
||||
@@ -162,7 +172,6 @@ impl Stroke {
|
||||
&self,
|
||||
rect: &Rect,
|
||||
svg_attrs: Option<&SvgAttrs>,
|
||||
scale: f32,
|
||||
antialias: bool,
|
||||
) -> skia::Paint {
|
||||
let mut paint = self.fill.to_paint(rect, antialias);
|
||||
@@ -171,7 +180,7 @@ impl Stroke {
|
||||
let width = match self.kind {
|
||||
StrokeKind::Inner => self.width,
|
||||
StrokeKind::Center => self.width,
|
||||
StrokeKind::Outer => self.width + (1. / scale),
|
||||
StrokeKind::Outer => self.width,
|
||||
};
|
||||
|
||||
paint.set_stroke_width(width);
|
||||
@@ -230,10 +239,9 @@ impl Stroke {
|
||||
is_open: bool,
|
||||
rect: &Rect,
|
||||
svg_attrs: Option<&SvgAttrs>,
|
||||
scale: f32,
|
||||
antialias: bool,
|
||||
) -> skia::Paint {
|
||||
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
|
||||
let mut paint = self.to_paint(rect, svg_attrs, antialias);
|
||||
match self.render_kind(is_open) {
|
||||
StrokeKind::Inner => {
|
||||
paint.set_stroke_width(2. * paint.stroke_width());
|
||||
@@ -254,10 +262,9 @@ impl Stroke {
|
||||
is_open: bool,
|
||||
rect: &Rect,
|
||||
svg_attrs: Option<&SvgAttrs>,
|
||||
scale: f32,
|
||||
antialias: bool,
|
||||
) -> skia::Paint {
|
||||
let mut paint = self.to_paint(rect, svg_attrs, scale, antialias);
|
||||
let mut paint = self.to_paint(rect, svg_attrs, antialias);
|
||||
match self.render_kind(is_open) {
|
||||
StrokeKind::Inner => {
|
||||
paint.set_stroke_width(2. * paint.stroke_width());
|
||||
@@ -284,6 +291,38 @@ impl Stroke {
|
||||
}
|
||||
}
|
||||
|
||||
fn align_rect_to_half_pixel(rect: &Rect, stroke_width: f32, scale: f32) -> Rect {
|
||||
if scale <= 0.0 {
|
||||
return *rect;
|
||||
}
|
||||
|
||||
let stroke_pixels = stroke_width * scale;
|
||||
let stroke_pixels_rounded = stroke_pixels.round();
|
||||
if !is_close_to(stroke_pixels, stroke_pixels_rounded) {
|
||||
return *rect;
|
||||
}
|
||||
|
||||
if (stroke_pixels_rounded as i32) % 2 == 0 {
|
||||
return *rect;
|
||||
}
|
||||
|
||||
let left_px = rect.left * scale;
|
||||
let top_px = rect.top * scale;
|
||||
let target_frac = 0.5;
|
||||
let dx_px = target_frac - (left_px - left_px.floor());
|
||||
let dy_px = target_frac - (top_px - top_px.floor());
|
||||
|
||||
if is_close_to(dx_px, 0.0) && is_close_to(dy_px, 0.0) {
|
||||
return *rect;
|
||||
}
|
||||
|
||||
Rect::from_xywh(
|
||||
rect.left + (dx_px / scale),
|
||||
rect.top + (dy_px / scale),
|
||||
rect.width(),
|
||||
rect.height(),
|
||||
)
|
||||
}
|
||||
fn cap_margin_for_cap(cap: Option<StrokeCap>, width: f32) -> f32 {
|
||||
match cap {
|
||||
Some(StrokeCap::LineArrow)
|
||||
|
||||
Reference in New Issue
Block a user