mirror of
https://github.com/penpot/penpot.git
synced 2026-02-24 10:47:49 -05:00
Compare commits
3 Commits
superalex-
...
alotor-exp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b0662cfa30 | ||
|
|
32d4026641 | ||
|
|
4477b2b4a0 |
@@ -58,7 +58,8 @@
|
||||
:share-id share-id
|
||||
:object-id (mapv :id objects)
|
||||
:route "objects"
|
||||
:skip-children skip-children}
|
||||
:skip-children skip-children
|
||||
:wasm "true"}
|
||||
uri (-> (cf/get :public-uri)
|
||||
(assoc :path "/render.html")
|
||||
(assoc :query (u/map->query-string params)))]
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
[app.main.ui.shapes.svg-raw :as svg-raw]
|
||||
[app.main.ui.shapes.text :as text]
|
||||
[app.main.ui.shapes.text.fontfaces :as ff]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as g]
|
||||
[app.util.http :as http]
|
||||
[app.util.strings :as ust]
|
||||
[app.util.thumbnails :as th]
|
||||
@@ -53,6 +55,7 @@
|
||||
[beicon.v2.core :as rx]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:const viewbox-decimal-precision 3)
|
||||
@@ -171,6 +174,8 @@
|
||||
;; Don't wrap svg elements inside a <g> otherwise some can break
|
||||
[:> svg-raw-wrapper {:shape shape :frame frame}]))))))
|
||||
|
||||
(set! wasm.api/shape-wrapper-factory shape-wrapper-factory)
|
||||
|
||||
(defn format-viewbox
|
||||
"Format a viewbox given a rectangle"
|
||||
[{:keys [x y width height] :or {x 0 y 0 width 100 height 100}}]
|
||||
@@ -480,6 +485,48 @@
|
||||
[:& ff/fontfaces-style {:fonts fonts}]
|
||||
[:& shape-wrapper {:shape object}]]]]))
|
||||
|
||||
(mf/defc object-wasm
|
||||
{::mf/wrap [mf/memo]}
|
||||
[{:keys [objects object-id embed skip-children]
|
||||
:or {embed false}
|
||||
:as props}]
|
||||
(let [object (get objects object-id)
|
||||
object (cond-> object
|
||||
(:hide-fill-on-export object)
|
||||
(assoc :fills [])
|
||||
|
||||
skip-children
|
||||
(assoc :shapes []))
|
||||
|
||||
{:keys [width height] :as bounds}
|
||||
(gsb/get-object-bounds objects object {:ignore-margin? false})
|
||||
|
||||
vbox (format-viewbox bounds)
|
||||
zoom 1
|
||||
canvas-ref (mf/use-ref nil)]
|
||||
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [canvas (mf/ref-val canvas-ref)]
|
||||
(->> @wasm.api/module
|
||||
(p/fmap
|
||||
(fn [ready?]
|
||||
(when ready?
|
||||
(try
|
||||
(when (wasm.api/init-canvas-context canvas)
|
||||
(wasm.api/initialize-viewport
|
||||
objects zoom vbox "transparent"
|
||||
(fn []
|
||||
(wasm.api/render-sync-shape object-id)
|
||||
(dom/set-attribute! canvas "id" (dm/str "screenshot-" object-id)))))
|
||||
(catch :default e
|
||||
(js/console.error "Error initializing canvas context:" e)
|
||||
false)))))))))
|
||||
[:canvas {:ref canvas-ref
|
||||
:width width
|
||||
:height height
|
||||
:style {:background "red"}}]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SPRITES (DEBUG)
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
[app.main.data.workspace.media :as dwm]
|
||||
[app.main.data.workspace.path :as dwdp]
|
||||
[app.main.data.workspace.specialized-panel :as-alias dwsp]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -50,41 +49,42 @@
|
||||
(mf/deps id blocked hidden type selected edition drawing-tool text-editing?
|
||||
node-editing? grid-editing? drawing-path? create-comment? @z? @space?
|
||||
panning read-only?)
|
||||
(fn [bevent]
|
||||
(fn [event]
|
||||
;; We need to handle editor related stuff here because
|
||||
;; handling on editor dom node does not works properly.
|
||||
(let [target (dom/get-target bevent)
|
||||
(let [target (dom/get-target event)
|
||||
editor (txu/closest-text-editor-content target)]
|
||||
;; Capture mouse pointer to detect the movements even if cursor
|
||||
;; leaves the viewport or the browser itself
|
||||
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
|
||||
(if editor
|
||||
(.setPointerCapture editor (.-pointerId bevent))
|
||||
(.setPointerCapture target (.-pointerId bevent))))
|
||||
(.setPointerCapture editor (.-pointerId event))
|
||||
(.setPointerCapture target (.-pointerId event))))
|
||||
|
||||
(when (or (dom/class? (dom/get-target bevent) "viewport-controls")
|
||||
(dom/class? (dom/get-target bevent) "viewport-selrect")
|
||||
(dom/child? (dom/get-target bevent) (dom/query ".grid-layout-editor")))
|
||||
(when (or (dom/class? (dom/get-target event) "viewport-controls")
|
||||
(dom/class? (dom/get-target event) "viewport-selrect")
|
||||
(dom/child? (dom/get-target event) (dom/query ".grid-layout-editor")))
|
||||
|
||||
(dom/stop-propagation bevent)
|
||||
(dom/stop-propagation event)
|
||||
|
||||
(when-not @z?
|
||||
(let [event (dom/event->native-event bevent)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
meta? (kbd/meta? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
mod? (kbd/mod? event)
|
||||
(let [native-event (dom/event->native-event event)
|
||||
ctrl? (kbd/ctrl? native-event)
|
||||
meta? (kbd/meta? native-event)
|
||||
shift? (kbd/shift? native-event)
|
||||
alt? (kbd/alt? native-event)
|
||||
mod? (kbd/mod? native-event)
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
|
||||
left-click? (and (not panning) (dom/left-mouse? bevent))
|
||||
middle-click? (and (not panning) (dom/middle-mouse? bevent))]
|
||||
left-click? (and (not panning) (dom/left-mouse? event))
|
||||
middle-click? (and (not panning) (dom/middle-mouse? event))]
|
||||
|
||||
(cond
|
||||
(or middle-click? (and left-click? @space?))
|
||||
(do
|
||||
(dom/prevent-default bevent)
|
||||
(dom/prevent-default event)
|
||||
(if mod?
|
||||
(let [raw-pt (dom/get-client-position event)
|
||||
(let [raw-pt (dom/get-client-position native-event)
|
||||
pt (uwvv/point->viewport raw-pt)]
|
||||
(st/emit! (dw/start-zooming pt)))
|
||||
(st/emit! (dw/start-panning))))
|
||||
@@ -94,18 +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))
|
||||
;; 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 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)
|
||||
@@ -187,10 +192,14 @@
|
||||
alt? (kbd/alt? event)
|
||||
meta? (kbd/meta? event)
|
||||
hovering? (some? @hover)
|
||||
native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
raw-pt (dom/get-client-position event)
|
||||
pt (uwvv/point->viewport raw-pt)]
|
||||
(st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?))
|
||||
|
||||
;; FIXME: Maybe we can transform this into a cond instead
|
||||
;; of multiple (when)s.
|
||||
(when (and hovering?
|
||||
(not @space?)
|
||||
(not edition)
|
||||
@@ -198,6 +207,8 @@
|
||||
(not drawing-tool))
|
||||
(st/emit! (dw/select-shape (:id @hover) shift?)))
|
||||
|
||||
;; FIXME: Maybe we can move into a function of the kind
|
||||
;; "text-editor-on-click"
|
||||
;; If clicking on a text shape and wasm render is enabled, forward cursor position
|
||||
(when (and hovering?
|
||||
(not @space?)
|
||||
@@ -208,9 +219,7 @@
|
||||
(when (and (= :text (:type hover-shape))
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
wasm.wasm/context-initialized?)
|
||||
(let [raw-pt (dom/get-client-position event)]
|
||||
;; FIXME
|
||||
(wasm.api/text-editor-set-cursor-from-point (.-x raw-pt) (.-y raw-pt))))))
|
||||
(wasm.api/text-editor-set-cursor-from-point (.-x off-pt) (.-y off-pt)))))
|
||||
|
||||
(when (and @z?
|
||||
(not @space?)
|
||||
@@ -261,6 +270,12 @@
|
||||
wasm.wasm/context-initialized?)
|
||||
(wasm.api/text-editor-start id)))
|
||||
|
||||
(and editable? (= id edition) (not read-only?)
|
||||
(= type :text)
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
wasm.wasm/context-initialized?)
|
||||
(wasm.api/text-editor-select-all)
|
||||
|
||||
(some? selected-shape)
|
||||
(do
|
||||
(reset! hover selected-shape)
|
||||
@@ -310,20 +325,24 @@
|
||||
;; Release pointer on mouse up
|
||||
(.releasePointerCapture target (.-pointerId event)))
|
||||
|
||||
(let [event (dom/event->native-event event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
meta? (kbd/meta? event)
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
ctrl? (kbd/ctrl? native-event)
|
||||
shift? (kbd/shift? native-event)
|
||||
alt? (kbd/alt? native-event)
|
||||
meta? (kbd/meta? native-event)
|
||||
|
||||
left-click? (= 1 (.-which event))
|
||||
middle-click? (= 2 (.-which event))]
|
||||
left-click? (= 1 (.-which native-event))
|
||||
middle-click? (= 2 (.-which native-event))]
|
||||
|
||||
(when left-click?
|
||||
(st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)))
|
||||
(st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?))
|
||||
|
||||
(when (wasm.api/text-editor-is-active?)
|
||||
(wasm.api/text-editor-pointer-up (.-x off-pt) (.-y off-pt))))
|
||||
|
||||
(when middle-click?
|
||||
(dom/prevent-default event)
|
||||
(dom/prevent-default native-event)
|
||||
|
||||
;; We store this so in Firefox the middle button won't do a paste of the content
|
||||
(mf/set-ref-val! disable-paste-ref true)
|
||||
@@ -381,7 +400,9 @@
|
||||
(let [last-position (mf/use-var nil)]
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [raw-pt (dom/get-client-position event)
|
||||
(let [native-event (unchecked-get event "nativeEvent")
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
raw-pt (dom/get-client-position event)
|
||||
pt (uwvv/point->viewport raw-pt)
|
||||
|
||||
;; We calculate the delta because Safari's MouseEvent.movementX/Y drop
|
||||
@@ -390,6 +411,12 @@
|
||||
(gpt/subtract raw-pt @last-position)
|
||||
(gpt/point 0 0))]
|
||||
|
||||
;; IMPORTANT! This function, right now it's called on EVERY pointermove. I think
|
||||
;; in the future (when we handle the UI in the render) should be better to
|
||||
;; have a "wasm.api/pointer-move" function that works as an entry point for
|
||||
;; all the pointer-move events.
|
||||
(wasm.api/text-editor-pointer-move (.-x off-pt) (.-y off-pt))
|
||||
|
||||
(rx/push! move-stream pt)
|
||||
(reset! last-position raw-pt)
|
||||
(st/emit! (mse/->PointerEvent :delta delta
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
(mf/defc object-svg
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [object-id embed skip-children]}]
|
||||
[{:keys [object-id embed skip-children wasm]}]
|
||||
(let [objects (mf/deref ref:objects)]
|
||||
|
||||
;; Set the globa CSS to assign the page size, needed for PDF
|
||||
@@ -77,27 +77,42 @@
|
||||
(mth/ceil height) "px")}))))
|
||||
|
||||
(when objects
|
||||
[:& (mf/provider ctx/is-render?) {:value true}
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]])))
|
||||
(if wasm
|
||||
[:& render/object-wasm
|
||||
{:objects objects
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]
|
||||
|
||||
(mf/defc objects-svg
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [object-ids embed skip-children]}]
|
||||
(when-let [objects (mf/deref ref:objects)]
|
||||
(for [object-id object-ids]
|
||||
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
||||
[:& (mf/provider ctx/is-render?) {:value true}
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:key (str object-id)
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]]))))
|
||||
|
||||
(mf/defc objects-svg
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [object-ids embed skip-children wasm]}]
|
||||
(when-let [objects (mf/deref ref:objects)]
|
||||
(for [object-id object-ids]
|
||||
(let [objects (render/adapt-objects-for-shape objects object-id)]
|
||||
(if wasm
|
||||
[:& render/object-wasm
|
||||
{:objects objects
|
||||
:key (str object-id)
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]
|
||||
|
||||
[:& (mf/provider ctx/is-render?) {:value true}
|
||||
[:& render/object-svg
|
||||
{:objects objects
|
||||
:key (str object-id)
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}]])))))
|
||||
|
||||
(defn- fetch-objects-bundle
|
||||
[& {:keys [file-id page-id share-id object-id] :as options}]
|
||||
(ptk/reify ::fetch-objects-bundle
|
||||
@@ -136,7 +151,7 @@
|
||||
(defn- render-objects
|
||||
[params]
|
||||
(try
|
||||
(let [{:keys [file-id page-id embed share-id object-id skip-children] :as params}
|
||||
(let [{:keys [file-id page-id embed share-id object-id skip-children wasm] :as params}
|
||||
(coerce-render-objects-params params)]
|
||||
(st/emit! (fetch-objects-bundle :file-id file-id :page-id page-id :share-id share-id :object-id object-id))
|
||||
(if (uuid? object-id)
|
||||
@@ -147,7 +162,8 @@
|
||||
:share-id share-id
|
||||
:object-id object-id
|
||||
:embed embed
|
||||
:skip-children skip-children}])
|
||||
:skip-children skip-children
|
||||
:wasm wasm}])
|
||||
|
||||
(mf/html
|
||||
[:& objects-svg
|
||||
@@ -156,7 +172,8 @@
|
||||
:share-id share-id
|
||||
:object-ids (into #{} object-id)
|
||||
:embed embed
|
||||
:skip-children skip-children}])))
|
||||
:skip-children skip-children
|
||||
:wasm wasm}])))
|
||||
(catch :default cause
|
||||
(when-let [explain (-> cause ex-data ::sm/explain)]
|
||||
(js/console.log "Unexpected error")
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.render :as render]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.shapes.text]
|
||||
[app.main.worker :as mw]
|
||||
@@ -87,7 +86,11 @@
|
||||
(def text-editor-start text-editor/text-editor-start)
|
||||
(def text-editor-stop text-editor/text-editor-stop)
|
||||
(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)
|
||||
(def text-editor-pointer-up text-editor/text-editor-pointer-up)
|
||||
(def text-editor-is-active? text-editor/text-editor-is-active?)
|
||||
(def text-editor-select-all text-editor/text-editor-select-all)
|
||||
(def text-editor-sync-content text-editor/text-editor-sync-content)
|
||||
|
||||
(def dpr
|
||||
@@ -96,6 +99,9 @@
|
||||
(def noop-fn
|
||||
(constantly nil))
|
||||
|
||||
;;
|
||||
(def shape-wrapper-factory nil)
|
||||
|
||||
(defn- yield-to-browser
|
||||
"Returns a promise that resolves after yielding to the browser's event loop.
|
||||
Uses requestAnimationFrame for smooth visual updates during loading."
|
||||
@@ -111,7 +117,7 @@
|
||||
(let [objects (mf/deref refs/workspace-page-objects)
|
||||
shape-wrapper
|
||||
(mf/with-memo [shape]
|
||||
(render/shape-wrapper-factory objects))]
|
||||
(shape-wrapper-factory objects))]
|
||||
|
||||
[:svg {:version "1.1"
|
||||
:xmlns "http://www.w3.org/2000/svg"
|
||||
@@ -263,22 +269,6 @@
|
||||
[attrs]
|
||||
(text-editor/apply-style-to-selection attrs use-shape set-shape-text-content))
|
||||
|
||||
(defn update-text-rect!
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)})))
|
||||
|
||||
(defn- ensure-text-content
|
||||
"Guarantee that the shape always sends a valid text tree to WASM. When the
|
||||
content is nil (freshly created text) we fall back to
|
||||
tc/default-text-content so the renderer receives typography information."
|
||||
[content]
|
||||
(or content (tc/v2-default-text-content)))
|
||||
|
||||
(defn set-parent-id
|
||||
[id]
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
@@ -996,65 +986,81 @@
|
||||
(render-finish)
|
||||
(perf/end-measure "set-view-box::zoom")))))
|
||||
|
||||
(defn update-text-rect!
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)})))
|
||||
|
||||
(defn- ensure-text-content
|
||||
"Guarantee that the shape always sends a valid text tree to WASM. When the
|
||||
content is nil (freshly created text) we fall back to
|
||||
tc/default-text-content so the renderer receives typography information."
|
||||
[content]
|
||||
(or content (tc/v2-default-text-content)))
|
||||
|
||||
(defn set-object
|
||||
[shape]
|
||||
(perf/begin-measure "set-object")
|
||||
(let [shape (svg-filters/apply-svg-derived shape)
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
(when shape
|
||||
(let [shape (svg-filters/apply-svg-derived shape)
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
|
||||
masked (get shape :masked-group)
|
||||
masked (get shape :masked-group)
|
||||
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group)
|
||||
[] (get shape :strokes))
|
||||
children (get shape :shapes)
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
content))
|
||||
bool-type (get shape :bool-type)
|
||||
grow-type (get shape :grow-type)
|
||||
blur (get shape :blur)
|
||||
svg-attrs (get shape :svg-attrs)
|
||||
shadows (get shape :shadow)]
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group)
|
||||
[] (get shape :strokes))
|
||||
children (get shape :shapes)
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
content))
|
||||
bool-type (get shape :bool-type)
|
||||
grow-type (get shape :grow-type)
|
||||
blur (get shape :blur)
|
||||
svg-attrs (get shape :svg-attrs)
|
||||
shadows (get shape :shadow)]
|
||||
|
||||
(shapes/set-shape-base-props shape)
|
||||
(shapes/set-shape-base-props shape)
|
||||
|
||||
;; Remaining properties that need separate calls (variable-length or conditional)
|
||||
(set-shape-children children)
|
||||
(set-shape-blur blur)
|
||||
(when (= type :group)
|
||||
(set-masked (boolean masked)))
|
||||
(when (= type :bool)
|
||||
(set-shape-bool-type bool-type))
|
||||
(when (and (some? content)
|
||||
(or (= type :path)
|
||||
(= type :bool)))
|
||||
(set-shape-path-content content))
|
||||
(when (some? svg-attrs)
|
||||
(set-shape-svg-attrs svg-attrs))
|
||||
(when (and (some? content) (= type :svg-raw))
|
||||
(set-shape-svg-raw-content (get-static-markup shape)))
|
||||
(set-shape-shadows shadows)
|
||||
(when (= type :text)
|
||||
(set-shape-grow-type grow-type))
|
||||
;; Remaining properties that need separate calls (variable-length or conditional)
|
||||
(set-shape-children children)
|
||||
(set-shape-blur blur)
|
||||
(when (= type :group)
|
||||
(set-masked (boolean masked)))
|
||||
(when (= type :bool)
|
||||
(set-shape-bool-type bool-type))
|
||||
(when (and (some? content)
|
||||
(or (= type :path)
|
||||
(= type :bool)))
|
||||
(set-shape-path-content content))
|
||||
(when (some? svg-attrs)
|
||||
(set-shape-svg-attrs svg-attrs))
|
||||
(when (and (some? content) (= type :svg-raw))
|
||||
(set-shape-svg-raw-content (get-static-markup shape)))
|
||||
(set-shape-shadows shadows)
|
||||
(when (= type :text)
|
||||
(set-shape-grow-type grow-type))
|
||||
|
||||
(set-shape-layout shape)
|
||||
(set-layout-data shape)
|
||||
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
(set-shape-text-content id content)
|
||||
(set-shape-text-images id content true)
|
||||
(set-shape-fills id fills true)
|
||||
(set-shape-strokes id strokes true)))
|
||||
pending_full (into [] (concat
|
||||
(set-shape-text-images id content false)
|
||||
(set-shape-fills id fills false)
|
||||
(set-shape-strokes id strokes false)))]
|
||||
(perf/end-measure "set-object")
|
||||
{:thumbnails pending_thumbnails
|
||||
:full pending_full})))
|
||||
(set-shape-layout shape)
|
||||
(set-layout-data shape)
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
(set-shape-text-content id content)
|
||||
(set-shape-text-images id content true)
|
||||
(set-shape-fills id fills true)
|
||||
(set-shape-strokes id strokes true)))
|
||||
pending_full (into [] (concat
|
||||
(set-shape-text-images id content false)
|
||||
(set-shape-fills id fills false)
|
||||
(set-shape-strokes id strokes false)))]
|
||||
(perf/end-measure "set-object")
|
||||
{:thumbnails pending_thumbnails
|
||||
:full pending_full}))))
|
||||
|
||||
(defn update-text-layouts
|
||||
[shapes]
|
||||
@@ -1652,6 +1658,33 @@
|
||||
(let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")]
|
||||
(run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur)))
|
||||
|
||||
(defn render-shape-pixels
|
||||
[shape-id scale]
|
||||
(let [buffer (uuid/get-u32 shape-id)
|
||||
|
||||
offset
|
||||
(h/call wasm/internal-module "_render_shape_pixels"
|
||||
(aget buffer 0)
|
||||
(aget buffer 1)
|
||||
(aget buffer 2)
|
||||
(aget buffer 3)
|
||||
scale)
|
||||
|
||||
offset-32
|
||||
(mem/->offset-32 offset)
|
||||
|
||||
heap (mem/get-heap-u8)
|
||||
heapu32 (mem/get-heap-u32)
|
||||
|
||||
length (aget heapu32 (mem/->offset-32 offset))
|
||||
width (aget heapu32 (+ (mem/->offset-32 offset) 1))
|
||||
height (aget heapu32 (+ (mem/->offset-32 offset) 2))
|
||||
|
||||
result (dr/read-image-bytes heap (+ offset 12) length)]
|
||||
|
||||
(mem/free)
|
||||
|
||||
result))
|
||||
|
||||
(defn init-wasm-module
|
||||
[module]
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
:center (gpt/point cx cy)
|
||||
:transform (gmt/matrix a b c d e f)}))
|
||||
|
||||
(defn read-image-bytes
|
||||
[heap offset length]
|
||||
(.slice ^js heap offset (+ offset length)))
|
||||
|
||||
(defn read-position-data-entry
|
||||
[heapu32 heapf32 offset]
|
||||
(let [paragraph (aget heapu32 (+ offset 0))
|
||||
|
||||
@@ -27,6 +27,21 @@
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
|
||||
|
||||
(defn text-editor-pointer-down
|
||||
[x y]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_pointer_down" x y)))
|
||||
|
||||
(defn text-editor-pointer-move
|
||||
[x y]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_pointer_move" x y)))
|
||||
|
||||
(defn text-editor-pointer-up
|
||||
[x y]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_pointer_up" x y)))
|
||||
|
||||
(defn text-editor-update-blink
|
||||
[timestamp-ms]
|
||||
(when wasm/context-initialized?
|
||||
@@ -83,9 +98,12 @@
|
||||
(h/call wasm/internal-module "_text_editor_stop")))
|
||||
|
||||
(defn text-editor-is-active?
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(not (zero? (h/call wasm/internal-module "_text_editor_is_active")))))
|
||||
([id]
|
||||
(when wasm/context-initialized?
|
||||
(not (zero? (h/call wasm/internal-module "_text_editor_is_active_with_id" id)))))
|
||||
([]
|
||||
(when wasm/context-initialized?
|
||||
(not (zero? (h/call wasm/internal-module "_text_editor_is_active"))))))
|
||||
|
||||
(defn text-editor-export-content
|
||||
[]
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
(ns debug
|
||||
(:require
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
@@ -458,7 +460,24 @@
|
||||
[]
|
||||
(.log js/console (clj->js @http/network-averages)))
|
||||
|
||||
|
||||
(defn print-last-exception
|
||||
[]
|
||||
(some-> errors/last-exception ex/print-throwable))
|
||||
|
||||
(defn ^:export export-image
|
||||
[]
|
||||
|
||||
(let [objects (dsh/lookup-page-objects @st/state)
|
||||
shape-id (->> (get-selected @st/state) first)
|
||||
bytes (wasm.api/render-shape-pixels shape-id 3.0)
|
||||
|
||||
blob (js/Blob. #js [bytes] #js {:type "image/png"})
|
||||
url (.createObjectURL js/URL blob)
|
||||
|
||||
a (.createElement js/document "a")]
|
||||
(set! (.-href a) url)
|
||||
(set! (.-download a) "export.png")
|
||||
(.click a)
|
||||
(.revokeObjectURL js/URL url)
|
||||
|
||||
nil))
|
||||
|
||||
@@ -76,3 +76,4 @@ export function getFills(fillStyle) {
|
||||
const [color, opacity] = getColor(fillStyle);
|
||||
return `[["^ ","~:fill-color","${color}","~:fill-opacity",${opacity}]]`;
|
||||
}
|
||||
|
||||
|
||||
@@ -162,12 +162,15 @@ class TextEditorPlayground {
|
||||
}
|
||||
|
||||
this.#module.call("use_shape", ...textShape.id);
|
||||
// FIXME: This function doesn't exists anymore.
|
||||
/*
|
||||
const caretPosition = this.#module.call(
|
||||
"get_caret_position_at",
|
||||
e.offsetX,
|
||||
e.offsetY,
|
||||
);
|
||||
console.log("caretPosition", caretPosition);
|
||||
*/
|
||||
};
|
||||
|
||||
#onResize = (_entries) => {
|
||||
|
||||
2
render-wasm/pnpm-workspace.yaml
Normal file
2
render-wasm/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@@ -742,6 +742,24 @@ pub extern "C" fn end_temp_objects() {
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_shape_pixels(a: u32, b: u32, c: u32, d: u32, scale: f32) -> *mut u8 {
|
||||
let id = uuid_from_u32_quartet(a, b, c, d);
|
||||
|
||||
with_state_mut!(state, {
|
||||
let (data, width, height) = state.render_shape_pixels(&id, scale, performance::get_time())
|
||||
.expect("Cannot render into texture");
|
||||
|
||||
let len = data.len() as u32;
|
||||
let mut buf = Vec::with_capacity(4 + data.len());
|
||||
buf.extend_from_slice(&len.to_le_bytes());
|
||||
buf.extend_from_slice(&width.to_le_bytes());
|
||||
buf.extend_from_slice(&height.to_le_bytes());
|
||||
buf.extend_from_slice(&data);
|
||||
mem::write_bytes(buf)
|
||||
})
|
||||
}
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
init_gl!();
|
||||
|
||||
@@ -18,6 +18,7 @@ use std::borrow::Cow;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use gpu_state::GpuState;
|
||||
|
||||
use options::RenderOptions;
|
||||
pub use surfaces::{SurfaceId, Surfaces};
|
||||
|
||||
@@ -43,6 +44,7 @@ const BLUR_DOWNSCALE_THRESHOLD: f32 = 8.0;
|
||||
|
||||
type ClipStack = Vec<(Rect, Option<Corners>, Matrix)>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NodeRenderState {
|
||||
pub id: Uuid,
|
||||
// We use this bool to keep that we've traversed all the children inside this node.
|
||||
@@ -537,7 +539,7 @@ impl RenderState {
|
||||
);
|
||||
}
|
||||
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
|
||||
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) {
|
||||
performance::begin_measure!("apply_drawing_to_render_canvas");
|
||||
|
||||
let paint = skia::Paint::default();
|
||||
@@ -545,12 +547,12 @@ impl RenderState {
|
||||
// Only draw surfaces that have content (dirty flag optimization)
|
||||
if self.surfaces.is_dirty(SurfaceId::TextDropShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
|
||||
.draw_into(SurfaceId::TextDropShadows, target, Some(&paint));
|
||||
}
|
||||
|
||||
if self.surfaces.is_dirty(SurfaceId::Fills) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
|
||||
.draw_into(SurfaceId::Fills, target, Some(&paint));
|
||||
}
|
||||
|
||||
let mut render_overlay_below_strokes = false;
|
||||
@@ -560,17 +562,17 @@ impl RenderState {
|
||||
|
||||
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
|
||||
}
|
||||
|
||||
if self.surfaces.is_dirty(SurfaceId::Strokes) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
|
||||
.draw_into(SurfaceId::Strokes, target, Some(&paint));
|
||||
}
|
||||
|
||||
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
|
||||
self.surfaces
|
||||
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
|
||||
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
|
||||
}
|
||||
|
||||
// Build mask of dirty surfaces that need clearing
|
||||
@@ -643,6 +645,7 @@ impl RenderState {
|
||||
offset: Option<(f32, f32)>,
|
||||
parent_shadows: Option<Vec<skia_safe::Paint>>,
|
||||
spread: Option<f32>,
|
||||
target_surface: SurfaceId,
|
||||
) {
|
||||
let surface_ids = fills_surface_id as u32
|
||||
| strokes_surface_id as u32
|
||||
@@ -694,7 +697,7 @@ impl RenderState {
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||
self.surfaces.apply_mut(target_surface as u32, |s| {
|
||||
let canvas = s.canvas();
|
||||
canvas.save();
|
||||
canvas.scale((scale, scale));
|
||||
@@ -706,7 +709,7 @@ impl RenderState {
|
||||
shape,
|
||||
&shape.fills,
|
||||
antialias,
|
||||
SurfaceId::Current,
|
||||
target_surface,
|
||||
None,
|
||||
);
|
||||
|
||||
@@ -716,12 +719,12 @@ impl RenderState {
|
||||
self,
|
||||
shape,
|
||||
&visible_strokes,
|
||||
Some(SurfaceId::Current),
|
||||
Some(target_surface),
|
||||
antialias,
|
||||
spread,
|
||||
);
|
||||
|
||||
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
|
||||
self.surfaces.apply_mut(target_surface as u32, |s| {
|
||||
s.canvas().restore();
|
||||
});
|
||||
|
||||
@@ -1134,7 +1137,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
if apply_to_current_surface {
|
||||
self.apply_drawing_to_render_canvas(Some(&shape));
|
||||
self.apply_drawing_to_render_canvas(Some(&shape), target_surface);
|
||||
}
|
||||
|
||||
// Only restore if we saved (optimization for simple shapes)
|
||||
@@ -1296,7 +1299,7 @@ impl RenderState {
|
||||
self.current_tile = None;
|
||||
self.render_in_progress = true;
|
||||
|
||||
self.apply_drawing_to_render_canvas(None);
|
||||
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
|
||||
|
||||
if sync_render {
|
||||
self.render_shape_tree_sync(base_object, tree, timestamp)?;
|
||||
@@ -1347,6 +1350,51 @@ impl RenderState {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(
|
||||
&mut self,
|
||||
id: &Uuid,
|
||||
tree: ShapesPoolRef,
|
||||
scale: f32,
|
||||
timestamp: i32,
|
||||
) -> Result<(Vec<u8>, i32, i32), String> {
|
||||
let target_surface = SurfaceId::Export;
|
||||
|
||||
self.surfaces
|
||||
.canvas(target_surface)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
|
||||
if tree.len() != 0 {
|
||||
let shape = tree.get(id).unwrap();
|
||||
let mut extrect = shape.extrect(tree, scale);
|
||||
let margins = self.surfaces.margins;
|
||||
extrect.offset((margins.width as f32 / scale, margins.height as f32 / scale));
|
||||
|
||||
self.surfaces.resize_export_surface(scale, extrect);
|
||||
self.surfaces.update_render_context(extrect, scale);
|
||||
|
||||
self.pending_nodes.push(NodeRenderState {
|
||||
id: *id,
|
||||
visited_children: false,
|
||||
clip_bounds: None,
|
||||
visited_mask: false,
|
||||
mask: false,
|
||||
});
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, false, true)?;
|
||||
}
|
||||
|
||||
self.surfaces.flush_and_submit(&mut self.gpu_state, target_surface);
|
||||
|
||||
let image = self.surfaces.snapshot(target_surface);
|
||||
let data = image.encode(
|
||||
&mut self.gpu_state.context,
|
||||
skia::EncodedImageFormat::PNG,
|
||||
100
|
||||
).expect("PNG encode failed");
|
||||
let skia::ISize { width, height } = image.dimensions();
|
||||
|
||||
Ok((data.as_bytes().to_vec(), width, height))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn should_stop_rendering(&self, iteration: i32, timestamp: i32) -> bool {
|
||||
iteration % NODE_BATCH_THRESHOLD == 0
|
||||
@@ -1354,7 +1402,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool) {
|
||||
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool, target_surface: SurfaceId) {
|
||||
// Masked groups needs two rendering passes, the first one rendering
|
||||
// the content and the second one rendering the mask so we need to do
|
||||
// an extra save_layer to keep all the masked group separate from
|
||||
@@ -1369,7 +1417,7 @@ impl RenderState {
|
||||
let paint = skia::Paint::default();
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.canvas(target_surface)
|
||||
.save_layer(&layer_rec);
|
||||
}
|
||||
}
|
||||
@@ -1386,7 +1434,7 @@ impl RenderState {
|
||||
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
|
||||
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.canvas(target_surface)
|
||||
.save_layer(&mask_rec);
|
||||
}
|
||||
|
||||
@@ -1415,7 +1463,7 @@ impl RenderState {
|
||||
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::Current)
|
||||
.canvas(target_surface)
|
||||
.save_layer(&layer_rec);
|
||||
}
|
||||
|
||||
@@ -1428,6 +1476,7 @@ impl RenderState {
|
||||
element: &Shape,
|
||||
visited_mask: bool,
|
||||
clip_bounds: Option<ClipStack>,
|
||||
target_surface: SurfaceId
|
||||
) {
|
||||
if visited_mask {
|
||||
// Because masked groups needs two rendering passes (first drawing
|
||||
@@ -1435,7 +1484,7 @@ impl RenderState {
|
||||
// extra restore.
|
||||
if let Type::Group(group) = element.shape_type {
|
||||
if group.masked {
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
self.surfaces.canvas(target_surface).restore();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1497,6 +1546,7 @@ impl RenderState {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
target_surface,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1505,7 +1555,7 @@ impl RenderState {
|
||||
let needs_layer = element.needs_layer();
|
||||
|
||||
if needs_layer {
|
||||
self.surfaces.canvas(SurfaceId::Current).restore();
|
||||
self.surfaces.canvas(target_surface).restore();
|
||||
}
|
||||
|
||||
self.focus_mode.exit(&element.id);
|
||||
@@ -1587,6 +1637,7 @@ impl RenderState {
|
||||
scale: f32,
|
||||
translation: (f32, f32),
|
||||
extra_layer_blur: Option<Blur>,
|
||||
target_surface: SurfaceId
|
||||
) {
|
||||
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
|
||||
transformed_shadow.to_mut().offset = (0.0, 0.0);
|
||||
@@ -1670,6 +1721,7 @@ impl RenderState {
|
||||
Some(shadow.offset),
|
||||
None,
|
||||
Some(shadow.spread),
|
||||
target_surface,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1716,6 +1768,7 @@ impl RenderState {
|
||||
Some(shadow.offset), // Offset is geometric
|
||||
None,
|
||||
Some(shadow.spread), // Spread is geometric
|
||||
target_surface,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1757,6 +1810,7 @@ impl RenderState {
|
||||
Some(shadow.offset), // Offset is geometric
|
||||
None,
|
||||
Some(shadow.spread), // Spread is geometric
|
||||
target_surface,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1811,6 +1865,7 @@ impl RenderState {
|
||||
scale: f32,
|
||||
translation: (f32, f32),
|
||||
node_render_state: &NodeRenderState,
|
||||
target_surface: SurfaceId
|
||||
) {
|
||||
let element_extrect = extrect.get_or_insert_with(|| element.extrect(tree, scale));
|
||||
let inherited_layer_blur = match element.shape_type {
|
||||
@@ -1833,6 +1888,7 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
None,
|
||||
target_surface,
|
||||
);
|
||||
|
||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||
@@ -1862,6 +1918,7 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
inherited_layer_blur,
|
||||
target_surface,
|
||||
);
|
||||
} else {
|
||||
let paint = skia::Paint::default();
|
||||
@@ -1898,6 +1955,7 @@ impl RenderState {
|
||||
None,
|
||||
Some(vec![new_shadow_paint.clone()]),
|
||||
None,
|
||||
target_surface,
|
||||
);
|
||||
});
|
||||
self.surfaces.canvas(SurfaceId::DropShadows).restore();
|
||||
@@ -1963,10 +2021,16 @@ impl RenderState {
|
||||
tree: ShapesPoolRef,
|
||||
timestamp: i32,
|
||||
allow_stop: bool,
|
||||
export: bool,
|
||||
) -> Result<(bool, bool), String> {
|
||||
let mut iteration = 0;
|
||||
let mut is_empty = true;
|
||||
|
||||
let mut target_surface = SurfaceId::Current;
|
||||
if export {
|
||||
target_surface = SurfaceId::Export;
|
||||
}
|
||||
|
||||
while let Some(node_render_state) = self.pending_nodes.pop() {
|
||||
let node_id = node_render_state.id;
|
||||
let visited_children = node_render_state.visited_children;
|
||||
@@ -1992,7 +2056,7 @@ impl RenderState {
|
||||
if visited_children {
|
||||
// Skip render_shape_exit for flattened containers
|
||||
if !element.can_flatten() {
|
||||
self.render_shape_exit(element, visited_mask, clip_bounds);
|
||||
self.render_shape_exit(element, visited_mask, clip_bounds, target_surface);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -2015,11 +2079,14 @@ impl RenderState {
|
||||
|
||||
let has_effects = transformed_element.has_effects_that_extend_bounds();
|
||||
|
||||
let is_visible = if is_container || has_effects {
|
||||
let is_visible = export || if is_container || has_effects {
|
||||
let element_extrect =
|
||||
extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale));
|
||||
element_extrect.intersects(self.render_area)
|
||||
&& !transformed_element.visually_insignificant(scale, tree)
|
||||
} else if !has_effects {
|
||||
// Simple shape: selrect check is sufficient, skip expensive extrect
|
||||
let selrect = transformed_element.selrect();
|
||||
selrect.intersects(self.render_area)
|
||||
} else {
|
||||
let selrect = transformed_element.selrect();
|
||||
selrect.intersects(self.render_area)
|
||||
@@ -2058,6 +2125,7 @@ impl RenderState {
|
||||
let translation = self
|
||||
.surfaces
|
||||
.get_render_context_translation(self.render_area, scale);
|
||||
|
||||
self.render_element_drop_shadows_and_composite(
|
||||
element,
|
||||
tree,
|
||||
@@ -2066,10 +2134,11 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
&node_render_state,
|
||||
target_surface,
|
||||
);
|
||||
}
|
||||
|
||||
self.render_shape_enter(element, mask);
|
||||
self.render_shape_enter(element, mask, target_surface);
|
||||
}
|
||||
|
||||
if !node_render_state.is_root() && self.focus_mode.is_active() {
|
||||
@@ -2096,6 +2165,7 @@ impl RenderState {
|
||||
scale,
|
||||
translation,
|
||||
&node_render_state,
|
||||
target_surface
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2110,13 +2180,14 @@ impl RenderState {
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
target_surface,
|
||||
);
|
||||
|
||||
self.surfaces
|
||||
.canvas(SurfaceId::DropShadows)
|
||||
.clear(skia::Color::TRANSPARENT);
|
||||
} else if visited_children {
|
||||
self.apply_drawing_to_render_canvas(Some(element));
|
||||
self.apply_drawing_to_render_canvas(Some(element), target_surface);
|
||||
}
|
||||
|
||||
// Skip nested state updates for flattened containers
|
||||
@@ -2246,7 +2317,7 @@ impl RenderState {
|
||||
let tile_is_visible = self.tile_viewbox.is_visible(¤t_tile);
|
||||
let can_stop = allow_stop && !tile_is_visible;
|
||||
let (is_empty, early_return) =
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop)?;
|
||||
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?;
|
||||
|
||||
if early_return {
|
||||
return Ok(());
|
||||
|
||||
@@ -104,4 +104,38 @@ impl GpuState {
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn create_surface_from_texture(
|
||||
&mut self,
|
||||
width: i32,
|
||||
height: i32,
|
||||
texture_id: u32
|
||||
) -> skia::Surface {
|
||||
let texture_info = TextureInfo {
|
||||
target: gl::TEXTURE_2D,
|
||||
id: texture_id,
|
||||
format: gl::RGBA8,
|
||||
protected: skia::gpu::Protected::No,
|
||||
};
|
||||
|
||||
let backend_texture = unsafe{
|
||||
gpu::backend_textures::make_gl(
|
||||
(width, height),
|
||||
gpu::Mipmapped::No,
|
||||
texture_info,
|
||||
String::from("export_texture"))
|
||||
};
|
||||
|
||||
gpu::surfaces::wrap_backend_texture(
|
||||
&mut self.context,
|
||||
&backend_texture,
|
||||
gpu::SurfaceOrigin::BottomLeft,
|
||||
None,
|
||||
skia::ColorType::RGBA8888,
|
||||
None,
|
||||
None,
|
||||
).unwrap()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -17,17 +17,18 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub enum SurfaceId {
|
||||
Target = 0b00_0000_0001,
|
||||
Filter = 0b00_0000_0010,
|
||||
Cache = 0b00_0000_0100,
|
||||
Current = 0b00_0000_1000,
|
||||
Fills = 0b00_0001_0000,
|
||||
Strokes = 0b00_0010_0000,
|
||||
DropShadows = 0b00_0100_0000,
|
||||
InnerShadows = 0b00_1000_0000,
|
||||
TextDropShadows = 0b01_0000_0000,
|
||||
UI = 0b10_0000_0000,
|
||||
Debug = 0b10_0000_0001,
|
||||
Target = 0b000_0000_0001,
|
||||
Filter = 0b000_0000_0010,
|
||||
Cache = 0b000_0000_0100,
|
||||
Current = 0b000_0000_1000,
|
||||
Fills = 0b000_0001_0000,
|
||||
Strokes = 0b000_0010_0000,
|
||||
DropShadows = 0b000_0100_0000,
|
||||
InnerShadows = 0b000_1000_0000,
|
||||
TextDropShadows = 0b001_0000_0000,
|
||||
Export = 0b010_0000_0000,
|
||||
UI = 0b100_0000_0000,
|
||||
Debug = 0b100_0000_0001,
|
||||
}
|
||||
|
||||
pub struct Surfaces {
|
||||
@@ -52,11 +53,15 @@ pub struct Surfaces {
|
||||
// for drawing debug info.
|
||||
debug: skia::Surface,
|
||||
// for drawing tiles.
|
||||
export: skia::Surface,
|
||||
|
||||
tiles: TileTextureCache,
|
||||
sampling_options: skia::SamplingOptions,
|
||||
margins: skia::ISize,
|
||||
pub margins: skia::ISize,
|
||||
// Tracks which surfaces have content (dirty flag bitmask)
|
||||
dirty_surfaces: u32,
|
||||
|
||||
extra_tile_dims: skia::ISize,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -77,6 +82,7 @@ impl Surfaces {
|
||||
let filter = gpu_state.create_surface_with_isize("filter".to_string(), extra_tile_dims);
|
||||
let cache = gpu_state.create_surface_with_dimensions("cache".to_string(), width, height);
|
||||
let current = gpu_state.create_surface_with_isize("current".to_string(), extra_tile_dims);
|
||||
|
||||
let drop_shadows =
|
||||
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
|
||||
let inner_shadows =
|
||||
@@ -87,10 +93,13 @@ impl Surfaces {
|
||||
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims);
|
||||
let shape_strokes =
|
||||
gpu_state.create_surface_with_isize("shape_strokes".to_string(), extra_tile_dims);
|
||||
let export =
|
||||
gpu_state.create_surface_with_isize("export".to_string(), extra_tile_dims);
|
||||
|
||||
let ui = gpu_state.create_surface_with_dimensions("ui".to_string(), width, height);
|
||||
let debug = gpu_state.create_surface_with_dimensions("debug".to_string(), width, height);
|
||||
|
||||
|
||||
let tiles = TileTextureCache::new();
|
||||
Surfaces {
|
||||
target,
|
||||
@@ -104,10 +113,12 @@ impl Surfaces {
|
||||
shape_strokes,
|
||||
ui,
|
||||
debug,
|
||||
export,
|
||||
tiles,
|
||||
sampling_options,
|
||||
margins,
|
||||
dirty_surfaces: 0,
|
||||
extra_tile_dims
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,6 +270,9 @@ impl Surfaces {
|
||||
if ids & SurfaceId::Debug as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Debug));
|
||||
}
|
||||
if ids & SurfaceId::Export as u32 != 0 {
|
||||
f(self.get_mut(SurfaceId::Export));
|
||||
}
|
||||
performance::begin_measure!("apply_mut::flags");
|
||||
}
|
||||
|
||||
@@ -284,6 +298,7 @@ impl Surfaces {
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32;
|
||||
|
||||
|
||||
// Clear surfaces before updating transformations to remove residual content
|
||||
self.apply_mut(surface_ids, |s| {
|
||||
s.canvas().clear(skia::Color::TRANSPARENT);
|
||||
@@ -305,7 +320,7 @@ impl Surfaces {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
||||
pub fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
|
||||
match id {
|
||||
SurfaceId::Target => &mut self.target,
|
||||
SurfaceId::Filter => &mut self.filter,
|
||||
@@ -318,6 +333,7 @@ impl Surfaces {
|
||||
SurfaceId::Strokes => &mut self.shape_strokes,
|
||||
SurfaceId::Debug => &mut self.debug,
|
||||
SurfaceId::UI => &mut self.ui,
|
||||
SurfaceId::Export => &mut self.export
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +350,7 @@ impl Surfaces {
|
||||
SurfaceId::Strokes => &self.shape_strokes,
|
||||
SurfaceId::Debug => &self.debug,
|
||||
SurfaceId::UI => &self.ui,
|
||||
SurfaceId::Export => &self.export
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,12 +437,14 @@ impl Surfaces {
|
||||
self.canvas(SurfaceId::TextDropShadows).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Strokes).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Current).restore_to_count(1);
|
||||
self.canvas(SurfaceId::Export).restore_to_count(1);
|
||||
self.apply_mut(
|
||||
SurfaceId::Fills as u32
|
||||
| SurfaceId::Strokes as u32
|
||||
| SurfaceId::Current as u32
|
||||
| SurfaceId::InnerShadows as u32
|
||||
| SurfaceId::TextDropShadows as u32,
|
||||
| SurfaceId::TextDropShadows as u32
|
||||
| SurfaceId::Export as u32,
|
||||
|s| {
|
||||
s.canvas().clear(color).reset_matrix();
|
||||
},
|
||||
@@ -548,6 +567,34 @@ impl Surfaces {
|
||||
pub fn gc(&mut self) {
|
||||
self.tiles.gc();
|
||||
}
|
||||
|
||||
pub fn resize_export_surface(&mut self, scale: f32, rect: skia::Rect) {
|
||||
let target_w = (scale * rect.width()).ceil() as i32;
|
||||
let target_h = (scale * rect.height()).ceil() as i32;
|
||||
|
||||
let max_w = i32::max(self.extra_tile_dims.width, target_w);
|
||||
let max_h = i32::max(self.extra_tile_dims.height, target_h);
|
||||
|
||||
println!(">> current {:?}, now: {} {}", self.extra_tile_dims, max_w, max_h);
|
||||
|
||||
if max_w > self.extra_tile_dims.width || max_h > self.extra_tile_dims.height {
|
||||
println!(">RESIZE SURFACES");
|
||||
self.drop_shadows =
|
||||
self.drop_shadows.new_surface_with_dimensions((max_w, max_h)).unwrap();
|
||||
self.inner_shadows =
|
||||
self.inner_shadows.new_surface_with_dimensions((max_w, max_h)).unwrap();
|
||||
self.text_drop_shadows =
|
||||
self.text_drop_shadows.new_surface_with_dimensions((max_w, max_h)).unwrap();
|
||||
self.text_drop_shadows =
|
||||
self.text_drop_shadows.new_surface_with_dimensions((max_w, max_h)).unwrap();
|
||||
self.shape_strokes =
|
||||
self.shape_strokes.new_surface_with_dimensions((max_w, max_h)).unwrap();
|
||||
|
||||
self.extra_tile_dims = skia::ISize::new(max_w, max_h);
|
||||
}
|
||||
|
||||
self.export = self.export.new_surface_with_dimensions((target_w, target_h)).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TileTextureCache {
|
||||
@@ -625,5 +672,5 @@ impl TileTextureCache {
|
||||
for k in self.grid.keys() {
|
||||
self.removed.insert(*k);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,11 @@ fn render_cursor(
|
||||
paint.set_color(editor_state.theme.cursor_color);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
let shape_matrix = shape.get_matrix();
|
||||
canvas.save();
|
||||
canvas.concat(&shape_matrix);
|
||||
canvas.draw_rect(rect, &paint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn render_selection(
|
||||
@@ -65,9 +69,14 @@ fn render_selection(
|
||||
paint.set_blend_mode(BlendMode::Multiply);
|
||||
paint.set_color(editor_state.theme.selection_color);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
let shape_matrix = shape.get_matrix();
|
||||
canvas.save();
|
||||
canvas.concat(&shape_matrix);
|
||||
for rect in rects {
|
||||
canvas.draw_rect(rect, &paint);
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn vertical_align_offset(
|
||||
@@ -99,8 +108,6 @@ fn calculate_cursor_rect(
|
||||
return None;
|
||||
}
|
||||
|
||||
let selrect = shape.selrect();
|
||||
|
||||
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||
if idx == cursor.paragraph {
|
||||
@@ -157,8 +164,8 @@ fn calculate_cursor_rect(
|
||||
};
|
||||
|
||||
return Some(Rect::from_xywh(
|
||||
selrect.x() + cursor_x,
|
||||
selrect.y() + y_offset,
|
||||
cursor_x,
|
||||
y_offset,
|
||||
editor_state.theme.cursor_width,
|
||||
cursor_height,
|
||||
));
|
||||
@@ -182,7 +189,6 @@ fn calculate_selection_rects(
|
||||
let paragraphs = text_content.paragraphs();
|
||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
||||
|
||||
let selrect = shape.selrect();
|
||||
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
||||
|
||||
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||
@@ -225,8 +231,8 @@ fn calculate_selection_rects(
|
||||
for text_box in text_boxes {
|
||||
let r = text_box.rect;
|
||||
rects.push(Rect::from_xywh(
|
||||
selrect.x() + r.left(),
|
||||
selrect.y() + y_offset + r.top(),
|
||||
r.left(),
|
||||
y_offset + r.top(),
|
||||
r.width(),
|
||||
r.height(),
|
||||
));
|
||||
|
||||
@@ -258,6 +258,18 @@ pub fn all_with_ancestors(
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
pub fn get_relative_point(
|
||||
point: &Point,
|
||||
view_matrix: &Matrix,
|
||||
shape_matrix: &Matrix,
|
||||
) -> Option<Point> {
|
||||
let inv_view_matrix = view_matrix.invert()?;
|
||||
let inv_shape_matrix = shape_matrix.invert()?;
|
||||
let transform_matrix: Matrix = Matrix::concat(&inv_shape_matrix, &inv_view_matrix);
|
||||
let shape_relative_point = transform_matrix.map_point(*point);
|
||||
Some(shape_relative_point)
|
||||
}
|
||||
|
||||
pub fn new(id: Uuid) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
||||
@@ -112,12 +112,15 @@ impl TextContentSize {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -126,12 +129,14 @@ impl TextPositionWithAffinity {
|
||||
position_with_affinity: PositionWithAffinity,
|
||||
paragraph: i32,
|
||||
span: i32,
|
||||
span_relative_offset: i32,
|
||||
offset: i32,
|
||||
) -> Self {
|
||||
Self {
|
||||
position_with_affinity,
|
||||
paragraph,
|
||||
span,
|
||||
span_relative_offset,
|
||||
offset,
|
||||
}
|
||||
}
|
||||
@@ -421,7 +426,10 @@ impl TextContent {
|
||||
self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y);
|
||||
}
|
||||
|
||||
pub fn get_caret_position_at(&self, point: &Point) -> Option<TextPositionWithAffinity> {
|
||||
pub fn get_caret_position_from_shape_coords(
|
||||
&self,
|
||||
point: &Point,
|
||||
) -> Option<TextPositionWithAffinity> {
|
||||
let mut offset_y = 0.0;
|
||||
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
||||
|
||||
@@ -487,6 +495,7 @@ impl TextContent {
|
||||
paragraph_index,
|
||||
span_index,
|
||||
span_offset,
|
||||
position_with_affinity.position,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -509,12 +518,23 @@ impl TextContent {
|
||||
0, // paragraph 0
|
||||
0, // span 0
|
||||
0, // offset 0
|
||||
0,
|
||||
));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_caret_position_from_screen_coords(
|
||||
&self,
|
||||
point: &Point,
|
||||
view_matrix: &Matrix,
|
||||
shape_matrix: &Matrix,
|
||||
) -> Option<TextPositionWithAffinity> {
|
||||
let shape_rel_point = Shape::get_relative_point(point, view_matrix, shape_matrix)?;
|
||||
self.get_caret_position_from_shape_coords(&shape_rel_point)
|
||||
}
|
||||
|
||||
/// Builds the ParagraphBuilders necessary to render
|
||||
/// this text.
|
||||
pub fn paragraph_builder_group_from_text(
|
||||
|
||||
@@ -99,6 +99,11 @@ impl State {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn render_shape_pixels(&mut self, id: &Uuid, scale: f32, timestamp: i32) -> Result<(Vec<u8>, i32, i32), String> {
|
||||
self.render_state
|
||||
.render_shape_pixels(id, &self.shapes, scale, timestamp)
|
||||
}
|
||||
|
||||
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
|
||||
// If zoom changed, we MUST rebuild the tile index before using it.
|
||||
// Otherwise, the index will have tiles from the old zoom level, causing visible
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::shapes::TextPositionWithAffinity;
|
||||
use crate::shapes::{TextContent, TextPositionWithAffinity};
|
||||
use crate::uuid::Uuid;
|
||||
use skia_safe::Color;
|
||||
use skia_safe::{
|
||||
textlayout::{Affinity, PositionWithAffinity},
|
||||
Color,
|
||||
};
|
||||
|
||||
/// Cursor position within text content.
|
||||
/// Uses character offsets for precise positioning.
|
||||
@@ -122,6 +125,9 @@ pub struct TextEditorState {
|
||||
pub theme: TextEditorTheme,
|
||||
pub selection: TextSelection,
|
||||
pub is_active: bool,
|
||||
// This property indicates that we've started
|
||||
// selecting something with the pointer.
|
||||
pub is_pointer_selection_active: bool,
|
||||
pub active_shape_id: Option<Uuid>,
|
||||
pub cursor_visible: bool,
|
||||
pub last_blink_time: f64,
|
||||
@@ -138,6 +144,7 @@ impl TextEditorState {
|
||||
},
|
||||
selection: TextSelection::new(),
|
||||
is_active: false,
|
||||
is_pointer_selection_active: false,
|
||||
active_shape_id: None,
|
||||
cursor_visible: true,
|
||||
last_blink_time: 0.0,
|
||||
@@ -151,6 +158,7 @@ impl TextEditorState {
|
||||
self.cursor_visible = true;
|
||||
self.last_blink_time = 0.0;
|
||||
self.selection = TextSelection::new();
|
||||
self.is_pointer_selection_active = false;
|
||||
self.pending_events.clear();
|
||||
}
|
||||
|
||||
@@ -158,7 +166,65 @@ impl TextEditorState {
|
||||
self.is_active = false;
|
||||
self.active_shape_id = None;
|
||||
self.cursor_visible = false;
|
||||
self.is_pointer_selection_active = false;
|
||||
self.pending_events.clear();
|
||||
self.reset_blink();
|
||||
}
|
||||
|
||||
pub fn start_pointer_selection(&mut self) -> bool {
|
||||
if self.is_pointer_selection_active {
|
||||
return false;
|
||||
}
|
||||
self.is_pointer_selection_active = true;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn stop_pointer_selection(&mut self) -> bool {
|
||||
if !self.is_pointer_selection_active {
|
||||
return false;
|
||||
}
|
||||
self.is_pointer_selection_active = false;
|
||||
true
|
||||
}
|
||||
|
||||
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;
|
||||
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 {
|
||||
return false;
|
||||
};
|
||||
let mut offset = 0;
|
||||
for span in last_paragraph.children() {
|
||||
offset += span.text.len();
|
||||
}
|
||||
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,
|
||||
));
|
||||
self.reset_blink();
|
||||
self.push_event(crate::state::EditorEvent::SelectionChanged);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_caret_from_position(&mut self, position: TextPositionWithAffinity) {
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use macros::ToJs;
|
||||
|
||||
use super::{fills::RawFillData, fonts::RawFontStyle};
|
||||
use crate::math::{Matrix, Point};
|
||||
|
||||
use crate::mem::{self, SerializableResult};
|
||||
use crate::shapes::{
|
||||
self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
|
||||
};
|
||||
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
||||
use crate::{
|
||||
with_current_shape, with_current_shape_mut, with_state, with_state_mut,
|
||||
with_state_mut_current_shape, STATE,
|
||||
};
|
||||
use crate::{with_current_shape, with_current_shape_mut, with_state, with_state_mut, STATE};
|
||||
|
||||
const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
|
||||
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
||||
@@ -388,32 +385,6 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 {
|
||||
with_state_mut_current_shape!(state, |shape: &Shape| {
|
||||
if let Type::Text(text_content) = &shape.shape_type {
|
||||
let mut matrix = Matrix::new_identity();
|
||||
let shape_matrix = shape.get_concatenated_matrix(&state.shapes);
|
||||
let view_matrix = state.render_state.viewbox.get_matrix();
|
||||
if let Some(inv_view_matrix) = view_matrix.invert() {
|
||||
matrix.post_concat(&inv_view_matrix);
|
||||
matrix.post_concat(&shape_matrix);
|
||||
|
||||
let mapped_point = matrix.map_point(Point::new(x, y));
|
||||
|
||||
if let Some(position_with_affinity) =
|
||||
text_content.get_caret_position_at(&mapped_point)
|
||||
{
|
||||
return position_with_affinity.position_with_affinity.position;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Trying to get caret position of a shape that it's not a text shape");
|
||||
}
|
||||
});
|
||||
-1
|
||||
}
|
||||
|
||||
const RAW_POSITION_DATA_SIZE: usize = size_of::<shapes::PositionData>();
|
||||
|
||||
impl From<[u8; RAW_POSITION_DATA_SIZE]> for shapes::PositionData {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
use macros::ToJs;
|
||||
|
||||
use crate::math::{Matrix, Point, Rect};
|
||||
use crate::mem;
|
||||
use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign};
|
||||
@@ -7,6 +5,7 @@ use crate::state::{TextCursor, 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)]
|
||||
@@ -54,6 +53,17 @@ pub extern "C" fn text_editor_is_active() -> bool {
|
||||
with_state!(state, { state.text_editor_state.is_active })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_is_active_with_id(a: u32, b: u32, c: u32, d: u32) -> bool {
|
||||
with_state!(state, {
|
||||
let shape_id = uuid_from_u32_quartet(a, b, c, d);
|
||||
let Some(active_shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return false;
|
||||
};
|
||||
state.text_editor_state.is_active && active_shape_id == shape_id
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) {
|
||||
with_state!(state, {
|
||||
@@ -70,45 +80,25 @@ pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_select_all() {
|
||||
pub extern "C" fn text_editor_select_all() -> bool {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
return false;
|
||||
};
|
||||
|
||||
let paragraphs = text_content.paragraphs();
|
||||
if paragraphs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let last_para_idx = paragraphs.len() - 1;
|
||||
let last_para = ¶graphs[last_para_idx];
|
||||
let total_chars: usize = last_para
|
||||
.children()
|
||||
.iter()
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum();
|
||||
|
||||
use crate::state::TextCursor;
|
||||
state.text_editor_state.selection.anchor = TextCursor::new(0, 0);
|
||||
state.text_editor_state.selection.focus = TextCursor::new(last_para_idx, total_chars);
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::SelectionChanged);
|
||||
});
|
||||
state.text_editor_state.select_all(text_content)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -121,146 +111,127 @@ pub extern "C" fn text_editor_poll_event() -> u8 {
|
||||
// ============================================================================
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (shape_matrix, view_matrix, selrect, vertical_align) = {
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
(
|
||||
shape.get_concatenated_matrix(&state.shapes),
|
||||
state.render_state.viewbox.get_matrix(),
|
||||
shape.selrect(),
|
||||
shape.vertical_align(),
|
||||
)
|
||||
};
|
||||
|
||||
let Some(inv_view_matrix) = view_matrix.invert() else {
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(inv_shape_matrix) = shape_matrix.invert() else {
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut matrix = Matrix::new_identity();
|
||||
matrix.post_concat(&inv_view_matrix);
|
||||
matrix.post_concat(&inv_shape_matrix);
|
||||
|
||||
let mapped_point = matrix.map_point(Point::new(x, y));
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
if text_content.layout.paragraphs.is_empty() && !text_content.paragraphs().is_empty() {
|
||||
let bounds = text_content.bounds;
|
||||
text_content.update_layout(bounds);
|
||||
}
|
||||
|
||||
// Calculate vertical alignment offset (same as in render/text_editor.rs)
|
||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
||||
let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum();
|
||||
let vertical_offset = match vertical_align {
|
||||
crate::shapes::VerticalAlign::Center => (selrect.height() - total_height) / 2.0,
|
||||
crate::shapes::VerticalAlign::Bottom => selrect.height() - total_height,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Adjust point: subtract selrect offset and vertical alignment
|
||||
// The text layout expects coordinates where (0, 0) is the top-left of the text content
|
||||
let adjusted_point = Point::new(
|
||||
mapped_point.x - selrect.x(),
|
||||
mapped_point.y - selrect.y() - vertical_offset,
|
||||
);
|
||||
|
||||
if let Some(position) = text_content.get_caret_position_at(&adjusted_point) {
|
||||
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_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state.text_editor_state.set_caret_from_position(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_extend_selection_to_point(x: f32, y: f32) {
|
||||
pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
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;
|
||||
};
|
||||
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;
|
||||
}
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
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;
|
||||
};
|
||||
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;
|
||||
}
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(position);
|
||||
}
|
||||
state.text_editor_state.stop_pointer_selection();
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
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;
|
||||
};
|
||||
|
||||
let (shape_matrix, view_matrix, selrect, vertical_align) = {
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
(
|
||||
shape.get_concatenated_matrix(&state.shapes),
|
||||
state.render_state.viewbox.get_matrix(),
|
||||
shape.selrect(),
|
||||
shape.vertical_align(),
|
||||
)
|
||||
};
|
||||
|
||||
let Some(inv_view_matrix) = view_matrix.invert() else {
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(inv_shape_matrix) = shape_matrix.invert() else {
|
||||
let shape_matrix = shape.get_matrix();
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut matrix = Matrix::new_identity();
|
||||
matrix.post_concat(&inv_view_matrix);
|
||||
matrix.post_concat(&inv_shape_matrix);
|
||||
|
||||
let mapped_point = matrix.map_point(Point::new(x, y));
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
if text_content.layout.paragraphs.is_empty() && !text_content.paragraphs().is_empty() {
|
||||
let bounds = text_content.bounds;
|
||||
text_content.update_layout(bounds);
|
||||
}
|
||||
|
||||
// Calculate vertical alignment offset (same as in render/text_editor.rs)
|
||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
||||
let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum();
|
||||
let vertical_offset = match vertical_align {
|
||||
crate::shapes::VerticalAlign::Center => (selrect.height() - total_height) / 2.0,
|
||||
crate::shapes::VerticalAlign::Bottom => selrect.height() - total_height,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Adjust point: subtract selrect offset and vertical alignment
|
||||
let adjusted_point = Point::new(
|
||||
mapped_point.x - selrect.x(),
|
||||
mapped_point.y - selrect.y() - vertical_offset,
|
||||
);
|
||||
|
||||
if let Some(position) = text_content.get_caret_position_at(&adjusted_point) {
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(position);
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user