mirror of
https://github.com/penpot/penpot.git
synced 2026-04-25 11:18:36 +00:00
🎉 Add overtype mode to text editor
This commit is contained in:
parent
3561b2d1eb
commit
0f2f0b6137
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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?
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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, ¤t_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, ¤t_cursor) {
|
||||
break;
|
||||
}
|
||||
current_cursor =
|
||||
TextPositionWithAffinity::new_without_affinity(current_cursor.paragraph + 1, 0);
|
||||
if let Some(new_offset) = replace_text_at_cursor(text_content, ¤t_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();
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user