feat(attedit): enhanced attribute editor with 3 tabs
Grow the attribute editor into a three-tab enhanced editor: - Attribute: the tag/prompt/value list; click a row to select it, edit its value below. - Text Options: the selected attribute's Text Style, Justification, Height, Rotation, Width Factor, Oblique Angle, Backwards, Upside down. - Properties: the selected attribute's Layer, Linetype, Color, Lineweight (the colour list also surfaces a non-standard current colour so it displays and round-trips). Applying writes value, text-formatting and common-property edits back to each attribute positionally (guarded on block name + count), with undo and a repaint. OK/Cancel are replaced by a single Apply button in a top toolbar that commits but keeps the dialog open, matching the other modal windows — the frame ✕ closes and discards un-applied edits. The frame adopts the shared style-window palette, tab styling and sizing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0eb347bf47
commit
0b60bcc075
6 changed files with 717 additions and 165 deletions
|
|
@ -361,11 +361,14 @@ pub(super) struct OpenCADStudio {
|
||||||
attr_editor_handle: Option<acadrust::Handle>,
|
attr_editor_handle: Option<acadrust::Handle>,
|
||||||
/// Block name shown in the editor's title bar.
|
/// Block name shown in the editor's title bar.
|
||||||
attr_editor_block: String,
|
attr_editor_block: String,
|
||||||
/// Working copy of the block's attributes as `(tag, prompt, value)`, in the
|
/// Working copy of the block's attributes, in the same order as
|
||||||
/// same order as `Insert::attributes`. The prompt is read from the block's
|
/// `Insert::attributes`. Each row carries the value plus the text-option and
|
||||||
/// matching ATTDEF (blank if none). Only the value is edited; the whole row
|
/// common-property fields the editor edits; written back on OK.
|
||||||
/// is written back on OK.
|
attr_editor_rows: Vec<crate::ui::window::attribute_editor::AttrRow>,
|
||||||
attr_editor_fields: Vec<(String, String, String)>,
|
/// Which editor tab is showing.
|
||||||
|
attr_editor_tab: crate::ui::window::attribute_editor::AttrTab,
|
||||||
|
/// Highlighted attribute row driving the Text Options / Properties tabs.
|
||||||
|
attr_editor_selected: usize,
|
||||||
/// Plugin ids the user turned off in the Plugin Manager. Disabled plugins
|
/// Plugin ids the user turned off in the Plugin Manager. Disabled plugins
|
||||||
/// keep their manifest listed but drop their ribbon tab and command
|
/// keep their manifest listed but drop their ribbon tab and command
|
||||||
/// dispatch. Persisted via [`settings::UserSettings::disabled_plugins`].
|
/// dispatch. Persisted via [`settings::UserSettings::disabled_plugins`].
|
||||||
|
|
@ -1456,10 +1459,29 @@ pub enum Message {
|
||||||
// ── Attribute editor dialog ───────────────────────────────────────────
|
// ── Attribute editor dialog ───────────────────────────────────────────
|
||||||
/// Open the attribute editor for an INSERT (double-click / ATTEDIT).
|
/// Open the attribute editor for an INSERT (double-click / ATTEDIT).
|
||||||
AttrEditorOpen(acadrust::Handle),
|
AttrEditorOpen(acadrust::Handle),
|
||||||
/// Live edit of the attribute value at row `idx` in the editor dialog.
|
/// Switch the editor's active tab.
|
||||||
|
AttrEditorTab(crate::ui::window::attribute_editor::AttrTab),
|
||||||
|
/// Select the row the Text Options / Properties tabs act on.
|
||||||
|
AttrEditorSelect(usize),
|
||||||
|
/// Live edit of the attribute value at row `idx` (Attribute tab).
|
||||||
AttrEditorInput { idx: usize, value: String },
|
AttrEditorInput { idx: usize, value: String },
|
||||||
/// Apply every attribute edit to the block and close the dialog (OK).
|
// Text Options — all act on the selected row:
|
||||||
AttrEditorOk,
|
AttrEditorTextStyle(String),
|
||||||
|
AttrEditorJustify(String),
|
||||||
|
AttrEditorHeight(String),
|
||||||
|
AttrEditorRotation(String),
|
||||||
|
AttrEditorWidth(String),
|
||||||
|
AttrEditorOblique(String),
|
||||||
|
AttrEditorBackwards(bool),
|
||||||
|
AttrEditorUpsideDown(bool),
|
||||||
|
// Properties — all act on the selected row:
|
||||||
|
AttrEditorLayer(String),
|
||||||
|
AttrEditorLinetype(String),
|
||||||
|
AttrEditorColor(String),
|
||||||
|
AttrEditorLineweight(acadrust::types::LineWeight),
|
||||||
|
/// Commit every attribute edit to the block, keeping the dialog open
|
||||||
|
/// (Apply); the frame ✕ closes and discards any un-applied edits.
|
||||||
|
AttrEditorApply,
|
||||||
/// Title-bar pressed: begin dragging the active modal.
|
/// Title-bar pressed: begin dragging the active modal.
|
||||||
ModalGrab,
|
ModalGrab,
|
||||||
/// Cursor moved while dragging the modal title bar.
|
/// Cursor moved while dragging the modal title bar.
|
||||||
|
|
@ -1934,7 +1956,9 @@ impl OpenCADStudio {
|
||||||
modal_dragging: false,
|
modal_dragging: false,
|
||||||
attr_editor_handle: None,
|
attr_editor_handle: None,
|
||||||
attr_editor_block: String::new(),
|
attr_editor_block: String::new(),
|
||||||
attr_editor_fields: Vec::new(),
|
attr_editor_rows: Vec::new(),
|
||||||
|
attr_editor_tab: crate::ui::window::attribute_editor::AttrTab::Attribute,
|
||||||
|
attr_editor_selected: 0,
|
||||||
disabled_plugins: rustc_hash::FxHashSet::default(),
|
disabled_plugins: rustc_hash::FxHashSet::default(),
|
||||||
external_plugins: Vec::new(),
|
external_plugins: Vec::new(),
|
||||||
loaded_plugin_ids: rustc_hash::FxHashSet::default(),
|
loaded_plugin_ids: rustc_hash::FxHashSet::default(),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use crate::scene::{
|
||||||
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
self, hover_id, CubeRegion, Scene, VIEWCUBE_DRAW_PX, VIEWCUBE_PAD, VIEWCUBE_PX,
|
||||||
};
|
};
|
||||||
use crate::ui::PropertiesPanel;
|
use crate::ui::PropertiesPanel;
|
||||||
|
use crate::ui::window::attribute_editor::{AttrRow, AttrTab};
|
||||||
use acadrust::types::Color as AcadColor;
|
use acadrust::types::Color as AcadColor;
|
||||||
use acadrust::{EntityType as AcadEntityType, Handle};
|
use acadrust::{EntityType as AcadEntityType, Handle};
|
||||||
use iced::time::Instant;
|
use iced::time::Instant;
|
||||||
|
|
@ -1215,22 +1216,19 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
pub(crate) fn open_attribute_editor(&mut self, handle: acadrust::Handle) {
|
pub(crate) fn open_attribute_editor(&mut self, handle: acadrust::Handle) {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let doc = &self.tabs[i].scene.document;
|
let doc = &self.tabs[i].scene.document;
|
||||||
// Ok((block, fields)) to open; Err(msg) to report and stay closed. The
|
// Ok((block, rows)) to open; Err(msg) to report and stay closed. The
|
||||||
// borrow of `doc` ends with this match, before any `self` mutation.
|
// borrow of `doc` ends with this match, before any `self` mutation.
|
||||||
let result = match doc.get_entity(handle) {
|
let result = match doc.get_entity(handle) {
|
||||||
Some(acadrust::EntityType::Insert(ins)) if !ins.attributes.is_empty() => {
|
Some(acadrust::EntityType::Insert(ins)) if !ins.attributes.is_empty() => {
|
||||||
// The prompt text lives on the block's ATTDEFs, not on the
|
// The prompt text lives on the block's ATTDEFs, not on the
|
||||||
// attribute instances — map tag → prompt from the definition.
|
// attribute instances — map tag → prompt from the definition.
|
||||||
let prompts = block_attr_prompts(doc, &ins.block_name);
|
let prompts = block_attr_prompts(doc, &ins.block_name);
|
||||||
let fields = ins
|
let rows = ins
|
||||||
.attributes
|
.attributes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|a| {
|
.map(|a| attr_row_from_entity(a, &prompts))
|
||||||
let prompt = prompts.get(&a.tag).cloned().unwrap_or_default();
|
|
||||||
(a.tag.clone(), prompt, a.get_value().to_string())
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
Ok((ins.block_name.clone(), fields))
|
Ok((ins.block_name.clone(), rows))
|
||||||
}
|
}
|
||||||
Some(acadrust::EntityType::Insert(_)) => {
|
Some(acadrust::EntityType::Insert(_)) => {
|
||||||
Err("ATTEDIT This block has no attributes.")
|
Err("ATTEDIT This block has no attributes.")
|
||||||
|
|
@ -1238,9 +1236,11 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
_ => Err("ATTEDIT Select a block with attributes."),
|
_ => Err("ATTEDIT Select a block with attributes."),
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok((block, fields)) => {
|
Ok((block, rows)) => {
|
||||||
self.attr_editor_block = block;
|
self.attr_editor_block = block;
|
||||||
self.attr_editor_fields = fields;
|
self.attr_editor_rows = rows;
|
||||||
|
self.attr_editor_selected = 0;
|
||||||
|
self.attr_editor_tab = AttrTab::Attribute;
|
||||||
self.attr_editor_handle = Some(handle);
|
self.attr_editor_handle = Some(handle);
|
||||||
self.active_modal = Some(crate::app::ModalKind::AttributeEditor);
|
self.active_modal = Some(crate::app::ModalKind::AttributeEditor);
|
||||||
self.modal_offset = iced::Vector::ZERO;
|
self.modal_offset = iced::Vector::ZERO;
|
||||||
|
|
@ -1249,6 +1249,13 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The currently-selected editor row, for the Text Options / Properties
|
||||||
|
/// edit handlers (which all act on that one attribute).
|
||||||
|
pub(super) fn attr_row_selected_mut(&mut self) -> Option<&mut AttrRow> {
|
||||||
|
let sel = self.attr_editor_selected;
|
||||||
|
self.attr_editor_rows.get_mut(sel)
|
||||||
|
}
|
||||||
|
|
||||||
/// Close the attribute editor without applying, if it is open. Used when
|
/// Close the attribute editor without applying, if it is open. Used when
|
||||||
/// the tab it belongs to closes or the active tab switches — its handle is
|
/// the tab it belongs to closes or the active tab switches — its handle is
|
||||||
/// document-local and must not outlive its tab.
|
/// document-local and must not outlive its tab.
|
||||||
|
|
@ -1259,45 +1266,35 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
}
|
}
|
||||||
self.attr_editor_handle = None;
|
self.attr_editor_handle = None;
|
||||||
self.attr_editor_block.clear();
|
self.attr_editor_block.clear();
|
||||||
self.attr_editor_fields.clear();
|
self.attr_editor_rows.clear();
|
||||||
|
self.attr_editor_selected = 0;
|
||||||
|
self.attr_editor_tab = AttrTab::Attribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Apply every edited attribute value back to the block and close the
|
/// Commit every edited attribute (value, text options, common properties)
|
||||||
/// editor (OK). Values are written verbatim (attribute text is free-form).
|
/// back to the block, keeping the dialog open (Apply). Closing is the frame
|
||||||
/// The edits are positional — same order the dialog was populated in — so
|
/// ✕ (`CloseModal`), which discards any un-applied edits — matching the
|
||||||
/// blocks with duplicate tags stay correct; a tag-based fallback covers the
|
/// other modal windows. Applied positionally so blocks with duplicate tags
|
||||||
/// unlikely case the attribute list changed underneath the open dialog.
|
/// stay correct; guarded on block name + count so an edit can never land on
|
||||||
pub(super) fn on_attr_editor_ok(&mut self) -> Task<Message> {
|
/// a different entity if the selection changed under the open dialog.
|
||||||
|
pub(super) fn on_attr_editor_apply(&mut self) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
let Some(handle) = self.attr_editor_handle.take() else {
|
let Some(handle) = self.attr_editor_handle else {
|
||||||
self.active_modal = None;
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
};
|
};
|
||||||
let fields = std::mem::take(&mut self.attr_editor_fields);
|
// Snapshot the working copy so the document can be mutated while the
|
||||||
self.attr_editor_block.clear();
|
// dialog's rows stay put (it remains open for further edits).
|
||||||
self.active_modal = None;
|
let rows = self.attr_editor_rows.clone();
|
||||||
self.modal_offset = iced::Vector::ZERO;
|
let block = self.attr_editor_block.clone();
|
||||||
|
|
||||||
self.push_undo_snapshot(i, "ATTEDIT");
|
self.push_undo_snapshot(i, "ATTEDIT");
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
if let Some(acadrust::EntityType::Insert(ins)) =
|
if let Some(acadrust::EntityType::Insert(ins)) =
|
||||||
self.tabs[i].scene.document.get_entity_mut(handle)
|
self.tabs[i].scene.document.get_entity_mut(handle)
|
||||||
{
|
{
|
||||||
if fields.len() == ins.attributes.len() {
|
if ins.block_name == block && rows.len() == ins.attributes.len() {
|
||||||
for (attr, (_, _, val)) in ins.attributes.iter_mut().zip(fields.iter()) {
|
for (attr, row) in ins.attributes.iter_mut().zip(rows.iter()) {
|
||||||
if attr.get_value() != val {
|
changed |= apply_attr_row(attr, row);
|
||||||
attr.set_value(val.clone());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (tag, _, val) in &fields {
|
|
||||||
if let Some(attr) = ins.attributes.iter_mut().find(|a| &a.tag == tag) {
|
|
||||||
if attr.get_value() != val {
|
|
||||||
attr.set_value(val.clone());
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1313,6 +1310,107 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Build the editor's working copy of one attribute. Angles are shown in
|
||||||
|
/// degrees; numeric fields become strings so the user can type freely.
|
||||||
|
fn attr_row_from_entity(
|
||||||
|
a: &acadrust::entities::AttributeEntity,
|
||||||
|
prompts: &rustc_hash::FxHashMap<String, String>,
|
||||||
|
) -> AttrRow {
|
||||||
|
let fmt = |v: f64| format!("{v}");
|
||||||
|
AttrRow {
|
||||||
|
tag: a.tag.clone(),
|
||||||
|
prompt: prompts.get(&a.tag).cloned().unwrap_or_default(),
|
||||||
|
value: a.get_value().to_string(),
|
||||||
|
text_style: a.text_style.clone(),
|
||||||
|
height: fmt(a.height),
|
||||||
|
rotation: fmt(a.rotation.to_degrees()),
|
||||||
|
width_factor: fmt(a.width_factor),
|
||||||
|
oblique: fmt(a.oblique_angle.to_degrees()),
|
||||||
|
h_align: a.horizontal_alignment,
|
||||||
|
v_align: a.vertical_alignment,
|
||||||
|
backwards: a.text_generation_flags & 0x2 != 0,
|
||||||
|
upside_down: a.text_generation_flags & 0x4 != 0,
|
||||||
|
layer: a.common.layer.clone(),
|
||||||
|
color: a.common.color,
|
||||||
|
linetype: a.common.linetype.clone(),
|
||||||
|
line_weight: a.common.line_weight,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write one edited row back onto its attribute; returns whether anything
|
||||||
|
/// changed. Numeric fields are parsed (angles from degrees); an unparseable
|
||||||
|
/// field is left as-is.
|
||||||
|
fn apply_attr_row(a: &mut acadrust::entities::AttributeEntity, row: &AttrRow) -> bool {
|
||||||
|
let mut ch = false;
|
||||||
|
if a.get_value() != row.value {
|
||||||
|
a.set_value(row.value.clone());
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
if a.text_style != row.text_style {
|
||||||
|
a.text_style = row.text_style.clone();
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
if let Ok(h) = row.height.trim().parse::<f64>() {
|
||||||
|
if a.height != h {
|
||||||
|
a.height = h;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(deg) = row.rotation.trim().parse::<f64>() {
|
||||||
|
let rad = deg.to_radians();
|
||||||
|
if (a.rotation - rad).abs() > 1e-12 {
|
||||||
|
a.rotation = rad;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(w) = row.width_factor.trim().parse::<f64>() {
|
||||||
|
if a.width_factor != w {
|
||||||
|
a.width_factor = w;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(deg) = row.oblique.trim().parse::<f64>() {
|
||||||
|
let rad = deg.to_radians();
|
||||||
|
if (a.oblique_angle - rad).abs() > 1e-12 {
|
||||||
|
a.oblique_angle = rad;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a.horizontal_alignment != row.h_align {
|
||||||
|
a.horizontal_alignment = row.h_align;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
if a.vertical_alignment != row.v_align {
|
||||||
|
a.vertical_alignment = row.v_align;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
// Text generation flags: bit 0x2 = backwards, bit 0x4 = upside down.
|
||||||
|
let new_flags = (a.text_generation_flags & !0x6)
|
||||||
|
| if row.backwards { 0x2 } else { 0 }
|
||||||
|
| if row.upside_down { 0x4 } else { 0 };
|
||||||
|
if new_flags != a.text_generation_flags {
|
||||||
|
a.text_generation_flags = new_flags;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
if a.common.layer != row.layer {
|
||||||
|
a.common.layer = row.layer.clone();
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
if a.common.color != row.color {
|
||||||
|
a.common.color = row.color;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
if a.common.linetype != row.linetype {
|
||||||
|
a.common.linetype = row.linetype.clone();
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
if a.common.line_weight != row.line_weight {
|
||||||
|
a.common.line_weight = row.line_weight;
|
||||||
|
ch = true;
|
||||||
|
}
|
||||||
|
ch
|
||||||
|
}
|
||||||
|
|
||||||
/// Map each attribute tag to the prompt text declared on the block's matching
|
/// Map each attribute tag to the prompt text declared on the block's matching
|
||||||
/// ATTDEF. Attribute instances (ATTRIB) carry only tag + value; the prompt is
|
/// ATTDEF. Attribute instances (ATTRIB) carry only tag + value; the prompt is
|
||||||
/// defined once on the block definition. Tags with no definition are absent.
|
/// defined once on the block definition. Tags with no definition are absent.
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,9 @@ impl OpenCADStudio {
|
||||||
Some(AttributeEditor) => {
|
Some(AttributeEditor) => {
|
||||||
self.attr_editor_handle = None;
|
self.attr_editor_handle = None;
|
||||||
self.attr_editor_block.clear();
|
self.attr_editor_block.clear();
|
||||||
self.attr_editor_fields.clear();
|
self.attr_editor_rows.clear();
|
||||||
|
self.attr_editor_selected = 0;
|
||||||
|
self.attr_editor_tab = crate::ui::window::attribute_editor::AttrTab::Attribute;
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
@ -2268,13 +2270,102 @@ impl OpenCADStudio {
|
||||||
self.open_attribute_editor(handle);
|
self.open_attribute_editor(handle);
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::AttrEditorInput { idx, value } => {
|
Message::AttrEditorTab(t) => {
|
||||||
if let Some(field) = self.attr_editor_fields.get_mut(idx) {
|
self.attr_editor_tab = t;
|
||||||
field.2 = value;
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorSelect(idx) => {
|
||||||
|
if idx < self.attr_editor_rows.len() {
|
||||||
|
self.attr_editor_selected = idx;
|
||||||
}
|
}
|
||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
Message::AttrEditorOk => self.on_attr_editor_ok(),
|
Message::AttrEditorInput { idx, value } => {
|
||||||
|
if let Some(r) = self.attr_editor_rows.get_mut(idx) {
|
||||||
|
r.value = value;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorTextStyle(s) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.text_style = s;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorJustify(label) => {
|
||||||
|
if let Some((h, v)) =
|
||||||
|
crate::ui::window::attribute_editor::justify_from_label(&label)
|
||||||
|
{
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.h_align = h;
|
||||||
|
r.v_align = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorHeight(s) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.height = s;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorRotation(s) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.rotation = s;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorWidth(s) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.width_factor = s;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorOblique(s) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.oblique = s;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorBackwards(b) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.backwards = b;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorUpsideDown(b) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.upside_down = b;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorLayer(s) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.layer = s;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorLinetype(s) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.linetype = if s == "ByLayer" { String::new() } else { s };
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorColor(label) => {
|
||||||
|
if let Some(c) = crate::ui::window::attribute_editor::color_from_label(&label) {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.color = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorLineweight(lw) => {
|
||||||
|
if let Some(r) = self.attr_row_selected_mut() {
|
||||||
|
r.line_weight = lw;
|
||||||
|
}
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
Message::AttrEditorApply => self.on_attr_editor_apply(),
|
||||||
Message::ModalGrab => {
|
Message::ModalGrab => {
|
||||||
// Start a drag; the first ModalDragMove seeds the reference.
|
// Start a drag; the first ModalDragMove seeds the reference.
|
||||||
self.modal_dragging = true;
|
self.modal_dragging = true;
|
||||||
|
|
|
||||||
|
|
@ -1363,7 +1363,7 @@ impl OpenCADStudio {
|
||||||
OverwriteWarning => (420, 180),
|
OverwriteWarning => (420, 180),
|
||||||
SaveDialog => (560, 480),
|
SaveDialog => (560, 480),
|
||||||
PointStyle => (360, 470),
|
PointStyle => (360, 470),
|
||||||
AttributeEditor => (460, 440),
|
AttributeEditor => (640, 500),
|
||||||
};
|
};
|
||||||
Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H))
|
Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -511,14 +511,36 @@ impl OpenCADStudio {
|
||||||
360,
|
360,
|
||||||
470,
|
470,
|
||||||
),
|
),
|
||||||
super::super::ModalKind::AttributeEditor => sized(
|
super::super::ModalKind::AttributeEditor => {
|
||||||
|
let doc = &self.tabs[self.active_tab].scene.document;
|
||||||
|
let layers: Vec<String> = doc.layers.iter().map(|l| l.name.clone()).collect();
|
||||||
|
let mut linetypes: Vec<String> = vec!["ByLayer".to_string()];
|
||||||
|
linetypes.extend(
|
||||||
|
doc.line_types
|
||||||
|
.iter()
|
||||||
|
.map(|lt| lt.name.clone())
|
||||||
|
.filter(|n| !n.is_empty() && n != "ByLayer"),
|
||||||
|
);
|
||||||
|
let styles: Vec<String> = doc
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.map(|s| s.name.trim().to_string())
|
||||||
|
.filter(|n| !n.is_empty())
|
||||||
|
.collect();
|
||||||
|
sized(
|
||||||
crate::ui::window::attribute_editor::view_window(
|
crate::ui::window::attribute_editor::view_window(
|
||||||
&self.attr_editor_block,
|
&self.attr_editor_block,
|
||||||
&self.attr_editor_fields,
|
&self.attr_editor_rows,
|
||||||
),
|
self.attr_editor_selected,
|
||||||
460,
|
self.attr_editor_tab,
|
||||||
440,
|
layers,
|
||||||
|
linetypes,
|
||||||
|
styles,
|
||||||
),
|
),
|
||||||
|
640,
|
||||||
|
500,
|
||||||
|
)
|
||||||
|
}
|
||||||
super::super::ModalKind::SaveDialog => sized(
|
super::super::ModalKind::SaveDialog => sized(
|
||||||
save_as_dialog_window(
|
save_as_dialog_window(
|
||||||
&self.save_dialog_filename,
|
&self.save_dialog_filename,
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,149 @@
|
||||||
//! Attribute editor dialog — edit the attribute values of a single block
|
//! Enhanced attribute editor dialog — edit the attributes of one block
|
||||||
//! reference (INSERT). Opened by double-clicking a block that carries
|
//! reference (INSERT). Opened by double-clicking a block that carries
|
||||||
//! attributes, or by the ATTEDIT command with such a block selected.
|
//! attributes, or by ATTEDIT with such a block selected.
|
||||||
//!
|
//!
|
||||||
//! Each attribute is one row: its prompt/tag (read-only) and an editable value
|
//! Three tabs, mirroring the standard enhanced attribute editor:
|
||||||
//! box. OK writes every value back to the block; Cancel (the ✕ or button)
|
//! * **Attribute** — the tag / prompt / value list; the selected row's value
|
||||||
//! discards. The heavy lifting — applying edits, undo, repaint — lives in the
|
//! is edited below the list.
|
||||||
//! update handler (`Message::AttrEditorOk`); this module is pure layout.
|
//! * **Text Options** — the selected attribute's text formatting (style,
|
||||||
|
//! justification, height, rotation, width, oblique, flags).
|
||||||
|
//! * **Properties** — the selected attribute's common entity properties
|
||||||
|
//! (layer, linetype, colour, lineweight).
|
||||||
|
//!
|
||||||
|
//! This module is pure layout over a working copy ([`AttrRow`]); applying the
|
||||||
|
//! edits, undo and repaint live in the update handler (`Message::AttrEditorApply`).
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
use acadrust::entities::{HorizontalAlignment, VerticalAlignment};
|
||||||
|
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||||
|
use iced::widget::{
|
||||||
|
button, checkbox, column, container, pick_list, row, scrollable, text, text_input, Space,
|
||||||
|
};
|
||||||
use iced::{Background, Border, Color, Element, Length, Theme};
|
use iced::{Background, Border, Color, Element, Length, Theme};
|
||||||
|
|
||||||
|
use crate::ui::properties::{lw_options, LwItem};
|
||||||
|
|
||||||
|
// Palette shared with the style-manager windows so the editor reads as one of
|
||||||
|
// them: toolbar (TB) on top, BG panels, ACTIVE for the selected tab / row.
|
||||||
|
const TB: Color = Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 };
|
||||||
const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
const BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 };
|
||||||
const WHITE: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 };
|
const WHITE: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 };
|
||||||
const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
const DIM: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 };
|
||||||
const ACCENT: Color = Color { r: 0.30, g: 0.62, b: 0.95, a: 1.0 };
|
const ACCENT: Color = Color { r: 0.25, g: 0.50, b: 0.85, a: 1.0 };
|
||||||
const FIELD_BG: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 };
|
const FIELD_BG: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 };
|
||||||
const BORDER: Color = Color { r: 0.32, g: 0.32, b: 0.32, a: 1.0 };
|
const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 };
|
||||||
|
const ACTIVE: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 };
|
||||||
|
const ROW_SEL: Color = ACTIVE;
|
||||||
|
const ROW_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 };
|
||||||
|
|
||||||
|
const LABEL_W: f32 = 120.0;
|
||||||
|
|
||||||
|
/// Which tab of the editor is showing.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub enum AttrTab {
|
||||||
|
Attribute,
|
||||||
|
TextOptions,
|
||||||
|
Properties,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Editable working copy of one attribute. Numeric fields are kept as strings
|
||||||
|
/// so the user can type freely; they are parsed when Apply is pressed. Angles
|
||||||
|
/// are shown and entered in degrees.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AttrRow {
|
||||||
|
pub tag: String,
|
||||||
|
pub prompt: String,
|
||||||
|
pub value: String,
|
||||||
|
// Text options
|
||||||
|
pub text_style: String,
|
||||||
|
pub height: String,
|
||||||
|
pub rotation: String,
|
||||||
|
pub width_factor: String,
|
||||||
|
pub oblique: String,
|
||||||
|
pub h_align: HorizontalAlignment,
|
||||||
|
pub v_align: VerticalAlignment,
|
||||||
|
pub backwards: bool,
|
||||||
|
pub upside_down: bool,
|
||||||
|
// Properties (common entity)
|
||||||
|
pub layer: String,
|
||||||
|
pub color: AcadColor,
|
||||||
|
pub linetype: String,
|
||||||
|
pub line_weight: LineWeight,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combined justification options (horizontal × vertical), matching the
|
||||||
|
/// standard text-justification list.
|
||||||
|
pub const JUSTIFY: &[(&str, HorizontalAlignment, VerticalAlignment)] = &[
|
||||||
|
("Left", HorizontalAlignment::Left, VerticalAlignment::Baseline),
|
||||||
|
("Center", HorizontalAlignment::Center, VerticalAlignment::Baseline),
|
||||||
|
("Right", HorizontalAlignment::Right, VerticalAlignment::Baseline),
|
||||||
|
("Aligned", HorizontalAlignment::Aligned, VerticalAlignment::Baseline),
|
||||||
|
("Middle", HorizontalAlignment::Middle, VerticalAlignment::Baseline),
|
||||||
|
("Fit", HorizontalAlignment::Fit, VerticalAlignment::Baseline),
|
||||||
|
("Top Left", HorizontalAlignment::Left, VerticalAlignment::Top),
|
||||||
|
("Top Center", HorizontalAlignment::Center, VerticalAlignment::Top),
|
||||||
|
("Top Right", HorizontalAlignment::Right, VerticalAlignment::Top),
|
||||||
|
("Middle Left", HorizontalAlignment::Left, VerticalAlignment::Middle),
|
||||||
|
("Middle Center", HorizontalAlignment::Center, VerticalAlignment::Middle),
|
||||||
|
("Middle Right", HorizontalAlignment::Right, VerticalAlignment::Middle),
|
||||||
|
("Bottom Left", HorizontalAlignment::Left, VerticalAlignment::Bottom),
|
||||||
|
("Bottom Center", HorizontalAlignment::Center, VerticalAlignment::Bottom),
|
||||||
|
("Bottom Right", HorizontalAlignment::Right, VerticalAlignment::Bottom),
|
||||||
|
];
|
||||||
|
|
||||||
|
/// The justification label for an (h, v) pair (defaults to "Left").
|
||||||
|
pub fn justify_label(h: HorizontalAlignment, v: VerticalAlignment) -> &'static str {
|
||||||
|
JUSTIFY
|
||||||
|
.iter()
|
||||||
|
.find(|(_, jh, jv)| *jh == h && *jv == v)
|
||||||
|
.map(|(l, _, _)| *l)
|
||||||
|
.unwrap_or("Left")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a justification label back to its (h, v) pair.
|
||||||
|
pub fn justify_from_label(label: &str) -> Option<(HorizontalAlignment, VerticalAlignment)> {
|
||||||
|
JUSTIFY
|
||||||
|
.iter()
|
||||||
|
.find(|(l, _, _)| *l == label)
|
||||||
|
.map(|(_, h, v)| (*h, *v))
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_OPTIONS: &[&str] = &[
|
||||||
|
"ByLayer", "ByBlock", "Red", "Yellow", "Green", "Cyan", "Blue", "Magenta", "White",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Display label for a colour (falls back to `Color <n>` for non-standard ACI).
|
||||||
|
pub fn color_label(c: AcadColor) -> String {
|
||||||
|
match c {
|
||||||
|
AcadColor::ByLayer => "ByLayer".into(),
|
||||||
|
AcadColor::ByBlock => "ByBlock".into(),
|
||||||
|
AcadColor::Index(1) => "Red".into(),
|
||||||
|
AcadColor::Index(2) => "Yellow".into(),
|
||||||
|
AcadColor::Index(3) => "Green".into(),
|
||||||
|
AcadColor::Index(4) => "Cyan".into(),
|
||||||
|
AcadColor::Index(5) => "Blue".into(),
|
||||||
|
AcadColor::Index(6) => "Magenta".into(),
|
||||||
|
AcadColor::Index(7) => "White".into(),
|
||||||
|
AcadColor::Index(n) => format!("Color {n}"),
|
||||||
|
AcadColor::Rgb { r, g, b } => format!("{r},{g},{b}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a colour label back to an `AcadColor` (None if unknown).
|
||||||
|
pub fn color_from_label(label: &str) -> Option<AcadColor> {
|
||||||
|
Some(match label {
|
||||||
|
"ByLayer" => AcadColor::ByLayer,
|
||||||
|
"ByBlock" => AcadColor::ByBlock,
|
||||||
|
"Red" => AcadColor::Index(1),
|
||||||
|
"Yellow" => AcadColor::Index(2),
|
||||||
|
"Green" => AcadColor::Index(3),
|
||||||
|
"Cyan" => AcadColor::Index(4),
|
||||||
|
"Blue" => AcadColor::Index(5),
|
||||||
|
"Magenta" => AcadColor::Index(6),
|
||||||
|
"White" => AcadColor::Index(7),
|
||||||
|
_ => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn field_style(_t: &Theme, _s: text_input::Status) -> text_input::Style {
|
fn field_style(_t: &Theme, _s: text_input::Status) -> text_input::Style {
|
||||||
text_input::Style {
|
text_input::Style {
|
||||||
|
|
@ -29,114 +156,304 @@ fn field_style(_t: &Theme, _s: text_input::Status) -> text_input::Style {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the attribute editor dialog body. `block` is the reference's block
|
fn accent_btn(_t: &Theme, status: button::Status) -> button::Style {
|
||||||
/// name (shown as a subtitle); `fields` are the `(tag, prompt, value)` triples
|
|
||||||
/// in attribute order — the row index is the routing key back to
|
|
||||||
/// `Message::AttrEditorInput`. Each row is labelled with the prompt (falling
|
|
||||||
/// back to the tag when the block defines none), so the label reads as it would
|
|
||||||
/// when inserting the block.
|
|
||||||
pub fn view_window<'a>(
|
|
||||||
block: &'a str,
|
|
||||||
fields: &'a [(String, String, String)],
|
|
||||||
) -> Element<'a, Message> {
|
|
||||||
let mut list = column![].spacing(6);
|
|
||||||
for (idx, (tag, prompt, value)) in fields.iter().enumerate() {
|
|
||||||
let label = if prompt.trim().is_empty() {
|
|
||||||
tag.as_str()
|
|
||||||
} else {
|
|
||||||
prompt.as_str()
|
|
||||||
};
|
|
||||||
|
|
||||||
let value_box = text_input("", value)
|
|
||||||
.on_input(move |v| Message::AttrEditorInput { idx, value: v })
|
|
||||||
.on_submit(Message::AttrEditorOk)
|
|
||||||
.style(field_style)
|
|
||||||
.size(13)
|
|
||||||
.padding([4, 6])
|
|
||||||
.width(Length::Fill);
|
|
||||||
|
|
||||||
let attr_row = row![
|
|
||||||
container(text(label).size(13).color(WHITE))
|
|
||||||
.width(170)
|
|
||||||
.padding([4, 6]),
|
|
||||||
value_box,
|
|
||||||
]
|
|
||||||
.spacing(8)
|
|
||||||
.align_y(iced::Center);
|
|
||||||
list = list.push(attr_row);
|
|
||||||
}
|
|
||||||
|
|
||||||
let body: Element<'_, Message> = if fields.is_empty() {
|
|
||||||
text("This block has no attributes.")
|
|
||||||
.size(13)
|
|
||||||
.color(DIM)
|
|
||||||
.into()
|
|
||||||
} else {
|
|
||||||
scrollable(list).height(Length::Fill).into()
|
|
||||||
};
|
|
||||||
|
|
||||||
let ok = button(text("OK").size(13).color(WHITE))
|
|
||||||
.padding([5, 22])
|
|
||||||
.on_press(Message::AttrEditorOk)
|
|
||||||
.style(|_: &Theme, status| {
|
|
||||||
let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
||||||
Color { r: 0.32, g: 0.55, b: 0.85, a: 1.0 }
|
Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 }
|
||||||
} else {
|
} else {
|
||||||
ACCENT
|
ACCENT
|
||||||
};
|
};
|
||||||
button::Style {
|
|
||||||
background: Some(Background::Color(bg)),
|
|
||||||
text_color: WHITE,
|
|
||||||
border: Border { radius: 4.0.into(), ..Default::default() },
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let cancel = button(text("Cancel").size(13).color(WHITE))
|
|
||||||
.padding([5, 18])
|
|
||||||
.on_press(Message::CloseModal)
|
|
||||||
.style(|_: &Theme, status| {
|
|
||||||
let bg = if matches!(status, button::Status::Hovered | button::Status::Pressed) {
|
|
||||||
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
|
|
||||||
} else {
|
|
||||||
Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 }
|
|
||||||
};
|
|
||||||
button::Style {
|
button::Style {
|
||||||
background: Some(Background::Color(bg)),
|
background: Some(Background::Color(bg)),
|
||||||
text_color: WHITE,
|
text_color: WHITE,
|
||||||
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
border: Border { color: BORDER, width: 1.0, radius: 4.0.into() },
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
let header = column![
|
/// Horizontal 1px divider in the shared border colour (matches style windows).
|
||||||
text("Edit Attributes").size(18).color(WHITE),
|
fn hdivider<'a>() -> Element<'a, Message> {
|
||||||
|
container(Space::new().width(Length::Fill).height(1))
|
||||||
|
.width(Length::Fill)
|
||||||
|
.height(1)
|
||||||
|
.style(|_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(BORDER)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One `label : widget` row with a fixed-width label column.
|
||||||
|
fn field_row<'a>(label: &'a str, widget: Element<'a, Message>) -> Element<'a, Message> {
|
||||||
|
row![
|
||||||
|
container(text(label).size(12).color(DIM)).width(LABEL_W),
|
||||||
|
widget,
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.align_y(iced::Center)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A labelled editable text field for one of the selected row's string buffers.
|
||||||
|
fn edit_field<'a>(
|
||||||
|
label: &'a str,
|
||||||
|
value: &'a str,
|
||||||
|
on_input: impl Fn(String) -> Message + 'a,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let ti = text_input("", value)
|
||||||
|
.on_input(on_input)
|
||||||
|
.on_submit(Message::AttrEditorApply)
|
||||||
|
.style(field_style)
|
||||||
|
.size(13)
|
||||||
|
.padding([3, 6])
|
||||||
|
.width(Length::Fill);
|
||||||
|
field_row(label, ti.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A labelled pick_list of owned string options.
|
||||||
|
fn pick_field<'a>(
|
||||||
|
label: &'a str,
|
||||||
|
options: Vec<String>,
|
||||||
|
selected: Option<String>,
|
||||||
|
on_select: impl Fn(String) -> Message + 'a,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let pl = pick_list(options, selected, on_select)
|
||||||
|
.text_size(13)
|
||||||
|
.padding([3, 6])
|
||||||
|
.width(Length::Fill);
|
||||||
|
field_row(label, pl.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_button<'a>(label: &'a str, this: AttrTab, active: AttrTab) -> Element<'a, Message> {
|
||||||
|
let is_active = this == active;
|
||||||
|
button(text(label).size(11).color(WHITE))
|
||||||
|
.padding([4, 12])
|
||||||
|
.on_press(Message::AttrEditorTab(this))
|
||||||
|
.style(move |_t: &Theme, status| button::Style {
|
||||||
|
background: Some(Background::Color(match (is_active, status) {
|
||||||
|
(true, _) => ACTIVE,
|
||||||
|
(false, button::Status::Hovered | button::Status::Pressed) => {
|
||||||
|
Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }
|
||||||
|
}
|
||||||
|
_ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 },
|
||||||
|
})),
|
||||||
|
text_color: WHITE,
|
||||||
|
border: Border { color: BORDER, width: 1.0, radius: 3.0.into() },
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the enhanced attribute editor. `rows` is the working copy (attribute
|
||||||
|
/// order); `selected` is the highlighted row driving the Text Options /
|
||||||
|
/// Properties tabs. `layers` / `linetypes` / `styles` are the document's pick
|
||||||
|
/// lists.
|
||||||
|
pub fn view_window<'a>(
|
||||||
|
block: &'a str,
|
||||||
|
rows: &'a [AttrRow],
|
||||||
|
selected: usize,
|
||||||
|
tab: AttrTab,
|
||||||
|
layers: Vec<String>,
|
||||||
|
linetypes: Vec<String>,
|
||||||
|
styles: Vec<String>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
// ── Top toolbar: block name on the left, Apply on the right ───────────
|
||||||
|
// Mirrors the style-manager windows (actions left, primary action right).
|
||||||
|
let apply = button(text("Apply").size(11).color(WHITE))
|
||||||
|
.padding([4, 14])
|
||||||
|
.on_press(Message::AttrEditorApply)
|
||||||
|
.style(accent_btn);
|
||||||
|
let toolbar = container(
|
||||||
|
row![
|
||||||
text(format!("Block: {block}")).size(12).color(DIM),
|
text(format!("Block: {block}")).size(12).color(DIM),
|
||||||
|
Space::new().width(Length::Fill),
|
||||||
|
apply,
|
||||||
|
]
|
||||||
|
.align_y(iced::Center),
|
||||||
|
)
|
||||||
|
.style(|_: &Theme| container::Style {
|
||||||
|
background: Some(Background::Color(TB)),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
.width(Length::Fill)
|
||||||
|
.padding([5, 8]);
|
||||||
|
|
||||||
|
let tabs = row![
|
||||||
|
tab_button("Attribute", AttrTab::Attribute, tab),
|
||||||
|
tab_button("Text Options", AttrTab::TextOptions, tab),
|
||||||
|
tab_button("Properties", AttrTab::Properties, tab),
|
||||||
]
|
]
|
||||||
.spacing(2);
|
.spacing(2);
|
||||||
|
|
||||||
container(
|
let body: Element<'_, Message> = if rows.is_empty() {
|
||||||
column![
|
text("This block has no attributes.").size(13).color(DIM).into()
|
||||||
header,
|
} else {
|
||||||
Space::new().height(10),
|
match tab {
|
||||||
body,
|
AttrTab::Attribute => attribute_tab(rows, selected),
|
||||||
Space::new().height(12),
|
AttrTab::TextOptions => text_options_tab(&rows[selected.min(rows.len() - 1)], styles),
|
||||||
row![
|
AttrTab::Properties => {
|
||||||
Space::new().width(Length::Fill),
|
properties_tab(&rows[selected.min(rows.len() - 1)], layers, linetypes)
|
||||||
cancel,
|
}
|
||||||
Space::new().width(8),
|
}
|
||||||
ok
|
};
|
||||||
]
|
|
||||||
.align_y(iced::Center),
|
let content = container(column![tabs, body].spacing(8))
|
||||||
]
|
.width(Length::Fill)
|
||||||
.padding(4),
|
.height(Length::Fill)
|
||||||
)
|
.padding(12);
|
||||||
|
|
||||||
|
container(column![toolbar, hdivider(), content])
|
||||||
.style(|_: &Theme| container::Style {
|
.style(|_: &Theme| container::Style {
|
||||||
background: Some(Background::Color(BG)),
|
background: Some(Background::Color(BG)),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
})
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.padding(14)
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attribute tab: tag / prompt / value list with row-select, plus a value box.
|
||||||
|
fn attribute_tab<'a>(rows: &'a [AttrRow], selected: usize) -> Element<'a, Message> {
|
||||||
|
let head = row![
|
||||||
|
container(text("Tag").size(11).color(DIM)).width(130),
|
||||||
|
container(text("Prompt").size(11).color(DIM)).width(Length::Fill),
|
||||||
|
container(text("Value").size(11).color(DIM)).width(140),
|
||||||
|
]
|
||||||
|
.spacing(6);
|
||||||
|
|
||||||
|
let mut list = column![].spacing(1);
|
||||||
|
for (idx, r) in rows.iter().enumerate() {
|
||||||
|
let is_sel = idx == selected;
|
||||||
|
let line = row![
|
||||||
|
container(text(r.tag.as_str()).size(12).color(WHITE)).width(130),
|
||||||
|
container(text(r.prompt.as_str()).size(12).color(DIM)).width(Length::Fill),
|
||||||
|
container(text(r.value.as_str()).size(12).color(WHITE)).width(140),
|
||||||
|
]
|
||||||
|
.spacing(6);
|
||||||
|
let btn = button(line)
|
||||||
|
.on_press(Message::AttrEditorSelect(idx))
|
||||||
|
.padding([3, 4])
|
||||||
|
.width(Length::Fill)
|
||||||
|
.style(move |_t: &Theme, status| {
|
||||||
|
let hovered = matches!(status, button::Status::Hovered);
|
||||||
|
let bg = if is_sel {
|
||||||
|
ROW_SEL
|
||||||
|
} else if hovered {
|
||||||
|
Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 }
|
||||||
|
} else {
|
||||||
|
ROW_BG
|
||||||
|
};
|
||||||
|
button::Style {
|
||||||
|
background: Some(Background::Color(bg)),
|
||||||
|
text_color: WHITE,
|
||||||
|
border: Border::default(),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
list = list.push(btn);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sel = selected.min(rows.len() - 1);
|
||||||
|
let value_box = text_input("", rows[sel].value.as_str())
|
||||||
|
.on_input(move |v| Message::AttrEditorInput { idx: sel, value: v })
|
||||||
|
.on_submit(Message::AttrEditorApply)
|
||||||
|
.style(field_style)
|
||||||
|
.size(13)
|
||||||
|
.padding([4, 6])
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
|
column![
|
||||||
|
head,
|
||||||
|
scrollable(list).height(Length::Fill),
|
||||||
|
Space::new().height(8),
|
||||||
|
field_row("Value:", value_box.into()),
|
||||||
|
]
|
||||||
|
.spacing(6)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Text Options tab: the selected attribute's text formatting.
|
||||||
|
fn text_options_tab<'a>(r: &'a AttrRow, styles: Vec<String>) -> Element<'a, Message> {
|
||||||
|
let style_sel = if r.text_style.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(r.text_style.clone())
|
||||||
|
};
|
||||||
|
let justify_opts: Vec<String> = JUSTIFY.iter().map(|(l, _, _)| l.to_string()).collect();
|
||||||
|
let justify_sel = Some(justify_label(r.h_align, r.v_align).to_string());
|
||||||
|
|
||||||
|
column![
|
||||||
|
pick_field("Text Style", styles, style_sel, |s| {
|
||||||
|
Message::AttrEditorTextStyle(s)
|
||||||
|
}),
|
||||||
|
pick_field("Justification", justify_opts, justify_sel, |s| {
|
||||||
|
Message::AttrEditorJustify(s)
|
||||||
|
}),
|
||||||
|
edit_field("Height", &r.height, Message::AttrEditorHeight),
|
||||||
|
edit_field("Rotation", &r.rotation, Message::AttrEditorRotation),
|
||||||
|
edit_field("Width Factor", &r.width_factor, Message::AttrEditorWidth),
|
||||||
|
edit_field("Oblique Angle", &r.oblique, Message::AttrEditorOblique),
|
||||||
|
field_row(
|
||||||
|
"",
|
||||||
|
checkbox(r.backwards)
|
||||||
|
.label("Backwards")
|
||||||
|
.on_toggle(Message::AttrEditorBackwards)
|
||||||
|
.size(15)
|
||||||
|
.text_size(12)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
field_row(
|
||||||
|
"",
|
||||||
|
checkbox(r.upside_down)
|
||||||
|
.label("Upside down")
|
||||||
|
.on_toggle(Message::AttrEditorUpsideDown)
|
||||||
|
.size(15)
|
||||||
|
.text_size(12)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Properties tab: the selected attribute's common entity properties.
|
||||||
|
fn properties_tab<'a>(
|
||||||
|
r: &'a AttrRow,
|
||||||
|
layers: Vec<String>,
|
||||||
|
linetypes: Vec<String>,
|
||||||
|
) -> Element<'a, Message> {
|
||||||
|
let layer_sel = Some(r.layer.clone());
|
||||||
|
let lt_sel = Some(if r.linetype.is_empty() {
|
||||||
|
"ByLayer".to_string()
|
||||||
|
} else {
|
||||||
|
r.linetype.clone()
|
||||||
|
});
|
||||||
|
let mut color_opts: Vec<String> = COLOR_OPTIONS.iter().map(|s| s.to_string()).collect();
|
||||||
|
// The attribute may carry a colour outside the standard set (ACI 8-255 or
|
||||||
|
// RGB). Surface its label so it displays correctly and stays selectable,
|
||||||
|
// rather than showing blank; picking a listed colour still overrides it.
|
||||||
|
let cur_color = color_label(r.color);
|
||||||
|
if !color_opts.iter().any(|o| o == &cur_color) {
|
||||||
|
color_opts.insert(0, cur_color.clone());
|
||||||
|
}
|
||||||
|
let color_sel = Some(cur_color);
|
||||||
|
|
||||||
|
let lw_opts = lw_options();
|
||||||
|
let lw_sel = LwItem(r.line_weight);
|
||||||
|
let lw = pick_list(lw_opts, Some(lw_sel), |it: LwItem| {
|
||||||
|
Message::AttrEditorLineweight(it.0)
|
||||||
|
})
|
||||||
|
.text_size(13)
|
||||||
|
.padding([3, 6])
|
||||||
|
.width(Length::Fill);
|
||||||
|
|
||||||
|
column![
|
||||||
|
pick_field("Layer", layers, layer_sel, Message::AttrEditorLayer),
|
||||||
|
pick_field("Linetype", linetypes, lt_sel, Message::AttrEditorLinetype),
|
||||||
|
pick_field("Color", color_opts, color_sel, |s| {
|
||||||
|
Message::AttrEditorColor(s)
|
||||||
|
}),
|
||||||
|
field_row("Lineweight", lw.into()),
|
||||||
|
]
|
||||||
|
.spacing(8)
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue