Merge remote-tracking branch 'origin/staging' into develop

This commit is contained in:
Andrey Antukh
2025-11-26 07:30:42 +01:00
21 changed files with 425 additions and 263 deletions

View File

@@ -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) - 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) - 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) - 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 ### :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 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 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 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 ## 2.11.1

View File

@@ -1679,7 +1679,7 @@ Will return a value that matches this schema:
["id" {:optional true} :string] ["id" {:optional true} :string]
["name" :string] ["name" :string]
["description" :string] ["description" :string]
["isSource" :boolean] ["isSource" {:optional true} :boolean]
["selectedTokenSets" ["selectedTokenSets"
[:map-of :string [:enum "enabled" "disabled"]]]]]] [:map-of :string [:enum "enabled" "disabled"]]]]]]
["$metadata" {:optional true} ["$metadata" {:optional true}

View File

@@ -115,6 +115,11 @@ services:
volumes: volumes:
- "valkey_data:/data" - "valkey_data:/data"
networks:
default:
aliases:
- redis
mailer: mailer:
image: sj26/mailcatcher:latest image: sj26/mailcatcher:latest
restart: always restart: always
@@ -123,6 +128,12 @@ services:
ports: ports:
- "1080:1080" - "1080:1080"
networks:
default:
aliases:
- mailer
# https://github.com/rroemhild/docker-test-openldap # https://github.com/rroemhild/docker-test-openldap
ldap: ldap:
image: rroemhild/test-openldap:2.1 image: rroemhild/test-openldap:2.1
@@ -136,3 +147,9 @@ services:
nofile: nofile:
soft: 1024 soft: 1024
hard: 1024 hard: 1024
networks:
default:
aliases:
- ldap

View File

@@ -1,5 +1,6 @@
--- ---
title: 3.07. Abstraction levels 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 # Code organization in abstraction levels

View File

@@ -1,5 +1,6 @@
--- ---
title: 3.06. Backend Guide 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 # # Backend guide #

View File

@@ -1,5 +1,6 @@
--- ---
title: 1.2 Install with Elestio 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 # Install with Elestio

View File

@@ -1,6 +1,7 @@
--- ---
title: Design Tokens title: Design Tokens
order: 5 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.
--- ---
<h1 id="design-tokens">Design Tokens</h1> <h1 id="design-tokens">Design Tokens</h1>

View File

@@ -195,7 +195,9 @@
(ptk/reify ::login-from-token (ptk/reify ::login-from-token
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (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 ;; NOTE: we need this to be asynchronous because the effect
;; should be called before proceed with the login process ;; should be called before proceed with the login process
(rx/observe-on :async))))) (rx/observe-on :async)))))

View File

@@ -77,22 +77,25 @@
(rx/of (rt/nav-raw :href href))) (rx/of (rt/nav-raw :href href)))
(rx/throw cause)))) (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 (defn fetch-profile
[] []
(ptk/reify ::fetch-profile (ptk/reify ::fetch-profile
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (rp/cmd! :get-profile) (->> (rp/cmd! :get-profile)
(rx/mapcat (fn [profile] (rx/mapcat on-fetch-profile-success)
(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/map (partial ptk/data-event ::profile-fetched)) (rx/map (partial ptk/data-event ::profile-fetched))
(rx/catch on-fetch-profile-exception))))) (rx/catch on-fetch-profile-exception)))))

View File

@@ -254,6 +254,10 @@
(declare ^:private paste-svg-text) (declare ^:private paste-svg-text)
(declare ^:private paste-shapes) (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 (defn create-paste-from-blob
[in-viewport?] [in-viewport?]
(fn [blob] (fn [blob]
@@ -290,7 +294,7 @@
(ptk/reify ::paste-from-clipboard (ptk/reify ::paste-from-clipboard
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(->> (clipboard/from-navigator) (->> (clipboard/from-navigator default-options)
(rx/mapcat default-paste-from-blob) (rx/mapcat default-paste-from-blob)
(rx/take 1))))) (rx/take 1)))))
@@ -308,7 +312,7 @@
;; we forbid that scenario so the default behaviour is executed ;; we forbid that scenario so the default behaviour is executed
(if is-editing? (if is-editing?
(rx/empty) (rx/empty)
(->> (clipboard/from-synthetic-clipboard-event event) (->> (clipboard/from-synthetic-clipboard-event event default-options)
(rx/mapcat (create-paste-from-blob in-viewport?)))))))) (rx/mapcat (create-paste-from-blob in-viewport?))))))))
(defn copy-selected-svg (defn copy-selected-svg
@@ -478,7 +482,7 @@
(js/console.error "Clipboard error:" cause)) (js/console.error "Clipboard error:" cause))
(rx/empty)))] (rx/empty)))]
(->> (clipboard/from-navigator) (->> (clipboard/from-navigator default-options)
(rx/mapcat #(.text %)) (rx/mapcat #(.text %))
(rx/map decode-entry) (rx/map decode-entry)
(rx/take 1) (rx/take 1)

View File

@@ -60,10 +60,10 @@
(defn render-thumbnail (defn render-thumbnail
[file-id revn] [file-id revn]
(if (features/active-feature? @st/state "render-wasm/v1") (if (features/active-feature? @st/state "render-wasm/v1")
(->> (mw/ask! {:cmd :thumbnails/generate-for-file-wasm (mw/ask! {:cmd :thumbnails/generate-for-file-wasm
:revn revn :revn revn
:file-id file-id :file-id file-id
:width thumbnail-width})) :width thumbnail-width})
(->> (mw/ask! {:cmd :thumbnails/generate-for-file (->> (mw/ask! {:cmd :thumbnails/generate-for-file
:revn revn :revn revn
:file-id file-id :file-id file-id

View File

@@ -83,74 +83,101 @@
::mf/register-as :management-dialog} ::mf/register-as :management-dialog}
[{:keys [subscription-type current-subscription editors subscribe-to-trial]}] [{:keys [subscription-type current-subscription editors subscribe-to-trial]}]
(let [unlimited-modal-step* (mf/use-state 1) (let [unlimited-modal-step*
unlimited-modal-step (deref unlimited-modal-step*) (mf/use-state 1)
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))))))
subscribe-to-enterprise (mf/use-fn unlimited-modal-step
(fn [] (deref unlimited-modal-step*)
(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 subscription-name
(fn [] (if subscribe-to-trial
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management" (if (= subscription-type "unlimited")
::ev/origin "settings" (tr "subscription.settings.unlimited-trial")
:section "subscription-management-modal"})) (tr "subscription.settings.enterprise-trial"))
(let [current-href (rt/get-current-href) (case subscription-type
returnUrl (js/encodeURIComponent current-href) "professional" (tr "subscription.settings.professional")
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)] "unlimited" (tr "subscription.settings.unlimited")
(st/emit! (rt/nav-raw :href href))) "enterprise" (tr "subscription.settings.enterprise")))
(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 min-editors
(mf/deps unlimited-modal-step) (if (seq editors) (count editors) 1)
(fn []
(if (= unlimited-modal-step 1)
(reset! unlimited-modal-step* 2)
(reset! unlimited-modal-step* 1))))
show-editors-list* (mf/use-state false) initial
show-editors-list (deref show-editors-list*) (mf/with-memo [min-editors]
handle-click (mf/use-fn {:min-members min-editors})
(fn [event]
(dom/stop-propagation event) form
(swap! show-editors-list* not)))] (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-overlay)}
[:div {:class (stl/css :modal-dialog)} [:div {:class (stl/css :modal-dialog)}
@@ -246,7 +273,7 @@
[:input [:input
{:class (stl/css :cancel-button) {:class (stl/css :cancel-button)
:type "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)}] :on-click #(subscribe-to-unlimited false)}]
[:input [:input

View File

@@ -8,10 +8,12 @@
@use "ds/typography.scss" as t; @use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *; @use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *; @use "ds/spacing.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.dashboard-section { .dashboard-section {
display: flex; display: flex;
width: 100%; inline-size: 100%;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
@@ -20,10 +22,10 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
max-width: deprecated.$s-500; max-inline-size: $sz-500;
margin-block-end: var(--sp-xxxl); margin-block-end: var(--sp-xxxl);
width: deprecated.$s-580; inline-size: px2rem(580);
margin: deprecated.$s-92 auto deprecated.$s-120 auto; margin: px2rem(92) auto px2rem(120) auto;
justify-content: center; justify-content: center;
} }
@@ -98,8 +100,8 @@
.plan-title-icon { .plan-title-icon {
@extend .button-icon; @extend .button-icon;
stroke: var(--color-foreground-primary); stroke: var(--color-foreground-primary);
height: var(--sp-xl); block-size: var(--sp-xl);
width: var(--sp-xl); inline-size: var(--sp-xl);
border-radius: 6px; border-radius: 6px;
border: 1.75px solid var(--color-foreground-primary); border: 1.75px solid var(--color-foreground-primary);
stroke-width: 2.25px; stroke-width: 2.25px;
@@ -140,7 +142,7 @@
} }
.other-subscriptions { .other-subscriptions {
margin-block-start: deprecated.$s-52; margin-block-start: px2rem(52);
} }
.cta-button { .cta-button {
@@ -155,8 +157,8 @@
.cta-button svg { .cta-button svg {
@extend .button-icon; @extend .button-icon;
height: var(--sp-l); block-size: var(--sp-l);
width: var(--sp-l); inline-size: var(--sp-l);
stroke: var(--color-accent-primary); stroke: var(--color-accent-primary);
margin-inline-start: var(--sp-xs); margin-inline-start: var(--sp-xs);
} }
@@ -179,14 +181,12 @@
.modal-dialog { .modal-dialog {
@extend .modal-container-base; @extend .modal-container-base;
display: grid; max-block-size: initial;
grid-template-rows: auto 1fr auto; min-inline-size: px2rem(548);
max-height: initial;
min-width: deprecated.$s-548;
} }
.modal-dialog.subscription-success { .modal-dialog.subscription-success {
min-width: deprecated.$s-648; min-inline-size: px2rem(648);
} }
.close-btn { .close-btn {
@@ -232,11 +232,11 @@
.modal-success-content { .modal-success-content {
display: flex; display: flex;
gap: deprecated.$s-40; gap: $sz-40;
} }
.modal-footer { .modal-footer {
margin-block-start: deprecated.$s-40; margin-block-start: $sz-40;
} }
.action-buttons { .action-buttons {
@@ -249,23 +249,28 @@
.primary-button { .primary-button {
@extend .modal-accept-btn; @extend .modal-accept-btn;
min-block-size: $sz-32;
block-size: auto;
} }
.cancel-button { .cancel-button {
@extend .modal-cancel-btn; @extend .modal-cancel-btn;
min-block-size: $sz-32;
white-space: break-spaces;
block-size: auto;
} }
.modal-start { .modal-start {
display: flex; display: flex;
justify-content: center; justify-content: center;
max-width: deprecated.$s-220; max-inline-size: $sz-224;
svg { svg {
width: 100%; inline-size: 100%;
height: auto; block-size: auto;
} }
@media (max-width: 992px) { @media (max-inline-size: 992px) {
display: none; display: none;
} }
} }
@@ -285,19 +290,19 @@
list-style-position: inside; list-style-position: inside;
list-style-type: none; list-style-type: none;
margin-inline-start: var(--sp-xl); margin-inline-start: var(--sp-xl);
max-height: deprecated.$s-216; max-block-size: px2rem(216);
overflow-y: auto; overflow-y: auto;
} }
.input-field { .input-field {
--input-icon-padding: var(--sp-s); --input-icon-padding: var(--sp-s);
width: deprecated.$s-80; inline-size: px2rem(80);
} }
.error-message { .error-message {
@include t.use-typography("body-small"); @include t.use-typography("body-small");
color: var(--color-foreground-error); color: var(--color-foreground-error);
margin-block-start: deprecated.$s-8; margin-block-start: var(--sp-s);
} }
.editors-wrapper { .editors-wrapper {
@@ -316,7 +321,7 @@
@include t.use-typography("body-small"); @include t.use-typography("body-small");
background-color: var(--color-background-tertiary); background-color: var(--color-background-tertiary);
border-radius: var(--sp-s); border-radius: var(--sp-s);
margin-block-start: deprecated.$s-40; margin-block-start: $sz-40;
padding-block: var(--sp-s); padding-block: var(--sp-s);
padding-inline: var(--sp-m); padding-inline: var(--sp-m);
} }

View File

@@ -128,11 +128,12 @@
(defn update-text-rect! (defn update-text-rect!
[id] [id]
(mw/emit! (when wasm/context-initialized?
{:cmd :index/update-text-rect (mw/emit!
:page-id (:current-page-id @st/state) {:cmd :index/update-text-rect
:shape-id id :page-id (:current-page-id @st/state)
:dimensions (get-text-dimensions id)})) :shape-id id
:dimensions (get-text-dimensions id)})))
(defn- ensure-text-content (defn- ensure-text-content
@@ -198,70 +199,71 @@
(defn set-shape-children (defn set-shape-children
[children] [children]
(perf/begin-measure "set-shape-children") (perf/begin-measure "set-shape-children")
(case (count children) (let [children (into [] (filter uuid?) children)]
0 (case (count children)
(h/call wasm/internal-module "_set_children_0") 0
(h/call wasm/internal-module "_set_children_0")
1 1
(let [[c1] children (let [[c1] children
c1 (uuid/get-u32 c1)] c1 (uuid/get-u32 c1)]
(h/call wasm/internal-module "_set_children_1" (h/call wasm/internal-module "_set_children_1"
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3))) (aget c1 0) (aget c1 1) (aget c1 2) (aget c1 3)))
2 2
(let [[c1 c2] children (let [[c1 c2] children
c1 (uuid/get-u32 c1) c1 (uuid/get-u32 c1)
c2 (uuid/get-u32 c2)] c2 (uuid/get-u32 c2)]
(h/call wasm/internal-module "_set_children_2" (h/call wasm/internal-module "_set_children_2"
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 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 c2 0) (aget c2 1) (aget c2 2) (aget c2 3)))
3 3
(let [[c1 c2 c3] children (let [[c1 c2 c3] children
c1 (uuid/get-u32 c1) c1 (uuid/get-u32 c1)
c2 (uuid/get-u32 c2) c2 (uuid/get-u32 c2)
c3 (uuid/get-u32 c3)] c3 (uuid/get-u32 c3)]
(h/call wasm/internal-module "_set_children_3" (h/call wasm/internal-module "_set_children_3"
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 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 c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3))) (aget c3 0) (aget c3 1) (aget c3 2) (aget c3 3)))
4 4
(let [[c1 c2 c3 c4] children (let [[c1 c2 c3 c4] children
c1 (uuid/get-u32 c1) c1 (uuid/get-u32 c1)
c2 (uuid/get-u32 c2) c2 (uuid/get-u32 c2)
c3 (uuid/get-u32 c3) c3 (uuid/get-u32 c3)
c4 (uuid/get-u32 c4)] c4 (uuid/get-u32 c4)]
(h/call wasm/internal-module "_set_children_4" (h/call wasm/internal-module "_set_children_4"
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 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 c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 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 c4 0) (aget c4 1) (aget c4 2) (aget c4 3)))
5 5
(let [[c1 c2 c3 c4 c5] children (let [[c1 c2 c3 c4 c5] children
c1 (uuid/get-u32 c1) c1 (uuid/get-u32 c1)
c2 (uuid/get-u32 c2) c2 (uuid/get-u32 c2)
c3 (uuid/get-u32 c3) c3 (uuid/get-u32 c3)
c4 (uuid/get-u32 c4) c4 (uuid/get-u32 c4)
c5 (uuid/get-u32 c5)] c5 (uuid/get-u32 c5)]
(h/call wasm/internal-module "_set_children_5" (h/call wasm/internal-module "_set_children_5"
(aget c1 0) (aget c1 1) (aget c1 2) (aget c1 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 c2 0) (aget c2 1) (aget c2 2) (aget c2 3)
(aget c3 0) (aget c3 1) (aget c3 2) (aget c3 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 c4 0) (aget c4 1) (aget c4 2) (aget c4 3)
(aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3))) (aget c5 0) (aget c5 1) (aget c5 2) (aget c5 3)))
;; Dynamic call for children > 5 ;; Dynamic call for children > 5
(let [heap (mem/get-heap-u32) (let [heap (mem/get-heap-u32)
size (mem/get-alloc-size children UUID-U8-SIZE) size (mem/get-alloc-size children UUID-U8-SIZE)
offset (mem/alloc->offset-32 size)] offset (mem/alloc->offset-32 size)]
(reduce (reduce
(fn [offset id] (fn [offset id]
(mem.h32/write-uuid offset heap id)) (mem.h32/write-uuid offset heap id))
offset offset
children) children)
(h/call wasm/internal-module "_set_children"))) (h/call wasm/internal-module "_set_children"))))
(perf/end-measure "set-shape-children") (perf/end-measure "set-shape-children")
nil) nil)
@@ -468,14 +470,14 @@
[attrs] [attrs]
(let [style (:style attrs) (let [style (:style attrs)
;; Filter to only supported attributes ;; 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 attrs (-> attrs
(dissoc :style) (dissoc :style)
(merge style) (merge style)
(select-keys allowed-keys)) (select-keys allowed-keys))
fill-rule (-> attrs :fillRule sr/translate-fill-rule) fill-rule (or (-> attrs :fill-rule sr/translate-fill-rule) (-> attrs :fillRule sr/translate-fill-rule))
stroke-linecap (-> attrs :strokeLinecap sr/translate-stroke-linecap) stroke-linecap (or (-> attrs :stroke-linecap sr/translate-stroke-linecap) (-> attrs :strokeLinecap sr/translate-stroke-linecap))
stroke-linejoin (-> attrs :strokeLinejoin sr/translate-stroke-linejoin) stroke-linejoin (or (-> attrs :stroke-linejoin sr/translate-stroke-linejoin) (-> attrs :strokeLinejoin sr/translate-stroke-linejoin))
fill-none (= "none" (-> attrs :fill))] fill-none (= "none" (-> attrs :fill))]
(h/call wasm/internal-module "_set_shape_svg_attrs" fill-rule stroke-linecap stroke-linejoin fill-none))) (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) (d/nilv align-self 0)
is-absolute is-absolute
(d/nilv z-index)))) (d/nilv z-index 0))))
(defn clear-layout (defn clear-layout
[] []
@@ -1031,8 +1033,9 @@
(into full-acc full))) (into full-acc full)))
{:thumbnails thumbnails-acc :full full-acc}))] {:thumbnails thumbnails-acc :full full-acc}))]
(perf/end-measure "set-objects") (perf/end-measure "set-objects")
(process-pending shapes thumbnails full render-callback (process-pending shapes thumbnails full noop-fn
(fn [] (fn []
(when render-callback (render-callback))
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))) (ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(defn clear-focus-mode (defn clear-focus-mode

View File

@@ -83,12 +83,13 @@
(defn update-text-layout (defn update-text-layout
[id] [id]
(let [shape-id-buffer (uuid/get-u32 id)] (when wasm/context-initialized?
(h/call wasm/internal-module "_update_shape_text_layout_for" (let [shape-id-buffer (uuid/get-u32 id)]
(aget shape-id-buffer 0) (h/call wasm/internal-module "_update_shape_text_layout_for"
(aget shape-id-buffer 1) (aget shape-id-buffer 0)
(aget shape-id-buffer 2) (aget shape-id-buffer 1)
(aget shape-id-buffer 3)))) (aget shape-id-buffer 2)
(aget shape-id-buffer 3)))))
;; IMPORTANT: Only TTF fonts can be stored. ;; IMPORTANT: Only TTF fonts can be stored.
(defn- store-font-buffer (defn- store-font-buffer

View File

@@ -18,47 +18,56 @@
"image/svg+xml"]) "image/svg+xml"])
(def ^:private default-options (def ^:private default-options
#js {:decodeTransit t/decode-str}) #js {:decodeTransit t/decode-str
:allowHTMLPaste false})
(defn- from-data-transfer (defn- from-data-transfer
"Get clipboard stream from DataTransfer instance" "Get clipboard stream from DataTransfer instance"
[data-transfer] ([data-transfer]
(->> (rx/from (impl/fromDataTransfer data-transfer default-options)) (from-data-transfer data-transfer default-options))
(rx/mapcat #(rx/from %)))) ([data-transfer options]
(->> (rx/from (impl/fromDataTransfer data-transfer options))
(rx/mapcat #(rx/from %)))))
(defn from-navigator (defn from-navigator
[] ([]
(->> (rx/from (impl/fromNavigator default-options)) (from-navigator default-options))
(rx/mapcat #(rx/from %)))) ([options]
(->> (rx/from (impl/fromNavigator options))
(rx/mapcat #(rx/from %)))))
(defn from-clipboard-event (defn from-clipboard-event
"Get clipboard stream from clipboard event" "Get clipboard stream from clipboard event"
[event] ([event]
(let [cdata (.-clipboardData ^js event)] (from-clipboard-event event default-options))
(from-data-transfer cdata))) ([event options]
(let [cdata (.-clipboardData ^js event)]
(from-data-transfer cdata options))))
(defn from-synthetic-clipboard-event (defn from-synthetic-clipboard-event
"Get clipboard stream from syntetic clipboard event" "Get clipboard stream from syntetic clipboard event"
[event] ([event options]
(let [target (let [target
(dom/get-target event) (dom/get-target event)
content-editable? content-editable?
(dom/is-content-editable? target) (dom/is-content-editable? target)
is-input? is-input?
(= (dom/get-tag-name target) "INPUT")] (= (dom/get-tag-name target) "INPUT")]
;; ignore when pasting into an editable control ;; ignore when pasting into an editable control
(when-not (or content-editable? is-input?) (when-not (or content-editable? is-input?)
(-> event (-> event
(dom/event->browser-event) (dom/event->browser-event)
(from-clipboard-event))))) (from-clipboard-event options))))))
(defn from-drop-event (defn from-drop-event
"Get clipboard stream from drop event" "Get clipboard stream from drop event"
[event] ([event]
(from-data-transfer (.-dataTransfer ^js event))) (from-drop-event event default-options))
([event options]
(from-data-transfer (.-dataTransfer ^js event) options)))
;; FIXME: rename to `write-text` ;; FIXME: rename to `write-text`
(defn to-clipboard (defn to-clipboard

View File

@@ -27,6 +27,7 @@ const exclusiveTypes = [
/** /**
* @typedef {Object} ClipboardSettings * @typedef {Object} ClipboardSettings
* @property {Function} [decodeTransit] * @property {Function} [decodeTransit]
* @property {boolean} [allowHTMLPaste]
*/ */
/** /**
@@ -38,9 +39,7 @@ const exclusiveTypes = [
*/ */
function parseText(text, options) { function parseText(text, options) {
options = options || {}; options = options || {};
const decodeTransit = options["decodeTransit"]; const decodeTransit = options["decodeTransit"];
if (decodeTransit) { if (decodeTransit) {
try { try {
decodeTransit(text); decodeTransit(text);
@@ -57,18 +56,85 @@ function parseText(text, options) {
} }
} }
/**
* Filters ClipboardItem types
*
* @param {ClipboardSettings} options
* @returns {Function<AllowedTypesFilterFunction>}
*/
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<AllowedTypesFilterFunction>}
*/
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] * @param {ClipboardSettings} [options]
* @returns {Promise<Array<Blob>>} * @returns {Promise<Array<Blob>>}
*/ */
export async function fromNavigator(options) { export async function fromNavigator(options) {
options = options || {};
const items = await navigator.clipboard.read(); const items = await navigator.clipboard.read();
return Promise.all( return Promise.all(
Array.from(items).map(async (item) => { Array.from(items).map(async (item) => {
const itemAllowedTypes = Array.from(item.types) const itemAllowedTypes = Array.from(item.types)
.filter((type) => allowedTypes.includes(type)) .filter(filterAllowedTypes(options))
.sort((a, b) => allowedTypes.indexOf(a) - allowedTypes.indexOf(b)); .sort(sortTypes);
if ( if (
itemAllowedTypes.length === 1 && itemAllowedTypes.length === 1 &&
@@ -96,12 +162,11 @@ export async function fromNavigator(options) {
* @returns {Promise<Array<Blob>>} * @returns {Promise<Array<Blob>>}
*/ */
export async function fromDataTransfer(dataTransfer, options) { export async function fromDataTransfer(dataTransfer, options) {
options = options || {};
const items = await Promise.all( const items = await Promise.all(
Array.from(dataTransfer.items) Array.from(dataTransfer.items)
.filter((item) => allowedTypes.includes(item.type)) .filter(filterAllowedItems(options))
.sort( .sort(sortItems)
(a, b) => allowedTypes.indexOf(a.type) - allowedTypes.indexOf(b.type),
)
.map(async (item) => { .map(async (item) => {
if (item.kind === "file") { if (item.kind === "file") {
return Promise.resolve(item.getAsFile()); return Promise.resolve(item.getAsFile());

View File

@@ -13,6 +13,7 @@
[app.common.logging :as log] [app.common.logging :as log]
[app.common.types.color :as cc] [app.common.types.color :as cc]
[app.common.uri :as u] [app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf] [app.config :as cf]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.render :as render] [app.main.render :as render]
@@ -125,58 +126,75 @@
(def thumbnail-aspect-ratio (/ 2 3)) (def thumbnail-aspect-ratio (/ 2 3))
(defmethod impl/handler :thumbnails/generate-for-file-wasm (defn render-canvas-blob
[{:keys [file-id revn width] :as message} _] [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/from @init-wasm)
(rx/mapcat #(request-data-for-thumbnail file-id revn false)) (rx/mapcat #(request-data-for-thumbnail file-id revn false))
(rx/mapcat (rx/mapcat
(fn [{:keys [page] :as file}] (fn [{:keys [page] :as file}]
(rx/create (rx/create
(fn [subs] (fn [subs]
(try (let [background-color (or (:background page) cc/canvas)
(let [background-color (or (:background page) cc/canvas) height (* width thumbnail-aspect-ratio)
height (* width thumbnail-aspect-ratio) canvas (js/OffscreenCanvas. width height)
canvas (js/OffscreenCanvas. width height) init? (wasm.api/init-canvas-context canvas)]
init? (wasm.api/init-canvas-context canvas)] (if init?
(if init? (let [objects (:objects page)
(let [objects (:objects page) frame (some->> page :thumbnail-frame-id (get objects))
frame (some->> page :thumbnail-frame-id (get objects)) vbox (if frame
vbox (if frame (-> (gsb/get-object-bounds objects frame)
(-> (gsb/get-object-bounds objects frame) (grc/fix-aspect-ratio thumbnail-aspect-ratio))
(grc/fix-aspect-ratio thumbnail-aspect-ratio)) (render/calculate-dimensions objects thumbnail-aspect-ratio))
(render/calculate-dimensions objects thumbnail-aspect-ratio)) zoom (/ width (:width vbox))]
zoom (/ width (:width vbox))]
(wasm.api/initialize-viewport (wasm.api/initialize-viewport
objects zoom vbox background-color objects zoom vbox background-color
(fn [] (fn []
(if frame (if frame
(wasm.api/render-sync-shape (:id frame)) (wasm.api/render-sync-shape (:id frame))
(wasm.api/render-sync)) (wasm.api/render-sync))
(-> (.convertToBlob canvas) (-> (render-canvas-blob canvas width height background-color)
(p/then (p/then #(rx/push! subs {:id id :data % :file-id file-id :revn revn}))
(fn [blob] (p/catch #(rx/error! subs %))
(let [data (p/finally #(rx/end! subs))))))
(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))))))
(do (rx/error! subs "Error loading webgl context") (rx/end! subs))
(rx/end! subs)))
nil) nil)))))))
(catch :default err (defonce thumbs-subject (rx/subject))
(.error js/console err)
(rx/error! subs err) (defonce thumbs-stream
(rx/end! subs))))))))) (->> 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)))))

View File

@@ -1641,9 +1641,9 @@ impl RenderState {
} }
children_ids.sort_by(|id1, id2| { children_ids.sort_by(|id1, id2| {
let z1 = tree.get(id1).map_or_else(|| 0, |s| s.z_index()); let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
let z2 = tree.get(id2).map_or_else(|| 0, |s| s.z_index()); let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
z1.cmp(&z2) z2.cmp(&z1)
}); });
} }

View File

@@ -332,6 +332,7 @@ fn propagate_reflow(
} }
_ => { _ => {
// Other shapes don't have to be reflown // Other shapes don't have to be reflown
reflow_parent = true;
} }
} }

View File

@@ -291,9 +291,10 @@ pub extern "C" fn set_shape_text_content() {
let bytes = mem::bytes(); let bytes = mem::bytes();
with_current_shape_mut!(state, |shape: &mut Shape| { with_current_shape_mut!(state, |shape: &mut Shape| {
let raw_text_data = RawParagraph::try_from(&bytes).unwrap(); let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
shape
.add_paragraph(raw_text_data.into()) if let Err(_) = shape.add_paragraph(raw_text_data.into()) {
.expect("Failed to add paragraph"); println!("Error with set_shape_text_content on {:?}", shape.id);
}
}); });
mem::free_bytes(); mem::free_bytes();
} }