🎉 Add overtype mode to text editor

This commit is contained in:
Aitor Moreno 2026-04-21 14:52:49 +02:00
parent 3561b2d1eb
commit 0f2f0b6137
6 changed files with 139 additions and 5 deletions

View File

@ -189,6 +189,13 @@
(sync-wasm-text-editor-content!)
(wasm.api/request-render "text-delete-forward"))
;; Insert
(= key "Insert")
(do
(dom/prevent-default event)
(text-editor/text-editor-toggle-overtype-mode)
(wasm.api/request-render "text-overtype-mode"))
;; Arrow keys
(= key "ArrowLeft")
(do

View File

@ -204,6 +204,7 @@
(def text-editor-blur text-editor/text-editor-blur)
(def text-editor-set-cursor-from-offset text-editor/text-editor-set-cursor-from-offset)
(def text-editor-set-cursor-from-point text-editor/text-editor-set-cursor-from-point)
(def text-editor-toggle-overtype-mode text-editor/text-editor-toggle-overtype-mode)
(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)

View File

@ -143,6 +143,12 @@
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_set_cursor_from_point" x y)))
(defn text-editor-toggle-overtype-mode
"Sets overtype mode"
[overtype]
(when wasm/context-initialized?
(h/call wasm/internal-module "_text_editor_toggle_overtype_mode" overtype)))
(defn text-editor-pointer-down
[{:keys [x y]}]
(when wasm/context-initialized?

View File

@ -326,6 +326,7 @@ pub struct TextEditorState {
// selecting something with the pointer.
pub is_pointer_selection_active: bool,
pub is_click_event_skipped: bool,
pub is_overtype_mode: bool,
pub active_shape_id: Option<Uuid>,
pub cursor_visible: bool,
pub last_blink_time: f64,
@ -345,6 +346,7 @@ impl TextEditorState {
has_focus: false,
is_pointer_selection_active: false,
is_click_event_skipped: false,
is_overtype_mode: false,
active_shape_id: None,
cursor_visible: true,
last_blink_time: 0.0,
@ -360,6 +362,7 @@ impl TextEditorState {
self.last_blink_time = 0.0;
self.selection.reset();
self.is_pointer_selection_active = false;
self.is_overtype_mode = false;
self.pending_events.clear();
}
@ -370,6 +373,7 @@ impl TextEditorState {
self.last_blink_time = 0.0;
// self.selection.reset();
self.is_pointer_selection_active = false;
self.is_overtype_mode = false;
self.pending_events.clear();
}
@ -380,6 +384,7 @@ impl TextEditorState {
self.last_blink_time = 0.0;
self.selection.reset();
self.is_pointer_selection_active = false;
self.is_overtype_mode = false;
self.pending_events.clear();
}
@ -516,6 +521,14 @@ impl TextEditorState {
self.push_event(TextEditorEvent::SelectionChanged);
}
pub fn set_overtype_mode(&mut self, overtype_mode: bool) {
self.is_overtype_mode = overtype_mode;
}
pub fn toggle_overtype_mode(&mut self) {
self.set_overtype_mode(!self.is_overtype_mode);
}
fn update_styles_from_selection(&mut self, text_content: &TextContent) -> bool {
let paragraphs = text_content.paragraphs();
if paragraphs.is_empty() {

View File

@ -276,6 +276,43 @@ pub fn find_text_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<
None
}
pub fn replace_text_with_newlines(
text_content: &mut TextContent,
cursor: &TextPositionWithAffinity,
text: &str,
) -> Option<TextPositionWithAffinity> {
let normalized = text.replace("\r\n", "\n").replace('\r', "\n");
let lines: Vec<&str> = normalized.split('\n').collect();
if lines.is_empty() {
return None;
}
let mut current_cursor = *cursor;
if let Some(new_offset) = replace_text_at_cursor(text_content, &current_cursor, lines[0]) {
current_cursor =
TextPositionWithAffinity::new_without_affinity(current_cursor.paragraph, new_offset);
} else {
return None;
}
for line in lines.iter().skip(1) {
if !split_paragraph_at_cursor(text_content, &current_cursor) {
break;
}
current_cursor =
TextPositionWithAffinity::new_without_affinity(current_cursor.paragraph + 1, 0);
if let Some(new_offset) = replace_text_at_cursor(text_content, &current_cursor, line) {
current_cursor = TextPositionWithAffinity::new_without_affinity(
current_cursor.paragraph,
new_offset,
);
}
}
Some(current_cursor)
}
/// Insert text at a cursor position, splitting on newlines into multiple paragraphs.
/// Returns the final cursor position after insertion.
pub fn insert_text_with_newlines(
@ -315,6 +352,8 @@ pub fn insert_text_with_newlines(
Some(current_cursor)
}
/// Insert text at a cursor position. Returns the new character offset after insertion.
pub fn insert_text_at_cursor(
text_content: &mut TextContent,
@ -356,6 +395,58 @@ pub fn insert_text_at_cursor(
Some(cursor.offset + text.chars().count())
}
/// Replace text at cursor position (overtype mode). Replaces N characters where N is the
/// length of the input text, returning the new cursor offset.
pub fn replace_text_at_cursor(
text_content: &mut TextContent,
cursor: &TextPositionWithAffinity,
text: &str,
) -> Option<usize> {
let text_len = text.chars().count();
if text_len == 0 {
return Some(cursor.offset);
}
let paragraphs = text_content.paragraphs_mut();
if cursor.paragraph >= paragraphs.len() {
return None;
}
let para = &mut paragraphs[cursor.paragraph];
let children = para.children_mut();
if children.is_empty() {
return None;
}
if children.len() == 1 && children[0].text.is_empty() {
children[0].set_text(text.to_string());
return Some(text_len);
}
let (span_idx, offset_in_span) = find_text_span_at_offset(para, cursor.offset)?;
let children = para.children_mut();
let span = &mut children[span_idx];
let mut new_text = span.text.clone();
let byte_offset = new_text
.char_indices()
.nth(offset_in_span)
.map(|(i, _)| i)
.unwrap_or(new_text.len());
let end_byte_offset = new_text
.char_indices()
.nth(offset_in_span + text_len)
.map(|(i, _)| i)
.unwrap_or(new_text.len());
new_text.replace_range(byte_offset..end_byte_offset, text);
span.set_text(new_text);
Some(cursor.offset + text_len)
}
/// Delete a range of text specified by a selection.
pub fn delete_selection_range(text_content: &mut TextContent, selection: &TextSelection) {
let start = selection.start();

View File

@ -455,6 +455,15 @@ pub extern "C" fn text_editor_composition_update() -> Result<()> {
Ok(())
}
#[no_mangle]
#[wasm_error]
pub extern "C" fn text_editor_toggle_overtype_mode() -> Result<()> {
with_state_mut!(state, {
state.text_editor_state.toggle_overtype_mode();
Ok(())
})
}
// FIXME: Review if all the return Ok(()) should be Err instead.
#[no_mangle]
#[wasm_error]
@ -491,11 +500,18 @@ pub extern "C" fn text_editor_insert_text() -> Result<()> {
}
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);
if !state.text_editor_state.is_overtype_mode {
if let Some(new_cursor) =
text_helpers::insert_text_with_newlines(text_content, &cursor, &text)
{
state.text_editor_state.selection.set_caret(new_cursor);
}
} else {
if let Some(new_cursor) =
text_helpers::replace_text_with_newlines(text_content, &cursor, &text)
{
state.text_editor_state.selection.set_caret(new_cursor);
}
}
text_content.layout.paragraphs.clear();