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:
parent
50115c55d8
commit
7190df8274
5 changed files with 280 additions and 5 deletions
|
|
@ -121,6 +121,9 @@ pub struct MTextEditorState {
|
|||
pub sel: Option<(usize, usize)>,
|
||||
/// Anchor offset for an in-progress drag selection.
|
||||
pub sel_anchor: usize,
|
||||
/// Text caret as a visible-character offset (0..=count). Used for typing
|
||||
/// directly into the preview.
|
||||
pub caret: usize,
|
||||
/// Canvas-space anchor where the toolbar + text area are drawn (the
|
||||
/// insertion-point click position).
|
||||
pub screen_anchor: iced::Point,
|
||||
|
|
@ -146,6 +149,7 @@ impl MTextEditorState {
|
|||
glyph_boxes: Vec::new(),
|
||||
sel: None,
|
||||
sel_anchor: 0,
|
||||
caret: 0,
|
||||
attachment: AttachmentPoint::TopLeft,
|
||||
line_spacing: 1.0,
|
||||
// Default box ~20 characters wide; overwritten with the entity's
|
||||
|
|
@ -263,6 +267,7 @@ pub fn visible_spans(raw: &str) -> Vec<(usize, usize)> {
|
|||
None => {}
|
||||
},
|
||||
'{' | '}' => { /* group markers — not visible */ }
|
||||
'\n' | '\r' => flush(&mut para, &mut result), // raw line break = paragraph
|
||||
'%' if it.peek().map(|&(_, c)| c) == Some('%') => {
|
||||
it.next(); // second '%'
|
||||
match it.peek().copied() {
|
||||
|
|
@ -340,6 +345,12 @@ impl super::OpenCADStudio {
|
|||
}
|
||||
self.mtext_editor = Some(state);
|
||||
self.rebuild_mtext_preview();
|
||||
// Place the caret at the end so typing works without a click first.
|
||||
let end = self.mtext_vis_count();
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
ed.caret = end;
|
||||
ed.sel = Some((end, end));
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-tessellate the current editor text into the editor's OWN preview
|
||||
|
|
@ -468,6 +479,116 @@ impl super::OpenCADStudio {
|
|||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
// ── Caret editing directly on the preview ───────────────────────────────
|
||||
|
||||
/// Insert text (or replace the current selection) at the preview caret.
|
||||
pub(super) fn mtext_type(&mut self, s: &str) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let raw0 = ed.content.text();
|
||||
let raw = raw0.strip_suffix('\n').unwrap_or(&raw0).to_string();
|
||||
let spans = visible_spans(&raw);
|
||||
let added = visible_spans(s).len();
|
||||
let (new_text, new_caret) = match ed.sel {
|
||||
Some((a, b)) if a < b && a < spans.len() && b <= spans.len() => {
|
||||
let (start, end) = (spans[a].0, spans[b - 1].1);
|
||||
let mut t = raw.clone();
|
||||
t.replace_range(start..end, s);
|
||||
(t, a + added)
|
||||
}
|
||||
_ => {
|
||||
let byte = spans.get(ed.caret).map(|s| s.0).unwrap_or(raw.len());
|
||||
let mut t = raw.clone();
|
||||
t.insert_str(byte, s);
|
||||
(t, ed.caret + added)
|
||||
}
|
||||
};
|
||||
ed.content = iced::widget::text_editor::Content::with_text(&new_text);
|
||||
ed.caret = new_caret;
|
||||
ed.sel = Some((new_caret, new_caret));
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
/// Delete the selection, or the visible character before the caret.
|
||||
pub(super) fn mtext_backspace(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let raw0 = ed.content.text();
|
||||
let raw = raw0.strip_suffix('\n').unwrap_or(&raw0).to_string();
|
||||
let spans = visible_spans(&raw);
|
||||
let (new_text, new_caret) = match ed.sel {
|
||||
Some((a, b)) if a < b && a < spans.len() && b <= spans.len() => {
|
||||
let (start, end) = (spans[a].0, spans[b - 1].1);
|
||||
let mut t = raw.clone();
|
||||
t.replace_range(start..end, "");
|
||||
(t, a)
|
||||
}
|
||||
_ if ed.caret > 0 && ed.caret <= spans.len() => {
|
||||
let (start, end) = (spans[ed.caret - 1].0, spans[ed.caret - 1].1);
|
||||
let mut t = raw.clone();
|
||||
t.replace_range(start..end, "");
|
||||
(t, ed.caret - 1)
|
||||
}
|
||||
_ => (raw, ed.caret),
|
||||
};
|
||||
ed.content = iced::widget::text_editor::Content::with_text(&new_text);
|
||||
ed.caret = new_caret;
|
||||
ed.sel = Some((new_caret, new_caret));
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
/// Delete the selection, or the visible character at the caret.
|
||||
pub(super) fn mtext_delete(&mut self) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let raw0 = ed.content.text();
|
||||
let raw = raw0.strip_suffix('\n').unwrap_or(&raw0).to_string();
|
||||
let spans = visible_spans(&raw);
|
||||
let (new_text, new_caret) = match ed.sel {
|
||||
Some((a, b)) if a < b && a < spans.len() && b <= spans.len() => {
|
||||
let (start, end) = (spans[a].0, spans[b - 1].1);
|
||||
let mut t = raw.clone();
|
||||
t.replace_range(start..end, "");
|
||||
(t, a)
|
||||
}
|
||||
_ if ed.caret < spans.len() => {
|
||||
let (start, end) = (spans[ed.caret].0, spans[ed.caret].1);
|
||||
let mut t = raw.clone();
|
||||
t.replace_range(start..end, "");
|
||||
(t, ed.caret)
|
||||
}
|
||||
_ => (raw, ed.caret),
|
||||
};
|
||||
ed.content = iced::widget::text_editor::Content::with_text(&new_text);
|
||||
ed.caret = new_caret;
|
||||
ed.sel = Some((new_caret, new_caret));
|
||||
}
|
||||
self.rebuild_mtext_preview();
|
||||
}
|
||||
|
||||
/// Move the caret by `delta` visible characters (clears the selection).
|
||||
pub(super) fn mtext_caret_move(&mut self, delta: i32) {
|
||||
if let Some(ed) = self.mtext_editor.as_mut() {
|
||||
let raw0 = ed.content.text();
|
||||
let raw = raw0.strip_suffix('\n').unwrap_or(&raw0);
|
||||
let n = visible_spans(raw).len() as i32;
|
||||
let c = (ed.caret as i32 + delta).clamp(0, n) as usize;
|
||||
ed.caret = c;
|
||||
ed.sel = Some((c, c));
|
||||
}
|
||||
}
|
||||
|
||||
/// Visible-character count of the current text.
|
||||
pub(super) fn mtext_vis_count(&self) -> usize {
|
||||
self.mtext_editor
|
||||
.as_ref()
|
||||
.map(|ed| {
|
||||
let raw0 = ed.content.text();
|
||||
let raw = raw0.strip_suffix('\n').unwrap_or(&raw0);
|
||||
visible_spans(raw).len()
|
||||
})
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Commit the editor — create a new MText or update the edited one.
|
||||
pub(super) fn mtext_commit(&mut self) {
|
||||
let i = self.active_tab;
|
||||
|
|
|
|||
Loading…
Reference in a new issue