mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🎉 Compute selection rects from pointer events
This commit is contained in:
parent
c58054d19c
commit
4477b2b4a0
@ -19,7 +19,6 @@
|
|||||||
[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.data.workspace.texts :as dwt]
|
|
||||||
[app.main.features :as features]
|
[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]
|
||||||
@ -50,41 +49,42 @@
|
|||||||
(mf/deps id blocked hidden type selected edition drawing-tool text-editing?
|
(mf/deps id blocked hidden type selected edition drawing-tool text-editing?
|
||||||
node-editing? grid-editing? drawing-path? create-comment? @z? @space?
|
node-editing? grid-editing? drawing-path? create-comment? @z? @space?
|
||||||
panning read-only?)
|
panning read-only?)
|
||||||
(fn [bevent]
|
(fn [event]
|
||||||
;; We need to handle editor related stuff here because
|
;; We need to handle editor related stuff here because
|
||||||
;; handling on editor dom node does not works properly.
|
;; handling on editor dom node does not works properly.
|
||||||
(let [target (dom/get-target bevent)
|
(let [target (dom/get-target event)
|
||||||
editor (txu/closest-text-editor-content target)]
|
editor (txu/closest-text-editor-content target)]
|
||||||
;; Capture mouse pointer to detect the movements even if cursor
|
;; Capture mouse pointer to detect the movements even if cursor
|
||||||
;; leaves the viewport or the browser itself
|
;; leaves the viewport or the browser itself
|
||||||
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
|
;; https://developer.mozilla.org/en-US/docs/Web/API/Element/setPointerCapture
|
||||||
(if editor
|
(if editor
|
||||||
(.setPointerCapture editor (.-pointerId bevent))
|
(.setPointerCapture editor (.-pointerId event))
|
||||||
(.setPointerCapture target (.-pointerId bevent))))
|
(.setPointerCapture target (.-pointerId event))))
|
||||||
|
|
||||||
(when (or (dom/class? (dom/get-target bevent) "viewport-controls")
|
(when (or (dom/class? (dom/get-target event) "viewport-controls")
|
||||||
(dom/class? (dom/get-target bevent) "viewport-selrect")
|
(dom/class? (dom/get-target event) "viewport-selrect")
|
||||||
(dom/child? (dom/get-target bevent) (dom/query ".grid-layout-editor")))
|
(dom/child? (dom/get-target event) (dom/query ".grid-layout-editor")))
|
||||||
|
|
||||||
(dom/stop-propagation bevent)
|
(dom/stop-propagation event)
|
||||||
|
|
||||||
(when-not @z?
|
(when-not @z?
|
||||||
(let [event (dom/event->native-event bevent)
|
(let [native-event (dom/event->native-event event)
|
||||||
ctrl? (kbd/ctrl? event)
|
ctrl? (kbd/ctrl? native-event)
|
||||||
meta? (kbd/meta? event)
|
meta? (kbd/meta? native-event)
|
||||||
shift? (kbd/shift? event)
|
shift? (kbd/shift? native-event)
|
||||||
alt? (kbd/alt? event)
|
alt? (kbd/alt? native-event)
|
||||||
mod? (kbd/mod? event)
|
mod? (kbd/mod? native-event)
|
||||||
|
off-pt (dom/get-offset-position native-event)
|
||||||
|
|
||||||
left-click? (and (not panning) (dom/left-mouse? bevent))
|
left-click? (and (not panning) (dom/left-mouse? event))
|
||||||
middle-click? (and (not panning) (dom/middle-mouse? bevent))]
|
middle-click? (and (not panning) (dom/middle-mouse? event))]
|
||||||
|
|
||||||
(cond
|
(cond
|
||||||
(or middle-click? (and left-click? @space?))
|
(or middle-click? (and left-click? @space?))
|
||||||
(do
|
(do
|
||||||
(dom/prevent-default bevent)
|
(dom/prevent-default event)
|
||||||
(if mod?
|
(if mod?
|
||||||
(let [raw-pt (dom/get-client-position event)
|
(let [raw-pt (dom/get-client-position native-event)
|
||||||
pt (uwvv/point->viewport raw-pt)]
|
pt (uwvv/point->viewport raw-pt)]
|
||||||
(st/emit! (dw/start-zooming pt)))
|
(st/emit! (dw/start-zooming pt)))
|
||||||
(st/emit! (dw/start-panning))))
|
(st/emit! (dw/start-panning))))
|
||||||
@ -94,18 +94,23 @@
|
|||||||
(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
|
;; Sync and stop WASM text editor when exiting edit mode
|
||||||
(when (and text-editing?
|
#_(when (and text-editing?
|
||||||
(features/active-feature? @st/state "render-wasm/v1")
|
(features/active-feature? @st/state "render-wasm/v1")
|
||||||
wasm.wasm/context-initialized?)
|
wasm.wasm/context-initialized?)
|
||||||
(when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)]
|
(when-let [{:keys [shape-id content]} (wasm.api/text-editor-sync-content)]
|
||||||
(st/emit! (dwt/v2-update-text-shape-content
|
(st/emit! (dwt/v2-update-text-shape-content
|
||||||
shape-id content
|
shape-id content
|
||||||
:update-name? true
|
:update-name? true
|
||||||
:finalize? true)))
|
:finalize? true)))
|
||||||
(wasm.api/text-editor-stop)))
|
(wasm.api/text-editor-stop)))
|
||||||
|
|
||||||
(when (and (not text-editing?)
|
(when (and (not text-editing?)
|
||||||
(not blocked)
|
(not blocked)
|
||||||
@ -187,10 +192,14 @@
|
|||||||
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?))
|
||||||
|
|
||||||
|
;; FIXME: Maybe we can transform this into a cond instead
|
||||||
|
;; of multiple (when)s.
|
||||||
(when (and hovering?
|
(when (and hovering?
|
||||||
(not @space?)
|
(not @space?)
|
||||||
(not edition)
|
(not edition)
|
||||||
@ -198,6 +207,8 @@
|
|||||||
(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
|
;; If clicking on a text shape and wasm render is enabled, forward cursor position
|
||||||
(when (and hovering?
|
(when (and hovering?
|
||||||
(not @space?)
|
(not @space?)
|
||||||
@ -208,9 +219,7 @@
|
|||||||
(when (and (= :text (:type hover-shape))
|
(when (and (= :text (:type hover-shape))
|
||||||
(features/active-feature? @st/state "text-editor-wasm/v1")
|
(features/active-feature? @st/state "text-editor-wasm/v1")
|
||||||
wasm.wasm/context-initialized?)
|
wasm.wasm/context-initialized?)
|
||||||
(let [raw-pt (dom/get-client-position event)]
|
(wasm.api/text-editor-set-cursor-from-point (.-x off-pt) (.-y off-pt)))))
|
||||||
;; FIXME
|
|
||||||
(wasm.api/text-editor-set-cursor-from-point (.-x raw-pt) (.-y raw-pt))))))
|
|
||||||
|
|
||||||
(when (and @z?
|
(when (and @z?
|
||||||
(not @space?)
|
(not @space?)
|
||||||
@ -261,6 +270,12 @@
|
|||||||
wasm.wasm/context-initialized?)
|
wasm.wasm/context-initialized?)
|
||||||
(wasm.api/text-editor-start id)))
|
(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
|
||||||
(reset! hover selected-shape)
|
(reset! hover selected-shape)
|
||||||
@ -310,20 +325,24 @@
|
|||||||
;; Release pointer on mouse up
|
;; Release pointer on mouse up
|
||||||
(.releasePointerCapture target (.-pointerId event)))
|
(.releasePointerCapture target (.-pointerId event)))
|
||||||
|
|
||||||
(let [event (dom/event->native-event event)
|
(let [native-event (dom/event->native-event event)
|
||||||
ctrl? (kbd/ctrl? event)
|
off-pt (dom/get-offset-position native-event)
|
||||||
shift? (kbd/shift? event)
|
ctrl? (kbd/ctrl? native-event)
|
||||||
alt? (kbd/alt? event)
|
shift? (kbd/shift? native-event)
|
||||||
meta? (kbd/meta? event)
|
alt? (kbd/alt? native-event)
|
||||||
|
meta? (kbd/meta? native-event)
|
||||||
|
|
||||||
left-click? (= 1 (.-which event))
|
left-click? (= 1 (.-which native-event))
|
||||||
middle-click? (= 2 (.-which event))]
|
middle-click? (= 2 (.-which native-event))]
|
||||||
|
|
||||||
(when left-click?
|
(when left-click?
|
||||||
(st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?)))
|
(st/emit! (mse/->MouseEvent :up ctrl? shift? alt? meta?))
|
||||||
|
|
||||||
|
(when (wasm.api/text-editor-is-active?)
|
||||||
|
(wasm.api/text-editor-pointer-up (.-x off-pt) (.-y off-pt))))
|
||||||
|
|
||||||
(when middle-click?
|
(when middle-click?
|
||||||
(dom/prevent-default event)
|
(dom/prevent-default native-event)
|
||||||
|
|
||||||
;; We store this so in Firefox the middle button won't do a paste of the content
|
;; 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)
|
(mf/set-ref-val! disable-paste-ref true)
|
||||||
@ -381,7 +400,9 @@
|
|||||||
(let [last-position (mf/use-var nil)]
|
(let [last-position (mf/use-var nil)]
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(let [raw-pt (dom/get-client-position event)
|
(let [native-event (unchecked-get event "nativeEvent")
|
||||||
|
off-pt (dom/get-offset-position native-event)
|
||||||
|
raw-pt (dom/get-client-position event)
|
||||||
pt (uwvv/point->viewport raw-pt)
|
pt (uwvv/point->viewport raw-pt)
|
||||||
|
|
||||||
;; We calculate the delta because Safari's MouseEvent.movementX/Y drop
|
;; We calculate the delta because Safari's MouseEvent.movementX/Y drop
|
||||||
@ -390,6 +411,12 @@
|
|||||||
(gpt/subtract raw-pt @last-position)
|
(gpt/subtract raw-pt @last-position)
|
||||||
(gpt/point 0 0))]
|
(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)
|
(rx/push! move-stream pt)
|
||||||
(reset! last-position raw-pt)
|
(reset! last-position raw-pt)
|
||||||
(st/emit! (mse/->PointerEvent :delta delta
|
(st/emit! (mse/->PointerEvent :delta delta
|
||||||
|
|||||||
@ -87,7 +87,11 @@
|
|||||||
(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-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-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-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 text-editor-sync-content text-editor/text-editor-sync-content)
|
||||||
|
|
||||||
(def dpr
|
(def dpr
|
||||||
@ -263,22 +267,6 @@
|
|||||||
[attrs]
|
[attrs]
|
||||||
(text-editor/apply-style-to-selection attrs use-shape set-shape-text-content))
|
(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
|
(defn set-parent-id
|
||||||
[id]
|
[id]
|
||||||
(let [buffer (uuid/get-u32 id)]
|
(let [buffer (uuid/get-u32 id)]
|
||||||
@ -996,6 +984,22 @@
|
|||||||
(render-finish)
|
(render-finish)
|
||||||
(perf/end-measure "set-view-box::zoom")))))
|
(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
|
(defn set-object
|
||||||
[shape]
|
[shape]
|
||||||
(perf/begin-measure "set-object")
|
(perf/begin-measure "set-object")
|
||||||
|
|||||||
@ -27,6 +27,21 @@
|
|||||||
(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)))
|
||||||
|
|
||||||
|
(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
|
(defn text-editor-update-blink
|
||||||
[timestamp-ms]
|
[timestamp-ms]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
@ -83,9 +98,12 @@
|
|||||||
(h/call wasm/internal-module "_text_editor_stop")))
|
(h/call wasm/internal-module "_text_editor_stop")))
|
||||||
|
|
||||||
(defn text-editor-is-active?
|
(defn text-editor-is-active?
|
||||||
[]
|
([id]
|
||||||
(when wasm/context-initialized?
|
(when wasm/context-initialized?
|
||||||
(not (zero? (h/call wasm/internal-module "_text_editor_is_active")))))
|
(not (zero? (h/call wasm/internal-module "_text_editor_is_active_with_id" id)))))
|
||||||
|
([]
|
||||||
|
(when wasm/context-initialized?
|
||||||
|
(not (zero? (h/call wasm/internal-module "_text_editor_is_active"))))))
|
||||||
|
|
||||||
(defn text-editor-export-content
|
(defn text-editor-export-content
|
||||||
[]
|
[]
|
||||||
|
|||||||
@ -76,3 +76,4 @@ export function getFills(fillStyle) {
|
|||||||
const [color, opacity] = getColor(fillStyle);
|
const [color, opacity] = getColor(fillStyle);
|
||||||
return `[["^ ","~:fill-color","${color}","~:fill-opacity",${opacity}]]`;
|
return `[["^ ","~:fill-color","${color}","~:fill-opacity",${opacity}]]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -162,12 +162,15 @@ class TextEditorPlayground {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.#module.call("use_shape", ...textShape.id);
|
this.#module.call("use_shape", ...textShape.id);
|
||||||
|
// FIXME: This function doesn't exists anymore.
|
||||||
|
/*
|
||||||
const caretPosition = this.#module.call(
|
const caretPosition = this.#module.call(
|
||||||
"get_caret_position_at",
|
"get_caret_position_at",
|
||||||
e.offsetX,
|
e.offsetX,
|
||||||
e.offsetY,
|
e.offsetY,
|
||||||
);
|
);
|
||||||
console.log("caretPosition", caretPosition);
|
console.log("caretPosition", caretPosition);
|
||||||
|
*/
|
||||||
};
|
};
|
||||||
|
|
||||||
#onResize = (_entries) => {
|
#onResize = (_entries) => {
|
||||||
|
|||||||
2
render-wasm/pnpm-workspace.yaml
Normal file
2
render-wasm/pnpm-workspace.yaml
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
@ -45,7 +45,11 @@ fn render_cursor(
|
|||||||
paint.set_color(editor_state.theme.cursor_color);
|
paint.set_color(editor_state.theme.cursor_color);
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
|
|
||||||
|
let shape_matrix = shape.get_matrix();
|
||||||
|
canvas.save();
|
||||||
|
canvas.concat(&shape_matrix);
|
||||||
canvas.draw_rect(rect, &paint);
|
canvas.draw_rect(rect, &paint);
|
||||||
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_selection(
|
fn render_selection(
|
||||||
@ -65,9 +69,14 @@ fn render_selection(
|
|||||||
paint.set_blend_mode(BlendMode::Multiply);
|
paint.set_blend_mode(BlendMode::Multiply);
|
||||||
paint.set_color(editor_state.theme.selection_color);
|
paint.set_color(editor_state.theme.selection_color);
|
||||||
paint.set_anti_alias(true);
|
paint.set_anti_alias(true);
|
||||||
|
|
||||||
|
let shape_matrix = shape.get_matrix();
|
||||||
|
canvas.save();
|
||||||
|
canvas.concat(&shape_matrix);
|
||||||
for rect in rects {
|
for rect in rects {
|
||||||
canvas.draw_rect(rect, &paint);
|
canvas.draw_rect(rect, &paint);
|
||||||
}
|
}
|
||||||
|
canvas.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn vertical_align_offset(
|
fn vertical_align_offset(
|
||||||
@ -99,8 +108,6 @@ fn calculate_cursor_rect(
|
|||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selrect = shape.selrect();
|
|
||||||
|
|
||||||
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
||||||
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
for (idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||||
if idx == cursor.paragraph {
|
if idx == cursor.paragraph {
|
||||||
@ -157,8 +164,8 @@ fn calculate_cursor_rect(
|
|||||||
};
|
};
|
||||||
|
|
||||||
return Some(Rect::from_xywh(
|
return Some(Rect::from_xywh(
|
||||||
selrect.x() + cursor_x,
|
cursor_x,
|
||||||
selrect.y() + y_offset,
|
y_offset,
|
||||||
editor_state.theme.cursor_width,
|
editor_state.theme.cursor_width,
|
||||||
cursor_height,
|
cursor_height,
|
||||||
));
|
));
|
||||||
@ -182,7 +189,6 @@ fn calculate_selection_rects(
|
|||||||
let paragraphs = text_content.paragraphs();
|
let paragraphs = text_content.paragraphs();
|
||||||
let layout_paragraphs: Vec<_> = text_content.layout.paragraphs.iter().flatten().collect();
|
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);
|
let mut y_offset = vertical_align_offset(shape, &layout_paragraphs);
|
||||||
|
|
||||||
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
for (para_idx, laid_out_para) in layout_paragraphs.iter().enumerate() {
|
||||||
@ -225,8 +231,8 @@ fn calculate_selection_rects(
|
|||||||
for text_box in text_boxes {
|
for text_box in text_boxes {
|
||||||
let r = text_box.rect;
|
let r = text_box.rect;
|
||||||
rects.push(Rect::from_xywh(
|
rects.push(Rect::from_xywh(
|
||||||
selrect.x() + r.left(),
|
r.left(),
|
||||||
selrect.y() + y_offset + r.top(),
|
y_offset + r.top(),
|
||||||
r.width(),
|
r.width(),
|
||||||
r.height(),
|
r.height(),
|
||||||
));
|
));
|
||||||
|
|||||||
@ -258,6 +258,18 @@ pub fn all_with_ancestors(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Shape {
|
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 {
|
pub fn new(id: Uuid) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
|
|||||||
@ -112,12 +112,15 @@ impl TextContentSize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Copy, Clone)]
|
||||||
pub struct TextPositionWithAffinity {
|
pub struct TextPositionWithAffinity {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub position_with_affinity: PositionWithAffinity,
|
pub position_with_affinity: PositionWithAffinity,
|
||||||
pub paragraph: i32,
|
pub paragraph: i32,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub span: i32,
|
pub span: i32,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub span_relative_offset: i32,
|
||||||
pub offset: i32,
|
pub offset: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,12 +129,14 @@ impl TextPositionWithAffinity {
|
|||||||
position_with_affinity: PositionWithAffinity,
|
position_with_affinity: PositionWithAffinity,
|
||||||
paragraph: i32,
|
paragraph: i32,
|
||||||
span: i32,
|
span: i32,
|
||||||
|
span_relative_offset: i32,
|
||||||
offset: i32,
|
offset: i32,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
position_with_affinity,
|
position_with_affinity,
|
||||||
paragraph,
|
paragraph,
|
||||||
span,
|
span,
|
||||||
|
span_relative_offset,
|
||||||
offset,
|
offset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -421,7 +426,10 @@ impl TextContent {
|
|||||||
self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y);
|
self.bounds = Rect::from_ltrb(p1.x, p1.y, p2.x, p2.y);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_caret_position_at(&self, point: &Point) -> Option<TextPositionWithAffinity> {
|
pub fn get_caret_position_from_shape_coords(
|
||||||
|
&self,
|
||||||
|
point: &Point,
|
||||||
|
) -> Option<TextPositionWithAffinity> {
|
||||||
let mut offset_y = 0.0;
|
let mut offset_y = 0.0;
|
||||||
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
let layout_paragraphs = self.layout.paragraphs.iter().flatten();
|
||||||
|
|
||||||
@ -487,6 +495,7 @@ impl TextContent {
|
|||||||
paragraph_index,
|
paragraph_index,
|
||||||
span_index,
|
span_index,
|
||||||
span_offset,
|
span_offset,
|
||||||
|
position_with_affinity.position,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -509,12 +518,23 @@ impl TextContent {
|
|||||||
0, // paragraph 0
|
0, // paragraph 0
|
||||||
0, // span 0
|
0, // span 0
|
||||||
0, // offset 0
|
0, // offset 0
|
||||||
|
0,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
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
|
/// Builds the ParagraphBuilders necessary to render
|
||||||
/// this text.
|
/// this text.
|
||||||
pub fn paragraph_builder_group_from_text(
|
pub fn paragraph_builder_group_from_text(
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use crate::shapes::TextPositionWithAffinity;
|
use crate::shapes::{TextContent, TextPositionWithAffinity};
|
||||||
use crate::uuid::Uuid;
|
use crate::uuid::Uuid;
|
||||||
use skia_safe::Color;
|
use skia_safe::{
|
||||||
|
textlayout::{Affinity, PositionWithAffinity},
|
||||||
|
Color,
|
||||||
|
};
|
||||||
|
|
||||||
/// Cursor position within text content.
|
/// Cursor position within text content.
|
||||||
/// Uses character offsets for precise positioning.
|
/// Uses character offsets for precise positioning.
|
||||||
@ -122,6 +125,9 @@ pub struct TextEditorState {
|
|||||||
pub theme: TextEditorTheme,
|
pub theme: TextEditorTheme,
|
||||||
pub selection: TextSelection,
|
pub selection: TextSelection,
|
||||||
pub is_active: bool,
|
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 active_shape_id: Option<Uuid>,
|
||||||
pub cursor_visible: bool,
|
pub cursor_visible: bool,
|
||||||
pub last_blink_time: f64,
|
pub last_blink_time: f64,
|
||||||
@ -138,6 +144,7 @@ impl TextEditorState {
|
|||||||
},
|
},
|
||||||
selection: TextSelection::new(),
|
selection: TextSelection::new(),
|
||||||
is_active: false,
|
is_active: false,
|
||||||
|
is_pointer_selection_active: false,
|
||||||
active_shape_id: None,
|
active_shape_id: None,
|
||||||
cursor_visible: true,
|
cursor_visible: true,
|
||||||
last_blink_time: 0.0,
|
last_blink_time: 0.0,
|
||||||
@ -151,6 +158,7 @@ impl TextEditorState {
|
|||||||
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 = TextSelection::new();
|
||||||
|
self.is_pointer_selection_active = false;
|
||||||
self.pending_events.clear();
|
self.pending_events.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +166,65 @@ 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.is_pointer_selection_active = false;
|
||||||
self.pending_events.clear();
|
self.pending_events.clear();
|
||||||
|
self.reset_blink();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start_pointer_selection(&mut self) -> bool {
|
||||||
|
if self.is_pointer_selection_active {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.is_pointer_selection_active = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop_pointer_selection(&mut self) -> bool {
|
||||||
|
if !self.is_pointer_selection_active {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.is_pointer_selection_active = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select_all(&mut self, content: &TextContent) -> bool {
|
||||||
|
self.is_pointer_selection_active = false;
|
||||||
|
self.set_caret_from_position(TextPositionWithAffinity::new(
|
||||||
|
PositionWithAffinity {
|
||||||
|
position: 0,
|
||||||
|
affinity: Affinity::Downstream,
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
));
|
||||||
|
let num_paragraphs = (content.paragraphs().len() - 1) as i32;
|
||||||
|
let Some(last_paragraph) = content.paragraphs().last() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let num_spans = (last_paragraph.children().len() - 1) as i32;
|
||||||
|
let Some(last_text_span) = last_paragraph.children().last() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let mut offset = 0;
|
||||||
|
for span in last_paragraph.children() {
|
||||||
|
offset += span.text.len();
|
||||||
|
}
|
||||||
|
self.extend_selection_from_position(TextPositionWithAffinity::new(
|
||||||
|
PositionWithAffinity {
|
||||||
|
position: offset as i32,
|
||||||
|
affinity: Affinity::Upstream,
|
||||||
|
},
|
||||||
|
num_paragraphs,
|
||||||
|
num_spans,
|
||||||
|
last_text_span.text.len() as i32,
|
||||||
|
offset as i32,
|
||||||
|
));
|
||||||
|
self.reset_blink();
|
||||||
|
self.push_event(crate::state::EditorEvent::SelectionChanged);
|
||||||
|
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_caret_from_position(&mut self, position: TextPositionWithAffinity) {
|
pub fn set_caret_from_position(&mut self, position: TextPositionWithAffinity) {
|
||||||
|
|||||||
@ -1,16 +1,13 @@
|
|||||||
use macros::ToJs;
|
use macros::ToJs;
|
||||||
|
|
||||||
use super::{fills::RawFillData, fonts::RawFontStyle};
|
use super::{fills::RawFillData, fonts::RawFontStyle};
|
||||||
use crate::math::{Matrix, Point};
|
|
||||||
use crate::mem::{self, SerializableResult};
|
use crate::mem::{self, SerializableResult};
|
||||||
use crate::shapes::{
|
use crate::shapes::{
|
||||||
self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
|
self, GrowType, Shape, TextAlign, TextDecoration, TextDirection, TextTransform, Type,
|
||||||
};
|
};
|
||||||
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
use crate::utils::{uuid_from_u32, uuid_from_u32_quartet};
|
||||||
use crate::{
|
use crate::{with_current_shape, with_current_shape_mut, with_state, with_state_mut, STATE};
|
||||||
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_SPAN_DATA_SIZE: usize = std::mem::size_of::<RawTextSpan>();
|
||||||
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
const RAW_PARAGRAPH_DATA_SIZE: usize = std::mem::size_of::<RawParagraphData>();
|
||||||
@ -388,32 +385,6 @@ pub extern "C" fn update_shape_text_layout_for(a: u32, b: u32, c: u32, d: u32) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub extern "C" fn get_caret_position_at(x: f32, y: f32) -> i32 {
|
|
||||||
with_state_mut_current_shape!(state, |shape: &Shape| {
|
|
||||||
if let Type::Text(text_content) = &shape.shape_type {
|
|
||||||
let mut matrix = Matrix::new_identity();
|
|
||||||
let shape_matrix = shape.get_concatenated_matrix(&state.shapes);
|
|
||||||
let view_matrix = state.render_state.viewbox.get_matrix();
|
|
||||||
if let Some(inv_view_matrix) = view_matrix.invert() {
|
|
||||||
matrix.post_concat(&inv_view_matrix);
|
|
||||||
matrix.post_concat(&shape_matrix);
|
|
||||||
|
|
||||||
let mapped_point = matrix.map_point(Point::new(x, y));
|
|
||||||
|
|
||||||
if let Some(position_with_affinity) =
|
|
||||||
text_content.get_caret_position_at(&mapped_point)
|
|
||||||
{
|
|
||||||
return position_with_affinity.position_with_affinity.position;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic!("Trying to get caret position of a shape that it's not a text shape");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
|
|
||||||
const RAW_POSITION_DATA_SIZE: usize = size_of::<shapes::PositionData>();
|
const RAW_POSITION_DATA_SIZE: usize = size_of::<shapes::PositionData>();
|
||||||
|
|
||||||
impl From<[u8; RAW_POSITION_DATA_SIZE]> for shapes::PositionData {
|
impl From<[u8; RAW_POSITION_DATA_SIZE]> for shapes::PositionData {
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
use macros::ToJs;
|
|
||||||
|
|
||||||
use crate::math::{Matrix, Point, Rect};
|
use crate::math::{Matrix, Point, Rect};
|
||||||
use crate::mem;
|
use crate::mem;
|
||||||
use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign};
|
use crate::shapes::{Paragraph, Shape, TextContent, Type, VerticalAlign};
|
||||||
@ -7,6 +5,7 @@ use crate::state::{TextCursor, TextSelection};
|
|||||||
use crate::utils::uuid_from_u32_quartet;
|
use crate::utils::uuid_from_u32_quartet;
|
||||||
use crate::utils::uuid_to_u32_quartet;
|
use crate::utils::uuid_to_u32_quartet;
|
||||||
use crate::{with_state, with_state_mut, STATE};
|
use crate::{with_state, with_state_mut, STATE};
|
||||||
|
use macros::ToJs;
|
||||||
|
|
||||||
#[derive(PartialEq, ToJs)]
|
#[derive(PartialEq, ToJs)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
@ -54,6 +53,17 @@ pub extern "C" fn text_editor_is_active() -> bool {
|
|||||||
with_state!(state, { state.text_editor_state.is_active })
|
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]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) {
|
pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) {
|
||||||
with_state!(state, {
|
with_state!(state, {
|
||||||
@ -70,45 +80,25 @@ pub extern "C" fn text_editor_get_active_shape_id(buffer_ptr: *mut u32) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_select_all() {
|
pub extern "C" fn text_editor_select_all() -> bool {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
if !state.text_editor_state.is_active {
|
if !state.text_editor_state.is_active {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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 false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(shape) = state.shapes.get(&shape_id) else {
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
return;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Type::Text(text_content) = &shape.shape_type else {
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
return;
|
return false;
|
||||||
};
|
};
|
||||||
|
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]
|
#[no_mangle]
|
||||||
@ -121,146 +111,127 @@ pub extern "C" fn text_editor_poll_event() -> u8 {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
pub extern "C" fn text_editor_pointer_down(x: f32, y: f32) {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
if !state.text_editor_state.is_active {
|
if !state.text_editor_state.is_active {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
};
|
};
|
||||||
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
let Some(inv_shape_matrix) = shape_matrix.invert() else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
let point = Point::new(x, y);
|
||||||
let mut matrix = Matrix::new_identity();
|
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||||
matrix.post_concat(&inv_view_matrix);
|
let shape_matrix = shape.get_matrix();
|
||||||
matrix.post_concat(&inv_shape_matrix);
|
state.text_editor_state.start_pointer_selection();
|
||||||
|
if let Some(position) =
|
||||||
let mapped_point = matrix.map_point(Point::new(x, y));
|
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||||
|
{
|
||||||
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);
|
state.text_editor_state.set_caret_from_position(position);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub extern "C" fn text_editor_extend_selection_to_point(x: f32, y: f32) {
|
pub extern "C" fn text_editor_pointer_move(x: f32, y: f32) {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||||
|
let point = Point::new(x, y);
|
||||||
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let shape_matrix = shape.get_matrix();
|
||||||
|
let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !state.text_editor_state.is_pointer_selection_active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(position) =
|
||||||
|
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.text_editor_state
|
||||||
|
.extend_selection_from_position(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn text_editor_pointer_up(x: f32, y: f32) {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let view_matrix: Matrix = state.render_state.viewbox.get_matrix();
|
||||||
|
let point = Point::new(x, y);
|
||||||
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some(shape) = state.shapes.get(&shape_id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let shape_matrix = shape.get_matrix();
|
||||||
|
let Some(_shape_rel_point) = Shape::get_relative_point(&point, &view_matrix, &shape_matrix)
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if !state.text_editor_state.is_pointer_selection_active {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(position) =
|
||||||
|
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||||
|
{
|
||||||
|
state
|
||||||
|
.text_editor_state
|
||||||
|
.extend_selection_from_position(position);
|
||||||
|
}
|
||||||
|
state.text_editor_state.stop_pointer_selection();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||||
with_state_mut!(state, {
|
with_state_mut!(state, {
|
||||||
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 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;
|
||||||
};
|
};
|
||||||
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
let shape_matrix = shape.get_matrix();
|
||||||
let Some(inv_shape_matrix) = shape_matrix.invert() else {
|
let Type::Text(text_content) = &shape.shape_type else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
if let Some(position) =
|
||||||
let mut matrix = Matrix::new_identity();
|
text_content.get_caret_position_from_screen_coords(&point, &view_matrix, &shape_matrix)
|
||||||
matrix.post_concat(&inv_view_matrix);
|
{
|
||||||
matrix.post_concat(&inv_shape_matrix);
|
state.text_editor_state.set_caret_from_position(position);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user