diff --git a/src/app/automation.rs b/src/app/automation.rs index 6e8a6ec7d..f9591943d 100644 --- a/src/app/automation.rs +++ b/src/app/automation.rs @@ -224,6 +224,9 @@ impl OpenCADStudio { Ok(doc) => { let i = self.active_tab; self.tabs[i].scene.document = doc; + crate::app::style_ops::ensure_standard_styles( + &mut self.tabs[i].scene.document, + ); self.tabs[i].adopt_active_ucs_from_header(); self.tabs[i].current_path = Some(PathBuf::from(path)); self.tabs[i].is_start = false; diff --git a/src/app/style_ops.rs b/src/app/style_ops.rs index 05b93fa59..87c529af4 100644 --- a/src/app/style_ops.rs +++ b/src/app/style_ops.rs @@ -22,6 +22,62 @@ use acadrust::objects::{MLineStyle, MultiLeaderStyle, ObjectType, TableStyle}; use acadrust::tables::{DimStyle, TextStyle}; use acadrust::types::Handle; +/// Guarantee the built-in "Standard" style of every kind exists in `doc` — +/// a foreign or damaged file saved without them leaves the style dropdowns +/// broken with no way to recover, and new text/dimensions have nothing to +/// reference (#366). Missing entries are re-seeded with the app defaults. +/// Called on every file open; a no-op for healthy documents. +pub(crate) fn ensure_standard_styles(doc: &mut acadrust::CadDocument) { + if !doc + .text_styles + .iter() + .any(|s| s.name.eq_ignore_ascii_case("Standard")) + { + let mut s = TextStyle::new("Standard"); + s.handle = doc.allocate_handle(); + let _ = doc.text_styles.add(s); + } + if !doc + .dim_styles + .iter() + .any(|s| s.name.eq_ignore_ascii_case("Standard")) + { + let mut s = DimStyle::new("Standard"); + s.handle = doc.allocate_handle(); + let _ = doc.dim_styles.add(s); + } + let has = |doc: &acadrust::CadDocument, pred: fn(&ObjectType) -> Option<&str>| { + doc.objects + .values() + .filter_map(pred) + .any(|n| n.eq_ignore_ascii_case("Standard")) + }; + if !has(doc, |o| match o { + ObjectType::TableStyle(s) => Some(&s.name), + _ => None, + }) { + let mut s = TableStyle::standard(); + s.handle = doc.allocate_handle(); + doc.objects.insert(s.handle, ObjectType::TableStyle(s)); + } + if !has(doc, |o| match o { + ObjectType::MultiLeaderStyle(s) => Some(&s.name), + _ => None, + }) { + let mut s = MultiLeaderStyle::standard(); + s.handle = doc.allocate_handle(); + doc.objects.insert(s.handle, ObjectType::MultiLeaderStyle(s)); + } + if !has(doc, |o| match o { + ObjectType::MLineStyle(s) => Some(&s.name), + _ => None, + }) { + let mut s = MLineStyle::standard(); + s.handle = doc.allocate_handle(); + doc.objects.insert(s.handle, ObjectType::MLineStyle(s)); + } +} + /// 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)] diff --git a/src/app/update/file.rs b/src/app/update/file.rs index af6d1d268..75b437787 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -435,6 +435,11 @@ pub(super) fn on_open_file(&mut self) -> Task { self.tabs[i].current_path = Some(path.clone()); self.tabs[i].scene.document = doc; + // A file saved without the built-in Standard styles (foreign + // or damaged) gets them re-seeded so nothing dangles (#366). + crate::app::style_ops::ensure_standard_styles( + &mut self.tabs[i].scene.document, + ); // Follow the file's saved current UCS from the moment it opens. self.tabs[i].adopt_active_ucs_from_header(); // Adopt the drawing's own Ortho ($ORTHOMODE) and running OSNAP diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index dbd865e5d..965fbab36 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1547,6 +1547,9 @@ impl OpenCADStudio { Some(content) => { crate::ui::modal::modal( composed, + self.modal_title(), + // Content width — outer size minus the frame padding. + self.modal_outer_size().map(|s| s.0 - 20.0).unwrap_or(420.0), content, Message::CloseModal, self.modal_offset, @@ -1560,6 +1563,8 @@ impl OpenCADStudio { if self.color_pick_target.is_some() { crate::ui::modal::modal( base, + "Select Color", + 420.0, iced::widget::container(crate::ui::color_select::color_grid_window( Message::ColorWindowPick, )) diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 051b7a5b6..190539528 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -3,6 +3,38 @@ use iced::widget::{button, column, container, pick_list, row, text, Space}; use iced::{Background, Border, Color, Element, Fill, Theme}; impl OpenCADStudio { + /// Title shown in the active modal's title bar, left of the move/close + /// buttons. Keep in sync with the [`Self::modal_content`] dispatch. + pub(super) fn modal_title(&self) -> &'static str { + use super::super::ModalKind as K; + match self.active_modal { + Some(K::About) => "About", + Some(K::Shortcuts) => "Keyboard Shortcuts", + Some(K::Aliases) => "Command Aliases", + Some(K::PluginManager) => "Plugin Manager", + Some(K::UpdateNotice) => "Update Available", + Some(K::Layers) => "Layer Manager", + Some(K::Plot) => "Plot", + Some(K::LayoutManager) => "Layout Manager", + Some(K::ScaleManager) => "Scale Manager", + Some(K::AnnoObjectScale) => "Annotation Object Scale", + Some(K::Plotstyle) => "Plot Style Editor", + Some(K::TextStyle) => "Text Style Manager", + Some(K::MlStyle) => "Multiline Style Manager", + Some(K::TableStyle) => "Table Style Manager", + Some(K::MLeaderStyle) => "Multileader Style Manager", + Some(K::DimStyle) => "Dimension Style Manager", + Some(K::AssocPrompt) => "Default Application", + Some(K::AecDropWarning) => "Save Warning", + Some(K::LayerDeleteWarning) => "Delete Layer", + Some(K::Unsaved) => "Unsaved Changes", + Some(K::PointStyle) => "Point Style", + Some(K::AttributeEditor) => "Attribute Editor", + Some(K::SaveDialog) => "Save Drawing As", + None => "", + } + } + /// Build the currently-open modal dialog's content (Plan B), or `None`. /// Each former pop-up window is constructed here and given a bounded size /// (About shrinks to its content). Rendered as an overlay by `view_main`. diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index c50419fd4..c5452d13f 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -702,6 +702,8 @@ pub(super) fn mtext_editor_overlay<'a>( // transparent fill lets the dimmed viewport show through beneath. crate::ui::modal::modal( iced::widget::Space::new().width(Fill).height(Fill), + "Text Editor", + 660.0 + modal_resize.x, content, Message::MTextCancel, modal_offset, diff --git a/src/ui/modal.rs b/src/ui/modal.rs index 7b75be02f..5b30ab83d 100644 --- a/src/ui/modal.rs +++ b/src/ui/modal.rs @@ -45,6 +45,8 @@ const GRIP_C: Color = Color { /// title bar; pass `Vector::ZERO` to keep it centred. pub fn modal<'a>( base: impl Into>, + title: &'a str, + title_width: f32, content: impl Into>, on_close: Message, offset: Vector, @@ -83,7 +85,28 @@ pub fn modal<'a>( .on_press(Message::ModalGrab) .interaction(iced::mouse::Interaction::Grab); - let title_bar = row![grip, close].spacing(6).align_y(iced::Center); + // The dialog name is centred across the dialog width with the grip + ✕ + // overlaid at the right edge. The bar takes an explicit `title_width` + // (the caller's content width) instead of `Fill` — a Fill child inside + // the Shrink frame would blow the dialog out to the full screen. + let title_text = iced::widget::text(title).size(15).color(Color { + r: 0.88, + g: 0.88, + b: 0.88, + a: 1.0, + }); + let title_bar = stack![ + container(title_text) + .width(Length::Fixed(title_width)) + .height(Length::Fixed(24.0)) + .align_x(iced::alignment::Horizontal::Center) + .align_y(iced::alignment::Vertical::Center), + container(row![grip, close].spacing(6).align_y(iced::Center)) + .width(Length::Fixed(title_width)) + .height(Length::Fixed(24.0)) + .align_x(iced::alignment::Horizontal::Right) + .align_y(iced::alignment::Vertical::Center), + ]; let panel_style = |_: &Theme| container::Style { background: Some(Background::Color(PANEL)),