mirror of
https://github.com/penpot/penpot.git
synced 2026-03-20 17:33:44 +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:
@@ -75,7 +75,22 @@
|
||||
on-composition-start
|
||||
(mf/use-fn
|
||||
(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
|
||||
(mf/use-fn
|
||||
@@ -83,7 +98,7 @@
|
||||
(reset! composing? false)
|
||||
(let [data (.-data event)]
|
||||
(when data
|
||||
(text-editor/text-editor-insert-text data)
|
||||
(text-editor/text-editor-composition-end data)
|
||||
(sync-wasm-text-editor-content!)
|
||||
(wasm.api/request-render "text-composition"))
|
||||
(when-let [node (mf/ref-val contenteditable-ref)]
|
||||
@@ -326,6 +341,7 @@
|
||||
:contentEditable true
|
||||
:suppressContentEditableWarning true
|
||||
:on-composition-start on-composition-start
|
||||
:on-composition-update on-composition-update
|
||||
:on-composition-end on-composition-end
|
||||
:on-key-down on-key-down
|
||||
:on-input on-input
|
||||
|
||||
@@ -66,17 +66,48 @@
|
||||
(let [res (h/call wasm/internal-module "_text_editor_poll_event")]
|
||||
res)))
|
||||
|
||||
(defn text-editor-insert-text
|
||||
(defn text-editor-encode-text-pre
|
||||
[text]
|
||||
(when wasm/context-initialized?
|
||||
(when (and (not (empty? text))
|
||||
wasm/context-initialized?)
|
||||
(let [encoder (js/TextEncoder.)
|
||||
buf (.encode encoder text)
|
||||
heapu8 (mem/get-heap-u8)
|
||||
size (mem/size buf)
|
||||
offset (mem/alloc size)]
|
||||
(mem/write-buffer offset heapu8 buf)
|
||||
(h/call wasm/internal-module "_text_editor_insert_text")
|
||||
(mem/free))))
|
||||
(mem/write-buffer offset heapu8 buf))))
|
||||
|
||||
(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
|
||||
([]
|
||||
|
||||
@@ -103,9 +103,68 @@ pub struct TextEditorTheme {
|
||||
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 theme: TextEditorTheme,
|
||||
pub selection: TextSelection,
|
||||
pub composition: TextComposition,
|
||||
pub is_active: bool,
|
||||
// This property indicates that we've started
|
||||
// selecting something with the pointer.
|
||||
@@ -125,6 +184,7 @@ impl TextEditorState {
|
||||
cursor_color: CURSOR_COLOR,
|
||||
},
|
||||
selection: TextSelection::new(),
|
||||
composition: TextComposition::new(),
|
||||
is_active: false,
|
||||
is_pointer_selection_active: false,
|
||||
active_shape_id: None,
|
||||
|
||||
@@ -293,6 +293,135 @@ pub extern "C" fn text_editor_set_cursor_from_point(x: f32, y: f32) {
|
||||
// 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.
|
||||
#[no_mangle]
|
||||
#[wasm_error]
|
||||
|
||||
Reference in New Issue
Block a user