Compare commits

..

7 Commits

Author SHA1 Message Date
Aitor Moreno
55539e83bd WIP 2026-02-27 13:58:44 +01:00
Aitor Moreno
740e790585 🎉 Add active-features? helper function (#8490) 2026-02-27 12:12:27 +01:00
Elena Torró
ed23c55550 Merge pull request #8483 from penpot/superalex-fix-opacity-for-dotted-strokes
🐛 Fix opacity for dotted strokes
2026-02-26 13:41:43 +01:00
Alejandro Alonso
5b5c868a87 🐛 Fix opacity for dotted strokes 2026-02-26 13:31:12 +01:00
Alejandro Alonso
1a3ac6bdf8 Merge pull request #8475 from penpot/elenatorro-13524-fix-token-highlight
🐛 Fix rotation token highlight and its application on the text-ed…
2026-02-26 13:00:45 +01:00
Elena Torro
2bd7c10e09 🔧 Fix variable name from wrong merge 2026-02-26 12:19:20 +01:00
Elena Torro
495371c079 🐛 Fix rotation token highlight and its application on the text-editor-v2 2026-02-26 11:57:11 +01:00
26 changed files with 638 additions and 523 deletions

View File

@@ -58,8 +58,7 @@
:share-id share-id
:object-id (mapv :id objects)
:route "objects"
:skip-children skip-children
:wasm "true"}
:skip-children skip-children}
uri (-> (cf/get :public-uri)
(assoc :path "/render.html")
(assoc :query (u/map->query-string params)))]

View File

@@ -620,61 +620,68 @@
ptk/WatchEvent
(watch [_ state _]
;; We do not allow to apply tokens while text editor is open.
(when (empty? (get state :workspace-editor-state))
(let [attributes-to-remove
;; Remove atomic typography tokens when applying composite and vice-verca
(cond
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
:else attributes-to-remove)]
(when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (if (contains? cf/flags :tokenscript)
(rx/of (ts/resolve-tokens tokens))
(sd/resolve-tokens tokens))
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
objects (dsh/lookup-page-objects state)
selected-shapes (select-keys objects shape-ids)
;; The classic text editor sets :workspace-editor-state; the WASM text editor
;; does not, so we also check :workspace-local :edition for text shapes.
(let [edition (get-in state [:workspace-local :edition])
objects (dsh/lookup-page-objects state)
text-editing? (and (some? edition)
(= :text (:type (get objects edition))))]
(when (and (empty? (get state :workspace-editor-state))
(not text-editing?))
(let [attributes-to-remove
;; Remove atomic typography tokens when applying composite and vice-verca
(cond
(ctt/typography-token-keys (:type token)) (set/union attributes-to-remove ctt/typography-keys)
(ctt/typography-keys (:type token)) (set/union attributes-to-remove ctt/typography-token-keys)
:else attributes-to-remove)]
(when-let [tokens (some-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (if (contains? cf/flags :tokenscript)
(rx/of (ts/resolve-tokens tokens))
(sd/resolve-tokens tokens))
(rx/mapcat
(fn [resolved-tokens]
(let [undo-id (js/Symbol)
objects (dsh/lookup-page-objects state)
selected-shapes (select-keys objects shape-ids)
shapes (->> selected-shapes
(filter (fn [[_ shape]]
(or
(and (ctsl/any-layout-immediate-child? objects shape)
(some ctt/spacing-margin-keys attributes))
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
(all-attrs-appliable-for-token? attributes (:type token)))))))
shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
shapes (->> selected-shapes
(filter (fn [[_ shape]]
(or
(and (ctsl/any-layout-immediate-child? objects shape)
(some ctt/spacing-margin-keys attributes))
(and (ctt/any-appliable-attr-for-shape? attributes (:type shape) (:layout shape))
(all-attrs-appliable-for-token? attributes (:type token)))))))
shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)
tokenized-attributes (cfo/attributes-map attributes token)
type (:type token)]
(rx/concat
(rx/of
(st/emit! (ev/event {::ev/name "apply-tokens"
:type type
:applied-to attributes
:applied-to-variant any-variant?}))
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always
(update :applied-tokens merge tokenized-attributes)))))
(when on-update-shape
(let [res (on-update-shape resolved-value shape-ids attributes)]
;; Composed updates return observables and need to be executed differently
(if (rx/observable? res)
res
(rx/of res))))
(rx/of (dwu/commit-undo-transaction undo-id)))))))))))))
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
resolved-value (if (contains? cf/flags :tokenscript)
(ts/tokenscript-symbols->penpot-unit resolved-value)
resolved-value)
tokenized-attributes (cfo/attributes-map attributes token)
type (:type token)]
(rx/concat
(rx/of
(st/emit! (ev/event {::ev/name "apply-tokens"
:type type
:applied-to attributes
:applied-to-variant any-variant?}))
(dwu/start-undo-transaction undo-id)
(dwsh/update-shapes shape-ids (fn [shape]
(cond-> shape
attributes-to-remove
(update :applied-tokens #(apply (partial dissoc %) attributes-to-remove))
:always
(update :applied-tokens merge tokenized-attributes)))))
(when on-update-shape
(let [res (on-update-shape resolved-value shape-ids attributes)]
;; Composed updates return observables and need to be executed differently
(if (rx/observable? res)
res
(rx/of res))))
(rx/of (dwu/commit-undo-transaction undo-id))))))))))))))
(defn apply-spacing-token-separated
"Handles edge-case for spacing token when applying token via toggle button.

View File

@@ -548,7 +548,7 @@
modif-tree
(dwm/build-modif-tree ids objects get-modifier)]
(rx/of (dwm/apply-wasm-modifiers modif-tree)))
(rx/of (dwm/apply-wasm-modifiers modif-tree :ignore-touched (:ignore-touched options))))
(let [page-id (or (:page-id options)
(:current-page-id state))

View File

@@ -86,6 +86,24 @@
: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))

View File

@@ -45,9 +45,7 @@
[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]
@@ -55,7 +53,6 @@
[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)
@@ -174,8 +171,6 @@
;; 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}}]
@@ -485,48 +480,6 @@
[:& 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)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -131,7 +131,8 @@
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

View File

@@ -0,0 +1,338 @@
;; 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"}]]]]))

View File

@@ -0,0 +1,12 @@
.text-editor {
height: 100%;
}
.text-editor-container {
height: 100%;
position: absolute;
opacity: 0;
overflow: hidden;
white-space: pre;
}

View File

@@ -89,7 +89,7 @@
:on-context-menu on-context-menu
:data-testid "layer-row"
:role "checkbox"
:aria-checked selected?
:aria-checked is-selected
:class (stl/css-case
:layer-row true
:highlight is-highlighted

View File

@@ -75,7 +75,11 @@
is-type-unfolded (contains? (set unfolded-token-paths) (name type))
editing-ref (mf/deref refs/workspace-editor-state)
not-editing? (empty? editing-ref)
edition (mf/deref refs/selected-edition)
objects (mf/deref refs/workspace-page-objects)
not-editing? (and (empty? editing-ref)
(not (and (some? edition)
(= :text (:type (get objects edition))))))
can-edit?
(mf/use-ctx ctx/can-edit?)

View File

@@ -94,23 +94,8 @@
(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)))
(st/emit! (dw/clear-edition-mode)))
(when (and (not text-editing?)
(not blocked)

View File

@@ -30,6 +30,7 @@
[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]
@@ -54,7 +55,6 @@
[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,10 +328,14 @@
(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)]
@@ -417,14 +421,7 @@
(when picking-color?
[:> pixel-overlay/pixel-overlay-wasm* {:viewport-ref viewport-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-ref canvas-ref}])]
[:canvas {:id "render"
:data-testid "canvas-wasm-shapes"
@@ -471,14 +468,20 @@
[: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 (and show-text-editor?
(not (features/active-feature? @st/state "text-editor-wasm/v1")))
(if (features/active-feature? @st/state "text-editor/v2")
(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")
[:& editor-v2/text-editor {:shape editing-shape
:canvas-ref canvas-ref
:ref text-editor-ref}]
[:& editor-v1/text-editor-svg {:shape editing-shape
:ref text-editor-ref}]))
:else [:& editor-v1/text-editor-svg {:shape editing-shape
:ref text-editor-ref}]))
(when show-frame-outline?
(let [outlined-frame-id

View File

@@ -63,7 +63,7 @@
(mf/defc object-svg
{::mf/wrap-props false}
[{:keys [object-id embed skip-children wasm]}]
[{:keys [object-id embed skip-children]}]
(let [objects (mf/deref ref:objects)]
;; Set the globa CSS to assign the page size, needed for PDF
@@ -77,41 +77,26 @@
(mth/ceil height) "px")}))))
(when objects
(if wasm
[:& render/object-wasm
{:objects objects
:object-id object-id
:embed embed
:skip-children skip-children}]
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:object-id object-id
:embed embed
:skip-children skip-children}]]))))
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{: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 wasm]}]
[{: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)]
(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}]])))))
[:& (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}]
@@ -151,7 +136,7 @@
(defn- render-objects
[params]
(try
(let [{:keys [file-id page-id embed share-id object-id skip-children wasm] :as params}
(let [{:keys [file-id page-id embed share-id object-id skip-children] :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)
@@ -162,8 +147,7 @@
:share-id share-id
:object-id object-id
:embed embed
:skip-children skip-children
:wasm wasm}])
:skip-children skip-children}])
(mf/html
[:& objects-svg
@@ -172,8 +156,7 @@
:share-id share-id
:object-ids (into #{} object-id)
:embed embed
:skip-children skip-children
:wasm wasm}])))
:skip-children skip-children}])))
(catch :default cause
(when-let [explain (-> cause ex-data ::sm/explain)]
(js/console.log "Unexpected error")

View File

@@ -23,6 +23,7 @@
[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]
@@ -85,6 +86,7 @@
;; 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)
@@ -99,9 +101,6 @@
(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."
@@ -117,7 +116,7 @@
(let [objects (mf/deref refs/workspace-page-objects)
shape-wrapper
(mf/with-memo [shape]
(shape-wrapper-factory objects))]
(render/shape-wrapper-factory objects))]
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
@@ -1005,62 +1004,62 @@
(defn set-object
[shape]
(perf/begin-measure "set-object")
(when shape
(let [shape (svg-filters/apply-svg-derived shape)
id (dm/get-prop shape :id)
type (dm/get-prop shape :type)
(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]
@@ -1658,33 +1657,6 @@
(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]

View File

@@ -45,10 +45,6 @@
: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))

View File

@@ -22,7 +22,14 @@
(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)))

View File

@@ -203,6 +203,7 @@
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)]
@@ -214,7 +215,17 @@
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-input"))
(when-let [node (mf/ref-val contenteditable-ref)]
(set! (.-textContent node) ""))))))]
(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)))]
[:div
{:ref contenteditable-ref
@@ -225,6 +236,8 @@
: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"

View File

@@ -6,8 +6,6 @@
(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]
@@ -460,24 +458,7 @@
[]
(.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 1.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))

View File

@@ -742,24 +742,6 @@ 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!();

View File

@@ -18,7 +18,6 @@ use std::borrow::Cow;
use std::collections::HashSet;
use gpu_state::GpuState;
use options::RenderOptions;
pub use surfaces::{SurfaceId, Surfaces};
@@ -44,7 +43,6 @@ 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.
@@ -539,7 +537,7 @@ impl RenderState {
);
}
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>, target: SurfaceId) {
pub fn apply_drawing_to_render_canvas(&mut self, shape: Option<&Shape>) {
performance::begin_measure!("apply_drawing_to_render_canvas");
let paint = skia::Paint::default();
@@ -547,12 +545,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, target, Some(&paint));
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
}
if self.surfaces.is_dirty(SurfaceId::Fills) {
self.surfaces
.draw_into(SurfaceId::Fills, target, Some(&paint));
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
}
let mut render_overlay_below_strokes = false;
@@ -562,17 +560,17 @@ impl RenderState {
if render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
self.surfaces
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
}
if self.surfaces.is_dirty(SurfaceId::Strokes) {
self.surfaces
.draw_into(SurfaceId::Strokes, target, Some(&paint));
.draw_into(SurfaceId::Strokes, SurfaceId::Current, Some(&paint));
}
if !render_overlay_below_strokes && self.surfaces.is_dirty(SurfaceId::InnerShadows) {
self.surfaces
.draw_into(SurfaceId::InnerShadows, target, Some(&paint));
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
}
// Build mask of dirty surfaces that need clearing
@@ -645,7 +643,6 @@ impl RenderState {
offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
outset: Option<f32>,
target_surface: SurfaceId,
) {
let surface_ids = fills_surface_id as u32
| strokes_surface_id as u32
@@ -689,16 +686,15 @@ impl RenderState {
&& !(shape.fills.is_empty() && has_nested_fills)
&& !shape
.svg_attrs
.as_ref().is_some_and(|attrs| attrs.fill_none)
&& target_surface != SurfaceId::Export;
.as_ref()
.is_some_and(|attrs| attrs.fill_none);
if can_render_directly {
let scale = self.get_scale();
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
self.surfaces.apply_mut(target_surface as u32, |s| {
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
let canvas = s.canvas();
canvas.save();
canvas.scale((scale, scale));
@@ -710,7 +706,7 @@ impl RenderState {
shape,
&shape.fills,
antialias,
target_surface,
SurfaceId::Current,
None,
);
@@ -720,12 +716,12 @@ impl RenderState {
self,
shape,
&visible_strokes,
Some(target_surface),
Some(SurfaceId::Current),
antialias,
outset,
);
self.surfaces.apply_mut(target_surface as u32, |s| {
self.surfaces.apply_mut(SurfaceId::Current as u32, |s| {
s.canvas().restore();
});
@@ -1147,7 +1143,7 @@ impl RenderState {
}
if apply_to_current_surface {
self.apply_drawing_to_render_canvas(Some(&shape), target_surface);
self.apply_drawing_to_render_canvas(Some(&shape));
}
// Only restore if we saved (optimization for simple shapes)
@@ -1309,7 +1305,7 @@ impl RenderState {
self.current_tile = None;
self.render_in_progress = true;
self.apply_drawing_to_render_canvas(None, SurfaceId::Current);
self.apply_drawing_to_render_canvas(None);
if sync_render {
self.render_shape_tree_sync(base_object, tree, timestamp)?;
@@ -1360,51 +1356,6 @@ 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
@@ -1412,7 +1363,7 @@ impl RenderState {
}
#[inline]
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool, target_surface: SurfaceId) {
pub fn render_shape_enter(&mut self, element: &Shape, mask: bool) {
// 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
@@ -1427,7 +1378,7 @@ impl RenderState {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(target_surface)
.canvas(SurfaceId::Current)
.save_layer(&layer_rec);
}
}
@@ -1444,7 +1395,7 @@ impl RenderState {
mask_paint.set_blend_mode(skia::BlendMode::DstIn);
let mask_rec = skia::canvas::SaveLayerRec::default().paint(&mask_paint);
self.surfaces
.canvas(target_surface)
.canvas(SurfaceId::Current)
.save_layer(&mask_rec);
}
@@ -1473,7 +1424,7 @@ impl RenderState {
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(target_surface)
.canvas(SurfaceId::Current)
.save_layer(&layer_rec);
}
@@ -1486,7 +1437,6 @@ 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
@@ -1494,7 +1444,7 @@ impl RenderState {
// extra restore.
if let Type::Group(group) = element.shape_type {
if group.masked {
self.surfaces.canvas(target_surface).restore();
self.surfaces.canvas(SurfaceId::Current).restore();
}
}
} else {
@@ -1556,7 +1506,6 @@ impl RenderState {
None,
None,
None,
target_surface,
);
}
@@ -1565,7 +1514,7 @@ impl RenderState {
let needs_layer = element.needs_layer();
if needs_layer {
self.surfaces.canvas(target_surface).restore();
self.surfaces.canvas(SurfaceId::Current).restore();
}
self.focus_mode.exit(&element.id);
@@ -1647,7 +1596,6 @@ 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);
@@ -1731,7 +1679,6 @@ impl RenderState {
Some(shadow.offset),
None,
Some(shadow.spread),
target_surface,
);
});
@@ -1778,7 +1725,6 @@ impl RenderState {
Some(shadow.offset), // Offset is geometric
None,
Some(shadow.spread),
target_surface,
);
});
@@ -1820,7 +1766,6 @@ impl RenderState {
Some(shadow.offset), // Offset is geometric
None,
Some(shadow.spread),
target_surface,
);
});
@@ -1875,7 +1820,6 @@ 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 {
@@ -1898,7 +1842,6 @@ impl RenderState {
scale,
translation,
None,
target_surface,
);
if !matches!(element.shape_type, Type::Bool(_)) {
@@ -1928,7 +1871,6 @@ impl RenderState {
scale,
translation,
inherited_layer_blur,
target_surface,
);
} else {
let paint = skia::Paint::default();
@@ -1965,7 +1907,6 @@ impl RenderState {
None,
Some(vec![new_shadow_paint.clone()]),
None,
target_surface,
);
});
self.surfaces.canvas(SurfaceId::DropShadows).restore();
@@ -1984,7 +1925,7 @@ impl RenderState {
if let Some(clips) = clip_bounds.as_ref() {
let antialias = element.should_use_antialias(scale);
self.surfaces.canvas(target_surface).save();
self.surfaces.canvas(SurfaceId::Current).save();
for (bounds, corners, transform) in clips.iter() {
let mut total_matrix = Matrix::new_identity();
total_matrix.pre_scale((scale, scale), None);
@@ -1992,18 +1933,18 @@ impl RenderState {
total_matrix.pre_concat(transform);
self.surfaces
.canvas(target_surface)
.canvas(SurfaceId::Current)
.concat(&total_matrix);
if let Some(corners) = corners {
let rrect = RRect::new_rect_radii(*bounds, corners);
self.surfaces.canvas(target_surface).clip_rrect(
self.surfaces.canvas(SurfaceId::Current).clip_rrect(
rrect,
skia::ClipOp::Intersect,
antialias,
);
} else {
self.surfaces.canvas(target_surface).clip_rect(
self.surfaces.canvas(SurfaceId::Current).clip_rect(
*bounds,
skia::ClipOp::Intersect,
antialias,
@@ -2011,15 +1952,15 @@ impl RenderState {
}
self.surfaces
.canvas(target_surface)
.canvas(SurfaceId::Current)
.concat(&total_matrix.invert().unwrap_or_default());
}
self.surfaces
.draw_into(SurfaceId::DropShadows, target_surface, None);
self.surfaces.canvas(target_surface).restore();
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
self.surfaces.canvas(SurfaceId::Current).restore();
} else {
self.surfaces
.draw_into(SurfaceId::DropShadows, target_surface, None);
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
}
self.surfaces
.canvas(SurfaceId::DropShadows)
@@ -2031,16 +1972,10 @@ 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;
@@ -2066,7 +2001,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, target_surface);
self.render_shape_exit(element, visited_mask, clip_bounds);
}
continue;
}
@@ -2089,14 +2024,11 @@ impl RenderState {
let has_effects = transformed_element.has_effects_that_extend_bounds();
let is_visible = export || if is_container || has_effects {
let is_visible = if is_container || has_effects {
let element_extrect =
extrect.get_or_insert_with(|| transformed_element.extrect(tree, scale));
element_extrect.intersects(self.render_area)
} else if !has_effects {
// Simple shape: selrect check is sufficient, skip expensive extrect
let selrect = transformed_element.selrect();
selrect.intersects(self.render_area)
&& !transformed_element.visually_insignificant(scale, tree)
} else {
let selrect = transformed_element.selrect();
selrect.intersects(self.render_area)
@@ -2135,7 +2067,6 @@ impl RenderState {
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
self.render_element_drop_shadows_and_composite(
element,
tree,
@@ -2144,11 +2075,10 @@ impl RenderState {
scale,
translation,
&node_render_state,
target_surface,
);
}
self.render_shape_enter(element, mask, target_surface);
self.render_shape_enter(element, mask);
}
if !node_render_state.is_root() && self.focus_mode.is_active() {
@@ -2175,7 +2105,6 @@ impl RenderState {
scale,
translation,
&node_render_state,
target_surface
);
}
@@ -2190,14 +2119,13 @@ 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), target_surface);
self.apply_drawing_to_render_canvas(Some(element));
}
// Skip nested state updates for flattened containers
@@ -2327,7 +2255,7 @@ impl RenderState {
let tile_is_visible = self.tile_viewbox.is_visible(&current_tile);
let can_stop = allow_stop && !tile_is_visible;
let (is_empty, early_return) =
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop, false)?;
self.render_shape_tree_partial_uncached(tree, timestamp, can_stop)?;
if early_return {
return Ok(());

View File

@@ -104,38 +104,4 @@ 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()
}
}

View File

@@ -41,8 +41,13 @@ fn draw_stroke_on_rect(
}
};
// By default just draw the rect. Only dotted inner/outer strokes need
// clipping to prevent the dotted pattern from appearing in wrong areas.
if let Some(clip_op) = stroke.clip_op() {
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
// Use a neutral layer (no extra paint) so opacity and filters
// come solely from the stroke paint. This avoids applying
// stroke alpha twice for dotted inner/outer strokes.
let layer_rec = skia::canvas::SaveLayerRec::default();
canvas.save_layer(&layer_rec);
match corners {
Some(radii) => {
@@ -81,7 +86,10 @@ fn draw_stroke_on_circle(
// By default just draw the circle. Only dotted inner/outer strokes need
// clipping to prevent the dotted pattern from appearing in wrong areas.
if let Some(clip_op) = stroke.clip_op() {
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
// Use a neutral layer (no extra paint) so opacity and filters
// come solely from the stroke paint. This avoids applying
// stroke alpha twice for dotted inner/outer strokes.
let layer_rec = skia::canvas::SaveLayerRec::default();
canvas.save_layer(&layer_rec);
let clip_path = {
let mut pb = skia::PathBuilder::new();

View File

@@ -17,18 +17,17 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
#[repr(u32)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId {
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,
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,
}
pub struct Surfaces {
@@ -53,15 +52,11 @@ pub struct Surfaces {
// for drawing debug info.
debug: skia::Surface,
// for drawing tiles.
export: skia::Surface,
tiles: TileTextureCache,
sampling_options: skia::SamplingOptions,
pub margins: skia::ISize,
margins: skia::ISize,
// Tracks which surfaces have content (dirty flag bitmask)
dirty_surfaces: u32,
extra_tile_dims: skia::ISize,
}
#[allow(dead_code)]
@@ -82,7 +77,6 @@ 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 =
@@ -93,13 +87,10 @@ 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,
@@ -113,12 +104,10 @@ impl Surfaces {
shape_strokes,
ui,
debug,
export,
tiles,
sampling_options,
margins,
dirty_surfaces: 0,
extra_tile_dims
}
}
@@ -270,9 +259,6 @@ 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");
}
@@ -298,7 +284,6 @@ 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);
@@ -320,7 +305,7 @@ impl Surfaces {
}
#[inline]
pub fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
fn get_mut(&mut self, id: SurfaceId) -> &mut skia::Surface {
match id {
SurfaceId::Target => &mut self.target,
SurfaceId::Filter => &mut self.filter,
@@ -333,7 +318,6 @@ impl Surfaces {
SurfaceId::Strokes => &mut self.shape_strokes,
SurfaceId::Debug => &mut self.debug,
SurfaceId::UI => &mut self.ui,
SurfaceId::Export => &mut self.export
}
}
@@ -350,7 +334,6 @@ impl Surfaces {
SurfaceId::Strokes => &self.shape_strokes,
SurfaceId::Debug => &self.debug,
SurfaceId::UI => &self.ui,
SurfaceId::Export => &self.export
}
}
@@ -465,14 +448,12 @@ 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::Export as u32,
| SurfaceId::TextDropShadows as u32,
|s| {
s.canvas().clear(color).reset_matrix();
},
@@ -591,32 +572,6 @@ 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);
if max_w > self.extra_tile_dims.width || max_h > self.extra_tile_dims.height {
self.extra_tile_dims = skia::ISize::new(max_w, max_h);
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.shape_fills =
self.shape_strokes.new_surface_with_dimensions((max_w, max_h)).unwrap();
}
self.export = self.export.new_surface_with_dimensions((target_w, target_h)).unwrap();
}
}
pub struct TileTextureCache {
@@ -694,5 +649,5 @@ impl TileTextureCache {
for k in self.grid.keys() {
self.removed.insert(*k);
}
}
}
}

View File

@@ -99,11 +99,6 @@ 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

View File

@@ -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,13 +195,11 @@ 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);
}

View File

@@ -126,11 +126,9 @@ 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_screen_coords(&point, &view_matrix, &shape_matrix)
text_content.get_caret_position_from_shape_coords(&point)
{
state.text_editor_state.set_caret_from_position(&position);
}
@@ -143,7 +141,6 @@ 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;
@@ -151,11 +148,6 @@ 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;
}
@@ -164,7 +156,7 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
};
if let Some(position) =
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
text_content.get_caret_position_from_shape_coords(&point)
{
state
.text_editor_state
@@ -179,7 +171,6 @@ 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;
@@ -187,11 +178,6 @@ 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;
}
@@ -199,7 +185,7 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
return;
};
if let Some(position) =
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
text_content.get_caret_position_from_shape_coords(&point)
{
state
.text_editor_state
@@ -209,6 +195,31 @@ 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, {