// In-place single-line TEXT editor: a plain text-entry box anchored at the // insertion-point click. Unlike MText, plain-text entities carry no inline // formatting, so there is no toolbar or rich preview — just a field the user // types into, committed on Enter. // // This module also hosts `begin_text_edit`, the shared router that opens the // right in-place editor (this plain box, or the rich MText editor) for any // text-bearing entity, plus the field read/write helpers both editors use to // commit back to the correct entity slot. use acadrust::types::Vector3; use acadrust::{EntityType, Handle, Text}; use glam::DVec3; /// Which text slot of which entity an editor session reads from and writes to. /// `Text`/`AttDef`/`AttEnt`/`Dim`/`Tolerance` are plain (single-line box); /// `MText`/`MLeader` are rich (MText editor). #[derive(Clone, Copy, PartialEq, Eq)] pub enum TextEntityField { /// `Text.value`. Text, /// `AttributeDefinition.default_value`. AttDef, /// `AttributeEntity` value. AttEnt, /// `Dimension` text override (`base.text`). Dim, /// `Tolerance.text` (feature-control-frame string). Tolerance, /// `MText.value`. MText, /// `MultiLeader.context.text_string`. MLeader, } impl TextEntityField { /// True for fields edited with the rich MText editor. pub fn is_rich(self) -> bool { matches!(self, TextEntityField::MText | TextEntityField::MLeader) } } /// Read the current text of an entity's editable slot, or `None` if the entity /// carries no editable text. pub fn read_text_field(entity: &EntityType) -> Option<(String, TextEntityField)> { match entity { EntityType::Text(t) => Some((t.value.clone(), TextEntityField::Text)), EntityType::MText(m) => Some((m.value.clone(), TextEntityField::MText)), EntityType::AttributeDefinition(a) => { Some((a.default_value.clone(), TextEntityField::AttDef)) } EntityType::AttributeEntity(a) => { Some((a.get_value().to_string(), TextEntityField::AttEnt)) } EntityType::Dimension(d) => Some((d.base().text.clone(), TextEntityField::Dim)), EntityType::Tolerance(t) => Some((t.text.clone(), TextEntityField::Tolerance)), EntityType::MultiLeader(ml) => { Some((ml.context.text_string.clone(), TextEntityField::MLeader)) } _ => None, } } /// Write `value` into an entity's editable slot. Returns true on a match. pub fn write_text_field(entity: &mut EntityType, field: TextEntityField, value: String) -> bool { match (entity, field) { (EntityType::Text(t), TextEntityField::Text) => t.value = value, (EntityType::MText(m), TextEntityField::MText) => m.value = value, (EntityType::AttributeDefinition(a), TextEntityField::AttDef) => a.default_value = value, (EntityType::AttributeEntity(a), TextEntityField::AttEnt) => a.set_value(value), (EntityType::Dimension(d), TextEntityField::Dim) => d.base_mut().text = value, (EntityType::Tolerance(t), TextEntityField::Tolerance) => t.text = value, (EntityType::MultiLeader(ml), TextEntityField::MLeader) => { ml.context.text_string = value; } _ => return false, } true } fn vec3(v: Vector3) -> DVec3 { DVec3::new(v.x, v.y, v.z) } /// Live state of the open in-place TEXT editor. Absent (`None`) when no editor /// is up. pub struct TextInlineState { /// World insertion point (WCS, same convention the committed entity uses). pub pos: DVec3, /// The plain text being entered. pub value: String, /// Text height (drawing units), used when creating a new TEXT entity. pub height: f64, /// `Some` when editing an existing entity; `None` for a fresh TEXT. pub editing: Option, /// Which entity slot this session writes to on commit. pub field: TextEntityField, /// Canvas-space anchor where the field is drawn (the insertion-point click). pub screen_anchor: iced::Point, } pub(super) fn can_edit_text(mut handle: Handle, document: &acadrust::CadDocument) -> bool { for _ in 0..8 { match document.get_entity(handle) { Some(acadrust::EntityType::Leader(l)) => { let ann = l.annotation_handle; if ann.is_null() || ann == handle { return false; } handle = ann; } _ => break, } } if let Some(entity) = document.get_entity(handle) { read_text_field(entity).is_some() } else { false } } impl super::OpenCADStudio { /// Open the right in-place editor for `handle`: the plain box for single- /// line text entities, the rich MText editor for MText / MultiLeader. A /// Leader resolves to the entity it annotates. Returns the focus task for /// the plain box (the rich editor needs no field focus). pub(super) fn begin_text_edit(&mut self, handle: Handle) -> iced::Task { let i = self.active_tab; // Resolve a Leader chain to the annotated entity. let mut target = handle; for _ in 0..8 { match self.tabs[i].scene.document.get_entity(target) { Some(EntityType::Leader(l)) => { let ann = l.annotation_handle; if ann.is_null() || ann == target { return iced::Task::none(); } target = ann; } _ => break, } } // Snapshot what we need before borrowing `self` mutably to open. let Some(entity) = self.tabs[i].scene.document.get_entity(target) else { return iced::Task::none(); }; let Some((value, field)) = read_text_field(entity) else { return iced::Task::none(); }; let (pos, height) = match entity { EntityType::Text(t) => (vec3(t.insertion_point), t.height), EntityType::MText(m) => (vec3(m.insertion_point), m.height), EntityType::AttributeDefinition(a) => (vec3(a.insertion_point), a.height), EntityType::AttributeEntity(a) => (vec3(a.insertion_point), a.height), EntityType::Dimension(d) => (vec3(d.base().insertion_point), 0.25), EntityType::Tolerance(t) => (vec3(t.insertion_point), t.text_height), EntityType::MultiLeader(ml) => { (vec3(ml.context.text_location), ml.context.text_height) } _ => (DVec3::ZERO, 0.25), }; if field.is_rich() { self.open_mtext_editor(pos, Some(target), &value, height); iced::Task::none() } else { self.open_text_inline(pos, Some(target), &value, height, field); iced::widget::operation::focus(iced::widget::Id::new(super::view::TEXT_INLINE_ID)) } } /// Open the in-place plain-text editor at `pos`, writing to `field` on /// commit. `handle` is `Some` when editing an existing entity. pub(super) fn open_text_inline( &mut self, pos: DVec3, handle: Option, initial: &str, height: f64, field: TextEntityField, ) { let mut state = TextInlineState { pos, value: initial.to_string(), height: if height > 0.0 { height } else { 0.25 }, editing: handle, field, screen_anchor: iced::Point::new(60.0, 90.0), }; if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos { state.screen_anchor = p; } self.text_inline = Some(state); } /// Commit the editor: create a new TEXT entity or update the edited slot. /// Empty content drops a new entity and leaves an edited one untouched. pub(super) fn text_inline_commit(&mut self) -> bool { let i = self.active_tab; let Some(ed) = self.text_inline.take() else { return false }; if ed.value.trim().is_empty() && ed.editing.is_none() { self.refresh_properties(); return false; } if let Some(h) = ed.editing { self.push_undo_snapshot(i, "TEXT"); if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(h) { write_text_field(entity, ed.field, ed.value.clone()); } self.tabs[i].scene.bump_geometry(); self.tabs[i].dirty = true; } else { let mut t = Text::with_value( &ed.value, Vector3::new(ed.pos.x, ed.pos.y, ed.pos.z), ) .with_height(ed.height); // New text inherits the document's current text style (STYLE), not // the entity default. See #92. let cur_style = self.tabs[i] .scene .document .header .current_text_style_name .clone(); if !cur_style.is_empty() { t.style = cur_style; } // Align new text to the active UCS (baseline along the UCS X axis). t.rotation = self.tabs[i].ucs_rotation_angle(); self.push_undo_snapshot(i, "TEXT"); self.commit_entity(EntityType::Text(t)); self.tabs[i].dirty = true; } self.refresh_properties(); true } /// Discard the editor without changing the drawing. pub(super) fn text_inline_cancel(&mut self) { self.text_inline = None; } }