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)
- 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

View File

@@ -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}

View File

@@ -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

View File

@@ -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

View File

@@ -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 #

View File

@@ -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

View File

@@ -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.
---
<h1 id="design-tokens">Design Tokens</h1>

View File

@@ -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)))))

View File

@@ -77,13 +77,8 @@
(rx/of (rt/nav-raw :href href)))
(rx/throw cause))))
(defn fetch-profile
[]
(ptk/reify ::fetch-profile
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-profile)
(rx/mapcat (fn [profile]
(defn on-fetch-profile-success
[profile]
(if (and (contains? cf/flags :subscriptions)
(is-authenticated? profile))
(->> (rp/cmd! :get-subscription-usage {})
@@ -92,7 +87,15 @@
(rx/catch (fn [cause]
(js/console.error "unexpected error on obtaining subscription usage" cause)
(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/catch on-fetch-profile-exception)))))

View File

@@ -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)

View File

@@ -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
(mw/ask! {:cmd :thumbnails/generate-for-file-wasm
:revn revn
:file-id file-id
:width thumbnail-width}))
:width thumbnail-width})
(->> (mw/ask! {:cmd :thumbnails/generate-for-file
:revn revn
:file-id file-id

View File

@@ -83,9 +83,14 @@
::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
(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"))
@@ -93,29 +98,42 @@
"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-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)
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)
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 [data (:clean-data @form)
return-url (-> (rt/get-current-href) (rt/encode-url))
(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=" (:min-members data) "&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=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 (:min-members data)})
:quantity form-data-min-editors})
(rt/nav-raw :href href))))))
subscribe-to-enterprise (mf/use-fn
subscribe-to-enterprise
(mf/use-fn
(fn []
(st/emit! (ptk/event ::ev/event {::ev/name "create-trial-subscription"
:type "enterprise"}))
@@ -123,7 +141,8 @@
href (dm/str "payments/subscriptions/create?type=enterprise&returnUrl=" return-url)]
(st/emit! (rt/nav-raw :href href)))))
handle-accept-dialog (mf/use-fn
handle-accept-dialog
(mf/use-fn
(fn []
(st/emit! (ptk/event ::ev/event {::ev/name "open-subscription-management"
::ev/origin "settings"
@@ -133,21 +152,29 @@
href (dm/str "payments/subscriptions/show?returnUrl=" returnUrl)]
(st/emit! (rt/nav-raw :href href)))
(modal/hide!)))
handle-close-dialog (mf/use-fn
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
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
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)))]
@@ -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

View File

@@ -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);
}

View File

@@ -128,11 +128,12 @@
(defn update-text-rect!
[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)}))
:dimensions (get-text-dimensions id)})))
(defn- ensure-text-content
@@ -198,6 +199,7 @@
(defn set-shape-children
[children]
(perf/begin-measure "set-shape-children")
(let [children (into [] (filter uuid?) children)]
(case (count children)
0
(h/call wasm/internal-module "_set_children_0")
@@ -261,7 +263,7 @@
(mem.h32/write-uuid offset heap id))
offset
children)
(h/call wasm/internal-module "_set_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

View File

@@ -83,12 +83,13 @@
(defn update-text-layout
[id]
(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))))
(aget shape-id-buffer 3)))))
;; IMPORTANT: Only TTF fonts can be stored.
(defn- store-font-buffer

View File

@@ -18,28 +18,35 @@
"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]
([event]
(from-clipboard-event event default-options))
([event options]
(let [cdata (.-clipboardData ^js event)]
(from-data-transfer cdata)))
(from-data-transfer cdata options))))
(defn from-synthetic-clipboard-event
"Get clipboard stream from syntetic clipboard event"
[event]
([event options]
(let [target
(dom/get-target event)
@@ -53,12 +60,14 @@
(when-not (or content-editable? is-input?)
(-> event
(dom/event->browser-event)
(from-clipboard-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

View File

@@ -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<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]
* @returns {Promise<Array<Blob>>}
*/
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<Array<Blob>>}
*/
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());

View File

@@ -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,16 +126,27 @@
(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)
@@ -155,28 +167,34 @@
(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 %)))
(-> (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)))))

View File

@@ -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)
});
}

View File

@@ -332,6 +332,7 @@ fn propagate_reflow(
}
_ => {
// 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();
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();
}