mirror of
https://github.com/penpot/penpot.git
synced 2026-02-24 10:47:49 -05:00
Compare commits
2 Commits
azazeln28-
...
superalex-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbb5c91acd | ||
|
|
5eba070a2c |
@@ -19,6 +19,7 @@
|
||||
[app.main.data.workspace.media :as dwm]
|
||||
[app.main.data.workspace.path :as dwdp]
|
||||
[app.main.data.workspace.specialized-panel :as-alias dwsp]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
@@ -49,42 +50,41 @@
|
||||
(mf/deps id blocked hidden type selected edition drawing-tool text-editing?
|
||||
node-editing? grid-editing? drawing-path? create-comment? @z? @space?
|
||||
panning read-only?)
|
||||
(fn [event]
|
||||
(fn [bevent]
|
||||
;; We need to handle editor related stuff here because
|
||||
;; handling on editor dom node does not works properly.
|
||||
(let [target (dom/get-target event)
|
||||
(let [target (dom/get-target bevent)
|
||||
editor (txu/closest-text-editor-content target)]
|
||||
;; Capture mouse pointer to detect the movements even if cursor
|
||||
;; leaves the viewport or the browser itself
|
||||
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
|
||||
(if editor
|
||||
(.setPointerCapture editor (.-pointerId event))
|
||||
(.setPointerCapture target (.-pointerId event))))
|
||||
(.setPointerCapture editor (.-pointerId bevent))
|
||||
(.setPointerCapture target (.-pointerId bevent))))
|
||||
|
||||
(when (or (dom/class? (dom/get-target event) "viewport-controls")
|
||||
(dom/class? (dom/get-target event) "viewport-selrect")
|
||||
(dom/child? (dom/get-target event) (dom/query ".grid-layout-editor")))
|
||||
(when (or (dom/class? (dom/get-target bevent) "viewport-controls")
|
||||
(dom/class? (dom/get-target bevent) "viewport-selrect")
|
||||
(dom/child? (dom/get-target bevent) (dom/query ".grid-layout-editor")))
|
||||
|
||||
(dom/stop-propagation event)
|
||||
(dom/stop-propagation bevent)
|
||||
|
||||
(when-not @z?
|
||||
(let [native-event (dom/event->native-event event)
|
||||
ctrl? (kbd/ctrl? native-event)
|
||||
meta? (kbd/meta? native-event)
|
||||
shift? (kbd/shift? native-event)
|
||||
alt? (kbd/alt? native-event)
|
||||
mod? (kbd/mod? native-event)
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
(let [event (dom/event->native-event bevent)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
meta? (kbd/meta? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
mod? (kbd/mod? event)
|
||||
|
||||
left-click? (and (not panning) (dom/left-mouse? event))
|
||||
middle-click? (and (not panning) (dom/middle-mouse? event))]
|
||||
left-click? (and (not panning) (dom/left-mouse? bevent))
|
||||
middle-click? (and (not panning) (dom/middle-mouse? bevent))]
|
||||
|
||||
(cond
|
||||
(or middle-click? (and left-click? @space?))
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(dom/prevent-default bevent)
|
||||
(if mod?
|
||||
(let [raw-pt (dom/get-client-position native-event)
|
||||
(let [raw-pt (dom/get-client-position event)
|
||||
pt (uwvv/point->viewport raw-pt)]
|
||||
(st/emit! (dw/start-zooming pt)))
|
||||
(st/emit! (dw/start-panning))))
|
||||
@@ -94,23 +94,18 @@
|
||||
(st/emit! (mse/->MouseEvent :down ctrl? shift? alt? meta?)
|
||||
::dwsp/interrupt)
|
||||
|
||||
(when (wasm.api/text-editor-is-active?)
|
||||
(wasm.api/text-editor-pointer-down (.-x off-pt) (.-y off-pt)))
|
||||
|
||||
(when (and (not= edition id) (or text-editing? grid-editing?))
|
||||
(st/emit! (dw/clear-edition-mode))
|
||||
;; FIXME: I think this is not completely correct because this
|
||||
;; is going to happen even when clicking or selecting text.
|
||||
;; Sync and stop WASM text editor when exiting edit mode
|
||||
#_(when (and text-editing?
|
||||
(features/active-feature? @st/state "render-wasm/v1")
|
||||
wasm.wasm/context-initialized?)
|
||||
(when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)]
|
||||
(st/emit! (dwt/v2-update-text-shape-content
|
||||
shape-id content
|
||||
:update-name? true
|
||||
:finalize? true)))
|
||||
(wasm.api/text-editor-stop)))
|
||||
(when (and text-editing?
|
||||
(features/active-feature? @st/state "render-wasm/v1")
|
||||
wasm.wasm/context-initialized?)
|
||||
(when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)]
|
||||
(st/emit! (dwt/v2-update-text-shape-content
|
||||
shape-id content
|
||||
:update-name? true
|
||||
:finalize? true)))
|
||||
(wasm.api/text-editor-stop)))
|
||||
|
||||
(when (and (not text-editing?)
|
||||
(not blocked)
|
||||
@@ -192,14 +187,10 @@
|
||||
alt? (kbd/alt? event)
|
||||
meta? (kbd/meta? event)
|
||||
hovering? (some? @hover)
|
||||
native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
raw-pt (dom/get-client-position event)
|
||||
pt (uwvv/point->viewport raw-pt)]
|
||||
(st/emit! (mse/->MouseEvent :click ctrl? shift? alt? meta?))
|
||||
|
||||
;; FIXME: Maybe we can transform this into a cond instead
|
||||
;; of multiple (when)s.
|
||||
(when (and hovering?
|
||||
(not @space?)
|
||||
(not edition)
|
||||
@@ -207,8 +198,6 @@
|
||||
(not drawing-tool))
|
||||
(st/emit! (dw/select-shape (:id @hover) shift?)))
|
||||
|
||||
;; FIXME: Maybe we can move into a function of the kind
|
||||
;; "text-editor-on-click"
|
||||
;; If clicking on a text shape and wasm render is enabled, forward cursor position
|
||||
(when (and hovering?
|
||||
(not @space?)
|
||||
@@ -219,7 +208,9 @@
|
||||
(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)))))
|
||||
(let [raw-pt (dom/get-client-position event)]
|
||||
;; FIXME
|
||||
(wasm.api/text-editor-set-cursor-from-point (.-x raw-pt) (.-y raw-pt))))))
|
||||
|
||||
(when (and @z?
|
||||
(not @space?)
|
||||
@@ -270,12 +261,6 @@
|
||||
wasm.wasm/context-initialized?)
|
||||
(wasm.api/text-editor-start id)))
|
||||
|
||||
(and editable? (= id edition) (not read-only?)
|
||||
(= type :text)
|
||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||
wasm.wasm/context-initialized?)
|
||||
(wasm.api/text-editor-select-all)
|
||||
|
||||
(some? selected-shape)
|
||||
(do
|
||||
(reset! hover selected-shape)
|
||||
@@ -325,24 +310,20 @@
|
||||
;; Release pointer on mouse up
|
||||
(.releasePointerCapture target (.-pointerId event)))
|
||||
|
||||
(let [native-event (dom/event->native-event event)
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
ctrl? (kbd/ctrl? native-event)
|
||||
shift? (kbd/shift? native-event)
|
||||
alt? (kbd/alt? native-event)
|
||||
meta? (kbd/meta? native-event)
|
||||
(let [event (dom/event->native-event event)
|
||||
ctrl? (kbd/ctrl? event)
|
||||
shift? (kbd/shift? event)
|
||||
alt? (kbd/alt? event)
|
||||
meta? (kbd/meta? event)
|
||||
|
||||
left-click? (= 1 (.-which native-event))
|
||||
middle-click? (= 2 (.-which native-event))]
|
||||
left-click? (= 1 (.-which event))
|
||||
middle-click? (= 2 (.-which event))]
|
||||
|
||||
(when left-click?
|
||||
(st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?))
|
||||
|
||||
(when (wasm.api/text-editor-is-active?)
|
||||
(wasm.api/text-editor-pointer-up (.-x off-pt) (.-y off-pt))))
|
||||
(st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)))
|
||||
|
||||
(when middle-click?
|
||||
(dom/prevent-default native-event)
|
||||
(dom/prevent-default event)
|
||||
|
||||
;; We store this so in Firefox the middle button won't do a paste of the content
|
||||
(mf/set-ref-val! disable-paste-ref true)
|
||||
@@ -400,9 +381,7 @@
|
||||
(let [last-position (mf/use-var nil)]
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [native-event (unchecked-get event "nativeEvent")
|
||||
off-pt (dom/get-offset-position native-event)
|
||||
raw-pt (dom/get-client-position event)
|
||||
(let [raw-pt (dom/get-client-position event)
|
||||
pt (uwvv/point->viewport raw-pt)
|
||||
|
||||
;; We calculate the delta because Safari's MouseEvent.movementX/Y drop
|
||||
@@ -411,12 +390,6 @@
|
||||
(gpt/subtract raw-pt @last-position)
|
||||
(gpt/point 0 0))]
|
||||
|
||||
;; IMPORTANT! This function, right now it's called on EVERY pointermove. I think
|
||||
;; in the future (when we handle the UI in the render) should be better to
|
||||
;; have a "wasm.api/pointer-move" function that works as an entry point for
|
||||
;; all the pointer-move events.
|
||||
(wasm.api/text-editor-pointer-move (.-x off-pt) (.-y off-pt))
|
||||
|
||||
(rx/push! move-stream pt)
|
||||
(reset! last-position raw-pt)
|
||||
(st/emit! (mse/->PointerEvent :delta delta
|
||||
|
||||
@@ -87,11 +87,7 @@
|
||||
(def text-editor-start text-editor/text-editor-start)
|
||||
(def text-editor-stop text-editor/text-editor-stop)
|
||||
(def text-editor-set-cursor-from-point text-editor/text-editor-set-cursor-from-point)
|
||||
(def text-editor-pointer-down text-editor/text-editor-pointer-down)
|
||||
(def text-editor-pointer-move text-editor/text-editor-pointer-move)
|
||||
(def text-editor-pointer-up text-editor/text-editor-pointer-up)
|
||||
(def text-editor-is-active? text-editor/text-editor-is-active?)
|
||||
(def text-editor-select-all text-editor/text-editor-select-all)
|
||||
(def text-editor-sync-content text-editor/text-editor-sync-content)
|
||||
|
||||
(def dpr
|
||||
@@ -267,6 +263,22 @@
|
||||
[attrs]
|
||||
(text-editor/apply-style-to-selection attrs use-shape set-shape-text-content))
|
||||
|
||||
(defn update-text-rect!
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)})))
|
||||
|
||||
(defn- ensure-text-content
|
||||
"Guarantee that the shape always sends a valid text tree to WASM. When the
|
||||
content is nil (freshly created text) we fall back to
|
||||
tc/default-text-content so the renderer receives typography information."
|
||||
[content]
|
||||
(or content (tc/v2-default-text-content)))
|
||||
|
||||
(defn set-parent-id
|
||||
[id]
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
@@ -984,22 +996,6 @@
|
||||
(render-finish)
|
||||
(perf/end-measure "set-view-box::zoom")))))
|
||||
|
||||
(defn update-text-rect!
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)})))
|
||||
|
||||
(defn- ensure-text-content
|
||||
"Guarantee that the shape always sends a valid text tree to WASM. When the
|
||||
content is nil (freshly created text) we fall back to
|
||||
tc/default-text-content so the renderer receives typography information."
|
||||
[content]
|
||||
(or content (tc/v2-default-text-content)))
|
||||
|
||||
(defn set-object
|
||||
[shape]
|
||||
(perf/begin-measure "set-object")
|
||||
|
||||
@@ -27,21 +27,6 @@
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
|
||||
|
||||
(defn text-editor-pointer-down
|
||||
[x y]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_pointer_down" x y)))
|
||||
|
||||
(defn text-editor-pointer-move
|
||||
[x y]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_pointer_move" x y)))
|
||||
|
||||
(defn text-editor-pointer-up
|
||||
[x y]
|
||||
(when wasm/context-initialized?
|
||||
(h/call wasm/internal-module "_text_editor_pointer_up" x y)))
|
||||
|
||||
(defn text-editor-update-blink
|
||||
[timestamp-ms]
|
||||
(when wasm/context-initialized?
|
||||
@@ -98,12 +83,9 @@
|
||||
(h/call wasm/internal-module "_text_editor_stop")))
|
||||
|
||||
(defn text-editor-is-active?
|
||||
([id]
|
||||
(when wasm/context-initialized?
|
||||
(not (zero? (h/call wasm/internal-module "_text_editor_is_active_with_id" id)))))
|
||||
([]
|
||||
(when wasm/context-initialized?
|
||||
(not (zero? (h/call wasm/internal-module "_text_editor_is_active"))))))
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(not (zero? (h/call wasm/internal-module "_text_editor_is_active")))))
|
||||
|
||||
(defn text-editor-export-content
|
||||
[]
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
[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."
|
||||
@@ -54,18 +56,17 @@
|
||||
(.focus node))
|
||||
js/undefined))
|
||||
|
||||
;; Animation loop for cursor blink
|
||||
(mf/use-effect
|
||||
(fn []
|
||||
(let [raf-id (atom nil)
|
||||
animate (fn animate []
|
||||
(when (text-editor/text-editor-is-active?)
|
||||
(wasm.api/request-render "cursor-blink")
|
||||
(reset! raf-id (js/requestAnimationFrame animate))))]
|
||||
(animate)
|
||||
(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 @raf-id
|
||||
(js/cancelAnimationFrame @raf-id))))))
|
||||
(when @timeout-id
|
||||
(js/clearTimeout @timeout-id))))))
|
||||
|
||||
;; Document-level keydown handler for control keys
|
||||
(mf/use-effect
|
||||
|
||||
@@ -76,4 +76,3 @@ export function getFills(fillStyle) {
|
||||
const [color, opacity] = getColor(fillStyle);
|
||||
return `[["^ ","~:fill-color","${color}","~:fill-opacity",${opacity}]]`;
|
||||
}
|
||||
|
||||
|
||||
@@ -162,15 +162,12 @@ class TextEditorPlayground {
|
||||
}
|
||||
|
||||
this.#module.call("use_shape", ...textShape.id);
|
||||
// FIXME: This function doesn't exists anymore.
|
||||
/*
|
||||
const caretPosition = this.#module.call(
|
||||
"get_caret_position_at",
|
||||
e.offsetX,
|
||||
e.offsetY,
|
||||
);
|
||||
console.log("caretPosition", caretPosition);
|
||||
*/
|
||||
};
|
||||
|
||||
#onResize = (_entries) => {
|
||||
|
||||
86
render-wasm/Cargo.lock
generated
86
render-wasm/Cargo.lock
generated
@@ -202,9 +202,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.0"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -214,9 +214,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.7.1"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
|
||||
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
@@ -253,12 +253,6 @@ version = "3.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.161"
|
||||
@@ -468,18 +462,27 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -500,11 +503,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.8"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -515,9 +518,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "skia-bindings"
|
||||
version = "0.87.0"
|
||||
version = "0.93.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "704242769235d2ffe66a2a0a3002661262fc4af08d32807c362d7b0160ee703c"
|
||||
checksum = "2359f7e30c9da3f322f8ca3d4ec0abbc12a40035ce758309db0cdab07b5d4476"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"cc",
|
||||
@@ -532,13 +535,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "skia-safe"
|
||||
version = "0.87.0"
|
||||
version = "0.93.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f7d94f3e7537c71ad4cf132eb26e3be8c8a886ed3649c4525c089041fc312b2"
|
||||
checksum = "7f9e837ea9d531c9efee8f980bfcdb7226b21db0285b0c3171d8be745829f940"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"bitflags",
|
||||
"lazy_static",
|
||||
"percent-encoding",
|
||||
"skia-bindings",
|
||||
"skia-svg-macros",
|
||||
@@ -579,38 +581,43 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.19"
|
||||
version = "1.0.3+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e"
|
||||
checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.8"
|
||||
version = "1.0.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
|
||||
checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.22.22"
|
||||
name = "toml_parser"
|
||||
version = "1.0.9+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5"
|
||||
checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.13"
|
||||
@@ -775,12 +782,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.6.20"
|
||||
version = "0.7.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
|
||||
[[package]]
|
||||
name = "xattr"
|
||||
|
||||
@@ -25,7 +25,7 @@ gl = "0.14.0"
|
||||
glam = "0.24.2"
|
||||
indexmap = "2.7.1"
|
||||
macros = { path = "macros" }
|
||||
skia-safe = { version = "0.87.0", default-features = false, features = [
|
||||
skia-safe = { version = "0.93.1", default-features = false, features = [
|
||||
"gl",
|
||||
"svg",
|
||||
"textlayout",
|
||||
|
||||
@@ -10,7 +10,7 @@ fi
|
||||
|
||||
export BUILD_NAME="${BUILD_NAME:-render-wasm}"
|
||||
export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"};
|
||||
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"}
|
||||
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"}
|
||||
|
||||
# 256 MB of initial heap to perform less
|
||||
# initial calls to memory grow.
|
||||
|
||||
@@ -11,7 +11,7 @@ fi
|
||||
. ./_build_env
|
||||
|
||||
export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"wasm32-unknown-emscripten"};
|
||||
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"}
|
||||
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-wasm32-unknown-emscripten-gl-svg-textlayout-binary-cache-webp.tar.gz"}
|
||||
|
||||
|
||||
ALLOWED_RULES="-D static_mut_refs"
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
onlyBuiltDependencies:
|
||||
- esbuild
|
||||
@@ -356,7 +356,7 @@ impl Bounds {
|
||||
}
|
||||
|
||||
pub fn from_rect(r: &Rect) -> Self {
|
||||
let [nw, ne, se, sw] = r.to_quad();
|
||||
let [nw, ne, se, sw] = r.to_quad(None);
|
||||
Self::new(nw, ne, se, sw)
|
||||
}
|
||||
|
||||
|
||||
@@ -477,30 +477,32 @@ pub fn debug_render_bool_paths(
|
||||
paint.set_alpha_f(1.0);
|
||||
paint.set_style(skia::PaintStyle::Stroke);
|
||||
|
||||
let mut path = skia::Path::default();
|
||||
path.move_to((b.1.start.x as f32, b.1.start.y as f32));
|
||||
|
||||
match b.1.handles {
|
||||
BezierHandles::Linear => {
|
||||
path.line_to((b.1.end.x as f32, b.1.end.y as f32));
|
||||
let path = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.move_to((b.1.start.x as f32, b.1.start.y as f32));
|
||||
match b.1.handles {
|
||||
BezierHandles::Linear => {
|
||||
pb.line_to((b.1.end.x as f32, b.1.end.y as f32));
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
pb.quad_to(
|
||||
(handle.x as f32, handle.y as f32),
|
||||
(b.1.end.x as f32, b.1.end.y as f32),
|
||||
);
|
||||
}
|
||||
BezierHandles::Cubic {
|
||||
handle_start,
|
||||
handle_end,
|
||||
} => {
|
||||
pb.cubic_to(
|
||||
(handle_start.x as f32, handle_start.y as f32),
|
||||
(handle_end.x as f32, handle_end.y as f32),
|
||||
(b.1.end.x as f32, b.1.end.y as f32),
|
||||
);
|
||||
}
|
||||
}
|
||||
BezierHandles::Quadratic { handle } => {
|
||||
path.quad_to(
|
||||
(handle.x as f32, handle.y as f32),
|
||||
(b.1.end.x as f32, b.1.end.y as f32),
|
||||
);
|
||||
}
|
||||
BezierHandles::Cubic {
|
||||
handle_start,
|
||||
handle_end,
|
||||
} => {
|
||||
path.cubic_to(
|
||||
(handle_start.x as f32, handle_start.y as f32),
|
||||
(handle_end.x as f32, handle_end.y as f32),
|
||||
(b.1.end.x as f32, b.1.end.y as f32),
|
||||
);
|
||||
}
|
||||
}
|
||||
pb.detach()
|
||||
};
|
||||
canvas.draw_path(&path, &paint);
|
||||
|
||||
let mut v1 = b.1.normal(TValue::Parametric(1.0));
|
||||
|
||||
@@ -51,15 +51,18 @@ fn draw_image_fill(
|
||||
canvas.clip_rect(container, skia::ClipOp::Intersect, antialias);
|
||||
}
|
||||
Type::Circle => {
|
||||
let mut oval_path = skia::Path::new();
|
||||
oval_path.add_oval(container, None);
|
||||
let oval_path = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.add_oval(container, None, None);
|
||||
pb.detach()
|
||||
};
|
||||
canvas.clip_path(&oval_path, skia::ClipOp::Intersect, antialias);
|
||||
}
|
||||
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
||||
if let Some(path) = shape_type.path() {
|
||||
if let Some(path_transform) = path_transform {
|
||||
canvas.clip_path(
|
||||
path.to_skia_path().transform(&path_transform),
|
||||
&path.to_skia_path().make_transform(&path_transform),
|
||||
skia::ClipOp::Intersect,
|
||||
antialias,
|
||||
);
|
||||
|
||||
@@ -24,7 +24,11 @@ pub fn render_overlay(zoom: f32, canvas: &skia::Canvas, shape: &Shape, shapes: S
|
||||
cell.anchor + hv + vv,
|
||||
cell.anchor + vv,
|
||||
];
|
||||
let polygon = skia::Path::polygon(&points, true, None, None);
|
||||
let polygon = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.add_polygon(&points, true);
|
||||
pb.detach()
|
||||
};
|
||||
canvas.draw_path(&polygon, &paint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,11 @@ fn draw_stroke_on_circle(
|
||||
if let Some(clip_op) = stroke.clip_op() {
|
||||
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
|
||||
canvas.save_layer(&layer_rec);
|
||||
let mut clip_path = skia::Path::new();
|
||||
clip_path.add_oval(rect, None);
|
||||
let clip_path = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.add_oval(rect, None, None);
|
||||
pb.detach()
|
||||
};
|
||||
canvas.clip_path(&clip_path, clip_op, antialias);
|
||||
canvas.draw_oval(stroke_rect, &paint);
|
||||
canvas.restore();
|
||||
@@ -153,8 +156,9 @@ fn draw_stroke_on_path(
|
||||
blur: Option<&ImageFilter>,
|
||||
antialias: bool,
|
||||
) {
|
||||
let mut skia_path = path.to_skia_path();
|
||||
skia_path.transform(path_transform.unwrap_or(&Matrix::default()));
|
||||
let skia_path = path
|
||||
.to_skia_path()
|
||||
.make_transform(path_transform.unwrap_or(&Matrix::default()));
|
||||
|
||||
let is_open = path.is_open();
|
||||
|
||||
@@ -174,15 +178,7 @@ fn draw_stroke_on_path(
|
||||
}
|
||||
}
|
||||
|
||||
handle_stroke_caps(
|
||||
&mut skia_path,
|
||||
stroke,
|
||||
canvas,
|
||||
is_open,
|
||||
paint,
|
||||
blur,
|
||||
antialias,
|
||||
);
|
||||
handle_stroke_caps(&skia_path, stroke, canvas, is_open, paint, blur, antialias);
|
||||
}
|
||||
|
||||
fn handle_stroke_cap(
|
||||
@@ -224,7 +220,7 @@ fn handle_stroke_cap(
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn handle_stroke_caps(
|
||||
path: &mut skia::Path,
|
||||
path: &skia::Path,
|
||||
stroke: &Stroke,
|
||||
canvas: &skia::Canvas,
|
||||
is_open: bool,
|
||||
@@ -232,8 +228,7 @@ fn handle_stroke_caps(
|
||||
blur: Option<&ImageFilter>,
|
||||
_antialias: bool,
|
||||
) {
|
||||
let mut points = vec![Point::default(); path.count_points()];
|
||||
path.get_points(&mut points);
|
||||
let mut points = path.points().to_vec();
|
||||
// Curves can have duplicated points, so let's remove consecutive duplicated points
|
||||
points.dedup();
|
||||
let c_points = points.len();
|
||||
@@ -304,13 +299,16 @@ fn draw_square_cap(
|
||||
let mut transformed_points = points;
|
||||
matrix.map_points(&mut transformed_points, &points);
|
||||
|
||||
let mut path = skia::Path::new();
|
||||
path.move_to(Point::new(center.x, center.y));
|
||||
path.move_to(transformed_points[0]);
|
||||
path.line_to(transformed_points[1]);
|
||||
path.line_to(transformed_points[2]);
|
||||
path.line_to(transformed_points[3]);
|
||||
path.close();
|
||||
let path = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.move_to(Point::new(center.x, center.y));
|
||||
pb.move_to(transformed_points[0]);
|
||||
pb.line_to(transformed_points[1]);
|
||||
pb.line_to(transformed_points[2]);
|
||||
pb.line_to(transformed_points[3]);
|
||||
pb.close();
|
||||
pb.detach()
|
||||
};
|
||||
canvas.draw_path(&path, paint);
|
||||
}
|
||||
|
||||
@@ -338,13 +336,15 @@ fn draw_arrow_cap(
|
||||
let mut transformed_points = points;
|
||||
matrix.map_points(&mut transformed_points, &points);
|
||||
|
||||
let mut path = skia::Path::new();
|
||||
path.move_to(transformed_points[1]);
|
||||
path.line_to(transformed_points[0]);
|
||||
path.line_to(transformed_points[2]);
|
||||
path.move_to(Point::new(center.x, center.y));
|
||||
path.line_to(transformed_points[0]);
|
||||
|
||||
let path = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.move_to(transformed_points[1]);
|
||||
pb.line_to(transformed_points[0]);
|
||||
pb.line_to(transformed_points[2]);
|
||||
pb.move_to(Point::new(center.x, center.y));
|
||||
pb.line_to(transformed_points[0]);
|
||||
pb.detach()
|
||||
};
|
||||
canvas.draw_path(&path, paint);
|
||||
}
|
||||
|
||||
@@ -372,12 +372,14 @@ fn draw_triangle_cap(
|
||||
let mut transformed_points = points;
|
||||
matrix.map_points(&mut transformed_points, &points);
|
||||
|
||||
let mut path = skia::Path::new();
|
||||
path.move_to(transformed_points[0]);
|
||||
path.line_to(transformed_points[1]);
|
||||
path.line_to(transformed_points[2]);
|
||||
path.close();
|
||||
|
||||
let path = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.move_to(transformed_points[0]);
|
||||
pb.line_to(transformed_points[1]);
|
||||
pb.line_to(transformed_points[2]);
|
||||
pb.close();
|
||||
pb.detach()
|
||||
};
|
||||
canvas.draw_path(&path, paint);
|
||||
}
|
||||
|
||||
@@ -441,8 +443,7 @@ fn draw_image_stroke_in_container(
|
||||
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
|
||||
if let Some(p) = shape_type.path() {
|
||||
canvas.save();
|
||||
let mut path = p.to_skia_path();
|
||||
path.transform(&path_transform.unwrap());
|
||||
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
|
||||
let stroke_kind = stroke.render_kind(p.is_open());
|
||||
match stroke_kind {
|
||||
StrokeKind::Inner => {
|
||||
@@ -464,7 +465,7 @@ fn draw_image_stroke_in_container(
|
||||
canvas.draw_path(&path, &thin_paint);
|
||||
}
|
||||
handle_stroke_caps(
|
||||
&mut path,
|
||||
&path,
|
||||
stroke,
|
||||
canvas,
|
||||
is_open,
|
||||
@@ -504,8 +505,7 @@ fn draw_image_stroke_in_container(
|
||||
// Clear outer stroke for paths if necessary. When adding an outer stroke we need to empty the stroke added too in the inner area.
|
||||
if let Type::Path(p) = &shape.shape_type {
|
||||
if stroke.render_kind(p.is_open()) == StrokeKind::Outer {
|
||||
let mut path = p.to_skia_path();
|
||||
path.transform(&path_transform.unwrap());
|
||||
let path = p.to_skia_path().make_transform(&path_transform.unwrap());
|
||||
let mut clear_paint = skia::Paint::default();
|
||||
clear_paint.set_blend_mode(skia::BlendMode::Clear);
|
||||
clear_paint.set_anti_alias(antialias);
|
||||
|
||||
@@ -457,9 +457,9 @@ impl Surfaces {
|
||||
);
|
||||
|
||||
let snapshot = self.current.image_snapshot();
|
||||
let mut direct_context = self.current.direct_context();
|
||||
let props = skia::image::RequiredProperties::default();
|
||||
let tile_image_opt = snapshot
|
||||
.make_subset(direct_context.as_mut(), rect)
|
||||
.make_subset(None, rect, props)
|
||||
.or_else(|| self.current.image_snapshot_with_bounds(rect));
|
||||
|
||||
if let Some(tile_image) = tile_image_opt {
|
||||
|
||||
@@ -45,11 +45,7 @@ fn render_cursor(
|
||||
paint.set_color(editor_state.theme.cursor_color);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
let shape_matrix = shape.get_matrix();
|
||||
canvas.save();
|
||||
canvas.concat(&shape_matrix);
|
||||
canvas.draw_rect(rect, &paint);
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn render_selection(
|
||||
@@ -69,14 +65,9 @@ fn render_selection(
|
||||
paint.set_blend_mode(BlendMode::Multiply);
|
||||
paint.set_color(editor_state.theme.selection_color);
|
||||
paint.set_anti_alias(true);
|
||||
|
||||
let shape_matrix = shape.get_matrix();
|
||||
canvas.save();
|
||||
canvas.concat(&shape_matrix);
|
||||
for rect in rects {
|
||||
canvas.draw_rect(rect, &paint);
|
||||
}
|
||||
canvas.restore();
|
||||
}
|
||||
|
||||
fn vertical_align_offset(
|
||||
@@ -108,10 +99,12 @@ fn calculate_cursor_rect(
|
||||
return None;
|
||||
}
|
||||
|
||||
let selrect = shape.selrect();
|
||||
|
||||
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||
if idx == cursor.paragraph {
|
||||
let char_pos = cursor.offset;
|
||||
let char_pos = cursor.char_offset;
|
||||
// For cursor, we get a zero-width range at the position
|
||||
// We need to handle edge cases:
|
||||
// - At start of paragraph: use position 0
|
||||
@@ -164,8 +157,8 @@ fn calculate_cursor_rect(
|
||||
};
|
||||
|
||||
return Some(Rect::from_xywh(
|
||||
cursor_x,
|
||||
y_offset,
|
||||
selrect.x() + cursor_x,
|
||||
selrect.y() + y_offset,
|
||||
editor_state.theme.cursor_width,
|
||||
cursor_height,
|
||||
));
|
||||
@@ -189,6 +182,7 @@ fn calculate_selection_rects(
|
||||
let paragraphs = text_content.paragraphs();
|
||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
||||
|
||||
let selrect = shape.selrect();
|
||||
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
||||
|
||||
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||
@@ -209,13 +203,13 @@ fn calculate_selection_rects(
|
||||
.sum();
|
||||
|
||||
let range_start = if para_idx == start.paragraph {
|
||||
start.offset
|
||||
start.char_offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.offset
|
||||
end.char_offset
|
||||
} else {
|
||||
para_char_count
|
||||
};
|
||||
@@ -231,8 +225,8 @@ fn calculate_selection_rects(
|
||||
for text_box in text_boxes {
|
||||
let r = text_box.rect;
|
||||
rects.push(Rect::from_xywh(
|
||||
r.left(),
|
||||
y_offset + r.top(),
|
||||
selrect.x() + r.left(),
|
||||
selrect.y() + y_offset + r.top(),
|
||||
r.width(),
|
||||
r.height(),
|
||||
));
|
||||
|
||||
@@ -258,18 +258,6 @@ pub fn all_with_ancestors(
|
||||
}
|
||||
|
||||
impl Shape {
|
||||
pub fn get_relative_point(
|
||||
point: &Point,
|
||||
view_matrix: &Matrix,
|
||||
shape_matrix: &Matrix,
|
||||
) -> Option<Point> {
|
||||
let inv_view_matrix = view_matrix.invert()?;
|
||||
let inv_shape_matrix = shape_matrix.invert()?;
|
||||
let transform_matrix: Matrix = Matrix::concat(&inv_shape_matrix, &inv_view_matrix);
|
||||
let shape_relative_point = transform_matrix.map_point(*point);
|
||||
Some(shape_relative_point)
|
||||
}
|
||||
|
||||
pub fn new(id: Uuid) -> Self {
|
||||
Self {
|
||||
id,
|
||||
@@ -1348,7 +1336,7 @@ impl Shape {
|
||||
if let Some(path) = self.shape_type.path() {
|
||||
let mut skia_path = path.to_skia_path();
|
||||
if let Some(path_transform) = self.to_path_transform() {
|
||||
skia_path.transform(&path_transform);
|
||||
skia_path = skia_path.make_transform(&path_transform);
|
||||
}
|
||||
if let Some(svg_attrs) = &self.svg_attrs {
|
||||
if svg_attrs.fill_rule == FillRule::Evenodd {
|
||||
|
||||
@@ -51,10 +51,10 @@ impl Gradient {
|
||||
rect.left + self.end.0 * rect.width(),
|
||||
rect.top + self.end.1 * rect.height(),
|
||||
);
|
||||
skia::shader::Shader::linear_gradient(
|
||||
skia::gradient_shader::linear(
|
||||
(start, end),
|
||||
self.colors.as_slice(),
|
||||
self.offsets.as_slice(),
|
||||
Some(self.offsets.as_slice()),
|
||||
skia::TileMode::Clamp,
|
||||
None,
|
||||
None,
|
||||
@@ -83,11 +83,11 @@ impl Gradient {
|
||||
transform.pre_scale((self.width * rect.width() / rect.height(), 1.), None);
|
||||
transform.pre_translate((-center.x, -center.y));
|
||||
|
||||
skia::shader::Shader::radial_gradient(
|
||||
skia::gradient_shader::radial(
|
||||
center,
|
||||
distance,
|
||||
self.colors.as_slice(),
|
||||
self.offsets.as_slice(),
|
||||
Some(self.offsets.as_slice()),
|
||||
skia::TileMode::Clamp,
|
||||
None,
|
||||
Some(&transform),
|
||||
|
||||
@@ -29,40 +29,28 @@ impl Default for Path {
|
||||
}
|
||||
}
|
||||
|
||||
fn to_verb(v: u8) -> skia::path::Verb {
|
||||
match v {
|
||||
0 => skia::path::Verb::Move,
|
||||
1 => skia::path::Verb::Line,
|
||||
2 => skia::path::Verb::Quad,
|
||||
3 => skia::path::Verb::Conic,
|
||||
4 => skia::path::Verb::Cubic,
|
||||
5 => skia::path::Verb::Close,
|
||||
_ => skia::path::Verb::Done,
|
||||
}
|
||||
}
|
||||
|
||||
impl Path {
|
||||
pub fn new(segments: Vec<Segment>) -> Self {
|
||||
let mut skia_path = skia::Path::new();
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
let mut start = None;
|
||||
|
||||
for segment in segments.iter() {
|
||||
let destination = match *segment {
|
||||
Segment::MoveTo(xy) => {
|
||||
start = Some(xy);
|
||||
skia_path.move_to(xy);
|
||||
pb.move_to(xy);
|
||||
None
|
||||
}
|
||||
Segment::LineTo(xy) => {
|
||||
skia_path.line_to(xy);
|
||||
pb.line_to(xy);
|
||||
Some(xy)
|
||||
}
|
||||
Segment::CurveTo((c1, c2, xy)) => {
|
||||
skia_path.cubic_to(c1, c2, xy);
|
||||
pb.cubic_to(c1, c2, xy);
|
||||
Some(xy)
|
||||
}
|
||||
Segment::Close => {
|
||||
skia_path.close();
|
||||
pb.close();
|
||||
None
|
||||
}
|
||||
};
|
||||
@@ -71,11 +59,12 @@ impl Path {
|
||||
if math::is_close_to(destination.0, start.0)
|
||||
&& math::is_close_to(destination.1, start.1)
|
||||
{
|
||||
skia_path.close();
|
||||
pb.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let skia_path = pb.detach();
|
||||
let open = subpaths::is_open_path(&segments);
|
||||
|
||||
Self {
|
||||
@@ -86,38 +75,31 @@ impl Path {
|
||||
}
|
||||
|
||||
pub fn from_skia_path(path: skia::Path) -> Self {
|
||||
let nv = path.count_verbs();
|
||||
let mut verbs = vec![0; nv];
|
||||
path.get_verbs(&mut verbs);
|
||||
|
||||
let np = path.count_points();
|
||||
let mut points = Vec::with_capacity(np);
|
||||
points.resize(np, skia::Point::default());
|
||||
path.get_points(&mut points);
|
||||
let verbs = path.verbs();
|
||||
let points = path.points();
|
||||
|
||||
let mut segments = Vec::new();
|
||||
|
||||
let mut current_point = 0;
|
||||
for verb in verbs {
|
||||
let verb = to_verb(verb);
|
||||
match verb {
|
||||
skia::path::Verb::Move => {
|
||||
skia::PathVerb::Move => {
|
||||
let p = points[current_point];
|
||||
segments.push(Segment::MoveTo((p.x, p.y)));
|
||||
current_point += 1;
|
||||
}
|
||||
skia::path::Verb::Line => {
|
||||
skia::PathVerb::Line => {
|
||||
let p = points[current_point];
|
||||
segments.push(Segment::LineTo((p.x, p.y)));
|
||||
current_point += 1;
|
||||
}
|
||||
skia::path::Verb::Quad => {
|
||||
skia::PathVerb::Quad => {
|
||||
let p1 = points[current_point];
|
||||
let p2 = points[current_point + 1];
|
||||
segments.push(Segment::CurveTo(((p1.x, p1.y), (p1.x, p1.y), (p2.x, p2.y))));
|
||||
current_point += 2;
|
||||
}
|
||||
skia::path::Verb::Conic => {
|
||||
skia::PathVerb::Conic => {
|
||||
// TODO: There is no way currently to access the conic weight
|
||||
// to transform this correctly
|
||||
let p1 = points[current_point];
|
||||
@@ -125,17 +107,14 @@ impl Path {
|
||||
segments.push(Segment::CurveTo(((p1.x, p1.y), (p1.x, p1.y), (p2.x, p2.y))));
|
||||
current_point += 2;
|
||||
}
|
||||
skia::path::Verb::Cubic => {
|
||||
skia::PathVerb::Cubic => {
|
||||
let p1 = points[current_point];
|
||||
let p2 = points[current_point + 1];
|
||||
let p3 = points[current_point + 2];
|
||||
segments.push(Segment::CurveTo(((p1.x, p1.y), (p2.x, p2.y), (p3.x, p3.y))));
|
||||
current_point += 3;
|
||||
}
|
||||
skia::path::Verb::Close => {
|
||||
segments.push(Segment::Close);
|
||||
}
|
||||
skia::path::Verb::Done => {
|
||||
skia::PathVerb::Close => {
|
||||
segments.push(Segment::Close);
|
||||
}
|
||||
}
|
||||
@@ -184,7 +163,7 @@ impl Path {
|
||||
_ => {}
|
||||
});
|
||||
|
||||
self.skia_path.transform(mtx);
|
||||
self.skia_path = self.skia_path.make_transform(mtx);
|
||||
}
|
||||
|
||||
pub fn segments(&self) -> &Vec<Segment> {
|
||||
|
||||
@@ -225,13 +225,16 @@ impl Stroke {
|
||||
if self.style != StrokeStyle::Solid {
|
||||
let path_effect = match self.style {
|
||||
StrokeStyle::Dotted => {
|
||||
let mut circle_path = skia::Path::new();
|
||||
let width = match self.kind {
|
||||
StrokeKind::Inner => self.width,
|
||||
StrokeKind::Center => self.width / 2.0,
|
||||
StrokeKind::Outer => self.width,
|
||||
};
|
||||
circle_path.add_circle((0.0, 0.0), width, None);
|
||||
let circle_path = {
|
||||
let mut pb = skia::PathBuilder::new();
|
||||
pb.add_circle((0.0, 0.0), width, None);
|
||||
pb.detach()
|
||||
};
|
||||
let advance = self.width + 5.0;
|
||||
skia::PathEffect::path_1d(
|
||||
&circle_path,
|
||||
|
||||
@@ -14,7 +14,6 @@ use skia_safe::{
|
||||
textlayout::ParagraphBuilder,
|
||||
textlayout::ParagraphStyle,
|
||||
textlayout::PositionWithAffinity,
|
||||
textlayout::Affinity,
|
||||
Contains,
|
||||
};
|
||||
|
||||
@@ -113,55 +112,29 @@ impl TextContentSize {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TextPositionWithAffinity {
|
||||
#[allow(dead_code)]
|
||||
pub position_with_affinity: PositionWithAffinity,
|
||||
pub paragraph: usize,
|
||||
pub offset: usize,
|
||||
}
|
||||
|
||||
impl PartialEq for TextPositionWithAffinity {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.paragraph == other.paragraph
|
||||
&& self.offset == other.offset
|
||||
}
|
||||
pub paragraph: i32,
|
||||
#[allow(dead_code)]
|
||||
pub span: i32,
|
||||
pub offset: i32,
|
||||
}
|
||||
|
||||
impl TextPositionWithAffinity {
|
||||
pub fn new(
|
||||
position_with_affinity: PositionWithAffinity,
|
||||
paragraph: usize,
|
||||
offset: usize,
|
||||
paragraph: i32,
|
||||
span: i32,
|
||||
offset: i32,
|
||||
) -> Self {
|
||||
Self {
|
||||
position_with_affinity,
|
||||
paragraph,
|
||||
span,
|
||||
offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
position_with_affinity: PositionWithAffinity {
|
||||
position: 0,
|
||||
affinity: Affinity::Downstream,
|
||||
},
|
||||
paragraph: 0,
|
||||
offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_without_affinity(paragraph: usize, offset: usize) -> Self {
|
||||
Self {
|
||||
position_with_affinity: PositionWithAffinity {
|
||||
position: offset as i32,
|
||||
affinity: Affinity::Downstream,
|
||||
},
|
||||
paragraph,
|
||||
offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -448,19 +421,14 @@ impl TextContent {
|
||||
self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y);
|
||||
}
|
||||
|
||||
pub fn get_caret_position_from_shape_coords(
|
||||
&self,
|
||||
point: &Point,
|
||||
) -> Option<TextPositionWithAffinity> {
|
||||
pub fn get_caret_position_at(&self, point: &Point) -> Option<TextPositionWithAffinity> {
|
||||
let mut offset_y = 0.0;
|
||||
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
||||
|
||||
let mut paragraph_index: usize = 0;
|
||||
// IMPORTANT! I'm keeping this because I think it should be better to have the span index
|
||||
// cached the same way we keep the paragraph index.
|
||||
#[allow(dead_code)]
|
||||
let mut _span_index: usize = 0;
|
||||
let mut paragraph_index: i32 = -1;
|
||||
let mut span_index: i32 = -1;
|
||||
for layout_paragraph in layout_paragraphs {
|
||||
paragraph_index += 1;
|
||||
let start_y = offset_y;
|
||||
let end_y = offset_y + layout_paragraph.height();
|
||||
|
||||
@@ -481,15 +449,16 @@ impl TextContent {
|
||||
// Computed position keeps the current position in terms
|
||||
// of number of characters of text. This is used to know
|
||||
// in which span we are.
|
||||
let mut computed_position: usize = 0;
|
||||
let mut span_offset: usize = 0;
|
||||
let mut computed_position = 0;
|
||||
let mut span_offset = 0;
|
||||
|
||||
// If paragraph has no spans, default to span 0, offset 0
|
||||
if paragraph.children().is_empty() {
|
||||
_span_index = 0;
|
||||
span_index = 0;
|
||||
span_offset = 0;
|
||||
} else {
|
||||
for span in paragraph.children() {
|
||||
span_index += 1;
|
||||
let length = span.text.chars().count();
|
||||
let start_position = computed_position;
|
||||
let end_position = computed_position + length;
|
||||
@@ -506,23 +475,22 @@ impl TextContent {
|
||||
&& end_position >= current_position
|
||||
{
|
||||
span_offset =
|
||||
position_with_affinity.position as usize - start_position;
|
||||
position_with_affinity.position - start_position as i32;
|
||||
break;
|
||||
}
|
||||
computed_position += length;
|
||||
_span_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return Some(TextPositionWithAffinity::new(
|
||||
position_with_affinity,
|
||||
paragraph_index,
|
||||
position_with_affinity.position,
|
||||
span_index,
|
||||
span_offset,
|
||||
));
|
||||
}
|
||||
}
|
||||
offset_y += layout_paragraph.height();
|
||||
paragraph_index += 1;
|
||||
}
|
||||
|
||||
// Handle completely empty text shapes: if there are no paragraphs or all paragraphs
|
||||
@@ -539,6 +507,7 @@ impl TextContent {
|
||||
return Some(TextPositionWithAffinity::new(
|
||||
default_position,
|
||||
0, // paragraph 0
|
||||
0, // span 0
|
||||
0, // offset 0
|
||||
));
|
||||
}
|
||||
@@ -546,16 +515,6 @@ impl TextContent {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_caret_position_from_screen_coords(
|
||||
&self,
|
||||
point: &Point,
|
||||
view_matrix: &Matrix,
|
||||
shape_matrix: &Matrix,
|
||||
) -> Option<TextPositionWithAffinity> {
|
||||
let shape_rel_point = Shape::get_relative_point(point, view_matrix, shape_matrix)?;
|
||||
self.get_caret_position_from_shape_coords(&shape_rel_point)
|
||||
}
|
||||
|
||||
/// Builds the ParagraphBuilders necessary to render
|
||||
/// this text.
|
||||
pub fn paragraph_builder_group_from_text(
|
||||
|
||||
@@ -101,7 +101,6 @@ impl TextPaths {
|
||||
if let Some((text_blob_path, text_blob_bounds)) =
|
||||
Self::get_text_blob_path(span_text, font, blob_offset_x, blob_offset_y)
|
||||
{
|
||||
let mut text_path = text_blob_path.clone();
|
||||
let text_width = font.measure_text(span_text, None).0;
|
||||
|
||||
let decoration = style_metric.text_style.decoration();
|
||||
@@ -111,16 +110,20 @@ impl TextPaths {
|
||||
let blob_top = blob_offset_y;
|
||||
let blob_height = text_blob_bounds.height();
|
||||
|
||||
if let Some(decoration_rect) = self.calculate_text_decoration_rect(
|
||||
decoration.ty,
|
||||
font_metrics,
|
||||
blob_left,
|
||||
blob_top,
|
||||
text_width,
|
||||
blob_height,
|
||||
) {
|
||||
text_path.add_rect(decoration_rect, None);
|
||||
}
|
||||
let text_path = {
|
||||
let mut pb = skia::PathBuilder::new_path(&text_blob_path);
|
||||
if let Some(decoration_rect) = self.calculate_text_decoration_rect(
|
||||
decoration.ty,
|
||||
font_metrics,
|
||||
blob_left,
|
||||
blob_top,
|
||||
text_width,
|
||||
blob_height,
|
||||
) {
|
||||
pb.add_rect(decoration_rect, None, None);
|
||||
}
|
||||
pb.detach()
|
||||
};
|
||||
|
||||
let mut paint = style_metric.text_style.foreground();
|
||||
paint.set_anti_alias(antialias);
|
||||
|
||||
@@ -1,16 +1,37 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::shapes::{TextContent, TextPositionWithAffinity};
|
||||
use crate::shapes::TextPositionWithAffinity;
|
||||
use crate::uuid::Uuid;
|
||||
use skia_safe::{
|
||||
textlayout::{Affinity, PositionWithAffinity},
|
||||
Color,
|
||||
};
|
||||
use skia_safe::Color;
|
||||
|
||||
/// Cursor position within text content.
|
||||
/// Uses character offsets for precise positioning.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
|
||||
pub struct TextCursor {
|
||||
pub paragraph: usize,
|
||||
pub char_offset: usize,
|
||||
}
|
||||
|
||||
impl TextCursor {
|
||||
pub fn new(paragraph: usize, char_offset: usize) -> Self {
|
||||
Self {
|
||||
paragraph,
|
||||
char_offset,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn zero() -> Self {
|
||||
Self {
|
||||
paragraph: 0,
|
||||
char_offset: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct TextSelection {
|
||||
pub anchor: TextPositionWithAffinity,
|
||||
pub focus: TextPositionWithAffinity,
|
||||
pub anchor: TextCursor,
|
||||
pub focus: TextCursor,
|
||||
}
|
||||
|
||||
impl TextSelection {
|
||||
@@ -18,10 +39,10 @@ impl TextSelection {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn from_position_with_affinity(position: TextPositionWithAffinity) -> Self {
|
||||
pub fn from_cursor(cursor: TextCursor) -> Self {
|
||||
Self {
|
||||
anchor: position,
|
||||
focus: position,
|
||||
anchor: cursor,
|
||||
focus: cursor,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,12 +54,12 @@ impl TextSelection {
|
||||
!self.is_collapsed()
|
||||
}
|
||||
|
||||
pub fn set_caret(&mut self, cursor: TextPositionWithAffinity) {
|
||||
pub fn set_caret(&mut self, cursor: TextCursor) {
|
||||
self.anchor = cursor;
|
||||
self.focus = cursor;
|
||||
}
|
||||
|
||||
pub fn extend_to(&mut self, cursor: TextPositionWithAffinity) {
|
||||
pub fn extend_to(&mut self, cursor: TextCursor) {
|
||||
self.focus = cursor;
|
||||
}
|
||||
|
||||
@@ -50,24 +71,24 @@ impl TextSelection {
|
||||
self.focus = self.anchor;
|
||||
}
|
||||
|
||||
pub fn start(&self) -> TextPositionWithAffinity {
|
||||
pub fn start(&self) -> TextCursor {
|
||||
if self.anchor.paragraph < self.focus.paragraph {
|
||||
self.anchor
|
||||
} else if self.anchor.paragraph > self.focus.paragraph {
|
||||
self.focus
|
||||
} else if self.anchor.offset <= self.focus.offset {
|
||||
} else if self.anchor.char_offset <= self.focus.char_offset {
|
||||
self.anchor
|
||||
} else {
|
||||
self.focus
|
||||
}
|
||||
}
|
||||
|
||||
pub fn end(&self) -> TextPositionWithAffinity {
|
||||
pub fn end(&self) -> TextCursor {
|
||||
if self.anchor.paragraph > self.focus.paragraph {
|
||||
self.anchor
|
||||
} else if self.anchor.paragraph < self.focus.paragraph {
|
||||
self.focus
|
||||
} else if self.anchor.offset >= self.focus.offset {
|
||||
} else if self.anchor.char_offset >= self.focus.char_offset {
|
||||
self.anchor
|
||||
} else {
|
||||
self.focus
|
||||
@@ -78,7 +99,7 @@ impl TextSelection {
|
||||
/// Events that the text editor can emit for frontend synchronization
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum TextEditorEvent {
|
||||
pub enum EditorEvent {
|
||||
None = 0,
|
||||
ContentChanged = 1,
|
||||
SelectionChanged = 2,
|
||||
@@ -101,13 +122,10 @@ pub struct TextEditorState {
|
||||
pub theme: TextEditorTheme,
|
||||
pub selection: TextSelection,
|
||||
pub is_active: bool,
|
||||
// This property indicates that we've started
|
||||
// selecting something with the pointer.
|
||||
pub is_pointer_selection_active: bool,
|
||||
pub active_shape_id: Option<Uuid>,
|
||||
pub cursor_visible: bool,
|
||||
pub last_blink_time: f64,
|
||||
pending_events: Vec<TextEditorEvent>,
|
||||
pending_events: Vec<EditorEvent>,
|
||||
}
|
||||
|
||||
impl TextEditorState {
|
||||
@@ -120,7 +138,6 @@ impl TextEditorState {
|
||||
},
|
||||
selection: TextSelection::new(),
|
||||
is_active: false,
|
||||
is_pointer_selection_active: false,
|
||||
active_shape_id: None,
|
||||
cursor_visible: true,
|
||||
last_blink_time: 0.0,
|
||||
@@ -134,7 +151,6 @@ impl TextEditorState {
|
||||
self.cursor_visible = true;
|
||||
self.last_blink_time = 0.0;
|
||||
self.selection = TextSelection::new();
|
||||
self.is_pointer_selection_active = false;
|
||||
self.pending_events.clear();
|
||||
}
|
||||
|
||||
@@ -142,77 +158,21 @@ impl TextEditorState {
|
||||
self.is_active = false;
|
||||
self.active_shape_id = None;
|
||||
self.cursor_visible = false;
|
||||
self.is_pointer_selection_active = false;
|
||||
self.pending_events.clear();
|
||||
}
|
||||
|
||||
pub fn set_caret_from_position(&mut self, position: TextPositionWithAffinity) {
|
||||
let cursor = TextCursor::new(position.paragraph as usize, position.offset as usize);
|
||||
self.selection.set_caret(cursor);
|
||||
self.reset_blink();
|
||||
self.push_event(EditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
pub fn start_pointer_selection(&mut self) -> bool {
|
||||
if self.is_pointer_selection_active {
|
||||
return false;
|
||||
}
|
||||
self.is_pointer_selection_active = true;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn stop_pointer_selection(&mut self) -> bool {
|
||||
if !self.is_pointer_selection_active {
|
||||
return false;
|
||||
}
|
||||
self.is_pointer_selection_active = false;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn select_all(&mut self, content: &TextContent) -> bool {
|
||||
self.is_pointer_selection_active = false;
|
||||
self.set_caret_from_position(TextPositionWithAffinity::new(
|
||||
PositionWithAffinity {
|
||||
position: 0,
|
||||
affinity: Affinity::Downstream,
|
||||
},
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
));
|
||||
let num_paragraphs = (content.paragraphs().len() - 1) as i32;
|
||||
let Some(last_paragraph) = content.paragraphs().last() else {
|
||||
return false;
|
||||
};
|
||||
let num_spans = (last_paragraph.children().len() - 1) as i32;
|
||||
let Some(last_text_span) = last_paragraph.children().last() else {
|
||||
return false;
|
||||
};
|
||||
let mut offset = 0;
|
||||
for span in last_paragraph.children() {
|
||||
offset += span.text.len();
|
||||
}
|
||||
self.extend_selection_from_position(TextPositionWithAffinity::new(
|
||||
PositionWithAffinity {
|
||||
position: offset as i32,
|
||||
affinity: Affinity::Upstream,
|
||||
},
|
||||
num_paragraphs,
|
||||
num_spans,
|
||||
last_text_span.text.len() as i32,
|
||||
offset as i32,
|
||||
));
|
||||
pub fn extend_selection_from_position(&mut self, position: TextPositionWithAffinity) {
|
||||
let cursor = TextCursor::new(position.paragraph as usize, position.offset as usize);
|
||||
self.selection.extend_to(cursor);
|
||||
self.reset_blink();
|
||||
self.push_event(crate::state::EditorEvent::SelectionChanged);
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.set_caret(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
pub fn extend_selection_from_position(&mut self, position: &TextPositionWithAffinity) {
|
||||
self.selection.extend_to(*position);
|
||||
self.reset_blink();
|
||||
self.push_event(TextEditorEvent::SelectionChanged);
|
||||
self.push_event(EditorEvent::SelectionChanged);
|
||||
}
|
||||
|
||||
pub fn update_blink(&mut self, timestamp_ms: f64) {
|
||||
@@ -238,17 +198,41 @@ impl TextEditorState {
|
||||
self.last_blink_time = 0.0;
|
||||
}
|
||||
|
||||
pub fn push_event(&mut self, event: TextEditorEvent) {
|
||||
pub fn push_event(&mut self, event: EditorEvent) {
|
||||
if self.pending_events.last() != Some(&event) {
|
||||
self.pending_events.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll_event(&mut self) -> TextEditorEvent {
|
||||
self.pending_events.pop().unwrap_or(TextEditorEvent::None)
|
||||
pub fn poll_event(&mut self) -> EditorEvent {
|
||||
self.pending_events.pop().unwrap_or(EditorEvent::None)
|
||||
}
|
||||
|
||||
pub fn has_pending_events(&self) -> bool {
|
||||
!self.pending_events.is_empty()
|
||||
}
|
||||
|
||||
pub fn set_caret_position_from(
|
||||
&mut self,
|
||||
text_position_with_affinity: TextPositionWithAffinity,
|
||||
) {
|
||||
self.set_caret_from_position(text_position_with_affinity);
|
||||
}
|
||||
}
|
||||
|
||||
/// TODO: Remove legacy code
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub struct TextNodePosition {
|
||||
pub paragraph: i32,
|
||||
pub span: i32,
|
||||
}
|
||||
|
||||
impl TextNodePosition {
|
||||
pub fn new(paragraph: i32, span: i32) -> Self {
|
||||
Self { paragraph, span }
|
||||
}
|
||||
|
||||
pub fn is_invalid(&self) -> bool {
|
||||
self.paragraph < 0 || self.span < 0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use macros::ToJs;
|
||||
|
||||
use super::{fills::RawFillData, fonts::RawFontStyle};
|
||||
|
||||
use crate::math::{Matrix, Point};
|
||||
use crate::mem::{self, SerializableResult};
|
||||
use crate::shapes::{
|
||||
self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
|
||||
};
|
||||
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
||||
use crate::{with_current_shape, with_current_shape_mut, with_state, with_state_mut, STATE};
|
||||
use crate::{
|
||||
with_current_shape, with_current_shape_mut, with_state, with_state_mut,
|
||||
with_state_mut_current_shape, STATE,
|
||||
};
|
||||
|
||||
const RAW_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
|
||||
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
||||
@@ -385,6 +388,32 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 {
|
||||
with_state_mut_current_shape!(state, |shape: &Shape| {
|
||||
if let Type::Text(text_content) = &shape.shape_type {
|
||||
let mut matrix = Matrix::new_identity();
|
||||
let shape_matrix = shape.get_concatenated_matrix(&state.shapes);
|
||||
let view_matrix = state.render_state.viewbox.get_matrix();
|
||||
if let Some(inv_view_matrix) = view_matrix.invert() {
|
||||
matrix.post_concat(&inv_view_matrix);
|
||||
matrix.post_concat(&shape_matrix);
|
||||
|
||||
let mapped_point = matrix.map_point(Point::new(x, y));
|
||||
|
||||
if let Some(position_with_affinity) =
|
||||
text_content.get_caret_position_at(&mapped_point)
|
||||
{
|
||||
return position_with_affinity.position_with_affinity.position;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Trying to get caret position of a shape that it's not a text shape");
|
||||
}
|
||||
});
|
||||
-1
|
||||
}
|
||||
|
||||
const RAW_POSITION_DATA_SIZE: usize = size_of::<shapes::PositionData>();
|
||||
|
||||
impl From<[u8; RAW_POSITION_DATA_SIZE]> for shapes::PositionData {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use macros::ToJs;
|
||||
|
||||
use crate::math::{Matrix, Point, Rect};
|
||||
use crate::mem;
|
||||
use crate::shapes::{Paragraph, Shape, TextContent, TextPositionWithAffinity, Type, VerticalAlign};
|
||||
use crate::state::{TextSelection};
|
||||
use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign};
|
||||
use crate::state::{TextCursor, TextSelection};
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
use crate::utils::uuid_to_u32_quartet;
|
||||
use crate::{with_state, with_state_mut, STATE};
|
||||
use macros::ToJs;
|
||||
|
||||
#[derive(PartialEq, ToJs)]
|
||||
#[repr(u8)]
|
||||
@@ -53,17 +54,6 @@ pub extern "C" fn text_editor_is_active() -> bool {
|
||||
with_state!(state, { state.text_editor_state.is_active })
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_is_active_with_id(a: u32, b: u32, c: u32, d: u32) -> bool {
|
||||
with_state!(state, {
|
||||
let shape_id = uuid_from_u32_quartet(a, b, c, d);
|
||||
let Some(active_shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return false;
|
||||
};
|
||||
state.text_editor_state.is_active && active_shape_id == shape_id
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) {
|
||||
with_state!(state, {
|
||||
@@ -80,25 +70,45 @@ pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) {
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_select_all() -> bool {
|
||||
pub extern "C" fn text_editor_select_all() {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return false;
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return false;
|
||||
return;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return false;
|
||||
return;
|
||||
};
|
||||
state.text_editor_state.select_all(text_content)
|
||||
})
|
||||
|
||||
let paragraphs = text_content.paragraphs();
|
||||
if paragraphs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let last_para_idx = paragraphs.len() - 1;
|
||||
let last_para = ¶graphs[last_para_idx];
|
||||
let total_chars: usize = last_para
|
||||
.children()
|
||||
.iter()
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum();
|
||||
|
||||
use crate::state::TextCursor;
|
||||
state.text_editor_state.selection.anchor = TextCursor::new(0, 0);
|
||||
state.text_editor_state.selection.focus = TextCursor::new(last_para_idx, total_chars);
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::EditorEvent::SelectionChanged);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -110,105 +120,6 @@ pub extern "C" fn text_editor_poll_event() -> u8 {
|
||||
// SELECTION MANAGEMENT
|
||||
// ============================================================================
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
}
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
};
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
let point = Point::new(x, y);
|
||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||
let shape_matrix = shape.get_matrix();
|
||||
state.text_editor_state.start_pointer_selection();
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state.text_editor_state.set_caret_from_position(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
}
|
||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||
let point = Point::new(x, y);
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
};
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
let shape_matrix = shape.get_matrix();
|
||||
let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !state.text_editor_state.is_pointer_selection_active {
|
||||
return;
|
||||
}
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
}
|
||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||
let point = Point::new(x, y);
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
};
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
let shape_matrix = shape.get_matrix();
|
||||
let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if !state.text_editor_state.is_pointer_selection_active {
|
||||
return;
|
||||
}
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(&position);
|
||||
}
|
||||
state.text_editor_state.stop_pointer_selection();
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
@@ -216,22 +127,140 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
return;
|
||||
}
|
||||
|
||||
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||
let point = Point::new(x, y);
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
};
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
|
||||
let (shape_matrix, view_matrix, selrect, vertical_align) = {
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
(
|
||||
shape.get_concatenated_matrix(&state.shapes),
|
||||
state.render_state.viewbox.get_matrix(),
|
||||
shape.selrect(),
|
||||
shape.vertical_align(),
|
||||
)
|
||||
};
|
||||
|
||||
let Some(inv_view_matrix) = view_matrix.invert() else {
|
||||
return;
|
||||
};
|
||||
let shape_matrix = shape.get_matrix();
|
||||
let Type::Text(text_content) = &shape.shape_type else {
|
||||
|
||||
let Some(inv_shape_matrix) = shape_matrix.invert() else {
|
||||
return;
|
||||
};
|
||||
if let Some(position) =
|
||||
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||
{
|
||||
state.text_editor_state.set_caret_from_position(&position);
|
||||
|
||||
let mut matrix = Matrix::new_identity();
|
||||
matrix.post_concat(&inv_view_matrix);
|
||||
matrix.post_concat(&inv_shape_matrix);
|
||||
|
||||
let mapped_point = matrix.map_point(Point::new(x, y));
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
if text_content.layout.paragraphs.is_empty() && !text_content.paragraphs().is_empty() {
|
||||
let bounds = text_content.bounds;
|
||||
text_content.update_layout(bounds);
|
||||
}
|
||||
|
||||
// Calculate vertical alignment offset (same as in render/text_editor.rs)
|
||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
||||
let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum();
|
||||
let vertical_offset = match vertical_align {
|
||||
crate::shapes::VerticalAlign::Center => (selrect.height() - total_height) / 2.0,
|
||||
crate::shapes::VerticalAlign::Bottom => selrect.height() - total_height,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Adjust point: subtract selrect offset and vertical alignment
|
||||
// The text layout expects coordinates where (0, 0) is the top-left of the text content
|
||||
let adjusted_point = Point::new(
|
||||
mapped_point.x - selrect.x(),
|
||||
mapped_point.y - selrect.y() - vertical_offset,
|
||||
);
|
||||
|
||||
if let Some(position) = text_content.get_caret_position_at(&adjusted_point) {
|
||||
state.text_editor_state.set_caret_from_position(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn text_editor_extend_selection_to_point(x: f32, y: f32) {
|
||||
with_state_mut!(state, {
|
||||
if !state.text_editor_state.is_active {
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let (shape_matrix, view_matrix, selrect, vertical_align) = {
|
||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
(
|
||||
shape.get_concatenated_matrix(&state.shapes),
|
||||
state.render_state.viewbox.get_matrix(),
|
||||
shape.selrect(),
|
||||
shape.vertical_align(),
|
||||
)
|
||||
};
|
||||
|
||||
let Some(inv_view_matrix) = view_matrix.invert() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(inv_shape_matrix) = shape_matrix.invert() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut matrix = Matrix::new_identity();
|
||||
matrix.post_concat(&inv_view_matrix);
|
||||
matrix.post_concat(&inv_shape_matrix);
|
||||
|
||||
let mapped_point = matrix.map_point(Point::new(x, y));
|
||||
|
||||
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||
return;
|
||||
};
|
||||
|
||||
if text_content.layout.paragraphs.is_empty() && !text_content.paragraphs().is_empty() {
|
||||
let bounds = text_content.bounds;
|
||||
text_content.update_layout(bounds);
|
||||
}
|
||||
|
||||
// Calculate vertical alignment offset (same as in render/text_editor.rs)
|
||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
||||
let total_height: f32 = layout_paragraphs.iter().map(|p| p.height()).sum();
|
||||
let vertical_offset = match vertical_align {
|
||||
crate::shapes::VerticalAlign::Center => (selrect.height() - total_height) / 2.0,
|
||||
crate::shapes::VerticalAlign::Bottom => selrect.height() - total_height,
|
||||
_ => 0.0,
|
||||
};
|
||||
|
||||
// Adjust point: subtract selrect offset and vertical alignment
|
||||
let adjusted_point = Point::new(
|
||||
mapped_point.x - selrect.x(),
|
||||
mapped_point.y - selrect.y() - vertical_offset,
|
||||
);
|
||||
|
||||
if let Some(position) = text_content.get_caret_position_at(&adjusted_point) {
|
||||
state
|
||||
.text_editor_state
|
||||
.extend_selection_from_position(position);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -276,7 +305,7 @@ pub extern "C" fn text_editor_insert_text() {
|
||||
let cursor = state.text_editor_state.selection.focus;
|
||||
|
||||
if let Some(new_offset) = insert_text_at_cursor(text_content, &cursor, &text) {
|
||||
let new_cursor = TextPositionWithAffinity::new_without_affinity(cursor.paragraph, new_offset);
|
||||
let new_cursor = TextCursor::new(cursor.paragraph, new_offset);
|
||||
state.text_editor_state.selection.set_caret(new_cursor);
|
||||
}
|
||||
|
||||
@@ -286,10 +315,10 @@ pub extern "C" fn text_editor_insert_text() {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
.push_event(crate::state::EditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
@@ -336,10 +365,10 @@ pub extern "C" fn text_editor_delete_backward() {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
.push_event(crate::state::EditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
@@ -384,10 +413,10 @@ pub extern "C" fn text_editor_delete_forward() {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
.push_event(crate::state::EditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
@@ -423,7 +452,7 @@ pub extern "C" fn text_editor_insert_paragraph() {
|
||||
let cursor = state.text_editor_state.selection.focus;
|
||||
|
||||
if split_paragraph_at_cursor(text_content, &cursor) {
|
||||
let new_cursor = TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0);
|
||||
let new_cursor = TextCursor::new(cursor.paragraph + 1, 0);
|
||||
state.text_editor_state.selection.set_caret(new_cursor);
|
||||
}
|
||||
|
||||
@@ -433,10 +462,10 @@ pub extern "C" fn text_editor_insert_paragraph() {
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||
.push_event(crate::state::EditorEvent::ContentChanged);
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||
.push_event(crate::state::EditorEvent::NeedsLayout);
|
||||
|
||||
state.render_state.mark_touched(shape_id);
|
||||
});
|
||||
@@ -494,7 +523,7 @@ pub extern "C" fn text_editor_move_cursor(direction: CursorDirection, extend_sel
|
||||
state.text_editor_state.reset_blink();
|
||||
state
|
||||
.text_editor_state
|
||||
.push_event(crate::state::TextEditorEvent::SelectionChanged);
|
||||
.push_event(crate::state::EditorEvent::SelectionChanged);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -711,12 +740,12 @@ pub extern "C" fn text_editor_export_selection() -> *mut u8 {
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum();
|
||||
let range_start = if para_idx == start.paragraph {
|
||||
start.offset
|
||||
start.char_offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.offset
|
||||
end.char_offset
|
||||
} else {
|
||||
para_char_count
|
||||
};
|
||||
@@ -764,9 +793,9 @@ pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 {
|
||||
let sel = &state.text_editor_state.selection;
|
||||
unsafe {
|
||||
*buffer_ptr = sel.anchor.paragraph as u32;
|
||||
*buffer_ptr.add(1) = sel.anchor.offset as u32;
|
||||
*buffer_ptr.add(1) = sel.anchor.char_offset as u32;
|
||||
*buffer_ptr.add(2) = sel.focus.paragraph as u32;
|
||||
*buffer_ptr.add(3) = sel.focus.offset as u32;
|
||||
*buffer_ptr.add(3) = sel.focus.char_offset as u32;
|
||||
}
|
||||
1
|
||||
})
|
||||
@@ -776,7 +805,7 @@ pub extern "C" fn text_editor_get_selection(buffer_ptr: *mut u32) -> u32 {
|
||||
// HELPERS: Cursor & Selection
|
||||
// ============================================================================
|
||||
|
||||
fn get_cursor_rect(text_content: &TextContent, cursor: &TextPositionWithAffinity, shape: &Shape) -> Option<Rect> {
|
||||
fn get_cursor_rect(text_content: &TextContent, cursor: &TextCursor, shape: &Shape) -> Option<Rect> {
|
||||
let paragraphs = text_content.paragraphs();
|
||||
if cursor.paragraph >= paragraphs.len() {
|
||||
return None;
|
||||
@@ -794,7 +823,7 @@ fn get_cursor_rect(text_content: &TextContent, cursor: &TextPositionWithAffinity
|
||||
let mut y_offset = valign_offset;
|
||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||
if idx == cursor.paragraph {
|
||||
let char_pos = cursor.offset;
|
||||
let char_pos = cursor.char_offset;
|
||||
|
||||
use skia_safe::textlayout::{RectHeightStyle, RectWidthStyle};
|
||||
let rects = laid_out_para.get_rects_for_range(
|
||||
@@ -869,13 +898,13 @@ fn get_selection_rects(
|
||||
.map(|span| span.text.chars().count())
|
||||
.sum();
|
||||
let range_start = if para_idx == start.paragraph {
|
||||
start.offset
|
||||
start.char_offset
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let range_end = if para_idx == end.paragraph {
|
||||
end.offset
|
||||
end.char_offset
|
||||
} else {
|
||||
para_char_count
|
||||
};
|
||||
@@ -914,40 +943,40 @@ fn paragraph_char_count(para: &Paragraph) -> usize {
|
||||
}
|
||||
|
||||
/// Clamp a cursor position to valid bounds within the text content.
|
||||
fn clamp_cursor(position: TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
fn clamp_cursor(cursor: TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
if paragraphs.is_empty() {
|
||||
return TextPositionWithAffinity::new_without_affinity(0, 0);
|
||||
return TextCursor::new(0, 0);
|
||||
}
|
||||
|
||||
let para_idx = position.paragraph.min(paragraphs.len() - 1);
|
||||
let para_idx = cursor.paragraph.min(paragraphs.len() - 1);
|
||||
let para_len = paragraph_char_count(¶graphs[para_idx]);
|
||||
let char_offset = position.offset.min(para_len);
|
||||
let char_offset = cursor.char_offset.min(para_len);
|
||||
|
||||
TextPositionWithAffinity::new_without_affinity(para_idx, char_offset)
|
||||
TextCursor::new(para_idx, char_offset)
|
||||
}
|
||||
|
||||
/// Move cursor left by one character.
|
||||
fn move_cursor_backward(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
if cursor.offset > 0 {
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, cursor.offset - 1)
|
||||
fn move_cursor_backward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
if cursor.char_offset > 0 {
|
||||
TextCursor::new(cursor.paragraph, cursor.char_offset - 1)
|
||||
} else if cursor.paragraph > 0 {
|
||||
let prev_para = cursor.paragraph - 1;
|
||||
let char_count = paragraph_char_count(¶graphs[prev_para]);
|
||||
TextPositionWithAffinity::new_without_affinity(prev_para, char_count)
|
||||
TextCursor::new(prev_para, char_count)
|
||||
} else {
|
||||
*cursor
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor right by one character.
|
||||
fn move_cursor_forward(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
fn move_cursor_forward(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
let para = ¶graphs[cursor.paragraph];
|
||||
let char_count = paragraph_char_count(para);
|
||||
|
||||
if cursor.offset < char_count {
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, cursor.offset + 1)
|
||||
if cursor.char_offset < char_count {
|
||||
TextCursor::new(cursor.paragraph, cursor.char_offset + 1)
|
||||
} else if cursor.paragraph < paragraphs.len() - 1 {
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph + 1, 0)
|
||||
TextCursor::new(cursor.paragraph + 1, 0)
|
||||
} else {
|
||||
*cursor
|
||||
}
|
||||
@@ -955,52 +984,52 @@ fn move_cursor_forward(cursor: &TextPositionWithAffinity, paragraphs: &[Paragrap
|
||||
|
||||
/// Move cursor up by one line.
|
||||
fn move_cursor_up(
|
||||
cursor: &TextPositionWithAffinity,
|
||||
cursor: &TextCursor,
|
||||
paragraphs: &[Paragraph],
|
||||
_text_content: &TextContent,
|
||||
_shape: &Shape,
|
||||
) -> TextPositionWithAffinity {
|
||||
) -> TextCursor {
|
||||
// TODO: Implement proper line-based navigation using line metrics
|
||||
if cursor.paragraph > 0 {
|
||||
let prev_para = cursor.paragraph - 1;
|
||||
let char_count = paragraph_char_count(¶graphs[prev_para]);
|
||||
let new_offset = cursor.offset.min(char_count);
|
||||
TextPositionWithAffinity::new_without_affinity(prev_para, new_offset)
|
||||
let new_offset = cursor.char_offset.min(char_count);
|
||||
TextCursor::new(prev_para, new_offset)
|
||||
} else {
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, 0)
|
||||
TextCursor::new(cursor.paragraph, 0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor down by one line.
|
||||
fn move_cursor_down(
|
||||
cursor: &TextPositionWithAffinity,
|
||||
cursor: &TextCursor,
|
||||
paragraphs: &[Paragraph],
|
||||
_text_content: &TextContent,
|
||||
_shape: &Shape,
|
||||
) -> TextPositionWithAffinity {
|
||||
) -> TextCursor {
|
||||
// TODO: Implement proper line-based navigation using line metrics
|
||||
if cursor.paragraph < paragraphs.len() - 1 {
|
||||
let next_para = cursor.paragraph + 1;
|
||||
let char_count = paragraph_char_count(¶graphs[next_para]);
|
||||
let new_offset = cursor.offset.min(char_count);
|
||||
TextPositionWithAffinity::new_without_affinity(next_para, new_offset)
|
||||
let new_offset = cursor.char_offset.min(char_count);
|
||||
TextCursor::new(next_para, new_offset)
|
||||
} else {
|
||||
let char_count = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, char_count)
|
||||
TextCursor::new(cursor.paragraph, char_count)
|
||||
}
|
||||
}
|
||||
|
||||
/// Move cursor to start of current line.
|
||||
fn move_cursor_line_start(cursor: &TextPositionWithAffinity, _paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
fn move_cursor_line_start(cursor: &TextCursor, _paragraphs: &[Paragraph]) -> TextCursor {
|
||||
// TODO: Implement proper line-start using line metrics
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, 0)
|
||||
TextCursor::new(cursor.paragraph, 0)
|
||||
}
|
||||
|
||||
/// Move cursor to end of current line.
|
||||
fn move_cursor_line_end(cursor: &TextPositionWithAffinity, paragraphs: &[Paragraph]) -> TextPositionWithAffinity {
|
||||
fn move_cursor_line_end(cursor: &TextCursor, paragraphs: &[Paragraph]) -> TextCursor {
|
||||
// TODO: Implement proper line-end using line metrics
|
||||
let char_count = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
TextPositionWithAffinity::new_without_affinity(cursor.paragraph, char_count)
|
||||
TextCursor::new(cursor.paragraph, char_count)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -1028,7 +1057,7 @@ fn find_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, u
|
||||
/// Insert text at a cursor position. Returns the new character offset after insertion.
|
||||
fn insert_text_at_cursor(
|
||||
text_content: &mut TextContent,
|
||||
cursor: &TextPositionWithAffinity,
|
||||
cursor: &TextCursor,
|
||||
text: &str,
|
||||
) -> Option<usize> {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
@@ -1048,7 +1077,7 @@ 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_span_at_offset(para, cursor.char_offset)?;
|
||||
|
||||
let children = para.children_mut();
|
||||
let span = &mut children[span_idx];
|
||||
@@ -1063,7 +1092,7 @@ fn insert_text_at_cursor(
|
||||
new_text.insert_str(byte_offset, text);
|
||||
span.set_text(new_text);
|
||||
|
||||
Some(cursor.offset + text.chars().count())
|
||||
Some(cursor.char_offset + text.chars().count())
|
||||
}
|
||||
|
||||
/// Delete a range of text specified by a selection.
|
||||
@@ -1079,18 +1108,18 @@ fn delete_selection_range(text_content: &mut TextContent, selection: &TextSelect
|
||||
if start.paragraph == end.paragraph {
|
||||
delete_range_in_paragraph(
|
||||
&mut paragraphs[start.paragraph],
|
||||
start.offset,
|
||||
end.offset,
|
||||
start.char_offset,
|
||||
end.char_offset,
|
||||
);
|
||||
} else {
|
||||
let start_para_len = paragraph_char_count(¶graphs[start.paragraph]);
|
||||
delete_range_in_paragraph(
|
||||
&mut paragraphs[start.paragraph],
|
||||
start.offset,
|
||||
start.char_offset,
|
||||
start_para_len,
|
||||
);
|
||||
|
||||
delete_range_in_paragraph(&mut paragraphs[end.paragraph], 0, end.offset);
|
||||
delete_range_in_paragraph(&mut paragraphs[end.paragraph], 0, end.char_offset);
|
||||
|
||||
if end.paragraph < paragraphs.len() {
|
||||
let end_para_children: Vec<_> =
|
||||
@@ -1189,13 +1218,13 @@ fn delete_range_in_paragraph(para: &mut Paragraph, start_offset: usize, end_offs
|
||||
}
|
||||
|
||||
/// Delete the character before the cursor. Returns the new cursor position.
|
||||
fn delete_char_before(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) -> Option<TextPositionWithAffinity> {
|
||||
if cursor.offset > 0 {
|
||||
fn delete_char_before(text_content: &mut TextContent, cursor: &TextCursor) -> Option<TextCursor> {
|
||||
if cursor.char_offset > 0 {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
let para = &mut paragraphs[cursor.paragraph];
|
||||
let delete_pos = cursor.offset - 1;
|
||||
delete_range_in_paragraph(para, delete_pos, cursor.offset);
|
||||
Some(TextPositionWithAffinity::new_without_affinity(cursor.paragraph, delete_pos))
|
||||
let delete_pos = cursor.char_offset - 1;
|
||||
delete_range_in_paragraph(para, delete_pos, cursor.char_offset);
|
||||
Some(TextCursor::new(cursor.paragraph, delete_pos))
|
||||
} else if cursor.paragraph > 0 {
|
||||
let prev_para_idx = cursor.paragraph - 1;
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
@@ -1211,14 +1240,14 @@ fn delete_char_before(text_content: &mut TextContent, cursor: &TextPositionWithA
|
||||
|
||||
paragraphs.remove(cursor.paragraph);
|
||||
|
||||
Some(TextPositionWithAffinity::new_without_affinity(prev_para_idx, prev_para_len))
|
||||
Some(TextCursor::new(prev_para_idx, prev_para_len))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete the character after the cursor.
|
||||
fn delete_char_after(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) {
|
||||
fn delete_char_after(text_content: &mut TextContent, cursor: &TextCursor) {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
if cursor.paragraph >= paragraphs.len() {
|
||||
return;
|
||||
@@ -1226,9 +1255,9 @@ fn delete_char_after(text_content: &mut TextContent, cursor: &TextPositionWithAf
|
||||
|
||||
let para_len = paragraph_char_count(¶graphs[cursor.paragraph]);
|
||||
|
||||
if cursor.offset < para_len {
|
||||
if cursor.char_offset < para_len {
|
||||
let para = &mut paragraphs[cursor.paragraph];
|
||||
delete_range_in_paragraph(para, cursor.offset, cursor.offset + 1);
|
||||
delete_range_in_paragraph(para, cursor.char_offset, cursor.char_offset + 1);
|
||||
} else if cursor.paragraph < paragraphs.len() - 1 {
|
||||
let next_para_idx = cursor.paragraph + 1;
|
||||
let next_children: Vec<_> = paragraphs[next_para_idx].children_mut().drain(..).collect();
|
||||
@@ -1241,7 +1270,7 @@ fn delete_char_after(text_content: &mut TextContent, cursor: &TextPositionWithAf
|
||||
}
|
||||
|
||||
/// Split a paragraph at the cursor position. Returns true if split was successful.
|
||||
fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextPositionWithAffinity) -> bool {
|
||||
fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextCursor) -> bool {
|
||||
let paragraphs = text_content.paragraphs_mut();
|
||||
if cursor.paragraph >= paragraphs.len() {
|
||||
return false;
|
||||
@@ -1249,7 +1278,7 @@ fn split_paragraph_at_cursor(text_content: &mut TextContent, cursor: &TextPositi
|
||||
|
||||
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_span_at_offset(para, cursor.char_offset) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -x
|
||||
|
||||
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"}
|
||||
export SKIA_BINARIES_URL=${SKIA_BINARIES_URL:-"https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"}
|
||||
export CARGO_BUILD_TARGET=${CARGO_BUILD_TARGET:-"x86_64-unknown-linux-gnu"};
|
||||
|
||||
_SCRIPT_DIR=$(dirname $0);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
_SCRIPT_DIR=$(dirname $0);
|
||||
|
||||
export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.87.0/skia-binaries-e551f334ad5cbdf43abf-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"
|
||||
export SKIA_BINARIES_URL="https://github.com/penpot/skia-binaries/releases/download/0.93.1/skia-binaries-319323662b1685a112f5-x86_64-unknown-linux-gnu-gl-svg-textlayout-binary-cache-webp.tar.gz"
|
||||
|
||||
pushd $_SCRIPT_DIR;
|
||||
cargo watch -x "test --bin render_wasm -- --show-output"
|
||||
|
||||
Reference in New Issue
Block a user