diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 7b2f816d0..7143e9ad1 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -172,35 +172,17 @@ impl OpenCADStudio { let i = self.active_tab; match result { CmdResult::NeedPoint => { - // If ATTEDIT just completed entity pick, inject attribute data. + // ATTEDIT finished its entity pick: hand the chosen block off to + // the attribute editor dialog and end the command (open_attribute + // _editor reports "no attributes" / "select a block" as needed). let attedit_handle = self.tabs[i] .active_cmd .as_ref() .and_then(|c| c.attedit_pending_handle()); if let Some(ins_handle) = attedit_handle { - if let Some(acadrust::EntityType::Insert(ins)) = - self.tabs[i].scene.document.get_entity(ins_handle) - { - let attrs: Vec<(String, String)> = ins - .attributes - .iter() - .map(|a| (a.tag.clone(), a.get_value().to_string())) - .collect(); - if attrs.is_empty() { - self.command_line - .push_error("ATTEDIT This INSERT has no attributes."); - self.tabs[i].active_cmd = None; - return Task::none(); - } - if let Some(cmd) = &mut self.tabs[i].active_cmd { - cmd.attedit_set_attrs(attrs); - } - } else { - self.command_line - .push_error("ATTEDIT Please select an INSERT entity with attributes."); - self.tabs[i].active_cmd = None; - return Task::none(); - } + self.tabs[i].active_cmd = None; + self.open_attribute_editor(ins_handle); + return Task::none(); } let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { @@ -458,28 +440,6 @@ impl OpenCADStudio { self.refresh_properties(); } CmdResult::ReplaceEntity(handle, new_entities) => { - // Detect ATTEDIT sentinel. - if new_entities.len() == 1 { - if let acadrust::EntityType::XLine(ref xl) = new_entities[0] { - let layer = xl.common.layer.clone(); - if let Some(encoded) = layer.strip_prefix("__ATTEDIT__") { - let label = self.history_label_from_active_cmd(i, "ATTEDIT"); - self.push_undo_snapshot(i, label); - crate::modules::draw::modify::attedit::apply_attedit( - &mut self.tabs[i].scene.document, - handle, - encoded, - ); - self.tabs[i].dirty = true; - self.tabs[i].active_cmd = None; - self.tabs[i].snap_result = None; - self.command_line - .push_output("ATTEDIT Attribute values updated."); - return Task::none(); - } - } - } - // Detect SPLINEDIT sentinel: a single XLine with a magic layer name. if new_entities.len() == 1 { if let acadrust::EntityType::XLine(ref xl) = new_entities[0] { diff --git a/src/app/commands/inquiry.rs b/src/app/commands/inquiry.rs index 15a9f7c1d..912ecd32b 100644 --- a/src/app/commands/inquiry.rs +++ b/src/app/commands/inquiry.rs @@ -196,13 +196,33 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); } - // Bare ATTEDIT (and the ATE alias) launch the interactive attribute - // editor; the command-line -ATTEDIT form is handled in the draw family. + // Bare ATTEDIT (and the ATE alias) open the attribute editor dialog. + // If a single block with attributes is already selected it opens on + // that block; otherwise the pick command runs and the editor opens + // once a block is chosen (see `command_driver`). "ATTEDIT" | "ATE" => { - use crate::modules::draw::modify::attedit::AtteditCommand; - let cmd_obj = AtteditCommand::new(); - self.command_line.push_info(&cmd_obj.prompt()); - self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); + let selected_attr_insert = { + let sel = self.tabs[i].scene.selected_entities(); + if sel.len() == 1 { + let (h, e) = sel[0]; + match e { + acadrust::EntityType::Insert(ins) if !ins.attributes.is_empty() => { + Some(h) + } + _ => None, + } + } else { + None + } + }; + if let Some(handle) = selected_attr_insert { + self.open_attribute_editor(handle); + } else { + use crate::modules::draw::modify::attedit::AtteditCommand; + let cmd_obj = AtteditCommand::new(); + self.command_line.push_info(&cmd_obj.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd_obj)); + } } // ── REFEDIT — in-place block editing ───────────────────────────── diff --git a/src/app/mod.rs b/src/app/mod.rs index 8919738d5..633f3ae3a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -356,6 +356,14 @@ pub(super) struct OpenCADStudio { modal_drag_last: Option, /// True while the modal title bar is held (a drag is in progress). modal_dragging: bool, + // ── Attribute editor dialog (ATTEDIT / double-click a block) ─────────── + /// INSERT whose attributes the editor modal is editing (`None` = closed). + attr_editor_handle: Option, + /// Block name shown in the editor's title bar. + attr_editor_block: String, + /// Working copy of the block's attributes as `(tag, value)`, in the same + /// order as `Insert::attributes`. Edited live; written back on OK. + attr_editor_fields: Vec<(String, String)>, /// Plugin ids the user turned off in the Plugin Manager. Disabled plugins /// keep their manifest listed but drop their ribbon tab and command /// dispatch. Persisted via [`settings::UserSettings::disabled_plugins`]. @@ -944,6 +952,7 @@ pub enum ModalKind { #[cfg_attr(target_arch = "wasm32", allow(dead_code))] AssocPrompt, PointStyle, + AttributeEditor, } /// Identifies a DimStyle field that can be edited in the dialog. @@ -1442,6 +1451,13 @@ pub enum Message { AboutOpen, /// Close whatever in-canvas modal dialog is open (Plan B). CloseModal, + // ── Attribute editor dialog ─────────────────────────────────────────── + /// Open the attribute editor for an INSERT (double-click / ATTEDIT). + AttrEditorOpen(acadrust::Handle), + /// Live edit of the attribute value at row `idx` in the editor dialog. + AttrEditorInput { idx: usize, value: String }, + /// Apply every attribute edit to the block and close the dialog (OK). + AttrEditorOk, /// Title-bar pressed: begin dragging the active modal. ModalGrab, /// Cursor moved while dragging the modal title bar. @@ -1914,6 +1930,9 @@ impl OpenCADStudio { modal_offset: iced::Vector::ZERO, modal_drag_last: None, modal_dragging: false, + attr_editor_handle: None, + attr_editor_block: String::new(), + attr_editor_fields: Vec::new(), disabled_plugins: rustc_hash::FxHashSet::default(), external_plugins: Vec::new(), loaded_plugin_ids: rustc_hash::FxHashSet::default(), diff --git a/src/app/update/command.rs b/src/app/update/command.rs index 2966a197c..dc2f96fde 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -27,6 +27,10 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if self.tabs.get(idx).map_or(false, |t| t.is_start) { return Task::none(); } + // Closing a tab shifts indices / the active tab. The attribute + // editor holds a document-local handle into one tab, so drop it + // now rather than risk it applying to a different tab's document. + self.cancel_attr_editor(); if self.tabs.get(idx).map_or(false, |t| t.dirty) { self.pending_close = Some(crate::app::PendingClose::Tab(idx)); return self.open_unsaved_dialog_window(); @@ -1204,4 +1208,102 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { self.refresh_properties(); Task::none() } + + /// Open the attribute editor dialog for the given INSERT, loading a working + /// copy of its attribute values. No-op (with a hint) when the block has no + /// attributes. Entry points: double-clicking such a block, or ATTEDIT. + pub(crate) fn open_attribute_editor(&mut self, handle: acadrust::Handle) { + let i = self.active_tab; + let loaded = match self.tabs[i].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Insert(ins)) if !ins.attributes.is_empty() => { + let block = ins.block_name.clone(); + let fields = ins + .attributes + .iter() + .map(|a| (a.tag.clone(), a.get_value().to_string())) + .collect::>(); + Some((block, fields)) + } + Some(acadrust::EntityType::Insert(_)) => { + self.command_line + .push_error("ATTEDIT This block has no attributes."); + None + } + _ => { + self.command_line + .push_error("ATTEDIT Select a block with attributes."); + None + } + }; + if let Some((block, fields)) = loaded { + self.attr_editor_block = block; + self.attr_editor_fields = fields; + self.attr_editor_handle = Some(handle); + self.active_modal = Some(crate::app::ModalKind::AttributeEditor); + self.modal_offset = iced::Vector::ZERO; + } + } + + /// 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 + /// document-local and must not outlive its tab. + pub(super) fn cancel_attr_editor(&mut self) { + if self.active_modal == Some(crate::app::ModalKind::AttributeEditor) { + self.active_modal = None; + self.modal_offset = iced::Vector::ZERO; + } + self.attr_editor_handle = None; + self.attr_editor_block.clear(); + self.attr_editor_fields.clear(); + } + + /// Apply every edited attribute value back to the block and close the + /// editor (OK). Values are written verbatim (attribute text is free-form). + /// The edits are positional — same order the dialog was populated in — so + /// blocks with duplicate tags stay correct; a tag-based fallback covers the + /// unlikely case the attribute list changed underneath the open dialog. + pub(super) fn on_attr_editor_ok(&mut self) -> Task { + let i = self.active_tab; + let Some(handle) = self.attr_editor_handle.take() else { + self.active_modal = None; + return Task::none(); + }; + let fields = std::mem::take(&mut self.attr_editor_fields); + self.attr_editor_block.clear(); + self.active_modal = None; + self.modal_offset = iced::Vector::ZERO; + + self.push_undo_snapshot(i, "ATTEDIT"); + let mut changed = false; + if let Some(acadrust::EntityType::Insert(ins)) = + self.tabs[i].scene.document.get_entity_mut(handle) + { + if fields.len() == ins.attributes.len() { + for (attr, (_, val)) in ins.attributes.iter_mut().zip(fields.iter()) { + if attr.get_value() != val { + 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; + } + } + } + } + } + if changed { + self.invalidate_property_targets(i, &[handle]); + self.tabs[i].dirty = true; + } else { + // Nothing changed — drop the snapshot pushed a moment ago. + let _ = self.tabs[i].history.undo_stack.pop(); + } + self.refresh_properties(); + Task::none() + } } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 7738192bd..2bb1ea058 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -104,6 +104,12 @@ impl OpenCADStudio { // Dismissing these via ✕ is the cancel/decline path. Some(Unsaved) => self.pending_close = None, Some(AssocPrompt) => self.mark_assoc_prompted(), + // Cancel: drop the working copy without touching the block. + Some(AttributeEditor) => { + self.attr_editor_handle = None; + self.attr_editor_block.clear(); + self.attr_editor_fields.clear(); + } _ => {} } self.active_modal = None; @@ -645,6 +651,11 @@ impl OpenCADStudio { Message::TabSwitch(idx) => { if idx < self.tabs.len() { + if idx != self.active_tab { + // The attribute editor is tab-scoped; leaving its tab + // drops it (its handle is that document's, not this one's). + self.cancel_attr_editor(); + } self.active_tab = idx; self.sync_ribbon_layers(); self.sync_ribbon_styles(); @@ -2253,6 +2264,17 @@ impl OpenCADStudio { self.close_active_modal(); Task::none() } + Message::AttrEditorOpen(handle) => { + self.open_attribute_editor(handle); + Task::none() + } + Message::AttrEditorInput { idx, value } => { + if let Some(field) = self.attr_editor_fields.get_mut(idx) { + field.1 = value; + } + Task::none() + } + Message::AttrEditorOk => self.on_attr_editor_ok(), Message::ModalGrab => { // Start a drag; the first ModalDragMove seeds the reference. self.modal_dragging = true; diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index ecf63bdf7..217c2a0a9 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -2437,9 +2437,18 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task { if is_editable_text { return self.begin_text_edit(handle); } - // Double-clicking a block reference enters in-place - // block edit (REFEDIT), so its geometry can be edited - // and the change reflects in every instance. (#136) + // Double-clicking a block with attributes opens the + // attribute editor (edit its values); a block with + // no attributes enters in-place block edit (REFEDIT), + // so its geometry can be edited and the change + // reflects in every instance. (#136, #192) + let insert_has_attrs = matches!( + self.tabs[i].scene.document.get_entity(handle), + Some(AcadEntityType::Insert(ins)) if !ins.attributes.is_empty() + ); + if insert_has_attrs { + return Task::done(Message::AttrEditorOpen(handle)); + } let is_insert = matches!( self.tabs[i].scene.document.get_entity(handle), Some(AcadEntityType::Insert(_)) diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 963a12666..3174ebdcf 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1363,6 +1363,7 @@ impl OpenCADStudio { OverwriteWarning => (420, 180), SaveDialog => (560, 480), PointStyle => (360, 470), + AttributeEditor => (460, 440), }; Some((w as f32 + EXTRA_W, h as f32 + EXTRA_H)) } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 2639e0aae..484b7f416 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -511,6 +511,14 @@ impl OpenCADStudio { 360, 470, ), + super::super::ModalKind::AttributeEditor => sized( + crate::ui::window::attribute_editor::view_window( + &self.attr_editor_block, + &self.attr_editor_fields, + ), + 460, + 440, + ), super::super::ModalKind::SaveDialog => sized( save_as_dialog_window( &self.save_dialog_filename, diff --git a/src/command.rs b/src/command.rs index 1ae6eedfb..72383f9b8 100644 --- a/src/command.rs +++ b/src/command.rs @@ -667,15 +667,12 @@ pub trait CadCommand: Send { None } - /// If this command needs attribute data injected (ATTEDIT), returns the - /// INSERT handle awaiting attr initialization; else None. + /// The block reference an ATTEDIT pick has resolved to, awaiting the + /// attribute editor dialog; else None. fn attedit_pending_handle(&self) -> Option { None } - /// Inject attribute (tag, value) pairs into the command after entity pick. - fn attedit_set_attrs(&mut self, _attrs: Vec<(String, String)>) {} - /// Inject attribute definitions (tag, prompt, default_value) for ATTREQ /// attr-filling after INSERT point is picked. fn attreq_set_attdefs(&mut self, _attdefs: Vec<(String, String, String)>) {} diff --git a/src/modules/draw/modify/attedit.rs b/src/modules/draw/modify/attedit.rs index 465a7a1c5..1e49ce407 100644 --- a/src/modules/draw/modify/attedit.rs +++ b/src/modules/draw/modify/attedit.rs @@ -1,39 +1,25 @@ -// ATTEDIT command — edit attribute values of a selected INSERT entity. +// ATTEDIT command — pick a block reference with attributes, then open the +// attribute editor dialog on it. // -// Workflow: -// Step 1: pick an INSERT entity that has attributes. -// Step 2+: for each attribute tag, show "TAG = " and accept new -// text via text input. Enter with empty string keeps the old value. -// After all attributes are processed, commit via ReplaceMany. +// This command only performs the entity pick. Once a block is chosen it +// reports the handle through `attedit_pending_handle`; the command host +// (`command_driver`) ends the command and opens the editor dialog. When a +// suitable block is already selected, the ATTEDIT dispatch opens the dialog +// directly and never starts this command (see `app::commands::inquiry`). -use acadrust::EntityType; use glam::DVec3; use crate::command::{CadCommand, CmdResult}; use crate::scene::model::wire_model::WireModel; pub struct AtteditCommand { - step: Step, -} - -enum Step { - /// Waiting for the user to pick an INSERT entity. - SelectInsert, - /// Editing the attribute at index `idx` of the collected insert data. - EditAttr { - handle: acadrust::Handle, - /// (tag, current_value) pairs collected from the insert. - attrs: Vec<(String, String)>, - /// Index of the attribute currently being edited. - idx: usize, - }, + /// The picked block reference, once the user clicks one. + picked: Option, } impl AtteditCommand { pub fn new() -> Self { - Self { - step: Step::SelectInsert, - } + Self { picked: None } } } @@ -43,77 +29,23 @@ impl CadCommand for AtteditCommand { } fn prompt(&self) -> String { - match &self.step { - Step::SelectInsert => "ATTEDIT Select block with attributes:".to_string(), - Step::EditAttr { attrs, idx, .. } => { - let (tag, val) = &attrs[*idx]; - format!( - "ATTEDIT {} = <{}> (Enter to keep, type new value):", - tag, val - ) - } - } + "ATTEDIT Select block with attributes:".to_string() } fn needs_entity_pick(&self) -> bool { - matches!(self.step, Step::SelectInsert) + self.picked.is_none() } fn on_entity_pick(&mut self, handle: acadrust::Handle, _pt: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; } - // We can't inspect the document here — store the handle and let the - // command host inject the attribute list via `init_with_attrs`. - // Instead, signal the host to call prepare_attedit(). - self.step = Step::EditAttr { - handle, - attrs: vec![], // will be filled by init_with_attrs() in cmd_result.rs - idx: 0, - }; + // Record the pick and yield: the host reads `attedit_pending_handle` + // and opens the editor dialog for this block. + self.picked = Some(handle); CmdResult::NeedPoint } - fn wants_text_input(&self) -> bool { - if let Step::EditAttr { ref attrs, idx, .. } = self.step { - !attrs.is_empty() && idx < attrs.len() - } else { - false - } - } - - fn wants_text_with_spaces(&self) -> bool { - // Editing an attribute value — same free-form semantics as the - // text-content prompts in TEXT / MTEXT / DDEDIT. - self.wants_text_input() - } - - fn on_text_input(&mut self, text: &str) -> Option { - let Step::EditAttr { handle, attrs, idx } = &mut self.step else { - return None; - }; - let handle = *handle; - - // Update the current attribute value if the user typed something. - if !text.trim().is_empty() { - attrs[*idx].1 = text.trim().to_string(); - } - *idx += 1; - - if *idx >= attrs.len() { - // All attributes done — build new Insert entity. - // We signal via a special CmdResult; the host will call apply_attedit(). - let pairs = attrs.clone(); - return Some(CmdResult::ReplaceEntity( - handle, - vec![make_attedit_sentinel(handle, &pairs)], - )); - } - - // More attributes to edit. - None - } - fn on_point(&mut self, _pt: DVec3) -> CmdResult { CmdResult::NeedPoint } @@ -125,55 +57,7 @@ impl CadCommand for AtteditCommand { } fn attedit_pending_handle(&self) -> Option { - if let Step::EditAttr { handle, attrs, .. } = &self.step { - if attrs.is_empty() { - return Some(*handle); - } - } - None - } - - fn attedit_set_attrs(&mut self, new_attrs: Vec<(String, String)>) { - if let Step::EditAttr { attrs, idx, .. } = &mut self.step { - *attrs = new_attrs; - *idx = 0; - } - } -} - -/// Make a sentinel entity carrying the edited attribute values. -/// Encodes all (tag=value) pairs in the layer field as "tag1\x01val1\x02tag2\x01val2...". -fn make_attedit_sentinel(_handle: acadrust::Handle, pairs: &[(String, String)]) -> EntityType { - let encoded: String = pairs - .iter() - .map(|(t, v)| format!("{}\x01{}", t, v)) - .collect::>() - .join("\x02"); - let mut xl = acadrust::entities::XLine::new( - acadrust::types::Vector3::zero(), - acadrust::types::Vector3::new(1.0, 0.0, 0.0), - ); - xl.common.layer = format!("__ATTEDIT__{}", encoded); - EntityType::XLine(xl) -} - -/// Apply edited attribute values to an INSERT entity in the document. -/// Called from `cmd_result.rs` when the sentinel is detected. -pub fn apply_attedit(doc: &mut acadrust::CadDocument, handle: acadrust::Handle, encoded: &str) { - let Some(EntityType::Insert(ins)) = doc.get_entity_mut(handle) else { - return; - }; - for pair in encoded.split('\x02') { - let mut parts = pair.splitn(2, '\x01'); - let Some(tag) = parts.next() else { - continue; - }; - let Some(val) = parts.next() else { - continue; - }; - if let Some(attrib) = ins.attributes.iter_mut().find(|a| a.tag == tag) { - attrib.set_value(val); - } + self.picked } } diff --git a/src/ui/window/attribute_editor.rs b/src/ui/window/attribute_editor.rs new file mode 100644 index 000000000..37024d3e4 --- /dev/null +++ b/src/ui/window/attribute_editor.rs @@ -0,0 +1,131 @@ +//! Attribute editor dialog — edit the attribute values of a single block +//! reference (INSERT). Opened by double-clicking a block that carries +//! attributes, or by the ATTEDIT command with such a block selected. +//! +//! Each attribute is one row: its tag (read-only) and an editable value box. +//! OK writes every value back to the block; Cancel (the ✕ or button) discards. +//! The heavy lifting — applying edits, undo, repaint — lives in the update +//! handler (`Message::AttrEditorOk`); this module is pure layout. + +use crate::app::Message; +use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; +use iced::{Background, Border, Color, Element, Length, Theme}; + +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 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 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 }; + +fn field_style(_t: &Theme, _s: text_input::Status) -> text_input::Style { + text_input::Style { + background: Background::Color(FIELD_BG), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + icon: WHITE, + placeholder: DIM, + value: WHITE, + selection: ACCENT, + } +} + +/// Build the attribute editor dialog body. `block` is the reference's block +/// name (shown as a subtitle); `fields` are the `(tag, value)` pairs in +/// attribute order — the row index is the routing key back to +/// `Message::AttrEditorInput`. +pub fn view_window<'a>(block: &'a str, fields: &'a [(String, String)]) -> Element<'a, Message> { + let mut list = column![].spacing(6); + for (idx, (tag, value)) in fields.iter().enumerate() { + 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(tag.as_str()).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) { + Color { r: 0.32, g: 0.55, b: 0.85, a: 1.0 } + } else { + 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 { + background: Some(Background::Color(bg)), + text_color: WHITE, + border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, + ..Default::default() + } + }); + + let header = column![ + text("Edit Attributes").size(18).color(WHITE), + text(format!("Block: {block}")).size(12).color(DIM), + ] + .spacing(2); + + container( + column![ + header, + Space::new().height(10), + body, + Space::new().height(12), + row![ + Space::new().width(Length::Fill), + cancel, + Space::new().width(8), + ok + ] + .align_y(iced::Center), + ] + .padding(4), + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BG)), + ..Default::default() + }) + .width(Length::Fill) + .height(Length::Fill) + .padding(14) + .into() +} diff --git a/src/ui/window/mod.rs b/src/ui/window/mod.rs index bce850592..9b6620ad7 100644 --- a/src/ui/window/mod.rs +++ b/src/ui/window/mod.rs @@ -6,3 +6,4 @@ pub mod shortcuts; pub mod layers; pub mod update_notice; pub mod open_progress; +pub mod attribute_editor;