Merge pull request #8526 from penpot/azazeln28-feat-double-click-word-boundary-selection

🎉 Add word boundary selection
This commit is contained in:
Alejandro Alonso 2026-03-09 12:53:30 +01:00 committed by GitHub
commit bdfa176b2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 125 additions and 9 deletions

View File

@ -235,28 +235,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
@ -303,6 +310,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

@ -122,6 +122,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 })