🎉 Add word boundary selection

This commit is contained in:
Aitor Moreno
2026-03-04 10:44:00 +01:00
parent ffae6d4281
commit 0b41a910bf
5 changed files with 125 additions and 9 deletions

View File

@@ -220,28 +220,35 @@
(fn [^js event]
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-pointer-down (.-x off-pt) (.-y off-pt)))))
(wasm.api/text-editor-pointer-down off-pt))))
on-pointer-move
(mf/use-fn
(fn [^js event]
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-pointer-move (.-x off-pt) (.-y off-pt)))))
(wasm.api/text-editor-pointer-move off-pt))))
on-pointer-up
(mf/use-fn
(fn [^js event]
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-pointer-up (.-x off-pt) (.-y off-pt)))))
(wasm.api/text-editor-pointer-up off-pt))))
on-click
(mf/use-fn
(fn [^js event]
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-set-cursor-from-offset (.-x off-pt) (.-y off-pt)))))
(wasm.api/text-editor-set-cursor-from-offset off-pt))))
on-double-click
(mf/use-fn
(fn [^js event]
(let [native-event (dom/event->native-event event)
off-pt (dom/get-offset-position native-event)]
(wasm.api/text-editor-select-word-boundary off-pt))))
on-focus
(mf/use-fn
@@ -288,6 +295,7 @@
[:foreignObject {:x x :y y :width width :height height}
[:div {:on-click on-click
:on-double-click on-double-click
:on-pointer-down on-pointer-down
:on-pointer-move on-pointer-move
:on-pointer-up on-pointer-up

View File

@@ -93,6 +93,7 @@
(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-select-word-boundary text-editor/text-editor-select-word-boundary)
(def text-editor-sync-content text-editor/text-editor-sync-content)
(def dpr

View File

@@ -25,28 +25,28 @@
(defn text-editor-set-cursor-from-offset
"Sets caret position from shape relative coordinates"
[x y]
[{:keys [x y]}]
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_set_cursor_from_offset" x y)))
(defn text-editor-set-cursor-from-point
"Sets caret position from screen (canvas) coordinates"
[x y]
[{:keys [x y]}]
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
(defn text-editor-pointer-down
[x y]
[{:keys [x y]}]
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_pointer_down" x y)))
(defn text-editor-pointer-move
[x y]
[{:keys [x y]}]
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_pointer_move" x y)))
(defn text-editor-pointer-up
[x y]
[{:keys [x y]}]
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_pointer_up" x y)))
@@ -100,6 +100,11 @@
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_select_all")))
(defn text-editor-select-word-boundary
[{:keys [x y]}]
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_select_word_boundary" x y)))
(defn text-editor-stop
[]
(when wasm/context-initialized?

View File

@@ -199,6 +199,80 @@ impl TextEditorState {
true
}
pub fn select_word_boundary(
&mut self,
content: &TextContent,
position: &TextPositionWithAffinity,
) {
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
self.is_pointer_selection_active = false;
let paragraphs = content.paragraphs();
if paragraphs.is_empty() || position.paragraph >= paragraphs.len() {
return;
}
let paragraph = &paragraphs[position.paragraph];
let paragraph_text: String = paragraph
.children()
.iter()
.map(|span| span.text.as_str())
.collect();
let chars: Vec<char> = paragraph_text.chars().collect();
if chars.is_empty() {
self.set_caret_from_position(&TextPositionWithAffinity::new_without_affinity(
position.paragraph,
0,
));
self.reset_blink();
self.push_event(TextEditorEvent::SelectionChanged);
return;
}
let mut offset = position.offset.min(chars.len());
if offset == chars.len() {
offset = offset.saturating_sub(1);
} else if !is_word_char(chars[offset]) && offset > 0 && is_word_char(chars[offset - 1]) {
offset -= 1;
}
if !is_word_char(chars[offset]) {
self.set_caret_from_position(&TextPositionWithAffinity::new_without_affinity(
position.paragraph,
position.offset.min(chars.len()),
));
self.reset_blink();
self.push_event(TextEditorEvent::SelectionChanged);
return;
}
let mut start = offset;
while start > 0 && is_word_char(chars[start - 1]) {
start -= 1;
}
let mut end = offset + 1;
while end < chars.len() && is_word_char(chars[end]) {
end += 1;
}
self.set_caret_from_position(&TextPositionWithAffinity::new_without_affinity(
position.paragraph,
start,
));
self.extend_selection_from_position(&TextPositionWithAffinity::new_without_affinity(
position.paragraph,
end,
));
self.reset_blink();
self.push_event(TextEditorEvent::SelectionChanged);
}
pub fn set_caret_from_position(&mut self, position: &TextPositionWithAffinity) {
self.selection.set_caret(*position);
self.push_event(TextEditorEvent::SelectionChanged);

View File

@@ -121,6 +121,34 @@ pub extern "C" fn text_editor_select_all() -> bool {
})
}
#[no_mangle]
pub extern "C" fn text_editor_select_word_boundary(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);
if let Some(position) = text_content.get_caret_position_from_shape_coords(&point) {
state
.text_editor_state
.select_word_boundary(text_content, &position);
}
})
}
#[no_mangle]
pub extern "C" fn text_editor_poll_event() -> u8 {
with_state_mut!(state, { state.text_editor_state.poll_event() as u8 })