mirror of
https://github.com/penpot/penpot.git
synced 2026-09-09 13:39:02 +00:00
Merge pull request #8593 from penpot/azazeln28-feat-text-editor-composition-update
🎉 Feat add text editor composition update
This commit is contained in:
commit
d59aa03924
@ -75,7 +75,22 @@
|
|||||||
on-composition-start
|
on-composition-start
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn [_event]
|
(fn [_event]
|
||||||
(reset! composing? true)))
|
(reset! composing? true)
|
||||||
|
(text-editor/text-editor-composition-start)))
|
||||||
|
|
||||||
|
on-composition-update
|
||||||
|
(mf/use-fn
|
||||||
|
(fn [event]
|
||||||
|
(when-not composing?
|
||||||
|
(reset! composing? true))
|
||||||
|
|
||||||
|
(let [data (.-data event)]
|
||||||
|
(when data
|
||||||
|
(text-editor/text-editor-composition-update data)
|
||||||
|
(sync-wasm-text-editor-content!)
|
||||||
|
(wasm.api/request-render "text-composition"))
|
||||||
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
|
(set! (.-textContent node) "")))))
|
||||||
|
|
||||||
on-composition-end
|
on-composition-end
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -83,7 +98,7 @@
|
|||||||
(reset! composing? false)
|
(reset! composing? false)
|
||||||
(let [data (.-data event)]
|
(let [data (.-data event)]
|
||||||
(when data
|
(when data
|
||||||
(text-editor/text-editor-insert-text data)
|
(text-editor/text-editor-composition-end data)
|
||||||
(sync-wasm-text-editor-content!)
|
(sync-wasm-text-editor-content!)
|
||||||
(wasm.api/request-render "text-composition"))
|
(wasm.api/request-render "text-composition"))
|
||||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||||
@ -326,6 +341,7 @@
|
|||||||
:contentEditable true
|
:contentEditable true
|
||||||
:suppressContentEditableWarning true
|
:suppressContentEditableWarning true
|
||||||
:on-composition-start on-composition-start
|
:on-composition-start on-composition-start
|
||||||
|
:on-composition-update on-composition-update
|
||||||
:on-composition-end on-composition-end
|
:on-composition-end on-composition-end
|
||||||
:on-key-down on-key-down
|
:on-key-down on-key-down
|
||||||
:on-input on-input
|
:on-input on-input
|
||||||
|
|||||||
@ -66,17 +66,48 @@
|
|||||||
(let [res (h/call wasm/internal-module "_text_editor_poll_event")]
|
(let [res (h/call wasm/internal-module "_text_editor_poll_event")]
|
||||||
res)))
|
res)))
|
||||||
|
|
||||||
(defn text-editor-insert-text
|
(defn text-editor-encode-text-pre
|
||||||
[text]
|
[text]
|
||||||
(when wasm/context-initialized?
|
(when (and (not (empty? text))
|
||||||
|
wasm/context-initialized?)
|
||||||
(let [encoder (js/TextEncoder.)
|
(let [encoder (js/TextEncoder.)
|
||||||
buf (.encode encoder text)
|
buf (.encode encoder text)
|
||||||
heapu8 (mem/get-heap-u8)
|
heapu8 (mem/get-heap-u8)
|
||||||
size (mem/size buf)
|
size (mem/size buf)
|
||||||
offset (mem/alloc size)]
|
offset (mem/alloc size)]
|
||||||
(mem/write-buffer offset heapu8 buf)
|
(mem/write-buffer offset heapu8 buf))))
|
||||||
(h/call wasm/internal-module "_text_editor_insert_text")
|
|
||||||
(mem/free))))
|
(defn text-editor-encode-text-post
|
||||||
|
[text]
|
||||||
|
(when (and (not (empty? text))
|
||||||
|
wasm/context-initialized?)
|
||||||
|
(mem/free)))
|
||||||
|
|
||||||
|
(defn text-editor-composition-start
|
||||||
|
[]
|
||||||
|
(when wasm/context-initialized?
|
||||||
|
(h/call wasm/internal-module "_text_editor_composition_start")))
|
||||||
|
|
||||||
|
(defn text-editor-composition-update
|
||||||
|
[text]
|
||||||
|
(when wasm/context-initialized?
|
||||||
|
(text-editor-encode-text-pre text)
|
||||||
|
(h/call wasm/internal-module "_text_editor_composition_update")
|
||||||
|
(text-editor-encode-text-post text)))
|
||||||
|
|
||||||
|
(defn text-editor-composition-end
|
||||||
|
[text]
|
||||||
|
(when wasm/context-initialized?
|
||||||
|
(text-editor-encode-text-pre text)
|
||||||
|
(h/call wasm/internal-module "_text_editor_composition_end")
|
||||||
|
(text-editor-encode-text-post text)))
|
||||||
|
|
||||||
|
(defn text-editor-insert-text
|
||||||
|
[text]
|
||||||
|
(when wasm/context-initialized?
|
||||||
|
(text-editor-encode-text-pre text)
|
||||||
|
(h/call wasm/internal-module "_text_editor_insert_text")
|
||||||
|
(text-editor-encode-text-post text)))
|
||||||
|
|
||||||
(defn text-editor-delete-backward
|
(defn text-editor-delete-backward
|
||||||
([]
|
([]
|
||||||
|
|||||||
@ -103,9 +103,68 @@ pub struct TextEditorTheme {
|
|||||||
pub cursor_color: Color,
|
pub cursor_color: Color,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct TextComposition {
|
||||||
|
pub previous: String,
|
||||||
|
pub current: String,
|
||||||
|
pub is_composing: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextComposition {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
previous: String::new(),
|
||||||
|
current: String::new(),
|
||||||
|
is_composing: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start(&mut self) -> bool {
|
||||||
|
if self.is_composing {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.is_composing = true;
|
||||||
|
self.previous = String::new();
|
||||||
|
self.current = String::new();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, text: &str) -> bool {
|
||||||
|
if !self.is_composing {
|
||||||
|
self.is_composing = true;
|
||||||
|
}
|
||||||
|
self.previous = self.current.clone();
|
||||||
|
self.current = text.to_owned();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end(&mut self) -> bool {
|
||||||
|
if !self.is_composing {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.is_composing = false;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selection(&self, selection: &TextSelection) -> TextSelection {
|
||||||
|
if self.previous.is_empty() {
|
||||||
|
return *selection;
|
||||||
|
}
|
||||||
|
|
||||||
|
let focus = selection.focus;
|
||||||
|
let previous_len = self.previous.chars().count();
|
||||||
|
let anchor = TextPositionWithAffinity::new_without_affinity(
|
||||||
|
focus.paragraph,
|
||||||
|
focus.offset + previous_len,
|
||||||
|
);
|
||||||
|
|
||||||
|
TextSelection { anchor, focus }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct TextEditorState {
|
pub struct TextEditorState {
|
||||||
pub theme: TextEditorTheme,
|
pub theme: TextEditorTheme,
|
||||||
pub selection: TextSelection,
|
pub selection: TextSelection,
|
||||||
|
pub composition: TextComposition,
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
// This property indicates that we've started
|
// This property indicates that we've started
|
||||||
// selecting something with the pointer.
|
// selecting something with the pointer.
|
||||||
@ -125,6 +184,7 @@ impl TextEditorState {
|
|||||||
cursor_color: CURSOR_COLOR,
|
cursor_color: CURSOR_COLOR,
|
||||||
},
|
},
|
||||||
selection: TextSelection::new(),
|
selection: TextSelection::new(),
|
||||||
|
composition: TextComposition::new(),
|
||||||
is_active: false,
|
is_active: false,
|
||||||
is_pointer_selection_active: false,
|
is_pointer_selection_active: false,
|
||||||
active_shape_id: None,
|
active_shape_id: None,
|
||||||
|
|||||||
@ -293,6 +293,135 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
|||||||
// TEXT OPERATIONS
|
// TEXT OPERATIONS
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn text_editor_composition_start() -> Result<()> {
|
||||||
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
state.text_editor_state.composition.start();
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn text_editor_composition_end() -> Result<()> {
|
||||||
|
let bytes = crate::mem::bytes();
|
||||||
|
let text = match String::from_utf8(bytes) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(_) => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
state.text_editor_state.composition.update(&text);
|
||||||
|
|
||||||
|
let selection = state
|
||||||
|
.text_editor_state
|
||||||
|
.composition
|
||||||
|
.get_selection(&state.text_editor_state.selection);
|
||||||
|
text_helpers::delete_selection_range(text_content, &selection);
|
||||||
|
|
||||||
|
let cursor = state.text_editor_state.selection.focus;
|
||||||
|
if let Some(new_cursor) =
|
||||||
|
text_helpers::insert_text_with_newlines(text_content, &cursor, &text)
|
||||||
|
{
|
||||||
|
state.text_editor_state.selection.set_caret(new_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
text_content.layout.paragraphs.clear();
|
||||||
|
text_content.layout.paragraph_builders.clear();
|
||||||
|
|
||||||
|
state.text_editor_state.reset_blink();
|
||||||
|
state
|
||||||
|
.text_editor_state
|
||||||
|
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||||
|
state
|
||||||
|
.text_editor_state
|
||||||
|
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||||
|
|
||||||
|
state.render_state.mark_touched(shape_id);
|
||||||
|
|
||||||
|
state.text_editor_state.composition.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
crate::mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
#[wasm_error]
|
||||||
|
pub extern "C" fn text_editor_composition_update() -> Result<()> {
|
||||||
|
let bytes = crate::mem::bytes();
|
||||||
|
let text = match String::from_utf8(bytes) {
|
||||||
|
Ok(text) => text,
|
||||||
|
Err(_) => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
with_state_mut!(state, {
|
||||||
|
if !state.text_editor_state.is_active {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(shape_id) = state.text_editor_state.active_shape_id else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(shape) = state.shapes.get_mut(&shape_id) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Type::Text(text_content) = &mut shape.shape_type else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
state.text_editor_state.composition.update(&text);
|
||||||
|
|
||||||
|
let selection = state
|
||||||
|
.text_editor_state
|
||||||
|
.composition
|
||||||
|
.get_selection(&state.text_editor_state.selection);
|
||||||
|
text_helpers::delete_selection_range(text_content, &selection);
|
||||||
|
|
||||||
|
let cursor = state.text_editor_state.selection.focus;
|
||||||
|
text_helpers::insert_text_with_newlines(text_content, &cursor, &text);
|
||||||
|
|
||||||
|
text_content.layout.paragraphs.clear();
|
||||||
|
text_content.layout.paragraph_builders.clear();
|
||||||
|
|
||||||
|
state.text_editor_state.reset_blink();
|
||||||
|
state
|
||||||
|
.text_editor_state
|
||||||
|
.push_event(crate::state::TextEditorEvent::ContentChanged);
|
||||||
|
state
|
||||||
|
.text_editor_state
|
||||||
|
.push_event(crate::state::TextEditorEvent::NeedsLayout);
|
||||||
|
|
||||||
|
state.render_state.mark_touched(shape_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
crate::mem::free_bytes()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: Review if all the return Ok(()) should be Err instead.
|
// FIXME: Review if all the return Ok(()) should be Err instead.
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
#[wasm_error]
|
#[wasm_error]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user