diff --git a/src/app/mod.rs b/src/app/mod.rs index 3dc6e873c..22f3a8d1e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -1,19 +1,22 @@ mod cmd_result; mod commands; -pub mod plugin_host; mod document; mod expr_eval; mod helpers; mod history; mod layers; -mod mtext_editor; mod model_ops; +mod mtext_editor; +pub mod plugin_host; mod properties; mod settings; +mod style_ops; mod text_inline; mod update; mod view; +pub use style_ops::StyleKind; + use document::DocumentTab; use crate::modules::ModuleEvent; @@ -382,6 +385,13 @@ pub(super) struct OpenCADStudio { ts_border_color: [[String; 6]; 3], ts_border_spacing: [[String; 6]; 3], + // ── Shared style-manager inline rename ──────────────────────────────── + /// Original name of the style currently being renamed inline (double-click + /// a style name in any style manager). `None` when not renaming. + style_rename: Option, + /// Edit buffer for the inline rename text input. + style_rename_buf: String, + // ── TextStyle Font Browser ──────────────────────────────────────────── textstyle_selected: String, /// Edit buffer for font file name. @@ -1091,6 +1101,13 @@ pub enum Message { TextStyleDialogSelect(String), TextStyleDialogSetCurrent, TextStyleDialogNew, + TextStyleDialogCopy, + // Shared inline-rename messages for every style manager. `StyleKind` + // routes the commit to the right backing store. + StyleRenameStart(StyleKind, String), + StyleRenameEdit(String), + StyleRenameCommit(StyleKind), + StyleRenameCancel, TextStyleDialogDelete, /// Edit a string field (FontFile / Width / Oblique). TextStyleEdit { @@ -1110,14 +1127,19 @@ pub enum Message { TableStyleDialogClose, TableStyleDialogSelect(String), TableStyleDialogNew, + TableStyleDialogCopy, TableStyleDialogDelete, + TableStyleDialogSetCurrent, /// Toggle the Annotative flag on the selected table style. TableStyleToggleAnnotative, /// Toggle a boolean flag (title_suppressed / header_suppressed / flow) on /// the selected table style. TableStyleToggle(&'static str), /// Update a general edit buffer (hmargin / vmargin). - TableStyleEdit { field: &'static str, value: String }, + TableStyleEdit { + field: &'static str, + value: String, + }, /// Write the general edit buffers back into the selected table style. TableStyleApply, /// Update a per-cell edit buffer (row 0=Data,1=Header,2=Title). @@ -1161,7 +1183,9 @@ pub enum Message { MlStyleDialogClose, MlStyleDialogSelect(String), MlStyleDialogSetCurrent, + MlStyleApply, MlStyleDialogNew, + MlStyleDialogCopy, MlStyleDialogDelete, // ── MLeaderStyle Dialog ─────────────────────────────────────────────── MLeaderStyleDialogOpen, @@ -1170,6 +1194,7 @@ pub enum Message { MLeaderStyleDialogSelect(String), MLeaderStyleDialogSetCurrent, MLeaderStyleDialogNew, + MLeaderStyleDialogCopy, MLeaderStyleDialogDelete, MLeaderStyleEdit { field: &'static str, @@ -1198,6 +1223,7 @@ pub enum Message { DimStyleDialogTab(u8), /// Create a new empty style (prompts via command line). DimStyleDialogNew, + DimStyleDialogCopy, /// Set the selected style as the document's current dim style. DimStyleDialogSetCurrent, /// Delete the selected style. @@ -1362,6 +1388,8 @@ impl OpenCADStudio { ps_lineweight_buf: "255".to_string(), ps_screening_buf: "100".to_string(), // TextStyle font browser + style_rename: None, + style_rename_buf: String::new(), textstyle_selected: "Standard".to_string(), textstyle_font: String::new(), textstyle_width: "1.0".to_string(), @@ -1551,59 +1579,63 @@ impl OpenCADStudio { use std::path::PathBuf; pub fn run() -> iced::Result { - iced::daemon(OpenCADStudio::boot, OpenCADStudio::update, OpenCADStudio::view) - .subscription(OpenCADStudio::subscription) - .title(|state: &OpenCADStudio, window_id: window::Id| { - if Some(window_id) == state.layer_window { - return "Layer Properties Manager".into(); - } - if Some(window_id) == state.page_setup_window { - return "Page Setup".into(); - } - if Some(window_id) == state.textstyle_window { - return "Text Style".into(); - } - if Some(window_id) == state.tablestyle_window { - return "Table Style".into(); - } - if Some(window_id) == state.mlstyle_window { - return "Multiline Style".into(); - } - if Some(window_id) == state.mleaderstyle_window { - return "Multileader Style".into(); - } - if Some(window_id) == state.layout_manager_window { - return "Layout Manager".into(); - } - if Some(window_id) == state.plotstyle_window { - return "Plot Style Table Editor".into(); - } - if Some(window_id) == state.dimstyle_window { - return "Dimension Style Manager".into(); - } - if Some(window_id) == state.shortcuts_window { - return "Keyboard Shortcuts".into(); - } - if Some(window_id) == state.about_window { - return "About Open CAD Studio".into(); - } - if Some(window_id) == state.update_notice_window { - return "Update Available".into(); - } - if Some(window_id) == state.unsaved_dialog_window { - return "Unsaved Changes".into(); - } - if Some(window_id) == state.save_dialog_window { - return "Save As".into(); - } - if let Some(tab) = state.tabs.get(state.active_tab) { - let dot = if tab.dirty { "● " } else { "" }; - let name = tab.tab_display_name(); - format!("{}Open CAD Studio — {}", dot, name) - } else { - "Open CAD Studio".to_string() - } - }) - .theme(|state: &OpenCADStudio, _| state.active_theme.clone()) - .run() + iced::daemon( + OpenCADStudio::boot, + OpenCADStudio::update, + OpenCADStudio::view, + ) + .subscription(OpenCADStudio::subscription) + .title(|state: &OpenCADStudio, window_id: window::Id| { + if Some(window_id) == state.layer_window { + return "Layer Properties Manager".into(); + } + if Some(window_id) == state.page_setup_window { + return "Page Setup".into(); + } + if Some(window_id) == state.textstyle_window { + return "Text Style".into(); + } + if Some(window_id) == state.tablestyle_window { + return "Table Style".into(); + } + if Some(window_id) == state.mlstyle_window { + return "Multiline Style".into(); + } + if Some(window_id) == state.mleaderstyle_window { + return "Multileader Style".into(); + } + if Some(window_id) == state.layout_manager_window { + return "Layout Manager".into(); + } + if Some(window_id) == state.plotstyle_window { + return "Plot Style Table Editor".into(); + } + if Some(window_id) == state.dimstyle_window { + return "Dimension Style Manager".into(); + } + if Some(window_id) == state.shortcuts_window { + return "Keyboard Shortcuts".into(); + } + if Some(window_id) == state.about_window { + return "About Open CAD Studio".into(); + } + if Some(window_id) == state.update_notice_window { + return "Update Available".into(); + } + if Some(window_id) == state.unsaved_dialog_window { + return "Unsaved Changes".into(); + } + if Some(window_id) == state.save_dialog_window { + return "Save As".into(); + } + if let Some(tab) = state.tabs.get(state.active_tab) { + let dot = if tab.dirty { "● " } else { "" }; + let name = tab.tab_display_name(); + format!("{}Open CAD Studio — {}", dot, name) + } else { + "Open CAD Studio".to_string() + } + }) + .theme(|state: &OpenCADStudio, _| state.active_theme.clone()) + .run() } diff --git a/src/app/style_ops.rs b/src/app/style_ops.rs new file mode 100644 index 000000000..55cc285bd --- /dev/null +++ b/src/app/style_ops.rs @@ -0,0 +1,475 @@ +//! Shared CRUD layer for every style manager (text / dimension / table / +//! multileader / multiline). +//! +//! The five managers all expose the same list operations — New, Copy, Delete, +//! Rename, Set-Current — over a named collection of styles. Only the *property +//! editor* and the *storage backend* differ, so those are the only parts kept +//! per-manager: +//! +//! * **Table-backed** (text, dim): live in `Table`, keyed by upper-cased +//! name. Renaming must re-key the entry and rewrite name-based entity +//! references (TEXT/MTEXT `style`, DIMENSION `style_name`). +//! * **Object-backed** (table, multileader, multiline): live in +//! `document.objects`, keyed by handle. Renaming only mutates the `name` +//! field; entities reference these by handle, so nothing else moves. +//! +//! Centralising the flow here is what fixes the bug class that kept recurring +//! when each manager was hand-copied: a dead New, a missing ribbon refresh, a +//! style added without a handle (dropped on DWG save, issue #67). + +use super::OpenCADStudio; +use acadrust::objects::{MLineStyle, MultiLeaderStyle, ObjectType, TableStyle}; +use acadrust::tables::{DimStyle, TextStyle}; +use acadrust::types::Handle; + +/// Which style manager an operation targets. Carried by the shared rename +/// messages so one handler can dispatch to the right storage. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StyleKind { + Text, + Dim, + Table, + MLeader, + MLine, +} + +impl StyleKind { + /// True when this style feeds the ribbon's quick-set dropdown. + fn in_ribbon(self) -> bool { + !matches!(self, StyleKind::MLine) + } +} + +impl OpenCADStudio { + // ── Queries ──────────────────────────────────────────────────────────── + + /// All style names for `kind`, in display order (object-backed styles are + /// sorted by name so the `HashMap` backing them renders stably). + pub(super) fn style_names(&self, kind: StyleKind) -> Vec { + let doc = &self.tabs[self.active_tab].scene.document; + let mut from_objects = |pick: fn(&ObjectType) -> Option<&str>| -> Vec { + let mut v: Vec = doc + .objects + .values() + .filter_map(pick) + .map(str::to_string) + .collect(); + v.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())); + v + }; + match kind { + StyleKind::Text => doc.text_styles.iter().map(|s| s.name.clone()).collect(), + StyleKind::Dim => doc.dim_styles.iter().map(|s| s.name.clone()).collect(), + StyleKind::Table => from_objects(|o| match o { + ObjectType::TableStyle(s) => Some(s.name.as_str()), + _ => None, + }), + StyleKind::MLeader => from_objects(|o| match o { + ObjectType::MultiLeaderStyle(s) => Some(s.name.as_str()), + _ => None, + }), + StyleKind::MLine => from_objects(|o| match o { + ObjectType::MLineStyle(s) => Some(s.name.as_str()), + _ => None, + }), + } + } + + pub(super) fn style_selected(&self, kind: StyleKind) -> String { + match kind { + StyleKind::Text => self.textstyle_selected.clone(), + StyleKind::Dim => self.dimstyle_selected.clone(), + StyleKind::Table => self.tablestyle_selected.clone(), + StyleKind::MLeader => self.mleaderstyle_selected.clone(), + StyleKind::MLine => self.mlstyle_selected.clone(), + } + } + + fn set_style_selected(&mut self, kind: StyleKind, name: String) { + match kind { + StyleKind::Text => self.textstyle_selected = name, + StyleKind::Dim => self.dimstyle_selected = name, + StyleKind::Table => self.tablestyle_selected = name, + StyleKind::MLeader => self.mleaderstyle_selected = name, + StyleKind::MLine => self.mlstyle_selected = name, + } + } + + fn style_exists(&self, kind: StyleKind, name: &str) -> bool { + self.style_names(kind) + .iter() + .any(|n| n.eq_ignore_ascii_case(name)) + } + + /// First free `Style{n}` name for a fresh style. + fn unique_new_name(&self, kind: StyleKind) -> String { + (1u32..) + .map(|n| format!("Style{n}")) + .find(|c| !self.style_exists(kind, c)) + .unwrap() + } + + /// First free `{base} ({n})` name for a copy / disambiguated entry. + fn unique_suffixed_name(&self, kind: StyleKind, base: &str) -> String { + (1u32..) + .map(|n| format!("{base} ({n})")) + .find(|c| !self.style_exists(kind, c)) + .unwrap() + } + + // ── Per-manager glue (the only kind-specific list code) ──────────────── + + /// Reload the property-editor buffers for the kinds that have them. + fn load_style_bufs(&mut self, kind: StyleKind) { + let i = self.active_tab; + match kind { + StyleKind::Text => self.load_textstyle_bufs(i), + StyleKind::Dim => self.load_dimstyle_bufs(i), + StyleKind::MLeader => self.load_mleaderstyle_bufs(i), + StyleKind::Table | StyleKind::MLine => {} + } + } + + /// Refresh anything that mirrors the style list / current style after a + /// mutation (ribbon dropdowns, geometry that depends on the style). + fn after_style_change(&mut self, kind: StyleKind) { + if kind.in_ribbon() { + self.sync_ribbon_styles(); + } + } + + fn insert_default_style(&mut self, kind: StyleKind, name: &str, handle: Handle) { + let doc = &mut self.tabs[self.active_tab].scene.document; + match kind { + StyleKind::Text => { + let mut s = TextStyle::new(name); + s.handle = handle; + let _ = doc.text_styles.add(s); + } + StyleKind::Dim => { + let mut s = DimStyle::new(name); + s.handle = handle; + let _ = doc.dim_styles.add(s); + } + StyleKind::Table => { + let mut s = TableStyle::standard(); + s.name = name.to_string(); + s.handle = handle; + doc.objects.insert(handle, ObjectType::TableStyle(s)); + } + StyleKind::MLeader => { + let mut s = MultiLeaderStyle::new(name); + s.handle = handle; + doc.objects.insert(handle, ObjectType::MultiLeaderStyle(s)); + } + StyleKind::MLine => { + let mut s = MLineStyle::standard(); + s.name = name.to_string(); + s.handle = handle; + doc.objects.insert(handle, ObjectType::MLineStyle(s)); + } + } + } + + /// Clone the style named `src` under `name` with a fresh `handle`. + /// Returns false if `src` no longer exists. + fn clone_style_as(&mut self, kind: StyleKind, src: &str, name: &str, handle: Handle) -> bool { + let doc = &mut self.tabs[self.active_tab].scene.document; + match kind { + StyleKind::Text => { + if let Some(mut s) = doc.text_styles.get(src).cloned() { + s.name = name.to_string(); + s.handle = handle; + let _ = doc.text_styles.add(s); + return true; + } + } + StyleKind::Dim => { + if let Some(mut s) = doc.dim_styles.get(src).cloned() { + s.name = name.to_string(); + s.handle = handle; + let _ = doc.dim_styles.add(s); + return true; + } + } + StyleKind::Table => { + if let Some(mut s) = find_object_style(doc, src, |o| match o { + ObjectType::TableStyle(s) => Some((s.name.as_str(), s.clone())), + _ => None, + }) { + s.name = name.to_string(); + s.handle = handle; + doc.objects.insert(handle, ObjectType::TableStyle(s)); + return true; + } + } + StyleKind::MLeader => { + if let Some(mut s) = find_object_style(doc, src, |o| match o { + ObjectType::MultiLeaderStyle(s) => Some((s.name.as_str(), s.clone())), + _ => None, + }) { + s.name = name.to_string(); + s.handle = handle; + doc.objects.insert(handle, ObjectType::MultiLeaderStyle(s)); + return true; + } + } + StyleKind::MLine => { + if let Some(mut s) = find_object_style(doc, src, |o| match o { + ObjectType::MLineStyle(s) => Some((s.name.as_str(), s.clone())), + _ => None, + }) { + s.name = name.to_string(); + s.handle = handle; + doc.objects.insert(handle, ObjectType::MLineStyle(s)); + return true; + } + } + } + false + } + + fn remove_style_storage(&mut self, kind: StyleKind, name: &str) -> bool { + let doc = &mut self.tabs[self.active_tab].scene.document; + match kind { + StyleKind::Text => doc.text_styles.remove(name).is_some(), + StyleKind::Dim => doc.dim_styles.remove(name).is_some(), + StyleKind::Table | StyleKind::MLeader | StyleKind::MLine => { + let kind2 = kind; + if let Some(h) = object_handle(doc, name, kind2) { + doc.objects.remove(&h).is_some() + } else { + false + } + } + } + } + + /// Rename `old`→`new` in the backing store, re-keying table entries and + /// rewriting name-based references + current-style pointers. + fn rename_style_storage(&mut self, kind: StyleKind, old: &str, new: &str) { + let i = self.active_tab; + match kind { + StyleKind::Text => { + let doc = &mut self.tabs[i].scene.document; + if let Some(mut s) = doc.text_styles.get(old).cloned() { + s.name = new.to_string(); + if !s.handle.is_valid() { + s.handle = doc.allocate_handle(); + } + let _ = doc.text_styles.add(s); + } + doc.text_styles.remove(old); + if doc.header.current_text_style_name.eq_ignore_ascii_case(old) { + doc.header.current_text_style_name = new.to_string(); + } + for e in doc.entities_mut() { + match e { + acadrust::entities::EntityType::Text(t) + if t.style.eq_ignore_ascii_case(old) => + { + t.style = new.to_string(); + } + acadrust::entities::EntityType::MText(t) + if t.style.eq_ignore_ascii_case(old) => + { + t.style = new.to_string(); + } + _ => {} + } + } + } + StyleKind::Dim => { + let doc = &mut self.tabs[i].scene.document; + if let Some(mut s) = doc.dim_styles.get(old).cloned() { + s.name = new.to_string(); + if !s.handle.is_valid() { + s.handle = doc.allocate_handle(); + } + let _ = doc.dim_styles.add(s); + } + doc.dim_styles.remove(old); + if doc.header.current_dimstyle_name.eq_ignore_ascii_case(old) { + doc.header.current_dimstyle_name = new.to_string(); + } + for e in doc.entities_mut() { + if let acadrust::entities::EntityType::Dimension(d) = e { + if d.base().style_name.eq_ignore_ascii_case(old) { + d.base_mut().style_name = new.to_string(); + } + } + } + } + StyleKind::Table => { + let doc = &mut self.tabs[i].scene.document; + if let Some(h) = object_handle(doc, old, kind) { + if let Some(ObjectType::TableStyle(s)) = doc.objects.get_mut(&h) { + s.name = new.to_string(); + } + } + if self.ribbon.active_table_style.eq_ignore_ascii_case(old) { + self.ribbon.active_table_style = new.to_string(); + } + } + StyleKind::MLeader => { + let doc = &mut self.tabs[i].scene.document; + if let Some(h) = object_handle(doc, old, kind) { + if let Some(ObjectType::MultiLeaderStyle(s)) = doc.objects.get_mut(&h) { + s.name = new.to_string(); + } + } + if self.tabs[i].active_mleader_style.eq_ignore_ascii_case(old) { + self.tabs[i].active_mleader_style = new.to_string(); + } + if self.ribbon.active_mleader_style.eq_ignore_ascii_case(old) { + self.ribbon.active_mleader_style = new.to_string(); + } + } + StyleKind::MLine => { + let doc = &mut self.tabs[i].scene.document; + if let Some(h) = object_handle(doc, old, kind) { + if let Some(ObjectType::MLineStyle(s)) = doc.objects.get_mut(&h) { + s.name = new.to_string(); + } + } + if doc.header.multiline_style.eq_ignore_ascii_case(old) { + doc.header.multiline_style = new.to_string(); + } + } + } + } + + // ── Public operations (called by the message handlers) ───────────────── + + pub(super) fn style_new(&mut self, kind: StyleKind) { + let i = self.active_tab; + let name = self.unique_new_name(kind); + self.push_undo_snapshot(i, "STYLE NEW"); + let h = self.tabs[i].scene.document.allocate_handle(); + self.insert_default_style(kind, &name, h); + self.set_style_selected(kind, name.clone()); + self.load_style_bufs(kind); + self.tabs[i].dirty = true; + self.after_style_change(kind); + self.command_line + .push_output(&format!("Style '{name}' created.")); + } + + pub(super) fn style_copy(&mut self, kind: StyleKind) { + let i = self.active_tab; + let src = self.style_selected(kind); + let name = self.unique_suffixed_name(kind, &src); + self.push_undo_snapshot(i, "STYLE COPY"); + let h = self.tabs[i].scene.document.allocate_handle(); + if !self.clone_style_as(kind, &src, &name, h) { + return; + } + self.set_style_selected(kind, name.clone()); + self.load_style_bufs(kind); + self.tabs[i].dirty = true; + self.after_style_change(kind); + self.command_line + .push_output(&format!("Style '{name}' created.")); + } + + pub(super) fn style_delete(&mut self, kind: StyleKind) { + let i = self.active_tab; + let name = self.style_selected(kind); + if name.eq_ignore_ascii_case("Standard") { + self.command_line + .push_error("Cannot delete the Standard style."); + return; + } + self.push_undo_snapshot(i, "STYLE DEL"); + if !self.remove_style_storage(kind, &name) { + return; + } + let first = self + .style_names(kind) + .into_iter() + .next() + .unwrap_or_else(|| "Standard".to_string()); + self.set_style_selected(kind, first); + self.load_style_bufs(kind); + self.tabs[i].dirty = true; + self.after_style_change(kind); + self.command_line + .push_output(&format!("Style '{name}' deleted.")); + } + + /// Begin inline rename of the double-clicked style. + pub(super) fn style_rename_start(&mut self, kind: StyleKind, name: String) { + self.set_style_selected(kind, name.clone()); + self.load_style_bufs(kind); + self.style_rename_buf = name.clone(); + self.style_rename = Some(name); + } + + /// Commit the inline rename. No-op (with feedback) on empty / unchanged / + /// colliding names, and the Standard style cannot be renamed. + pub(super) fn style_rename_commit(&mut self, kind: StyleKind) { + let i = self.active_tab; + let Some(old) = self.style_rename.take() else { + return; + }; + let new = self.style_rename_buf.trim().to_string(); + self.style_rename_buf.clear(); + if new.is_empty() || new.eq_ignore_ascii_case(&old) { + return; + } + if old.eq_ignore_ascii_case("Standard") { + self.command_line + .push_error("Cannot rename the Standard style."); + return; + } + if self.style_exists(kind, &new) { + self.command_line + .push_error(&format!("Style '{new}' already exists.")); + return; + } + self.push_undo_snapshot(i, "STYLE RENAME"); + self.rename_style_storage(kind, &old, &new); + if self.style_selected(kind).eq_ignore_ascii_case(&old) { + self.set_style_selected(kind, new.clone()); + } + self.load_style_bufs(kind); + self.tabs[i].dirty = true; + self.after_style_change(kind); + self.command_line + .push_output(&format!("Renamed '{old}' → '{new}'.")); + } + + pub(super) fn style_rename_cancel(&mut self) { + self.style_rename = None; + self.style_rename_buf.clear(); + } +} + +// ── Object-store helpers ─────────────────────────────────────────────────── + +/// Find the object-backed style named `name` and return a clone. `pick` maps a +/// matching variant to `(its name, a clone of the inner style)`. +fn find_object_style( + doc: &acadrust::CadDocument, + name: &str, + pick: impl Fn(&ObjectType) -> Option<(&str, T)>, +) -> Option { + doc.objects.values().find_map(|o| { + let (n, val) = pick(o)?; + n.eq_ignore_ascii_case(name).then_some(val) + }) +} + +fn object_handle(doc: &acadrust::CadDocument, name: &str, kind: StyleKind) -> Option { + doc.objects.iter().find_map(|(&h, o)| { + let matches = match (kind, o) { + (StyleKind::Table, ObjectType::TableStyle(s)) => s.name.eq_ignore_ascii_case(name), + (StyleKind::MLeader, ObjectType::MultiLeaderStyle(s)) => { + s.name.eq_ignore_ascii_case(name) + } + (StyleKind::MLine, ObjectType::MLineStyle(s)) => s.name.eq_ignore_ascii_case(name), + _ => false, + }; + matches.then_some(h) + }) +} diff --git a/src/app/update.rs b/src/app/update.rs index f167bbbe6..c818abc35 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -161,8 +161,9 @@ impl OpenCADStudio { .file_name() .map(|n| n.to_string_lossy().into_owned()) .unwrap_or_else(|| "unknown".into()); - let phase = - std::sync::Arc::new(std::sync::atomic::AtomicU8::new(super::OPEN_PHASE_READING)); + let phase = std::sync::Arc::new(std::sync::atomic::AtomicU8::new( + super::OPEN_PHASE_READING, + )); self.opening = Some(super::OpenProgress { name: name.clone(), size_bytes, @@ -229,8 +230,11 @@ impl OpenCADStudio { // CANNOSCALEVALUE (paper/drawing factor); the multiplier we use // for text/dim sizing is its inverse (1:50 -> 0.02 -> 50.0). let cannoscale_value = self.tabs[i].scene.document.header.annotation_scale_value; - self.tabs[i].scene.annotation_scale = - if cannoscale_value > 1e-9 { (1.0 / cannoscale_value) as f32 } else { 1.0 }; + self.tabs[i].scene.annotation_scale = if cannoscale_value > 1e-9 { + (1.0 / cannoscale_value) as f32 + } else { + 1.0 + }; // Auto-resolve XREFs relative to the opened file's directory. let mut xref_ms = 0u32; @@ -931,8 +935,7 @@ impl OpenCADStudio { // fresh blank drawing (legacy behaviour). if self.tabs.len() == 1 { self.tab_counter += 1; - self.tabs[0] = - super::document::DocumentTab::new_drawing(self.tab_counter); + self.tabs[0] = super::document::DocumentTab::new_drawing(self.tab_counter); self.active_tab = 0; } else { self.tabs.remove(idx); @@ -955,8 +958,7 @@ impl OpenCADStudio { // prompts) where Space must reach the buffer as a literal // character. `wants_text_with_spaces()` flags those prompts. let i = self.active_tab; - let allow_literal_space = self - .tabs[i] + let allow_literal_space = self.tabs[i] .active_cmd .as_ref() .map(|c| c.wants_text_input() && c.wants_text_with_spaces()) @@ -990,10 +992,7 @@ impl OpenCADStudio { // reshaping the field set when going polar → cartesian // (Distance → X, Y) or 2-D → 3-D (X, Y → X, Y, Z). // See #35. - if s == "," - && self.dyn_input - && !self.tabs[i].dyn_fields.is_empty() - { + if s == "," && self.dyn_input && !self.tabs[i].dyn_fields.is_empty() { self.dyn_comma_advance(); self.command_line.autocomplete_cursor = None; return self.focus_cmd_input(); @@ -1003,11 +1002,14 @@ impl OpenCADStudio { // the command line. Letters still go to the command line // so command-option keywords keep working. let dyn_field_char = !s.is_empty() - && s.chars() - .all(|c| c.is_ascii_digit() - || matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')')); + && s.chars().all(|c| { + c.is_ascii_digit() + || matches!(c, '.' | '-' | '+' | '*' | '/' | '^' | '%' | '(' | ')') + }); if dyn_field_char && self.dyn_input && !self.tabs[i].dyn_fields.is_empty() { - let a = self.tabs[i].dyn_active.min(self.tabs[i].dyn_fields.len() - 1); + let a = self.tabs[i] + .dyn_active + .min(self.tabs[i].dyn_fields.len() - 1); self.tabs[i].dyn_fields[a] .buffer .get_or_insert_with(String::new) @@ -1029,7 +1031,9 @@ impl OpenCADStudio { // Backspace edits the focused dynamic-input field first; // emptying it unlocks the field (back to cursor tracking). if self.dyn_input && !self.tabs[i].dyn_fields.is_empty() { - let a = self.tabs[i].dyn_active.min(self.tabs[i].dyn_fields.len() - 1); + let a = self.tabs[i] + .dyn_active + .min(self.tabs[i].dyn_fields.len() - 1); if let Some(buf) = self.tabs[i].dyn_fields[a].buffer.as_mut() { buf.pop(); if buf.is_empty() { @@ -1083,9 +1087,7 @@ impl OpenCADStudio { // While autocomplete is showing suggestions, ↑ walks up // that list. Otherwise it falls back to recall history. let i = self.active_tab; - if self.tabs[i].active_cmd.is_none() - && self.command_line.autocomplete_prev() - { + if self.tabs[i].active_cmd.is_none() && self.command_line.autocomplete_prev() { return Task::none(); } self.command_line.history_prev(); @@ -1100,9 +1102,7 @@ impl OpenCADStudio { return Task::none(); } let i = self.active_tab; - if self.tabs[i].active_cmd.is_none() - && self.command_line.autocomplete_next() - { + if self.tabs[i].active_cmd.is_none() && self.command_line.autocomplete_next() { return Task::none(); } self.command_line.history_next(); @@ -1140,17 +1140,9 @@ impl OpenCADStudio { let i = self.active_tab; use crate::entities::traits::EntityTypeOps; self.push_undo_snapshot(i, pending.label); - if let Some(entity) = self - .tabs[i] - .scene - .document - .get_entity_mut(pending.handle) + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(pending.handle) { - entity.apply_grip_menu_value( - pending.grip_id, - pending.action, - v, - ); + entity.apply_grip_menu_value(pending.grip_id, pending.action, v); } self.tabs[i].scene.bump_geometry(); self.tabs[i].dirty = true; @@ -1164,7 +1156,11 @@ impl OpenCADStudio { self.awaiting_vports = false; let cfg = self.command_line.input.trim().to_string(); self.command_line.input.clear(); - let cfg = if cfg.is_empty() { "SINGLE".to_string() } else { cfg }; + let cfg = if cfg.is_empty() { + "SINGLE".to_string() + } else { + cfg + }; return self.dispatch_command(&format!("VPORTS {cfg}")); } // If the user navigated the autocomplete list with the @@ -1182,9 +1178,7 @@ impl OpenCADStudio { let i = self.active_tab; // With the command line empty, a typed dynamic-input value // commits as a point pick instead of an empty submit. - if self.tabs[i].active_cmd.is_some() - && self.command_line.input.trim().is_empty() - { + if self.tabs[i].active_cmd.is_some() && self.command_line.input.trim().is_empty() { if let Some(task) = self.try_dyn_commit() { return task; } @@ -1342,11 +1336,7 @@ impl OpenCADStudio { } // Grip popup open → Enter commits the highlighted item. if self.grip_popup.is_some() { - let idx = self - .grip_popup - .as_ref() - .map(|p| p.selected) - .unwrap_or(0); + let idx = self.grip_popup.as_ref().map(|p| p.selected).unwrap_or(0); return Task::done(Message::GripMenuPick(idx)); } // A typed dynamic-input value commits as a point pick @@ -1942,8 +1932,7 @@ impl OpenCADStudio { // viewport's screen rectangle in a paper layout. let i = self.active_tab; let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; - let (ox, oy) = match self - .tabs[i] + let (ox, oy) = match self.tabs[i] .scene .active_viewport .and_then(|h| self.tabs[i].scene.viewport_screen_rect(h, (vw, vh))) @@ -1971,8 +1960,7 @@ impl OpenCADStudio { // above the shader and can mask it). Map the cursor into // the active viewport's local box and use that box's size, // since that's where the cube is actually drawn. - let tile = match self - .tabs[i] + let tile = match self.tabs[i] .scene .active_viewport .and_then(|h| self.tabs[i].scene.viewport_screen_rect(h, (vw, vh))) @@ -2029,8 +2017,7 @@ impl OpenCADStudio { // `hover_id` returns None outside the cube box, which clears // any stale highlight from the previous `CursorMoved`. let (svw, svh) = self.tabs[i].scene.selection.borrow().vp_size; - let cube_tile = match self - .tabs[i] + let cube_tile = match self.tabs[i] .scene .active_viewport .and_then(|h| self.tabs[i].scene.viewport_screen_rect(h, (svw, svh))) @@ -2140,8 +2127,7 @@ impl OpenCADStudio { // Pan scale uses the active tile's size (ortho size // is relative to viewport height), so a tiled pane // pans at the correct rate. - let bounds = self - .tabs[i] + let bounds = self.tabs[i] .scene .active_model_tile_bounds(vp_size.0, vp_size.1); // Drop `sel` before calling mutable scene methods. @@ -2155,11 +2141,11 @@ impl OpenCADStudio { } else { // `bounds` is the active tile; pan by its height so // the point under the cursor tracks correctly. - self.tabs[i] - .scene - .camera - .borrow_mut() - .pan_screen(dx, dy, bounds.height); + self.tabs[i].scene.camera.borrow_mut().pan_screen( + dx, + dy, + bounds.height, + ); self.tabs[i].scene.camera_generation += 1; } self.tabs[i].scene.selection.borrow_mut().middle_last_pos = Some(p); @@ -2177,8 +2163,7 @@ impl OpenCADStudio { // follow the pane the cursor is in. During a drag the active // tile stays put so the operation finishes in its own pane. if !dragging && vp_size.0 > 1.0 && vp_size.1 > 1.0 { - if self - .tabs[i] + if self.tabs[i] .scene .set_active_model_tile_at(p.x / vp_size.0, p.y / vp_size.1) { @@ -2193,8 +2178,7 @@ impl OpenCADStudio { // the active pane. `p_full` keeps the canvas-space cursor // for screen overlays (cursor marker, snap glyph). let p_full = p; - let tile_b = self - .tabs[i] + let tile_b = self.tabs[i] .scene .active_model_tile_bounds(vp_size.0, vp_size.1); let p = iced::Point { @@ -2429,7 +2413,8 @@ impl OpenCADStudio { } else { None }; - self.snapper.otrack_snap(cursor_world, view_proj, bounds, step) + self.snapper + .otrack_snap(cursor_world, view_proj, bounds, step) } else { None } @@ -2485,8 +2470,7 @@ impl OpenCADStudio { ) }); if let Some(pick) = pick { - let world = - glam::Vec3::new(pick.x as f32, pick.y as f32, effective.z); + let world = glam::Vec3::new(pick.x as f32, pick.y as f32, effective.z); let ndc = view_proj.project_point3(world); let screen = iced::Point::new( (ndc.x + 1.0) * 0.5 * bounds.width, @@ -2541,9 +2525,7 @@ impl OpenCADStudio { )); } if let Some(cmd) = self.tabs[i].active_cmd.as_mut() { - if let Some(hint) = - cmd.entity_pick_acquire_hint(hover_handle) - { + if let Some(hint) = cmd.entity_pick_acquire_hint(hover_handle) { cmd.set_acquisition_hint(Some(hint)); } } @@ -2567,8 +2549,8 @@ impl OpenCADStudio { // floating free near no angle (issue #70). let step = self.polar_increment_deg.to_radians(); let angle = dy.atan2(dx); - let snapped = step > 1e-6 - && ((angle / step).round() * step - angle).abs() < 1e-3; + let snapped = + step > 1e-6 && ((angle / step).round() * step - angle).abs() < 1e-3; if snapped && (dx * dx + dy * dy).sqrt() > 1e-4 { let far = 1e5_f32; let dir = glam::Vec3::new(dx, dy, 0.0).normalize(); @@ -2725,9 +2707,10 @@ impl OpenCADStudio { width: vw, height: vh, }; - if let Some(edge) = self.tabs[i] - .scene - .hit_model_tile_edge(p, canvas_bounds, TILE_EDGE_HIT_PX) + if let Some(edge) = + self.tabs[i] + .scene + .hit_model_tile_edge(p, canvas_bounds, TILE_EDGE_HIT_PX) { self.tile_drag = Some(crate::app::TileDrag { orient: edge.orient, @@ -2740,8 +2723,7 @@ impl OpenCADStudio { // Tiled Model layout: clicking a non-active tile activates // it (swapping in its camera) instead of selecting / drawing. if self.tabs[i].active_cmd.is_none() && vw > 1.0 && vh > 1.0 { - if self - .tabs[i] + if self.tabs[i] .scene .set_active_model_tile_at(p.x / vw, p.y / vh) { @@ -2832,9 +2814,7 @@ impl OpenCADStudio { if self.tile_drag.take().is_some() { let (vw, vh) = self.tabs[i].scene.selection.borrow().vp_size; let (min_w, min_h) = tile_min_norm(vw, vh); - self.tabs[i] - .scene - .collapse_small_model_tiles(min_w, min_h); + self.tabs[i].scene.collapse_small_model_tiles(min_w, min_h); self.tabs[i].scene.camera_generation += 1; return Task::none(); } @@ -3024,9 +3004,10 @@ impl OpenCADStudio { }); if let Some(pick) = pick { let center = glam::Vec3::new(pick.x as f32, pick.y as f32, world_pt.z); - let result = self.tabs[i].active_cmd.as_mut().map(|c| { - c.on_structure_pick(pick.handle, center) - }); + let result = self.tabs[i] + .active_cmd + .as_mut() + .map(|c| c.on_structure_pick(pick.handle, center)); self.command_line .push_info(&format!("{} acquired.", pick.label)); result @@ -3342,58 +3323,59 @@ impl OpenCADStudio { } if !handled_by_cycling { - let hit = scene::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds) - .and_then(|s| Scene::handle_from_wire_name(s)) - .or_else(|| { - scene::hit_test::click_hit_hatch( - p, - &self.tabs[i].scene.visible_hatches_for_click(), - vp_mat, - bounds, - ) - }) - .or_else(|| { - // Block-internal hatch: resolve to the - // parent Insert (AutoCAD behaviour). - scene::hit_test::click_hit_insert_hatch( - p, - &self.tabs[i].scene.insert_hatches_for_click(), - vp_mat, - bounds, - ) - }) - .or_else(|| { - // 3D solids: click anywhere on the shaded - // body, not just the thin projected edges. - self.tabs[i].scene.mesh_click_hit(p, vp_mat, bounds) - }); - // Selection filter: drop a pick whose type is excluded. - let hit = - hit.filter(|&h| self.tabs[i].scene.passes_selection_filter(h)); - if let Some(handle) = hit { - // Individual picks accumulate (issue #47): - // each plain click adds to the selection, - // Shift+click removes the picked entity. - // Esc / empty-space click clears. - if self.shift_down { - self.tabs[i].scene.deselect_entity(handle); + let hit = + scene::hit_test::click_hit(p, &all_wires[..], vp_mat, bounds) + .and_then(|s| Scene::handle_from_wire_name(s)) + .or_else(|| { + scene::hit_test::click_hit_hatch( + p, + &self.tabs[i].scene.visible_hatches_for_click(), + vp_mat, + bounds, + ) + }) + .or_else(|| { + // Block-internal hatch: resolve to the + // parent Insert (AutoCAD behaviour). + scene::hit_test::click_hit_insert_hatch( + p, + &self.tabs[i].scene.insert_hatches_for_click(), + vp_mat, + bounds, + ) + }) + .or_else(|| { + // 3D solids: click anywhere on the shaded + // body, not just the thin projected edges. + self.tabs[i].scene.mesh_click_hit(p, vp_mat, bounds) + }); + // Selection filter: drop a pick whose type is excluded. + let hit = + hit.filter(|&h| self.tabs[i].scene.passes_selection_filter(h)); + if let Some(handle) = hit { + // Individual picks accumulate (issue #47): + // each plain click adds to the selection, + // Shift+click removes the picked entity. + // Esc / empty-space click clears. + if self.shift_down { + self.tabs[i].scene.deselect_entity(handle); + } else { + self.tabs[i].scene.select_entity(handle, false); + self.tabs[i].scene.expand_selection_for_groups(&[handle]); + } + self.refresh_properties(); + selection_just_completed = true; } else { - self.tabs[i].scene.select_entity(handle, false); - self.tabs[i].scene.expand_selection_for_groups(&[handle]); + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + // Full-canvas space: ViewportMove updates + // box_current in canvas coords and the overlay + // draws there; release maps back into the tile. + sel.box_anchor = Some(p_full); + sel.box_current = Some(p_full); + sel.box_crossing = false; } - self.refresh_properties(); - selection_just_completed = true; - } else { - self.tabs[i].scene.deselect_all(); - self.refresh_properties(); - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - // Full-canvas space: ViewportMove updates - // box_current in canvas coords and the overlay - // draws there; release maps back into the tile. - sel.box_anchor = Some(p_full); - sel.box_current = Some(p_full); - sel.box_crossing = false; - } } } else { let a = box_anchor.unwrap(); @@ -3493,8 +3475,7 @@ impl OpenCADStudio { // Any text-bearing entity opens its in-place editor // (plain box or rich MText editor, per type). A // Leader resolves to the entity it annotates. - let is_editable_text = self - .tabs[i] + let is_editable_text = self.tabs[i] .scene .document .get_entity(handle) @@ -3718,8 +3699,7 @@ impl OpenCADStudio { // viewport's screen rectangle in a paper layout. Map the // cursor into that area before hit-testing so paper-space // picks line up with the gizmo. - let (cx, cy, w, h) = match self - .tabs[i] + let (cx, cy, w, h) = match self.tabs[i] .scene .active_viewport .and_then(|hndl| self.tabs[i].scene.viewport_screen_rect(hndl, (vw, vh))) @@ -3843,8 +3823,7 @@ impl OpenCADStudio { // Actions that need a follow-up number stash a pending // state + prompt; the next typed value drives // `apply_grip_menu_value`. - let prompt = self - .tabs[i] + let prompt = self.tabs[i] .scene .document .get_entity(popup.handle) @@ -3882,12 +3861,7 @@ impl OpenCADStudio { } else { None }; - if let Some(entity) = self - .tabs[i] - .scene - .document - .get_entity_mut(popup.handle) - { + if let Some(entity) = self.tabs[i].scene.document.get_entity_mut(popup.handle) { entity.apply_grip_menu(popup.grip_id, item.action); } self.tabs[i].scene.bump_geometry(); @@ -3897,9 +3871,7 @@ impl OpenCADStudio { // Grab the new arrow so it follows the cursor (click places it, // Esc removes it). if let Some(new_gid) = add_leader_gid { - if let Some(g) = - self.tabs[i].selected_grips.iter().find(|g| g.id == new_gid) - { + if let Some(g) = self.tabs[i].selected_grips.iter().find(|g| g.id == new_gid) { self.tabs[i].active_grip = Some(GripEdit { handle: popup.handle, grip_id: new_gid, @@ -4202,7 +4174,11 @@ impl OpenCADStudio { } Message::MTextFont(f) => { if let Some(ed) = self.mtext_editor.as_mut() { - ed.font = if f == "[Style default]" { String::new() } else { f }; + ed.font = if f == "[Style default]" { + String::new() + } else { + f + }; } self.rebuild_mtext_preview(); Task::none() @@ -4810,9 +4786,9 @@ impl OpenCADStudio { let i = self.active_tab; let handles = self.property_target_handles(i); if !handles.is_empty() { - if let Some(raw_val) = self.tabs[i].properties.edit_buf.remove(field) { - let val = super::expr_eval::eval_to_string(&raw_val); - self.push_undo_snapshot(i, "CHPROP"); + if let Some(raw_val) = self.tabs[i].properties.edit_buf.remove(field) { + let val = super::expr_eval::eval_to_string(&raw_val); + self.push_undo_snapshot(i, "CHPROP"); if field == "frozen_layers" { // Resolve layer names → handles, then apply to viewports. let layer_handles: Vec = val @@ -5659,8 +5635,7 @@ impl OpenCADStudio { } else { // Create a new PlotSettings object and insert it. let mut ps = PlotSettings::new(layout_name.clone()); - ps.handle = - self.tabs[i].scene.document.allocate_handle(); + ps.handle = self.tabs[i].scene.document.allocate_handle(); let h = ps.handle; self.tabs[i] .scene @@ -6174,52 +6149,39 @@ impl OpenCADStudio { self.push_undo_snapshot(i, "STYLE SET"); self.tabs[i].scene.document.header.current_text_style_name = name.clone(); self.tabs[i].dirty = true; + self.sync_ribbon_styles(); self.command_line .push_output(&format!("Current text style: {}", name)); } Task::none() } Message::TextStyleDialogNew => { - let i = self.active_tab; - let doc = &self.tabs[i].scene.document; - let mut n = 1u32; - let new_name = loop { - let candidate = format!("Style{}", n); - if !doc.text_styles.contains(&candidate) { - break candidate; - } - n += 1; - }; - self.push_undo_snapshot(i, "STYLE NEW"); - let style = acadrust::tables::TextStyle::new(&new_name); - let _ = self.tabs[i].scene.document.text_styles.add(style); - self.textstyle_selected = new_name.clone(); - self.textstyle_font = String::new(); - self.textstyle_width = "1.0".to_string(); - self.textstyle_oblique = "0.0".to_string(); - self.tabs[i].dirty = true; + self.style_new(crate::app::StyleKind::Text); + Task::none() + } + Message::TextStyleDialogCopy => { + self.style_copy(crate::app::StyleKind::Text); Task::none() } Message::TextStyleDialogDelete => { - let i = self.active_tab; - let name = self.textstyle_selected.clone(); - if name.eq_ignore_ascii_case("Standard") { - self.command_line - .push_error("Cannot delete the Standard text style."); - return Task::none(); - } - self.push_undo_snapshot(i, "STYLE DEL"); - self.tabs[i].scene.document.text_styles.remove(&name); - self.textstyle_selected = self.tabs[i] - .scene - .document - .text_styles - .iter() - .next() - .map(|s| s.name.clone()) - .unwrap_or_else(|| "Standard".to_string()); - self.load_textstyle_bufs(i); - self.tabs[i].dirty = true; + self.style_delete(crate::app::StyleKind::Text); + Task::none() + } + // ── Shared inline rename (all style managers) ───────────────── + Message::StyleRenameStart(kind, name) => { + self.style_rename_start(kind, name); + Task::none() + } + Message::StyleRenameEdit(s) => { + self.style_rename_buf = s; + Task::none() + } + Message::StyleRenameCommit(kind) => { + self.style_rename_commit(kind); + Task::none() + } + Message::StyleRenameCancel => { + self.style_rename_cancel(); Task::none() } Message::TextStyleEdit { field, value } => { @@ -6432,7 +6394,8 @@ impl OpenCADStudio { use acadrust::objects::TableBorderType; let i = self.active_tab; if let Some(s) = self.tablestyle_mut(i) { - if let Some(bd) = Self::ts_cell_of(s, cell).and_then(|c| Self::ts_border_of(c, border)) + if let Some(bd) = + Self::ts_cell_of(s, cell).and_then(|c| Self::ts_border_of(c, border)) { bd.border_type = match value.as_str() { "Double" => TableBorderType::Double, @@ -6449,7 +6412,8 @@ impl OpenCADStudio { Message::TableStyleBorderToggleInvisible { cell, border } => { let i = self.active_tab; if let Some(s) = self.tablestyle_mut(i) { - if let Some(bd) = Self::ts_cell_of(s, cell).and_then(|c| Self::ts_border_of(c, border)) + if let Some(bd) = + Self::ts_cell_of(s, cell).and_then(|c| Self::ts_border_of(c, border)) { bd.is_invisible = !bd.is_invisible; } @@ -6519,7 +6483,10 @@ impl OpenCADStudio { self.ts_border_spacing[r][b].trim().parse().ok(), ) }); - if let Some(c) = self.tablestyle_mut(i).and_then(|s| Self::ts_cell_of(s, row)) { + if let Some(c) = self + .tablestyle_mut(i) + .and_then(|s| Self::ts_cell_of(s, row)) + { if !ts.is_empty() { c.text_style_name = ts; } @@ -6596,76 +6563,26 @@ impl OpenCADStudio { } Message::TableStyleDialogNew => { - use acadrust::objects::ObjectType; - let i = self.active_tab; - let doc = &self.tabs[i].scene.document; - let mut n = 1u32; - let new_name = loop { - let candidate = format!("TS{}", n); - let taken = doc.objects.values().any(|o| { - matches!(o, ObjectType::TableStyle(s) if s.name.eq_ignore_ascii_case(&candidate)) - }); - if !taken { - break candidate; - } - n += 1; - }; - self.push_undo_snapshot(i, "TABLESTYLE NEW"); - let mut style = acadrust::objects::TableStyle::standard(); - style.name = new_name.clone(); - let nh = self.tabs[i].scene.document.allocate_handle(); - style.handle = nh; - self.tabs[i] - .scene - .document - .objects - .insert(nh, ObjectType::TableStyle(style)); - self.tablestyle_selected = new_name; - self.tabs[i].dirty = true; + self.style_new(crate::app::StyleKind::Table); + Task::none() + } + Message::TableStyleDialogCopy => { + self.style_copy(crate::app::StyleKind::Table); Task::none() } Message::TableStyleDialogDelete => { - use acadrust::objects::ObjectType; - let i = self.active_tab; + self.style_delete(crate::app::StyleKind::Table); + Task::none() + } + Message::TableStyleDialogSetCurrent => { let name = self.tablestyle_selected.clone(); - if name.eq_ignore_ascii_case("Standard") { + if self + .style_names(crate::app::StyleKind::Table) + .contains(&name) + { + self.ribbon.active_table_style = name.clone(); self.command_line - .push_error("Cannot delete the Standard style."); - return Task::none(); - } - let handle = self.tabs[i] - .scene - .document - .objects - .iter() - .find_map(|(&h, o)| { - if let ObjectType::TableStyle(s) = o { - if s.name == name { - Some(h) - } else { - None - } - } else { - None - } - }); - if let Some(h) = handle { - self.push_undo_snapshot(i, "TABLESTYLE DEL"); - self.tabs[i].scene.document.objects.remove(&h); - self.tablestyle_selected = self.tabs[i] - .scene - .document - .objects - .values() - .find_map(|o| { - if let ObjectType::TableStyle(s) = o { - Some(s.name.clone()) - } else { - None - } - }) - .unwrap_or_else(|| "Standard".to_string()); - self.tabs[i].dirty = true; + .push_output(&format!("Current table style: {name}")); } Task::none() } @@ -6740,81 +6657,21 @@ impl OpenCADStudio { } Task::none() } + // Placeholder so the Multiline manager has the same Set Current + + // Apply pair as every other style manager. The editor is currently + // read-only, so there is nothing to apply yet — wire this up when + // editable MLineStyle properties land. + Message::MlStyleApply => Task::none(), Message::MlStyleDialogNew => { - use acadrust::objects::ObjectType; - let i = self.active_tab; - // Generate a unique name. - let doc = &self.tabs[i].scene.document; - let mut n = 1u32; - let base = "MLS"; - let new_name = loop { - let candidate = format!("{}{}", base, n); - let taken = doc.objects.values().any(|o| { - matches!(o, ObjectType::MLineStyle(s) if s.name.eq_ignore_ascii_case(&candidate)) - }); - if !taken { - break candidate; - } - n += 1; - }; - self.push_undo_snapshot(i, "MLSTYLE NEW"); - let mut style = acadrust::objects::MLineStyle::standard(); - style.name = new_name.clone(); - let nh = self.tabs[i].scene.document.allocate_handle(); - style.handle = nh; - self.tabs[i] - .scene - .document - .objects - .insert(nh, ObjectType::MLineStyle(style)); - self.mlstyle_selected = new_name; - self.tabs[i].dirty = true; + self.style_new(crate::app::StyleKind::MLine); + Task::none() + } + Message::MlStyleDialogCopy => { + self.style_copy(crate::app::StyleKind::MLine); Task::none() } Message::MlStyleDialogDelete => { - use acadrust::objects::ObjectType; - let i = self.active_tab; - let name = self.mlstyle_selected.clone(); - if name.eq_ignore_ascii_case("Standard") { - self.command_line - .push_error("Cannot delete the Standard style."); - return Task::none(); - } - let handle = self.tabs[i] - .scene - .document - .objects - .iter() - .find_map(|(&h, o)| { - if let ObjectType::MLineStyle(s) = o { - if s.name == name { - Some(h) - } else { - None - } - } else { - None - } - }); - if let Some(h) = handle { - self.push_undo_snapshot(i, "MLSTYLE DEL"); - self.tabs[i].scene.document.objects.remove(&h); - // Select first remaining style. - self.mlstyle_selected = self.tabs[i] - .scene - .document - .objects - .values() - .find_map(|o| { - if let ObjectType::MLineStyle(s) = o { - Some(s.name.clone()) - } else { - None - } - }) - .unwrap_or_else(|| "Standard".to_string()); - self.tabs[i].dirty = true; - } + self.style_delete(crate::app::StyleKind::MLine); Task::none() } @@ -6891,71 +6748,15 @@ impl OpenCADStudio { Task::none() } Message::MLeaderStyleDialogNew => { - use acadrust::objects::ObjectType; - let i = self.active_tab; - let doc = &self.tabs[i].scene.document; - let mut n = 1u32; - let new_name = loop { - let candidate = format!("MLeader{}", n); - let taken = doc.objects.values().any(|o| { - matches!(o, ObjectType::MultiLeaderStyle(s) if s.name.eq_ignore_ascii_case(&candidate)) - }); - if !taken { - break candidate; - } - n += 1; - }; - self.push_undo_snapshot(i, "MLEADERSTYLE NEW"); - let mut style = acadrust::objects::MultiLeaderStyle::new(&new_name); - let nh = self.tabs[i].scene.document.allocate_handle(); - style.handle = nh; - self.tabs[i] - .scene - .document - .objects - .insert(nh, ObjectType::MultiLeaderStyle(style)); - self.mleaderstyle_selected = new_name; - self.load_mleaderstyle_bufs(i); - self.tabs[i].dirty = true; + self.style_new(crate::app::StyleKind::MLeader); + Task::none() + } + Message::MLeaderStyleDialogCopy => { + self.style_copy(crate::app::StyleKind::MLeader); Task::none() } Message::MLeaderStyleDialogDelete => { - use acadrust::objects::ObjectType; - let i = self.active_tab; - let name = self.mleaderstyle_selected.clone(); - if name.eq_ignore_ascii_case("Standard") { - self.command_line - .push_error("Cannot delete the Standard style."); - return Task::none(); - } - let handle = self.tabs[i] - .scene - .document - .objects - .iter() - .find_map(|(&h, o)| match o { - ObjectType::MultiLeaderStyle(s) if s.name == name => Some(h), - _ => None, - }); - if let Some(h) = handle { - self.push_undo_snapshot(i, "MLEADERSTYLE DEL"); - self.tabs[i].scene.document.objects.remove(&h); - self.mleaderstyle_selected = self.tabs[i] - .scene - .document - .objects - .values() - .find_map(|o| { - if let ObjectType::MultiLeaderStyle(s) = o { - Some(s.name.clone()) - } else { - None - } - }) - .unwrap_or_else(|| "Standard".to_string()); - self.load_mleaderstyle_bufs(i); - self.tabs[i].dirty = true; - } + self.style_delete(crate::app::StyleKind::MLeader); Task::none() } Message::MLeaderStyleEdit { field, value } => { @@ -7144,20 +6945,7 @@ impl OpenCADStudio { } Message::MLeaderStyleApply => { let i = self.active_tab; - let ( - ld, - lg, - asz, - th, - sf, - bg, - fsa, - ssa, - mp, - dt, - lc, - tc, - ) = ( + let (ld, lg, asz, th, sf, bg, fsa, ssa, mp, dt, lc, tc) = ( self.mls_landing_distance.parse::().ok(), self.mls_landing_gap.parse::().ok(), self.mls_arrowhead_size.parse::().ok(), @@ -7302,11 +7090,11 @@ impl OpenCADStudio { Task::none() } Message::DimStyleDialogNew => { - // Delegate to the DIMSTYLE NEW command via command line prompt. - self.command_line.push_info("Enter new DimStyle name:"); - if let Some(id) = self.dimstyle_window.take() { - return window::close(id); - } + self.style_new(crate::app::StyleKind::Dim); + Task::none() + } + Message::DimStyleDialogCopy => { + self.style_copy(crate::app::StyleKind::Dim); Task::none() } Message::DimStyleDialogSetCurrent => { @@ -7315,6 +7103,7 @@ impl OpenCADStudio { self.tabs[i].scene.document.header.current_dimstyle_name = self.dimstyle_selected.clone(); self.tabs[i].dirty = true; + self.sync_ribbon_styles(); self.command_line.push_output(&format!( "Current dim style set to '{}'.", self.dimstyle_selected @@ -7322,32 +7111,7 @@ impl OpenCADStudio { Task::none() } Message::DimStyleDialogDelete => { - let i = self.active_tab; - let name = self.dimstyle_selected.clone(); - if name == "Standard" { - self.command_line - .push_error("Cannot delete the Standard dim style."); - } else if self.tabs[i] - .scene - .document - .dim_styles - .remove(&name) - .is_some() - { - self.tabs[i].dirty = true; - // Select first remaining style. - self.dimstyle_selected = self.tabs[i] - .scene - .document - .dim_styles - .iter() - .next() - .map(|s| s.name.clone()) - .unwrap_or_else(|| "Standard".to_string()); - self.load_dimstyle_bufs(i); - self.command_line - .push_output(&format!("DimStyle '{}' deleted.", name)); - } + self.style_delete(crate::app::StyleKind::Dim); Task::none() } Message::DsEdit(field, val) => { @@ -7418,8 +7182,7 @@ impl OpenCADStudio { self.tabs[i].dyn_active = 0; return; } - let field = self - .tabs[i] + let field = self.tabs[i] .active_cmd .as_ref() .map(|c| c.dyn_field()) @@ -7430,8 +7193,7 @@ impl OpenCADStudio { // `on_text_input` instead of `on_point`. A distance prompt keeps the // `Distance` box (so a perpendicular-distance live value reads // naturally); everything else uses the typed-only `Scalar` box. - let wants_text = self - .tabs[i] + let wants_text = self.tabs[i] .active_cmd .as_ref() .map(|c| c.wants_text_input()) @@ -7463,8 +7225,11 @@ impl OpenCADStudio { // reshaped via `,` (see #35) the existing set is still a valid // Point configuration and must not be reverted on every mouse // move. - let current: Vec = - self.tabs[i].dyn_fields.iter().map(|f| f.component).collect(); + let current: Vec = self.tabs[i] + .dyn_fields + .iter() + .map(|f| f.component) + .collect(); // Only treat a cartesian / polar variant as "good enough to keep" // when the user explicitly reshaped via `,`. Otherwise we follow // the command's default so e.g. clicking the first point of LINE @@ -7664,7 +7429,7 @@ impl OpenCADStudio { let val = |idx: usize, live: f32| -> f32 { fields[idx] .buffer - .as_ref() + .as_ref() .map(|s| s.trim().replace(',', ".")) .and_then(|s| crate::app::expr_eval::eval_number(&s).map(|v| v as f32)) .unwrap_or(live) @@ -7679,9 +7444,11 @@ impl OpenCADStudio { // position relative to base; typed values are relative deltas. let has_base = self.last_point.is_some(); match comps.as_slice() { - [DynComponent::X, DynComponent::Y] if has_base => { - Some(glam::Vec3::new(base.x + val(0, dx), base.y + val(1, dy), base.z)) - } + [DynComponent::X, DynComponent::Y] if has_base => Some(glam::Vec3::new( + base.x + val(0, dx), + base.y + val(1, dy), + base.z, + )), [DynComponent::X, DynComponent::Y] => { Some(glam::Vec3::new(val(0, w.x), val(1, w.y), base.z)) } @@ -7692,15 +7459,17 @@ impl OpenCADStudio { base.z + val(2, 0.0), )) } - [DynComponent::X, DynComponent::Y, DynComponent::Z] => Some(glam::Vec3::new( - val(0, w.x), - val(1, w.y), - val(2, base.z), - )), + [DynComponent::X, DynComponent::Y, DynComponent::Z] => { + Some(glam::Vec3::new(val(0, w.x), val(1, w.y), val(2, base.z))) + } [DynComponent::Distance, DynComponent::Angle] => { let d = val(0, live_d); let a = val(1, live_a.to_degrees()).to_radians(); - Some(glam::Vec3::new(base.x + d * a.cos(), base.y + d * a.sin(), base.z)) + Some(glam::Vec3::new( + base.x + d * a.cos(), + base.y + d * a.sin(), + base.z, + )) } [DynComponent::Distance] => { // Keep the cursor's direction, override the magnitude. @@ -7735,12 +7504,10 @@ impl OpenCADStudio { // The user picked a shape — `sync_dyn_fields` preserves it until // the next commit / command-start clears the flag. self.dyn_user_reshaped = true; - let active = self - .tabs[i] + let active = self.tabs[i] .dyn_active .min(self.tabs[i].dyn_fields.len() - 1); - let comps: Vec = self - .tabs[i] + let comps: Vec = self.tabs[i] .dyn_fields .iter() .map(|f| f.component) @@ -7749,12 +7516,10 @@ impl OpenCADStudio { match (comps.as_slice(), active) { // First polar field — `,` switches to cartesian, locking the // typed value as X. - ([DynComponent::Distance, DynComponent::Angle], 0) - | ([DynComponent::Distance], 0) => { + ([DynComponent::Distance, DynComponent::Angle], 0) | ([DynComponent::Distance], 0) => { let mut x_field = DynFieldEntry::new(DynComponent::X); x_field.buffer = cur_buf; - self.tabs[i].dyn_fields = - vec![x_field, DynFieldEntry::new(DynComponent::Y)]; + self.tabs[i].dyn_fields = vec![x_field, DynFieldEntry::new(DynComponent::Y)]; self.tabs[i].dyn_active = 1; } // Already cartesian X (first field) — just advance to Y. @@ -7801,7 +7566,11 @@ impl OpenCADStudio { .map(|c| c.wants_text_input()) .unwrap_or(false); if !wants_text { - if let Some(text) = self.tabs[i].dyn_fields.iter().find_map(|f| f.buffer.clone()) { + if let Some(text) = self.tabs[i] + .dyn_fields + .iter() + .find_map(|f| f.buffer.clone()) + { if let Some(dist) = super::expr_eval::eval_number(text.trim()) { let pt = base + dir * dist as f32; self.last_point = Some(pt); @@ -7823,8 +7592,7 @@ impl OpenCADStudio { // `on_text_input` (a count, radius, distance) rather than resolving a // point. Only the typed buffer matters here — a mouse-driven live // value commits through the viewport click, not Enter. - let wants_text = self - .tabs[i] + let wants_text = self.tabs[i] .active_cmd .as_ref() .map(|c| c.wants_text_input()) @@ -7943,10 +7711,16 @@ impl OpenCADStudio { { self.ts_cell_textstyle[r] = c.text_style_name.clone(); self.ts_cell_height[r] = format!("{:.4}", c.text_height); - self.ts_cell_textcolor[r] = - c.text_color.index().map(|v| v.to_string()).unwrap_or_default(); - self.ts_cell_fillcolor[r] = - c.fill_color.index().map(|v| v.to_string()).unwrap_or_default(); + self.ts_cell_textcolor[r] = c + .text_color + .index() + .map(|v| v.to_string()) + .unwrap_or_default(); + self.ts_cell_fillcolor[r] = c + .fill_color + .index() + .map(|v| v.to_string()) + .unwrap_or_default(); self.ts_cell_datatype[r] = c.data_type.to_string(); self.ts_cell_unittype[r] = c.unit_type.to_string(); self.ts_cell_format[r] = c.format_string.clone(); @@ -7968,10 +7742,7 @@ impl OpenCADStudio { } /// Mutable access to the currently selected multileader style. - fn mleaderstyle_mut( - &mut self, - tab: usize, - ) -> Option<&mut acadrust::objects::MultiLeaderStyle> { + fn mleaderstyle_mut(&mut self, tab: usize) -> Option<&mut acadrust::objects::MultiLeaderStyle> { use acadrust::objects::ObjectType; let name = self.mleaderstyle_selected.clone(); self.tabs[tab] @@ -7986,7 +7757,7 @@ impl OpenCADStudio { } /// Populate all edit buffers from the currently selected multileader style. - fn load_mleaderstyle_bufs(&mut self, tab: usize) { + pub(super) fn load_mleaderstyle_bufs(&mut self, tab: usize) { use acadrust::objects::ObjectType; let name = self.mleaderstyle_selected.clone(); let Some(s) = self.tabs[tab] @@ -8036,7 +7807,7 @@ impl OpenCADStudio { } /// Populate all edit buffers from the currently selected dim style. - fn load_dimstyle_bufs(&mut self, tab: usize) { + pub(super) fn load_dimstyle_bufs(&mut self, tab: usize) { let doc = &self.tabs[tab].scene.document; let Some(ds) = doc.dim_styles.get(&self.dimstyle_selected) else { return; @@ -8356,7 +8127,7 @@ impl OpenCADStudio { } } - fn load_textstyle_bufs(&mut self, tab: usize) { + pub(super) fn load_textstyle_bufs(&mut self, tab: usize) { let doc = &self.tabs[tab].scene.document; if let Some(s) = doc.text_styles.get(&self.textstyle_selected) { self.textstyle_font = s.font_file.clone(); @@ -8376,7 +8147,11 @@ impl OpenCADStudio { /// through the file (and is read correctly by other CAD applications). fn sync_annotation_scale_header(scene: &mut Scene) { let anno = scene.annotation_scale; - let value = if anno.abs() > 1e-9 { 1.0 / anno as f64 } else { 1.0 }; + let value = if anno.abs() > 1e-9 { + 1.0 / anno as f64 + } else { + 1.0 + }; // Prefer the name of a matching scale already in the drawing's list; // fall back to a formatted ratio when none matches. let name = scene diff --git a/src/app/view.rs b/src/app/view.rs index abcb01ea1..f2e3ad89b 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -1,15 +1,15 @@ use super::document::DocumentTab; +use super::document::{DynComponent, DynFieldEntry}; use super::helpers::grid_plane_from_camera; use super::history::history_dropdown_labels; -use super::document::{DynComponent, DynFieldEntry}; use super::{Message, OpenCADStudio}; use crate::scene::grip::{grips_to_screen, grips_to_screen_paper}; use crate::scene::viewport_pane::ViewportPane; use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD}; use crate::ui::overlay; use iced::widget::{ - button, column, container, mouse_area, pick_list, row, shader, stack, text, text_input, - Row, Space, + button, column, container, mouse_area, pick_list, row, shader, stack, text, text_input, Row, + Space, }; use iced::window; use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme}; @@ -84,6 +84,8 @@ impl OpenCADStudio { backward, upside_down, annotative, + rename_active: self.style_rename.as_deref(), + rename_buf: &self.style_rename_buf, }); } if Some(window_id) == self.tablestyle_window { @@ -130,6 +132,8 @@ impl OpenCADStudio { &self.ts_border_lw, &self.ts_border_color, &self.ts_border_spacing, + self.style_rename.as_deref(), + &self.style_rename_buf, ); } if Some(window_id) == self.mlstyle_window { @@ -164,6 +168,8 @@ impl OpenCADStudio { &self.mlstyle_selected, selected_style, tab.scene.document.header.multiline_style.clone(), + self.style_rename.as_deref(), + &self.style_rename_buf, ); } if Some(window_id) == self.mleaderstyle_window { @@ -243,39 +249,43 @@ impl OpenCADStudio { ), None => Default::default(), }; - return crate::ui::mleaderstyle::view_window(crate::ui::mleaderstyle::MLeaderStyleView { - styles, - selected: &self.mleaderstyle_selected, - style: selected_style, - current: tab.active_mleader_style.clone(), - landing_distance: &self.mls_landing_distance, - landing_gap: &self.mls_landing_gap, - arrowhead_size: &self.mls_arrowhead_size, - text_height: &self.mls_text_height, - scale_factor: &self.mls_scale_factor, - break_gap: &self.mls_break_gap, - first_seg_angle: &self.mls_first_seg_angle, - second_seg_angle: &self.mls_second_seg_angle, - max_points: &self.mls_max_points, - default_text: &self.mls_default_text, - line_color: &self.mls_line_color, - text_color: &self.mls_text_color, - description: &self.mls_description, - line_weight: &self.mls_line_weight, - align_space: &self.mls_align_space, - block_color: &self.mls_block_color, - block_rotation: &self.mls_block_rotation, - block_scale_x: &self.mls_block_scale_x, - block_scale_y: &self.mls_block_scale_y, - block_scale_z: &self.mls_block_scale_z, - block_opts, - lt_opts, - textstyle_opts, - line_type_name, - arrowhead_name, - text_style_name, - block_content_name, - }); + return crate::ui::mleaderstyle::view_window( + crate::ui::mleaderstyle::MLeaderStyleView { + styles, + selected: &self.mleaderstyle_selected, + style: selected_style, + current: tab.active_mleader_style.clone(), + landing_distance: &self.mls_landing_distance, + landing_gap: &self.mls_landing_gap, + arrowhead_size: &self.mls_arrowhead_size, + text_height: &self.mls_text_height, + scale_factor: &self.mls_scale_factor, + break_gap: &self.mls_break_gap, + first_seg_angle: &self.mls_first_seg_angle, + second_seg_angle: &self.mls_second_seg_angle, + max_points: &self.mls_max_points, + default_text: &self.mls_default_text, + line_color: &self.mls_line_color, + text_color: &self.mls_text_color, + description: &self.mls_description, + line_weight: &self.mls_line_weight, + align_space: &self.mls_align_space, + block_color: &self.mls_block_color, + block_rotation: &self.mls_block_rotation, + block_scale_x: &self.mls_block_scale_x, + block_scale_y: &self.mls_block_scale_y, + block_scale_z: &self.mls_block_scale_z, + block_opts, + lt_opts, + textstyle_opts, + line_type_name, + arrowhead_name, + text_style_name, + block_content_name, + rename_active: self.style_rename.as_deref(), + rename_buf: &self.style_rename_buf, + }, + ); } if Some(window_id) == self.layout_manager_window { let i = self.active_tab; @@ -441,6 +451,8 @@ impl OpenCADStudio { block_opts, lt_opts, }, + self.style_rename.as_deref(), + &self.style_rename_buf, ); } if Some(window_id) == self.shortcuts_window { @@ -450,10 +462,7 @@ impl OpenCADStudio { return crate::ui::about::view_window(); } if Some(window_id) == self.update_notice_window { - let latest = self - .update_notice_version - .as_deref() - .unwrap_or("?"); + let latest = self.update_notice_version.as_deref().unwrap_or("?"); let body = self.update_notice_body.as_deref().unwrap_or(""); return crate::ui::update_notice::view_window(latest, body); } @@ -502,9 +511,9 @@ impl OpenCADStudio { self.show_viewcube, tab.render_mode, )) - .width(Fill) - .height(Fill) - .into() + .width(Fill) + .height(Fill) + .into() }; let selection_overlay = { @@ -737,7 +746,12 @@ impl OpenCADStudio { // container background, the white sheet + paper entities + borders // come from the full-canvas top-locked "sheet" viewport, and the // floating content viewports overlay it (same path as model space). - const DESK: Color = Color { r: 0.22, g: 0.24, b: 0.28, a: 1.0 }; + const DESK: Color = Color { + r: 0.22, + g: 0.24, + b: 0.28, + a: 1.0, + }; stack![ container(viewport_3d) .style(move |_: &Theme| container::Style { @@ -967,9 +981,7 @@ impl OpenCADStudio { .max() .unwrap_or(8) as f32; let row_w = max_len * 7.0 + 24.0; - let mut col = column![] - .spacing(0) - .width(iced::Length::Fixed(row_w)); + let mut col = column![].spacing(0).width(iced::Length::Fixed(row_w)); for (idx, item) in popup.items.iter().enumerate() { let is_sel = idx == popup.selected; let label = item.label; @@ -1028,7 +1040,8 @@ impl OpenCADStudio { // the first item immediately, matching the right-click // context menu's "panel below the click point" feel. let anchor = iced::Point::new(popup.anchor.x + 12.0, popup.anchor.y + 12.0); - viewport_stack = viewport_stack.push(position_canvas_overlay(anchor, menu_panel.into())); + viewport_stack = + viewport_stack.push(position_canvas_overlay(anchor, menu_panel.into())); } } @@ -1079,8 +1092,10 @@ impl OpenCADStudio { }, ..Default::default() }); - viewport_stack = viewport_stack - .push(position_canvas_overlay(iced::Point::new(12.0, 40.0), panel.into())); + viewport_stack = viewport_stack.push(position_canvas_overlay( + iced::Point::new(12.0, 40.0), + panel.into(), + )); } // Selection-cycling list box: pick among overlapping objects. @@ -1089,9 +1104,10 @@ impl OpenCADStudio { let items: Vec<(acadrust::Handle, String)> = cands .iter() .filter_map(|&h| { - tab.scene.document.get_entity(h).map(|e| { - (h, crate::entities::traits::entity_type_name(e).to_string()) - }) + tab.scene + .document + .get_entity(h) + .map(|e| (h, crate::entities::traits::entity_type_name(e).to_string())) }) .collect(); if !items.is_empty() { @@ -1172,23 +1188,22 @@ impl OpenCADStudio { // so the command-line field must release focus / its on_input. // The MText preview also captures keystrokes (typing edits it), so the // command line must likewise release its on_input there. - let dyn_capturing = (self.dyn_input - && tab.active_cmd.is_some() - && !tab.dyn_fields.is_empty()) - || self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) - || self.text_inline.is_some(); + let dyn_capturing = + (self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty()) + || self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) + || self.text_inline.is_some(); let command_line_overlay = iced::widget::container(self.command_line.view(allow_autocomplete, dyn_capturing)) - .width(Fill) - .height(Fill) - .align_x(iced::alignment::Horizontal::Center) - .align_y(iced::alignment::Vertical::Bottom) - .padding(iced::Padding { - top: 0.0, - right: 0.0, - bottom: 2.0, - left: 0.0, - }); + .width(Fill) + .height(Fill) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Bottom) + .padding(iced::Padding { + top: 0.0, + right: 0.0, + bottom: 2.0, + left: 0.0, + }); let center_stack = iced::widget::stack![ row![properties_el, viewport_stack].width(Fill).height(Fill), @@ -1305,9 +1320,7 @@ impl OpenCADStudio { }; let units_layer: Element<'_, Message> = if self.units_popup_open { - crate::ui::units_popup::units_popup_overlay( - tab.scene.document.header.insertion_units, - ) + crate::ui::units_popup::units_popup_overlay(tab.scene.document.header.insertion_units) } else { iced::widget::Space::new().width(0).height(0).into() }; @@ -1352,9 +1365,7 @@ impl OpenCADStudio { let qselect_layer: Element<'_, Message> = if let Some(state) = &self.qselect { let types = tab.scene.entity_type_names_in_layout(); - let properties = tab - .scene - .qselect_properties(state.type_filter.as_deref()); + let properties = tab.scene.qselect_properties(state.type_filter.as_deref()); qselect_overlay(state, &types, &properties) } else { iced::widget::Space::new().width(0).height(0).into() @@ -1443,7 +1454,10 @@ impl OpenCADStudio { Some(Message::SetShiftDown(m.shift())) } iced::Event::Keyboard(keyboard::Event::KeyPressed { - key, modifiers, text, .. + key, + modifiers, + text, + .. }) => { let ctrl = modifiers.control(); let shift = modifiers.shift(); @@ -1695,7 +1709,11 @@ pub(super) fn doc_tab_bar<'a>(tabs: &'a [DocumentTab], active_tab: usize) -> Ele bar = bar.push( container(row_inner).style(move |_: &Theme| container::Style { border: Border { - color: if is_active { BORDER_COLOR } else { Color::TRANSPARENT }, + color: if is_active { + BORDER_COLOR + } else { + Color::TRANSPARENT + }, width: if is_active { 1.0 } else { 0.0 }, radius: 0.0.into(), }, @@ -1787,8 +1805,18 @@ fn text_inline_overlay( ed: &super::text_inline::TextInlineState, canvas: (f32, f32), ) -> Element<'_, Message> { - const PANEL_BG: Color = Color { r: 0.16, g: 0.16, b: 0.16, a: 0.98 }; - const BORDER: Color = Color { r: 0.40, g: 0.40, b: 0.40, a: 1.0 }; + const PANEL_BG: Color = Color { + r: 0.16, + g: 0.16, + b: 0.16, + a: 0.98, + }; + const BORDER: Color = Color { + r: 0.40, + g: 0.40, + b: 0.40, + a: 1.0, + }; let field = text_input("Text", &ed.value) .id(iced::widget::Id::new(TEXT_INLINE_ID)) @@ -1801,7 +1829,11 @@ fn text_inline_overlay( let panel = container(field) .style(move |_: &Theme| container::Style { background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 5.0.into() }, + border: Border { + color: BORDER, + width: 1.0, + radius: 5.0.into(), + }, ..Default::default() }) .padding(4); @@ -1977,7 +2009,15 @@ impl iced::widget::canvas::Program for MTextPreview { iced::Point::new(p0.x.min(p1.x), p0.y.min(p1.y)), iced::Size::new((p1.x - p0.x).abs(), (p1.y - p0.y).abs()), ); - frame.fill(&rect, Color { r: 0.20, g: 0.42, b: 0.72, a: 0.45 }); + frame.fill( + &rect, + Color { + r: 0.20, + g: 0.42, + b: 0.72, + a: 0.45, + }, + ); } } } @@ -2010,7 +2050,12 @@ impl iced::widget::canvas::Program for MTextPreview { frame.stroke( &path, Stroke::default() - .with_color(Color { r: 0.95, g: 0.95, b: 0.55, a: 1.0 }) + .with_color(Color { + r: 0.95, + g: 0.95, + b: 0.55, + a: 1.0, + }) .with_width(1.5), ); } else if collapsed { @@ -2034,7 +2079,12 @@ impl iced::widget::canvas::Program for MTextPreview { frame.stroke( &path, Stroke::default() - .with_color(Color { r: 0.95, g: 0.95, b: 0.55, a: 1.0 }) + .with_color(Color { + r: 0.95, + g: 0.95, + b: 0.55, + a: 1.0, + }) .with_width(1.5), ); } @@ -2079,20 +2129,52 @@ fn mtext_editor_overlay<'a>( use super::mtext_editor::{JustifyChoice, MTextFmt, ParaAlign}; use iced::widget::{canvas, svg, text_editor}; - const PANEL_BG: Color = Color { r: 0.16, g: 0.16, b: 0.16, a: 0.98 }; - const BORDER: Color = Color { r: 0.40, g: 0.40, b: 0.40, a: 1.0 }; - const TEXT_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const FIELD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 }; + const PANEL_BG: Color = Color { + r: 0.16, + g: 0.16, + b: 0.16, + a: 0.98, + }; + const BORDER: Color = Color { + r: 0.40, + g: 0.40, + b: 0.40, + a: 1.0, + }; + const TEXT_COL: Color = Color { + r: 0.88, + g: 0.88, + b: 0.88, + a: 1.0, + }; + const FIELD_BG: Color = Color { + r: 0.12, + g: 0.12, + b: 0.12, + a: 1.0, + }; let btn_style = |_: &Theme, status: button::Status| button::Style { background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => { - Color { r: 0.28, g: 0.40, b: 0.55, a: 1.0 } - } - _ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }, + button::Status::Hovered | button::Status::Pressed => Color { + r: 0.28, + g: 0.40, + b: 0.55, + a: 1.0, + }, + _ => Color { + r: 0.22, + g: 0.22, + b: 0.22, + a: 1.0, + }, })), text_color: TEXT_COL, - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + border: Border { + color: BORDER, + width: 1.0, + radius: 3.0.into(), + }, shadow: iced::Shadow::default(), snap: false, }; @@ -2132,7 +2214,10 @@ fn mtext_editor_overlay<'a>( ed.font.clone() }; let font_pl = pick_list( - MTEXT_FONTS.iter().map(|s| s.to_string()).collect::>(), + MTEXT_FONTS + .iter() + .map(|s| s.to_string()) + .collect::>(), Some(font_sel), Message::MTextFont, ) @@ -2144,7 +2229,10 @@ fn mtext_editor_overlay<'a>( .map(|(n, _)| n.to_string()) .unwrap_or_else(|| "ByLayer".to_string()); let color_pl = pick_list( - MTEXT_COLORS.iter().map(|(n, _)| n.to_string()).collect::>(), + MTEXT_COLORS + .iter() + .map(|(n, _)| n.to_string()) + .collect::>(), Some(color_sel), |name: String| { let aci = MTEXT_COLORS @@ -2163,13 +2251,34 @@ fn mtext_editor_overlay<'a>( font_pl, small_input("2.5", &ed.height, Message::MTextHeight, 64.0), iced::widget::Space::new().width(6), - icon_btn(include_bytes!("../../assets/icons/mt_bold.svg"), Message::MTextFmt(MTextFmt::Bold)), - icon_btn(include_bytes!("../../assets/icons/mt_italic.svg"), Message::MTextFmt(MTextFmt::Italic)), - icon_btn(include_bytes!("../../assets/icons/mt_underline.svg"), Message::MTextFmt(MTextFmt::Underline)), - icon_btn(include_bytes!("../../assets/icons/mt_overline.svg"), Message::MTextFmt(MTextFmt::Overline)), - icon_btn(include_bytes!("../../assets/icons/mt_strike.svg"), Message::MTextFmt(MTextFmt::Strike)), - icon_btn(include_bytes!("../../assets/icons/mt_upper.svg"), Message::MTextFmt(MTextFmt::Uppercase)), - icon_btn(include_bytes!("../../assets/icons/mt_lower.svg"), Message::MTextFmt(MTextFmt::Lowercase)), + icon_btn( + include_bytes!("../../assets/icons/mt_bold.svg"), + Message::MTextFmt(MTextFmt::Bold) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_italic.svg"), + Message::MTextFmt(MTextFmt::Italic) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_underline.svg"), + Message::MTextFmt(MTextFmt::Underline) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_overline.svg"), + Message::MTextFmt(MTextFmt::Overline) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_strike.svg"), + Message::MTextFmt(MTextFmt::Strike) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_upper.svg"), + Message::MTextFmt(MTextFmt::Uppercase) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_lower.svg"), + Message::MTextFmt(MTextFmt::Lowercase) + ), iced::widget::Space::new().width(Fill), color_pl, ] @@ -2192,45 +2301,92 @@ fn mtext_editor_overlay<'a>( lbl("◊"), small_input("0", &ed.char_space, Message::MTextCharSpace, 48.0), iced::widget::Space::new().width(6), - icon_btn(include_bytes!("../../assets/icons/mt_align_left.svg"), Message::MTextAlign(ParaAlign::Left)), - icon_btn(include_bytes!("../../assets/icons/mt_align_center.svg"), Message::MTextAlign(ParaAlign::Center)), - icon_btn(include_bytes!("../../assets/icons/mt_align_right.svg"), Message::MTextAlign(ParaAlign::Right)), - icon_btn(include_bytes!("../../assets/icons/mt_align_justify.svg"), Message::MTextAlign(ParaAlign::Justify)), + icon_btn( + include_bytes!("../../assets/icons/mt_align_left.svg"), + Message::MTextAlign(ParaAlign::Left) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_align_center.svg"), + Message::MTextAlign(ParaAlign::Center) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_align_right.svg"), + Message::MTextAlign(ParaAlign::Right) + ), + icon_btn( + include_bytes!("../../assets/icons/mt_align_justify.svg"), + Message::MTextAlign(ParaAlign::Justify) + ), iced::widget::Space::new().width(6), justify, lbl("LS"), - button(lbl("1")).on_press(Message::MTextLineSpacing(1.0)).padding(3).style(btn_style), - button(lbl("1.5")).on_press(Message::MTextLineSpacing(1.5)).padding(3).style(btn_style), - button(lbl("2")).on_press(Message::MTextLineSpacing(2.0)).padding(3).style(btn_style), + button(lbl("1")) + .on_press(Message::MTextLineSpacing(1.0)) + .padding(3) + .style(btn_style), + button(lbl("1.5")) + .on_press(Message::MTextLineSpacing(1.5)) + .padding(3) + .style(btn_style), + button(lbl("2")) + .on_press(Message::MTextLineSpacing(2.0)) + .padding(3) + .style(btn_style), iced::widget::Space::new().width(Fill), - icon_btn(include_bytes!("../../assets/icons/mt_ok.svg"), Message::MTextOk), - icon_btn(include_bytes!("../../assets/icons/mt_cancel.svg"), Message::MTextCancel), + icon_btn( + include_bytes!("../../assets/icons/mt_ok.svg"), + Message::MTextOk + ), + icon_btn( + include_bytes!("../../assets/icons/mt_cancel.svg"), + Message::MTextCancel + ), ] .spacing(4) .align_y(iced::Alignment::Center); // ── Segmented Edit | Preview toggle (between toolbar and body) ──────── - let seg_btn = move |label: &'static str, active: bool, on: Message| -> Element<'static, Message> { - button(text(label).size(12).color(if active { - Color::WHITE - } else { - Color { r: 0.80, g: 0.80, b: 0.80, a: 1.0 } - })) - .on_press(on) - .padding([4, 14]) - .style(move |_: &Theme, _| button::Style { - background: Some(Background::Color(if active { - Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 } + let seg_btn = + move |label: &'static str, active: bool, on: Message| -> Element<'static, Message> { + button(text(label).size(12).color(if active { + Color::WHITE } else { - Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 } - })), - text_color: TEXT_COL, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - shadow: iced::Shadow::default(), - snap: false, - }) - .into() - }; + Color { + r: 0.80, + g: 0.80, + b: 0.80, + a: 1.0, + } + })) + .on_press(on) + .padding([4, 14]) + .style(move |_: &Theme, _| button::Style { + background: Some(Background::Color(if active { + Color { + r: 0.20, + g: 0.42, + b: 0.72, + a: 1.0, + } + } else { + Color { + r: 0.20, + g: 0.20, + b: 0.20, + a: 1.0, + } + })), + text_color: TEXT_COL, + border: Border { + color: BORDER, + width: 1.0, + radius: 4.0.into(), + }, + shadow: iced::Shadow::default(), + snap: false, + }) + .into() + }; let toggle = container( row![ seg_btn("Edit", !ed.show_preview, Message::MTextShowPreview(false)), @@ -2281,11 +2437,17 @@ fn mtext_editor_overlay<'a>( scale, content_h, }; - let cv = canvas(prog).width(Fill).height(iced::Length::Fixed(content_h)); + let cv = canvas(prog) + .width(Fill) + .height(iced::Length::Fixed(content_h)); container(iced::widget::scrollable(cv).height(iced::Length::Fixed(VIEW_H))) .style(move |_: &Theme| container::Style { background: Some(Background::Color(FIELD_BG)), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + border: Border { + color: BORDER, + width: 1.0, + radius: 3.0.into(), + }, ..Default::default() }) .padding(2) @@ -2304,7 +2466,11 @@ fn mtext_editor_overlay<'a>( let panel = container(column![row1, row2, toggle, body].spacing(5)) .style(move |_: &Theme| container::Style { background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 5.0.into() }, + border: Border { + color: BORDER, + width: 1.0, + radius: 5.0.into(), + }, ..Default::default() }) .padding(6) @@ -2449,7 +2615,11 @@ fn viewport_context_menu_overlay( Message::Command("COPY".to_string()), )); items.push(sep()); - let arrow = if draworder_open { "Draw Order \u{25be}" } else { "Draw Order \u{25b8}" }; + let arrow = if draworder_open { + "Draw Order \u{25be}" + } else { + "Draw Order \u{25b8}" + }; items.push(item(arrow.to_string(), Message::DrawOrderSubmenuToggle)); if draworder_open { items.push(subitem( @@ -2623,13 +2793,48 @@ fn qselect_overlay<'a>( properties: &[(String, String)], ) -> Element<'a, Message> { use iced::widget::{checkbox, pick_list}; - const BG: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 0.98 }; - const BORDER: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; - const TEXT: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const BTN_OK: Color = Color { r: 0.22, g: 0.42, b: 0.68, a: 1.0 }; - const BTN_OK_HOV: Color = Color { r: 0.30, g: 0.52, b: 0.80, a: 1.0 }; - const BTN_BG: Color = Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }; - const BTN_HOV: Color = Color { r: 0.30, g: 0.30, b: 0.30, a: 1.0 }; + const BG: Color = Color { + r: 0.12, + g: 0.12, + b: 0.12, + a: 0.98, + }; + const BORDER: Color = Color { + r: 0.35, + g: 0.35, + b: 0.35, + a: 1.0, + }; + const TEXT: Color = Color { + r: 0.88, + g: 0.88, + b: 0.88, + a: 1.0, + }; + const BTN_OK: Color = Color { + r: 0.22, + g: 0.42, + b: 0.68, + a: 1.0, + }; + const BTN_OK_HOV: Color = Color { + r: 0.30, + g: 0.52, + b: 0.80, + a: 1.0, + }; + const BTN_BG: Color = Color { + r: 0.22, + g: 0.22, + b: 0.22, + a: 1.0, + }; + const BTN_HOV: Color = Color { + r: 0.30, + g: 0.30, + b: 0.30, + a: 1.0, + }; let mut type_options: Vec = vec![QSELECT_ANY_TYPE.to_string()]; type_options.extend(types.iter().map(|s| (*s).to_string())); @@ -2658,18 +2863,26 @@ fn qselect_overlay<'a>( .type_filter .clone() .unwrap_or_else(|| QSELECT_ANY_TYPE.to_string()); - let prop_sel = state.property.clone().unwrap_or(crate::app::QSelectPropertyChoice { - field: String::new(), - label: QSELECT_ANY_PROP.to_string(), - }); + let prop_sel = state + .property + .clone() + .unwrap_or(crate::app::QSelectPropertyChoice { + field: String::new(), + label: QSELECT_ANY_PROP.to_string(), + }); // The value field is disabled (visually de-emphasised; we still // render the same widget) when no property is picked or the // operator is "*Any value" — both of those skip the value test. - let value_enabled = state.property.is_some() - && !matches!(state.operator, crate::app::QSelectOp::Any); + let value_enabled = + state.property.is_some() && !matches!(state.operator, crate::app::QSelectOp::Any); - let label = |s: &'static str| text(s).size(12).color(TEXT).width(iced::Length::Fixed(90.0)); + let label = |s: &'static str| { + text(s) + .size(12) + .color(TEXT) + .width(iced::Length::Fixed(90.0)) + }; let btn = |lbl: &'static str, msg: Message, base: Color, hov: Color| { button(text(lbl).size(12).color(TEXT)) @@ -2717,13 +2930,17 @@ fn qselect_overlay<'a>( Space::new().height(6), row![ label("Property:"), - pick_list(prop_options, Some(prop_sel), |p: crate::app::QSelectPropertyChoice| { - if p.field.is_empty() { - Message::QSelectSetProperty(None) - } else { - Message::QSelectSetProperty(Some(p)) + pick_list( + prop_options, + Some(prop_sel), + |p: crate::app::QSelectPropertyChoice| { + if p.field.is_empty() { + Message::QSelectSetProperty(None) + } else { + Message::QSelectSetProperty(Some(p)) + } } - }) + ) .width(Fill), ] .align_y(iced::Alignment::Center) @@ -2731,8 +2948,12 @@ fn qselect_overlay<'a>( Space::new().height(6), row![ label("Operator:"), - pick_list(op_options, Some(state.operator), Message::QSelectSetOperator) - .width(Fill), + pick_list( + op_options, + Some(state.operator), + Message::QSelectSetOperator + ) + .width(Fill), ] .align_y(iced::Alignment::Center) .spacing(8), @@ -2784,8 +3005,7 @@ fn qselect_overlay<'a>( .on_press(Message::QSelectClose) .on_right_press(Message::QSelectClose); - let centered = container(iced::widget::opaque(panel)) - .center(Fill); + let centered = container(iced::widget::opaque(panel)).center(Fill); stack![catcher, centered].into() } @@ -2977,7 +3197,9 @@ fn save_as_dialog_window<'a>( row![ text(icon).size(13), Space::new().width(6), - text(crate::ui::text_util::elide(name.as_str(), 48)).size(13).color(color), + text(crate::ui::text_util::elide(name.as_str(), 48)) + .size(13) + .color(color), ] .align_y(iced::Alignment::Center), ) @@ -3164,14 +3386,44 @@ fn unsaved_changes_dialog_window(name: &str) -> Element<'static, Message> { // The page picks up the application icon's red-brown (#B03020) as a tint so // it visually belongs to OpenCADStudio without overpowering the dark workspace. -const BRAND: Color = Color { r: 0.690, g: 0.188, b: 0.125, a: 1.0 }; // #B03020 -const BRAND_DARK: Color = Color { r: 0.45, g: 0.12, b: 0.08, a: 1.0 }; +const BRAND: Color = Color { + r: 0.690, + g: 0.188, + b: 0.125, + a: 1.0, +}; // #B03020 +const BRAND_DARK: Color = Color { + r: 0.45, + g: 0.12, + b: 0.08, + a: 1.0, +}; pub(super) fn start_page_view<'a>() -> Element<'a, Message> { - const TEXT: Color = Color { r: 0.94, g: 0.93, b: 0.92, a: 1.0 }; - const MUTED: Color = Color { r: 0.62, g: 0.62, b: 0.62, a: 1.0 }; - const CARD_BG: Color = Color { r: 0.12, g: 0.12, b: 0.13, a: 1.0 }; - const CARD_BORDER: Color = Color { r: 0.20, g: 0.20, b: 0.22, a: 1.0 }; + const TEXT: Color = Color { + r: 0.94, + g: 0.93, + b: 0.92, + a: 1.0, + }; + const MUTED: Color = Color { + r: 0.62, + g: 0.62, + b: 0.62, + a: 1.0, + }; + const CARD_BG: Color = Color { + r: 0.12, + g: 0.12, + b: 0.13, + a: 1.0, + }; + const CARD_BORDER: Color = Color { + r: 0.20, + g: 0.20, + b: 0.22, + a: 1.0, + }; // Brand-tinted "Welcome to" — the "OpenCADStudio" word takes the accent colour // (Thunderbird-style coloured headline split). @@ -3195,12 +3447,27 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> { .padding([10, 22]) .style(move |_: &Theme, status| button::Style { background: Some(Background::Color(match status { - button::Status::Hovered => Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 }, - _ => Color { r: 0.13, g: 0.13, b: 0.15, a: 1.0 }, + button::Status::Hovered => Color { + r: 0.18, + g: 0.18, + b: 0.20, + a: 1.0, + }, + _ => Color { + r: 0.13, + g: 0.13, + b: 0.15, + a: 1.0, + }, })), text_color: TEXT, border: Border { - color: Color { r: 0.30, g: 0.30, b: 0.33, a: 1.0 }, + color: Color { + r: 0.30, + g: 0.30, + b: 0.33, + a: 1.0, + }, width: 1.0, radius: 6.0.into(), }, @@ -3234,7 +3501,12 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> { radius: 6.0.into(), }, shadow: iced::Shadow { - color: Color { r: 0.0, g: 0.0, b: 0.0, a: 0.4 }, + color: Color { + r: 0.0, + g: 0.0, + b: 0.0, + a: 0.4, + }, offset: iced::Vector::new(0.0, 2.0), blur_radius: 6.0, }, @@ -3314,11 +3586,21 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> { // the action row, matching the Thunderbird coloured-glow look against // the dark page. let primary_glow = container(primary_row) - .padding(iced::Padding { top: 4.0, right: 8.0, bottom: 4.0, left: 8.0 }) + .padding(iced::Padding { + top: 4.0, + right: 8.0, + bottom: 4.0, + left: 8.0, + }) .style(|_: &Theme| container::Style { background: Some(Background::Color(Color::TRANSPARENT)), shadow: iced::Shadow { - color: Color { r: BRAND.r, g: BRAND.g, b: BRAND.b, a: 0.45 }, + color: Color { + r: BRAND.r, + g: BRAND.g, + b: BRAND.b, + a: 0.45, + }, offset: iced::Vector::ZERO, blur_radius: 80.0, }, @@ -3344,13 +3626,23 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> { // Page background reverts to plain dark — the glow alone provides the // brand colour cue, the rest of the page stays neutral so it reads as // "workspace area" not "advertising banner". - const PAGE_BG: Color = Color { r: 0.08, g: 0.08, b: 0.085, a: 1.0 }; + const PAGE_BG: Color = Color { + r: 0.08, + g: 0.08, + b: 0.085, + a: 1.0, + }; container(content) .style(|_: &Theme| container::Style { background: Some(Background::Color(PAGE_BG)), ..Default::default() }) - .padding(iced::Padding { top: 40.0, right: 60.0, bottom: 40.0, left: 60.0 }) + .padding(iced::Padding { + top: 40.0, + right: 60.0, + bottom: 40.0, + left: 60.0, + }) .width(Fill) .height(Fill) .into() @@ -3363,14 +3655,43 @@ pub(super) fn start_page_view<'a>() -> Element<'a, Message> { // Start tab. The list is restored from disk at boot and re-saved on every // open — entries persist across sessions. pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Element<'a, Message> { - const PANEL_BG: Color = Color { r: 0.10, g: 0.10, b: 0.11, a: 1.0 }; - const PANEL_BORDER: Color = Color { r: 0.18, g: 0.18, b: 0.20, a: 1.0 }; - const ITEM_HOVER: Color = Color { r: 0.16, g: 0.16, b: 0.18, a: 1.0 }; - const TEXT: Color = Color { r: 0.92, g: 0.91, b: 0.90, a: 1.0 }; - const MUTED: Color = Color { r: 0.60, g: 0.60, b: 0.62, a: 1.0 }; + const PANEL_BG: Color = Color { + r: 0.10, + g: 0.10, + b: 0.11, + a: 1.0, + }; + const PANEL_BORDER: Color = Color { + r: 0.18, + g: 0.18, + b: 0.20, + a: 1.0, + }; + const ITEM_HOVER: Color = Color { + r: 0.16, + g: 0.16, + b: 0.18, + a: 1.0, + }; + const TEXT: Color = Color { + r: 0.92, + g: 0.91, + b: 0.90, + a: 1.0, + }; + const MUTED: Color = Color { + r: 0.60, + g: 0.60, + b: 0.62, + a: 1.0, + }; - let header = container(text("Recent Documents").size(11).color(MUTED)) - .padding(iced::Padding { top: 12.0, right: 14.0, bottom: 8.0, left: 14.0 }); + let header = container(text("Recent Documents").size(11).color(MUTED)).padding(iced::Padding { + top: 12.0, + right: 14.0, + bottom: 8.0, + left: 14.0, + }); let body: Element<'a, Message> = if recents.is_empty() { container( @@ -3395,8 +3716,12 @@ pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Eleme let path_for_open = path.clone(); let open_btn = button( column![ - text(crate::ui::text_util::elide(&name, 32)).size(12).color(TEXT), - text(crate::ui::text_util::elide(&dir, 42)).size(10).color(MUTED), + text(crate::ui::text_util::elide(&name, 32)) + .size(12) + .color(TEXT), + text(crate::ui::text_util::elide(&dir, 42)) + .size(10) + .color(MUTED), ] .spacing(2), ) @@ -3423,7 +3748,12 @@ pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Eleme .padding([4, 8]) .style(|_: &Theme, status| button::Style { background: Some(Background::Color(match status { - button::Status::Hovered => Color { r: 0.45, g: 0.15, b: 0.15, a: 1.0 }, + button::Status::Hovered => Color { + r: 0.45, + g: 0.15, + b: 0.15, + a: 1.0, + }, _ => Color::TRANSPARENT, })), text_color: MUTED, @@ -3435,11 +3765,7 @@ pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Eleme ..Default::default() }); - col = col.push( - row![open_btn, remove_btn] - .spacing(0) - .align_y(iced::Center), - ); + col = col.push(row![open_btn, remove_btn].spacing(0).align_y(iced::Center)); } iced::widget::scrollable(col).into() }; @@ -3465,9 +3791,7 @@ pub(super) fn recent_files_panel<'a>(recents: &'a [std::path::PathBuf]) -> Eleme /// Shared by the model-space viewport (top-left) and each active /// paper-space viewport. Emits `SetRenderMode`, which the update loop /// routes to the active viewport entity or the model-layout tab. -fn render_mode_picker<'a>( - current: acadrust::entities::ViewportRenderMode, -) -> Element<'a, Message> { +fn render_mode_picker<'a>(current: acadrust::entities::ViewportRenderMode) -> Element<'a, Message> { use acadrust::entities::ViewportRenderMode as M; let render_modes: Vec = vec![ RenderModeChoice(M::Wireframe2D), @@ -3530,11 +3854,7 @@ fn dyn_component_label(c: DynComponent) -> String { /// The string shown inside a dynamic-input box: the typed buffer when the /// field is locked, otherwise the live value derived from the cursor /// world position (and the base point for polar quantities). -fn dyn_component_value( - f: &DynFieldEntry, - w: glam::Vec3, - base: Option, -) -> String { +fn dyn_component_value(f: &DynFieldEntry, w: glam::Vec3, base: Option) -> String { if let Some(b) = &f.buffer { return b.clone(); } diff --git a/src/ui/dimstyle.rs b/src/ui/dimstyle.rs index b4fa60835..99eadd67a 100644 --- a/src/ui/dimstyle.rs +++ b/src/ui/dimstyle.rs @@ -6,18 +6,6 @@ use iced::widget::{ }; use iced::{Background, Border, Color, Element, Fill, Theme}; -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 BORDER: Color = Color { r: 0.35, g: 0.35, @@ -54,12 +42,6 @@ const FIELD: Color = Color { b: 0.10, a: 1.0, }; -const LIST: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; /// All DimStyle field values needed by the view. pub struct DimStyleValues<'a> { @@ -146,39 +128,6 @@ pub struct DimStyleValues<'a> { pub lt_opts: Vec, } -fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - (true, _) => ACCENT, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT, - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - } -} - fn tab_btn_style(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { move |_: &Theme, st| button::Style { background: Some(Background::Color(match (active, st) { @@ -206,33 +155,6 @@ fn tab_btn_style(active: bool) -> impl Fn(&Theme, button::Status) -> button::Sty } } -fn list_item_style(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (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.18, - g: 0.18, - b: 0.18, - a: 1.0, - }, - })), - text_color: TEXT, - border: Border { - color: BORDER, - width: 0.0, - radius: 3.0.into(), - }, - ..Default::default() - } -} - fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { text_input::Style { background: Background::Color(FIELD), @@ -264,83 +186,9 @@ pub fn view_window<'a>( selected: &'a str, tab: u8, vals: DimStyleValues<'a>, + rename_active: Option<&'a str>, + rename_buf: &'a str, ) -> Element<'a, Message> { - // ── Toolbar ─────────────────────────────────────────────────────────── - let toolbar = container( - row![ - button(text("New").size(11)) - .on_press(Message::DimStyleDialogNew) - .style(btn_s(false)) - .padding([4, 10]), - button(text("Delete").size(11)) - .on_press(Message::DimStyleDialogDelete) - .style(btn_s(false)) - .padding([4, 10]), - button(text("Set Current").size(11)) - .on_press(Message::DimStyleDialogSetCurrent) - .style(btn_s(false)) - .padding([4, 10]), - Space::new().width(Fill), - button(text("Apply").size(11)) - .on_press(Message::DimStyleDialogApply) - .style(btn_s(true)) - .padding([4, 14]), - button(text("Close").size(11)) - .on_press(Message::DimStyleDialogClose) - .style(btn_s(false)) - .padding([4, 10]), - ] - .spacing(4) - .align_y(iced::Center), - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), - ..Default::default() - }) - .width(Fill) - .padding([5, 8]); - - // ── Style list panel ────────────────────────────────────────────────── - let mut list_col = column![].spacing(2); - for name in &styles { - let active = name.as_str() == selected; - list_col = list_col.push( - button(text(name.clone()).size(11)) - .on_press(Message::DimStyleDialogSelect(name.clone())) - .style(list_item_style(active)) - .padding([4, 8]) - .width(Fill), - ); - } - let style_list = container( - column![ - text("Styles").size(10).color(DIM), - container(scrollable(list_col).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), - border: Border { - color: BORDER, - width: 1.0, - radius: 3.0.into() - }, - ..Default::default() - }) - .width(180) - .height(Fill) - .padding(2), - ] - .spacing(4) - .height(Fill), - ) - .width(180) - .height(Fill) - .padding(iced::Padding { - top: 12.0, - right: 8.0, - bottom: 12.0, - left: 12.0, - }); - // ── Tab bar ─────────────────────────────────────────────────────────── let tabs = row![ button(text("Lines").size(11)) @@ -448,24 +296,88 @@ pub fn view_window<'a>( .align_y(iced::Center), chk("Suppress 1st line (DIMSE1)", vals.dimse1, DsField::Dimse1), chk("Suppress 2nd line (DIMSE2)", vals.dimse2, DsField::Dimse2), - row![lbl("Dim line color ACI (DIMCLRD)"), mk_field(DsField::Dimclrd, vals.dimclrd)].spacing(8).align_y(iced::Center), - row![lbl("Dim line weight (DIMLWD)"), mk_field(DsField::Dimlwd, vals.dimlwd)].spacing(8).align_y(iced::Center), - row![lbl("Ext line color ACI (DIMCLRE)"), mk_field(DsField::Dimclre, vals.dimclre)].spacing(8).align_y(iced::Center), - row![lbl("Ext line weight (DIMLWE)"), mk_field(DsField::Dimlwe, vals.dimlwe)].spacing(8).align_y(iced::Center), - chk("Fixed-length ext lines (DIMFXLON)", vals.dimfxlon, DsField::Dimfxlon), - row![lbl("Fixed length (DIMFXL)"), mk_field(DsField::Dimfxl, vals.dimfxl)].spacing(8).align_y(iced::Center), - hrow("Dim line linetype (DIMLTYPE)", vals.lt_opts.clone(), vals.dimltex_name.clone(), "dimltex_handle"), - hrow("Ext line 1 linetype (DIMLTEX1)", vals.lt_opts.clone(), vals.dimltex1_name.clone(), "dimltex1_handle"), - hrow("Ext line 2 linetype (DIMLTEX2)", vals.lt_opts.clone(), vals.dimltex2_name.clone(), "dimltex2_handle"), + row![ + lbl("Dim line color ACI (DIMCLRD)"), + mk_field(DsField::Dimclrd, vals.dimclrd) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Dim line weight (DIMLWD)"), + mk_field(DsField::Dimlwd, vals.dimlwd) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Ext line color ACI (DIMCLRE)"), + mk_field(DsField::Dimclre, vals.dimclre) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Ext line weight (DIMLWE)"), + mk_field(DsField::Dimlwe, vals.dimlwe) + ] + .spacing(8) + .align_y(iced::Center), + chk( + "Fixed-length ext lines (DIMFXLON)", + vals.dimfxlon, + DsField::Dimfxlon + ), + row![ + lbl("Fixed length (DIMFXL)"), + mk_field(DsField::Dimfxl, vals.dimfxl) + ] + .spacing(8) + .align_y(iced::Center), + hrow( + "Dim line linetype (DIMLTYPE)", + vals.lt_opts.clone(), + vals.dimltex_name.clone(), + "dimltex_handle" + ), + hrow( + "Ext line 1 linetype (DIMLTEX1)", + vals.lt_opts.clone(), + vals.dimltex1_name.clone(), + "dimltex1_handle" + ), + hrow( + "Ext line 2 linetype (DIMLTEX2)", + vals.lt_opts.clone(), + vals.dimltex2_name.clone(), + "dimltex2_handle" + ), ] .spacing(7) .into(), 1 => column![ text("Arrows").size(11).color(ACCENT), - hrow("Arrowhead (DIMBLK)", vals.block_opts.clone(), vals.dimblk_name.clone(), "dimblk"), - hrow("1st arrowhead (DIMBLK1)", vals.block_opts.clone(), vals.dimblk1_name.clone(), "dimblk1"), - hrow("2nd arrowhead (DIMBLK2)", vals.block_opts.clone(), vals.dimblk2_name.clone(), "dimblk2"), - hrow("Leader arrowhead (DIMLDRBLK)", vals.block_opts.clone(), vals.dimldrblk_name.clone(), "dimldrblk"), + hrow( + "Arrowhead (DIMBLK)", + vals.block_opts.clone(), + vals.dimblk_name.clone(), + "dimblk" + ), + hrow( + "1st arrowhead (DIMBLK1)", + vals.block_opts.clone(), + vals.dimblk1_name.clone(), + "dimblk1" + ), + hrow( + "2nd arrowhead (DIMBLK2)", + vals.block_opts.clone(), + vals.dimblk2_name.clone(), + "dimblk2" + ), + hrow( + "Leader arrowhead (DIMLDRBLK)", + vals.block_opts.clone(), + vals.dimldrblk_name.clone(), + "dimldrblk" + ), row![ lbl("Arrow size (DIMASZ)"), mk_field(DsField::Dimasz, vals.dimasz) @@ -484,9 +396,23 @@ pub fn view_window<'a>( ] .spacing(8) .align_y(iced::Center), - chk("Separate arrow blocks (DIMSAH)", vals.dimsah, DsField::Dimsah), - row![lbl("Arc length symbol (DIMARCSYM)"), mk_field(DsField::Dimarcsym, vals.dimarcsym)].spacing(8).align_y(iced::Center), - row![lbl("Jog angle ° (DIMJOGANG)"), mk_field(DsField::Dimjogang, vals.dimjogang)].spacing(8).align_y(iced::Center), + chk( + "Separate arrow blocks (DIMSAH)", + vals.dimsah, + DsField::Dimsah + ), + row![ + lbl("Arc length symbol (DIMARCSYM)"), + mk_field(DsField::Dimarcsym, vals.dimarcsym) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Jog angle ° (DIMJOGANG)"), + mk_field(DsField::Dimjogang, vals.dimjogang) + ] + .spacing(8) + .align_y(iced::Center), ] .spacing(7) .into(), @@ -512,12 +438,41 @@ pub fn view_window<'a>( .align_y(iced::Center), chk("Horizontal inside (DIMTIH)", vals.dimtih, DsField::Dimtih), chk("Horizontal outside (DIMTOH)", vals.dimtoh, DsField::Dimtoh), - row![lbl("Text color ACI (DIMCLRT)"), mk_field(DsField::Dimclrt, vals.dimclrt)].spacing(8).align_y(iced::Center), - row![lbl("Horizontal just (DIMJUST)"), mk_field(DsField::Dimjust, vals.dimjust)].spacing(8).align_y(iced::Center), - row![lbl("Vertical pos (DIMTVP)"), mk_field(DsField::Dimtvp, vals.dimtvp)].spacing(8).align_y(iced::Center), - row![lbl("Text fill mode (DIMTFILL)"), mk_field(DsField::Dimtfill, vals.dimtfill)].spacing(8).align_y(iced::Center), - row![lbl("Fill color ACI (DIMTFILLCLR)"), mk_field(DsField::Dimtfillclr, vals.dimtfillclr)].spacing(8).align_y(iced::Center), - chk("Left-to-right (DIMTXTDIRECTION)", vals.dimtxtdirection, DsField::Dimtxtdirection), + row![ + lbl("Text color ACI (DIMCLRT)"), + mk_field(DsField::Dimclrt, vals.dimclrt) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Horizontal just (DIMJUST)"), + mk_field(DsField::Dimjust, vals.dimjust) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Vertical pos (DIMTVP)"), + mk_field(DsField::Dimtvp, vals.dimtvp) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Text fill mode (DIMTFILL)"), + mk_field(DsField::Dimtfill, vals.dimtfill) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Fill color ACI (DIMTFILLCLR)"), + mk_field(DsField::Dimtfillclr, vals.dimtfillclr) + ] + .spacing(8) + .align_y(iced::Center), + chk( + "Left-to-right (DIMTXTDIRECTION)", + vals.dimtxtdirection, + DsField::Dimtxtdirection + ), ] .spacing(7) .into(), @@ -555,36 +510,143 @@ pub fn view_window<'a>( ] .spacing(8) .align_y(iced::Center), - row![lbl("Decimal sep ASCII (DIMDSEP)"), mk_field(DsField::Dimdsep, vals.dimdsep)].spacing(8).align_y(iced::Center), - row![lbl("Round off (DIMRND)"), mk_field(DsField::Dimrnd, vals.dimrnd)].spacing(8).align_y(iced::Center), - row![lbl("Zero suppress (DIMZIN)"), mk_field(DsField::Dimzin, vals.dimzin)].spacing(8).align_y(iced::Center), - row![lbl("Fraction format (DIMFRAC)"), mk_field(DsField::Dimfrac, vals.dimfrac)].spacing(8).align_y(iced::Center), - row![lbl("Angular unit (DIMAUNIT)"), mk_field(DsField::Dimaunit, vals.dimaunit)].spacing(8).align_y(iced::Center), - row![lbl("Angular decimals (DIMADEC)"), mk_field(DsField::Dimadec, vals.dimadec)].spacing(8).align_y(iced::Center), - row![lbl("Unit format (DIMUNIT)"), mk_field(DsField::Dimunit, vals.dimunit)].spacing(8).align_y(iced::Center), - row![lbl("Angular zero supp (DIMAZIN)"), mk_field(DsField::Dimazin, vals.dimazin)].spacing(8).align_y(iced::Center), + row![ + lbl("Decimal sep ASCII (DIMDSEP)"), + mk_field(DsField::Dimdsep, vals.dimdsep) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Round off (DIMRND)"), + mk_field(DsField::Dimrnd, vals.dimrnd) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Zero suppress (DIMZIN)"), + mk_field(DsField::Dimzin, vals.dimzin) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Fraction format (DIMFRAC)"), + mk_field(DsField::Dimfrac, vals.dimfrac) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Angular unit (DIMAUNIT)"), + mk_field(DsField::Dimaunit, vals.dimaunit) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Angular decimals (DIMADEC)"), + mk_field(DsField::Dimadec, vals.dimadec) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Unit format (DIMUNIT)"), + mk_field(DsField::Dimunit, vals.dimunit) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Angular zero supp (DIMAZIN)"), + mk_field(DsField::Dimazin, vals.dimazin) + ] + .spacing(8) + .align_y(iced::Center), text("Fit").size(11).color(ACCENT), - row![lbl("Fit (DIMATFIT)"), mk_field(DsField::Dimatfit, vals.dimatfit)].spacing(8).align_y(iced::Center), - row![lbl("Text movement (DIMTMOVE)"), mk_field(DsField::Dimtmove, vals.dimtmove)].spacing(8).align_y(iced::Center), - row![lbl("Fit (legacy DIMFIT)"), mk_field(DsField::Dimfit, vals.dimfit)].spacing(8).align_y(iced::Center), + row![ + lbl("Fit (DIMATFIT)"), + mk_field(DsField::Dimatfit, vals.dimatfit) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Text movement (DIMTMOVE)"), + mk_field(DsField::Dimtmove, vals.dimtmove) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Fit (legacy DIMFIT)"), + mk_field(DsField::Dimfit, vals.dimfit) + ] + .spacing(8) + .align_y(iced::Center), chk("Force text inside (DIMTIX)", vals.dimtix, DsField::Dimtix), - chk("Suppress outside arrows (DIMSOXD)", vals.dimsoxd, DsField::Dimsoxd), + chk( + "Suppress outside arrows (DIMSOXD)", + vals.dimsoxd, + DsField::Dimsoxd + ), chk("Place text manually (DIMUPT)", vals.dimupt, DsField::Dimupt), - chk("Dim line between ext (DIMTOFL)", vals.dimtofl, DsField::Dimtofl), + chk( + "Dim line between ext (DIMTOFL)", + vals.dimtofl, + DsField::Dimtofl + ), ] .spacing(7) .into(), 5 => column![ text("Alternate Units").size(11).color(ACCENT), - chk("Enable alternate units (DIMALT)", vals.dimalt, DsField::Dimalt), - row![lbl("Multiplier (DIMALTF)"), mk_field(DsField::Dimaltf, vals.dimaltf)].spacing(8).align_y(iced::Center), - row![lbl("Decimals (DIMALTD)"), mk_field(DsField::Dimaltd, vals.dimaltd)].spacing(8).align_y(iced::Center), - row![lbl("Unit format (DIMALTU)"), mk_field(DsField::Dimaltu, vals.dimaltu)].spacing(8).align_y(iced::Center), - row![lbl("Tol decimals (DIMALTTD)"), mk_field(DsField::Dimalttd, vals.dimalttd)].spacing(8).align_y(iced::Center), - row![lbl("Round off (DIMALTRND)"), mk_field(DsField::Dimaltrnd, vals.dimaltrnd)].spacing(8).align_y(iced::Center), - row![lbl("Suffix (DIMAPOST)"), mk_field(DsField::Dimapost, vals.dimapost)].spacing(8).align_y(iced::Center), - row![lbl("Zero suppress (DIMALTZ)"), mk_field(DsField::Dimaltz, vals.dimaltz)].spacing(8).align_y(iced::Center), - row![lbl("Tol zero supp (DIMALTTZ)"), mk_field(DsField::Dimalttz, vals.dimalttz)].spacing(8).align_y(iced::Center), + chk( + "Enable alternate units (DIMALT)", + vals.dimalt, + DsField::Dimalt + ), + row![ + lbl("Multiplier (DIMALTF)"), + mk_field(DsField::Dimaltf, vals.dimaltf) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Decimals (DIMALTD)"), + mk_field(DsField::Dimaltd, vals.dimaltd) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Unit format (DIMALTU)"), + mk_field(DsField::Dimaltu, vals.dimaltu) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Tol decimals (DIMALTTD)"), + mk_field(DsField::Dimalttd, vals.dimalttd) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Round off (DIMALTRND)"), + mk_field(DsField::Dimaltrnd, vals.dimaltrnd) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Suffix (DIMAPOST)"), + mk_field(DsField::Dimapost, vals.dimapost) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Zero suppress (DIMALTZ)"), + mk_field(DsField::Dimaltz, vals.dimaltz) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Tol zero supp (DIMALTTZ)"), + mk_field(DsField::Dimalttz, vals.dimalttz) + ] + .spacing(8) + .align_y(iced::Center), ] .spacing(7) .into(), @@ -616,8 +678,18 @@ pub fn view_window<'a>( ] .spacing(8) .align_y(iced::Center), - row![lbl("Tol. vert just (DIMTOLJ)"), mk_field(DsField::Dimtolj, vals.dimtolj)].spacing(8).align_y(iced::Center), - row![lbl("Tol. zero supp (DIMTZIN)"), mk_field(DsField::Dimtzin, vals.dimtzin)].spacing(8).align_y(iced::Center), + row![ + lbl("Tol. vert just (DIMTOLJ)"), + mk_field(DsField::Dimtolj, vals.dimtolj) + ] + .spacing(8) + .align_y(iced::Center), + row![ + lbl("Tol. zero supp (DIMTZIN)"), + mk_field(DsField::Dimtzin, vals.dimtzin) + ] + .spacing(8) + .align_y(iced::Center), ] .spacing(7) .into(), @@ -648,23 +720,19 @@ pub fn view_window<'a>( left: 0.0, }); - // ── Vertical separator ──────────────────────────────────────────────── - let vsep = container(Space::new().width(1).height(Fill)) - .width(1) - .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), - ..Default::default() - }); - - let body = row![style_list, vsep, right_panel].height(Fill); - - container(column![toolbar, hdivider(), body].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) - .width(Fill) - .height(Fill) - .into() + crate::ui::style_manager::view(crate::ui::style_manager::Scaffold { + kind: crate::app::StyleKind::Dim, + styles: &styles, + selected, + current: None, + rename_active, + rename_buf, + on_new: Message::DimStyleDialogNew, + on_copy: Message::DimStyleDialogCopy, + on_delete: Message::DimStyleDialogDelete, + on_select: Message::DimStyleDialogSelect, + on_set_current: Message::DimStyleDialogSetCurrent, + on_apply: Message::DimStyleDialogApply, + editor: right_panel.into(), + }) } diff --git a/src/ui/mleaderstyle.rs b/src/ui/mleaderstyle.rs index c99c4e619..fe576fee5 100644 --- a/src/ui/mleaderstyle.rs +++ b/src/ui/mleaderstyle.rs @@ -6,18 +6,6 @@ use iced::widget::{ }; use iced::{Background, Border, Color, Element, Fill, Theme}; -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 BORDER: Color = Color { r: 0.35, g: 0.35, @@ -42,18 +30,6 @@ const ACCENT: Color = Color { b: 0.85, a: 1.0, }; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const LIST: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { move |_: &Theme, st| button::Style { @@ -88,45 +64,6 @@ fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { } } -fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (true, _) => ACTIVE, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - text_color: TEXT, - ..Default::default() - } -} - -fn hdivider<'a>() -> Element<'a, Message> { - container(Space::new().width(Fill).height(1)) - .width(Fill) - .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), - ..Default::default() - }) - .into() -} - -fn vsep<'a>() -> Element<'a, Message> { - container(Space::new().width(1).height(Fill)) - .width(1) - .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), - ..Default::default() - }) - .into() -} - /// View-model borrowed from the app for one render of the dialog. pub struct MLeaderStyleView<'a> { pub styles: Vec, @@ -161,6 +98,10 @@ pub struct MLeaderStyleView<'a> { pub arrowhead_name: String, pub text_style_name: String, pub block_content_name: String, + /// Name of the style being renamed inline (double-clicked), if any. + pub rename_active: Option<&'a str>, + /// Edit buffer for the inline rename text input. + pub rename_buf: &'a str, } fn section<'a>(label: &'static str) -> Element<'a, Message> { @@ -254,76 +195,6 @@ fn chk<'a>(label: &'static str, val: bool, field: &'static str) -> Element<'a, M } pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> { - // ── Toolbar ─────────────────────────────────────────────────────────── - let toolbar = container( - row![ - button(text("New").size(11)) - .on_press(Message::MLeaderStyleDialogNew) - .style(btn_s(true)) - .padding([4, 10]), - button(text("Delete").size(11)) - .on_press(Message::MLeaderStyleDialogDelete) - .style(btn_s(false)) - .padding([4, 10]), - button(text("Set Current").size(11)) - .on_press(Message::MLeaderStyleDialogSetCurrent) - .style(btn_s(false)) - .padding([4, 10]), - ] - .spacing(4) - .align_y(iced::Center), - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), - ..Default::default() - }) - .width(Fill) - .padding([5, 8]); - - // ── Left: Style list ────────────────────────────────────────────────── - let style_items: Vec> = v - .styles - .iter() - .map(|name| { - let is_sel = name.as_str() == v.selected; - button(text(name.clone()).size(11)) - .on_press(Message::MLeaderStyleDialogSelect(name.clone())) - .style(list_item(is_sel)) - .padding([4, 8]) - .width(Fill) - .into() - }) - .collect(); - - let style_list = container( - column![ - text(format!("Current: {}", v.current)).size(10).color(DIM), - container(scrollable(column(style_items).spacing(2)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), - border: Border { - color: BORDER, - width: 1.0, - radius: 3.0.into() - }, - ..Default::default() - }) - .width(Fill) - .height(Fill) - .padding(2), - ] - .spacing(4) - .height(Fill), - ) - .width(200) - .height(Fill) - .padding(iced::Padding { - top: 12.0, - right: 8.0, - bottom: 12.0, - left: 12.0, - }); - // ── Right: Details panel ────────────────────────────────────────────── let details: Element<'_, Message> = if let Some(s) = v.style { scrollable( @@ -356,17 +227,37 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> { v.arrowhead_name.clone(), "arrowhead_handle" ), - num_row("Arrowhead size:", "0.18", v.arrowhead_size, "arrowhead_size"), + num_row( + "Arrowhead size:", + "0.18", + v.arrowhead_size, + "arrowhead_size" + ), num_row("Break gap size:", "0.125", v.break_gap, "break_gap"), // Leader Structure section("Leader Structure"), chk("Enable landing", s.enable_landing, "enable_landing"), chk("Enable dogleg", s.enable_dogleg, "enable_dogleg"), - num_row("Landing distance:", "8.0", v.landing_distance, "landing_distance"), + num_row( + "Landing distance:", + "8.0", + v.landing_distance, + "landing_distance" + ), num_row("Landing gap:", "0.09", v.landing_gap, "landing_gap"), num_row("Max leader points:", "2", v.max_points, "max_points"), - num_row("First seg. angle:", "0", v.first_seg_angle, "first_seg_angle"), - num_row("Second seg. angle:", "0", v.second_seg_angle, "second_seg_angle"), + num_row( + "First seg. angle:", + "0", + v.first_seg_angle, + "first_seg_angle" + ), + num_row( + "Second seg. angle:", + "0", + v.second_seg_angle, + "second_seg_angle" + ), num_row("Scale factor:", "1.0", v.scale_factor, "scale_factor"), num_row("Align space:", "4.0", v.align_space, "align_space"), enum_row( @@ -462,7 +353,11 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> { num_row("Block scale X:", "1.0", v.block_scale_x, "block_scale_x"), num_row("Block scale Y:", "1.0", v.block_scale_y, "block_scale_y"), num_row("Block scale Z:", "1.0", v.block_scale_z, "block_scale_z"), - chk("Enable block scale", s.enable_block_scale, "enable_block_scale"), + chk( + "Enable block scale", + s.enable_block_scale, + "enable_block_scale" + ), chk( "Enable block rotation", s.enable_block_rotation, @@ -487,14 +382,19 @@ pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> { let right_panel = container(details).width(Fill).height(Fill); - let body = row![style_list, vsep(), right_panel].height(Fill); - - container(column![toolbar, hdivider(), body].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) - .width(Fill) - .height(Fill) - .into() + crate::ui::style_manager::view(crate::ui::style_manager::Scaffold { + kind: crate::app::StyleKind::MLeader, + styles: &v.styles, + selected: v.selected, + current: Some(v.current.as_str()), + rename_active: v.rename_active, + rename_buf: v.rename_buf, + on_new: Message::MLeaderStyleDialogNew, + on_copy: Message::MLeaderStyleDialogCopy, + on_delete: Message::MLeaderStyleDialogDelete, + on_select: Message::MLeaderStyleDialogSelect, + on_set_current: Message::MLeaderStyleDialogSetCurrent, + on_apply: Message::MLeaderStyleApply, + editor: right_panel.into(), + }) } diff --git a/src/ui/mlstyle.rs b/src/ui/mlstyle.rs index 164d80113..95b0df30e 100644 --- a/src/ui/mlstyle.rs +++ b/src/ui/mlstyle.rs @@ -1,211 +1,24 @@ //! Multiline Style Manager window — fills the entire OS window. use crate::app::Message; -use iced::widget::{button, column, container, row, scrollable, text, Space}; -use iced::{Background, Border, Color, Element, Fill, Theme}; +use iced::widget::{column, container, row, scrollable, text, Space}; +use iced::{Background, Color, Element, Fill, Theme}; -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 BORDER: Color = Color { - r: 0.35, - g: 0.35, - b: 0.35, - a: 1.0, -}; -const TEXT: 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 ACCENT: Color = Color { - r: 0.25, - g: 0.50, - b: 0.85, - a: 1.0, -}; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const LIST: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; - -fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - (true, _) => ACCENT, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT, - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - } -} - -fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (true, _) => ACTIVE, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - text_color: TEXT, - ..Default::default() - } -} - -fn hdivider<'a>() -> Element<'a, Message> { - container(Space::new().width(Fill).height(1)) - .width(Fill) - .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), - ..Default::default() - }) - .into() -} - -fn vsep<'a>() -> Element<'a, Message> { - container(Space::new().width(1).height(Fill)) - .width(1) - .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), - ..Default::default() - }) - .into() -} pub fn view_window<'a>( styles: Vec, selected: &'a str, selected_style: Option<&'a acadrust::objects::MLineStyle>, current_style: String, + rename_active: Option<&'a str>, + rename_buf: &'a str, ) -> Element<'a, Message> { - // ── Toolbar ─────────────────────────────────────────────────────────── - let toolbar = container( - row![ - button(text("Set Current").size(11)) - .on_press(Message::MlStyleDialogSetCurrent) - .style(btn_s(true)) - .padding([4, 10]), - button(text("New").size(11)) - .on_press(Message::MlStyleDialogNew) - .style(btn_s(false)) - .padding([4, 10]), - button(text("Delete").size(11)) - .on_press(Message::MlStyleDialogDelete) - .style(btn_s(false)) - .padding([4, 10]), - ] - .spacing(4) - .align_y(iced::Center), - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), - ..Default::default() - }) - .width(Fill) - .padding([5, 8]); - - // ── Left: Style list ────────────────────────────────────────────────── - let style_items: Vec> = styles - .iter() - .map(|name| { - let is_sel = name.as_str() == selected; - let is_cur = *name == current_style; - let label = if is_cur { - format!("{name} ◀") - } else { - name.clone() - }; - button(text(label).size(11)) - .on_press(Message::MlStyleDialogSelect(name.clone())) - .style(list_item(is_sel)) - .padding([4, 8]) - .width(Fill) - .into() - }) - .collect(); - - let style_list = container( - column![ - text("Styles").size(10).color(DIM), - container(scrollable(column(style_items).spacing(2)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), - border: Border { - color: BORDER, - width: 1.0, - radius: 3.0.into() - }, - ..Default::default() - }) - .width(Fill) - .height(Fill) - .padding(2), - ] - .spacing(4) - .height(Fill), - ) - .width(200) - .height(Fill) - .padding(iced::Padding { - top: 12.0, - right: 8.0, - bottom: 12.0, - left: 12.0, - }); - // ── Right: Details panel ────────────────────────────────────────────── let info_row = |label: &'static str, val: String| -> Element<'_, Message> { row![ @@ -266,14 +79,19 @@ pub fn view_window<'a>( let right_panel = container(details).width(Fill).height(Fill); - let body = row![style_list, vsep(), right_panel].height(Fill); - - container(column![toolbar, hdivider(), body].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) - .width(Fill) - .height(Fill) - .into() + crate::ui::style_manager::view(crate::ui::style_manager::Scaffold { + kind: crate::app::StyleKind::MLine, + styles: &styles, + selected, + current: Some(current_style.as_str()), + rename_active, + rename_buf, + on_new: Message::MlStyleDialogNew, + on_copy: Message::MlStyleDialogCopy, + on_delete: Message::MlStyleDialogDelete, + on_select: Message::MlStyleDialogSelect, + on_set_current: Message::MlStyleDialogSetCurrent, + on_apply: Message::MlStyleApply, + editor: right_panel.into(), + }) } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index c66a56a89..c1fe9fba9 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,11 +4,10 @@ pub const ROW_H: f32 = 26.0; pub mod about; pub mod app_menu; -pub mod isolate_popup; -pub mod update_notice; pub mod command_line; pub mod cycle_popup; pub mod dimstyle; +pub mod isolate_popup; pub mod layers; pub mod layout_manager; pub mod mleaderstyle; @@ -26,10 +25,13 @@ pub mod snap_popup; pub mod statusbar; pub mod statusbar_config; pub mod statusbar_menu; +pub mod style_list; +pub mod style_manager; pub mod tablestyle; pub mod text_util; pub mod textstyle; pub mod units_popup; +pub mod update_notice; pub use app_menu::AppMenu; pub use command_line::CommandLine; diff --git a/src/ui/style_list.rs b/src/ui/style_list.rs new file mode 100644 index 000000000..5518e2bf6 --- /dev/null +++ b/src/ui/style_list.rs @@ -0,0 +1,43 @@ +//! Shared style-manager list row. +//! +//! Every style manager renders the same left-hand list of style names where a +//! single click selects and a double click renames inline. Only the row's +//! button style differs per manager, so it is passed in. + +use crate::app::{Message, StyleKind}; +use iced::widget::button::{Status, Style}; +use iced::widget::{button, mouse_area, text, text_input}; +use iced::{Element, Fill, Theme}; + +/// One row of the style list. Renders an editable `text_input` when `name` is +/// the style being renamed (`rename_active`), otherwise a selectable button +/// wrapped in a `mouse_area` whose double click starts the rename. +pub fn item<'a>( + name: &str, + label: String, + kind: StyleKind, + on_select: Message, + rename_active: Option<&str>, + rename_buf: &'a str, + style: impl Fn(&Theme, Status) -> Style + 'a, +) -> Element<'a, Message> { + if rename_active == Some(name) { + text_input("", rename_buf) + .on_input(Message::StyleRenameEdit) + .on_submit(Message::StyleRenameCommit(kind)) + .size(11) + .padding([4, 8]) + .width(Fill) + .into() + } else { + mouse_area( + button(text(label).size(11)) + .on_press(on_select) + .style(style) + .padding([4, 8]) + .width(Fill), + ) + .on_double_click(Message::StyleRenameStart(kind, name.to_string())) + .into() + } +} diff --git a/src/ui/style_manager.rs b/src/ui/style_manager.rs new file mode 100644 index 000000000..5fbecde02 --- /dev/null +++ b/src/ui/style_manager.rs @@ -0,0 +1,262 @@ +//! Shared scaffold for every style-manager window. +//! +//! All five managers (text / dimension / table / multileader / multiline) +//! share the same frame: a top toolbar (New / Copy / Delete on the left, +//! manager-specific actions such as Set Current / Apply on the right), a style +//! list on the left, and a property editor on the right. Only the editor +//! differs, so each manager builds just that and hands it to [`view`]; the +//! toolbar, list, inline-rename wiring and chrome live here once. + +use crate::app::{Message, StyleKind}; +use iced::widget::button::{Status, Style}; +use iced::widget::{button, column, container, row, scrollable, text, Space}; +use iced::{Background, Border, Color, Element, Fill, Theme}; + +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 BORDER: Color = Color { + r: 0.35, + g: 0.35, + b: 0.35, + a: 1.0, +}; +const TEXT: 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 ACCENT: Color = Color { + r: 0.25, + g: 0.50, + b: 0.85, + a: 1.0, +}; +const ACTIVE: Color = Color { + r: 0.20, + g: 0.40, + b: 0.70, + a: 1.0, +}; +const LIST: Color = Color { + r: 0.12, + g: 0.12, + b: 0.12, + a: 1.0, +}; + +/// Everything the shared frame needs. The per-manager `editor` element is the +/// only bespoke part. +/// +/// The toolbar is uniform across every manager: New / Copy / Delete on the +/// left, then **Set Current** and **Apply** on the right. Each manager just +/// supplies the two messages, so the right side can never drift or go missing. +/// +/// Two lifetimes: `'a` is what the returned element keeps alive (the editor and +/// the rename buffer the inline `text_input` borrows); `'b` is the transient +/// list data (`styles`, `selected`, …) that the frame only reads while building +/// rows, so callers may pass a locally-built `Vec`. +pub struct Scaffold<'a, 'b> { + pub kind: StyleKind, + pub styles: &'b [String], + pub selected: &'b str, + /// Current style for this manager, marked with a ◀ in the list. `None` + /// when the manager has no "current" concept. + pub current: Option<&'b str>, + pub rename_active: Option<&'b str>, + pub rename_buf: &'a str, + pub on_new: Message, + pub on_copy: Message, + pub on_delete: Message, + /// Tuple-variant constructor for the per-row select message + /// (e.g. `Message::TextStyleDialogSelect`). + pub on_select: fn(String) -> Message, + /// "Set Current" action (right side). Every manager has one. + pub on_set_current: Message, + /// "Apply" action (right side, primary). Every manager has one. + pub on_apply: Message, + pub editor: Element<'a, Message>, +} + +pub fn view<'a, 'b>(s: Scaffold<'a, 'b>) -> Element<'a, Message> { + // ── Toolbar: New / Copy / Delete | … | Set Current / Apply ──────────── + let bar = row![ + tb_button("New", s.on_new, false), + tb_button("Copy", s.on_copy, false), + tb_button("Delete", s.on_delete, false), + Space::new().width(Fill), + tb_button("Set Current", s.on_set_current, false), + tb_button("Apply", s.on_apply, true), + ] + .spacing(4) + .align_y(iced::Center); + let toolbar = container(bar) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(TB)), + ..Default::default() + }) + .width(Fill) + .padding([5, 8]); + + // ── Left: style list (single click selects, double click renames) ───── + let rows: Vec> = s + .styles + .iter() + .map(|name| { + let is_sel = name.as_str() == s.selected; + let label = if s.current == Some(name.as_str()) { + format!("{name} ◀") + } else { + name.clone() + }; + crate::ui::style_list::item( + name, + label, + s.kind, + (s.on_select)(name.clone()), + s.rename_active, + s.rename_buf, + list_item(is_sel), + ) + }) + .collect(); + + let list_panel = container( + column![ + text("Styles").size(10).color(DIM), + container(scrollable(column(rows).spacing(1)).height(Fill)) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(LIST)), + border: Border { + color: BORDER, + width: 1.0, + radius: 3.0.into() + }, + ..Default::default() + }) + .width(Fill) + .height(Fill) + .padding(2), + ] + .spacing(4) + .height(Fill), + ) + .width(170) + .height(Fill) + .padding(iced::Padding { + top: 12.0, + right: 8.0, + bottom: 12.0, + left: 12.0, + }); + + let body = row![list_panel, vsep(), s.editor].height(Fill); + + container(column![toolbar, hdivider(), body]) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BG)), + ..Default::default() + }) + .width(Fill) + .height(Fill) + .into() +} + +// ── Shared chrome ────────────────────────────────────────────────────────── + +fn tb_button<'a>(label: &'a str, msg: Message, accent: bool) -> Element<'a, Message> { + let pad = if accent { [4, 14] } else { [4, 10] }; + button(text(label).size(11)) + .on_press(msg) + .style(btn_s(accent)) + .padding(pad) + .into() +} + +fn btn_s(accent: bool) -> impl Fn(&Theme, Status) -> Style { + move |_: &Theme, st| Style { + background: Some(Background::Color(match (accent, st) { + (true, Status::Hovered | Status::Pressed) => Color { + r: 0.20, + g: 0.42, + b: 0.72, + a: 1.0, + }, + (false, Status::Hovered | Status::Pressed) => Color { + r: 0.28, + g: 0.28, + b: 0.28, + a: 1.0, + }, + (true, _) => ACCENT, + _ => Color { + r: 0.22, + g: 0.22, + b: 0.22, + a: 1.0, + }, + })), + text_color: TEXT, + border: Border { + color: BORDER, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + } +} + +fn list_item(active: bool) -> impl Fn(&Theme, Status) -> Style { + move |_: &Theme, st| Style { + background: Some(Background::Color(match (active, st) { + (true, _) => ACTIVE, + (false, Status::Hovered | Status::Pressed) => Color { + r: 0.26, + g: 0.26, + b: 0.26, + a: 1.0, + }, + _ => Color::TRANSPARENT, + })), + text_color: TEXT, + ..Default::default() + } +} + +fn hdivider<'a>() -> Element<'a, Message> { + container(Space::new().width(Fill).height(1)) + .width(Fill) + .height(1) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BORDER)), + ..Default::default() + }) + .into() +} + +fn vsep<'a>() -> Element<'a, Message> { + container(Space::new().width(1).height(Fill)) + .width(1) + .height(Fill) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BORDER)), + ..Default::default() + }) + .into() +} diff --git a/src/ui/tablestyle.rs b/src/ui/tablestyle.rs index 5a478f236..ed570fd3b 100644 --- a/src/ui/tablestyle.rs +++ b/src/ui/tablestyle.rs @@ -7,18 +7,6 @@ use iced::widget::{ }; use iced::{Background, Border, Color, Element, Fill, Theme}; -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 BORDER: Color = Color { r: 0.35, g: 0.35, @@ -43,18 +31,6 @@ const ACCENT: Color = Color { b: 0.85, a: 1.0, }; -const ACTIVE: Color = Color { - r: 0.20, - g: 0.40, - b: 0.70, - a: 1.0, -}; -const LIST: Color = Color { - r: 0.12, - g: 0.12, - b: 0.12, - a: 1.0, -}; fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { move |_: &Theme, st| button::Style { @@ -89,45 +65,6 @@ fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { } } -fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (true, _) => ACTIVE, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.26, - g: 0.26, - b: 0.26, - a: 1.0, - }, - _ => Color::TRANSPARENT, - })), - text_color: TEXT, - ..Default::default() - } -} - -fn hdivider<'a>() -> Element<'a, Message> { - container(Space::new().width(Fill).height(1)) - .width(Fill) - .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), - ..Default::default() - }) - .into() -} - -fn vsep<'a>() -> Element<'a, Message> { - container(Space::new().width(1).height(Fill)) - .width(1) - .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), - ..Default::default() - }) - .into() -} - pub fn view_window<'a>( styles: Vec, selected: &'a str, @@ -145,72 +82,9 @@ pub fn view_window<'a>( border_lw: &'a [[String; 6]; 3], border_color: &'a [[String; 6]; 3], border_spacing: &'a [[String; 6]; 3], + rename_active: Option<&'a str>, + rename_buf: &'a str, ) -> Element<'a, Message> { - // ── Toolbar ─────────────────────────────────────────────────────────── - let toolbar = container( - row![ - button(text("New").size(11)) - .on_press(Message::TableStyleDialogNew) - .style(btn_s(true)) - .padding([4, 10]), - button(text("Delete").size(11)) - .on_press(Message::TableStyleDialogDelete) - .style(btn_s(false)) - .padding([4, 10]), - ] - .spacing(4) - .align_y(iced::Center), - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), - ..Default::default() - }) - .width(Fill) - .padding([5, 8]); - - // ── Left: Style list ────────────────────────────────────────────────── - let style_items: Vec> = styles - .iter() - .map(|name| { - let is_sel = name.as_str() == selected; - button(text(name.clone()).size(11)) - .on_press(Message::TableStyleDialogSelect(name.clone())) - .style(list_item(is_sel)) - .padding([4, 8]) - .width(Fill) - .into() - }) - .collect(); - - let style_list = container( - column![ - text("Styles").size(10).color(DIM), - container(scrollable(column(style_items).spacing(2)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), - border: Border { - color: BORDER, - width: 1.0, - radius: 3.0.into() - }, - ..Default::default() - }) - .width(Fill) - .height(Fill) - .padding(2), - ] - .spacing(4) - .height(Fill), - ) - .width(200) - .height(Fill) - .padding(iced::Padding { - top: 12.0, - right: 8.0, - bottom: 12.0, - left: 12.0, - }); - // ── Right: Details panel ────────────────────────────────────────────── let info_row = |label: &'static str, val: String| -> Element<'_, Message> { row![ @@ -235,7 +109,11 @@ pub fn view_window<'a>( row![ text(label).size(11).color(DIM).width(150), text_input(placeholder, value) - .on_input(move |v| Message::TableStyleCellEdit { row, field, value: v }) + .on_input(move |v| Message::TableStyleCellEdit { + row, + field, + value: v + }) .size(11) .width(100), ] @@ -246,10 +124,25 @@ pub fn view_window<'a>( let mut col = Column::new() .spacing(3) .push(text(row_label).size(11).color(ACCENT)) - .push(cell_in(" Text style:", "Standard", &cell_textstyle[r], "textstyle")) + .push(cell_in( + " Text style:", + "Standard", + &cell_textstyle[r], + "textstyle", + )) .push(cell_in(" Text height:", "0.18", &cell_height[r], "height")) - .push(cell_in(" Text color (ACI):", "256", &cell_textcolor[r], "textcolor")) - .push(cell_in(" Fill color (ACI):", "256", &cell_fillcolor[r], "fillcolor")) + .push(cell_in( + " Text color (ACI):", + "256", + &cell_textcolor[r], + "textcolor", + )) + .push(cell_in( + " Fill color (ACI):", + "256", + &cell_fillcolor[r], + "fillcolor", + )) .push( row![ text(" Alignment:").size(11).color(DIM).width(150), @@ -287,7 +180,11 @@ pub fn view_window<'a>( .push(cell_in(" Data type:", "0", &cell_datatype[r], "datatype")) .push(cell_in(" Unit type:", "0", &cell_unittype[r], "unittype")) .push(cell_in(" Format string:", "", &cell_format[r], "format")) - .push(text(" Borders (type / weight / color / spacing / hidden)").size(10).color(DIM)); + .push( + text(" Borders (type / weight / color / spacing / hidden)") + .size(10) + .color(DIM), + ); let borders: [(&'static str, &acadrust::objects::TableCellBorder); 6] = [ ("L", &rs.left_border), @@ -303,26 +200,51 @@ pub fn view_window<'a>( row![ text(format!(" {bname}")).size(11).color(DIM).width(28), pick_list( - ["Single", "Double"].iter().map(|s| s.to_string()).collect::>(), + ["Single", "Double"] + .iter() + .map(|s| s.to_string()) + .collect::>(), Some(format!("{:?}", bd.border_type)), - move |value| Message::TableStyleBorderSetType { cell: row, border: bu, value }, + move |value| Message::TableStyleBorderSetType { + cell: row, + border: bu, + value + }, ) .text_size(10) .width(74), text_input("wt", &border_lw[r][b]) - .on_input(move |v| Message::TableStyleBorderEdit { cell: row, border: bu, field: "lw", value: v }) + .on_input(move |v| Message::TableStyleBorderEdit { + cell: row, + border: bu, + field: "lw", + value: v + }) .size(10) .width(46), text_input("clr", &border_color[r][b]) - .on_input(move |v| Message::TableStyleBorderEdit { cell: row, border: bu, field: "color", value: v }) + .on_input(move |v| Message::TableStyleBorderEdit { + cell: row, + border: bu, + field: "color", + value: v + }) .size(10) .width(46), text_input("gap", &border_spacing[r][b]) - .on_input(move |v| Message::TableStyleBorderEdit { cell: row, border: bu, field: "spacing", value: v }) + .on_input(move |v| Message::TableStyleBorderEdit { + cell: row, + border: bu, + field: "spacing", + value: v + }) .size(10) .width(46), checkbox(bd.is_invisible) - .on_toggle(move |_| Message::TableStyleBorderToggleInvisible { cell: row, border: bu }) + .on_toggle(move |_| Message::TableStyleBorderToggleInvisible { + cell: row, + border: bu + }) .size(13), ] .spacing(5) @@ -346,7 +268,10 @@ pub fn view_window<'a>( row![ text("Description:").size(11).color(DIM).width(160), text_input("", description_buf) - .on_input(|v| Message::TableStyleEdit { field: "description", value: v }) + .on_input(|v| Message::TableStyleEdit { + field: "description", + value: v + }) .size(11) .width(160), ] @@ -355,7 +280,10 @@ pub fn view_window<'a>( row![ text("Flow direction:").size(11).color(DIM).width(160), pick_list( - ["Down", "Up"].iter().map(|s| s.to_string()).collect::>(), + ["Down", "Up"] + .iter() + .map(|s| s.to_string()) + .collect::>(), Some(format!("{:?}", s.flow_direction)), Message::TableStyleSetFlow, ) @@ -372,7 +300,10 @@ pub fn view_window<'a>( row![ text("H Margin:").size(11).color(DIM).width(160), text_input("1.5", hmargin_buf) - .on_input(|v| Message::TableStyleEdit { field: "hmargin", value: v }) + .on_input(|v| Message::TableStyleEdit { + field: "hmargin", + value: v + }) .size(11) .width(100), ] @@ -381,7 +312,10 @@ pub fn view_window<'a>( row![ text("V Margin:").size(11).color(DIM).width(160), text_input("1.5", vmargin_buf) - .on_input(|v| Message::TableStyleEdit { field: "vmargin", value: v }) + .on_input(|v| Message::TableStyleEdit { + field: "vmargin", + value: v + }) .size(11) .width(100), ] @@ -419,14 +353,19 @@ pub fn view_window<'a>( let right_panel = container(details).width(Fill).height(Fill); - let body = row![style_list, vsep(), right_panel].height(Fill); - - container(column![toolbar, hdivider(), body].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) - .width(Fill) - .height(Fill) - .into() + crate::ui::style_manager::view(crate::ui::style_manager::Scaffold { + kind: crate::app::StyleKind::Table, + styles: &styles, + selected, + current: None, + rename_active, + rename_buf, + on_new: Message::TableStyleDialogNew, + on_copy: Message::TableStyleDialogCopy, + on_delete: Message::TableStyleDialogDelete, + on_select: Message::TableStyleDialogSelect, + on_set_current: Message::TableStyleDialogSetCurrent, + on_apply: Message::TableStyleApply, + editor: right_panel.into(), + }) } diff --git a/src/ui/textstyle.rs b/src/ui/textstyle.rs index 83c49fed8..bbed79770 100644 --- a/src/ui/textstyle.rs +++ b/src/ui/textstyle.rs @@ -1,6 +1,7 @@ //! Text Style Font Browser window — fills the entire OS window. use crate::app::Message; +use crate::app::StyleKind; use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, Space}; use iced::{Background, Border, Color, Element, Fill, Theme}; @@ -17,20 +18,12 @@ pub struct TextStyleView<'a> { pub backward: bool, pub upside_down: bool, pub annotative: bool, + /// Name of the style being renamed inline (double-clicked), if any. + pub rename_active: Option<&'a str>, + /// Edit buffer for the inline rename text input. + pub rename_buf: &'a str, } -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 BORDER: Color = Color { r: 0.35, g: 0.35, @@ -75,61 +68,11 @@ const LIST: Color = Color { }; const BUILTIN_FONTS: &[&str] = &[ - "Standard", - "ISO", - "Simplex", - "RomanS", - "RomanD", - "RomanC", - "RomanT", - "ItalicC", - "ItalicT", - "ScriptS", - "ScriptC", - "GothGBT", - "GothGRT", - "GothITT", - "Cursive", - "GreekC", - "Symbol", - "ISO", - "ISO3098", - "Unicode", + "Standard", "ISO", "Simplex", "RomanS", "RomanD", "RomanC", "RomanT", "ItalicC", "ItalicT", + "ScriptS", "ScriptC", "GothGBT", "GothGRT", "GothITT", "Cursive", "GreekC", "Symbol", "ISO", + "ISO3098", "Unicode", ]; -fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { - move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (accent, st) { - (true, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.20, - g: 0.42, - b: 0.72, - a: 1.0, - }, - (false, button::Status::Hovered | button::Status::Pressed) => Color { - r: 0.28, - g: 0.28, - b: 0.28, - a: 1.0, - }, - (true, _) => ACCENT, - _ => Color { - r: 0.22, - g: 0.22, - b: 0.22, - a: 1.0, - }, - })), - text_color: TEXT, - border: Border { - color: BORDER, - width: 1.0, - radius: 4.0.into(), - }, - ..Default::default() - } -} - fn list_item(active: bool) -> impl Fn(&Theme, button::Status) -> button::Style { move |_: &Theme, st| button::Style { background: Some(Background::Color(match (active, st) { @@ -162,17 +105,6 @@ fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { } } -fn hdivider<'a>() -> Element<'a, Message> { - container(Space::new().width(Fill).height(1)) - .width(Fill) - .height(1) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER)), - ..Default::default() - }) - .into() -} - fn vsep<'a>() -> Element<'a, Message> { container(Space::new().width(1).height(Fill)) .width(1) @@ -197,81 +129,9 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> { backward, upside_down, annotative, + rename_active, + rename_buf, } = v; - // ── Toolbar ─────────────────────────────────────────────────────────── - let toolbar = container( - row![ - button(text("New").size(11)) - .on_press(Message::TextStyleDialogNew) - .style(btn_s(false)) - .padding([4, 10]), - button(text("Delete").size(11)) - .on_press(Message::TextStyleDialogDelete) - .style(btn_s(false)) - .padding([4, 10]), - Space::new().width(Fill), - button(text("Set Current").size(11)) - .on_press(Message::TextStyleDialogSetCurrent) - .style(btn_s(false)) - .padding([4, 10]), - button(text("Apply").size(11)) - .on_press(Message::TextStyleApply) - .style(btn_s(true)) - .padding([4, 14]), - ] - .spacing(4) - .align_y(iced::Center), - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(TB)), - ..Default::default() - }) - .width(Fill) - .padding([5, 8]); - - // ── Left: Style list ────────────────────────────────────────────────── - let style_items: Vec> = styles - .iter() - .map(|name| { - let is_sel = name.as_str() == selected; - button(text(name.clone()).size(11)) - .on_press(Message::TextStyleDialogSelect(name.clone())) - .style(list_item(is_sel)) - .padding([4, 8]) - .width(Fill) - .into() - }) - .collect(); - - let style_panel = container( - column![ - text("Styles").size(10).color(DIM), - container(scrollable(column(style_items).spacing(1)).height(Fill)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(LIST)), - border: Border { - color: BORDER, - width: 1.0, - radius: 3.0.into() - }, - ..Default::default() - }) - .width(Fill) - .height(Fill) - .padding(2), - ] - .spacing(4) - .height(Fill), - ) - .width(170) - .height(Fill) - .padding(iced::Padding { - top: 12.0, - right: 8.0, - bottom: 12.0, - left: 12.0, - }); - // ── Middle: Font browser ────────────────────────────────────────────── let font_items: Vec> = BUILTIN_FONTS .iter() @@ -319,7 +179,12 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> { .padding([12, 8]); // Labeled numeric/text field row → TextStyleEdit { field, value }. - fn frow<'a>(label: &'a str, ph: &'a str, buf: &'a str, field: &'static str) -> Element<'a, Message> { + fn frow<'a>( + label: &'a str, + ph: &'a str, + buf: &'a str, + field: &'static str, + ) -> Element<'a, Message> { row![ text(label).size(11).color(DIM).width(120), text_input(ph, buf) @@ -387,14 +252,21 @@ pub fn view_window<'a>(v: TextStyleView<'a>) -> Element<'a, Message> { left: 8.0, }); - let body = row![style_panel, vsep(), font_panel, vsep(), props_panel].height(Fill); + let editor = row![font_panel, vsep(), props_panel].height(Fill); - container(column![toolbar, hdivider(), body].spacing(0)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BG)), - ..Default::default() - }) - .width(Fill) - .height(Fill) - .into() + crate::ui::style_manager::view(crate::ui::style_manager::Scaffold { + kind: StyleKind::Text, + styles: &styles, + selected, + current: None, + rename_active, + rename_buf, + on_new: Message::TextStyleDialogNew, + on_copy: Message::TextStyleDialogCopy, + on_delete: Message::TextStyleDialogDelete, + on_select: Message::TextStyleDialogSelect, + on_set_current: Message::TextStyleDialogSetCurrent, + on_apply: Message::TextStyleApply, + editor: editor.into(), + }) }