Keep locked-layer objects selectable for inspection while suppressing grips, property edits, command mutations, and stale editor writes.
275 lines
11 KiB
Rust
275 lines
11 KiB
Rust
// 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<Handle>,
|
|
/// 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<super::Message> {
|
|
let i = self.active_tab;
|
|
if self.tabs[i].scene.is_layer_locked(handle) {
|
|
return iced::Task::none();
|
|
}
|
|
// 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,
|
|
}
|
|
}
|
|
if self.tabs[i].scene.is_layer_locked(target) {
|
|
return iced::Task::none();
|
|
}
|
|
// 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);
|
|
self.unfocus_widgets()
|
|
} 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<Handle>,
|
|
initial: &str,
|
|
height: f64,
|
|
field: TextEntityField,
|
|
) {
|
|
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
|
return;
|
|
}
|
|
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 {
|
|
if self.tabs[i].scene.is_layer_locked(h) {
|
|
self.refresh_properties();
|
|
return false;
|
|
}
|
|
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_entities(&[(h, crate::scene::ChangeKind::Modified)]);
|
|
self.tabs[i].dirty = true;
|
|
} else {
|
|
let plane = if self.tabs[i].editing_model_space() {
|
|
self.tabs[i].ucs_xform().working_plane()
|
|
} else {
|
|
crate::command::WorkingPlane::default()
|
|
};
|
|
let position = plane.to_local(ed.pos);
|
|
let mut t = Text::with_value(
|
|
&ed.value,
|
|
Vector3::new(position.x, position.y, position.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;
|
|
}
|
|
let annotative = crate::scene::annotative::text_style_is_annotative(
|
|
&self.tabs[i].scene.document,
|
|
&t.style,
|
|
);
|
|
self.push_undo_snapshot(i, "TEXT");
|
|
let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t)));
|
|
if annotative {
|
|
let scale = self.tabs[i].scene.current_annotation_scale_handle();
|
|
if let (Some(handle), Some(scale)) = (handle, scale) {
|
|
crate::scene::annotative::create_annotation_context(
|
|
&mut self.tabs[i].scene.document,
|
|
handle,
|
|
scale,
|
|
);
|
|
}
|
|
}
|
|
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;
|
|
}
|
|
}
|