feat: Implement in-place MText editor with formatting toolbar and live preview

- Added SVG icons for text formatting options (bold, italic, underline, overline, strike, uppercase, lowercase).
- Introduced a new `MTextEditorState` struct to manage the state of the MText editor.
- Implemented functionality to open the MText editor from the command line and edit existing MText entities.
- Added message handling for various MText editor actions (formatting, height, style, font, justification, alignment, line spacing).
- Created a dedicated preview area for MText rendering within the editor.
- Updated the command structure to support opening the MText editor and handling text input.
- Refactored existing MText command logic to integrate with the new editor.
This commit is contained in:
Hakan Seven 2026-05-30 23:24:06 +03:00
commit de08bd598c
20 changed files with 871 additions and 36 deletions

View file

@ -1230,6 +1230,11 @@ impl OpenCADStudio {
}
Message::CommandEscape => {
// Open MText editor swallows Escape (cancel without committing).
if self.mtext_editor.is_some() {
self.mtext_cancel();
return Task::none();
}
// Grip popup intercepts Escape — dismisses the menu
// without doing anything else.
if self.grip_popup.take().is_some() {
@ -2929,6 +2934,20 @@ impl OpenCADStudio {
let hit = scene::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds)
.and_then(|s| Scene::handle_from_wire_name(s));
if let Some(handle) = hit {
// MText opens the in-place editor; other text types
// keep the command-line DDEDIT path.
if let Some(AcadEntityType::MText(m)) =
self.tabs[i].scene.document.get_entity(handle)
{
let pos = glam::Vec3::new(
m.insertion_point.x as f32,
m.insertion_point.y as f32,
m.insertion_point.z as f32,
);
let (val, h) = (m.value.clone(), m.height);
self.open_mtext_editor(pos, Some(handle), &val, h);
return Task::none();
}
if let Some(entity) = self.tabs[i].scene.document.get_entity(handle) {
use crate::modules::annotate::ddedit::{
entity_text, DdeditCommand,
@ -3441,6 +3460,94 @@ impl OpenCADStudio {
Task::none()
}
// ── In-place MText editor ───────────────────────────────────
Message::MTextEdit(action) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.content.perform(action);
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextFmt(kind) => {
self.mtext_apply_fmt(kind);
Task::none()
}
Message::MTextHeight(s) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.height = s;
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextColor(aci) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.color_aci = aci;
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextStyle(s) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.style = s;
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextFont(f) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.font = if f == "[Style default]" { String::new() } else { f };
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextOblique(s) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.oblique = s;
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextWidth(s) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.width = s;
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextCharSpace(s) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.char_space = s;
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextJustify(ap) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.attachment = ap;
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextAlign(a) => {
self.mtext_apply_align(a);
Task::none()
}
Message::MTextLineSpacing(f) => {
if let Some(ed) = self.mtext_editor.as_mut() {
ed.line_spacing = f;
}
self.rebuild_mtext_preview();
Task::none()
}
Message::MTextOk => {
self.mtext_commit();
Task::none()
}
Message::MTextCancel => {
self.mtext_cancel();
Task::none()
}
Message::DrawOrderSubmenuToggle => {
let i = self.active_tab;
let mut sel = self.tabs[i].scene.selection.borrow_mut();