feat(mtext): edit text directly in the preview with a caret

The rendered MText preview is now an editable surface: it shows a text caret,
click to position it between glyphs, and type/Backspace/Delete/Enter to edit.
Left/Right arrows move the caret; drag-selection + toolbar formatting still
work and typing replaces a selection.

Keystrokes are routed by mapping visible-character offsets to byte spans in
the value (raw newlines count as paragraph breaks, matching the layout). While
the preview is open the command line releases its on_input so typed glyphs
reach the preview instead, and the caret starts at the end of the text so
typing works without a click first.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan Seven 2026-05-31 10:16:30 +03:00
commit 7190df8274
5 changed files with 280 additions and 5 deletions

View file

@ -879,6 +879,13 @@ impl OpenCADStudio {
}
Message::CommandAppendChar(s) => {
// While the MText preview is up, typed glyphs edit it directly.
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
if s.chars().all(|c| !c.is_control()) {
self.mtext_type(&s);
}
return Task::none();
}
// Filter out control characters — only push the typed
// glyph(s). `Tab`, etc. arrive as Named keys, not here.
if s.chars().all(|c| !c.is_control()) {
@ -919,6 +926,10 @@ impl OpenCADStudio {
}
Message::CommandBackspace => {
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
self.mtext_backspace();
return Task::none();
}
let i = self.active_tab;
// Backspace edits the focused dynamic-input field first;
// emptying it unlocks the field (back to cursor tracking).
@ -1201,6 +1212,11 @@ impl OpenCADStudio {
}
Message::CommandFinalize => {
// In the MText preview, Enter inserts a line break.
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
self.mtext_type("\n");
return Task::none();
}
// Grip popup open → Enter commits the highlighted item.
if self.grip_popup.is_some() {
let idx = self
@ -3443,6 +3459,11 @@ impl OpenCADStudio {
}
Message::DeleteSelected => {
// In the MText preview, Delete removes text at the caret.
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
self.mtext_delete();
return Task::none();
}
let i = self.active_tab;
self.tabs[i].scene.selection.borrow_mut().context_menu = None;
let handles: Vec<_> = self.tabs[i].scene.selected.iter().cloned().collect();
@ -3544,12 +3565,21 @@ impl OpenCADStudio {
ed.show_preview = on;
}
self.rebuild_mtext_preview();
Task::none()
// Focus the text area when switching to Edit so the caret
// shows and typing/clicking edits immediately.
if on {
Task::none()
} else {
iced::widget::operation::focus(iced::widget::Id::new(
super::view::MTEXT_TEXT_ID,
))
}
}
Message::MTextSelStart(off) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.sel_anchor = off;
ed.sel = Some((off, off));
ed.caret = off;
}
Task::none()
}
@ -3557,9 +3587,26 @@ impl OpenCADStudio {
if let Some(ed) = self.mtext_editor.as_mut() {
let a = ed.sel_anchor;
ed.sel = Some((a.min(off), a.max(off)));
ed.caret = off;
}
Task::none()
}
Message::MTextType(s) => {
self.mtext_type(&s);
Task::none()
}
Message::MTextBackspace => {
self.mtext_backspace();
Task::none()
}
Message::MTextDelete => {
self.mtext_delete();
Task::none()
}
Message::MTextCaretMove(d) => {
self.mtext_caret_move(d);
Task::none()
}
Message::MTextOk => {
self.mtext_commit();
Task::none()