mirror of
https://github.com/penpot/penpot.git
synced 2026-02-28 04:38:17 -05:00
Compare commits
1 Commits
azazeln28-
...
ladybenko-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30de0bd79e |
@@ -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)
|
||||
|
||||
@@ -86,24 +86,6 @@
|
||||
:else
|
||||
(enabled-by-flags? state feature))))
|
||||
|
||||
(defn active-features?
|
||||
"Given a state and a set of features, check if the features are all enabled."
|
||||
([state a]
|
||||
(js/console.warn "Please, use active-feature? instead")
|
||||
(active-feature? state a))
|
||||
([state a b]
|
||||
(and ^boolean (active-feature? state a)
|
||||
^boolean (active-feature? state b)))
|
||||
([state a b c]
|
||||
(and ^boolean (active-feature? state a)
|
||||
^boolean (active-feature? state b)
|
||||
^boolean (active-feature? state c)))
|
||||
([state a b c & others]
|
||||
(and ^boolean (active-feature? state a)
|
||||
^boolean (active-feature? state b)
|
||||
^boolean (active-feature? state c)
|
||||
^boolean (every? #(active-feature? state %) others))))
|
||||
|
||||
(def ^:private features-ref
|
||||
(l/derived (l/key :features) st/state))
|
||||
|
||||
|
||||
@@ -131,8 +131,7 @@
|
||||
|
||||
on-style-change
|
||||
(fn [event]
|
||||
(let [
|
||||
styles (styles/get-styles-from-event event)]
|
||||
(let [styles (styles/get-styles-from-event event)]
|
||||
(st/emit! (dwt/v2-update-text-editor-styles shape-id styles))))
|
||||
|
||||
on-needs-layout
|
||||
|
||||
@@ -1,338 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.shapes.text.v3-editor
|
||||
"Contenteditable DOM element for WASM text editor input"
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.math :as mth]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.css-cursors :as cur]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.text-editor :as text-editor]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.object :as obj]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]))
|
||||
|
||||
(def caret-blink-interval-ms 250)
|
||||
|
||||
(defn- sync-wasm-text-editor-content!
|
||||
"Sync WASM text editor content back to the shape via the standard
|
||||
commit pipeline. Called after every text-modifying input."
|
||||
[& {:keys [finalize?]}]
|
||||
(when-let [{:keys [shape-id content]}
|
||||
(text-editor/text-editor-sync-content)]
|
||||
(st/emit! (dwt/v2-update-text-shape-content
|
||||
shape-id content
|
||||
:update-name? true
|
||||
:finalize? finalize?))))
|
||||
|
||||
(defn- font-family-from-font-id [font-id]
|
||||
(if (str/includes? font-id "gfont-noto-sans")
|
||||
(let [lang (str/replace font-id #"gfont\-noto\-sans\-" "")]
|
||||
(if (>= (count lang) 3) (str/capital lang) (str/upper lang)))
|
||||
"Noto Color Emoji"))
|
||||
|
||||
(mf/defc text-editor
|
||||
"Contenteditable element positioned over the text shape to capture input events."
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [shape (obj/get props "shape")
|
||||
shape-id (dm/get-prop shape :id)
|
||||
|
||||
clip-id (dm/str "text-edition-clip" shape-id)
|
||||
|
||||
contenteditable-ref (mf/use-ref nil)
|
||||
composing? (mf/use-state false)
|
||||
|
||||
fallback-fonts (wasm.api/fonts-from-text-content (:content shape) false)
|
||||
fallback-families (map (fn [font]
|
||||
(font-family-from-font-id (:font-id font))) fallback-fonts)
|
||||
|
||||
;; Calculate screen position from shape bounds
|
||||
bounds (gsh/shape->rect shape)
|
||||
|
||||
x (mth/min (dm/get-prop bounds :x)
|
||||
(dm/get-prop shape :x))
|
||||
y (mth/min (dm/get-prop bounds :y)
|
||||
(dm/get-prop shape :y))
|
||||
width (mth/max (dm/get-prop bounds :width)
|
||||
(dm/get-prop shape :width))
|
||||
height (mth/max (dm/get-prop bounds :height)
|
||||
(dm/get-prop shape :height))
|
||||
|
||||
[{:keys [x y width height]} transform]
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
||||
selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
||||
selrect-height (:height selrect)
|
||||
selrect-width (:width selrect)
|
||||
max-width (max width selrect-width)
|
||||
max-height (max height selrect-height)
|
||||
valign (-> shape :content :vertical-align)
|
||||
y (:y selrect)
|
||||
y (case valign
|
||||
"bottom" (+ y (- selrect-height height))
|
||||
"center" (+ y (/ (- selrect-height height) 2))
|
||||
y)]
|
||||
[(assoc selrect :y y :width max-width :height max-height) transform])
|
||||
|
||||
on-composition-start
|
||||
(mf/use-fn
|
||||
(fn [_event]
|
||||
(reset! composing? true)))
|
||||
|
||||
on-composition-end
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(reset! composing? false)
|
||||
(let [data (.-data event)]
|
||||
(when data
|
||||
(text-editor/text-editor-insert-text data)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-composition"))
|
||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||
(set! (.-textContent node) "")))))
|
||||
|
||||
on-paste
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(dom/prevent-default event)
|
||||
(let [clipboard-data (.-clipboardData event)
|
||||
text (.getData clipboard-data "text/plain")]
|
||||
(when (and text (seq text))
|
||||
(text-editor/text-editor-insert-text text)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-paste"))
|
||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||
(set! (.-textContent node) "")))))
|
||||
|
||||
on-copy
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(when (text-editor/text-editor-is-active?)
|
||||
(dom/prevent-default event)
|
||||
(when (text-editor/text-editor-get-selection)
|
||||
(let [text (text-editor/text-editor-export-selection)]
|
||||
(.setData (.-clipboardData event) "text/plain" text))))))
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event) (.-key event))
|
||||
(when (and (text-editor/text-editor-is-active?)
|
||||
(not @composing?))
|
||||
|
||||
(let [key (.-key event)
|
||||
ctrl? (or (.-ctrlKey event) (.-metaKey event))
|
||||
shift? (.-shiftKey event)]
|
||||
|
||||
(cond
|
||||
;; Escape: finalize and stop
|
||||
(= key "Escape")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(sync-wasm-text-editor-content! :finalize? true)
|
||||
(text-editor/text-editor-stop))
|
||||
|
||||
;; Ctrl+A: select all (key is "a" or "A" depending on platform)
|
||||
(and ctrl? (= (str/lower key) "a"))
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-select-all)
|
||||
(wasm.api/request-render "text-select-all"))
|
||||
|
||||
;; Enter
|
||||
(= key "Enter")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-insert-paragraph)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-paragraph"))
|
||||
|
||||
;; Backspace
|
||||
(= key "Backspace")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-delete-backward)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-delete-backward"))
|
||||
|
||||
;; Delete
|
||||
(= key "Delete")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-delete-forward)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-delete-forward"))
|
||||
|
||||
;; Arrow keys
|
||||
(= key "ArrowLeft")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 0 shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
|
||||
(= key "ArrowRight")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 1 shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
|
||||
(= key "ArrowUp")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 2 shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
|
||||
(= key "ArrowDown")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 3 shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
|
||||
(= key "Home")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 4 shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
|
||||
(= key "End")
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(text-editor/text-editor-move-cursor 5 shift?)
|
||||
(wasm.api/request-render "text-cursor-move"))
|
||||
|
||||
;; Let contenteditable handle text input via on-input
|
||||
:else nil)))))
|
||||
|
||||
on-input
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log "event" event)
|
||||
(let [native-event (.-nativeEvent event)
|
||||
input-type (.-inputType native-event)
|
||||
data (.-data native-event)]
|
||||
;; Skip composition-related input events - composition-end handles those
|
||||
(when (and (not @composing?)
|
||||
(not= input-type "insertCompositionText"))
|
||||
(when (and data (seq data))
|
||||
(text-editor/text-editor-insert-text data)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-input"))
|
||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||
(set! (.-textContent node) ""))))))
|
||||
|
||||
on-pointer-down
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event))
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-pointer-down (.-x off-pt) (.-y off-pt)))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event))
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-pointer-move (.-x off-pt) (.-y off-pt)))))
|
||||
|
||||
on-pointer-up
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event))
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-pointer-up (.-x off-pt) (.-y off-pt)))))
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event))
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)]
|
||||
(wasm.api/text-editor-set-cursor-from-offset (.-x off-pt) (.-y off-pt)))))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event) event)))
|
||||
|
||||
on-blur
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event) event)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/text-editor-stop)))
|
||||
|
||||
style #js {:pointerEvents "all"
|
||||
"--editor-container-width" (dm/str width "px")
|
||||
"--editor-container-height" (dm/str height "px")
|
||||
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")}]
|
||||
|
||||
;; Focus contenteditable on mount
|
||||
(mf/use-effect
|
||||
(mf/deps contenteditable-ref)
|
||||
(fn []
|
||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||
(js/console.log "focusing")
|
||||
(.focus node))))
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [timeout-id (atom nil)
|
||||
schedule-blink (fn schedule-blink []
|
||||
(when (text-editor/text-editor-is-active?)
|
||||
(wasm.api/request-render "cursor-blink"))
|
||||
(reset! timeout-id (js/setTimeout schedule-blink caret-blink-interval-ms)))]
|
||||
(schedule-blink)
|
||||
(fn []
|
||||
(when @timeout-id
|
||||
(js/clearTimeout @timeout-id))))))
|
||||
|
||||
;; Composition and input events
|
||||
[:g.text-editor {:clip-path (dm/fmt "url(#%)" clip-id)
|
||||
:transform (dm/str transform)
|
||||
:data-testid "text-editor"}
|
||||
[:defs
|
||||
[:clipPath {:id clip-id}
|
||||
[:rect {:x x :y y :width width :height height}]]]
|
||||
|
||||
[:foreignObject {:x x :y y :width width :height height}
|
||||
[:div {:on-click on-click
|
||||
:on-pointer-down on-pointer-down
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-pointer-up on-pointer-up
|
||||
:class (stl/css :text-editor)
|
||||
:style style}
|
||||
[:div
|
||||
{:ref contenteditable-ref
|
||||
:contentEditable true
|
||||
:suppressContentEditableWarning true
|
||||
:on-composition-start on-composition-start
|
||||
:on-composition-end on-composition-end
|
||||
:on-key-down on-key-down
|
||||
:on-input on-input
|
||||
:on-paste on-paste
|
||||
:on-copy on-copy
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
;; FIXME on-click
|
||||
;; :on-click on-click
|
||||
:id "text-editor-wasm-input"
|
||||
:class (dm/str (cur/get-dynamic "text" (:rotation shape))
|
||||
" "
|
||||
(stl/css :text-editor-container))
|
||||
:data-testid "text-editor-container"}]]]]))
|
||||
@@ -1,12 +0,0 @@
|
||||
.text-editor {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.text-editor-container {
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
}
|
||||
@@ -94,8 +94,23 @@
|
||||
(st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?)
|
||||
::dwsp/interrupt)
|
||||
|
||||
(when (wasm.api/text-editor-is-active?)
|
||||
(wasm.api/text-editor-pointer-down (.-x off-pt) (.-y off-pt)))
|
||||
|
||||
(when (and (not= edition id) (or text-editing? grid-editing?))
|
||||
(st/emit! (dw/clear-edition-mode)))
|
||||
(st/emit! (dw/clear-edition-mode))
|
||||
;; FIXME: I think this is not completely correct because this
|
||||
;; is going to happen even when clicking or selecting text.
|
||||
;; Sync and stop WASM text editor when exiting edit mode
|
||||
#_(when (and text-editing?
|
||||
(features/active-feature? @st/state "render-wasm/v1")
|
||||
wasm.wasm/context-initialized?)
|
||||
(when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)]
|
||||
(st/emit! (dwt/v2-update-text-shape-content
|
||||
shape-id content
|
||||
:update-name? true
|
||||
:finalize? true)))
|
||||
(wasm.api/text-editor-stop)))
|
||||
|
||||
(when (and (not text-editing?)
|
||||
(not blocked)
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
[app.main.ui.workspace.shapes.text.editor :as editor-v1]
|
||||
[app.main.ui.workspace.shapes.text.text-edition-outline :refer [text-edition-outline]]
|
||||
[app.main.ui.workspace.shapes.text.v2-editor :as editor-v2]
|
||||
[app.main.ui.workspace.shapes.text.v3-editor :as editor-v3]
|
||||
[app.main.ui.workspace.top-toolbar :refer [top-toolbar*]]
|
||||
[app.main.ui.workspace.viewport.actions :as actions]
|
||||
[app.main.ui.workspace.viewport.comments :as comments]
|
||||
@@ -55,6 +54,7 @@
|
||||
[app.main.ui.workspace.viewport.viewport-ref :refer [create-viewport-ref]]
|
||||
[app.main.ui.workspace.viewport.widgets :as widgets]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.text-editor-input :refer [text-editor-input]]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.text-editor :as ted]
|
||||
[beicon.v2.core :as rx]
|
||||
@@ -328,14 +328,10 @@
|
||||
|
||||
(mf/with-effect [show-text-editor? workspace-editor-state edition]
|
||||
(let [active-editor-state (get workspace-editor-state edition)]
|
||||
(js/console.log "show-text-editor?" show-text-editor?)
|
||||
(when (and show-text-editor? active-editor-state)
|
||||
(let [content (-> active-editor-state
|
||||
(ted/get-editor-current-content)
|
||||
(ted/export-content))]
|
||||
(when-not (wasm.api/text-editor-is-active? edition)
|
||||
(prn "hola")
|
||||
(wasm.api/text-editor-start edition))
|
||||
(wasm.api/use-shape edition)
|
||||
(wasm.api/set-shape-text-content edition content)
|
||||
(let [dimension (wasm.api/get-text-dimensions)]
|
||||
@@ -421,7 +417,14 @@
|
||||
|
||||
(when picking-color?
|
||||
[:> pixel-overlay/pixel-overlay-wasm* {:viewport-ref viewport-ref
|
||||
:canvas-ref canvas-ref}])]
|
||||
:canvas-ref canvas-ref}])
|
||||
|
||||
;; WASM text editor contenteditable (must be outside SVG to work)
|
||||
(when (and show-text-editor?
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1"))
|
||||
[:& text-editor-input {:shape editing-shape
|
||||
:zoom zoom
|
||||
:vbox vbox}])]
|
||||
|
||||
[:canvas {:id "render"
|
||||
:data-testid "canvas-wasm-shapes"
|
||||
@@ -468,20 +471,14 @@
|
||||
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
||||
;; Text editor handling:
|
||||
;; - When text-editor-wasm/v1 is active, contenteditable is rendered in viewport-overlays (HTML DOM)
|
||||
(when show-text-editor?
|
||||
(cond
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
[:& editor-v3/text-editor {:shape editing-shape
|
||||
:canvas-ref canvas-ref
|
||||
:ref text-editor-ref}]
|
||||
|
||||
(features/active-feature? @st/state "text-editor/v2")
|
||||
(when (and show-text-editor?
|
||||
(not (features/active-feature? @st/state "text-editor-wasm/v1")))
|
||||
(if (features/active-feature? @st/state "text-editor/v2")
|
||||
[:& editor-v2/text-editor {:shape editing-shape
|
||||
:canvas-ref canvas-ref
|
||||
:ref text-editor-ref}]
|
||||
|
||||
:else [:& editor-v1/text-editor-svg {:shape editing-shape
|
||||
:ref text-editor-ref}]))
|
||||
[:& editor-v1/text-editor-svg {:shape editing-shape
|
||||
:ref text-editor-ref}]))
|
||||
|
||||
(when show-frame-outline?
|
||||
(let [outlined-frame-id
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
;; Re-export public text editor functions
|
||||
(def text-editor-start text-editor/text-editor-start)
|
||||
(def text-editor-stop text-editor/text-editor-stop)
|
||||
(def text-editor-set-cursor-from-offset text-editor/text-editor-set-cursor-from-offset)
|
||||
(def text-editor-set-cursor-from-point text-editor/text-editor-set-cursor-from-point)
|
||||
(def text-editor-pointer-down text-editor/text-editor-pointer-down)
|
||||
(def text-editor-pointer-move text-editor/text-editor-pointer-move)
|
||||
|
||||
@@ -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#)))))))
|
||||
|
||||
@@ -22,14 +22,7 @@
|
||||
(aget buffer 2)
|
||||
(aget buffer 3)))))
|
||||
|
||||
(defn text-editor-set-cursor-from-offset
|
||||
"Sets caret position from shape relative coordinates"
|
||||
[x y]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_set_cursor_from_offset" x y)))
|
||||
|
||||
(defn text-editor-set-cursor-from-point
|
||||
"Sets caret position from screen (canvas) coordinates"
|
||||
[x y]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
|
||||
|
||||
@@ -203,7 +203,6 @@
|
||||
on-input
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log "event" event)
|
||||
(let [native-event (.-nativeEvent event)
|
||||
input-type (.-inputType native-event)
|
||||
data (.-data native-event)]
|
||||
@@ -215,17 +214,7 @@
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-input"))
|
||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||
(set! (.-textContent node) ""))))))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event) event)))
|
||||
|
||||
on-blur
|
||||
(mf/use-fn
|
||||
(fn [^js event]
|
||||
(js/console.log (.-type event) event)))]
|
||||
(set! (.-textContent node) ""))))))]
|
||||
|
||||
[:div
|
||||
{:ref contenteditable-ref
|
||||
@@ -236,8 +225,6 @@
|
||||
:on-input on-input
|
||||
:on-paste on-paste
|
||||
:on-copy on-copy
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
;; FIXME on-click
|
||||
;; :on-click on-click
|
||||
:id "text-editor-wasm-input"
|
||||
|
||||
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]);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -142,9 +142,9 @@ impl TextEditorState {
|
||||
self.is_active = false;
|
||||
self.active_shape_id = None;
|
||||
self.cursor_visible = false;
|
||||
self.last_blink_time = 0.0;
|
||||
self.is_pointer_selection_active = false;
|
||||
self.pending_events.clear();
|
||||
self.reset_blink();
|
||||
}
|
||||
|
||||
pub fn start_pointer_selection(&mut self) -> bool {
|
||||
@@ -195,11 +195,13 @@ impl TextEditorState {
|
||||
|
||||
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.set_caret(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.extend_to(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,3 +1,6 @@
|
||||
use macros::{wasm_error, ToJs};
|
||||
|
||||
use crate::error::Error;
|
||||
use crate::math::{Matrix, Point, Rect};
|
||||
use crate::mem;
|
||||
use crate::shapes::{Paragraph, Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||
@@ -5,7 +8,6 @@ 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)]
|
||||
@@ -126,9 +128,11 @@ pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
|
||||
return;
|
||||
};
|
||||
let point = Point::new(x, y);
|
||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||
let shape_matrix = shape.get_matrix();
|
||||
state.text_editor_state.start_pointer_selection();
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_shape_coords(&point)
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state.text_editor_state.set_caret_from_position(&position);
|
||||
}
|
||||
@@ -141,6 +145,7 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
}
|
||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||
let point = Point::new(x, y);
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
@@ -148,6 +153,11 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
let shape_matrix = shape.get_matrix();
|
||||
let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !state.text_editor_state.is_pointer_selection_active {
|
||||
return;
|
||||
}
|
||||
@@ -156,7 +166,7 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
||||
};
|
||||
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_shape_coords(&point)
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state
|
||||
.text_editor_state
|
||||
@@ -171,6 +181,7 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
}
|
||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||
let point = Point::new(x, y);
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
@@ -178,6 +189,11 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
let shape_matrix = shape.get_matrix();
|
||||
let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !state.text_editor_state.is_pointer_selection_active {
|
||||
return;
|
||||
}
|
||||
@@ -185,7 +201,7 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||
return;
|
||||
};
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_shape_coords(&point)
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state
|
||||
.text_editor_state
|
||||
@@ -195,31 +211,6 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_set_cursor_from_offset(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
}
|
||||
|
||||
let point = Point::new(x, y);
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
};
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_shape_coords(&point)
|
||||
{
|
||||
state.text_editor_state.set_caret_from_position(&position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
@@ -251,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;
|
||||
@@ -306,7 +297,8 @@ pub extern "C" fn text_editor_insert_text() {
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
|
||||
crate::mem::free_bytes();
|
||||
crate::mem::free_bytes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
Reference in New Issue
Block a user