diff --git a/CHANGES.md b/CHANGES.md
index 5514733acb..44a4d950d3 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -84,6 +84,7 @@ example. It's still usable as before, we just removed the example.
- Add new shape validation mechanism for shapes [Github #7696](https://github.com/penpot/penpot/pull/7696)
- Apply color tokens from sidebar [Taiga #11353](https://tree.taiga.io/project/penpot/us/11353)
- Display tokens in the inspect tab [Taiga #9313](https://tree.taiga.io/project/penpot/us/9313)
+- Refactor clipboard behavior to assess some minor inconsistencies and make pasting binary data faster. [Taiga #12571](https://tree.taiga.io/project/penpot/task/12571)
### :bug: Bugs fixed
@@ -98,6 +99,7 @@ example. It's still usable as before, we just removed the example.
- Fix shortcut conflict in text editor (increase/decrease font size vs word selection)
- Fix problem with plugins generating code for pages different than current one [Taiga #12312](https://tree.taiga.io/project/penpot/issue/12312)
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
+- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
## 2.11.1
diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc
index ec3a3e7bf9..70e36bfc2f 100644
--- a/common/src/app/common/types/tokens_lib.cljc
+++ b/common/src/app/common/types/tokens_lib.cljc
@@ -1679,7 +1679,7 @@ Will return a value that matches this schema:
["id" {:optional true} :string]
["name" :string]
["description" :string]
- ["isSource" :boolean]
+ ["isSource" {:optional true} :boolean]
["selectedTokenSets"
[:map-of :string [:enum "enabled" "disabled"]]]]]]
["$metadata" {:optional true}
diff --git a/docker/devenv/docker-compose.yaml b/docker/devenv/docker-compose.yaml
index 33e2036ca8..89580ef8b4 100644
--- a/docker/devenv/docker-compose.yaml
+++ b/docker/devenv/docker-compose.yaml
@@ -115,6 +115,11 @@ services:
volumes:
- "valkey_data:/data"
+ networks:
+ default:
+ aliases:
+ - redis
+
mailer:
image: sj26/mailcatcher:latest
restart: always
@@ -123,6 +128,12 @@ services:
ports:
- "1080:1080"
+ networks:
+ default:
+ aliases:
+ - mailer
+
+
# https://github.com/rroemhild/docker-test-openldap
ldap:
image: rroemhild/test-openldap:2.1
@@ -136,3 +147,9 @@ services:
nofile:
soft: 1024
hard: 1024
+
+ networks:
+ default:
+ aliases:
+ - ldap
+
diff --git a/docs/technical-guide/developer/abstraction-levels.md b/docs/technical-guide/developer/abstraction-levels.md
index b2cd59d0b6..361b007691 100644
--- a/docs/technical-guide/developer/abstraction-levels.md
+++ b/docs/technical-guide/developer/abstraction-levels.md
@@ -1,5 +1,6 @@
---
title: 3.07. Abstraction levels
+desc: "Penpot Technical Guide: organize data and logic in clear abstraction layers—ADTs, file ops, event-sourced changes, business rules, and data events."
---
# Code organization in abstraction levels
diff --git a/docs/technical-guide/developer/backend.md b/docs/technical-guide/developer/backend.md
index 0f60c928d7..f2e7bc4d15 100644
--- a/docs/technical-guide/developer/backend.md
+++ b/docs/technical-guide/developer/backend.md
@@ -1,5 +1,6 @@
---
title: 3.06. Backend Guide
+desc: "Penpot Technical Guide: Backend basics - REPL setup, loading fixtures, database migrations, and clj-kondo linting to speed development workflows."
---
# Backend guide #
diff --git a/docs/technical-guide/getting-started/elestio.md b/docs/technical-guide/getting-started/elestio.md
index 0723ba0f16..b6dc49af2a 100644
--- a/docs/technical-guide/getting-started/elestio.md
+++ b/docs/technical-guide/getting-started/elestio.md
@@ -1,5 +1,6 @@
---
title: 1.2 Install with Elestio
+desc: "Step-by-step guide to deploy a self-hosted Penpot on Elestio: 3-minute setup, managed DNS/SMTP/SSL/backups, Docker Compose config, updates & support."
---
# Install with Elestio
diff --git a/docs/user-guide/design-systems/design-tokens.njk b/docs/user-guide/design-systems/design-tokens.njk
index ec8a21aa0b..3a328fe5a8 100644
--- a/docs/user-guide/design-systems/design-tokens.njk
+++ b/docs/user-guide/design-systems/design-tokens.njk
@@ -1,6 +1,7 @@
---
title: Design Tokens
order: 5
+desc: Learn how to create, manage and apply Penpot Design Tokens using W3C DTCG format, with sets, themes, aliases, equations and JSON import/export.
---
Design Tokens
diff --git a/frontend/src/app/main/data/auth.cljs b/frontend/src/app/main/data/auth.cljs
index cfa11e4263..a933b83590 100644
--- a/frontend/src/app/main/data/auth.cljs
+++ b/frontend/src/app/main/data/auth.cljs
@@ -195,7 +195,9 @@
(ptk/reify ::login-from-token
ptk/WatchEvent
(watch [_ _ _]
- (->> (rx/of (logged-in (with-meta profile {::ev/source "login-with-token"})))
+ (->> (dp/on-fetch-profile-success profile)
+ (rx/map (fn [profile]
+ (logged-in (with-meta profile {::ev/source "login-with-token"}))))
;; NOTE: we need this to be asynchronous because the effect
;; should be called before proceed with the login process
(rx/observe-on :async)))))
diff --git a/frontend/src/app/main/data/profile.cljs b/frontend/src/app/main/data/profile.cljs
index 01dd5ba7d3..1a77f6b9d3 100644
--- a/frontend/src/app/main/data/profile.cljs
+++ b/frontend/src/app/main/data/profile.cljs
@@ -77,22 +77,25 @@
(rx/of (rt/nav-raw :href href)))
(rx/throw cause))))
+(defn on-fetch-profile-success
+ [profile]
+ (if (and (contains? cf/flags :subscriptions)
+ (is-authenticated? profile))
+ (->> (rp/cmd! :get-subscription-usage {})
+ (rx/map (fn [{:keys [editors]}]
+ (update-in profile [:props :subscription] assoc :editors editors)))
+ (rx/catch (fn [cause]
+ (js/console.error "unexpected error on obtaining subscription usage" cause)
+ (rx/of profile))))
+ (rx/of profile)))
+
(defn fetch-profile
[]
(ptk/reify ::fetch-profile
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-profile)
- (rx/mapcat (fn [profile]
- (if (and (contains? cf/flags :subscriptions)
- (is-authenticated? profile))
- (->> (rp/cmd! :get-subscription-usage {})
- (rx/map (fn [{:keys [editors]}]
- (update-in profile [:props :subscription] assoc :editors editors)))
- (rx/catch (fn [cause]
- (js/console.error "unexpected error on obtaining subscription usage" cause)
- (rx/of profile))))
- (rx/of profile))))
+ (rx/mapcat on-fetch-profile-success)
(rx/map (partial ptk/data-event ::profile-fetched))
(rx/catch on-fetch-profile-exception)))))
diff --git a/frontend/src/app/main/data/workspace/clipboard.cljs b/frontend/src/app/main/data/workspace/clipboard.cljs
index c850b014e5..9e95f9c769 100644
--- a/frontend/src/app/main/data/workspace/clipboard.cljs
+++ b/frontend/src/app/main/data/workspace/clipboard.cljs
@@ -254,6 +254,10 @@
(declare ^:private paste-svg-text)
(declare ^:private paste-shapes)
+(def ^:private default-options
+ #js {:decodeTransit t/decode-str
+ :allowHTMLPaste (features/active-feature? @st/state "text-editor/v2-html-paste")})
+
(defn create-paste-from-blob
[in-viewport?]
(fn [blob]
@@ -290,7 +294,7 @@
(ptk/reify ::paste-from-clipboard
ptk/WatchEvent
(watch [_ _ _]
- (->> (clipboard/from-navigator)
+ (->> (clipboard/from-navigator default-options)
(rx/mapcat default-paste-from-blob)
(rx/take 1)))))
@@ -308,7 +312,7 @@
;; we forbid that scenario so the default behaviour is executed
(if is-editing?
(rx/empty)
- (->> (clipboard/from-synthetic-clipboard-event event)
+ (->> (clipboard/from-synthetic-clipboard-event event default-options)
(rx/mapcat (create-paste-from-blob in-viewport?))))))))
(defn copy-selected-svg
@@ -478,7 +482,7 @@
(js/console.error "Clipboard error:" cause))
(rx/empty)))]
- (->> (clipboard/from-navigator)
+ (->> (clipboard/from-navigator default-options)
(rx/mapcat #(.text %))
(rx/map decode-entry)
(rx/take 1)
diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs
index 695e9fb44e..e6cc837b52 100644
--- a/frontend/src/app/main/ui/dashboard/grid.cljs
+++ b/frontend/src/app/main/ui/dashboard/grid.cljs
@@ -60,10 +60,10 @@
(defn render-thumbnail
[file-id revn]
(if (features/active-feature? @st/state "render-wasm/v1")
- (->> (mw/ask! {:cmd :thumbnails/generate-for-file-wasm
- :revn revn
- :file-id file-id
- :width thumbnail-width}))
+ (mw/ask! {:cmd :thumbnails/generate-for-file-wasm
+ :revn revn
+ :file-id file-id
+ :width thumbnail-width})
(->> (mw/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id
diff --git a/frontend/src/app/main/ui/settings/subscription.cljs b/frontend/src/app/main/ui/settings/subscription.cljs
index c0ade1f61c..b3fa6e2d44 100644
--- a/frontend/src/app/main/ui/settings/subscription.cljs
+++ b/frontend/src/app/main/ui/settings/subscription.cljs
@@ -83,74 +83,101 @@
::mf/register-as :management-dialog}
[{:keys [subscription-type current-subscription editors subscribe-to-trial]}]
- (let [unlimited-modal-step* (mf/use-state 1)
- unlimited-modal-step (deref unlimited-modal-step*)
- subscription-name (if subscribe-to-trial
- (if (= subscription-type "unlimited")
- (tr "subscription.settings.unlimited-trial")
- (tr "subscription.settings.enterprise-trial"))
- (case subscription-type
- "professional" (tr "subscription.settings.professional")
- "unlimited" (tr "subscription.settings.unlimited")
- "enterprise" (tr "subscription.settings.enterprise")))
- min-editors (if (seq editors) (count editors) 1)
- initial (mf/with-memo [min-editors]
- {:min-members min-editors})
- form (fm/use-form :schema (schema:seats-form min-editors)
- :initial initial)
- submit-in-progress* (mf/use-state false)
- subscribe-to-unlimited (mf/use-fn
- (mf/deps form)
- (fn [add-payment-details]
- (when (not @submit-in-progress*)
- (let [data (:clean-data @form)
- return-url (-> (rt/get-current-href) (rt/encode-url))
- href (if add-payment-details
- (dm/str "payments/subscriptions/create?type=unlimited&show=true&quantity=" (:min-members data) "&returnUrl=" return-url)
- (dm/str "payments/subscriptions/create?type=unlimited&show=false&quantity=" (:min-members data) "&returnUrl=" return-url))]
- (reset! submit-in-progress* true)
- (reset! form nil)
- (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
- :type "unlimited"
- :quantity (:min-members data)})
- (rt/nav-raw :href href))))))
+ (let [unlimited-modal-step*
+ (mf/use-state 1)
- subscribe-to-enterprise (mf/use-fn
- (fn []
- (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
- :type "enterprise"}))
- (let [return-url (-> (rt/get-current-href) (rt/encode-url))
- href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)]
- (st/emit! (rt/nav-raw :href href)))))
+ unlimited-modal-step
+ (deref unlimited-modal-step*)
- handle-accept-dialog (mf/use-fn
- (fn []
- (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
- ::ev/origin "settings"
- :section "subscription-management-modal"}))
- (let [current-href (rt/get-current-href)
- returnUrl (js/encodeURIComponent current-href)
- href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
- (st/emit! (rt/nav-raw :href href)))
- (modal/hide!)))
- handle-close-dialog (mf/use-fn
- (fn []
- (st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"}))
- (modal/hide!)))
+ subscription-name
+ (if subscribe-to-trial
+ (if (= subscription-type "unlimited")
+ (tr "subscription.settings.unlimited-trial")
+ (tr "subscription.settings.enterprise-trial"))
+ (case subscription-type
+ "professional" (tr "subscription.settings.professional")
+ "unlimited" (tr "subscription.settings.unlimited")
+ "enterprise" (tr "subscription.settings.enterprise")))
- handle-unlimited-modal-step (mf/use-fn
- (mf/deps unlimited-modal-step)
- (fn []
- (if (= unlimited-modal-step 1)
- (reset! unlimited-modal-step* 2)
- (reset! unlimited-modal-step* 1))))
+ min-editors
+ (if (seq editors) (count editors) 1)
- show-editors-list* (mf/use-state false)
- show-editors-list (deref show-editors-list*)
- handle-click (mf/use-fn
- (fn [event]
- (dom/stop-propagation event)
- (swap! show-editors-list* not)))]
+ initial
+ (mf/with-memo [min-editors]
+ {:min-members min-editors})
+
+ form
+ (fm/use-form :schema (schema:seats-form min-editors)
+ :initial initial)
+
+ form-data-min-editors
+ (-> @form :clean-data :min-members)
+
+ submit-in-progress*
+ (mf/use-state false)
+
+ subscribe-to-unlimited
+ (mf/use-fn
+ (mf/deps form-data-min-editors)
+ (fn [add-payment-details]
+ (when (not @submit-in-progress*)
+ (let [return-url (-> (rt/get-current-href) (rt/encode-url))
+ href (if add-payment-details
+ (dm/str "payments/subscriptions/create?type=unlimited&show=true&quantity=" form-data-min-editors "&returnUrl=" return-url)
+ (dm/str "payments/subscriptions/create?type=unlimited&show=false&quantity=" form-data-min-editors "&returnUrl=" return-url))]
+ (reset! submit-in-progress* true)
+ (reset! form nil)
+ (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
+ :type "unlimited"
+ :quantity form-data-min-editors})
+ (rt/nav-raw :href href))))))
+
+ subscribe-to-enterprise
+ (mf/use-fn
+ (fn []
+ (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
+ :type "enterprise"}))
+ (let [return-url (-> (rt/get-current-href) (rt/encode-url))
+ href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)]
+ (st/emit! (rt/nav-raw :href href)))))
+
+ handle-accept-dialog
+ (mf/use-fn
+ (fn []
+ (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
+ ::ev/origin "settings"
+ :section "subscription-management-modal"}))
+ (let [current-href (rt/get-current-href)
+ returnUrl (js/encodeURIComponent current-href)
+ href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
+ (st/emit! (rt/nav-raw :href href)))
+ (modal/hide!)))
+
+ handle-close-dialog
+ (mf/use-fn
+ (fn []
+ (st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"}))
+ (modal/hide!)))
+
+ handle-unlimited-modal-step
+ (mf/use-fn
+ (mf/deps unlimited-modal-step)
+ (fn []
+ (if (= unlimited-modal-step 1)
+ (reset! unlimited-modal-step* 2)
+ (reset! unlimited-modal-step* 1))))
+
+ show-editors-list*
+ (mf/use-state false)
+
+ show-editors-list
+ (deref show-editors-list*)
+
+ handle-click
+ (mf/use-fn
+ (fn [event]
+ (dom/stop-propagation event)
+ (swap! show-editors-list* not)))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)}
@@ -246,7 +273,7 @@
[:input
{:class (stl/css :cancel-button)
:type "button"
- :value (tr "ubscription.settings.management-dialog.step-2-skip-button")
+ :value (tr "subscription.settings.management-dialog.step-2-skip-button")
:on-click #(subscribe-to-unlimited false)}]
[:input
diff --git a/frontend/src/app/main/ui/settings/subscription.scss b/frontend/src/app/main/ui/settings/subscription.scss
index 5bb2443551..e86d54cd16 100644
--- a/frontend/src/app/main/ui/settings/subscription.scss
+++ b/frontend/src/app/main/ui/settings/subscription.scss
@@ -8,10 +8,12 @@
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
+@use "ds/_sizes.scss" as *;
+@use "ds/_utils.scss" as *;
.dashboard-section {
display: flex;
- width: 100%;
+ inline-size: 100%;
justify-content: center;
align-items: center;
}
@@ -20,10 +22,10 @@
display: flex;
justify-content: center;
flex-direction: column;
- max-width: deprecated.$s-500;
+ max-inline-size: $sz-500;
margin-block-end: var(--sp-xxxl);
- width: deprecated.$s-580;
- margin: deprecated.$s-92 auto deprecated.$s-120 auto;
+ inline-size: px2rem(580);
+ margin: px2rem(92) auto px2rem(120) auto;
justify-content: center;
}
@@ -98,8 +100,8 @@
.plan-title-icon {
@extend .button-icon;
stroke: var(--color-foreground-primary);
- height: var(--sp-xl);
- width: var(--sp-xl);
+ block-size: var(--sp-xl);
+ inline-size: var(--sp-xl);
border-radius: 6px;
border: 1.75px solid var(--color-foreground-primary);
stroke-width: 2.25px;
@@ -140,7 +142,7 @@
}
.other-subscriptions {
- margin-block-start: deprecated.$s-52;
+ margin-block-start: px2rem(52);
}
.cta-button {
@@ -155,8 +157,8 @@
.cta-button svg {
@extend .button-icon;
- height: var(--sp-l);
- width: var(--sp-l);
+ block-size: var(--sp-l);
+ inline-size: var(--sp-l);
stroke: var(--color-accent-primary);
margin-inline-start: var(--sp-xs);
}
@@ -179,14 +181,12 @@
.modal-dialog {
@extend .modal-container-base;
- display: grid;
- grid-template-rows: auto 1fr auto;
- max-height: initial;
- min-width: deprecated.$s-548;
+ max-block-size: initial;
+ min-inline-size: px2rem(548);
}
.modal-dialog.subscription-success {
- min-width: deprecated.$s-648;
+ min-inline-size: px2rem(648);
}
.close-btn {
@@ -232,11 +232,11 @@
.modal-success-content {
display: flex;
- gap: deprecated.$s-40;
+ gap: $sz-40;
}
.modal-footer {
- margin-block-start: deprecated.$s-40;
+ margin-block-start: $sz-40;
}
.action-buttons {
@@ -249,23 +249,28 @@
.primary-button {
@extend .modal-accept-btn;
+ min-block-size: $sz-32;
+ block-size: auto;
}
.cancel-button {
@extend .modal-cancel-btn;
+ min-block-size: $sz-32;
+ white-space: break-spaces;
+ block-size: auto;
}
.modal-start {
display: flex;
justify-content: center;
- max-width: deprecated.$s-220;
+ max-inline-size: $sz-224;
svg {
- width: 100%;
- height: auto;
+ inline-size: 100%;
+ block-size: auto;
}
- @media (max-width: 992px) {
+ @media (max-inline-size: 992px) {
display: none;
}
}
@@ -285,19 +290,19 @@
list-style-position: inside;
list-style-type: none;
margin-inline-start: var(--sp-xl);
- max-height: deprecated.$s-216;
+ max-block-size: px2rem(216);
overflow-y: auto;
}
.input-field {
--input-icon-padding: var(--sp-s);
- width: deprecated.$s-80;
+ inline-size: px2rem(80);
}
.error-message {
@include t.use-typography("body-small");
color: var(--color-foreground-error);
- margin-block-start: deprecated.$s-8;
+ margin-block-start: var(--sp-s);
}
.editors-wrapper {
@@ -316,7 +321,7 @@
@include t.use-typography("body-small");
background-color: var(--color-background-tertiary);
border-radius: var(--sp-s);
- margin-block-start: deprecated.$s-40;
+ margin-block-start: $sz-40;
padding-block: var(--sp-s);
padding-inline: var(--sp-m);
}
diff --git a/frontend/src/app/render_wasm/api.cljs b/frontend/src/app/render_wasm/api.cljs
index dcae37fd02..ba5525791e 100644
--- a/frontend/src/app/render_wasm/api.cljs
+++ b/frontend/src/app/render_wasm/api.cljs
@@ -128,11 +128,12 @@
(defn update-text-rect!
[id]
- (mw/emit!
- {:cmd :index/update-text-rect
- :page-id (:current-page-id @st/state)
- :shape-id id
- :dimensions (get-text-dimensions id)}))
+ (when wasm/context-initialized?
+ (mw/emit!
+ {:cmd :index/update-text-rect
+ :page-id (:current-page-id @st/state)
+ :shape-id id
+ :dimensions (get-text-dimensions id)})))
(defn- ensure-text-content
@@ -198,70 +199,71 @@
(defn set-shape-children
[children]
(perf/begin-measure "set-shape-children")
- (case (count children)
- 0
- (h/call wasm/internal-module "_set_children_0")
+ (let [children (into [] (filter uuid?) children)]
+ (case (count children)
+ 0
+ (h/call wasm/internal-module "_set_children_0")
- 1
- (let [[c1] children
- c1 (uuid/get-u32 c1)]
- (h/call wasm/internal-module "_set_children_1"
- (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)))
+ 1
+ (let [[c1] children
+ c1 (uuid/get-u32 c1)]
+ (h/call wasm/internal-module "_set_children_1"
+ (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)))
- 2
- (let [[c1 c2] children
- c1 (uuid/get-u32 c1)
- c2 (uuid/get-u32 c2)]
- (h/call wasm/internal-module "_set_children_2"
- (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
- (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)))
+ 2
+ (let [[c1 c2] children
+ c1 (uuid/get-u32 c1)
+ c2 (uuid/get-u32 c2)]
+ (h/call wasm/internal-module "_set_children_2"
+ (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
+ (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)))
- 3
- (let [[c1 c2 c3] children
- c1 (uuid/get-u32 c1)
- c2 (uuid/get-u32 c2)
- c3 (uuid/get-u32 c3)]
- (h/call wasm/internal-module "_set_children_3"
- (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
- (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
- (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)))
+ 3
+ (let [[c1 c2 c3] children
+ c1 (uuid/get-u32 c1)
+ c2 (uuid/get-u32 c2)
+ c3 (uuid/get-u32 c3)]
+ (h/call wasm/internal-module "_set_children_3"
+ (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
+ (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
+ (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)))
- 4
- (let [[c1 c2 c3 c4] children
- c1 (uuid/get-u32 c1)
- c2 (uuid/get-u32 c2)
- c3 (uuid/get-u32 c3)
- c4 (uuid/get-u32 c4)]
- (h/call wasm/internal-module "_set_children_4"
- (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
- (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
- (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
- (aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)))
+ 4
+ (let [[c1 c2 c3 c4] children
+ c1 (uuid/get-u32 c1)
+ c2 (uuid/get-u32 c2)
+ c3 (uuid/get-u32 c3)
+ c4 (uuid/get-u32 c4)]
+ (h/call wasm/internal-module "_set_children_4"
+ (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
+ (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
+ (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
+ (aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)))
- 5
- (let [[c1 c2 c3 c4 c5] children
- c1 (uuid/get-u32 c1)
- c2 (uuid/get-u32 c2)
- c3 (uuid/get-u32 c3)
- c4 (uuid/get-u32 c4)
- c5 (uuid/get-u32 c5)]
- (h/call wasm/internal-module "_set_children_5"
- (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
- (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
- (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
- (aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)
- (aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3)))
+ 5
+ (let [[c1 c2 c3 c4 c5] children
+ c1 (uuid/get-u32 c1)
+ c2 (uuid/get-u32 c2)
+ c3 (uuid/get-u32 c3)
+ c4 (uuid/get-u32 c4)
+ c5 (uuid/get-u32 c5)]
+ (h/call wasm/internal-module "_set_children_5"
+ (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)
+ (aget c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
+ (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)
+ (aget c4 0) (aget c4 1) (aget c4 2) (aget c4 3)
+ (aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3)))
- ;; Dynamic call for children > 5
- (let [heap (mem/get-heap-u32)
- size (mem/get-alloc-size children UUID-U8-SIZE)
- offset (mem/alloc->offset-32 size)]
- (reduce
- (fn [offset id]
- (mem.h32/write-uuid offset heap id))
- offset
- children)
- (h/call wasm/internal-module "_set_children")))
+ ;; Dynamic call for children > 5
+ (let [heap (mem/get-heap-u32)
+ size (mem/get-alloc-size children UUID-U8-SIZE)
+ offset (mem/alloc->offset-32 size)]
+ (reduce
+ (fn [offset id]
+ (mem.h32/write-uuid offset heap id))
+ offset
+ children)
+ (h/call wasm/internal-module "_set_children"))))
(perf/end-measure "set-shape-children")
nil)
@@ -468,14 +470,14 @@
[attrs]
(let [style (:style attrs)
;; Filter to only supported attributes
- allowed-keys #{:fill :fillRule :strokeLinecap :strokeLinejoin}
+ allowed-keys #{:fill :fillRule :fill-rule :strokeLinecap :stroke-linecap :strokeLinejoin :stroke-linejoin}
attrs (-> attrs
(dissoc :style)
(merge style)
(select-keys allowed-keys))
- fill-rule (-> attrs :fillRule sr/translate-fill-rule)
- stroke-linecap (-> attrs :strokeLinecap sr/translate-stroke-linecap)
- stroke-linejoin (-> attrs :strokeLinejoin sr/translate-stroke-linejoin)
+ fill-rule (or (-> attrs :fill-rule sr/translate-fill-rule) (-> attrs :fillRule sr/translate-fill-rule))
+ stroke-linecap (or (-> attrs :stroke-linecap sr/translate-stroke-linecap) (-> attrs :strokeLinecap sr/translate-stroke-linecap))
+ stroke-linejoin (or (-> attrs :stroke-linejoin sr/translate-stroke-linejoin) (-> attrs :strokeLinejoin sr/translate-stroke-linejoin))
fill-none (= "none" (-> attrs :fill))]
(h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none)))
@@ -739,7 +741,7 @@
(d/nilv align-self 0)
is-absolute
- (d/nilv z-index))))
+ (d/nilv z-index 0))))
(defn clear-layout
[]
@@ -1031,8 +1033,9 @@
(into full-acc full)))
{:thumbnails thumbnails-acc :full full-acc}))]
(perf/end-measure "set-objects")
- (process-pending shapes thumbnails full render-callback
+ (process-pending shapes thumbnails full noop-fn
(fn []
+ (when render-callback (render-callback))
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(defn clear-focus-mode
diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs
index 1962ed224f..1e3bcc6ca5 100644
--- a/frontend/src/app/render_wasm/api/fonts.cljs
+++ b/frontend/src/app/render_wasm/api/fonts.cljs
@@ -83,12 +83,13 @@
(defn update-text-layout
[id]
- (let [shape-id-buffer (uuid/get-u32 id)]
- (h/call wasm/internal-module "_update_shape_text_layout_for"
- (aget shape-id-buffer 0)
- (aget shape-id-buffer 1)
- (aget shape-id-buffer 2)
- (aget shape-id-buffer 3))))
+ (when wasm/context-initialized?
+ (let [shape-id-buffer (uuid/get-u32 id)]
+ (h/call wasm/internal-module "_update_shape_text_layout_for"
+ (aget shape-id-buffer 0)
+ (aget shape-id-buffer 1)
+ (aget shape-id-buffer 2)
+ (aget shape-id-buffer 3)))))
;; IMPORTANT: Only TTF fonts can be stored.
(defn- store-font-buffer
diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs
index 7c7b56da26..e70c567881 100644
--- a/frontend/src/app/util/clipboard.cljs
+++ b/frontend/src/app/util/clipboard.cljs
@@ -18,47 +18,56 @@
"image/svg+xml"])
(def ^:private default-options
- #js {:decodeTransit t/decode-str})
+ #js {:decodeTransit t/decode-str
+ :allowHTMLPaste false})
(defn- from-data-transfer
"Get clipboard stream from DataTransfer instance"
- [data-transfer]
- (->> (rx/from (impl/fromDataTransfer data-transfer default-options))
- (rx/mapcat #(rx/from %))))
+ ([data-transfer]
+ (from-data-transfer data-transfer default-options))
+ ([data-transfer options]
+ (->> (rx/from (impl/fromDataTransfer data-transfer options))
+ (rx/mapcat #(rx/from %)))))
(defn from-navigator
- []
- (->> (rx/from (impl/fromNavigator default-options))
- (rx/mapcat #(rx/from %))))
+ ([]
+ (from-navigator default-options))
+ ([options]
+ (->> (rx/from (impl/fromNavigator options))
+ (rx/mapcat #(rx/from %)))))
(defn from-clipboard-event
"Get clipboard stream from clipboard event"
- [event]
- (let [cdata (.-clipboardData ^js event)]
- (from-data-transfer cdata)))
+ ([event]
+ (from-clipboard-event event default-options))
+ ([event options]
+ (let [cdata (.-clipboardData ^js event)]
+ (from-data-transfer cdata options))))
(defn from-synthetic-clipboard-event
"Get clipboard stream from syntetic clipboard event"
- [event]
- (let [target
- (dom/get-target event)
+ ([event options]
+ (let [target
+ (dom/get-target event)
- content-editable?
- (dom/is-content-editable? target)
+ content-editable?
+ (dom/is-content-editable? target)
- is-input?
- (= (dom/get-tag-name target) "INPUT")]
+ is-input?
+ (= (dom/get-tag-name target) "INPUT")]
;; ignore when pasting into an editable control
- (when-not (or content-editable? is-input?)
- (-> event
- (dom/event->browser-event)
- (from-clipboard-event)))))
+ (when-not (or content-editable? is-input?)
+ (-> event
+ (dom/event->browser-event)
+ (from-clipboard-event options))))))
(defn from-drop-event
"Get clipboard stream from drop event"
- [event]
- (from-data-transfer (.-dataTransfer ^js event)))
+ ([event]
+ (from-drop-event event default-options))
+ ([event options]
+ (from-data-transfer (.-dataTransfer ^js event) options)))
;; FIXME: rename to `write-text`
(defn to-clipboard
diff --git a/frontend/src/app/util/clipboard.js b/frontend/src/app/util/clipboard.js
index 5fe4d88865..96f4080a7e 100644
--- a/frontend/src/app/util/clipboard.js
+++ b/frontend/src/app/util/clipboard.js
@@ -27,6 +27,7 @@ const exclusiveTypes = [
/**
* @typedef {Object} ClipboardSettings
* @property {Function} [decodeTransit]
+ * @property {boolean} [allowHTMLPaste]
*/
/**
@@ -38,9 +39,7 @@ const exclusiveTypes = [
*/
function parseText(text, options) {
options = options || {};
-
const decodeTransit = options["decodeTransit"];
-
if (decodeTransit) {
try {
decodeTransit(text);
@@ -57,18 +56,85 @@ function parseText(text, options) {
}
}
+/**
+ * Filters ClipboardItem types
+ *
+ * @param {ClipboardSettings} options
+ * @returns {Function}
+ */
+function filterAllowedTypes(options) {
+ /**
+ * @param {string} type
+ * @returns {boolean}
+ */
+ return function filter(type) {
+ if (
+ (!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) &&
+ type === "text/html"
+ ) {
+ return false;
+ }
+ return allowedTypes.includes(type);
+ };
+}
+
+/**
+ * Filters DataTransferItems
+ *
+ * @param {ClipboardSettings} options
+ * @returns {Function}
+ */
+function filterAllowedItems(options) {
+ /**
+ * @param {DataTransferItem}
+ * @returns {boolean}
+ */
+ return function filter(item) {
+ if (
+ (!("allowHTMLPaste" in options) || !options["allowHTMLPaste"]) &&
+ item.type === "text/html"
+ ) {
+ return false;
+ }
+ return allowedTypes.includes(item.type);
+ };
+}
+
+/**
+ * Sorts ClipboardItem types
+ *
+ * @param {string} a
+ * @param {string} b
+ * @returns {number}
+ */
+function sortTypes(a, b) {
+ return allowedTypes.indexOf(a) - allowedTypes.indexOf(b);
+}
+
+/**
+ * Sorts DataTransferItems
+ *
+ * @param {DataTransferItem} a
+ * @param {DataTransferItem} b
+ * @returns {number}
+ */
+function sortItems(a, b) {
+ return allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type);
+}
+
/**
*
* @param {ClipboardSettings} [options]
* @returns {Promise>}
*/
export async function fromNavigator(options) {
+ options = options || {};
const items = await navigator.clipboard.read();
return Promise.all(
Array.from(items).map(async (item) => {
const itemAllowedTypes = Array.from(item.types)
- .filter((type) => allowedTypes.includes(type))
- .sort((a, b) => allowedTypes.indexOf(a) - allowedTypes.indexOf(b));
+ .filter(filterAllowedTypes(options))
+ .sort(sortTypes);
if (
itemAllowedTypes.length === 1 &&
@@ -96,12 +162,11 @@ export async function fromNavigator(options) {
* @returns {Promise>}
*/
export async function fromDataTransfer(dataTransfer, options) {
+ options = options || {};
const items = await Promise.all(
Array.from(dataTransfer.items)
- .filter((item) => allowedTypes.includes(item.type))
- .sort(
- (a, b) => allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type),
- )
+ .filter(filterAllowedItems(options))
+ .sort(sortItems)
.map(async (item) => {
if (item.kind === "file") {
return Promise.resolve(item.getAsFile());
diff --git a/frontend/src/app/worker/thumbnails.cljs b/frontend/src/app/worker/thumbnails.cljs
index 5871c8c58e..517d7895a4 100644
--- a/frontend/src/app/worker/thumbnails.cljs
+++ b/frontend/src/app/worker/thumbnails.cljs
@@ -13,6 +13,7 @@
[app.common.logging :as log]
[app.common.types.color :as cc]
[app.common.uri :as u]
+ [app.common.uuid :as uuid]
[app.config :as cf]
[app.main.fonts :as fonts]
[app.main.render :as render]
@@ -125,58 +126,75 @@
(def thumbnail-aspect-ratio (/ 2 3))
-(defmethod impl/handler :thumbnails/generate-for-file-wasm
- [{:keys [file-id revn width] :as message} _]
+(defn render-canvas-blob
+ [canvas width height background-color]
+ (-> (.convertToBlob canvas)
+ (p/then
+ (fn [blob]
+ (rds/renderToStaticMarkup
+ (mf/element
+ svg-wrapper
+ #js {:data-uri (blob->uri blob)
+ :width width
+ :height height
+ :background background-color}))))))
+(defn process-wasm-thumbnail
+ [{:keys [id file-id revn width] :as message}]
(->> (rx/from @init-wasm)
(rx/mapcat #(request-data-for-thumbnail file-id revn false))
(rx/mapcat
(fn [{:keys [page] :as file}]
(rx/create
(fn [subs]
- (try
- (let [background-color (or (:background page) cc/canvas)
- height (* width thumbnail-aspect-ratio)
- canvas (js/OffscreenCanvas. width height)
- init? (wasm.api/init-canvas-context canvas)]
- (if init?
- (let [objects (:objects page)
- frame (some->> page :thumbnail-frame-id (get objects))
- vbox (if frame
- (-> (gsb/get-object-bounds objects frame)
- (grc/fix-aspect-ratio thumbnail-aspect-ratio))
- (render/calculate-dimensions objects thumbnail-aspect-ratio))
- zoom (/ width (:width vbox))]
+ (let [background-color (or (:background page) cc/canvas)
+ height (* width thumbnail-aspect-ratio)
+ canvas (js/OffscreenCanvas. width height)
+ init? (wasm.api/init-canvas-context canvas)]
+ (if init?
+ (let [objects (:objects page)
+ frame (some->> page :thumbnail-frame-id (get objects))
+ vbox (if frame
+ (-> (gsb/get-object-bounds objects frame)
+ (grc/fix-aspect-ratio thumbnail-aspect-ratio))
+ (render/calculate-dimensions objects thumbnail-aspect-ratio))
+ zoom (/ width (:width vbox))]
- (wasm.api/initialize-viewport
- objects zoom vbox background-color
- (fn []
- (if frame
- (wasm.api/render-sync-shape (:id frame))
- (wasm.api/render-sync))
+ (wasm.api/initialize-viewport
+ objects zoom vbox background-color
+ (fn []
+ (if frame
+ (wasm.api/render-sync-shape (:id frame))
+ (wasm.api/render-sync))
- (-> (.convertToBlob canvas)
- (p/then
- (fn [blob]
- (let [data
- (rds/renderToStaticMarkup
- (mf/element
- svg-wrapper
- #js {:data-uri (blob->uri blob)
- :width width
- :height height
- :background background-color}))]
- (rx/push! subs {:data data :file-id file-id :revn revn}))))
- (p/catch #(do (.error js/console %)
- (rx/error! subs %)))
- (p/finally #(rx/end! subs))))))
+ (-> (render-canvas-blob canvas width height background-color)
+ (p/then #(rx/push! subs {:id id :data % :file-id file-id :revn revn}))
+ (p/catch #(rx/error! subs %))
+ (p/finally #(rx/end! subs))))))
- (do (rx/error! subs "Error loading webgl context")
- (rx/end! subs)))
+ (rx/end! subs))
- nil)
+ nil)))))))
- (catch :default err
- (.error js/console err)
- (rx/error! subs err)
- (rx/end! subs)))))))))
+(defonce thumbs-subject (rx/subject))
+
+(defonce thumbs-stream
+ (->> thumbs-subject
+ (rx/mapcat process-wasm-thumbnail)
+ (rx/share)))
+
+(defmethod impl/handler :thumbnails/generate-for-file-wasm
+ [message _]
+ (rx/create
+ (fn [subs]
+ (let [id (uuid/next)
+ sid
+ (->> thumbs-stream
+ (rx/filter #(= id (:id %)))
+ (rx/subs!
+ #(do
+ (rx/push! subs %)
+ (rx/end! subs))))]
+ (rx/push! thumbs-subject (assoc message :id id))
+
+ #(rx/dispose! sid)))))
diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs
index d7fc7b30af..3875da7f00 100644
--- a/render-wasm/src/render.rs
+++ b/render-wasm/src/render.rs
@@ -1641,9 +1641,9 @@ impl RenderState {
}
children_ids.sort_by(|id1, id2| {
- let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index());
- let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index());
- z1.cmp(&z2)
+ let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
+ let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
+ z2.cmp(&z1)
});
}
diff --git a/render-wasm/src/shapes/modifiers.rs b/render-wasm/src/shapes/modifiers.rs
index c7ca3e5484..f090cf98a7 100644
--- a/render-wasm/src/shapes/modifiers.rs
+++ b/render-wasm/src/shapes/modifiers.rs
@@ -332,6 +332,7 @@ fn propagate_reflow(
}
_ => {
// Other shapes don't have to be reflown
+ reflow_parent = true;
}
}
diff --git a/render-wasm/src/wasm/text.rs b/render-wasm/src/wasm/text.rs
index a45b03c9de..f4dcc12086 100644
--- a/render-wasm/src/wasm/text.rs
+++ b/render-wasm/src/wasm/text.rs
@@ -291,9 +291,10 @@ pub extern "C" fn set_shape_text_content() {
let bytes = mem::bytes();
with_current_shape_mut!(state, |shape: &mut Shape| {
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
- shape
- .add_paragraph(raw_text_data.into())
- .expect("Failed to add paragraph");
+
+ if let Err(_) = shape.add_paragraph(raw_text_data.into()) {
+ println!("Error with set_shape_text_content on {:?}", shape.id);
+ }
});
mem::free_bytes();
}