mirror of
https://github.com/penpot/penpot.git
synced 2026-08-01 19:06:18 +00:00
Merge pull request #8492 from penpot/azazeln28-fix-text-editor-initialization
♻️ Refactor Text Editor v3
This commit is contained in:
commit
52b4e803ff
314
frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs
Normal file
314
frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
;; 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.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]))
|
||||||
|
|
||||||
|
(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)
|
||||||
|
|
||||||
|
[{: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]
|
||||||
|
(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)
|
||||||
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
|
(.blur node)))
|
||||||
|
|
||||||
|
;; 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]
|
||||||
|
(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]
|
||||||
|
(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]
|
||||||
|
(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]
|
||||||
|
(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]
|
||||||
|
(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]
|
||||||
|
(wasm.api/text-editor-start shape-id)))
|
||||||
|
|
||||||
|
on-blur
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [^js _event]
|
||||||
|
(sync-wasm-text-editor-content! {:finalize? true})
|
||||||
|
(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)]
|
||||||
|
(.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"}]]]]))
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
.text-editor {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-editor-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
@ -19,13 +19,11 @@
|
|||||||
[app.main.data.workspace.media :as dwm]
|
[app.main.data.workspace.media :as dwm]
|
||||||
[app.main.data.workspace.path :as dwdp]
|
[app.main.data.workspace.path :as dwdp]
|
||||||
[app.main.data.workspace.specialized-panel :as-alias dwsp]
|
[app.main.data.workspace.specialized-panel :as-alias dwsp]
|
||||||
[app.main.features :as features]
|
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.workspace.sidebar.assets.components :as wsac]
|
[app.main.ui.workspace.sidebar.assets.components :as wsac]
|
||||||
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
|
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
|
||||||
[app.render-wasm.api :as wasm.api]
|
[app.render-wasm.api :as wasm.api]
|
||||||
[app.render-wasm.wasm :as wasm.wasm]
|
|
||||||
[app.util.dom :as dom]
|
[app.util.dom :as dom]
|
||||||
[app.util.dom.dnd :as dnd]
|
[app.util.dom.dnd :as dnd]
|
||||||
[app.util.dom.normalize-wheel :as nw]
|
[app.util.dom.normalize-wheel :as nw]
|
||||||
@ -74,7 +72,6 @@
|
|||||||
shift? (kbd/shift? native-event)
|
shift? (kbd/shift? native-event)
|
||||||
alt? (kbd/alt? native-event)
|
alt? (kbd/alt? native-event)
|
||||||
mod? (kbd/mod? native-event)
|
mod? (kbd/mod? native-event)
|
||||||
off-pt (dom/get-offset-position native-event)
|
|
||||||
|
|
||||||
left-click? (and (not panning) (dom/left-mouse? event))
|
left-click? (and (not panning) (dom/left-mouse? event))
|
||||||
middle-click? (and (not panning) (dom/middle-mouse? event))]
|
middle-click? (and (not panning) (dom/middle-mouse? event))]
|
||||||
@ -94,23 +91,8 @@
|
|||||||
(st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?)
|
(st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?)
|
||||||
::dwsp/interrupt)
|
::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?))
|
(when (and (not= edition id) (or text-editing? grid-editing?))
|
||||||
(st/emit! (dw/clear-edition-mode))
|
(st/emit! (dw/clear-edition-mode)))
|
||||||
;; FIXME: I think this is not completely correct because this
|
|
||||||
;; is going to happen even when clicking or selecting text.
|
|
||||||
;; Sync and stop WASM text editor when exiting edit mode
|
|
||||||
#_(when (and text-editing?
|
|
||||||
(features/active-feature? @st/state "render-wasm/v1")
|
|
||||||
wasm.wasm/context-initialized?)
|
|
||||||
(when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)]
|
|
||||||
(st/emit! (dwt/v2-update-text-shape-content
|
|
||||||
shape-id content
|
|
||||||
:update-name? true
|
|
||||||
:finalize? true)))
|
|
||||||
(wasm.api/text-editor-stop)))
|
|
||||||
|
|
||||||
(when (and (not text-editing?)
|
(when (and (not text-editing?)
|
||||||
(not blocked)
|
(not blocked)
|
||||||
@ -192,8 +174,6 @@
|
|||||||
alt? (kbd/alt? event)
|
alt? (kbd/alt? event)
|
||||||
meta? (kbd/meta? event)
|
meta? (kbd/meta? event)
|
||||||
hovering? (some? @hover)
|
hovering? (some? @hover)
|
||||||
native-event (dom/event->native-event event)
|
|
||||||
off-pt (dom/get-offset-position native-event)
|
|
||||||
raw-pt (dom/get-client-position event)
|
raw-pt (dom/get-client-position event)
|
||||||
pt (uwvv/point->viewport raw-pt)]
|
pt (uwvv/point->viewport raw-pt)]
|
||||||
(st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?))
|
(st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?))
|
||||||
@ -207,20 +187,6 @@
|
|||||||
(not drawing-tool))
|
(not drawing-tool))
|
||||||
(st/emit! (dw/select-shape (:id @hover) shift?)))
|
(st/emit! (dw/select-shape (:id @hover) shift?)))
|
||||||
|
|
||||||
;; FIXME: Maybe we can move into a function of the kind
|
|
||||||
;; "text-editor-on-click"
|
|
||||||
;; If clicking on a text shape and wasm render is enabled, forward cursor position
|
|
||||||
(when (and hovering?
|
|
||||||
(not @space?)
|
|
||||||
edition ;; Only when already in edit mode
|
|
||||||
(not drawing-path?)
|
|
||||||
(not drawing-tool))
|
|
||||||
(let [hover-shape @hover]
|
|
||||||
(when (and (= :text (:type hover-shape))
|
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
|
||||||
wasm.wasm/context-initialized?)
|
|
||||||
(wasm.api/text-editor-set-cursor-from-point (.-x off-pt) (.-y off-pt)))))
|
|
||||||
|
|
||||||
(when (and @z?
|
(when (and @z?
|
||||||
(not @space?)
|
(not @space?)
|
||||||
(not edition)
|
(not edition)
|
||||||
@ -262,19 +228,7 @@
|
|||||||
(and editable? (not= id edition) (not read-only?))
|
(and editable? (not= id edition) (not read-only?))
|
||||||
(do
|
(do
|
||||||
(st/emit! (dw/select-shape id)
|
(st/emit! (dw/select-shape id)
|
||||||
(dw/start-editing-selected))
|
(dw/start-editing-selected)))
|
||||||
;; If using wasm text-editor, notify WASM to start editing this shape
|
|
||||||
;; and set cursor position from the double-click location
|
|
||||||
(when (and (= type :text)
|
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
|
||||||
wasm.wasm/context-initialized?)
|
|
||||||
(wasm.api/text-editor-start id)))
|
|
||||||
|
|
||||||
(and editable? (= id edition) (not read-only?)
|
|
||||||
(= type :text)
|
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
|
||||||
wasm.wasm/context-initialized?)
|
|
||||||
(wasm.api/text-editor-select-all)
|
|
||||||
|
|
||||||
(some? selected-shape)
|
(some? selected-shape)
|
||||||
(do
|
(do
|
||||||
|
|||||||
@ -30,6 +30,7 @@
|
|||||||
[app.main.ui.workspace.shapes.text.editor :as editor-v1]
|
[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.text-edition-outline :refer [text-edition-outline]]
|
||||||
[app.main.ui.workspace.shapes.text.v2-editor :as editor-v2]
|
[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.top-toolbar :refer [top-toolbar*]]
|
||||||
[app.main.ui.workspace.viewport.actions :as actions]
|
[app.main.ui.workspace.viewport.actions :as actions]
|
||||||
[app.main.ui.workspace.viewport.comments :as comments]
|
[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.viewport-ref :refer [create-viewport-ref]]
|
||||||
[app.main.ui.workspace.viewport.widgets :as widgets]
|
[app.main.ui.workspace.viewport.widgets :as widgets]
|
||||||
[app.render-wasm.api :as wasm.api]
|
[app.render-wasm.api :as wasm.api]
|
||||||
[app.render-wasm.text-editor-input :refer [text-editor-input]]
|
|
||||||
[app.util.debug :as dbg]
|
[app.util.debug :as dbg]
|
||||||
[app.util.text-editor :as ted]
|
[app.util.text-editor :as ted]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
@ -417,14 +417,7 @@
|
|||||||
|
|
||||||
(when picking-color?
|
(when picking-color?
|
||||||
[:> pixel-overlay/pixel-overlay-wasm* {:viewport-ref viewport-ref
|
[:> pixel-overlay/pixel-overlay-wasm* {:viewport-ref viewport-ref
|
||||||
:canvas-ref canvas-ref}])
|
:canvas-ref canvas-ref}])]
|
||||||
|
|
||||||
;; WASM text editor contenteditable (must be outside SVG to work)
|
|
||||||
(when (and show-text-editor?
|
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1"))
|
|
||||||
[:& text-editor-input {:shape editing-shape
|
|
||||||
:zoom zoom
|
|
||||||
:vbox vbox}])]
|
|
||||||
|
|
||||||
[:canvas {:id "render"
|
[:canvas {:id "render"
|
||||||
:data-testid "canvas-wasm-shapes"
|
:data-testid "canvas-wasm-shapes"
|
||||||
@ -471,14 +464,20 @@
|
|||||||
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
[:g {:style {:pointer-events (if disable-events? "none" "auto")}}
|
||||||
;; Text editor handling:
|
;; Text editor handling:
|
||||||
;; - When text-editor-wasm/v1 is active, contenteditable is rendered in viewport-overlays (HTML DOM)
|
;; - When text-editor-wasm/v1 is active, contenteditable is rendered in viewport-overlays (HTML DOM)
|
||||||
(when (and show-text-editor?
|
(when show-text-editor?
|
||||||
(not (features/active-feature? @st/state "text-editor-wasm/v1")))
|
(cond
|
||||||
(if (features/active-feature? @st/state "text-editor/v2")
|
(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
|
[:& editor-v2/text-editor {:shape editing-shape
|
||||||
:canvas-ref canvas-ref
|
:canvas-ref canvas-ref
|
||||||
:ref text-editor-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?
|
(when show-frame-outline?
|
||||||
(let [outlined-frame-id
|
(let [outlined-frame-id
|
||||||
|
|||||||
@ -86,6 +86,7 @@
|
|||||||
;; Re-export public text editor functions
|
;; Re-export public text editor functions
|
||||||
(def text-editor-start text-editor/text-editor-start)
|
(def text-editor-start text-editor/text-editor-start)
|
||||||
(def text-editor-stop text-editor/text-editor-stop)
|
(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-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-down text-editor/text-editor-pointer-down)
|
||||||
(def text-editor-pointer-move text-editor/text-editor-pointer-move)
|
(def text-editor-pointer-move text-editor/text-editor-pointer-move)
|
||||||
|
|||||||
@ -16,13 +16,21 @@
|
|||||||
[id]
|
[id]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(let [buffer (uuid/get-u32 id)]
|
(let [buffer (uuid/get-u32 id)]
|
||||||
(h/call wasm/internal-module "_text_editor_start"
|
(when-not (h/call wasm/internal-module "_text_editor_start"
|
||||||
(aget buffer 0)
|
(aget buffer 0)
|
||||||
(aget buffer 1)
|
(aget buffer 1)
|
||||||
(aget buffer 2)
|
(aget buffer 2)
|
||||||
(aget buffer 3)))))
|
(aget buffer 3))
|
||||||
|
(throw (js/Error. "TextEditor initialization failed"))))))
|
||||||
|
|
||||||
|
(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
|
(defn text-editor-set-cursor-from-point
|
||||||
|
"Sets caret position from screen (canvas) coordinates"
|
||||||
[x y]
|
[x y]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
|
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
|
||||||
@ -95,7 +103,8 @@
|
|||||||
(defn text-editor-stop
|
(defn text-editor-stop
|
||||||
[]
|
[]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(h/call wasm/internal-module "_text_editor_stop")))
|
(when-not (h/call wasm/internal-module "_text_editor_stop")
|
||||||
|
(throw (js/Error. "TextEditor finalization failed")))))
|
||||||
|
|
||||||
(defn text-editor-is-active?
|
(defn text-editor-is-active?
|
||||||
([id]
|
([id]
|
||||||
@ -160,6 +169,7 @@
|
|||||||
(finally
|
(finally
|
||||||
(mem/free))))))
|
(mem/free))))))
|
||||||
|
|
||||||
|
;; This is used as a intermediate cache between Clojure global state and WASM state.
|
||||||
(def ^:private shape-text-contents (atom {}))
|
(def ^:private shape-text-contents (atom {}))
|
||||||
|
|
||||||
(defn- merge-exported-texts-into-content
|
(defn- merge-exported-texts-into-content
|
||||||
|
|||||||
@ -1,241 +0,0 @@
|
|||||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
||||||
;;
|
|
||||||
;; Copyright (c) KALEIDOS INC
|
|
||||||
|
|
||||||
(ns app.render-wasm.text-editor-input
|
|
||||||
"Contenteditable DOM element for WASM text editor input"
|
|
||||||
(:require
|
|
||||||
[app.common.geom.shapes :as gsh]
|
|
||||||
[app.main.data.workspace.texts :as dwt]
|
|
||||||
[app.main.store :as st]
|
|
||||||
[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]
|
|
||||||
[goog.events :as events]
|
|
||||||
[rumext.v2 :as mf])
|
|
||||||
(:import goog.events.EventType))
|
|
||||||
|
|
||||||
(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?))))
|
|
||||||
|
|
||||||
(mf/defc text-editor-input
|
|
||||||
"Contenteditable element positioned over the text shape to capture input events."
|
|
||||||
{::mf/wrap-props false}
|
|
||||||
[props]
|
|
||||||
(let [shape (obj/get props "shape")
|
|
||||||
zoom (obj/get props "zoom")
|
|
||||||
vbox (obj/get props "vbox")
|
|
||||||
|
|
||||||
contenteditable-ref (mf/use-ref nil)
|
|
||||||
composing? (mf/use-state false)
|
|
||||||
|
|
||||||
;; Calculate screen position from shape bounds
|
|
||||||
shape-bounds (gsh/shape->rect shape)
|
|
||||||
screen-x (* (- (:x shape-bounds) (:x vbox)) zoom)
|
|
||||||
screen-y (* (- (:y shape-bounds) (:y vbox)) zoom)
|
|
||||||
screen-w (* (:width shape-bounds) zoom)
|
|
||||||
screen-h (* (:height shape-bounds) zoom)]
|
|
||||||
|
|
||||||
;; Focus contenteditable on mount
|
|
||||||
(mf/use-effect
|
|
||||||
(fn []
|
|
||||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
|
||||||
(.focus node))
|
|
||||||
js/undefined))
|
|
||||||
|
|
||||||
(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))))))
|
|
||||||
|
|
||||||
;; Document-level keydown handler for control keys
|
|
||||||
(mf/use-effect
|
|
||||||
(fn []
|
|
||||||
(let [on-doc-keydown
|
|
||||||
(fn [e]
|
|
||||||
(when (and (text-editor/text-editor-is-active?)
|
|
||||||
(not @composing?))
|
|
||||||
(let [key (.-key e)
|
|
||||||
ctrl? (or (.-ctrlKey e) (.-metaKey e))
|
|
||||||
shift? (.-shiftKey e)]
|
|
||||||
(cond
|
|
||||||
;; Escape: finalize and stop
|
|
||||||
(= key "Escape")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(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 e)
|
|
||||||
(text-editor/text-editor-select-all)
|
|
||||||
(wasm.api/request-render "text-select-all"))
|
|
||||||
|
|
||||||
;; Enter
|
|
||||||
(= key "Enter")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-insert-paragraph)
|
|
||||||
(sync-wasm-text-editor-content!)
|
|
||||||
(wasm.api/request-render "text-paragraph"))
|
|
||||||
|
|
||||||
;; Backspace
|
|
||||||
(= key "Backspace")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(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 e)
|
|
||||||
(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 e)
|
|
||||||
(text-editor/text-editor-move-cursor 0 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "ArrowRight")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 1 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "ArrowUp")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 2 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "ArrowDown")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 3 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "Home")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(text-editor/text-editor-move-cursor 4 shift?)
|
|
||||||
(wasm.api/request-render "text-cursor-move"))
|
|
||||||
|
|
||||||
(= key "End")
|
|
||||||
(do
|
|
||||||
(dom/prevent-default e)
|
|
||||||
(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))))]
|
|
||||||
(events/listen js/document EventType.KEYDOWN on-doc-keydown true)
|
|
||||||
(fn []
|
|
||||||
(events/unlisten js/document EventType.KEYDOWN on-doc-keydown true)))))
|
|
||||||
|
|
||||||
;; Composition and input events
|
|
||||||
(let [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-input
|
|
||||||
(mf/use-fn
|
|
||||||
(fn [^js 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) ""))))))]
|
|
||||||
|
|
||||||
[:div
|
|
||||||
{:ref contenteditable-ref
|
|
||||||
:contentEditable true
|
|
||||||
:suppressContentEditableWarning true
|
|
||||||
:on-composition-start on-composition-start
|
|
||||||
:on-composition-end on-composition-end
|
|
||||||
:on-input on-input
|
|
||||||
:on-paste on-paste
|
|
||||||
:on-copy on-copy
|
|
||||||
;; FIXME on-click
|
|
||||||
;; :on-click on-click
|
|
||||||
:id "text-editor-wasm-input"
|
|
||||||
;; FIXME
|
|
||||||
:style {:position "absolute"
|
|
||||||
:left (str screen-x "px")
|
|
||||||
:top (str screen-y "px")
|
|
||||||
:width (str screen-w "px")
|
|
||||||
:height (str screen-h "px")
|
|
||||||
:opacity 0
|
|
||||||
:overflow "hidden"
|
|
||||||
:white-space "pre"
|
|
||||||
:cursor "text"
|
|
||||||
:z-index 10}}])))
|
|
||||||
@ -161,6 +161,13 @@ impl TextPositionWithAffinity {
|
|||||||
offset,
|
offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.position_with_affinity.position = 0;
|
||||||
|
self.position_with_affinity.affinity = Affinity::Downstream;
|
||||||
|
self.paragraph = 0;
|
||||||
|
self.offset = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
|||||||
@ -33,6 +33,11 @@ impl TextSelection {
|
|||||||
!self.is_collapsed()
|
!self.is_collapsed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.anchor.reset();
|
||||||
|
self.focus.reset();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_caret(&mut self, cursor: TextPositionWithAffinity) {
|
pub fn set_caret(&mut self, cursor: TextPositionWithAffinity) {
|
||||||
self.anchor = cursor;
|
self.anchor = cursor;
|
||||||
self.focus = cursor;
|
self.focus = cursor;
|
||||||
@ -133,7 +138,7 @@ impl TextEditorState {
|
|||||||
self.active_shape_id = Some(shape_id);
|
self.active_shape_id = Some(shape_id);
|
||||||
self.cursor_visible = true;
|
self.cursor_visible = true;
|
||||||
self.last_blink_time = 0.0;
|
self.last_blink_time = 0.0;
|
||||||
self.selection = TextSelection::new();
|
self.selection.reset();
|
||||||
self.is_pointer_selection_active = false;
|
self.is_pointer_selection_active = false;
|
||||||
self.pending_events.clear();
|
self.pending_events.clear();
|
||||||
}
|
}
|
||||||
@ -142,9 +147,10 @@ impl TextEditorState {
|
|||||||
self.is_active = false;
|
self.is_active = false;
|
||||||
self.active_shape_id = None;
|
self.active_shape_id = None;
|
||||||
self.cursor_visible = false;
|
self.cursor_visible = false;
|
||||||
|
self.last_blink_time = 0.0;
|
||||||
|
self.selection.reset();
|
||||||
self.is_pointer_selection_active = false;
|
self.is_pointer_selection_active = false;
|
||||||
self.pending_events.clear();
|
self.pending_events.clear();
|
||||||
self.reset_blink();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_pointer_selection(&mut self) -> bool {
|
pub fn start_pointer_selection(&mut self) -> bool {
|
||||||
@ -195,13 +201,11 @@ impl TextEditorState {
|
|||||||
|
|
||||||
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||||
self.selection.set_caret(*position);
|
self.selection.set_caret(*position);
|
||||||
self.reset_blink();
|
|
||||||
self.push_event(TextEditorEvent::SelectionChanged);
|
self.push_event(TextEditorEvent::SelectionChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||||
self.selection.extend_to(*position);
|
self.selection.extend_to(*position);
|
||||||
self.reset_blink();
|
|
||||||
self.push_event(TextEditorEvent::SelectionChanged);
|
self.push_event(TextEditorEvent::SelectionChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,10 +42,14 @@ pub extern "C" fn text_editor_start(a: u32, b: u32, c: u32, d: u32) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_stop() {
|
pub extern "C" fn text_editor_stop() -> bool {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
state.text_editor_state.stop();
|
state.text_editor_state.stop();
|
||||||
});
|
true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@ -126,12 +130,8 @@ pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let point = Point::new(x, y);
|
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();
|
state.text_editor_state.start_pointer_selection();
|
||||||
if let Some(position) =
|
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
|
||||||
{
|
|
||||||
state.text_editor_state.set_caret_from_position(&position);
|
state.text_editor_state.set_caret_from_position(&position);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -143,7 +143,6 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
|||||||
if !state.text_editor_state.is_active {
|
if !state.text_editor_state.is_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
|
||||||
let point = Point::new(x, y);
|
let point = Point::new(x, y);
|
||||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
return;
|
return;
|
||||||
@ -151,11 +150,6 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
|||||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
return;
|
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 {
|
if !state.text_editor_state.is_pointer_selection_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -163,9 +157,7 @@ pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(position) =
|
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
|
||||||
{
|
|
||||||
state
|
state
|
||||||
.text_editor_state
|
.text_editor_state
|
||||||
.extend_selection_from_position(&position);
|
.extend_selection_from_position(&position);
|
||||||
@ -179,7 +171,6 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
|||||||
if !state.text_editor_state.is_active {
|
if !state.text_editor_state.is_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
|
||||||
let point = Point::new(x, y);
|
let point = Point::new(x, y);
|
||||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
return;
|
return;
|
||||||
@ -187,20 +178,13 @@ pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
|||||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
return;
|
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 {
|
if !state.text_editor_state.is_pointer_selection_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Type::Text(text_content) = &shape.shape_type else {
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if let Some(position) =
|
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
|
||||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
|
||||||
{
|
|
||||||
state
|
state
|
||||||
.text_editor_state
|
.text_editor_state
|
||||||
.extend_selection_from_position(&position);
|
.extend_selection_from_position(&position);
|
||||||
@ -209,6 +193,29 @@ 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]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user