mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🎉 Add style data from text editor v3
This commit is contained in:
parent
12382cfbb9
commit
101b2fe9e6
@ -509,6 +509,7 @@
|
||||
(dissoc
|
||||
:current-file-id
|
||||
:workspace-editor-state
|
||||
:workspace-wasm-editor-styles
|
||||
:workspace-media-objects
|
||||
:workspace-persistence
|
||||
:workspace-presence
|
||||
|
||||
@ -8,6 +8,9 @@
|
||||
(:require
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.path.common :as dwpc]
|
||||
[app.main.features :as features]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.render-wasm.text-editor :as text-editor]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
@ -50,12 +53,19 @@
|
||||
(-> state
|
||||
(update :workspace-local dissoc :edition :edit-path)
|
||||
(update :workspace-drawing dissoc :tool :object :lock)
|
||||
(dissoc :workspace-grid-edition)))
|
||||
(dissoc :workspace-grid-edition)
|
||||
(dissoc :workspace-wasm-editor-styles)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [id (get-in state [:workspace-local :edition])]
|
||||
(rx/concat
|
||||
(when (some? id)
|
||||
(dwpc/finish-path)))))))
|
||||
(dwpc/finish-path)))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "text-editor-wasm/v1")
|
||||
(text-editor/text-editor-dispose)
|
||||
(wasm.api/request-render "clear-edition-mode")))))
|
||||
|
||||
|
||||
@ -114,11 +114,22 @@
|
||||
|
||||
(defn calculate-text-values
|
||||
[shape]
|
||||
(let [state-map (if (features/active-feature? @st/state "text-editor/v2")
|
||||
(let [state-map (cond
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
(deref refs/workspace-wasm-editor-styles)
|
||||
|
||||
(features/active-feature? @st/state "text-editor/v2")
|
||||
(deref refs/workspace-v2-editor-state)
|
||||
|
||||
:else
|
||||
(deref refs/workspace-editor-state))
|
||||
|
||||
editor-styles (when (features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
(get state-map (:id shape)))
|
||||
|
||||
editor-state (when-not (features/active-feature? @st/state "text-editor/v2")
|
||||
(get state-map (:id shape)))
|
||||
|
||||
editor-instance (when (features/active-feature? @st/state "text-editor/v2")
|
||||
(deref refs/workspace-editor))]
|
||||
(d/merge
|
||||
@ -126,12 +137,14 @@
|
||||
{:shape shape
|
||||
:attrs txt/root-attrs})
|
||||
(dwt/current-paragraph-values
|
||||
{:editor-state editor-state
|
||||
{:editor-styles editor-styles
|
||||
:editor-state editor-state
|
||||
:editor-instance editor-instance
|
||||
:shape shape
|
||||
:attrs txt/paragraph-attrs})
|
||||
(dwt/current-text-values
|
||||
{:editor-state editor-state
|
||||
{:editor-styles editor-styles
|
||||
:editor-state editor-state
|
||||
:editor-instance editor-instance
|
||||
:shape shape
|
||||
:attrs txt/text-node-attrs}))))
|
||||
|
||||
@ -250,6 +250,14 @@
|
||||
[{:keys [attrs shape]}]
|
||||
(shape-current-values shape txt/is-root-node? attrs))
|
||||
|
||||
(defn v3-current-text-values
|
||||
[{:keys [editor-styles attrs]}]
|
||||
(let [result (-> editor-styles
|
||||
;; If we use dm/select-keys compilation fails
|
||||
(select-keys attrs))
|
||||
result (if (empty? result) txt/default-text-attrs result)]
|
||||
result))
|
||||
|
||||
(defn v2-current-text-values
|
||||
[{:keys [editor-instance attrs]}]
|
||||
(let [result (-> (.-currentStyle editor-instance)
|
||||
@ -266,8 +274,9 @@
|
||||
(shape-current-values shape txt/is-paragraph-node? attrs)))
|
||||
|
||||
(defn current-paragraph-values
|
||||
[{:keys [editor-state editor-instance attrs shape] :as options}]
|
||||
[{:keys [editor-styles editor-state editor-instance attrs shape] :as options}]
|
||||
(cond
|
||||
(some? editor-styles) (v3-current-text-values options)
|
||||
(some? editor-instance) (v2-current-text-values options)
|
||||
(some? editor-state) (v1-current-paragraph-values options)
|
||||
:else (shape-current-values shape txt/is-paragraph-node? attrs)))
|
||||
@ -282,8 +291,9 @@
|
||||
result))
|
||||
|
||||
(defn current-text-values
|
||||
[{:keys [editor-state editor-instance attrs shape] :as options}]
|
||||
[{:keys [editor-styles editor-state editor-instance attrs shape] :as options}]
|
||||
(cond
|
||||
(some? editor-styles) (v3-current-text-values options)
|
||||
(some? editor-instance) (v2-current-text-values options)
|
||||
(some? editor-state) (v1-current-text-values options)
|
||||
:else (shape-current-values shape txt/is-text-node? attrs)))
|
||||
@ -480,13 +490,21 @@
|
||||
(ptk/reify ::update-text-with-function
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
;; This is only called when `[:workspace-editor-state id]` is set, this property
|
||||
;; keeps a Draft.js EditorState object.
|
||||
(d/update-in-when state [:workspace-editor-state id] ted/update-editor-current-inline-styles-fn (comp update-node-fn migrate-node)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (or
|
||||
(and (features/active-feature? state "text-editor/v2") (nil? (:workspace-editor state)))
|
||||
(and (not (features/active-feature? state "text-editor/v2")) (nil? (get-in state [:workspace-editor-state id]))))
|
||||
(and (features/active-feature? state "text-editor-wasm/v1")
|
||||
(nil? (get-in state [:workspace-wasm-editor-styles id])))
|
||||
(and (features/active-feature? state "text-editor/v2")
|
||||
(not (features/active-feature? state "text-editor-wasm/v1"))
|
||||
(nil? (:workspace-editor state)))
|
||||
(and (not (features/active-feature? state "text-editor/v2"))
|
||||
(not (features/active-feature? state "text-editor-wasm/v1"))
|
||||
(nil? (get-in state [:workspace-editor-state id]))))
|
||||
(let [page-id (or (get options :page-id)
|
||||
(get state :current-page-id))
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
@ -513,7 +531,12 @@
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "text-editor/v2")
|
||||
(cond
|
||||
(features/active-feature? state "text-editor-wasm/v1")
|
||||
(let [styles ((comp update-node-fn migrate-node))]
|
||||
(wasm.api/apply-styles-to-selection styles))
|
||||
|
||||
(features/active-feature? state "text-editor/v2")
|
||||
(when-let [instance (:workspace-editor state)]
|
||||
(let [styles (some-> (editor.v2/getCurrentStyle instance)
|
||||
(styles/get-styles-from-style-declaration :removed-mixed true)
|
||||
@ -792,7 +815,7 @@
|
||||
(when has-selection?
|
||||
(let [span-attrs (select-keys attrs txt/text-node-attrs)]
|
||||
(when (not (empty? span-attrs))
|
||||
(let [result (wasm.api/apply-style-to-selection span-attrs)]
|
||||
(let [result (wasm.api/apply-styles-to-selection span-attrs)]
|
||||
(when result
|
||||
(rx/of (v2-update-text-shape-content
|
||||
(:shape-id result) (:content result)
|
||||
@ -905,7 +928,7 @@
|
||||
{:typography-ref-id typ-id
|
||||
:typography-ref-file file-id}))))))))
|
||||
|
||||
;; -- New Editor
|
||||
;; -- Text Editor v2
|
||||
|
||||
(defn v2-update-text-editor-styles
|
||||
[id new-styles]
|
||||
@ -1121,3 +1144,7 @@
|
||||
(cond-> (or (some? width) (some? height))
|
||||
(gsh/transform-shape (ctm/change-size shape width height))))))
|
||||
{:undo-group (when new-shape? id)})))))))
|
||||
|
||||
;; -- Text Editor v3
|
||||
|
||||
;; @see texts_v3.cljs
|
||||
|
||||
20
frontend/src/app/main/data/workspace/texts_v3.cljs
Normal file
20
frontend/src/app/main/data/workspace/texts_v3.cljs
Normal file
@ -0,0 +1,20 @@
|
||||
;; 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.data.workspace.texts-v3
|
||||
(:require
|
||||
[app.common.types.text :as txt]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(defn v3-update-text-editor-styles
|
||||
[id new-styles]
|
||||
(ptk/reify ::v3-update-text-editor-styles
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [merged-styles (merge (txt/get-default-text-attrs)
|
||||
(get-in state [:workspace-global :default-font])
|
||||
new-styles)]
|
||||
(update-in state [:workspace-wasm-editor-styles id] (fnil merge {}) merged-styles)))))
|
||||
@ -378,6 +378,9 @@
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state))
|
||||
|
||||
(def workspace-wasm-editor-styles
|
||||
(l/derived :workspace-wasm-editor-styles st/state))
|
||||
|
||||
(def workspace-wasm-modifiers
|
||||
(l/derived :workspace-wasm-modifiers st/state))
|
||||
|
||||
|
||||
@ -317,10 +317,6 @@
|
||||
(reset! timeout-id (js/setTimeout schedule-blink caret-blink-interval-ms)))]
|
||||
(schedule-blink)
|
||||
(fn []
|
||||
;; ESTO ES JUSTO LO QUE NO QUIERO, NO QUIERO QUE SE HAGA
|
||||
;; DISPOSE CUANDO SE DESMONTA EL COMPONENTE.
|
||||
#_(when (text-editor/text-editor-dispose)
|
||||
(wasm.api/request-render "text-editor-dispose"))
|
||||
(when @timeout-id
|
||||
(js/clearTimeout @timeout-id))))))
|
||||
|
||||
|
||||
@ -305,6 +305,7 @@
|
||||
(when (not= "INPUT" (-> (dom/get-active) (dom/get-tag-name)))
|
||||
(let [node (txu/get-text-editor-content)]
|
||||
(dom/focus! node))))))}]
|
||||
|
||||
(hooks/use-stream
|
||||
expand-stream
|
||||
#(swap! state* assoc-in [:more-options] true))
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.main.ui.workspace.sidebar.options.shapes.text
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.text :as txt]
|
||||
@ -85,10 +86,20 @@
|
||||
(mf/deref parents-by-ids-ref)
|
||||
|
||||
state-map
|
||||
(if (features/active-feature? @st/state "text-editor/v2")
|
||||
(cond
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
(mf/deref refs/workspace-wasm-editor-styles)
|
||||
|
||||
(features/active-feature? @st/state "text-editor/v2")
|
||||
(mf/deref refs/workspace-v2-editor-state)
|
||||
|
||||
:else
|
||||
(mf/deref refs/workspace-editor-state))
|
||||
|
||||
editor-styles
|
||||
(when (features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
(get state-map id))
|
||||
|
||||
editor-state
|
||||
(when (not (features/active-feature? @st/state "text-editor/v2"))
|
||||
(get state-map id))
|
||||
@ -99,25 +110,28 @@
|
||||
|
||||
fill-values
|
||||
(dwt/current-text-values
|
||||
{:editor-state editor-state
|
||||
{:editor-styles editor-styles
|
||||
:editor-state editor-state
|
||||
:editor-instance editor-instance
|
||||
:shape shape
|
||||
:attrs (conj txt/text-fill-attrs :fills)})
|
||||
|
||||
text-values
|
||||
(merge
|
||||
(d/merge
|
||||
(select-keys shape [:grow-type])
|
||||
(select-keys shape fill/fill-attrs)
|
||||
(dwt/current-root-values
|
||||
{:shape shape
|
||||
:attrs txt/root-attrs})
|
||||
(dwt/current-paragraph-values
|
||||
{:editor-state editor-state
|
||||
{:editor-styles editor-styles
|
||||
:editor-state editor-state
|
||||
:editor-instance editor-instance
|
||||
:shape shape
|
||||
:attrs txt/paragraph-attrs})
|
||||
(dwt/current-text-values
|
||||
{:editor-state editor-state
|
||||
{:editor-styles editor-styles
|
||||
:editor-state editor-state
|
||||
:editor-instance editor-instance
|
||||
:shape shape
|
||||
:attrs txt/text-node-attrs}))]
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.workspace.texts-v3 :as texts]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.router :as rt]
|
||||
[app.main.store :as st]
|
||||
@ -51,6 +52,7 @@
|
||||
[cuerdas.core :as str]
|
||||
[promesa.core :as p]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def use-dpr? (contains? cf/flags :render-wasm-dpr))
|
||||
|
||||
(defn text-editor-wasm?
|
||||
@ -71,7 +73,6 @@
|
||||
(def ^:const INPUT-MODIFIER-U8-SIZE 44)
|
||||
(def ^:const INPUT-MODIFIER-U32-SIZE (/ INPUT-MODIFIER-U8-SIZE 4))
|
||||
|
||||
|
||||
(def ^:const GRID-LAYOUT-ROW-U8-SIZE 8)
|
||||
(def ^:const GRID-LAYOUT-COLUMN-U8-SIZE 8)
|
||||
(def ^:const GRID-LAYOUT-CELL-U8-SIZE 36)
|
||||
@ -86,6 +87,13 @@
|
||||
;; Threshold below which we use synchronous processing (no chunking overhead)
|
||||
(def ^:const ASYNC_THRESHOLD 100)
|
||||
|
||||
;; Text editor events.
|
||||
(def ^:const TEXT_EDITOR_EVENT_NONE 0)
|
||||
(def ^:const TEXT_EDITOR_EVENT_CONTENT_CHANGED 1)
|
||||
(def ^:const TEXT_EDITOR_EVENT_SELECTION_CHANGED 2)
|
||||
(def ^:const TEXT_EDITOR_EVENT_STYLES_CHANGED 3)
|
||||
(def ^:const TEXT_EDITOR_EVENT_NEEDS_LAYOUT 4)
|
||||
|
||||
;; Re-export public WebGL functions
|
||||
(def capture-canvas-pixels webgl/capture-canvas-pixels)
|
||||
(def restore-previous-canvas-pixels webgl/restore-previous-canvas-pixels)
|
||||
@ -99,6 +107,7 @@
|
||||
(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-get-current-styles text-editor/text-editor-get-current-styles)
|
||||
(def text-editor-has-focus? text-editor/text-editor-has-focus?)
|
||||
(def text-editor-has-selection? text-editor/text-editor-has-selection?)
|
||||
(def text-editor-select-all text-editor/text-editor-select-all)
|
||||
@ -162,18 +171,24 @@
|
||||
|
||||
;; Update text editor blink (so cursor toggles) using the same timestamp
|
||||
(try
|
||||
(when wasm/context-initialized?
|
||||
;; Render text editor overlay on top of main canvas (only if feature enabled)
|
||||
;; Determine if text-editor-wasm feature is active without requiring
|
||||
;; app.main.features to avoid circular dependency: check runtime and
|
||||
;; persisted feature sets in the store state.
|
||||
(when (is-text-editor-wasm-enabled @st/state)
|
||||
(text-editor/text-editor-update-blink timestamp)
|
||||
(text-editor/text-editor-render-overlay)
|
||||
;; Poll for editor events; if any event occurs, trigger a re-render
|
||||
(let [ev (text-editor/text-editor-poll-event)]
|
||||
(when (and ev (not= ev 0))
|
||||
(request-render "text-editor-event")))))
|
||||
(when (is-text-editor-wasm-enabled @st/state)
|
||||
(text-editor/text-editor-update-blink timestamp)
|
||||
(text-editor/text-editor-render-overlay)
|
||||
;; Poll for editor events; if any event occurs, trigger a re-render
|
||||
(let [ev (text-editor/text-editor-poll-event)]
|
||||
(when (and ev (not= ev TEXT_EDITOR_EVENT_NONE))
|
||||
;; When StylesChanged, get the current styles.
|
||||
(case ev
|
||||
;; StylesChanged Event
|
||||
TEXT_EDITOR_EVENT_STYLES_CHANGED
|
||||
(let [current-styles (text-editor/text-editor-get-current-styles)
|
||||
shape-id (text-editor/text-editor-get-active-shape-id)]
|
||||
(st/emit! (texts/v3-update-text-editor-styles shape-id current-styles)))
|
||||
|
||||
;; Default case
|
||||
nil)
|
||||
|
||||
(request-render "text-editor-event"))))
|
||||
(catch :default e
|
||||
(js/console.error "text-editor overlay/update failed:" e)))
|
||||
|
||||
@ -279,11 +294,12 @@
|
||||
(h/call wasm/internal-module "_update_shape_text_layout")
|
||||
result))
|
||||
|
||||
(defn apply-style-to-selection
|
||||
(defn apply-styles-to-selection
|
||||
"Apply style attrs to the currently selected text spans.
|
||||
Updates the cached content, pushes to WASM, and returns {:shape-id :content} for saving."
|
||||
[attrs]
|
||||
(text-editor/apply-style-to-selection attrs use-shape set-shape-text-content))
|
||||
(text-editor/apply-styles-to-selection attrs use-shape set-shape-text-content)
|
||||
(request-render "apply-styles-to-selection"))
|
||||
|
||||
(defn set-parent-id
|
||||
[id]
|
||||
|
||||
@ -75,6 +75,20 @@
|
||||
:builtin
|
||||
uuid/zero))
|
||||
|
||||
(defn uuid->font-id
|
||||
[font-uuid]
|
||||
(if (= font-uuid uuid/zero)
|
||||
"sourcesanspro"
|
||||
(or (:id (fonts/find-font-data {:uuid font-uuid}))
|
||||
(dm/str "custom-" font-uuid))))
|
||||
|
||||
(defn uuid->font-variant-id
|
||||
[font-id font-variant-uuid]
|
||||
(if (= font-variant-uuid uuid/zero)
|
||||
"regular"
|
||||
(or (:id (d/seek #(= (:uuid %) font-variant-uuid)
|
||||
(:variants (fonts/get-font-data font-id))))
|
||||
"regular")))
|
||||
|
||||
(defn ^:private font-id->asset-id [font-id font-variant-id font-weight font-style]
|
||||
(case (font-backend font-id)
|
||||
|
||||
@ -64,25 +64,12 @@
|
||||
(defn read-string
|
||||
"Read a UTF-8 string from WASM memory given a byte pointer/offset.
|
||||
Uses Emscripten's UTF8ToString to decode the string."
|
||||
[ptr]
|
||||
(h/call wasm/internal-module "UTF8ToString" ptr))
|
||||
|
||||
(defn read-null-terminated-string
|
||||
"Read a null-terminated UTF-8 string from WASM memory.
|
||||
Manually reads bytes until null terminator and decodes using TextDecoder."
|
||||
[ptr]
|
||||
(when (and ptr (not (zero? ptr)))
|
||||
(let [heap (get-heap-u8)
|
||||
;; Find the null terminator
|
||||
end-idx (loop [idx ptr]
|
||||
(if (zero? (aget heap idx))
|
||||
idx
|
||||
(recur (inc idx))))
|
||||
;; Extract the bytes (excluding null terminator)
|
||||
bytes (.slice heap ptr end-idx)
|
||||
;; Decode using TextDecoder
|
||||
decoder (js/TextDecoder. "utf-8")]
|
||||
(.decode decoder bytes))))
|
||||
([ptr max-bytes ignore-null]
|
||||
(h/call wasm/internal-module "UTF8ToString" ptr max-bytes ignore-null))
|
||||
([ptr max-bytes]
|
||||
(h/call wasm/internal-module "UTF8ToString" ptr max-bytes))
|
||||
([ptr]
|
||||
(h/call wasm/internal-module "UTF8ToString" ptr)))
|
||||
|
||||
(defn slice
|
||||
"Returns a copy of a portion of a typed array into a new typed array
|
||||
|
||||
@ -7,11 +7,119 @@
|
||||
(ns app.render-wasm.text-editor
|
||||
"Text editor WASM bindings"
|
||||
(:require
|
||||
[app.common.types.fills.impl :as types.fills.impl]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.fonts :as main-fonts]
|
||||
[app.render-wasm.api.fonts :as fonts]
|
||||
[app.render-wasm.helpers :as h]
|
||||
[app.render-wasm.mem :as mem]
|
||||
[app.render-wasm.wasm :as wasm]))
|
||||
|
||||
(def ^:const TEXT_EDITOR_STYLES_METADATA_SIZE (* 30 4))
|
||||
(def ^:const TEXT_EDITOR_STYLES_FILL_SOLID 0)
|
||||
(def ^:const TEXT_EDITOR_STYLES_FILL_LINEAR_GRADIENT 1)
|
||||
(def ^:const TEXT_EDITOR_STYLES_FILL_RADIAL_GRADIENT 2)
|
||||
(def ^:const TEXT_EDITOR_STYLES_FILL_IMAGE 3)
|
||||
|
||||
(defn- rgba->fill-color
|
||||
[rgba]
|
||||
(let [rgb (bit-and rgba 0x00ffffff)
|
||||
hex (.toString rgb 16)]
|
||||
(str "#" (.padStart hex 6 "0"))))
|
||||
|
||||
(defn- rgba->opacity
|
||||
[rgba]
|
||||
(let [alpha (bit-and (bit-shift-right rgba 24) 0xff)]
|
||||
(/ (js/Math.round (* (/ alpha 255) 100)) 100)))
|
||||
|
||||
(defn- u8->opacity
|
||||
[alpha]
|
||||
(/ (js/Math.round (* (/ alpha 255) 100)) 100))
|
||||
|
||||
(defn- read-fill-from-heap
|
||||
[heap-u8 heap-u32 heap-i32 heap-f32 fill-byte-offset]
|
||||
(let [fill-type (aget heap-u8 fill-byte-offset)
|
||||
fill-u32-offset (mem/->offset-32 fill-byte-offset)]
|
||||
(case fill-type
|
||||
TEXT_EDITOR_STYLES_FILL_SOLID
|
||||
(let [rgba (aget heap-u32 (+ fill-u32-offset 1))]
|
||||
{:fill-color (rgba->fill-color rgba)
|
||||
:fill-opacity (rgba->opacity rgba)})
|
||||
|
||||
TEXT_EDITOR_STYLES_FILL_LINEAR_GRADIENT
|
||||
(let [gradient-u32-offset (mem/->offset-32 (+ fill-byte-offset 4))
|
||||
start-x (aget heap-f32 gradient-u32-offset)
|
||||
start-y (aget heap-f32 (+ gradient-u32-offset 1))
|
||||
end-x (aget heap-f32 (+ gradient-u32-offset 2))
|
||||
end-y (aget heap-f32 (+ gradient-u32-offset 3))
|
||||
alpha (aget heap-u8 (+ fill-byte-offset 20))
|
||||
width (aget heap-f32 (+ gradient-u32-offset 5))
|
||||
stop-count (aget heap-u8 (+ fill-byte-offset 28))
|
||||
stops (->> (range stop-count)
|
||||
(map (fn [idx]
|
||||
(let [stop-offset (+ fill-byte-offset 32 (* idx 8))
|
||||
stop-u32-offset (mem/->offset-32 stop-offset)
|
||||
rgba (aget heap-u32 stop-u32-offset)
|
||||
offset (aget heap-f32 (+ stop-u32-offset 1))]
|
||||
{:color (rgba->fill-color rgba)
|
||||
:opacity (rgba->opacity rgba)
|
||||
:offset (/ (js/Math.round (* offset 100)) 100)})))
|
||||
(into []))]
|
||||
{:fill-opacity (u8->opacity alpha)
|
||||
:fill-color-gradient {:start-x start-x
|
||||
:start-y start-y
|
||||
:end-x end-x
|
||||
:end-y end-y
|
||||
:width width
|
||||
:stops stops
|
||||
:type :linear}})
|
||||
|
||||
TEXT_EDITOR_STYLES_FILL_RADIAL_GRADIENT
|
||||
(let [gradient-u32-offset (mem/->offset-32 (+ fill-byte-offset 4))
|
||||
start-x (aget heap-f32 gradient-u32-offset)
|
||||
start-y (aget heap-f32 (+ gradient-u32-offset 1))
|
||||
end-x (aget heap-f32 (+ gradient-u32-offset 2))
|
||||
end-y (aget heap-f32 (+ gradient-u32-offset 3))
|
||||
alpha (aget heap-u8 (+ fill-byte-offset 20))
|
||||
width (aget heap-f32 (+ gradient-u32-offset 5))
|
||||
stop-count (aget heap-u8 (+ fill-byte-offset 28))
|
||||
stops (->> (range stop-count)
|
||||
(map (fn [idx]
|
||||
(let [stop-offset (+ fill-byte-offset 32 (* idx 8))
|
||||
stop-u32-offset (mem/->offset-32 stop-offset)
|
||||
rgba (aget heap-u32 stop-u32-offset)
|
||||
offset (aget heap-f32 (+ stop-u32-offset 1))]
|
||||
{:color (rgba->fill-color rgba)
|
||||
:opacity (rgba->opacity rgba)
|
||||
:offset (/ (js/Math.round (* offset 100)) 100)})))
|
||||
(into []))]
|
||||
{:fill-opacity (u8->opacity alpha)
|
||||
:fill-color-gradient {:start-x start-x
|
||||
:start-y start-y
|
||||
:end-x end-x
|
||||
:end-y end-y
|
||||
:width width
|
||||
:stops stops
|
||||
:type :radial}})
|
||||
|
||||
TEXT_EDITOR_STYLES_FILL_IMAGE
|
||||
(let [a (aget heap-u32 (+ fill-u32-offset 1))
|
||||
b (aget heap-u32 (+ fill-u32-offset 2))
|
||||
c (aget heap-u32 (+ fill-u32-offset 3))
|
||||
d (aget heap-u32 (+ fill-u32-offset 4))
|
||||
alpha (aget heap-u8 (+ fill-byte-offset 20))
|
||||
flags (aget heap-u8 (+ fill-byte-offset 21))
|
||||
width (aget heap-i32 (+ fill-u32-offset 6))
|
||||
height (aget heap-i32 (+ fill-u32-offset 7))]
|
||||
{:fill-opacity (u8->opacity alpha)
|
||||
:fill-image {:id (uuid/from-unsigned-parts a b c d)
|
||||
:width width
|
||||
:height height
|
||||
:keep-aspect-ratio (not (zero? (bit-and flags 0x01)))
|
||||
:name "sample"}})
|
||||
|
||||
nil)))
|
||||
|
||||
(defn text-editor-focus
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
@ -66,6 +174,143 @@
|
||||
(let [res (h/call wasm/internal-module "_text_editor_poll_event")]
|
||||
res)))
|
||||
|
||||
(defn- text-editor-get-style-property
|
||||
([state value]
|
||||
(text-editor-get-style-property state value value))
|
||||
([state value default-value]
|
||||
(case state
|
||||
0 default-value
|
||||
1 value
|
||||
2 :multiple
|
||||
0)))
|
||||
|
||||
(defn- text-editor-translate-vertical-align
|
||||
[vertical-align]
|
||||
(case vertical-align
|
||||
0 "top"
|
||||
1 "center"
|
||||
2 "bottom"))
|
||||
|
||||
(defn- text-editor-translate-text-align
|
||||
[text-align]
|
||||
(case text-align
|
||||
0 "left"
|
||||
1 "center"
|
||||
2 "right"
|
||||
text-align))
|
||||
|
||||
(defn- text-editor-translate-text-direction
|
||||
[text-direction]
|
||||
(case text-direction
|
||||
0 "ltr"
|
||||
1 "rtl"
|
||||
text-direction))
|
||||
|
||||
(defn- text-editor-translate-text-transform
|
||||
[text-transform]
|
||||
(case text-transform
|
||||
0 "none"
|
||||
1 "lowercase"
|
||||
2 "uppercase"
|
||||
3 "capitalize"
|
||||
text-transform))
|
||||
|
||||
(defn- text-editor-translate-text-decoration
|
||||
[text-decoration]
|
||||
(case text-decoration
|
||||
0 "none"
|
||||
1 "underline"
|
||||
2 "linethrough"
|
||||
3 "overline"
|
||||
text-decoration))
|
||||
|
||||
(defn- text-editor-translate-font-style
|
||||
[font-style]
|
||||
(case font-style
|
||||
0 "normal"
|
||||
1 "italic"
|
||||
font-style))
|
||||
|
||||
(defn- text-editor-compute-font-variant-id
|
||||
[font-id font-weight font-style]
|
||||
(let [font-data (main-fonts/get-font-data font-id)
|
||||
variant (main-fonts/find-closest-variant font-data font-weight font-style)]
|
||||
(or (:id variant)
|
||||
(:name variant)
|
||||
"regular")))
|
||||
|
||||
(defn text-editor-get-current-styles
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(let [ptr (h/call wasm/internal-module "_text_editor_get_current_styles")]
|
||||
(when (and ptr (not (zero? ptr)))
|
||||
(let [heap-u8 (mem/get-heap-u8)
|
||||
heap-u32 (mem/get-heap-u32)
|
||||
heap-i32 (mem/get-heap-i32)
|
||||
heap-f32 (mem/get-heap-f32)
|
||||
u32-offset (mem/->offset-32 ptr)
|
||||
vertical-align (aget heap-u32 u32-offset)
|
||||
text-align-state (aget heap-u32 (+ u32-offset 1))
|
||||
text-direction-state (aget heap-u32 (+ u32-offset 2))
|
||||
text-decoration-state (aget heap-u32 (+ u32-offset 3))
|
||||
text-transform-state (aget heap-u32 (+ u32-offset 4))
|
||||
font-family-state (aget heap-u32 (+ u32-offset 5))
|
||||
font-size-state (aget heap-u32 (+ u32-offset 6))
|
||||
font-weight-state (aget heap-u32 (+ u32-offset 7))
|
||||
font-variant-id-state (aget heap-u32 (+ u32-offset 8))
|
||||
line-height-state (aget heap-u32 (+ u32-offset 9))
|
||||
letter-spacing-state (aget heap-u32 (+ u32-offset 10))
|
||||
num-fills (aget heap-u32 (+ u32-offset 11))
|
||||
|
||||
text-align-value (aget heap-u32 (+ u32-offset 12))
|
||||
text-direction-value (aget heap-u32 (+ u32-offset 13))
|
||||
text-decoration-value (aget heap-u32 (+ u32-offset 14))
|
||||
text-transform-value (aget heap-u32 (+ u32-offset 15))
|
||||
font-family-id-a (aget heap-u32 (+ u32-offset 16))
|
||||
font-family-id-b (aget heap-u32 (+ u32-offset 17))
|
||||
font-family-id-c (aget heap-u32 (+ u32-offset 18))
|
||||
font-family-id-d (aget heap-u32 (+ u32-offset 19))
|
||||
font-family-id-value (uuid/from-unsigned-parts font-family-id-a font-family-id-b font-family-id-c font-family-id-d)
|
||||
font-family-style-value (aget heap-u32 (+ u32-offset 20))
|
||||
_font-family-weight-value (aget heap-u32 (+ u32-offset 21))
|
||||
font-size-value (aget heap-f32 (+ u32-offset 22))
|
||||
font-weight-value (aget heap-i32 (+ u32-offset 23))
|
||||
line-height-value (aget heap-f32 (+ u32-offset 28))
|
||||
letter-spacing-value (aget heap-f32 (+ u32-offset 29))
|
||||
font-id (fonts/uuid->font-id font-family-id-value)
|
||||
font-style-value (text-editor-translate-font-style (text-editor-get-style-property font-family-state font-family-style-value))
|
||||
font-variant-id-computed (text-editor-compute-font-variant-id font-id font-weight-value font-style-value)
|
||||
|
||||
fills (->> (range num-fills)
|
||||
(map (fn [idx]
|
||||
(read-fill-from-heap
|
||||
heap-u8 heap-u32 heap-i32 heap-f32
|
||||
(+ ptr
|
||||
TEXT_EDITOR_STYLES_METADATA_SIZE
|
||||
(* idx types.fills.impl/FILL-U8-SIZE)))))
|
||||
(filter some?)
|
||||
(into []))
|
||||
|
||||
result {:vertical-align (text-editor-translate-vertical-align vertical-align)
|
||||
:text-align (text-editor-translate-text-align (text-editor-get-style-property text-align-state text-align-value))
|
||||
:text-direction (text-editor-translate-text-direction (text-editor-get-style-property text-direction-state text-direction-value))
|
||||
:text-decoration (text-editor-translate-text-decoration (text-editor-get-style-property text-decoration-state text-decoration-value))
|
||||
:text-transform (text-editor-translate-text-transform (text-editor-get-style-property text-transform-state text-transform-value))
|
||||
:line-height (text-editor-get-style-property line-height-state line-height-value)
|
||||
:letter-spacing (text-editor-get-style-property letter-spacing-state letter-spacing-value)
|
||||
:font-size (text-editor-get-style-property font-size-state font-size-value)
|
||||
:font-weight (text-editor-get-style-property font-weight-state font-weight-value)
|
||||
:font-style font-style-value
|
||||
:font-family (text-editor-get-style-property font-family-state font-id)
|
||||
:font-id (text-editor-get-style-property font-family-state font-id)
|
||||
:font-variant-id (text-editor-get-style-property font-variant-id-state font-variant-id-computed)
|
||||
:typography-ref-file nil
|
||||
:typography-ref-id nil
|
||||
:fills fills}]
|
||||
|
||||
(mem/free)
|
||||
result)))))
|
||||
|
||||
(defn text-editor-encode-text-pre
|
||||
[text]
|
||||
(when (and (not (empty? text))
|
||||
@ -171,7 +416,7 @@
|
||||
(when wasm/context-initialized?
|
||||
(let [ptr (h/call wasm/internal-module "_text_editor_export_content")]
|
||||
(when (and ptr (not (zero? ptr)))
|
||||
(let [json-str (mem/read-null-terminated-string ptr)]
|
||||
(let [json-str (mem/read-string ptr)]
|
||||
(mem/free)
|
||||
(js/JSON.parse json-str))))))
|
||||
|
||||
@ -181,7 +426,7 @@
|
||||
(when wasm/context-initialized?
|
||||
(let [ptr (h/call wasm/internal-module "_text_editor_export_selection")]
|
||||
(when (and ptr (not (zero? ptr)))
|
||||
(let [text (mem/read-null-terminated-string ptr)]
|
||||
(let [text (mem/read-string ptr)]
|
||||
(mem/free)
|
||||
text)))))
|
||||
|
||||
@ -342,38 +587,52 @@
|
||||
[para]
|
||||
(apply + (map (fn [span] (count (:text span))) (:children para))))
|
||||
|
||||
(defn apply-style-to-selection
|
||||
(defn apply-styles-to-selection
|
||||
[attrs use-shape-fn set-shape-text-content-fn]
|
||||
(when wasm/context-initialized?
|
||||
(let [shape-id (text-editor-get-active-shape-id)
|
||||
sel (text-editor-get-selection)]
|
||||
(when (and shape-id sel)
|
||||
(let [shape-id (text-editor-get-active-shape-id)
|
||||
selection (text-editor-get-selection)]
|
||||
|
||||
(when (and shape-id selection)
|
||||
(let [content (get @shape-text-contents shape-id)]
|
||||
(when content
|
||||
(let [{:keys [start-para start-offset end-para end-offset]}
|
||||
(normalize-selection sel)
|
||||
collapsed? (and (= start-para end-para) (= start-offset end-offset))
|
||||
para-set (first (:children content))
|
||||
paras (:children para-set)
|
||||
new-paras
|
||||
(let [normalized-selection (normalize-selection selection)
|
||||
{:keys [start-para start-offset end-para end-offset]} normalized-selection
|
||||
|
||||
collapsed? (and (= start-para end-para) (= start-offset end-offset))
|
||||
|
||||
paragraph-set (first (:children content))
|
||||
paragraphs (:children paragraph-set)
|
||||
|
||||
new-paragraphs
|
||||
(when (not collapsed?)
|
||||
(mapv (fn [idx para]
|
||||
(cond
|
||||
;; paragraph outside the range of paragraphs.
|
||||
(or (< idx start-para) (> idx end-para))
|
||||
para
|
||||
|
||||
;; same paragraph.
|
||||
(= start-para end-para)
|
||||
(apply-attrs-to-paragraph para start-offset end-offset attrs)
|
||||
|
||||
;; first paragraph
|
||||
(= idx start-para)
|
||||
(apply-attrs-to-paragraph para start-offset (para-char-count para) attrs)
|
||||
|
||||
;; final paragraph
|
||||
(= idx end-para)
|
||||
(apply-attrs-to-paragraph para 0 end-offset attrs)
|
||||
|
||||
;; any other paragraph
|
||||
:else
|
||||
(apply-attrs-to-paragraph para 0 (para-char-count para) attrs)))
|
||||
(range (count paras))
|
||||
paras))
|
||||
new-content (when new-paras
|
||||
|
||||
(range (count paragraphs))
|
||||
paragraphs))
|
||||
new-content (when new-paragraphs
|
||||
(assoc content :children
|
||||
[(assoc para-set :children new-paras)]))]
|
||||
[(assoc paragraph-set :children new-paragraphs)]))]
|
||||
(when new-content
|
||||
(swap! shape-text-contents assoc shape-id new-content)
|
||||
(use-shape-fn shape-id)
|
||||
|
||||
@ -39,7 +39,7 @@ export EMCC_CFLAGS="--no-entry \
|
||||
-sERROR_ON_UNDEFINED_SYMBOLS=0 \
|
||||
-sMAX_WEBGL_VERSION=2 \
|
||||
-sEXPORT_NAME=createRustSkiaModule \
|
||||
-sEXPORTED_RUNTIME_METHODS=GL,stringToUTF8,HEAPU8,HEAP32,HEAPU32,HEAPF32 \
|
||||
-sEXPORTED_RUNTIME_METHODS=GL,UTF8ToString,stringToUTF8,HEAPU8,HEAP32,HEAPU32,HEAPF32 \
|
||||
-sENVIRONMENT=web \
|
||||
-sMODULARIZE=1 \
|
||||
-sEXPORT_ES6=1";
|
||||
|
||||
@ -153,10 +153,11 @@ pub enum ConstraintH {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum VerticalAlign {
|
||||
Top,
|
||||
Center,
|
||||
Bottom,
|
||||
Top = 0,
|
||||
Center = 1,
|
||||
Bottom = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Copy)]
|
||||
|
||||
@ -3,9 +3,10 @@ use std::fmt;
|
||||
use crate::uuid::Uuid;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
#[repr(u8)]
|
||||
pub enum FontStyle {
|
||||
Normal,
|
||||
Italic,
|
||||
Normal = 0,
|
||||
Italic = 1,
|
||||
}
|
||||
|
||||
impl fmt::Display for FontStyle {
|
||||
@ -33,6 +34,16 @@ impl FontFamily {
|
||||
pub fn alias(&self) -> String {
|
||||
format!("{}", self)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> Uuid {
|
||||
self.id
|
||||
}
|
||||
pub fn style(&self) -> FontStyle {
|
||||
self.style
|
||||
}
|
||||
pub fn weight(&self) -> u32 {
|
||||
self.weight
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FontFamily {
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::shapes::{TextContent, TextPositionWithAffinity};
|
||||
use macros::ToJs;
|
||||
|
||||
use crate::shapes::{
|
||||
Fill, FontFamily, TextAlign, TextContent, TextDecoration, TextDirection,
|
||||
TextPositionWithAffinity, TextTransform, VerticalAlign,
|
||||
};
|
||||
use crate::uuid::Uuid;
|
||||
use crate::wasm::text::helpers as text_helpers;
|
||||
use crate::wasm::text::helpers::{self as text_helpers, find_text_span_at_offset};
|
||||
use crate::wasm::text_editor::CursorDirection;
|
||||
use skia_safe::{
|
||||
textlayout::{Affinity, PositionWithAffinity},
|
||||
Color,
|
||||
@ -82,13 +88,14 @@ impl TextSelection {
|
||||
}
|
||||
|
||||
/// Events that the text editor can emit for frontend synchronization
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, ToJs)]
|
||||
pub enum TextEditorEvent {
|
||||
None = 0,
|
||||
ContentChanged = 1,
|
||||
SelectionChanged = 2,
|
||||
NeedsLayout = 3,
|
||||
StylesChanged = 3,
|
||||
NeedsLayout = 4,
|
||||
}
|
||||
|
||||
/// FIXME: It should be better to get these constants from the frontend through the API.
|
||||
@ -97,6 +104,154 @@ const CURSOR_WIDTH: f32 = 1.5;
|
||||
const CURSOR_COLOR: Color = Color::BLACK;
|
||||
const CURSOR_BLINK_INTERVAL_MS: f64 = 530.0;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextEditorStyles {
|
||||
pub vertical_align: VerticalAlign,
|
||||
pub text_align: Multiple<TextAlign>, // Multiple
|
||||
pub text_direction: Multiple<TextDirection>, // Multiple
|
||||
pub text_decoration: Multiple<TextDecoration>,
|
||||
pub text_transform: Multiple<TextTransform>,
|
||||
pub font_family: Multiple<FontFamily>,
|
||||
pub font_size: Multiple<f32>,
|
||||
pub font_weight: Multiple<i32>,
|
||||
pub font_variant_id: Multiple<Uuid>,
|
||||
pub line_height: Multiple<f32>,
|
||||
pub letter_spacing: Multiple<f32>,
|
||||
pub fills: Vec<Fill>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[repr(u8)]
|
||||
pub enum MultipleState {
|
||||
Undefined = 0,
|
||||
Single = 1,
|
||||
Multiple = 2,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Multiple<T> {
|
||||
state: MultipleState,
|
||||
value: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Multiple<T> {
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
state: MultipleState::Undefined,
|
||||
value: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(state: MultipleState, value: Option<T>) -> Self {
|
||||
Self { state, value }
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &MultipleState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub fn value(&self) -> &Option<T> {
|
||||
&self.value
|
||||
}
|
||||
|
||||
pub fn is_undefined(&self) -> bool {
|
||||
self.state == MultipleState::Undefined
|
||||
}
|
||||
|
||||
pub fn is_multiple(&self) -> bool {
|
||||
self.state == MultipleState::Multiple
|
||||
}
|
||||
|
||||
pub fn is_single(&self) -> bool {
|
||||
self.state == MultipleState::Single
|
||||
}
|
||||
|
||||
pub fn is_single_some(&self) -> bool {
|
||||
!self.is_undefined() && self.value.is_some()
|
||||
}
|
||||
|
||||
pub fn is_single_none(&self) -> bool {
|
||||
!self.is_undefined() && self.value.is_none()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.state = MultipleState::Undefined;
|
||||
self.value = None;
|
||||
}
|
||||
|
||||
pub fn set(&mut self, value: Option<T>) {
|
||||
if self.state == MultipleState::Undefined || self.state == MultipleState::Multiple {
|
||||
self.state = MultipleState::Single;
|
||||
}
|
||||
self.value = value;
|
||||
}
|
||||
|
||||
pub fn set_single(&mut self, value: Option<T>) {
|
||||
self.state = MultipleState::Single;
|
||||
self.value = value;
|
||||
}
|
||||
|
||||
pub fn set_multiple(&mut self) {
|
||||
self.state = MultipleState::Multiple;
|
||||
self.value = None;
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, value: Option<T>) -> bool
|
||||
where
|
||||
T: PartialEq,
|
||||
{
|
||||
if self.state == MultipleState::Multiple {
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.state == MultipleState::Undefined {
|
||||
self.set_single(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.value.as_ref() != value.as_ref() {
|
||||
self.set_multiple();
|
||||
return false;
|
||||
}
|
||||
|
||||
self.value = value;
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl TextEditorStyles {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
vertical_align: VerticalAlign::Top,
|
||||
text_align: Multiple::empty(),
|
||||
text_direction: Multiple::empty(),
|
||||
text_decoration: Multiple::empty(),
|
||||
text_transform: Multiple::empty(),
|
||||
font_family: Multiple::empty(),
|
||||
font_size: Multiple::empty(),
|
||||
font_weight: Multiple::empty(),
|
||||
font_variant_id: Multiple::empty(),
|
||||
line_height: Multiple::empty(),
|
||||
letter_spacing: Multiple::empty(),
|
||||
fills: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.text_align.reset();
|
||||
self.text_direction.reset();
|
||||
self.text_decoration.reset();
|
||||
self.text_transform.reset();
|
||||
self.font_family.reset();
|
||||
self.font_size.reset();
|
||||
self.font_weight.reset();
|
||||
self.font_variant_id.reset();
|
||||
self.line_height.reset();
|
||||
self.letter_spacing.reset();
|
||||
self.fills.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextEditorTheme {
|
||||
pub selection_color: Color,
|
||||
pub cursor_width: f32,
|
||||
@ -172,6 +327,7 @@ pub struct TextEditorState {
|
||||
pub active_shape_id: Option<Uuid>,
|
||||
pub cursor_visible: bool,
|
||||
pub last_blink_time: f64,
|
||||
pub current_styles: TextEditorStyles,
|
||||
pending_events: Vec<TextEditorEvent>,
|
||||
}
|
||||
|
||||
@ -191,6 +347,7 @@ impl TextEditorState {
|
||||
cursor_visible: true,
|
||||
last_blink_time: 0.0,
|
||||
pending_events: Vec::new(),
|
||||
current_styles: TextEditorStyles::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,11 +397,11 @@ impl TextEditorState {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self, content: &TextContent) -> bool {
|
||||
pub fn select_all(&mut self, text_content: &TextContent) -> bool {
|
||||
self.is_pointer_selection_active = false;
|
||||
self.set_caret_from_position(&TextPositionWithAffinity::empty());
|
||||
let num_paragraphs = content.paragraphs().len() - 1;
|
||||
let Some(last_paragraph) = content.paragraphs().last() else {
|
||||
let num_paragraphs = text_content.paragraphs().len() - 1;
|
||||
let Some(last_paragraph) = text_content.paragraphs().last() else {
|
||||
return false;
|
||||
};
|
||||
#[allow(dead_code)]
|
||||
@ -264,20 +421,21 @@ impl TextEditorState {
|
||||
num_paragraphs,
|
||||
offset,
|
||||
));
|
||||
self.update_styles(text_content);
|
||||
self.reset_blink();
|
||||
self.push_event(crate::state::TextEditorEvent::SelectionChanged);
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn select_word_boundary(
|
||||
&mut self,
|
||||
content: &TextContent,
|
||||
text_content: &TextContent,
|
||||
position: &TextPositionWithAffinity,
|
||||
) {
|
||||
self.is_pointer_selection_active = false;
|
||||
|
||||
let paragraphs = content.paragraphs();
|
||||
let paragraphs = text_content.paragraphs();
|
||||
if paragraphs.is_empty() || position.paragraph >= paragraphs.len() {
|
||||
return;
|
||||
}
|
||||
@ -295,6 +453,7 @@ impl TextEditorState {
|
||||
position.paragraph,
|
||||
0,
|
||||
));
|
||||
self.update_styles(text_content);
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
return;
|
||||
@ -316,6 +475,7 @@ impl TextEditorState {
|
||||
position.paragraph,
|
||||
position.offset.min(chars.len()),
|
||||
));
|
||||
self.update_styles(text_content);
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
return;
|
||||
@ -339,6 +499,7 @@ impl TextEditorState {
|
||||
position.paragraph,
|
||||
end,
|
||||
));
|
||||
self.update_styles(text_content);
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
@ -353,6 +514,183 @@ impl TextEditorState {
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
fn update_styles_from_selection(&mut self, text_content: &TextContent) -> bool {
|
||||
let paragraphs = text_content.paragraphs();
|
||||
if paragraphs.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let start = self.selection.start();
|
||||
if start.paragraph >= paragraphs.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let end = self.selection.end();
|
||||
let end_paragraph = end.paragraph.min(paragraphs.len() - 1);
|
||||
|
||||
self.current_styles.reset();
|
||||
|
||||
let mut has_selected_content = false;
|
||||
let mut has_fills = false;
|
||||
let mut fills_are_multiple = false;
|
||||
|
||||
for (para_idx, paragraph) in paragraphs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(end_paragraph + 1)
|
||||
.skip(start.paragraph)
|
||||
{
|
||||
let paragraph_char_count: usize = paragraph
|
||||
.children()
|
||||
.iter()
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum();
|
||||
|
||||
let range_start = if para_idx == start.paragraph {
|
||||
start.offset.min(paragraph_char_count)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.offset.min(paragraph_char_count)
|
||||
} else {
|
||||
paragraph_char_count
|
||||
};
|
||||
|
||||
if range_start >= range_end {
|
||||
continue;
|
||||
}
|
||||
|
||||
has_selected_content = true;
|
||||
self.current_styles
|
||||
.text_align
|
||||
.merge(Some(paragraph.text_align()));
|
||||
|
||||
let mut char_pos = 0;
|
||||
for span in paragraph.children() {
|
||||
let span_len = span.text.chars().count();
|
||||
let span_start = char_pos;
|
||||
let span_end = char_pos + span_len;
|
||||
char_pos += span_len;
|
||||
|
||||
let selected_start = range_start.max(span_start);
|
||||
let selected_end = range_end.min(span_end);
|
||||
if selected_start >= selected_end {
|
||||
continue;
|
||||
}
|
||||
|
||||
self.current_styles
|
||||
.text_direction
|
||||
.merge(Some(span.text_direction));
|
||||
self.current_styles
|
||||
.text_decoration
|
||||
.merge(span.text_decoration);
|
||||
self.current_styles
|
||||
.text_transform
|
||||
.merge(span.text_transform);
|
||||
self.current_styles
|
||||
.font_family
|
||||
.merge(Some(span.font_family));
|
||||
self.current_styles.font_size.merge(Some(span.font_size));
|
||||
self.current_styles
|
||||
.font_weight
|
||||
.merge(Some(span.font_weight));
|
||||
self.current_styles
|
||||
.font_variant_id
|
||||
.merge(Some(span.font_variant_id));
|
||||
self.current_styles
|
||||
.line_height
|
||||
.merge(Some(span.line_height));
|
||||
self.current_styles
|
||||
.letter_spacing
|
||||
.merge(Some(span.letter_spacing));
|
||||
|
||||
if !fills_are_multiple {
|
||||
if !has_fills {
|
||||
self.current_styles.fills = span.fills.clone();
|
||||
has_fills = true;
|
||||
} else if self.current_styles.fills != span.fills {
|
||||
fills_are_multiple = true;
|
||||
self.current_styles.fills.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
has_selected_content
|
||||
}
|
||||
|
||||
fn update_styles_from_caret(&mut self, text_content: &TextContent) -> bool {
|
||||
let focus = self.selection.focus;
|
||||
let paragraphs = text_content.paragraphs();
|
||||
let Some(current_paragraph) = paragraphs.get(focus.paragraph) else {
|
||||
return false;
|
||||
};
|
||||
let current_offset = focus.offset;
|
||||
let current_text_span = find_text_span_at_offset(current_paragraph, current_offset);
|
||||
|
||||
self.current_styles
|
||||
.text_align
|
||||
.set_single(Some(current_paragraph.text_align()));
|
||||
if let Some((text_span_index, _)) = current_text_span {
|
||||
if let Some(text_span) = current_paragraph.children().get(text_span_index) {
|
||||
self.current_styles
|
||||
.text_direction
|
||||
.set_single(Some(text_span.text_direction));
|
||||
self.current_styles
|
||||
.text_decoration
|
||||
.set_single(text_span.text_decoration);
|
||||
self.current_styles
|
||||
.text_transform
|
||||
.set_single(text_span.text_transform);
|
||||
self.current_styles
|
||||
.font_family
|
||||
.set_single(Some(text_span.font_family));
|
||||
self.current_styles
|
||||
.font_size
|
||||
.set_single(Some(text_span.font_size));
|
||||
self.current_styles
|
||||
.font_weight
|
||||
.set_single(Some(text_span.font_weight));
|
||||
self.current_styles
|
||||
.font_variant_id
|
||||
.set_single(Some(text_span.font_variant_id));
|
||||
self.current_styles
|
||||
.line_height
|
||||
.set_single(Some(text_span.line_height));
|
||||
self.current_styles
|
||||
.letter_spacing
|
||||
.set_single(Some(text_span.letter_spacing));
|
||||
self.current_styles.fills = text_span.fills.clone();
|
||||
}
|
||||
} else {
|
||||
self.current_styles
|
||||
.line_height
|
||||
.set_single(Some(current_paragraph.line_height()));
|
||||
self.current_styles
|
||||
.letter_spacing
|
||||
.set_single(Some(current_paragraph.letter_spacing()));
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn update_styles(&mut self, text_content: &TextContent) -> bool {
|
||||
if self.selection.is_selection() {
|
||||
let styles_were_updated = self.update_styles_from_selection(text_content);
|
||||
if styles_were_updated {
|
||||
self.push_event(TextEditorEvent::StylesChanged);
|
||||
}
|
||||
return styles_were_updated;
|
||||
}
|
||||
// It is a caret.
|
||||
let styles_were_updated = self.update_styles_from_caret(text_content);
|
||||
if styles_were_updated {
|
||||
self.push_event(TextEditorEvent::StylesChanged);
|
||||
}
|
||||
styles_were_updated
|
||||
}
|
||||
|
||||
pub fn update_blink(&mut self, timestamp_ms: f64) {
|
||||
if !self.has_focus {
|
||||
return;
|
||||
@ -389,6 +727,145 @@ impl TextEditorState {
|
||||
pub fn has_pending_events(&self) -> bool {
|
||||
!self.pending_events.is_empty()
|
||||
}
|
||||
|
||||
pub fn delete_backward(&mut self, text_content: &mut TextContent, word_boundary: bool) {
|
||||
if self.selection.is_selection() {
|
||||
text_helpers::delete_selection_range(text_content, &self.selection);
|
||||
let start = self.selection.start();
|
||||
let clamped = text_helpers::clamp_cursor(start, text_content.paragraphs());
|
||||
self.selection.set_caret(clamped);
|
||||
} else if word_boundary {
|
||||
let cursor = self.selection.focus;
|
||||
if let Some(new_cursor) = text_helpers::delete_word_before(text_content, &cursor) {
|
||||
self.selection.set_caret(new_cursor);
|
||||
}
|
||||
} else {
|
||||
let cursor = self.selection.focus;
|
||||
if let Some(new_cursor) = text_helpers::delete_char_before(text_content, &cursor) {
|
||||
self.selection.set_caret(new_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
text_content.layout.paragraphs.clear();
|
||||
text_content.layout.paragraph_builders.clear();
|
||||
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::ContentChanged);
|
||||
self.push_event(TextEditorEvent::NeedsLayout);
|
||||
}
|
||||
|
||||
pub fn delete_forward(&mut self, text_content: &mut TextContent, word_boundary: bool) {
|
||||
if self.selection.is_selection() {
|
||||
text_helpers::delete_selection_range(text_content, &self.selection);
|
||||
let start = self.selection.start();
|
||||
let clamped = text_helpers::clamp_cursor(start, text_content.paragraphs());
|
||||
self.selection.set_caret(clamped);
|
||||
} else if word_boundary {
|
||||
let cursor = self.selection.focus;
|
||||
text_helpers::delete_word_after(text_content, &cursor);
|
||||
let clamped = text_helpers::clamp_cursor(cursor, text_content.paragraphs());
|
||||
self.selection.set_caret(clamped);
|
||||
} else {
|
||||
let cursor = self.selection.focus;
|
||||
text_helpers::delete_char_after(text_content, &cursor);
|
||||
let clamped = text_helpers::clamp_cursor(cursor, text_content.paragraphs());
|
||||
self.selection.set_caret(clamped);
|
||||
}
|
||||
|
||||
text_content.layout.paragraphs.clear();
|
||||
text_content.layout.paragraph_builders.clear();
|
||||
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::ContentChanged);
|
||||
self.push_event(TextEditorEvent::NeedsLayout);
|
||||
}
|
||||
|
||||
pub fn insert_paragraph(&mut self, text_content: &mut TextContent) {
|
||||
if self.selection.is_selection() {
|
||||
text_helpers::delete_selection_range(text_content, &self.selection);
|
||||
let start = self.selection.start();
|
||||
self.selection.set_caret(start);
|
||||
}
|
||||
|
||||
let cursor = self.selection.focus;
|
||||
if text_helpers::split_paragraph_at_cursor(text_content, &cursor) {
|
||||
let new_cursor =
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0);
|
||||
self.selection.set_caret(new_cursor);
|
||||
}
|
||||
|
||||
text_content.layout.paragraphs.clear();
|
||||
text_content.layout.paragraph_builders.clear();
|
||||
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::ContentChanged);
|
||||
self.push_event(TextEditorEvent::NeedsLayout);
|
||||
}
|
||||
|
||||
pub fn move_cursor(
|
||||
&mut self,
|
||||
text_content: &TextContent,
|
||||
direction: CursorDirection,
|
||||
word_boundary: bool,
|
||||
extend_selection: bool,
|
||||
) -> bool {
|
||||
let paragraphs = text_content.paragraphs();
|
||||
if paragraphs.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let focus = self.selection.focus;
|
||||
|
||||
// Get the text direction of the span at the current cursor position
|
||||
let text_span_text_direction = if focus.paragraph < paragraphs.len() {
|
||||
text_helpers::get_text_span_text_direction_at_offset(
|
||||
¶graphs[focus.paragraph],
|
||||
focus.offset,
|
||||
)
|
||||
} else {
|
||||
TextDirection::LTR
|
||||
};
|
||||
|
||||
// For horizontal navigation, swap Backward/Forward when in RTL text
|
||||
let adjusted_direction = if text_span_text_direction == TextDirection::RTL {
|
||||
match direction {
|
||||
CursorDirection::Backward => CursorDirection::Forward,
|
||||
CursorDirection::Forward => CursorDirection::Backward,
|
||||
other => other,
|
||||
}
|
||||
} else {
|
||||
direction
|
||||
};
|
||||
|
||||
let new_cursor = match adjusted_direction {
|
||||
CursorDirection::Backward => {
|
||||
text_helpers::move_cursor_backward(&focus, paragraphs, word_boundary)
|
||||
}
|
||||
CursorDirection::Forward => {
|
||||
text_helpers::move_cursor_forward(&focus, paragraphs, word_boundary)
|
||||
}
|
||||
CursorDirection::LineBefore => {
|
||||
text_helpers::move_cursor_up(&focus, paragraphs, text_content)
|
||||
}
|
||||
CursorDirection::LineAfter => {
|
||||
text_helpers::move_cursor_down(&focus, paragraphs, text_content)
|
||||
}
|
||||
CursorDirection::LineStart => text_helpers::move_cursor_line_start(&focus, paragraphs),
|
||||
CursorDirection::LineEnd => text_helpers::move_cursor_line_end(&focus, paragraphs),
|
||||
};
|
||||
|
||||
if extend_selection {
|
||||
self.selection.extend_to(new_cursor);
|
||||
} else {
|
||||
self.selection.set_caret(new_cursor);
|
||||
}
|
||||
|
||||
self.update_styles(text_content);
|
||||
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
fn is_word_char(c: char) -> bool {
|
||||
|
||||
@ -36,12 +36,44 @@ impl From<RawFillData> for shapes::Fill {
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&shapes::Fill> for RawFillData {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(fill: &shapes::Fill) -> Result<Self, Self::Error> {
|
||||
match fill {
|
||||
shapes::Fill::Solid(shapes::SolidColor(color)) => {
|
||||
Ok(RawFillData::Solid(solid::RawSolidData {
|
||||
color: ((color.a() as u32) << 24)
|
||||
| ((color.r() as u32) << 16)
|
||||
| ((color.g() as u32) << 8)
|
||||
| (color.b() as u32),
|
||||
}))
|
||||
}
|
||||
shapes::Fill::LinearGradient(_) => {
|
||||
Err("LinearGradient serialization is not implemented".to_string())
|
||||
}
|
||||
shapes::Fill::RadialGradient(_) => {
|
||||
Err("RadialGradient serialization is not implemented".to_string())
|
||||
}
|
||||
shapes::Fill::Image(_) => {
|
||||
Err("Image fill serialization is not implemented".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<[u8; RAW_FILL_DATA_SIZE]> for RawFillData {
|
||||
fn from(bytes: [u8; RAW_FILL_DATA_SIZE]) -> Self {
|
||||
unsafe { std::mem::transmute(bytes) }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RawFillData> for [u8; RAW_FILL_DATA_SIZE] {
|
||||
fn from(fill_data: RawFillData) -> Self {
|
||||
unsafe { std::mem::transmute(fill_data) }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for RawFillData {
|
||||
type Error = String;
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
@ -54,7 +86,7 @@ impl TryFrom<&[u8]> for RawFillData {
|
||||
}
|
||||
|
||||
// FIXME: return Result
|
||||
pub fn parse_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec<shapes::Fill> {
|
||||
pub fn read_fills_from_bytes(buffer: &[u8], num_fills: usize) -> Vec<shapes::Fill> {
|
||||
buffer
|
||||
.chunks_exact(RAW_FILL_DATA_SIZE)
|
||||
.take(num_fills)
|
||||
@ -74,7 +106,7 @@ pub extern "C" fn set_shape_fills() -> Result<()> {
|
||||
// The first byte contains the actual number of fills
|
||||
let num_fills = bytes.first().copied().unwrap_or(0) as usize;
|
||||
// Skip the first 4 bytes (header with fill count) and parse only the actual fills
|
||||
let fills = parse_fills_from_bytes(&bytes[4..], num_fills);
|
||||
let fills = read_fills_from_bytes(&bytes[4..], num_fills);
|
||||
shape.set_fills(fills);
|
||||
mem::free_bytes()?;
|
||||
});
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use crate::shapes::{Paragraph, TextContent, TextPositionWithAffinity};
|
||||
use crate::shapes::{Paragraph, TextContent, TextDirection, TextPositionWithAffinity};
|
||||
use crate::state::TextSelection;
|
||||
|
||||
/// Get total character count in a paragraph.
|
||||
@ -10,11 +10,11 @@ pub fn paragraph_char_count(para: &Paragraph) -> usize {
|
||||
}
|
||||
|
||||
/// Get the text direction of the span at a given offset in a paragraph.
|
||||
pub fn get_span_text_direction_at_offset(
|
||||
pub fn get_text_span_text_direction_at_offset(
|
||||
para: &Paragraph,
|
||||
char_offset: usize,
|
||||
) -> skia_safe::textlayout::TextDirection {
|
||||
if let Some((span_idx, _)) = find_span_at_offset(para, char_offset) {
|
||||
) -> TextDirection {
|
||||
if let Some((span_idx, _)) = find_text_span_at_offset(para, char_offset) {
|
||||
if let Some(span) = para.children().get(span_idx) {
|
||||
return span.text_direction;
|
||||
}
|
||||
@ -258,7 +258,7 @@ pub fn paragraph_text_char_at(para: &Paragraph, offset: usize) -> Option<char> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn find_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, usize)> {
|
||||
pub fn find_text_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, usize)> {
|
||||
let children = para.children();
|
||||
let mut accumulated = 0;
|
||||
for (span_idx, span) in children.iter().enumerate() {
|
||||
@ -338,7 +338,7 @@ pub fn insert_text_at_cursor(
|
||||
return Some(text.chars().count());
|
||||
}
|
||||
|
||||
let (span_idx, offset_in_span) = find_span_at_offset(para, cursor.offset)?;
|
||||
let (span_idx, offset_in_span) = find_text_span_at_offset(para, cursor.offset)?;
|
||||
|
||||
let children = para.children_mut();
|
||||
let span = &mut children[span_idx];
|
||||
@ -690,7 +690,7 @@ pub fn split_paragraph_at_cursor(
|
||||
|
||||
let para = ¶graphs[cursor.paragraph];
|
||||
|
||||
let Some((span_idx, offset_in_span)) = find_span_at_offset(para, cursor.offset) else {
|
||||
let Some((span_idx, offset_in_span)) = find_text_span_at_offset(para, cursor.offset) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@ -4,13 +4,16 @@ use crate::math::{Matrix, Point, Rect};
|
||||
use crate::mem;
|
||||
use crate::render::text_editor as text_editor_render;
|
||||
use crate::render::SurfaceId;
|
||||
use crate::shapes::{Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||
use crate::state::TextSelection;
|
||||
use crate::shapes::{Shape, TextAlign, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||
use crate::state::{TextEditorEvent, TextSelection};
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
use crate::utils::uuid_to_u32_quartet;
|
||||
use crate::wasm::text::helpers as text_helpers;
|
||||
use crate::wasm::fills::RawFillData;
|
||||
use crate::wasm::text::{
|
||||
helpers as text_helpers, RawTextAlign, RawTextDecoration, RawTextDirection, RawTextTransform,
|
||||
};
|
||||
use crate::{with_state, with_state_mut, STATE};
|
||||
use skia_safe::{textlayout::TextDirection, Color};
|
||||
use skia_safe::Color;
|
||||
|
||||
#[derive(PartialEq, ToJs)]
|
||||
#[repr(u8)]
|
||||
@ -194,6 +197,7 @@ pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
|
||||
state.text_editor_state.start_pointer_selection();
|
||||
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||
state.text_editor_state.set_caret_from_position(&position);
|
||||
state.text_editor_state.update_styles(text_content);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -222,6 +226,7 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(&position);
|
||||
state.text_editor_state.update_styles(text_content);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -249,6 +254,7 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(&position);
|
||||
state.text_editor_state.update_styles(text_content);
|
||||
}
|
||||
state.text_editor_state.stop_pointer_selection();
|
||||
});
|
||||
@ -312,7 +318,7 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
#[wasm_error]
|
||||
pub extern "C" fn text_editor_composition_start() -> Result<()> {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
if !state.text_editor_state.has_focus {
|
||||
return Ok(());
|
||||
}
|
||||
state.text_editor_state.composition.start();
|
||||
@ -331,7 +337,7 @@ pub extern "C" fn text_editor_composition_end() -> Result<()> {
|
||||
};
|
||||
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
if !state.text_editor_state.has_focus {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -392,7 +398,7 @@ pub extern "C" fn text_editor_composition_update() -> Result<()> {
|
||||
};
|
||||
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
if !state.text_editor_state.has_focus {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@ -486,10 +492,10 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
.push_event(TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
.push_event(TextEditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
@ -517,36 +523,9 @@ pub extern "C" fn text_editor_delete_backward(word_boundary: bool) {
|
||||
return;
|
||||
};
|
||||
|
||||
let selection = state.text_editor_state.selection;
|
||||
|
||||
if selection.is_selection() {
|
||||
text_helpers::delete_selection_range(text_content, &selection);
|
||||
let start = selection.start();
|
||||
let clamped = text_helpers::clamp_cursor(start, text_content.paragraphs());
|
||||
state.text_editor_state.selection.set_caret(clamped);
|
||||
} else if word_boundary {
|
||||
let cursor = selection.focus;
|
||||
if let Some(new_cursor) = text_helpers::delete_word_before(text_content, &cursor) {
|
||||
state.text_editor_state.selection.set_caret(new_cursor);
|
||||
}
|
||||
} else {
|
||||
let cursor = selection.focus;
|
||||
if let Some(new_cursor) = text_helpers::delete_char_before(text_content, &cursor) {
|
||||
state.text_editor_state.selection.set_caret(new_cursor);
|
||||
}
|
||||
}
|
||||
|
||||
text_content.layout.paragraphs.clear();
|
||||
text_content.layout.paragraph_builders.clear();
|
||||
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
.delete_backward(text_content, word_boundary);
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
}
|
||||
@ -570,36 +549,9 @@ pub extern "C" fn text_editor_delete_forward(word_boundary: bool) {
|
||||
return;
|
||||
};
|
||||
|
||||
let selection = state.text_editor_state.selection;
|
||||
|
||||
if selection.is_selection() {
|
||||
text_helpers::delete_selection_range(text_content, &selection);
|
||||
let start = selection.start();
|
||||
let clamped = text_helpers::clamp_cursor(start, text_content.paragraphs());
|
||||
state.text_editor_state.selection.set_caret(clamped);
|
||||
} else if word_boundary {
|
||||
let cursor = selection.focus;
|
||||
text_helpers::delete_word_after(text_content, &cursor);
|
||||
let clamped = text_helpers::clamp_cursor(cursor, text_content.paragraphs());
|
||||
state.text_editor_state.selection.set_caret(clamped);
|
||||
} else {
|
||||
let cursor = selection.focus;
|
||||
text_helpers::delete_char_after(text_content, &cursor);
|
||||
let clamped = text_helpers::clamp_cursor(cursor, text_content.paragraphs());
|
||||
state.text_editor_state.selection.set_caret(clamped);
|
||||
}
|
||||
|
||||
text_content.layout.paragraphs.clear();
|
||||
text_content.layout.paragraph_builders.clear();
|
||||
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
.delete_forward(text_content, word_boundary);
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
}
|
||||
@ -623,33 +575,7 @@ pub extern "C" fn text_editor_insert_paragraph() {
|
||||
return;
|
||||
};
|
||||
|
||||
let selection = state.text_editor_state.selection;
|
||||
|
||||
if selection.is_selection() {
|
||||
text_helpers::delete_selection_range(text_content, &selection);
|
||||
let start = selection.start();
|
||||
state.text_editor_state.selection.set_caret(start);
|
||||
}
|
||||
|
||||
let cursor = state.text_editor_state.selection.focus;
|
||||
|
||||
if text_helpers::split_paragraph_at_cursor(text_content, &cursor) {
|
||||
let new_cursor =
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0);
|
||||
state.text_editor_state.selection.set_caret(new_cursor);
|
||||
}
|
||||
|
||||
text_content.layout.paragraphs.clear();
|
||||
text_content.layout.paragraph_builders.clear();
|
||||
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
|
||||
state.text_editor_state.insert_paragraph(text_content);
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
}
|
||||
@ -681,63 +607,12 @@ pub extern "C" fn text_editor_move_cursor(
|
||||
return;
|
||||
};
|
||||
|
||||
let paragraphs = text_content.paragraphs();
|
||||
if paragraphs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let current = state.text_editor_state.selection.focus;
|
||||
|
||||
// Get the text direction of the span at the current cursor position
|
||||
let span_text_direction = if current.paragraph < paragraphs.len() {
|
||||
text_helpers::get_span_text_direction_at_offset(
|
||||
¶graphs[current.paragraph],
|
||||
current.offset,
|
||||
)
|
||||
} else {
|
||||
TextDirection::LTR
|
||||
};
|
||||
|
||||
// For horizontal navigation, swap Backward/Forward when in RTL text
|
||||
let adjusted_direction = if span_text_direction == TextDirection::RTL {
|
||||
match direction {
|
||||
CursorDirection::Backward => CursorDirection::Forward,
|
||||
CursorDirection::Forward => CursorDirection::Backward,
|
||||
other => other,
|
||||
}
|
||||
} else {
|
||||
direction
|
||||
};
|
||||
|
||||
let new_cursor = match adjusted_direction {
|
||||
CursorDirection::Backward => {
|
||||
text_helpers::move_cursor_backward(¤t, paragraphs, word_boundary)
|
||||
}
|
||||
CursorDirection::Forward => {
|
||||
text_helpers::move_cursor_forward(¤t, paragraphs, word_boundary)
|
||||
}
|
||||
CursorDirection::LineBefore => {
|
||||
text_helpers::move_cursor_up(¤t, paragraphs, text_content)
|
||||
}
|
||||
CursorDirection::LineAfter => {
|
||||
text_helpers::move_cursor_down(¤t, paragraphs, text_content)
|
||||
}
|
||||
CursorDirection::LineStart => {
|
||||
text_helpers::move_cursor_line_start(¤t, paragraphs)
|
||||
}
|
||||
CursorDirection::LineEnd => text_helpers::move_cursor_line_end(¤t, paragraphs),
|
||||
};
|
||||
|
||||
if extend_selection {
|
||||
state.text_editor_state.selection.extend_to(new_cursor);
|
||||
} else {
|
||||
state.text_editor_state.selection.set_caret(new_cursor);
|
||||
}
|
||||
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::SelectionChanged);
|
||||
state.text_editor_state.move_cursor(
|
||||
text_content,
|
||||
direction,
|
||||
word_boundary,
|
||||
extend_selection,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@ -779,6 +654,171 @@ pub extern "C" fn text_editor_get_cursor_rect() -> *mut u8 {
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_get_current_styles() -> *mut u8 {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.has_focus {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
|
||||
let Type::Text(_text_content) = &shape.shape_type else {
|
||||
return std::ptr::null_mut();
|
||||
};
|
||||
|
||||
let styles = &state.text_editor_state.current_styles;
|
||||
|
||||
let vertical_align = match styles.vertical_align {
|
||||
VerticalAlign::Top => 0_u32,
|
||||
VerticalAlign::Center => 1_u32,
|
||||
VerticalAlign::Bottom => 2_u32,
|
||||
};
|
||||
|
||||
let text_align = styles
|
||||
.text_align
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| match value {
|
||||
TextAlign::Left => RawTextAlign::Left as u32,
|
||||
TextAlign::Start => RawTextAlign::Left as u32,
|
||||
TextAlign::Center => RawTextAlign::Center as u32,
|
||||
TextAlign::Right => RawTextAlign::Right as u32,
|
||||
TextAlign::End => RawTextAlign::Right as u32,
|
||||
TextAlign::Justify => RawTextAlign::Justify as u32,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let text_direction = styles
|
||||
.text_direction
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| match value {
|
||||
skia_safe::textlayout::TextDirection::LTR => RawTextDirection::Ltr as u32,
|
||||
skia_safe::textlayout::TextDirection::RTL => RawTextDirection::Rtl as u32,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let text_decoration = styles
|
||||
.text_decoration
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| {
|
||||
if *value == skia_safe::textlayout::TextDecoration::UNDERLINE {
|
||||
RawTextDecoration::Underline as u32
|
||||
} else if *value == skia_safe::textlayout::TextDecoration::LINE_THROUGH {
|
||||
RawTextDecoration::LineThrough as u32
|
||||
} else if *value == skia_safe::textlayout::TextDecoration::OVERLINE {
|
||||
RawTextDecoration::Overline as u32
|
||||
} else {
|
||||
RawTextDecoration::None as u32
|
||||
}
|
||||
})
|
||||
.unwrap_or(RawTextDecoration::None as u32);
|
||||
|
||||
let text_transform = styles
|
||||
.text_transform
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| match value {
|
||||
crate::shapes::TextTransform::Uppercase => RawTextTransform::Uppercase as u32,
|
||||
crate::shapes::TextTransform::Lowercase => RawTextTransform::Lowercase as u32,
|
||||
crate::shapes::TextTransform::Capitalize => RawTextTransform::Capitalize as u32,
|
||||
})
|
||||
.unwrap_or(RawTextTransform::None as u32);
|
||||
|
||||
let font_family_id = styles
|
||||
.font_family
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| {
|
||||
let (a, b, c, d) = uuid_to_u32_quartet(&value.id());
|
||||
[a, b, c, d]
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let font_family_weight = styles
|
||||
.font_family
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| value.weight())
|
||||
.unwrap_or_default();
|
||||
|
||||
let font_family_style = styles
|
||||
.font_family
|
||||
.value()
|
||||
.as_ref()
|
||||
.map(|value| value.style() as u32)
|
||||
.unwrap_or_default();
|
||||
|
||||
let font_size = styles.font_size.value().unwrap_or(0.0);
|
||||
let font_weight = styles.font_weight.value().unwrap_or(0);
|
||||
let line_height = styles.line_height.value().unwrap_or(0.0);
|
||||
let letter_spacing = styles.letter_spacing.value().unwrap_or(0.0);
|
||||
|
||||
let mut font_variant_id = [0_u32; 4];
|
||||
if let Some(value) = styles.font_variant_id.value().as_ref() {
|
||||
let (a, b, c, d) = uuid_to_u32_quartet(value);
|
||||
font_variant_id = [a, b, c, d];
|
||||
}
|
||||
|
||||
let mut fill_bytes = Vec::new();
|
||||
let mut fill_count: u32 = 0;
|
||||
for fill in &styles.fills {
|
||||
if let Ok(raw_fill) = RawFillData::try_from(fill) {
|
||||
fill_bytes
|
||||
.extend_from_slice(&<[u8; std::mem::size_of::<RawFillData>()]>::from(raw_fill));
|
||||
fill_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Layout: 48-byte fixed header + fixed values + serialized fills.
|
||||
let mut bytes = Vec::with_capacity(132 + fill_bytes.len());
|
||||
|
||||
bytes.extend_from_slice(&vertical_align.to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.text_align.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.text_direction.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.text_decoration.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.text_transform.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.font_family.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.font_size.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.font_weight.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.font_variant_id.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.line_height.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&(*styles.letter_spacing.state() as u32).to_le_bytes());
|
||||
bytes.extend_from_slice(&fill_count.to_le_bytes());
|
||||
|
||||
// Value section.
|
||||
bytes.extend_from_slice(&text_align.to_le_bytes());
|
||||
bytes.extend_from_slice(&text_direction.to_le_bytes());
|
||||
bytes.extend_from_slice(&text_decoration.to_le_bytes());
|
||||
bytes.extend_from_slice(&text_transform.to_le_bytes());
|
||||
bytes.extend_from_slice(&font_family_id[0].to_le_bytes());
|
||||
bytes.extend_from_slice(&font_family_id[1].to_le_bytes());
|
||||
bytes.extend_from_slice(&font_family_id[2].to_le_bytes());
|
||||
bytes.extend_from_slice(&font_family_id[3].to_le_bytes());
|
||||
bytes.extend_from_slice(&font_family_style.to_le_bytes());
|
||||
bytes.extend_from_slice(&font_family_weight.to_le_bytes());
|
||||
bytes.extend_from_slice(&font_size.to_le_bytes());
|
||||
bytes.extend_from_slice(&font_weight.to_le_bytes());
|
||||
bytes.extend_from_slice(&font_variant_id[0].to_le_bytes());
|
||||
bytes.extend_from_slice(&font_variant_id[1].to_le_bytes());
|
||||
bytes.extend_from_slice(&font_variant_id[2].to_le_bytes());
|
||||
bytes.extend_from_slice(&font_variant_id[3].to_le_bytes());
|
||||
bytes.extend_from_slice(&line_height.to_le_bytes());
|
||||
bytes.extend_from_slice(&letter_spacing.to_le_bytes());
|
||||
bytes.extend_from_slice(&fill_bytes);
|
||||
|
||||
mem::write_bytes(bytes)
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_get_selection_rects() -> *mut u8 {
|
||||
with_state_mut!(state, {
|
||||
@ -804,7 +844,6 @@ pub extern "C" fn text_editor_get_selection_rects() -> *mut u8 {
|
||||
|
||||
let selection = &state.text_editor_state.selection;
|
||||
let rects = get_selection_rects(text_content, selection, shape);
|
||||
|
||||
if rects.is_empty() {
|
||||
return std::ptr::null_mut();
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user