mirror of
https://github.com/penpot/penpot.git
synced 2026-02-27 20:28:24 -05:00
Compare commits
22 Commits
niwinz-dev
...
ladybenko-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30de0bd79e | ||
|
|
ed23c55550 | ||
|
|
5b5c868a87 | ||
|
|
1a3ac6bdf8 | ||
|
|
de5d4f4292 | ||
|
|
2bd7c10e09 | ||
|
|
495371c079 | ||
|
|
75b1c0c1b1 | ||
|
|
5ea4b03108 | ||
|
|
0fef5b7e5d | ||
|
|
8a1fdd9dd1 | ||
|
|
a080a9e646 | ||
|
|
a728d5a5f2 | ||
|
|
6072234230 | ||
|
|
41f2877801 | ||
|
|
e2576d049a | ||
|
|
4db9c373e6 | ||
|
|
09a9407867 | ||
|
|
7be03e2ea6 | ||
|
|
05165ce014 | ||
|
|
96677713fc | ||
|
|
a12b59d101 |
@@ -31,6 +31,7 @@
|
||||
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
|
||||
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
|
||||
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
|
||||
- Fix 45 rotated board titles rendered incorrectly [Taiga #13306](https://tree.taiga.io/project/penpot/issue/13306)
|
||||
|
||||
## 2.13.3
|
||||
|
||||
|
||||
@@ -58,4 +58,3 @@
|
||||
(when (nil? (:data file))
|
||||
(migrate-file conn file)))
|
||||
(db/exec-one! conn ["drop table page cascade;"])))
|
||||
|
||||
|
||||
@@ -404,6 +404,8 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
return content !== "";
|
||||
}, { timeout: 1000 });
|
||||
|
||||
await this.page.waitForTimeout(3000);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -417,7 +419,8 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.viewport.click({ button: "right" });
|
||||
return this.page.getByText("Paste", { exact: true }).click();
|
||||
}
|
||||
return this.page.keyboard.press("ControlOrMeta+V");
|
||||
await this.page.keyboard.press("ControlOrMeta+V");
|
||||
await this.page.waitForTimeout(3000);
|
||||
}
|
||||
|
||||
async panOnViewportAt(x, y, width, height) {
|
||||
|
||||
@@ -383,24 +383,26 @@ test("User cut paste a component with path inside a variant", async ({
|
||||
|
||||
const variant = await findVariant(workspacePage, 0);
|
||||
|
||||
//Create a component
|
||||
// Create a component
|
||||
await workspacePage.ellipseShapeButton.click();
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
|
||||
await workspacePage.clickLeafLayer("Ellipse");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
await workspacePage.page.waitForTimeout(3000);
|
||||
|
||||
//Rename the component
|
||||
// Rename the component
|
||||
await workspacePage.layers.getByText("Ellipse").dblclick();
|
||||
await workspacePage.page
|
||||
.getByTestId("layer-item")
|
||||
.getByRole("textbox")
|
||||
.pressSequentially("button / hover");
|
||||
await workspacePage.page.keyboard.press("Enter");
|
||||
await workspacePage.page.waitForTimeout(3000);
|
||||
|
||||
//Cut the component
|
||||
// Cut the component
|
||||
await workspacePage.cut("keyboard");
|
||||
|
||||
//Paste the component inside the variant
|
||||
// Paste the component inside the variant
|
||||
await variant.container.click();
|
||||
await workspacePage.paste("keyboard");
|
||||
|
||||
@@ -427,6 +429,7 @@ test("User drag and drop a component with path inside a variant", async ({
|
||||
await workspacePage.clickWithDragViewportAt(500, 500, 20, 20);
|
||||
await workspacePage.clickLeafLayer("Ellipse");
|
||||
await workspacePage.page.keyboard.press("ControlOrMeta+k");
|
||||
await workspacePage.page.waitForTimeout(3000);
|
||||
|
||||
//Rename the component
|
||||
await workspacePage.layers.getByText("Ellipse").dblclick();
|
||||
|
||||
@@ -620,61 +620,68 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
;; We do not allow to apply tokens while text editor is open.
|
||||
(when (empty? (get state :workspace-editor-state))
|
||||
(let [attributes-to-remove
|
||||
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||
(cond
|
||||
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||
:else attributes-to-remove)]
|
||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)
|
||||
(ctob/get-tokens-in-active-sets))]
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [undo-id (js/Symbol)
|
||||
objects (dsh/lookup-page-objects state)
|
||||
selected-shapes (select-keys objects shape-ids)
|
||||
;; The classic text editor sets :workspace-editor-state; the WASM text editor
|
||||
;; does not, so we also check :workspace-local :edition for text shapes.
|
||||
(let [edition (get-in state [:workspace-local :edition])
|
||||
objects (dsh/lookup-page-objects state)
|
||||
text-editing? (and (some? edition)
|
||||
(= :text (:type (get objects edition))))]
|
||||
(when (and (empty? (get state :workspace-editor-state))
|
||||
(not text-editing?))
|
||||
(let [attributes-to-remove
|
||||
;; Remove atomic typography tokens when applying composite and vice-verca
|
||||
(cond
|
||||
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
|
||||
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
|
||||
:else attributes-to-remove)]
|
||||
(when-let [tokens (some-> (dsh/lookup-file-data state)
|
||||
(get :tokens-lib)
|
||||
(ctob/get-tokens-in-active-sets))]
|
||||
(->> (if (contains? cf/flags :tokenscript)
|
||||
(rx/of (ts/resolve-tokens tokens))
|
||||
(sd/resolve-tokens tokens))
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [undo-id (js/Symbol)
|
||||
objects (dsh/lookup-page-objects state)
|
||||
selected-shapes (select-keys objects shape-ids)
|
||||
|
||||
shapes (->> selected-shapes
|
||||
(filter (fn [[_ shape]]
|
||||
(or
|
||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||
(some ctt/spacing-margin-keys attributes))
|
||||
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
||||
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
shapes (->> selected-shapes
|
||||
(filter (fn [[_ shape]]
|
||||
(or
|
||||
(and (ctsl/any-layout-immediate-child? objects shape)
|
||||
(some ctt/spacing-margin-keys attributes))
|
||||
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
|
||||
(all-attrs-appliable-for-token? attributes (:type token)))))))
|
||||
shape-ids (d/nilv (keys shapes) [])
|
||||
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
|
||||
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||
:type type
|
||||
:applied-to attributes
|
||||
:applied-to-variant any-variant?}))
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes shape-ids (fn [shape]
|
||||
(cond-> shape
|
||||
attributes-to-remove
|
||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||
:always
|
||||
(update :applied-tokens merge tokenized-attributes)))))
|
||||
(when on-update-shape
|
||||
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
||||
;; Composed updates return observables and need to be executed differently
|
||||
(if (rx/observable? res)
|
||||
res
|
||||
(rx/of res))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id)))))))))))))
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
resolved-value (if (contains? cf/flags :tokenscript)
|
||||
(ts/tokenscript-symbols->penpot-unit resolved-value)
|
||||
resolved-value)
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
(st/emit! (ev/event {::ev/name "apply-tokens"
|
||||
:type type
|
||||
:applied-to attributes
|
||||
:applied-to-variant any-variant?}))
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dwsh/update-shapes shape-ids (fn [shape]
|
||||
(cond-> shape
|
||||
attributes-to-remove
|
||||
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
|
||||
:always
|
||||
(update :applied-tokens merge tokenized-attributes)))))
|
||||
(when on-update-shape
|
||||
(let [res (on-update-shape resolved-value shape-ids attributes)]
|
||||
;; Composed updates return observables and need to be executed differently
|
||||
(if (rx/observable? res)
|
||||
res
|
||||
(rx/of res))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id))))))))))))))
|
||||
|
||||
(defn apply-spacing-token-separated
|
||||
"Handles edge-case for spacing token when applying token via toggle button.
|
||||
|
||||
@@ -548,7 +548,7 @@
|
||||
modif-tree
|
||||
(dwm/build-modif-tree ids objects get-modifier)]
|
||||
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree)))
|
||||
(rx/of (dwm/apply-wasm-modifiers modif-tree :ignore-touched (:ignore-touched options))))
|
||||
|
||||
(let [page-id (or (:page-id options)
|
||||
(:current-page-id state))
|
||||
|
||||
@@ -116,6 +116,17 @@
|
||||
(ex/print-throwable cause :prefix "Unexpected Error")
|
||||
(show-not-blocking-error cause))))
|
||||
|
||||
(defmethod ptk/handle-error :wasm-non-blocking
|
||||
[error]
|
||||
(when-let [cause (::instance error)]
|
||||
(show-not-blocking-error cause)))
|
||||
|
||||
(defmethod ptk/handle-error :wasm-critical
|
||||
[error]
|
||||
(when-let [cause (::instance error)]
|
||||
(ex/print-throwable cause :prefix "WASM critical error"))
|
||||
(st/emit! (rt/assign-exception error)))
|
||||
|
||||
;; We receive a explicit authentication error; If the uri is for
|
||||
;; workspace, dashboard, viewer or settings, then assign the exception
|
||||
;; for show the error page. Otherwise this explicitly clears all
|
||||
@@ -327,20 +338,24 @@
|
||||
(str/starts-with? message "invalid props on component")
|
||||
(str/starts-with? message "Unexpected token "))))
|
||||
|
||||
(handle-uncaught [cause]
|
||||
(when cause
|
||||
(set! last-exception cause)
|
||||
(let [data (ex-data cause)
|
||||
type (get data :type)]
|
||||
(if (#{:wasm-critical :wasm-non-blocking} type)
|
||||
(on-error cause)
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(ex/print-throwable cause :prefix "Uncaught Exception")
|
||||
(ts/schedule #(show-not-blocking-error cause)))))))
|
||||
|
||||
(on-unhandled-error [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "error")]
|
||||
(set! last-exception cause)
|
||||
(when-not (is-ignorable-exception? cause)
|
||||
(ex/print-throwable cause :prefix "Uncaught Exception")
|
||||
(ts/schedule #(show-not-blocking-error cause)))))
|
||||
(handle-uncaught (unchecked-get event "error")))
|
||||
|
||||
(on-unhandled-rejection [event]
|
||||
(.preventDefault ^js event)
|
||||
(when-let [cause (unchecked-get event "reason")]
|
||||
(set! last-exception cause)
|
||||
(ex/print-throwable cause :prefix "Uncaught Rejection")
|
||||
(ts/schedule #(show-not-blocking-error cause))))]
|
||||
(handle-uncaught (unchecked-get event "reason")))]
|
||||
|
||||
(.addEventListener g/window "error" on-unhandled-error)
|
||||
(.addEventListener g/window "unhandledrejection" on-unhandled-rejection)
|
||||
|
||||
@@ -183,9 +183,6 @@
|
||||
[id]
|
||||
(l/derived #(contains? % id) selected-shapes))
|
||||
|
||||
(def highlighted-shapes
|
||||
(l/derived :highlighted workspace-local))
|
||||
|
||||
(def export-in-progress?
|
||||
(l/derived :export-in-progress? export))
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
[app.common.types.component :as ctk]
|
||||
[app.main.data.viewer :as dv]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner]]
|
||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item-inner*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[okulary.core :as l]
|
||||
@@ -26,7 +26,6 @@
|
||||
(mf/defc layer-item
|
||||
[{:keys [item selected objects depth component-child? hide-toggle?] :as props}]
|
||||
(let [id (:id item)
|
||||
hidden? (:hidden item)
|
||||
selected? (contains? selected id)
|
||||
item-ref (mf/use-ref nil)
|
||||
depth (+ depth 1)
|
||||
@@ -68,18 +67,17 @@
|
||||
(when (and (= (count selected) 1) selected?)
|
||||
(dom/scroll-into-view-if-needed! (mf/ref-val item-ref) true))))
|
||||
|
||||
[:& layer-item-inner
|
||||
[:> layer-item-inner*
|
||||
{:ref item-ref
|
||||
:item item
|
||||
:depth depth
|
||||
:read-only? true
|
||||
:highlighted? false
|
||||
:selected? selected?
|
||||
:component-tree? component-tree?
|
||||
:hidden? hidden?
|
||||
:filtered? false
|
||||
:expanded? expanded?
|
||||
:hide-toggle? hide-toggle?
|
||||
:is-read-only true
|
||||
:is-highlighted false
|
||||
:is-selected selected?
|
||||
:is-component-tree component-tree?
|
||||
:is-filtered false
|
||||
:is-expanded expanded?
|
||||
:hide-toggle hide-toggle?
|
||||
:on-select-shape select-shape
|
||||
:on-toggle-collapse toggle-collapse}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.component :as ctk]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.container :as ctn]
|
||||
@@ -37,6 +38,8 @@
|
||||
(defonce ^:private sidebar-hover-queue (atom {:enter #{} :leave #{}}))
|
||||
(defonce ^:private sidebar-hover-pending? (atom false))
|
||||
|
||||
(def ^:const default-chunk-size 50)
|
||||
|
||||
(defn- schedule-sidebar-hover-flush []
|
||||
(when (compare-and-set! sidebar-hover-pending? false true)
|
||||
(ts/raf
|
||||
@@ -48,12 +51,11 @@
|
||||
(when (seq enter)
|
||||
(apply st/emit! (map dw/highlight-shape enter))))))))
|
||||
|
||||
(mf/defc layer-item-inner
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [item depth parent-size name-ref children ref style
|
||||
(mf/defc layer-item-inner*
|
||||
[{:keys [item depth parent-size name-ref children ref style rename-id
|
||||
;; Flags
|
||||
read-only? highlighted? selected? component-tree?
|
||||
filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle?
|
||||
is-read-only is-highlighted is-selected is-component-tree
|
||||
is-filtered is-expanded dnd-over dnd-over-top dnd-over-bot hide-toggle
|
||||
;; Callbacks
|
||||
on-select-shape on-context-menu on-pointer-enter on-pointer-leave on-zoom-to-selected
|
||||
on-toggle-collapse on-enable-drag on-disable-drag on-toggle-visibility on-toggle-blocking]}]
|
||||
@@ -64,7 +66,7 @@
|
||||
hidden? (:hidden item)
|
||||
has-shapes? (-> item :shapes seq boolean)
|
||||
touched? (-> item :touched seq boolean)
|
||||
parent-board? (and (cfh/frame-shape? item)
|
||||
root-board? (and (cfh/frame-shape? item)
|
||||
(= uuid/zero (:parent-id item)))
|
||||
absolute? (ctl/item-absolute? item)
|
||||
is-variant? (ctk/is-variant? item)
|
||||
@@ -73,9 +75,11 @@
|
||||
variant-name (when is-variant? (:variant-name item))
|
||||
variant-error (when is-variant? (:variant-error item))
|
||||
|
||||
data (deref refs/workspace-data)
|
||||
component (ctkl/get-component data (:component-id item))
|
||||
variant-properties (:variant-properties component)
|
||||
component-id (get item :component-id)
|
||||
data (mf/deref refs/workspace-data)
|
||||
variant-properties (-> (ctkl/get-component data component-id)
|
||||
(get :variant-properties))
|
||||
|
||||
icon-shape (usi/get-shape-icon item)]
|
||||
|
||||
[:*
|
||||
@@ -85,30 +89,30 @@
|
||||
:on-context-menu on-context-menu
|
||||
:data-testid "layer-row"
|
||||
:role "checkbox"
|
||||
:aria-checked selected?
|
||||
:aria-checked is-selected
|
||||
:class (stl/css-case
|
||||
:layer-row true
|
||||
:highlight highlighted?
|
||||
:highlight is-highlighted
|
||||
:component (ctk/instance-head? item)
|
||||
:masked (:masked-group item)
|
||||
:selected selected?
|
||||
:selected is-selected
|
||||
:type-frame (cfh/frame-shape? item)
|
||||
:type-bool (cfh/bool-shape? item)
|
||||
:type-comp (or component-tree? is-variant-container?)
|
||||
:type-comp (or is-component-tree is-variant-container?)
|
||||
:hidden hidden?
|
||||
:dnd-over dnd-over?
|
||||
:dnd-over-top dnd-over-top?
|
||||
:dnd-over-bot dnd-over-bot?
|
||||
:root-board parent-board?)
|
||||
:dnd-over dnd-over
|
||||
:dnd-over-top dnd-over-top
|
||||
:dnd-over-bot dnd-over-bot
|
||||
:root-board root-board?)
|
||||
:style style}
|
||||
[:span {:class (stl/css-case
|
||||
:tab-indentation true
|
||||
:filtered filtered?)
|
||||
:filtered is-filtered)
|
||||
:style {"--depth" depth}}]
|
||||
[:div {:class (stl/css-case
|
||||
:element-list-body true
|
||||
:filtered filtered?
|
||||
:selected selected?
|
||||
:filtered is-filtered
|
||||
:selected is-selected
|
||||
:icon-layer (= (:type item) :icon))
|
||||
:style {"--depth" depth}
|
||||
:on-pointer-enter on-pointer-enter
|
||||
@@ -117,12 +121,12 @@
|
||||
|
||||
(if (< 0 (count (:shapes item)))
|
||||
[:div {:class (stl/css :button-content)}
|
||||
(when (and (not hide-toggle?) (not filtered?))
|
||||
(when (and (not hide-toggle) (not is-filtered))
|
||||
[:button {:class (stl/css-case
|
||||
:toggle-content true
|
||||
:inverse expanded?)
|
||||
:inverse is-expanded)
|
||||
:data-testid "toggle-content"
|
||||
:aria-expanded expanded?
|
||||
:aria-expanded is-expanded
|
||||
:on-click on-toggle-collapse}
|
||||
deprecated-icon/arrow])
|
||||
|
||||
@@ -133,7 +137,7 @@
|
||||
[:> icon* {:icon-id icon-shape :size "s" :data-testid (str "icon-" icon-shape)}]]]
|
||||
|
||||
[:div {:class (stl/css :button-content)}
|
||||
(when (not ^boolean filtered?)
|
||||
(when (not ^boolean is-filtered)
|
||||
[:span {:class (stl/css :toggle-content)}])
|
||||
[:div {:class (stl/css :icon-shape)
|
||||
:on-double-click on-zoom-to-selected}
|
||||
@@ -142,25 +146,26 @@
|
||||
[:> icon* {:icon-id icon-shape :size "s" :data-testid (str "icon-" icon-shape)}]]])
|
||||
|
||||
[:> layer-name* {:ref name-ref
|
||||
:rename-id rename-id
|
||||
:shape-id id
|
||||
:shape-name name
|
||||
:is-shape-touched touched?
|
||||
:disabled-double-click read-only?
|
||||
:disabled-double-click is-read-only
|
||||
:on-start-edit on-disable-drag
|
||||
:on-stop-edit on-enable-drag
|
||||
:depth depth
|
||||
:is-blocked blocked?
|
||||
:parent-size parent-size
|
||||
:is-selected selected?
|
||||
:type-comp (or component-tree? is-variant-container?)
|
||||
:is-selected is-selected
|
||||
:type-comp (or is-component-tree is-variant-container?)
|
||||
:type-frame (cfh/frame-shape? item)
|
||||
:variant-id variant-id
|
||||
:variant-name variant-name
|
||||
:variant-properties variant-properties
|
||||
:variant-error variant-error
|
||||
:component-id (:id component)
|
||||
:component-id component-id
|
||||
:is-hidden hidden?}]]
|
||||
(when (not read-only?)
|
||||
(when (not ^boolean is-read-only)
|
||||
[:div {:class (stl/css-case
|
||||
:element-actions true
|
||||
:is-parent has-shapes?
|
||||
@@ -185,41 +190,86 @@
|
||||
|
||||
children]))
|
||||
|
||||
;; Memoized for performance
|
||||
(mf/defc layer-item
|
||||
{::mf/props :obj
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted style render-children?]
|
||||
:or {render-children? true}}]
|
||||
(let [id (:id item)
|
||||
blocked? (:blocked item)
|
||||
hidden? (:hidden item)
|
||||
(mf/defc layer-item*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [index item selected objects rename-id
|
||||
is-sortable is-filtered depth is-component-child
|
||||
highlighted style render-children parent-size]
|
||||
:or {render-children true}}]
|
||||
(let [id (get item :id)
|
||||
blocked? (get item :blocked)
|
||||
hidden? (get item :hidden)
|
||||
|
||||
shapes (get item :shapes)
|
||||
shapes (mf/with-memo [shapes objects]
|
||||
(loop [counter 0
|
||||
shapes (seq shapes)
|
||||
result (list)]
|
||||
|
||||
(if-let [id (first shapes)]
|
||||
(if-let [obj (get objects id)]
|
||||
(do
|
||||
;; NOTE: this is a bit hacky, but reduces substantially
|
||||
;; the allocation; If we use enumeration, we allocate
|
||||
;; new sequence and add one iteration on each render,
|
||||
;; independently if objects are changed or not. If we
|
||||
;; store counter on metadata, we still need to create a
|
||||
;; new allocation for each shape; with this method we
|
||||
;; bypass this by mutating a private property on the
|
||||
;; object removing extra allocation and extra iteration
|
||||
;; on every request.
|
||||
(unchecked-set obj "__$__counter" counter)
|
||||
(recur (inc counter)
|
||||
(rest shapes)
|
||||
(conj result obj)))
|
||||
(recur (inc counter)
|
||||
(rest shapes)
|
||||
result))
|
||||
|
||||
(-> result vec not-empty))))
|
||||
|
||||
drag-disabled* (mf/use-state false)
|
||||
drag-disabled? (deref drag-disabled*)
|
||||
|
||||
scroll-to-middle? (mf/use-var true)
|
||||
scroll-middle-ref (mf/use-ref true)
|
||||
expanded-iref (mf/with-memo [id]
|
||||
(-> (l/in [:expanded id])
|
||||
(l/derived refs/workspace-local)))
|
||||
expanded? (mf/deref expanded-iref)
|
||||
(l/derived #(dm/get-in % [:expanded id]) refs/workspace-local))
|
||||
is-expanded (mf/deref expanded-iref)
|
||||
|
||||
selected? (contains? selected id)
|
||||
highlighted? (contains? highlighted id)
|
||||
is-selected (contains? selected id)
|
||||
is-highlighted (contains? highlighted id)
|
||||
|
||||
container? (or (cfh/frame-shape? item)
|
||||
(cfh/group-shape? item))
|
||||
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
parent-board? (and (cfh/frame-shape? item)
|
||||
is-read-only (mf/use-ctx ctx/workspace-read-only?)
|
||||
root-board? (and (cfh/frame-shape? item)
|
||||
(= uuid/zero (:parent-id item)))
|
||||
|
||||
name-node-ref (mf/use-ref)
|
||||
|
||||
depth (+ depth 1)
|
||||
|
||||
is-component-tree (or ^boolean is-component-child
|
||||
^boolean (ctk/instance-root? item)
|
||||
^boolean (ctk/instance-head? item))
|
||||
|
||||
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))
|
||||
|
||||
;; Lazy loading of child elements via IntersectionObserver
|
||||
children-count* (mf/use-state 0)
|
||||
children-count (deref children-count*)
|
||||
|
||||
lazy-ref (mf/use-ref nil)
|
||||
observer-ref (mf/use-ref nil)
|
||||
|
||||
toggle-collapse
|
||||
(mf/use-fn
|
||||
(mf/deps expanded?)
|
||||
(mf/deps is-expanded)
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(if (and expanded? (kbd/shift? event))
|
||||
(if (and is-expanded (kbd/shift? event))
|
||||
(st/emit! (dwc/collapse-all))
|
||||
(st/emit! (dwc/toggle-collapse id)))))
|
||||
|
||||
@@ -244,13 +294,13 @@
|
||||
|
||||
select-shape
|
||||
(mf/use-fn
|
||||
(mf/deps id filtered? objects)
|
||||
(mf/deps id is-filtered objects)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(reset! scroll-to-middle? false)
|
||||
(mf/set-ref-val! scroll-middle-ref false)
|
||||
(cond
|
||||
(kbd/shift? event)
|
||||
(if filtered?
|
||||
(if is-filtered
|
||||
(st/emit! (dw/shift-select-shapes id objects))
|
||||
(st/emit! (dw/shift-select-shapes id)))
|
||||
|
||||
@@ -285,11 +335,11 @@
|
||||
|
||||
on-context-menu
|
||||
(mf/use-fn
|
||||
(mf/deps item read-only?)
|
||||
(mf/deps item is-read-only)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(when-not read-only?
|
||||
(when-not is-read-only
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(st/emit! (dw/show-shape-context-menu {:position pos :shape item}))))))
|
||||
|
||||
@@ -302,7 +352,7 @@
|
||||
|
||||
on-drop
|
||||
(mf/use-fn
|
||||
(mf/deps id objects expanded? selected)
|
||||
(mf/deps id objects is-expanded selected)
|
||||
(fn [side _data]
|
||||
(let [single? (= (count selected) 1)
|
||||
same? (and single? (= (first selected) id))]
|
||||
@@ -315,32 +365,34 @@
|
||||
(= side :center)
|
||||
id
|
||||
|
||||
(and expanded? (= side :bot) (d/not-empty? (:shapes shape)))
|
||||
(and is-expanded (= side :bot) (d/not-empty? (:shapes shape)))
|
||||
id
|
||||
|
||||
:else
|
||||
(cfh/get-parent-id objects id))
|
||||
|
||||
[parent-id _] (ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
|
||||
[parent-id _]
|
||||
(ctn/find-valid-parent-and-frame-ids parent-id objects (map #(get objects %) selected) false files)
|
||||
|
||||
parent (get objects parent-id)
|
||||
current-index (d/index-of (:shapes parent) id)
|
||||
|
||||
to-index (cond
|
||||
(= side :center) 0
|
||||
(and expanded? (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
|
||||
(and is-expanded (= side :bot) (d/not-empty? (:shapes shape))) (count (:shapes parent))
|
||||
;; target not found in parent (while lazy loading)
|
||||
(neg? current-index) nil
|
||||
(= side :top) (inc current-index)
|
||||
:else current-index)]
|
||||
|
||||
(when (some? to-index)
|
||||
(st/emit! (dw/relocate-selected-shapes parent-id to-index))))))))
|
||||
|
||||
on-hold
|
||||
(mf/use-fn
|
||||
(mf/deps id expanded?)
|
||||
(mf/deps id is-expanded)
|
||||
(fn []
|
||||
(when-not expanded?
|
||||
(when-not is-expanded
|
||||
(st/emit! (dwc/toggle-collapse id)))))
|
||||
|
||||
zoom-to-selected
|
||||
@@ -361,112 +413,114 @@
|
||||
:data {:id (:id item)
|
||||
:index index
|
||||
:name (:name item)}
|
||||
:draggable? (and
|
||||
sortable?
|
||||
(not read-only?)
|
||||
(not (ctn/has-any-copy-parent? objects item)))) ;; We don't want to change the structure of component copies
|
||||
;; We don't want to change the structure of component copies
|
||||
:draggable? (and ^boolean is-sortable
|
||||
^boolean (not is-read-only)
|
||||
^boolean (not (ctn/has-any-copy-parent? objects item))))]
|
||||
|
||||
ref (mf/use-ref)
|
||||
depth (+ depth 1)
|
||||
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
|
||||
|
||||
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))
|
||||
|
||||
;; Lazy loading of child elements via IntersectionObserver
|
||||
children-count* (mf/use-state 0)
|
||||
children-count (deref children-count*)
|
||||
lazy-ref (mf/use-ref nil)
|
||||
observer-var (mf/use-var nil)
|
||||
chunk-size 50]
|
||||
|
||||
(mf/with-effect [selected? selected]
|
||||
(mf/with-effect [is-selected selected]
|
||||
(let [single? (= (count selected) 1)
|
||||
node (mf/ref-val ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
parent-node (dom/get-parent-at node 2)
|
||||
first-child-node (dom/get-first-child parent-node)
|
||||
node (mf/ref-val name-node-ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
parent-node (dom/get-parent-at node 2)
|
||||
first-child-node (dom/get-first-child parent-node)
|
||||
scroll-to-middle? (mf/ref-val scroll-middle-ref)
|
||||
|
||||
subid
|
||||
(when (and single? selected? @scroll-to-middle?)
|
||||
(when (and ^boolean single?
|
||||
^boolean is-selected
|
||||
^boolean scroll-to-middle?)
|
||||
(ts/schedule
|
||||
100
|
||||
#(when (and node scroll-node)
|
||||
(let [scroll-distance-ratio (dom/get-scroll-distance-ratio node scroll-node)
|
||||
scroll-behavior (if (> scroll-distance-ratio 1) "instant" "smooth")]
|
||||
(dom/scroll-into-view-if-needed! first-child-node #js {:block "center" :behavior scroll-behavior :inline "start"})
|
||||
(reset! scroll-to-middle? true)))))]
|
||||
(mf/set-ref-val! scroll-middle-ref true)))))]
|
||||
|
||||
#(when (some? subid)
|
||||
(rx/dispose! subid))))
|
||||
|
||||
;; Setup scroll-driven lazy loading when expanded
|
||||
;; and ensures selected children are loaded immediately
|
||||
(mf/with-effect [expanded? (:shapes item) selected]
|
||||
(let [shapes-vec (:shapes item)
|
||||
total (count shapes-vec)]
|
||||
(if expanded?
|
||||
(mf/with-effect [is-expanded shapes selected]
|
||||
(let [total (count shapes)]
|
||||
(if ^boolean is-expanded
|
||||
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
|
||||
;; Find if any selected id is a direct child and get its render index
|
||||
selected-child-render-idx
|
||||
(when (and (> total chunk-size) (seq selected))
|
||||
(let [shapes-reversed (vec (reverse shapes-vec))]
|
||||
(some (fn [sel-id]
|
||||
(let [idx (.indexOf shapes-reversed sel-id)]
|
||||
(when (>= idx 0) idx)))
|
||||
selected)))
|
||||
(when (> total default-chunk-size)
|
||||
(some (fn [sel-id]
|
||||
(let [idx (.indexOf shapes sel-id)]
|
||||
(when (>= idx 0) idx)))
|
||||
selected))
|
||||
|
||||
;; Load at least enough to include the selected child plus extra
|
||||
;; for context (so it can be centered in the scroll view)
|
||||
min-count (if selected-child-render-idx
|
||||
(+ selected-child-render-idx chunk-size)
|
||||
chunk-size)
|
||||
current @children-count*
|
||||
new-count (min total (max current chunk-size min-count))]
|
||||
min-count
|
||||
(if selected-child-render-idx
|
||||
(+ selected-child-render-idx default-chunk-size)
|
||||
default-chunk-size)
|
||||
|
||||
current-count
|
||||
@children-count*
|
||||
|
||||
new-count
|
||||
(mth/min total (mth/max current-count default-chunk-size min-count))]
|
||||
|
||||
(reset! children-count* new-count))
|
||||
(reset! children-count* 0))))
|
||||
|
||||
(reset! children-count* 0))
|
||||
|
||||
(fn []
|
||||
(when-let [obs (mf/ref-val observer-ref)]
|
||||
(.disconnect obs)
|
||||
(mf/set-ref-val! obs nil)))))
|
||||
|
||||
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
||||
;; and (shapes item) to reconnect observer after shape changes
|
||||
(mf/with-effect [children-count expanded? (:shapes item)]
|
||||
(let [total (count (:shapes item))
|
||||
node (mf/ref-val ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
lazy-node (mf/ref-val lazy-ref)]
|
||||
(mf/with-effect [children-count is-expanded shapes]
|
||||
(let [total (count shapes)
|
||||
name-node (mf/ref-val name-node-ref)
|
||||
scroll-node (dom/get-parent-with-data name-node "scroll-container")
|
||||
lazy-node (mf/ref-val lazy-ref)]
|
||||
|
||||
;; Disconnect previous observer
|
||||
(when-let [obs ^js @observer-var]
|
||||
(when-let [obs (mf/ref-val observer-ref)]
|
||||
(.disconnect obs)
|
||||
(reset! observer-var nil))
|
||||
(mf/set-ref-val! observer-ref nil))
|
||||
|
||||
;; Setup new observer if there are more children to load
|
||||
(when (and expanded?
|
||||
(< children-count total)
|
||||
scroll-node
|
||||
lazy-node)
|
||||
(when (and ^boolean is-expanded
|
||||
^boolean (< children-count total)
|
||||
^boolean scroll-node
|
||||
^boolean lazy-node)
|
||||
(let [cb (fn [entries]
|
||||
(when (and (seq entries)
|
||||
(.-isIntersecting (first entries)))
|
||||
(when (and (pos? (alength entries))
|
||||
(.-isIntersecting ^js (aget entries 0)))
|
||||
;; Load next chunk when sentinel intersects
|
||||
(let [current @children-count*
|
||||
next-count (min total (+ current chunk-size))]
|
||||
(let [next-count (mth/min total (+ children-count default-chunk-size))]
|
||||
(reset! children-count* next-count))))
|
||||
observer (js/IntersectionObserver. cb #js {:root scroll-node})]
|
||||
(.observe observer lazy-node)
|
||||
(reset! observer-var observer)))))
|
||||
(mf/set-ref-val! observer-ref observer)))))
|
||||
|
||||
[:& layer-item-inner
|
||||
[:> layer-item-inner*
|
||||
{:ref dref
|
||||
:item item
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:name-ref ref
|
||||
:read-only? read-only?
|
||||
:highlighted? highlighted?
|
||||
:selected? selected?
|
||||
:component-tree? component-tree?
|
||||
:filtered? filtered?
|
||||
:expanded? expanded?
|
||||
:dnd-over? (= (:over dprops) :center)
|
||||
:dnd-over-top? (= (:over dprops) :top)
|
||||
:dnd-over-bot? (= (:over dprops) :bot)
|
||||
:name-ref name-node-ref
|
||||
:rename-id rename-id
|
||||
:is-read-only is-read-only
|
||||
:is-highlighted is-highlighted
|
||||
:is-selected is-selected
|
||||
:is-component-tree is-component-tree
|
||||
:is-filtered is-filtered
|
||||
:is-expanded is-expanded
|
||||
:dnd-over (= (:over dprops) :center)
|
||||
:dnd-over-top (= (:over dprops) :top)
|
||||
:dnd-over-bot (= (:over dprops) :bot)
|
||||
:on-select-shape select-shape
|
||||
:on-context-menu on-context-menu
|
||||
:on-pointer-enter on-pointer-enter
|
||||
@@ -479,29 +533,28 @@
|
||||
:on-toggle-blocking toggle-blocking
|
||||
:style style}
|
||||
|
||||
(when (and render-children?
|
||||
(:shapes item)
|
||||
expanded?)
|
||||
(when (and ^boolean render-children
|
||||
^boolean shapes
|
||||
^boolean is-expanded)
|
||||
[:div {:class (stl/css-case
|
||||
:element-children true
|
||||
:parent-selected selected?
|
||||
:sticky-children parent-board?)
|
||||
:parent-selected is-selected
|
||||
:sticky-children root-board?)
|
||||
:data-testid (dm/str "children-" id)}
|
||||
(let [all-children (reverse (d/enumerate (:shapes item)))
|
||||
visible (take children-count all-children)]
|
||||
(for [[index id] visible]
|
||||
(when-let [item (get objects id)]
|
||||
[:& layer-item
|
||||
{:item item
|
||||
:highlighted highlighted
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key (dm/str id)
|
||||
:sortable? sortable?
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:component-child? component-tree?}])))
|
||||
(when (< children-count (count (:shapes item)))
|
||||
(for [item (take children-count shapes)]
|
||||
[:> layer-item*
|
||||
{:item item
|
||||
:rename-id rename-id
|
||||
:highlighted highlighted
|
||||
:selected selected
|
||||
:index (unchecked-get item "__$__counter")
|
||||
:objects objects
|
||||
:key (dm/str (get item :id))
|
||||
:is-sortable is-sortable
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:is-component-child is-component-tree}])
|
||||
|
||||
(when (< children-count (count shapes))
|
||||
[:div {:ref lazy-ref
|
||||
:class (stl/css :lazy-load-sentinel)}])])]))
|
||||
|
||||
@@ -16,39 +16,35 @@
|
||||
[app.util.dom :as dom]
|
||||
[app.util.keyboard :as kbd]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private space-for-icons 110)
|
||||
|
||||
(def lens:shape-for-rename
|
||||
(-> (l/in [:workspace-local :shape-for-rename])
|
||||
(l/derived st/state)))
|
||||
(def ^:private ^:const space-for-icons 110)
|
||||
|
||||
(mf/defc layer-name*
|
||||
{::mf/wrap-props false
|
||||
::mf/forward-ref true}
|
||||
[{:keys [shape-id shape-name is-shape-touched disabled-double-click
|
||||
[{:keys [shape-id rename-id shape-name is-shape-touched disabled-double-click
|
||||
on-start-edit on-stop-edit depth parent-size is-selected
|
||||
type-comp type-frame component-id is-hidden is-blocked
|
||||
variant-id variant-name variant-properties variant-error]} external-ref]
|
||||
variant-id variant-name variant-properties variant-error ref]}]
|
||||
|
||||
(let [edition* (mf/use-state false)
|
||||
edition? (deref edition*)
|
||||
|
||||
local-ref (mf/use-ref)
|
||||
ref (d/nilv external-ref local-ref)
|
||||
ref (d/nilv ref local-ref)
|
||||
|
||||
shape-for-rename (mf/deref lens:shape-for-rename)
|
||||
shape-name
|
||||
(if variant-id
|
||||
(d/nilv variant-error variant-name)
|
||||
shape-name)
|
||||
|
||||
shape-name (if variant-id
|
||||
(d/nilv variant-error variant-name)
|
||||
shape-name)
|
||||
default-value
|
||||
(mf/with-memo [variant-id variant-error variant-properties]
|
||||
(if variant-id
|
||||
(or variant-error (ctv/properties-map->formula variant-properties))
|
||||
shape-name))
|
||||
|
||||
default-value (if variant-id
|
||||
(or variant-error (ctv/properties-map->formula variant-properties))
|
||||
shape-name)
|
||||
|
||||
has-path? (str/includes? shape-name "/")
|
||||
has-path?
|
||||
(str/includes? shape-name "/")
|
||||
|
||||
start-edit
|
||||
(mf/use-fn
|
||||
@@ -85,10 +81,11 @@
|
||||
(when (kbd/enter? event) (accept-edit))
|
||||
(when (kbd/esc? event) (cancel-edit))))
|
||||
|
||||
parent-size (dm/str (- parent-size space-for-icons) "px")]
|
||||
parent-size
|
||||
(dm/str (- parent-size space-for-icons) "px")]
|
||||
|
||||
(mf/with-effect [shape-for-rename edition? start-edit shape-id]
|
||||
(when (and (= shape-for-rename shape-id)
|
||||
(mf/with-effect [rename-id edition? start-edit shape-id]
|
||||
(when (and (= rename-id shape-id)
|
||||
(not ^boolean edition?))
|
||||
(start-edit)))
|
||||
|
||||
@@ -110,21 +107,24 @@
|
||||
:auto-focus true
|
||||
:id (dm/str "layer-name-" shape-id)
|
||||
:default-value (d/nilv default-value "")}]
|
||||
|
||||
[:*
|
||||
[:span
|
||||
{:class (stl/css-case
|
||||
:element-name true
|
||||
:left-ellipsis has-path?
|
||||
:selected is-selected
|
||||
:hidden is-hidden
|
||||
:type-comp type-comp
|
||||
:type-frame type-frame)
|
||||
:id (dm/str "layer-name-" shape-id)
|
||||
:style {"--depth" depth "--parent-size" parent-size}
|
||||
:ref ref
|
||||
:on-double-click start-edit}
|
||||
(if (dbg/enabled? :show-ids)
|
||||
(str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
|
||||
[:span {:class (stl/css-case
|
||||
:element-name true
|
||||
:left-ellipsis has-path?
|
||||
:selected is-selected
|
||||
:hidden is-hidden
|
||||
:type-comp type-comp
|
||||
:type-frame type-frame)
|
||||
:id (dm/str "layer-name-" shape-id)
|
||||
:style {"--depth" depth "--parent-size" parent-size}
|
||||
:ref ref
|
||||
:on-double-click start-edit}
|
||||
|
||||
(if ^boolean (dbg/enabled? :show-ids)
|
||||
(dm/str (d/nilv shape-name "") " | " (str/slice (str shape-id) 24))
|
||||
(d/nilv shape-name ""))]
|
||||
(when (and (dbg/enabled? :show-touched) ^boolean is-shape-touched)
|
||||
|
||||
(when (and ^boolean (dbg/enabled? :show-touched)
|
||||
^boolean is-shape-touched)
|
||||
[:span {:class (stl/css :element-name-touched)} "*"])])))
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.notifications.badge :refer [badge-notification]]
|
||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item]]
|
||||
[app.main.ui.workspace.sidebar.layer-item :refer [layer-item*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@@ -31,92 +31,160 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[goog.events :as events]
|
||||
[rumext.v2 :as mf])
|
||||
(:import
|
||||
goog.events.EventType))
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private ref:highlighted-shapes
|
||||
(l/derived (fn [local]
|
||||
(-> local
|
||||
(get :highlighted)
|
||||
(not-empty)))
|
||||
refs/workspace-local))
|
||||
|
||||
(def ^:private ref:shape-for-rename
|
||||
(l/derived (l/key :shape-for-rename) refs/workspace-local))
|
||||
|
||||
(defn- use-selected-shapes
|
||||
"A convencience hook wrapper for get selected shapes"
|
||||
[]
|
||||
(let [selected (mf/deref refs/selected-shapes)]
|
||||
(hooks/use-equal-memo selected)))
|
||||
|
||||
;; This components is a piece for sharding equality check between top
|
||||
;; level frames and try to avoid rerender frames that are does not
|
||||
;; affected by the selected set.
|
||||
(mf/defc frame-wrapper
|
||||
{::mf/props :obj}
|
||||
(mf/defc frame-wrapper*
|
||||
[{:keys [selected] :as props}]
|
||||
(let [pending-selected (mf/use-var selected)
|
||||
current-selected (mf/use-state selected)
|
||||
props (mf/spread-object props {:selected @current-selected})
|
||||
(let [pending-selected-ref
|
||||
(mf/use-ref selected)
|
||||
|
||||
current-selected
|
||||
(mf/use-state selected)
|
||||
|
||||
props
|
||||
(mf/spread-object props {:selected @current-selected})
|
||||
|
||||
set-selected
|
||||
(mf/use-memo
|
||||
(fn []
|
||||
(throttle-fn
|
||||
50
|
||||
#(when-let [pending-selected @pending-selected]
|
||||
(reset! current-selected pending-selected)))))]
|
||||
(mf/with-memo []
|
||||
(throttle-fn 50 #(when-let [pending-selected (mf/ref-val pending-selected-ref)]
|
||||
(reset! current-selected pending-selected))))]
|
||||
|
||||
(mf/with-effect [selected set-selected]
|
||||
(reset! pending-selected selected)
|
||||
(set-selected)
|
||||
(mf/set-ref-val! pending-selected-ref selected)
|
||||
(^function set-selected)
|
||||
(fn []
|
||||
(reset! pending-selected nil)
|
||||
#(rx/dispose! set-selected)))
|
||||
(mf/set-ref-val! pending-selected-ref nil)
|
||||
(rx/dispose! set-selected)))
|
||||
|
||||
[:> layer-item props]))
|
||||
[:> layer-item* props]))
|
||||
|
||||
(mf/defc layers-tree*
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects is-filtered parent-size] :as props}]
|
||||
(let [selected (use-selected-shapes)
|
||||
highlighted (mf/deref ref:highlighted-shapes)
|
||||
root (get objects uuid/zero)
|
||||
|
||||
rename-id (mf/deref ref:shape-for-rename)
|
||||
|
||||
shapes (get root :shapes)
|
||||
shapes (mf/with-memo [shapes objects]
|
||||
(loop [counter 0
|
||||
shapes (seq shapes)
|
||||
result (list)]
|
||||
(if-let [id (first shapes)]
|
||||
(if-let [obj (get objects id)]
|
||||
(do
|
||||
;; NOTE: this is a bit hacky, but reduces substantially
|
||||
;; the allocation; If we use enumeration, we allocate
|
||||
;; new sequence and add one iteration on each render,
|
||||
;; independently if objects are changed or not. If we
|
||||
;; store counter on metadata, we still need to create a
|
||||
;; new allocation for each shape; with this method we
|
||||
;; bypass this by mutating a private property on the
|
||||
;; object removing extra allocation and extra iteration
|
||||
;; on every request.
|
||||
(unchecked-set obj "__$__counter" counter)
|
||||
(recur (inc counter)
|
||||
(rest shapes)
|
||||
(conj result obj)))
|
||||
(recur (inc counter)
|
||||
(rest shapes)
|
||||
result))
|
||||
result)))]
|
||||
|
||||
(mf/defc layers-tree
|
||||
{::mf/wrap [mf/memo #(mf/throttle % 200)]
|
||||
::mf/wrap-props false}
|
||||
[{:keys [objects filtered? parent-size] :as props}]
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected (hooks/use-equal-memo selected)
|
||||
highlighted (mf/deref refs/highlighted-shapes)
|
||||
highlighted (hooks/use-equal-memo highlighted)
|
||||
root (get objects uuid/zero)]
|
||||
[:div {:class (stl/css :element-list) :data-testid "layer-item"}
|
||||
[:> hooks/sortable-container* {}
|
||||
(for [[index id] (reverse (d/enumerate (:shapes root)))]
|
||||
(when-let [obj (get objects id)]
|
||||
(if (cfh/frame-shape? obj)
|
||||
[:& frame-wrapper
|
||||
{:item obj
|
||||
:selected selected
|
||||
:highlighted highlighted
|
||||
:index index
|
||||
:objects objects
|
||||
:key id
|
||||
:sortable? true
|
||||
:filtered? filtered?
|
||||
:parent-size parent-size
|
||||
:depth -1}]
|
||||
[:& layer-item
|
||||
{:item obj
|
||||
:selected selected
|
||||
:highlighted highlighted
|
||||
:index index
|
||||
:objects objects
|
||||
:key id
|
||||
:sortable? true
|
||||
:filtered? filtered?
|
||||
:depth -1
|
||||
:parent-size parent-size}])))]]))
|
||||
(for [obj shapes]
|
||||
(if (cfh/frame-shape? obj)
|
||||
[:> frame-wrapper*
|
||||
{:item obj
|
||||
:rename-id rename-id
|
||||
:selected selected
|
||||
:highlighted highlighted
|
||||
:index (unchecked-get obj "__$__counter")
|
||||
:objects objects
|
||||
:key (dm/str (get obj :id))
|
||||
:is-sortable true
|
||||
:is-filtered is-filtered
|
||||
:parent-size parent-size
|
||||
:depth -1}]
|
||||
[:> layer-item*
|
||||
{:item obj
|
||||
:rename-id rename-id
|
||||
:selected selected
|
||||
:highlighted highlighted
|
||||
:index (unchecked-get obj "__$__counter")
|
||||
:objects objects
|
||||
:key (dm/str (get obj :id))
|
||||
:is-sortable true
|
||||
:is-filtered is-filtered
|
||||
:depth -1
|
||||
:parent-size parent-size}]))]]))
|
||||
|
||||
(mf/defc filters-tree
|
||||
{::mf/wrap [mf/memo #(mf/throttle % 200)]
|
||||
::mf/wrap-props false}
|
||||
(mf/defc layers-tree-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [objects] :as props}]
|
||||
;; This is a performance sensitive componet, so we use lower-level primitives for
|
||||
;; reduce residual allocation for this specific case
|
||||
(let [state-tmp (mf/useState objects)
|
||||
objects' (aget state-tmp 0)
|
||||
set-objects (aget state-tmp 1)
|
||||
|
||||
subject-s (mf/with-memo []
|
||||
(rx/subject))
|
||||
changes-s (mf/with-memo [subject-s]
|
||||
(->> subject-s
|
||||
(rx/debounce 500)))
|
||||
|
||||
props (mf/spread-props props {:objects objects'})]
|
||||
|
||||
(mf/with-effect [objects subject-s]
|
||||
(rx/push! subject-s objects))
|
||||
|
||||
(mf/with-effect [changes-s]
|
||||
(let [sub (rx/subscribe changes-s set-objects)]
|
||||
#(rx/dispose! sub)))
|
||||
|
||||
[:> layers-tree* props]))
|
||||
|
||||
(mf/defc filters-tree*
|
||||
{::mf/wrap [mf/memo #(mf/throttle % 300)]
|
||||
::mf/private true}
|
||||
[{:keys [objects parent-size]}]
|
||||
(let [selected (mf/deref refs/selected-shapes)
|
||||
selected (hooks/use-equal-memo selected)
|
||||
root (get objects uuid/zero)]
|
||||
(let [selected (use-selected-shapes)
|
||||
root (get objects uuid/zero)]
|
||||
[:ul {:class (stl/css :element-list)}
|
||||
(for [[index id] (d/enumerate (:shapes root))]
|
||||
(when-let [obj (get objects id)]
|
||||
[:& layer-item
|
||||
[:> layer-item*
|
||||
{:item obj
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key id
|
||||
:sortable? false
|
||||
:filtered? true
|
||||
:is-sortable false
|
||||
:is-filtered true
|
||||
:depth -1
|
||||
:parent-size parent-size}]))]))
|
||||
|
||||
@@ -132,6 +200,7 @@
|
||||
keys
|
||||
(filter #(not= uuid/zero %))
|
||||
vec)]
|
||||
|
||||
(update reparented-objects uuid/zero assoc :shapes reparented-shapes)))
|
||||
|
||||
;; --- Layers Toolbox
|
||||
@@ -277,9 +346,11 @@
|
||||
(swap! state* update :num-items + 100))))]
|
||||
|
||||
(mf/with-effect []
|
||||
(let [keys [(events/listen globals/document EventType.KEYDOWN on-key-down)
|
||||
(events/listen globals/document EventType.CLICK hide-menu)]]
|
||||
(fn [] (doseq [key keys] (events/unlistenByKey key)))))
|
||||
(let [key1 (events/listen globals/document "keydown" on-key-down)
|
||||
key2 (events/listen globals/document "click" hide-menu)]
|
||||
(fn []
|
||||
(events/unlistenByKey key1)
|
||||
(events/unlistenByKey key2))))
|
||||
|
||||
[filtered-objects
|
||||
handle-show-more
|
||||
@@ -464,6 +535,8 @@
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [size-parent]}]
|
||||
(let [page (mf/deref refs/workspace-page)
|
||||
page-id (get page :id)
|
||||
|
||||
focus (mf/deref refs/workspace-focus-selected)
|
||||
|
||||
objects (hooks/with-focus-objects (:objects page) focus)
|
||||
@@ -473,7 +546,8 @@
|
||||
observer-var (mf/use-var nil)
|
||||
lazy-load-ref (mf/use-ref nil)
|
||||
|
||||
[filtered-objects show-more filter-component] (use-search page objects)
|
||||
[filtered-objects show-more filter-component]
|
||||
(use-search page objects)
|
||||
|
||||
intersection-callback
|
||||
(fn [entries]
|
||||
@@ -519,25 +593,25 @@
|
||||
[:div {:class (stl/css :tool-window-content)
|
||||
:data-scroll-container true
|
||||
:ref on-render-container}
|
||||
[:& filters-tree {:objects filtered-objects
|
||||
:key (dm/str (:id page))
|
||||
:parent-size size-parent}]
|
||||
[:> filters-tree* {:objects filtered-objects
|
||||
:key (dm/str page-id)
|
||||
:parent-size size-parent}]
|
||||
[:div {:ref lazy-load-ref}]]
|
||||
[:div {:on-scroll on-scroll
|
||||
:class (stl/css :tool-window-content)
|
||||
:data-scroll-container true
|
||||
:style {:display (when (some? filtered-objects) "none")}}
|
||||
|
||||
[:& layers-tree {:objects filtered-objects
|
||||
:key (dm/str (:id page))
|
||||
:filtered? true
|
||||
:parent-size size-parent}]]]
|
||||
[:> layers-tree-wrapper* {:objects filtered-objects
|
||||
:key (dm/str page-id)
|
||||
:is-filtered true
|
||||
:parent-size size-parent}]]]
|
||||
|
||||
[:div {:on-scroll on-scroll
|
||||
:class (stl/css :tool-window-content)
|
||||
:data-scroll-container true
|
||||
:style {:display (when (some? filtered-objects) "none")}}
|
||||
[:& layers-tree {:objects objects
|
||||
:key (dm/str (:id page))
|
||||
:filtered? false
|
||||
:parent-size size-parent}]])]))
|
||||
[:> layers-tree-wrapper* {:objects objects
|
||||
:key (dm/str page-id)
|
||||
:is-filtered false
|
||||
:parent-size size-parent}]])]))
|
||||
|
||||
@@ -92,6 +92,19 @@
|
||||
(def ^:private xf:map-type (map :type))
|
||||
(def ^:private xf:mapcat-type-to-options (mapcat type->options))
|
||||
|
||||
(defn fixed-decimal-value
|
||||
"Fixes the amount of decimals that are kept"
|
||||
([value]
|
||||
(fixed-decimal-value value 2))
|
||||
|
||||
([value decimals]
|
||||
(cond
|
||||
(string? value)
|
||||
(fixed-decimal-value (parse-double value) decimals)
|
||||
|
||||
(number? value)
|
||||
(parse-double (.toFixed value decimals)))))
|
||||
|
||||
(mf/defc measures-menu*
|
||||
[{:keys [ids values applied-tokens type shapes]}]
|
||||
(let [token-numeric-inputs
|
||||
@@ -300,7 +313,7 @@
|
||||
(mf/deps ids)
|
||||
(fn [value]
|
||||
(if (or (string? value) (number? value))
|
||||
(do
|
||||
(let [value (fixed-decimal-value value)]
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(st/emit! (udw/increase-rotation ids value)))
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
|
||||
@@ -75,7 +75,11 @@
|
||||
is-type-unfolded (contains? (set unfolded-token-paths) (name type))
|
||||
|
||||
editing-ref (mf/deref refs/workspace-editor-state)
|
||||
not-editing? (empty? editing-ref)
|
||||
edition (mf/deref refs/selected-edition)
|
||||
objects (mf/deref refs/workspace-page-objects)
|
||||
not-editing? (and (empty? editing-ref)
|
||||
(not (and (some? edition)
|
||||
(= :text (:type (get objects edition))))))
|
||||
|
||||
can-edit?
|
||||
(mf/use-ctx ctx/can-edit?)
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
|
||||
(defn top?
|
||||
[cur cand]
|
||||
(let [closey? (mth/close? (:y cand) (:y cur))]
|
||||
(let [closey? (mth/close? (:y cand) (:y cur) 0.01)]
|
||||
(cond
|
||||
(and closey? (< (:x cand) (:x cur))) cand
|
||||
closey? cur
|
||||
@@ -64,13 +64,19 @@
|
||||
|
||||
(defn right?
|
||||
[cur cand]
|
||||
(let [closex? (mth/close? (:x cand) (:x cur))]
|
||||
(let [closex? (mth/close? (:x cand) (:x cur) 0.01)]
|
||||
(cond
|
||||
(and closex? (< (:y cand) (:y cur))) cand
|
||||
closex? cur
|
||||
(> (:x cand) (:x cur)) cand
|
||||
:else cur)))
|
||||
|
||||
(defn title-transform-use-width?
|
||||
[{:keys [rotation] :as shape}]
|
||||
(let [side (mth/ceil (/ (- rotation 45) 90))
|
||||
use-width? (even? side)]
|
||||
use-width?))
|
||||
|
||||
(defn title-transform
|
||||
[{:keys [points] :as shape} zoom grid-edition?]
|
||||
(let [leftmost (->> points (reduce left?))
|
||||
|
||||
@@ -129,13 +129,15 @@
|
||||
(fn [_]
|
||||
(on-frame-leave (:id frame))))
|
||||
|
||||
main-instance? (ctk/main-instance? frame)
|
||||
is-variant? (:is-variant-container frame)
|
||||
main-instance? (ctk/main-instance? frame)
|
||||
is-variant? (:is-variant-container frame)
|
||||
|
||||
text-width (* (:width frame) zoom)
|
||||
show-icon? (and (or (:use-for-thumbnail frame) is-grid-edition main-instance? is-variant?)
|
||||
(not (<= text-width 15)))
|
||||
text-pos-x (if show-icon? 15 0)
|
||||
use-width? (vwu/title-transform-use-width? frame)
|
||||
|
||||
text-width (* (if use-width? (:width frame) (:height frame)) zoom)
|
||||
show-icon? (and (or (:use-for-thumbnail frame) is-grid-edition main-instance? is-variant?)
|
||||
(not (<= text-width 15)))
|
||||
text-pos-x (if show-icon? 15 0)
|
||||
|
||||
edition* (mf/use-state false)
|
||||
edition? (deref edition*)
|
||||
@@ -178,7 +180,6 @@
|
||||
(when (kbd/enter? event) (accept-edit))
|
||||
(when (kbd/esc? event) (cancel-edit))))]
|
||||
|
||||
|
||||
(when (not (:hidden frame))
|
||||
[:g.frame-title {:id (dm/str "frame-title-" (:id frame))
|
||||
:data-edit-grid is-grid-edition
|
||||
|
||||
@@ -7,11 +7,28 @@
|
||||
(ns app.render-wasm.helpers
|
||||
#?(:cljs (:require-macros [app.render-wasm.helpers])))
|
||||
|
||||
(def ^:export error-code
|
||||
"WASM error code constants (must match render-wasm/src/error.rs and mem.rs)."
|
||||
{0x01 :wasm-non-blocking 0x02 :wasm-critical})
|
||||
|
||||
(defmacro call
|
||||
"A helper for easy call wasm defined function in a module."
|
||||
"A helper for easy call wasm defined function in a module.
|
||||
Catches any exception thrown by the WASM function, reads the error code from
|
||||
WASM when available, and rethrows ex-info with :type (:wasm-non-blocking or
|
||||
:wasm-critical) in ex-data. The uncaught-error-handler in app.main.errors
|
||||
routes these to on-error so critical shows Internal Error, non-blocking shows toast."
|
||||
[module name & params]
|
||||
(let [fn-sym (with-meta (gensym "fn-") {:tag 'function})]
|
||||
(let [fn-sym (with-meta (gensym "fn-") {:tag 'function})
|
||||
e-sym (gensym "e")
|
||||
code-sym (gensym "code")]
|
||||
`(let [~fn-sym (cljs.core/unchecked-get ~module ~name)]
|
||||
;; DEBUG
|
||||
;; (println "##" ~name)
|
||||
(~fn-sym ~@params))))
|
||||
(try
|
||||
(~fn-sym ~@params)
|
||||
(catch :default ~e-sym
|
||||
(let [read-code# (cljs.core/unchecked-get ~module "_read_error_code")
|
||||
~code-sym (when read-code# (read-code#))
|
||||
type# (or (get app.render-wasm.helpers/error-code ~code-sym) :wasm-critical)
|
||||
ex# (ex-info (str "WASM error (type: " type# ")")
|
||||
{:fn ~name :type type# :message (.-message ~e-sym) :error-code ~code-sym}
|
||||
~e-sym)]
|
||||
(throw ex#)))))))
|
||||
|
||||
23
render-wasm/Cargo.lock
generated
23
render-wasm/Cargo.lock
generated
@@ -297,6 +297,8 @@ name = "macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
@@ -426,6 +428,7 @@ dependencies = [
|
||||
"indexmap",
|
||||
"macros",
|
||||
"skia-safe",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -579,6 +582,26 @@ dependencies = [
|
||||
"xattr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "1.0.3+spec-1.1.0"
|
||||
|
||||
@@ -32,6 +32,7 @@ skia-safe = { version = "0.93.1", default-features = false, features = [
|
||||
"binary-cache",
|
||||
"webp",
|
||||
] }
|
||||
thiserror = "2.0.18"
|
||||
uuid = { version = "1.11.0", features = ["v4", "js"] }
|
||||
|
||||
[profile.release]
|
||||
|
||||
2
render-wasm/macros/Cargo.lock
generated
2
render-wasm/macros/Cargo.lock
generated
@@ -13,6 +13,8 @@ name = "macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
[package]
|
||||
name = "macros"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
heck = "0.5.0"
|
||||
proc-macro2 = "1.0"
|
||||
quote = "1.0"
|
||||
syn = "2.0.106"
|
||||
|
||||
@@ -6,9 +6,109 @@ use std::sync;
|
||||
|
||||
use heck::{ToKebabCase, ToPascalCase};
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Block, GenericArgument, ItemFn, ReturnType, Type};
|
||||
|
||||
type Result<T> = std::result::Result<T, String>;
|
||||
|
||||
/// Attribute macro for WASM-exported functions. The function **must** return
|
||||
/// `std::result::Result<T, E>` where T is a C ABI type and E implements
|
||||
/// `std::error::Error` and `Into<u8>`. The macro:
|
||||
/// - Clears the error code at entry.
|
||||
/// - Runs the body in `std::panic::catch_unwind`.
|
||||
/// - Unwraps the Result: `Ok(x)` → return x; `Err(e)` → set error code in memory and panic
|
||||
/// (so ClojureScript can catch the exception and read the code via `read_error_code`).
|
||||
/// - On panic from the body: sets critical error code (0x02) and resumes unwind.
|
||||
#[proc_macro_attribute]
|
||||
pub fn wasm_error(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let mut input = parse_macro_input!(item as ItemFn);
|
||||
let body = (*input.block).clone();
|
||||
|
||||
let (attrs, boxed_ty) = match &input.sig.output {
|
||||
ReturnType::Type(attrs, boxed_ty) => (attrs, boxed_ty),
|
||||
ReturnType::Default => {
|
||||
return quote! {
|
||||
compile_error!(
|
||||
"#[wasm_error] requires the function to return std::result::Result<T, E> where E: std::error::Error + Into<u8>"
|
||||
);
|
||||
}
|
||||
.into();
|
||||
}
|
||||
};
|
||||
|
||||
let (inner_ty, error_ty) = match crate_error_result_inner_type(boxed_ty) {
|
||||
Some(t) => (t, quote!(crate::error::Error)),
|
||||
None => {
|
||||
return quote! {
|
||||
compile_error!(
|
||||
"#[wasm_error] requires the function to return crate::error::Result<T>. T must be a C ABI type (u32, u8, bool, (), etc.)"
|
||||
);
|
||||
}
|
||||
.into();
|
||||
}
|
||||
};
|
||||
|
||||
let block: Block = syn::parse2(quote! {
|
||||
{
|
||||
crate::mem::clear_error_code();
|
||||
let __wasm_err_result = std::panic::catch_unwind(|| -> std::result::Result<#inner_ty, #error_ty> {
|
||||
#body
|
||||
});
|
||||
match __wasm_err_result {
|
||||
Ok(__inner) => match __inner {
|
||||
Ok(__val) => __val,
|
||||
Err(__e) => {
|
||||
let _: &dyn std::error::Error = &__e;
|
||||
let __msg = __e.to_string();
|
||||
crate::mem::set_error_code(__e.into());
|
||||
panic!("WASM error: {}",__msg);
|
||||
}
|
||||
},
|
||||
Err(__payload) => {
|
||||
crate::mem::set_error_code(0x02); // critical, same as Error::Critical
|
||||
std::panic::resume_unwind(__payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.expect("block parse");
|
||||
|
||||
input.sig.output = ReturnType::Type(attrs.clone(), Box::new(inner_ty.clone()));
|
||||
input.block = Box::new(block);
|
||||
quote! { #input }.into()
|
||||
}
|
||||
|
||||
/// If the type is crate::error::Result<T> or a single-segment Result<T> (e.g. with
|
||||
/// `use crate::error::Result`), returns Some(T). Otherwise None.
|
||||
fn crate_error_result_inner_type(ty: &Type) -> Option<&Type> {
|
||||
let path = match ty {
|
||||
Type::Path(tp) => &tp.path,
|
||||
_ => return None,
|
||||
};
|
||||
let segs: Vec<_> = path.segments.iter().collect();
|
||||
let last = path.segments.last()?;
|
||||
if last.ident != "Result" {
|
||||
return None;
|
||||
}
|
||||
let args = match &last.arguments {
|
||||
syn::PathArguments::AngleBracketed(a) => &a.args,
|
||||
_ => return None,
|
||||
};
|
||||
if args.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
// Accept crate::error::Result<T> or bare Result<T> (from use)
|
||||
let ok = segs.len() == 1
|
||||
|| (segs.len() == 3 && segs[0].ident == "crate" && segs[1].ident == "error");
|
||||
if !ok {
|
||||
return None;
|
||||
}
|
||||
match &args[0] {
|
||||
GenericArgument::Type(t) => Some(t),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro_derive(ToJs)]
|
||||
pub fn derive_to_cljs(input: TokenStream) -> TokenStream {
|
||||
let input = syn::parse_macro_input!(input as syn::DeriveInput);
|
||||
|
||||
25
render-wasm/src/error.rs
Normal file
25
render-wasm/src/error.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub const RECOVERABLE_ERROR: u8 = 0x01;
|
||||
pub const CRITICAL_ERROR: u8 = 0x02;
|
||||
|
||||
// This is not really dead code, #[wasm_error] macro replaces this by something else.
|
||||
#[allow(dead_code)]
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("[Recoverable] {0}")]
|
||||
RecoverableError(String),
|
||||
#[error("[Critical] {0}")]
|
||||
CriticalError(String),
|
||||
}
|
||||
|
||||
impl From<Error> for u8 {
|
||||
fn from(error: Error) -> Self {
|
||||
match error {
|
||||
Error::RecoverableError(_) => RECOVERABLE_ERROR,
|
||||
Error::CriticalError(_) => CRITICAL_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod emscripten;
|
||||
mod error;
|
||||
mod math;
|
||||
mod mem;
|
||||
mod options;
|
||||
@@ -14,12 +15,16 @@ mod view;
|
||||
mod wapi;
|
||||
mod wasm;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
use macros::wasm_error;
|
||||
use math::{Bounds, Matrix};
|
||||
use mem::SerializableResult;
|
||||
use shapes::{StructureEntry, StructureEntryType, TransformEntry};
|
||||
use skia_safe as skia;
|
||||
use state::State;
|
||||
use std::collections::HashMap;
|
||||
use utils::uuid_from_u32_quartet;
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -95,22 +100,27 @@ macro_rules! with_state_mut_current_shape {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init(width: i32, height: i32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn init(width: i32, height: i32) -> Result<()> {
|
||||
let state_box = Box::new(State::new(width, height));
|
||||
unsafe {
|
||||
STATE = Some(state_box);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_browser(browser: u8) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_browser(browser: u8) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.set_browser(browser);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn clean_up() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn clean_up() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
// Cancel the current animation frame if it exists so
|
||||
// it won't try to render without context
|
||||
@@ -118,49 +128,60 @@ pub extern "C" fn clean_up() {
|
||||
render_state.cancel_animation_frame();
|
||||
});
|
||||
unsafe { STATE = None }
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_render_options(debug: u32, dpr: f32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_render_options(debug: u32, dpr: f32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.set_debug_flags(debug);
|
||||
render_state.set_dpr(dpr);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_canvas_background(raw_color: u32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_canvas_background(raw_color: u32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let color = skia::Color::new(raw_color);
|
||||
state.set_background_color(color);
|
||||
state.rebuild_tiles_shallow();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render(_: i32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render(_: i32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.rebuild_touched_tiles();
|
||||
state
|
||||
.start_render_loop(performance::get_time())
|
||||
.expect("Error rendering");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_sync() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_sync() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.rebuild_tiles();
|
||||
state
|
||||
.render_sync(performance::get_time())
|
||||
.expect("Error rendering");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
state.use_shape(id);
|
||||
@@ -179,34 +200,42 @@ pub extern "C" fn render_sync_shape(a: u32, b: u32, c: u32, d: u32) {
|
||||
state.rebuild_tiles_from(Some(&id));
|
||||
state
|
||||
.render_sync_shape(&id, performance::get_time())
|
||||
.expect("Error rendering");
|
||||
.map_err(|e| Error::RecoverableError(e.to_string()))?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_from_cache(_: i32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_from_cache(_: i32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.render_state.cancel_animation_frame();
|
||||
state.render_from_cache();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_preview_mode(enabled: bool) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_preview_mode(enabled: bool) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.render_state.set_preview_mode(enabled);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_preview() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn render_preview() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.render_preview(performance::get_time());
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) -> Result<()> {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
with_state_mut!(state, {
|
||||
state
|
||||
@@ -225,37 +254,45 @@ pub extern "C" fn process_animation_frame(timestamp: i32) {
|
||||
std::panic::resume_unwind(err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn reset_canvas() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn reset_canvas() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.render_state_mut().reset_canvas();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn resize_viewbox(width: i32, height: i32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn resize_viewbox(width: i32, height: i32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.resize(width, height);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view(zoom: f32, x: f32, y: f32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
performance::begin_measure!("set_view");
|
||||
let render_state = state.render_state_mut();
|
||||
render_state.set_view(zoom, x, y);
|
||||
performance::end_measure!("set_view");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "profile-macros")]
|
||||
static mut VIEW_INTERACTION_START: i32 = 0;
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_view_start() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_start() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
#[cfg(feature = "profile-macros")]
|
||||
unsafe {
|
||||
@@ -265,10 +302,12 @@ pub extern "C" fn set_view_start() {
|
||||
state.render_state.options.set_fast_mode(true);
|
||||
performance::end_measure!("set_view_start");
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_view_end() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_view_end() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let _end_start = performance::begin_timed_log!("set_view_end");
|
||||
performance::begin_measure!("set_view_end");
|
||||
@@ -304,17 +343,21 @@ pub extern "C" fn set_view_end() {
|
||||
performance::console_log!("[PERF] view_interaction: {}ms", total_time);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn clear_focus_mode() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn clear_focus_mode() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.clear_focus_mode();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_focus_mode() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_focus_mode() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
@@ -325,83 +368,111 @@ pub extern "C" fn set_focus_mode() {
|
||||
with_state_mut!(state, {
|
||||
state.set_focus_mode(entries);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn init_shapes_pool(capacity: usize) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn init_shapes_pool(capacity: usize) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.init_shapes_pool(capacity);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn use_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
state.use_shape(id);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn touch_shape(a: u32, b: u32, c: u32, d: u32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn touch_shape(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let shape_id = uuid_from_u32_quartet(a, b, c, d);
|
||||
state.touch_shape(shape_id);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_parent(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
state.set_parent_for_current_shape(id);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_masked_group(masked: bool) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_masked_group(masked: bool) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_masked(masked);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_selrect(left: f32, top: f32, right: f32, bottom: f32) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_selrect(left, top, right, bottom);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_clip_content(clip_content: bool) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_clip_content(clip_content: bool) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_clip(clip_content);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_rotation(rotation: f32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_rotation(rotation: f32) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_rotation(rotation);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_transform(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_transform(
|
||||
a: f32,
|
||||
b: f32,
|
||||
c: f32,
|
||||
d: f32,
|
||||
e: f32,
|
||||
f: f32,
|
||||
) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_transform(a, b, c, d, e, f);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn add_shape_child(a: u32, b: u32, c: u32, d: u32) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
shape.add_child(id);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_children_set(entries: Vec<Uuid>) {
|
||||
fn set_children_set(entries: Vec<Uuid>) -> Result<()> {
|
||||
let mut deleted = Vec::new();
|
||||
let mut parent_id = None;
|
||||
|
||||
@@ -420,7 +491,9 @@ fn set_children_set(entries: Vec<Uuid>) {
|
||||
|
||||
with_state_mut!(state, {
|
||||
let Some(parent_id) = parent_id else {
|
||||
return;
|
||||
return Err(Error::RecoverableError(
|
||||
"set_children_set: Parent ID not found".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
for id in deleted {
|
||||
@@ -428,21 +501,27 @@ fn set_children_set(entries: Vec<Uuid>) {
|
||||
state.touch_shape(id);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_children_0() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_children_0() -> Result<()> {
|
||||
let entries = vec![];
|
||||
set_children_set(entries);
|
||||
set_children_set(entries)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_children_1(a1: u32, b1: u32, c1: u32, d1: u32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_children_1(a1: u32, b1: u32, c1: u32, d1: u32) -> Result<()> {
|
||||
let entries = vec![uuid_from_u32_quartet(a1, b1, c1, d1)];
|
||||
set_children_set(entries);
|
||||
set_children_set(entries)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_children_2(
|
||||
a1: u32,
|
||||
b1: u32,
|
||||
@@ -452,15 +531,17 @@ pub extern "C" fn set_children_2(
|
||||
b2: u32,
|
||||
c2: u32,
|
||||
d2: u32,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let entries = vec![
|
||||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||||
];
|
||||
set_children_set(entries);
|
||||
set_children_set(entries)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_children_3(
|
||||
a1: u32,
|
||||
b1: u32,
|
||||
@@ -474,16 +555,18 @@ pub extern "C" fn set_children_3(
|
||||
b3: u32,
|
||||
c3: u32,
|
||||
d3: u32,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let entries = vec![
|
||||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||||
uuid_from_u32_quartet(a3, b3, c3, d3),
|
||||
];
|
||||
set_children_set(entries);
|
||||
set_children_set(entries)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_children_4(
|
||||
a1: u32,
|
||||
b1: u32,
|
||||
@@ -501,17 +584,19 @@ pub extern "C" fn set_children_4(
|
||||
b4: u32,
|
||||
c4: u32,
|
||||
d4: u32,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let entries = vec![
|
||||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||||
uuid_from_u32_quartet(a3, b3, c3, d3),
|
||||
uuid_from_u32_quartet(a4, b4, c4, d4),
|
||||
];
|
||||
set_children_set(entries);
|
||||
set_children_set(entries)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_children_5(
|
||||
a1: u32,
|
||||
b1: u32,
|
||||
@@ -533,7 +618,7 @@ pub extern "C" fn set_children_5(
|
||||
b5: u32,
|
||||
c5: u32,
|
||||
d5: u32,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
let entries = vec![
|
||||
uuid_from_u32_quartet(a1, b1, c1, d1),
|
||||
uuid_from_u32_quartet(a2, b2, c2, d2),
|
||||
@@ -541,11 +626,13 @@ pub extern "C" fn set_children_5(
|
||||
uuid_from_u32_quartet(a4, b4, c4, d4),
|
||||
uuid_from_u32_quartet(a5, b5, c5, d5),
|
||||
];
|
||||
set_children_set(entries);
|
||||
set_children_set(entries)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_children() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_children() -> Result<()> {
|
||||
let bytes = mem::bytes_or_empty();
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
@@ -553,58 +640,76 @@ pub extern "C" fn set_children() {
|
||||
.map(|data| Uuid::try_from(data).unwrap())
|
||||
.collect();
|
||||
|
||||
set_children_set(entries);
|
||||
set_children_set(entries)?;
|
||||
|
||||
if !bytes.is_empty() {
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn is_image_cached(a: u32, b: u32, c: u32, d: u32, is_thumbnail: bool) -> bool {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn is_image_cached(
|
||||
a: u32,
|
||||
b: u32,
|
||||
c: u32,
|
||||
d: u32,
|
||||
is_thumbnail: bool,
|
||||
) -> Result<bool> {
|
||||
with_state_mut!(state, {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
state.render_state().has_image(&id, is_thumbnail)
|
||||
let result = state.render_state().has_image(&id, is_thumbnail);
|
||||
Ok(result)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_svg_raw_content() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_svg_raw_content() -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let bytes = mem::bytes();
|
||||
let svg_raw_content = String::from_utf8(bytes)
|
||||
.unwrap()
|
||||
.map_err(|e| Error::RecoverableError(e.to_string()))?
|
||||
.trim_end_matches('\0')
|
||||
.to_string();
|
||||
shape
|
||||
.set_svg_raw_content(svg_raw_content)
|
||||
.expect("Failed to set svg raw content");
|
||||
shape.set_svg_raw_content(svg_raw_content);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_opacity(opacity: f32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_opacity(opacity: f32) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_opacity(opacity);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_hidden(hidden: bool) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_hidden(hidden: bool) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_hidden(hidden);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_corners(r1: f32, r2: f32, r3: f32, r4: f32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_corners(r1: f32, r2: f32, r3: f32, r4: f32) -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
shape.set_corners((r1, r2, r3, r4));
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_selection_rect() -> *mut u8 {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn get_selection_rect() -> Result<*mut u8> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
@@ -619,40 +724,41 @@ pub extern "C" fn get_selection_rect() -> *mut u8 {
|
||||
})
|
||||
.collect();
|
||||
|
||||
with_state_mut!(state, {
|
||||
let result_bound = with_state_mut!(state, {
|
||||
let bbs: Vec<_> = entries
|
||||
.iter()
|
||||
.flat_map(|id| state.shapes.get(id).map(|b| b.bounds()))
|
||||
.collect();
|
||||
|
||||
let result_bound = if bbs.len() == 1 {
|
||||
if bbs.len() == 1 {
|
||||
bbs[0]
|
||||
} else {
|
||||
Bounds::join_bounds(&bbs)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
let width = result_bound.width();
|
||||
let height = result_bound.height();
|
||||
let center = result_bound.center();
|
||||
let transform = result_bound.transform_matrix().unwrap_or(Matrix::default());
|
||||
let width = result_bound.width();
|
||||
let height = result_bound.height();
|
||||
let center = result_bound.center();
|
||||
let transform = result_bound.transform_matrix().unwrap_or(Matrix::default());
|
||||
|
||||
let mut bytes = vec![0; 40];
|
||||
bytes[0..4].clone_from_slice(&width.to_le_bytes());
|
||||
bytes[4..8].clone_from_slice(&height.to_le_bytes());
|
||||
bytes[8..12].clone_from_slice(¢er.x.to_le_bytes());
|
||||
bytes[12..16].clone_from_slice(¢er.y.to_le_bytes());
|
||||
bytes[16..20].clone_from_slice(&transform[0].to_le_bytes());
|
||||
bytes[20..24].clone_from_slice(&transform[3].to_le_bytes());
|
||||
bytes[24..28].clone_from_slice(&transform[1].to_le_bytes());
|
||||
bytes[28..32].clone_from_slice(&transform[4].to_le_bytes());
|
||||
bytes[32..36].clone_from_slice(&transform[2].to_le_bytes());
|
||||
bytes[36..40].clone_from_slice(&transform[5].to_le_bytes());
|
||||
mem::write_bytes(bytes)
|
||||
})
|
||||
let mut bytes = vec![0; 40];
|
||||
bytes[0..4].clone_from_slice(&width.to_le_bytes());
|
||||
bytes[4..8].clone_from_slice(&height.to_le_bytes());
|
||||
bytes[8..12].clone_from_slice(¢er.x.to_le_bytes());
|
||||
bytes[12..16].clone_from_slice(¢er.y.to_le_bytes());
|
||||
bytes[16..20].clone_from_slice(&transform[0].to_le_bytes());
|
||||
bytes[20..24].clone_from_slice(&transform[3].to_le_bytes());
|
||||
bytes[24..28].clone_from_slice(&transform[1].to_le_bytes());
|
||||
bytes[28..32].clone_from_slice(&transform[4].to_le_bytes());
|
||||
bytes[32..36].clone_from_slice(&transform[2].to_le_bytes());
|
||||
bytes[36..40].clone_from_slice(&transform[5].to_le_bytes());
|
||||
Ok(mem::write_bytes(bytes))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_structure_modifiers() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_structure_modifiers() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<_> = bytes
|
||||
@@ -690,18 +796,22 @@ pub extern "C" fn set_structure_modifiers() {
|
||||
}
|
||||
});
|
||||
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn clean_modifiers() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn clean_modifiers() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
state.shapes.clean_all();
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_modifiers() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_modifiers() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<_> = bytes
|
||||
@@ -720,26 +830,31 @@ pub extern "C" fn set_modifiers() {
|
||||
state.set_modifiers(modifiers);
|
||||
state.rebuild_modifier_tiles(ids);
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn start_temp_objects() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn start_temp_objects() -> Result<()> {
|
||||
unsafe {
|
||||
#[allow(static_mut_refs)]
|
||||
let mut state = STATE.take().expect("Got an invalid state pointer");
|
||||
state = Box::new(state.start_temp_objects());
|
||||
STATE = Some(state);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn end_temp_objects() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn end_temp_objects() -> Result<()> {
|
||||
unsafe {
|
||||
#[allow(static_mut_refs)]
|
||||
let mut state = STATE.take().expect("Got an invalid state pointer");
|
||||
state = Box::new(state.end_temp_objects());
|
||||
STATE = Some(state);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
use std::alloc::{alloc, Layout};
|
||||
use std::ptr;
|
||||
use std::sync::Mutex;
|
||||
|
||||
const LAYOUT_ALIGN: usize = 4;
|
||||
use crate::error::{Error, Result, CRITICAL_ERROR};
|
||||
|
||||
static BUFFERU8: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
||||
pub const LAYOUT_ALIGN: usize = 4;
|
||||
|
||||
pub static BUFFERU8: Mutex<Option<Vec<u8>>> = Mutex::new(None);
|
||||
pub static BUFFER_ERROR: Mutex<u8> = Mutex::new(0x00);
|
||||
|
||||
pub fn clear_error_code() {
|
||||
let mut guard = BUFFER_ERROR.lock().unwrap();
|
||||
*guard = 0x00;
|
||||
}
|
||||
|
||||
/// Sets the error buffer from a byte. Used by #[wasm_error] when E: Into<u8>.
|
||||
pub fn set_error_code(code: u8) {
|
||||
let mut guard = BUFFER_ERROR.lock().unwrap();
|
||||
*guard = code;
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn alloc_bytes(len: usize) -> *mut u8 {
|
||||
let mut guard = BUFFERU8.lock().unwrap();
|
||||
|
||||
if guard.is_some() {
|
||||
panic!("Bytes already allocated");
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let layout = Layout::from_size_align_unchecked(len, LAYOUT_ALIGN);
|
||||
let ptr = alloc(layout);
|
||||
if ptr.is_null() {
|
||||
panic!("Allocation failed");
|
||||
}
|
||||
// TODO: Maybe this could be removed.
|
||||
ptr::write_bytes(ptr, 0, len);
|
||||
*guard = Some(Vec::from_raw_parts(ptr, len, len));
|
||||
ptr
|
||||
pub extern "C" fn read_error_code() -> u8 {
|
||||
if let Ok(guard) = BUFFER_ERROR.lock() {
|
||||
*guard
|
||||
} else {
|
||||
CRITICAL_ERROR
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,13 +40,6 @@ pub fn write_bytes(mut bytes: Vec<u8>) -> *mut u8 {
|
||||
ptr
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn free_bytes() {
|
||||
let mut guard = BUFFERU8.lock().unwrap();
|
||||
*guard = None;
|
||||
std::mem::drop(guard);
|
||||
}
|
||||
|
||||
pub fn bytes() -> Vec<u8> {
|
||||
let mut guard = BUFFERU8.lock().unwrap();
|
||||
guard.take().expect("Buffer is not initialized")
|
||||
@@ -57,6 +50,15 @@ pub fn bytes_or_empty() -> Vec<u8> {
|
||||
guard.take().unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn free_bytes() -> Result<()> {
|
||||
let mut guard = BUFFERU8
|
||||
.lock()
|
||||
.map_err(|_| Error::CriticalError("Failed to lock buffer".to_string()))?;
|
||||
*guard = None;
|
||||
std::mem::drop(guard);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub trait SerializableResult: From<Self::BytesType> + Into<Self::BytesType> {
|
||||
type BytesType;
|
||||
fn clone_to_slice(&self, slice: &mut [u8]);
|
||||
|
||||
@@ -41,8 +41,13 @@ fn draw_stroke_on_rect(
|
||||
}
|
||||
};
|
||||
|
||||
// By default just draw the rect. Only dotted inner/outer strokes need
|
||||
// clipping to prevent the dotted pattern from appearing in wrong areas.
|
||||
if let Some(clip_op) = stroke.clip_op() {
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
// Use a neutral layer (no extra paint) so opacity and filters
|
||||
// come solely from the stroke paint. This avoids applying
|
||||
// stroke alpha twice for dotted inner/outer strokes.
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default();
|
||||
canvas.save_layer(&layer_rec);
|
||||
match corners {
|
||||
Some(radii) => {
|
||||
@@ -81,7 +86,10 @@ fn draw_stroke_on_circle(
|
||||
// By default just draw the circle. Only dotted inner/outer strokes need
|
||||
// clipping to prevent the dotted pattern from appearing in wrong areas.
|
||||
if let Some(clip_op) = stroke.clip_op() {
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
// Use a neutral layer (no extra paint) so opacity and filters
|
||||
// come solely from the stroke paint. This avoids applying
|
||||
// stroke alpha twice for dotted inner/outer strokes.
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default();
|
||||
canvas.save_layer(&layer_rec);
|
||||
let clip_path = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
|
||||
@@ -111,7 +111,7 @@ fn calculate_cursor_rect(
|
||||
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||
if idx == cursor.paragraph {
|
||||
let char_pos = cursor.char_offset;
|
||||
let char_pos = cursor.offset;
|
||||
// For cursor, we get a zero-width range at the position
|
||||
// We need to handle edge cases:
|
||||
// - At start of paragraph: use position 0
|
||||
@@ -209,13 +209,13 @@ fn calculate_selection_rects(
|
||||
.sum();
|
||||
|
||||
let range_start = if para_idx == start.paragraph {
|
||||
start.char_offset
|
||||
start.offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.char_offset
|
||||
end.offset
|
||||
} else {
|
||||
para_char_count
|
||||
};
|
||||
|
||||
@@ -705,9 +705,8 @@ impl Shape {
|
||||
self.invalidate_extrect();
|
||||
}
|
||||
|
||||
pub fn set_svg_raw_content(&mut self, content: String) -> Result<(), String> {
|
||||
pub fn set_svg_raw_content(&mut self, content: String) {
|
||||
self.shape_type = Type::SVGRaw(SVGRaw::from_content(content));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_blend_mode(&mut self, mode: BlendMode) {
|
||||
|
||||
@@ -11,6 +11,7 @@ use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
||||
use skia_safe::{
|
||||
self as skia,
|
||||
paint::{self, Paint},
|
||||
textlayout::Affinity,
|
||||
textlayout::ParagraphBuilder,
|
||||
textlayout::ParagraphStyle,
|
||||
textlayout::PositionWithAffinity,
|
||||
@@ -112,31 +113,51 @@ impl TextContentSize {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct TextPositionWithAffinity {
|
||||
#[allow(dead_code)]
|
||||
pub position_with_affinity: PositionWithAffinity,
|
||||
pub paragraph: i32,
|
||||
#[allow(dead_code)]
|
||||
pub span: i32,
|
||||
#[allow(dead_code)]
|
||||
pub span_relative_offset: i32,
|
||||
pub offset: i32,
|
||||
pub paragraph: usize,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
impl PartialEq for TextPositionWithAffinity {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.paragraph == other.paragraph && self.offset == other.offset
|
||||
}
|
||||
}
|
||||
|
||||
impl TextPositionWithAffinity {
|
||||
pub fn new(
|
||||
position_with_affinity: PositionWithAffinity,
|
||||
paragraph: i32,
|
||||
span: i32,
|
||||
span_relative_offset: i32,
|
||||
offset: i32,
|
||||
paragraph: usize,
|
||||
offset: usize,
|
||||
) -> Self {
|
||||
Self {
|
||||
position_with_affinity,
|
||||
paragraph,
|
||||
span,
|
||||
span_relative_offset,
|
||||
offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
position_with_affinity: PositionWithAffinity {
|
||||
position: 0,
|
||||
affinity: Affinity::Downstream,
|
||||
},
|
||||
paragraph: 0,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_without_affinity(paragraph: usize, offset: usize) -> Self {
|
||||
Self {
|
||||
position_with_affinity: PositionWithAffinity {
|
||||
position: offset as i32,
|
||||
affinity: Affinity::Downstream,
|
||||
},
|
||||
paragraph,
|
||||
offset,
|
||||
}
|
||||
}
|
||||
@@ -433,10 +454,11 @@ impl TextContent {
|
||||
let mut offset_y = 0.0;
|
||||
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
||||
|
||||
let mut paragraph_index: i32 = -1;
|
||||
let mut span_index: i32 = -1;
|
||||
for layout_paragraph in layout_paragraphs {
|
||||
paragraph_index += 1;
|
||||
// IMPORTANT! I'm keeping this because I think it should be better to have the span index
|
||||
// cached the same way we keep the paragraph index.
|
||||
#[allow(dead_code)]
|
||||
let mut _span_index: usize = 0;
|
||||
for (paragraph_index, layout_paragraph) in layout_paragraphs.enumerate() {
|
||||
let start_y = offset_y;
|
||||
let end_y = offset_y + layout_paragraph.height();
|
||||
|
||||
@@ -453,20 +475,22 @@ impl TextContent {
|
||||
if matches {
|
||||
let position_with_affinity =
|
||||
layout_paragraph.get_glyph_position_at_coordinate(*point);
|
||||
if let Some(paragraph) = self.paragraphs().get(paragraph_index as usize) {
|
||||
if let Some(paragraph) = self.paragraphs().get(paragraph_index) {
|
||||
// Computed position keeps the current position in terms
|
||||
// of number of characters of text. This is used to know
|
||||
// in which span we are.
|
||||
let mut computed_position = 0;
|
||||
let mut span_offset = 0;
|
||||
let mut computed_position: usize = 0;
|
||||
|
||||
// This could be useful in the future as part of the TextPositionWithAffinity.
|
||||
#[allow(dead_code)]
|
||||
let mut _span_offset: usize = 0;
|
||||
|
||||
// If paragraph has no spans, default to span 0, offset 0
|
||||
if paragraph.children().is_empty() {
|
||||
span_index = 0;
|
||||
span_offset = 0;
|
||||
_span_index = 0;
|
||||
_span_offset = 0;
|
||||
} else {
|
||||
for span in paragraph.children() {
|
||||
span_index += 1;
|
||||
let length = span.text.chars().count();
|
||||
let start_position = computed_position;
|
||||
let end_position = computed_position + length;
|
||||
@@ -475,27 +499,26 @@ impl TextContent {
|
||||
// Handle empty spans: if the span is empty and current position
|
||||
// matches the start, this is the right span
|
||||
if length == 0 && current_position == start_position {
|
||||
span_offset = 0;
|
||||
_span_offset = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
if start_position <= current_position
|
||||
&& end_position >= current_position
|
||||
{
|
||||
span_offset =
|
||||
position_with_affinity.position - start_position as i32;
|
||||
_span_offset =
|
||||
position_with_affinity.position as usize - start_position;
|
||||
break;
|
||||
}
|
||||
computed_position += length;
|
||||
_span_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return Some(TextPositionWithAffinity::new(
|
||||
position_with_affinity,
|
||||
paragraph_index,
|
||||
span_index,
|
||||
span_offset,
|
||||
position_with_affinity.position,
|
||||
position_with_affinity.position as usize,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -516,9 +539,7 @@ impl TextContent {
|
||||
return Some(TextPositionWithAffinity::new(
|
||||
default_position,
|
||||
0, // paragraph 0
|
||||
0, // span 0
|
||||
0, // offset 0
|
||||
0,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -7,34 +7,10 @@ use skia_safe::{
|
||||
Color,
|
||||
};
|
||||
|
||||
/// Cursor position within text content.
|
||||
/// Uses character offsets for precise positioning.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
|
||||
pub struct TextCursor {
|
||||
pub paragraph: usize,
|
||||
pub char_offset: usize,
|
||||
}
|
||||
|
||||
impl TextCursor {
|
||||
pub fn new(paragraph: usize, char_offset: usize) -> Self {
|
||||
Self {
|
||||
paragraph,
|
||||
char_offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self {
|
||||
paragraph: 0,
|
||||
char_offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct TextSelection {
|
||||
pub anchor: TextCursor,
|
||||
pub focus: TextCursor,
|
||||
pub anchor: TextPositionWithAffinity,
|
||||
pub focus: TextPositionWithAffinity,
|
||||
}
|
||||
|
||||
impl TextSelection {
|
||||
@@ -42,10 +18,10 @@ impl TextSelection {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn from_cursor(cursor: TextCursor) -> Self {
|
||||
pub fn from_position_with_affinity(position: TextPositionWithAffinity) -> Self {
|
||||
Self {
|
||||
anchor: cursor,
|
||||
focus: cursor,
|
||||
anchor: position,
|
||||
focus: position,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,12 +33,12 @@ impl TextSelection {
|
||||
!self.is_collapsed()
|
||||
}
|
||||
|
||||
pub fn set_caret(&mut self, cursor: TextCursor) {
|
||||
pub fn set_caret(&mut self, cursor: TextPositionWithAffinity) {
|
||||
self.anchor = cursor;
|
||||
self.focus = cursor;
|
||||
}
|
||||
|
||||
pub fn extend_to(&mut self, cursor: TextCursor) {
|
||||
pub fn extend_to(&mut self, cursor: TextPositionWithAffinity) {
|
||||
self.focus = cursor;
|
||||
}
|
||||
|
||||
@@ -74,24 +50,24 @@ impl TextSelection {
|
||||
self.focus = self.anchor;
|
||||
}
|
||||
|
||||
pub fn start(&self) -> TextCursor {
|
||||
pub fn start(&self) -> TextPositionWithAffinity {
|
||||
if self.anchor.paragraph < self.focus.paragraph {
|
||||
self.anchor
|
||||
} else if self.anchor.paragraph > self.focus.paragraph {
|
||||
self.focus
|
||||
} else if self.anchor.char_offset <= self.focus.char_offset {
|
||||
} else if self.anchor.offset <= self.focus.offset {
|
||||
self.anchor
|
||||
} else {
|
||||
self.focus
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> TextCursor {
|
||||
pub fn end(&self) -> TextPositionWithAffinity {
|
||||
if self.anchor.paragraph > self.focus.paragraph {
|
||||
self.anchor
|
||||
} else if self.anchor.paragraph < self.focus.paragraph {
|
||||
self.focus
|
||||
} else if self.anchor.char_offset >= self.focus.char_offset {
|
||||
} else if self.anchor.offset >= self.focus.offset {
|
||||
self.anchor
|
||||
} else {
|
||||
self.focus
|
||||
@@ -102,7 +78,7 @@ impl TextSelection {
|
||||
/// Events that the text editor can emit for frontend synchronization
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum EditorEvent {
|
||||
pub enum TextEditorEvent {
|
||||
None = 0,
|
||||
ContentChanged = 1,
|
||||
SelectionChanged = 2,
|
||||
@@ -131,7 +107,7 @@ pub struct TextEditorState {
|
||||
pub active_shape_id: Option<Uuid>,
|
||||
pub cursor_visible: bool,
|
||||
pub last_blink_time: f64,
|
||||
pending_events: Vec<EditorEvent>,
|
||||
pending_events: Vec<TextEditorEvent>,
|
||||
}
|
||||
|
||||
impl TextEditorState {
|
||||
@@ -189,56 +165,44 @@ impl TextEditorState {
|
||||
|
||||
pub fn select_all(&mut self, content: &TextContent) -> bool {
|
||||
self.is_pointer_selection_active = false;
|
||||
self.set_caret_from_position(TextPositionWithAffinity::new(
|
||||
PositionWithAffinity {
|
||||
position: 0,
|
||||
affinity: Affinity::Downstream,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
));
|
||||
let num_paragraphs = (content.paragraphs().len() - 1) as i32;
|
||||
self.set_caret_from_position(&TextPositionWithAffinity::empty());
|
||||
let num_paragraphs = content.paragraphs().len() - 1;
|
||||
let Some(last_paragraph) = content.paragraphs().last() else {
|
||||
return false;
|
||||
};
|
||||
let num_spans = (last_paragraph.children().len() - 1) as i32;
|
||||
let Some(last_text_span) = last_paragraph.children().last() else {
|
||||
#[allow(dead_code)]
|
||||
let _num_spans = last_paragraph.children().len() - 1;
|
||||
let Some(_last_text_span) = last_paragraph.children().last() else {
|
||||
return false;
|
||||
};
|
||||
let mut offset = 0;
|
||||
for span in last_paragraph.children() {
|
||||
offset += span.text.len();
|
||||
}
|
||||
self.extend_selection_from_position(TextPositionWithAffinity::new(
|
||||
self.extend_selection_from_position(&TextPositionWithAffinity::new(
|
||||
PositionWithAffinity {
|
||||
position: offset as i32,
|
||||
affinity: Affinity::Upstream,
|
||||
},
|
||||
num_paragraphs,
|
||||
num_spans,
|
||||
last_text_span.text.len() as i32,
|
||||
offset as i32,
|
||||
offset,
|
||||
));
|
||||
self.reset_blink();
|
||||
self.push_event(crate::state::EditorEvent::SelectionChanged);
|
||||
self.push_event(crate::state::TextEditorEvent::SelectionChanged);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_caret_from_position(&mut self, position: TextPositionWithAffinity) {
|
||||
let cursor = TextCursor::new(position.paragraph as usize, position.offset as usize);
|
||||
self.selection.set_caret(cursor);
|
||||
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.set_caret(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(EditorEvent::SelectionChanged);
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
pub fn extend_selection_from_position(&mut self, position: TextPositionWithAffinity) {
|
||||
let cursor = TextCursor::new(position.paragraph as usize, position.offset as usize);
|
||||
self.selection.extend_to(cursor);
|
||||
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.extend_to(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(EditorEvent::SelectionChanged);
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
pub fn update_blink(&mut self, timestamp_ms: f64) {
|
||||
@@ -264,41 +228,17 @@ impl TextEditorState {
|
||||
self.last_blink_time = 0.0;
|
||||
}
|
||||
|
||||
pub fn push_event(&mut self, event: EditorEvent) {
|
||||
pub fn push_event(&mut self, event: TextEditorEvent) {
|
||||
if self.pending_events.last() != Some(&event) {
|
||||
self.pending_events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_event(&mut self) -> EditorEvent {
|
||||
self.pending_events.pop().unwrap_or(EditorEvent::None)
|
||||
pub fn poll_event(&mut self) -> TextEditorEvent {
|
||||
self.pending_events.pop().unwrap_or(TextEditorEvent::None)
|
||||
}
|
||||
|
||||
pub fn has_pending_events(&self) -> bool {
|
||||
!self.pending_events.is_empty()
|
||||
}
|
||||
|
||||
pub fn set_caret_position_from(
|
||||
&mut self,
|
||||
text_position_with_affinity: TextPositionWithAffinity,
|
||||
) {
|
||||
self.set_caret_from_position(text_position_with_affinity);
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: Remove legacy code
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub struct TextNodePosition {
|
||||
pub paragraph: i32,
|
||||
pub span: i32,
|
||||
}
|
||||
|
||||
impl TextNodePosition {
|
||||
pub fn new(paragraph: i32, span: i32) -> Self {
|
||||
Self { paragraph, span }
|
||||
}
|
||||
|
||||
pub fn is_invalid(&self) -> bool {
|
||||
self.paragraph < 0 || self.span < 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod blurs;
|
||||
pub mod fills;
|
||||
pub mod fonts;
|
||||
pub mod layouts;
|
||||
pub mod mem;
|
||||
pub mod paths;
|
||||
pub mod shadows;
|
||||
pub mod shapes;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use macros::ToJs;
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use crate::mem;
|
||||
use crate::shapes;
|
||||
@@ -67,7 +67,8 @@ pub fn parse_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec<shapes::Fi
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_fills() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_fills() -> Result<()> {
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let bytes = mem::bytes();
|
||||
// The first byte contains the actual number of fills
|
||||
@@ -75,8 +76,9 @@ pub extern "C" fn set_shape_fills() {
|
||||
// Skip the first 4 bytes (header with fill count) and parse only the actual fills
|
||||
let fills = parse_fills_from_bytes(&bytes[4..], num_fills);
|
||||
shape.set_fills(fills);
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use crate::mem;
|
||||
use macros::wasm_error;
|
||||
// use crate::mem::SerializableResult;
|
||||
use crate::error::Error;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::with_state_mut;
|
||||
use crate::STATE;
|
||||
@@ -65,7 +67,8 @@ impl TryFrom<Vec<u8>> for ShapeImageIds {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn store_image() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn store_image() -> crate::error::Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
||||
|
||||
@@ -87,7 +90,8 @@ pub extern "C" fn store_image() {
|
||||
state.touch_shape(ids.shape_id);
|
||||
});
|
||||
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stores an image from an existing WebGL texture, avoiding re-decoding
|
||||
@@ -99,13 +103,17 @@ pub extern "C" fn store_image() {
|
||||
/// - bytes 40-43: width (i32)
|
||||
/// - bytes 44-47: height (i32)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn store_image_from_texture() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn store_image_from_texture() -> crate::error::Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
if bytes.len() < 48 {
|
||||
// FIXME: Review if this should be an critical or a recoverable error.
|
||||
eprintln!("store_image_from_texture: insufficient data");
|
||||
mem::free_bytes();
|
||||
return;
|
||||
mem::free_bytes()?;
|
||||
return Err(Error::RecoverableError(
|
||||
"store_image_from_texture: insufficient data".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let ids = ShapeImageIds::try_from(bytes[0..IMAGE_IDS_SIZE].to_vec()).unwrap();
|
||||
@@ -139,5 +147,6 @@ pub extern "C" fn store_image_from_texture() {
|
||||
state.touch_shape(ids.shape_id);
|
||||
});
|
||||
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use macros::ToJs;
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use crate::mem;
|
||||
use crate::shapes::{FontFamily, FontStyle};
|
||||
@@ -30,6 +30,7 @@ impl From<RawFontStyle> for FontStyle {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn store_font(
|
||||
a: u32,
|
||||
b: u32,
|
||||
@@ -39,7 +40,7 @@ pub extern "C" fn store_font(
|
||||
style: u8,
|
||||
is_emoji: bool,
|
||||
is_fallback: bool,
|
||||
) {
|
||||
) -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
let font_bytes = mem::bytes();
|
||||
@@ -52,8 +53,9 @@ pub extern "C" fn store_font(
|
||||
.fonts_mut()
|
||||
.add(family, &font_bytes, is_emoji, is_fallback);
|
||||
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use macros::ToJs;
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use crate::mem;
|
||||
use crate::shapes::{GridCell, GridDirection, GridTrack, GridTrackType};
|
||||
@@ -7,6 +7,9 @@ use crate::{uuid_from_u32_quartet, with_current_shape_mut, with_state, with_stat
|
||||
|
||||
use super::align;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::Result;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(C, align(1))]
|
||||
struct RawGridCell {
|
||||
@@ -168,7 +171,8 @@ pub extern "C" fn set_grid_layout_data(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_grid_columns() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_grid_columns() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<GridTrack> = bytes
|
||||
@@ -181,11 +185,13 @@ pub extern "C" fn set_grid_columns() {
|
||||
shape.set_grid_columns(entries);
|
||||
});
|
||||
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_grid_rows() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_grid_rows() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let entries: Vec<GridTrack> = bytes
|
||||
@@ -198,11 +204,13 @@ pub extern "C" fn set_grid_rows() {
|
||||
shape.set_grid_rows(entries);
|
||||
});
|
||||
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_grid_cells() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_grid_cells() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
let cells: Vec<RawGridCell> = bytes
|
||||
@@ -215,7 +223,8 @@ pub extern "C" fn set_grid_cells() {
|
||||
shape.set_grid_cells(cells.into_iter().map(|raw| raw.into()).collect());
|
||||
});
|
||||
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
38
render-wasm/src/wasm/mem.rs
Normal file
38
render-wasm/src/wasm/mem.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::alloc::{alloc, Layout};
|
||||
use std::ptr;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
use crate::mem::{BUFFERU8, LAYOUT_ALIGN};
|
||||
use macros::wasm_error;
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn alloc_bytes(len: usize) -> Result<*mut u8> {
|
||||
let mut guard = BUFFERU8
|
||||
.lock()
|
||||
.map_err(|_| Error::CriticalError("Failed to lock buffer".to_string()))?;
|
||||
|
||||
if guard.is_some() {
|
||||
return Err(Error::CriticalError("Bytes already allocated".to_string()));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let layout = Layout::from_size_align_unchecked(len, LAYOUT_ALIGN);
|
||||
let ptr = alloc(layout);
|
||||
if ptr.is_null() {
|
||||
return Err(Error::CriticalError("Allocation failed".to_string()));
|
||||
}
|
||||
// TODO: Maybe this could be removed.
|
||||
ptr::write_bytes(ptr, 0, len);
|
||||
*guard = Some(Vec::from_raw_parts(ptr, len, len));
|
||||
Ok(ptr)
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
pub extern "C" fn free_bytes() -> Result<()> {
|
||||
crate::mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
#![allow(unused_mut, unused_variables)]
|
||||
use macros::ToJs;
|
||||
use macros::{wasm_error, ToJs};
|
||||
use mem::SerializableResult;
|
||||
use std::mem::size_of;
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
@@ -161,12 +161,14 @@ pub extern "C" fn start_shape_path_buffer() {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_path_chunk_buffer() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_path_chunk_buffer() -> Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
let buffer = get_path_upload_buffer();
|
||||
let mut buffer = buffer.lock().unwrap();
|
||||
buffer.extend_from_slice(&bytes);
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use macros::ToJs;
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use super::RawSegmentData;
|
||||
use crate::math;
|
||||
@@ -8,6 +8,9 @@ use crate::{mem, SerializableResult};
|
||||
use crate::{with_current_shape_mut, with_state, STATE};
|
||||
use std::mem::size_of;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::error::{Error, Result};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, ToJs)]
|
||||
#[repr(u8)]
|
||||
#[allow(dead_code)]
|
||||
@@ -43,15 +46,19 @@ pub extern "C" fn set_shape_bool_type(raw_bool_type: u8) {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn calculate_bool(raw_bool_type: u8) -> *mut u8 {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn calculate_bool(raw_bool_type: u8) -> Result<*mut u8> {
|
||||
let bytes = mem::bytes_or_empty();
|
||||
|
||||
let entries: Vec<Uuid> = bytes
|
||||
.chunks(size_of::<<Uuid as SerializableResult>::BytesType>())
|
||||
.map(|data| Uuid::try_from(data).unwrap())
|
||||
.collect();
|
||||
.map(|data| {
|
||||
// FIXME: Review if this should be an critical or a recoverable error.
|
||||
Uuid::try_from(data).map_err(|_| Error::RecoverableError("Invalid UUID".to_string()))
|
||||
})
|
||||
.collect::<Result<Vec<Uuid>>>()?;
|
||||
|
||||
mem::free_bytes();
|
||||
mem::free_bytes()?;
|
||||
|
||||
let bool_type = RawBoolType::from(raw_bool_type).into();
|
||||
let result;
|
||||
@@ -64,5 +71,5 @@ pub extern "C" fn calculate_bool(raw_bool_type: u8) -> *mut u8 {
|
||||
.map(RawSegmentData::from_segment)
|
||||
.collect();
|
||||
});
|
||||
mem::write_vec(result)
|
||||
Ok(mem::write_vec(result))
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use macros::ToJs;
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use super::{fills::RawFillData, fonts::RawFontStyle};
|
||||
|
||||
@@ -9,6 +9,8 @@ use crate::shapes::{
|
||||
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
||||
use crate::{with_current_shape, with_current_shape_mut, with_state, with_state_mut, STATE};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
|
||||
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
||||
|
||||
@@ -285,16 +287,22 @@ pub extern "C" fn clear_shape_text() {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_text_content() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn set_shape_text_content() -> crate::error::Result<()> {
|
||||
let bytes = mem::bytes();
|
||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||
let raw_text_data = RawParagraph::try_from(&bytes).unwrap();
|
||||
|
||||
if shape.add_paragraph(raw_text_data.into()).is_err() {
|
||||
println!("Error with set_shape_text_content on {:?}", shape.id);
|
||||
}
|
||||
shape.add_paragraph(raw_text_data.into()).map_err(|_| {
|
||||
Error::RecoverableError(format!(
|
||||
"Error with set_shape_text_content on {:?}",
|
||||
shape.id
|
||||
))
|
||||
})?;
|
||||
});
|
||||
mem::free_bytes();
|
||||
|
||||
mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::math::{Matrix, Point, Rect};
|
||||
use crate::mem;
|
||||
use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign};
|
||||
use crate::state::{TextCursor, TextSelection};
|
||||
use crate::shapes::{Paragraph, Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||
use crate::state::TextSelection;
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
use crate::utils::uuid_to_u32_quartet;
|
||||
use crate::{with_state, with_state_mut, STATE};
|
||||
use macros::ToJs;
|
||||
|
||||
#[derive(PartialEq, ToJs)]
|
||||
#[repr(u8)]
|
||||
@@ -132,7 +134,7 @@ pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state.text_editor_state.set_caret_from_position(position);
|
||||
state.text_editor_state.set_caret_from_position(&position);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -168,7 +170,7 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
||||
{
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(position);
|
||||
.extend_selection_from_position(&position);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -203,7 +205,7 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||
{
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(position);
|
||||
.extend_selection_from_position(&position);
|
||||
}
|
||||
state.text_editor_state.stop_pointer_selection();
|
||||
});
|
||||
@@ -231,7 +233,7 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state.text_editor_state.set_caret_from_position(position);
|
||||
state.text_editor_state.set_caret_from_position(&position);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -240,29 +242,29 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
// TEXT OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
// FIXME: Review if all the return Ok(()) should be Err instead.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_insert_text() {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn text_editor_insert_text() -> Result<()> {
|
||||
let bytes = crate::mem::bytes();
|
||||
let text = match String::from_utf8(bytes) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return,
|
||||
};
|
||||
let text = String::from_utf8(bytes)
|
||||
.map_err(|_| Error::RecoverableError("Invalid UTF-8 string".to_string()))?;
|
||||
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let selection = state.text_editor_state.selection;
|
||||
@@ -276,7 +278,8 @@ pub extern "C" fn text_editor_insert_text() {
|
||||
let cursor = state.text_editor_state.selection.focus;
|
||||
|
||||
if let Some(new_offset) = insert_text_at_cursor(text_content, &cursor, &text) {
|
||||
let new_cursor = TextCursor::new(cursor.paragraph, new_offset);
|
||||
let new_cursor =
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, new_offset);
|
||||
state.text_editor_state.selection.set_caret(new_cursor);
|
||||
}
|
||||
|
||||
@@ -286,15 +289,16 @@ pub extern "C" fn text_editor_insert_text() {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::ContentChanged);
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
|
||||
crate::mem::free_bytes();
|
||||
crate::mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -336,10 +340,10 @@ pub extern "C" fn text_editor_delete_backward() {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::ContentChanged);
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
@@ -384,10 +388,10 @@ pub extern "C" fn text_editor_delete_forward() {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::ContentChanged);
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
@@ -423,7 +427,8 @@ pub extern "C" fn text_editor_insert_paragraph() {
|
||||
let cursor = state.text_editor_state.selection.focus;
|
||||
|
||||
if split_paragraph_at_cursor(text_content, &cursor) {
|
||||
let new_cursor = TextCursor::new(cursor.paragraph + 1, 0);
|
||||
let new_cursor =
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0);
|
||||
state.text_editor_state.selection.set_caret(new_cursor);
|
||||
}
|
||||
|
||||
@@ -433,10 +438,10 @@ pub extern "C" fn text_editor_insert_paragraph() {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::ContentChanged);
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
@@ -494,7 +499,7 @@ pub extern "C" fn text_editor_move_cursor(direction: CursorDirection, extend_sel
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::SelectionChanged);
|
||||
.push_event(crate::state::TextEditorEvent::SelectionChanged);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -711,12 +716,12 @@ pub extern "C" fn text_editor_export_selection() -> *mut u8 {
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum();
|
||||
let range_start = if para_idx == start.paragraph {
|
||||
start.char_offset
|
||||
start.offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.char_offset
|
||||
end.offset
|
||||
} else {
|
||||
para_char_count
|
||||
};
|
||||
@@ -764,9 +769,9 @@ pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 {
|
||||
let sel = &state.text_editor_state.selection;
|
||||
unsafe {
|
||||
*buffer_ptr = sel.anchor.paragraph as u32;
|
||||
*buffer_ptr.add(1) = sel.anchor.char_offset as u32;
|
||||
*buffer_ptr.add(1) = sel.anchor.offset as u32;
|
||||
*buffer_ptr.add(2) = sel.focus.paragraph as u32;
|
||||
*buffer_ptr.add(3) = sel.focus.char_offset as u32;
|
||||
*buffer_ptr.add(3) = sel.focus.offset as u32;
|
||||
}
|
||||
1
|
||||
})
|
||||
@@ -776,7 +781,11 @@ pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 {
|
||||
// HELPERS: Cursor & Selection
|
||||
// ============================================================================
|
||||
|
||||
fn get_cursor_rect(text_content: &TextContent, cursor: &TextCursor, shape: &Shape) -> Option<Rect> {
|
||||
fn get_cursor_rect(
|
||||
text_content: &TextContent,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
shape: &Shape,
|
||||
) -> Option<Rect> {
|
||||
let paragraphs = text_content.paragraphs();
|
||||
if cursor.paragraph >= paragraphs.len() {
|
||||
return None;
|
||||
@@ -794,7 +803,7 @@ fn get_cursor_rect(text_content: &TextContent, cursor: &TextCursor, shape: &Shap
|
||||
let mut y_offset = valign_offset;
|
||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||
if idx == cursor.paragraph {
|
||||
let char_pos = cursor.char_offset;
|
||||
let char_pos = cursor.offset;
|
||||
|
||||
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
||||
let rects = laid_out_para.get_rects_for_range(
|
||||
@@ -869,13 +878,13 @@ fn get_selection_rects(
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum();
|
||||
let range_start = if para_idx == start.paragraph {
|
||||
start.char_offset
|
||||
start.offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.char_offset
|
||||
end.offset
|
||||
} else {
|
||||
para_char_count
|
||||
};
|
||||
@@ -914,40 +923,49 @@ fn paragraph_char_count(para: &Paragraph) -> usize {
|
||||
}
|
||||
|
||||
/// Clamp a cursor position to valid bounds within the text content.
|
||||
fn clamp_cursor(cursor: TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
fn clamp_cursor(
|
||||
position: TextPositionWithAffinity,
|
||||
paragraphs: &[Paragraph],
|
||||
) -> TextPositionWithAffinity {
|
||||
if paragraphs.is_empty() {
|
||||
return TextCursor::new(0, 0);
|
||||
return TextPositionWithAffinity::new_without_affinity(0, 0);
|
||||
}
|
||||
|
||||
let para_idx = cursor.paragraph.min(paragraphs.len() - 1);
|
||||
let para_idx = position.paragraph.min(paragraphs.len() - 1);
|
||||
let para_len = paragraph_char_count(¶graphs[para_idx]);
|
||||
let char_offset = cursor.char_offset.min(para_len);
|
||||
let char_offset = position.offset.min(para_len);
|
||||
|
||||
TextCursor::new(para_idx, char_offset)
|
||||
TextPositionWithAffinity::new_without_affinity(para_idx, char_offset)
|
||||
}
|
||||
|
||||
/// Move cursor left by one character.
|
||||
fn move_cursor_backward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
if cursor.char_offset > 0 {
|
||||
TextCursor::new(cursor.paragraph, cursor.char_offset - 1)
|
||||
fn move_cursor_backward(
|
||||
cursor: &TextPositionWithAffinity,
|
||||
paragraphs: &[Paragraph],
|
||||
) -> TextPositionWithAffinity {
|
||||
if cursor.offset > 0 {
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, cursor.offset - 1)
|
||||
} else if cursor.paragraph > 0 {
|
||||
let prev_para = cursor.paragraph - 1;
|
||||
let char_count = paragraph_char_count(¶graphs[prev_para]);
|
||||
TextCursor::new(prev_para, char_count)
|
||||
TextPositionWithAffinity::new_without_affinity(prev_para, char_count)
|
||||
} else {
|
||||
*cursor
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor right by one character.
|
||||
fn move_cursor_forward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
fn move_cursor_forward(
|
||||
cursor: &TextPositionWithAffinity,
|
||||
paragraphs: &[Paragraph],
|
||||
) -> TextPositionWithAffinity {
|
||||
let para = ¶graphs[cursor.paragraph];
|
||||
let char_count = paragraph_char_count(para);
|
||||
|
||||
if cursor.char_offset < char_count {
|
||||
TextCursor::new(cursor.paragraph, cursor.char_offset + 1)
|
||||
if cursor.offset < char_count {
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, cursor.offset + 1)
|
||||
} else if cursor.paragraph < paragraphs.len() - 1 {
|
||||
TextCursor::new(cursor.paragraph + 1, 0)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0)
|
||||
} else {
|
||||
*cursor
|
||||
}
|
||||
@@ -955,52 +973,58 @@ fn move_cursor_forward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCur
|
||||
|
||||
/// Move cursor up by one line.
|
||||
fn move_cursor_up(
|
||||
cursor: &TextCursor,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
paragraphs: &[Paragraph],
|
||||
_text_content: &TextContent,
|
||||
_shape: &Shape,
|
||||
) -> TextCursor {
|
||||
) -> TextPositionWithAffinity {
|
||||
// TODO: Implement proper line-based navigation using line metrics
|
||||
if cursor.paragraph > 0 {
|
||||
let prev_para = cursor.paragraph - 1;
|
||||
let char_count = paragraph_char_count(¶graphs[prev_para]);
|
||||
let new_offset = cursor.char_offset.min(char_count);
|
||||
TextCursor::new(prev_para, new_offset)
|
||||
let new_offset = cursor.offset.min(char_count);
|
||||
TextPositionWithAffinity::new_without_affinity(prev_para, new_offset)
|
||||
} else {
|
||||
TextCursor::new(cursor.paragraph, 0)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor down by one line.
|
||||
fn move_cursor_down(
|
||||
cursor: &TextCursor,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
paragraphs: &[Paragraph],
|
||||
_text_content: &TextContent,
|
||||
_shape: &Shape,
|
||||
) -> TextCursor {
|
||||
) -> TextPositionWithAffinity {
|
||||
// TODO: Implement proper line-based navigation using line metrics
|
||||
if cursor.paragraph < paragraphs.len() - 1 {
|
||||
let next_para = cursor.paragraph + 1;
|
||||
let char_count = paragraph_char_count(¶graphs[next_para]);
|
||||
let new_offset = cursor.char_offset.min(char_count);
|
||||
TextCursor::new(next_para, new_offset)
|
||||
let new_offset = cursor.offset.min(char_count);
|
||||
TextPositionWithAffinity::new_without_affinity(next_para, new_offset)
|
||||
} else {
|
||||
let char_count = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
TextCursor::new(cursor.paragraph, char_count)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, char_count)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor to start of current line.
|
||||
fn move_cursor_line_start(cursor: &TextCursor, _paragraphs: &[Paragraph]) -> TextCursor {
|
||||
fn move_cursor_line_start(
|
||||
cursor: &TextPositionWithAffinity,
|
||||
_paragraphs: &[Paragraph],
|
||||
) -> TextPositionWithAffinity {
|
||||
// TODO: Implement proper line-start using line metrics
|
||||
TextCursor::new(cursor.paragraph, 0)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, 0)
|
||||
}
|
||||
|
||||
/// Move cursor to end of current line.
|
||||
fn move_cursor_line_end(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
fn move_cursor_line_end(
|
||||
cursor: &TextPositionWithAffinity,
|
||||
paragraphs: &[Paragraph],
|
||||
) -> TextPositionWithAffinity {
|
||||
// TODO: Implement proper line-end using line metrics
|
||||
let char_count = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
TextCursor::new(cursor.paragraph, char_count)
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, char_count)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1028,7 +1052,7 @@ fn find_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, u
|
||||
/// Insert text at a cursor position. Returns the new character offset after insertion.
|
||||
fn insert_text_at_cursor(
|
||||
text_content: &mut TextContent,
|
||||
cursor: &TextCursor,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
text: &str,
|
||||
) -> Option<usize> {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
@@ -1048,7 +1072,7 @@ fn insert_text_at_cursor(
|
||||
return Some(text.chars().count());
|
||||
}
|
||||
|
||||
let (span_idx, offset_in_span) = find_span_at_offset(para, cursor.char_offset)?;
|
||||
let (span_idx, offset_in_span) = find_span_at_offset(para, cursor.offset)?;
|
||||
|
||||
let children = para.children_mut();
|
||||
let span = &mut children[span_idx];
|
||||
@@ -1063,7 +1087,7 @@ fn insert_text_at_cursor(
|
||||
new_text.insert_str(byte_offset, text);
|
||||
span.set_text(new_text);
|
||||
|
||||
Some(cursor.char_offset + text.chars().count())
|
||||
Some(cursor.offset + text.chars().count())
|
||||
}
|
||||
|
||||
/// Delete a range of text specified by a selection.
|
||||
@@ -1077,20 +1101,16 @@ fn delete_selection_range(text_content: &mut TextContent, selection: &TextSelect
|
||||
}
|
||||
|
||||
if start.paragraph == end.paragraph {
|
||||
delete_range_in_paragraph(
|
||||
&mut paragraphs[start.paragraph],
|
||||
start.char_offset,
|
||||
end.char_offset,
|
||||
);
|
||||
delete_range_in_paragraph(&mut paragraphs[start.paragraph], start.offset, end.offset);
|
||||
} else {
|
||||
let start_para_len = paragraph_char_count(¶graphs[start.paragraph]);
|
||||
delete_range_in_paragraph(
|
||||
&mut paragraphs[start.paragraph],
|
||||
start.char_offset,
|
||||
start.offset,
|
||||
start_para_len,
|
||||
);
|
||||
|
||||
delete_range_in_paragraph(&mut paragraphs[end.paragraph], 0, end.char_offset);
|
||||
delete_range_in_paragraph(&mut paragraphs[end.paragraph], 0, end.offset);
|
||||
|
||||
if end.paragraph < paragraphs.len() {
|
||||
let end_para_children: Vec<_> =
|
||||
@@ -1189,13 +1209,19 @@ fn delete_range_in_paragraph(para: &mut Paragraph, start_offset: usize, end_offs
|
||||
}
|
||||
|
||||
/// Delete the character before the cursor. Returns the new cursor position.
|
||||
fn delete_char_before(text_content: &mut TextContent, cursor: &TextCursor) -> Option<TextCursor> {
|
||||
if cursor.char_offset > 0 {
|
||||
fn delete_char_before(
|
||||
text_content: &mut TextContent,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
) -> Option<TextPositionWithAffinity> {
|
||||
if cursor.offset > 0 {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
let para = &mut paragraphs[cursor.paragraph];
|
||||
let delete_pos = cursor.char_offset - 1;
|
||||
delete_range_in_paragraph(para, delete_pos, cursor.char_offset);
|
||||
Some(TextCursor::new(cursor.paragraph, delete_pos))
|
||||
let delete_pos = cursor.offset - 1;
|
||||
delete_range_in_paragraph(para, delete_pos, cursor.offset);
|
||||
Some(TextPositionWithAffinity::new_without_affinity(
|
||||
cursor.paragraph,
|
||||
delete_pos,
|
||||
))
|
||||
} else if cursor.paragraph > 0 {
|
||||
let prev_para_idx = cursor.paragraph - 1;
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
@@ -1211,14 +1237,17 @@ fn delete_char_before(text_content: &mut TextContent, cursor: &TextCursor) -> Op
|
||||
|
||||
paragraphs.remove(cursor.paragraph);
|
||||
|
||||
Some(TextCursor::new(prev_para_idx, prev_para_len))
|
||||
Some(TextPositionWithAffinity::new_without_affinity(
|
||||
prev_para_idx,
|
||||
prev_para_len,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the character after the cursor.
|
||||
fn delete_char_after(text_content: &mut TextContent, cursor: &TextCursor) {
|
||||
fn delete_char_after(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
if cursor.paragraph >= paragraphs.len() {
|
||||
return;
|
||||
@@ -1226,9 +1255,9 @@ fn delete_char_after(text_content: &mut TextContent, cursor: &TextCursor) {
|
||||
|
||||
let para_len = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
|
||||
if cursor.char_offset < para_len {
|
||||
if cursor.offset < para_len {
|
||||
let para = &mut paragraphs[cursor.paragraph];
|
||||
delete_range_in_paragraph(para, cursor.char_offset, cursor.char_offset + 1);
|
||||
delete_range_in_paragraph(para, cursor.offset, cursor.offset + 1);
|
||||
} else if cursor.paragraph < paragraphs.len() - 1 {
|
||||
let next_para_idx = cursor.paragraph + 1;
|
||||
let next_children: Vec<_> = paragraphs[next_para_idx].children_mut().drain(..).collect();
|
||||
@@ -1241,7 +1270,10 @@ fn delete_char_after(text_content: &mut TextContent, cursor: &TextCursor) {
|
||||
}
|
||||
|
||||
/// Split a paragraph at the cursor position. Returns true if split was successful.
|
||||
fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextCursor) -> bool {
|
||||
fn split_paragraph_at_cursor(
|
||||
text_content: &mut TextContent,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
) -> bool {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
if cursor.paragraph >= paragraphs.len() {
|
||||
return false;
|
||||
@@ -1249,7 +1281,7 @@ fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextCursor
|
||||
|
||||
let para = ¶graphs[cursor.paragraph];
|
||||
|
||||
let Some((span_idx, offset_in_span)) = find_span_at_offset(para, cursor.char_offset) else {
|
||||
let Some((span_idx, offset_in_span)) = find_span_at_offset(para, cursor.offset) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user