diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs index d1d3cd477b..25a8c0c0fe 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs @@ -111,6 +111,21 @@ (let [text (text-editor/text-editor-export-selection)] (.setData (.-clipboardData event) "text/plain" text)))))) + on-cut + (mf/use-fn + (fn [^js event] + (when (text-editor/text-editor-is-active?) + (dom/prevent-default event) + (when (text-editor/text-editor-get-selection) + (let [text (text-editor/text-editor-export-selection)] + (.setData (.-clipboardData event) "text/plain" (or text "")) + (when (and text (seq text)) + (text-editor/text-editor-delete-backward) + (sync-wasm-text-editor-content!) + (wasm.api/request-render "text-cut")))) + (when-let [node (mf/ref-val contenteditable-ref)] + (set! (.-textContent node) ""))))) + on-key-down (mf/use-fn (fn [^js event] @@ -303,6 +318,7 @@ :on-input on-input :on-paste on-paste :on-copy on-copy + :on-cut on-cut :on-focus on-focus :on-blur on-blur ;; FIXME on-click diff --git a/render-wasm/src/shapes/text.rs b/render-wasm/src/shapes/text.rs index ed5c6c290d..298b44cc55 100644 --- a/render-wasm/src/shapes/text.rs +++ b/render-wasm/src/shapes/text.rs @@ -576,6 +576,7 @@ impl TextContent { for paragraph in self.paragraphs() { let paragraph_style = paragraph.paragraph_to_style(); let mut builder = ParagraphBuilder::new(¶graph_style, fonts); + let mut has_text = false; for span in paragraph.children() { let remove_alpha = use_shadow.unwrap_or(false) && !span.is_transparent(); let text_style = span.to_style( @@ -585,9 +586,15 @@ impl TextContent { paragraph.line_height(), ); let text: String = span.apply_text_transform(); + if !text.is_empty() { + has_text = true; + } builder.push_style(&text_style); builder.add_text(&text); } + if !has_text { + builder.add_text(" "); + } paragraph_group.push(vec![builder]); } diff --git a/render-wasm/src/wasm/text_editor.rs b/render-wasm/src/wasm/text_editor.rs index 2728ba9f2f..7d2b6aac93 100644 --- a/render-wasm/src/wasm/text_editor.rs +++ b/render-wasm/src/wasm/text_editor.rs @@ -298,9 +298,7 @@ pub extern "C" fn text_editor_insert_text() { let cursor = state.text_editor_state.selection.focus; - if let Some(new_offset) = insert_text_at_cursor(text_content, &cursor, &text) { - let new_cursor = - TextPositionWithAffinity::new_without_affinity(cursor.paragraph, new_offset); + if let Some(new_cursor) = insert_text_with_newlines(text_content, &cursor, &text) { state.text_editor_state.selection.set_caret(new_cursor); } @@ -767,12 +765,10 @@ pub extern "C" fn text_editor_export_selection() -> *mut u8 { char_pos += span_len; } } - if !para_text.is_empty() { - if !result.is_empty() { - result.push('\n'); - } - result.push_str(¶_text); + if para_idx > start.paragraph { + result.push('\n'); } + result.push_str(¶_text); } let mut bytes = result.into_bytes(); bytes.push(0); @@ -1069,6 +1065,45 @@ fn find_span_at_offset(para: &Paragraph, char_offset: usize) -> Option<(usize, u None } +/// Insert text at a cursor position, splitting on newlines into multiple paragraphs. +/// Returns the final cursor position after insertion. +fn insert_text_with_newlines( + text_content: &mut TextContent, + cursor: &TextPositionWithAffinity, + text: &str, +) -> Option { + 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) = insert_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) = insert_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. Returns the new character offset after insertion. fn insert_text_at_cursor( text_content: &mut TextContent,