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,13 +77,8 @@
(rx/of (rt/nav-raw :href href))) (rx/of (rt/nav-raw :href href)))
(rx/throw cause)))) (rx/throw cause))))
(defn fetch-profile (defn on-fetch-profile-success
[] [profile]
(ptk/reify ::fetch-profile
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-profile)
(rx/mapcat (fn [profile]
(if (and (contains? cf/flags :subscriptions) (if (and (contains? cf/flags :subscriptions)
(is-authenticated? profile)) (is-authenticated? profile))
(->> (rp/cmd! :get-subscription-usage {}) (->> (rp/cmd! :get-subscription-usage {})
@@ -92,7 +87,15 @@
(rx/catch (fn [cause] (rx/catch (fn [cause]
(js/console.error "unexpected error on obtaining subscription usage" cause) (js/console.error "unexpected error on obtaining subscription usage" cause)
(rx/of profile)))) (rx/of profile))))
(rx/of profile)))) (rx/of profile)))
(defn fetch-profile
[]
(ptk/reify ::fetch-profile
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-profile)
(rx/mapcat on-fetch-profile-success)
(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,9 +83,14 @@
::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
unlimited-modal-step
(deref unlimited-modal-step*)
subscription-name
(if subscribe-to-trial
(if (= subscription-type "unlimited") (if (= subscription-type "unlimited")
(tr "subscription.settings.unlimited-trial") (tr "subscription.settings.unlimited-trial")
(tr "subscription.settings.enterprise-trial")) (tr "subscription.settings.enterprise-trial"))
@@ -93,29 +98,42 @@
"professional" (tr "subscription.settings.professional") "professional" (tr "subscription.settings.professional")
"unlimited" (tr "subscription.settings.unlimited") "unlimited" (tr "subscription.settings.unlimited")
"enterprise" (tr "subscription.settings.enterprise"))) "enterprise" (tr "subscription.settings.enterprise")))
min-editors (if (seq editors) (count editors) 1)
initial (mf/with-memo [min-editors] min-editors
(if (seq editors) (count editors) 1)
initial
(mf/with-memo [min-editors]
{:min-members min-editors}) {:min-members min-editors})
form (fm/use-form :schema (schema:seats-form min-editors)
form
(fm/use-form :schema (schema:seats-form min-editors)
:initial initial) :initial initial)
submit-in-progress* (mf/use-state false)
subscribe-to-unlimited (mf/use-fn form-data-min-editors
(mf/deps form) (-> @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] (fn [add-payment-details]
(when (not @submit-in-progress*) (when (not @submit-in-progress*)
(let [data (:clean-data @form) (let [return-url (-> (rt/get-current-href) (rt/encode-url))
return-url (-> (rt/get-current-href) (rt/encode-url))
href (if add-payment-details 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=true&quantity=" form-data-min-editors "&returnUrl=" return-url)
(dm/str "payments/subscriptions/create?type=unlimited&show=false&quantity=" (:min-members data) "&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! submit-in-progress* true)
(reset! form nil) (reset! form nil)
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
:type "unlimited" :type "unlimited"
:quantity (:min-members data)}) :quantity form-data-min-editors})
(rt/nav-raw :href href)))))) (rt/nav-raw :href href))))))
subscribe-to-enterprise (mf/use-fn subscribe-to-enterprise
(mf/use-fn
(fn [] (fn []
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription" (st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
:type "enterprise"})) :type "enterprise"}))
@@ -123,7 +141,8 @@
href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)] href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)]
(st/emit! (rt/nav-raw :href href))))) (st/emit! (rt/nav-raw :href href)))))
handle-accept-dialog (mf/use-fn handle-accept-dialog
(mf/use-fn
(fn [] (fn []
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management" (st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
::ev/origin "settings" ::ev/origin "settings"
@@ -133,21 +152,29 @@
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)] href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
(st/emit! (rt/nav-raw :href href))) (st/emit! (rt/nav-raw :href href)))
(modal/hide!))) (modal/hide!)))
handle-close-dialog (mf/use-fn
handle-close-dialog
(mf/use-fn
(fn [] (fn []
(st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"})) (st/emit! (ptk/event ::ev/event {::ev/name "close-subscription-modal"}))
(modal/hide!))) (modal/hide!)))
handle-unlimited-modal-step (mf/use-fn handle-unlimited-modal-step
(mf/use-fn
(mf/deps unlimited-modal-step) (mf/deps unlimited-modal-step)
(fn [] (fn []
(if (= unlimited-modal-step 1) (if (= unlimited-modal-step 1)
(reset! unlimited-modal-step* 2) (reset! unlimited-modal-step* 2)
(reset! unlimited-modal-step* 1)))) (reset! unlimited-modal-step* 1))))
show-editors-list* (mf/use-state false) show-editors-list*
show-editors-list (deref show-editors-list*) (mf/use-state false)
handle-click (mf/use-fn
show-editors-list
(deref show-editors-list*)
handle-click
(mf/use-fn
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! show-editors-list* not)))] (swap! show-editors-list* not)))]
@@ -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]
(when wasm/context-initialized?
(mw/emit! (mw/emit!
{:cmd :index/update-text-rect {:cmd :index/update-text-rect
:page-id (:current-page-id @st/state) :page-id (:current-page-id @st/state)
:shape-id id :shape-id id
:dimensions (get-text-dimensions id)})) :dimensions (get-text-dimensions id)})))
(defn- ensure-text-content (defn- ensure-text-content
@@ -198,6 +199,7 @@
(defn set-shape-children (defn set-shape-children
[children] [children]
(perf/begin-measure "set-shape-children") (perf/begin-measure "set-shape-children")
(let [children (into [] (filter uuid?) children)]
(case (count children) (case (count children)
0 0
(h/call wasm/internal-module "_set_children_0") (h/call wasm/internal-module "_set_children_0")
@@ -261,7 +263,7 @@
(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]
(when wasm/context-initialized?
(let [shape-id-buffer (uuid/get-u32 id)] (let [shape-id-buffer (uuid/get-u32 id)]
(h/call wasm/internal-module "_update_shape_text_layout_for" (h/call wasm/internal-module "_update_shape_text_layout_for"
(aget shape-id-buffer 0) (aget shape-id-buffer 0)
(aget shape-id-buffer 1) (aget shape-id-buffer 1)
(aget shape-id-buffer 2) (aget shape-id-buffer 2)
(aget shape-id-buffer 3)))) (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,28 +18,35 @@
"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]
(from-clipboard-event event default-options))
([event options]
(let [cdata (.-clipboardData ^js event)] (let [cdata (.-clipboardData ^js event)]
(from-data-transfer cdata))) (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)
@@ -53,12 +60,14 @@
(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,16 +126,27 @@
(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)
@@ -155,28 +167,34 @@
(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
(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)))))) (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();
} }