diff --git a/src/app/mod.rs b/src/app/mod.rs index 8696539a4..5b699c98e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -614,6 +614,9 @@ pub(super) struct OpenCADStudio { /// The open in-canvas modal dialog, if any (Plan B: shared overlay instead /// of OS windows). active_modal: Option, + /// Plot modal geometry preserved while the Plot Style editor is open as + /// a child dialog. None means Plotstyle was opened directly (e.g. command). + plotstyle_parent_plot_geometry: Option<(iced::Vector, iced::Vector)>, /// FIND dialog inputs and current result cursor. find_replace: FindReplaceState, /// Set once the user acknowledges the AEC-drop warning, so re-entering the @@ -1187,6 +1190,8 @@ pub enum ColorPickTarget { DimStyle(DsField), MLeader(&'static str), Table(u8, &'static str), + /// Plot-style colour override for the currently selected ACI entry. + PlotStyle, /// Selected entities' colour (left properties panel). Properties, /// Selected entities' background colour (hatch / MTEXT background row). @@ -2741,6 +2746,7 @@ pub enum Message { /// Edit buffers changed. PlotStylePanelColorBuf(String), PlotStylePanelLwBuf(String), + PlotStylePanelLwSet(u8), PlotStylePanelScreenBuf(String), /// Apply current edit buffers to the selected ACI entry. PlotStylePanelApply, @@ -3153,6 +3159,7 @@ impl OpenCADStudio { color_picker_tab: ColorPickerTab::Index, recent_colors: Vec::new(), active_modal: None, + plotstyle_parent_plot_geometry: None, find_replace: FindReplaceState::default(), aec_drop_acknowledged: false, aec_drop_count: 0, diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 08258172d..5c437aa1a 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -4125,8 +4125,7 @@ pub(super) fn on_open_file(&mut self) -> Task { if let Ok(sc) = self.ps_screening_buf.trim().parse::() { entry.screening = sc.min(100); } - self.command_line - .push_output(crate::tf!("Plot style ACI {aci} updated.").as_ref()); + } } else { // No table loaded: create an identity table and apply. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index fbdc58d73..2c0adb8ed 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -125,6 +125,22 @@ impl OpenCADStudio { /// changes, and the ribbon tool that launched the dialog is de-highlighted. fn close_active_modal(&mut self) { use super::ModalKind::*; + // Plot Style opened from PLOT behaves as a child modal. + // Closing it restores the parent Plot dialog instead of returning + // to the drawing. + if self.active_modal == Some(Plotstyle) { + if let Some((plot_offset, plot_resize)) = + self.plotstyle_parent_plot_geometry.take() + { + self.active_modal = Some(Plot); + + self.reset_modal_geometry(); + self.modal_offset = plot_offset; + self.modal_resize = plot_resize; + + return; + } + } if self.active_modal == Some(Plot) && self.print_all_options { if let Some(previous) = self.print_all_options_prev.take() { self.plot_dialog = previous; @@ -6529,6 +6545,19 @@ impl OpenCADStudio { self.ps_screening_buf = entry .map(|e| e.screening.to_string()) .unwrap_or("100".into()); + // When launched from PLOT, preserve the parent dialog so the Plot Style + // editor can appear above it instead of replacing it. + if self.active_modal == Some(super::ModalKind::Plot) { + self.plotstyle_parent_plot_geometry = + Some((self.modal_offset, self.modal_resize)); + + // The child editor starts with its own centred geometry. + self.reset_modal_geometry(); + } else { + // Direct command launch: Plotstyle is a normal standalone modal. + self.plotstyle_parent_plot_geometry = None; + } + self.active_modal = Some(super::ModalKind::Plotstyle); Task::none() } @@ -6558,15 +6587,21 @@ impl OpenCADStudio { } Message::PlotStylePanelColorBuf(s) => { self.ps_color_buf = s; - Task::none() + self.on_plot_style_panel_apply() } + Message::PlotStylePanelLwBuf(s) => { self.ps_lineweight_buf = s; - Task::none() + self.on_plot_style_panel_apply() } + Message::PlotStylePanelLwSet(index) => { + self.ps_lineweight_buf = index.to_string(); + self.on_plot_style_panel_apply() + } + Message::PlotStylePanelScreenBuf(s) => { self.ps_screening_buf = s; - Task::none() + self.on_plot_style_panel_apply() } Message::PlotStylePanelApply => self.on_plot_style_panel_apply(), diff --git a/src/app/update/style.rs b/src/app/update/style.rs index 0274b140e..2a8fd1153 100644 --- a/src/app/update/style.rs +++ b/src/app/update/style.rs @@ -1115,6 +1115,26 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { } pub(super) fn on_color_window_pick(&mut self, color: acadrust::types::Color) -> Task { + if matches!( + self.color_pick_target.as_ref().map(|(target, _)| target), + Some(crate::app::ColorPickTarget::PlotStyle) + ) { + self.color_pick_target = None; + + let rgb = match color { + acadrust::types::Color::Rgb { r, g, b } => Some((r, g, b)), + acadrust::types::Color::Index(index) => { + acadrust::types::aci_table::aci_to_rgb(index) + } + _ => None, + }; + + if let Some((r, g, b)) = rgb { + self.ps_color_buf = format!("#{r:02X}{g:02X}{b:02X}"); + } + + return self.on_plot_style_panel_apply(); + } let s = crate::ui::color_select::color_to_aci_string(color); let edit = match self.color_pick_target.take().map(|(target, _)| target) { Some(crate::app::ColorPickTarget::DimStyle(f)) => Some(Message::DsEdit(f, s)), @@ -1147,6 +1167,7 @@ pub(super) fn on_text_style_dialog_open(&mut self) -> Task { Some(crate::app::ColorPickTarget::LayerState(idx)) => { Some(Message::LayerStateEditorLayerColor(idx, color)) } + Some(crate::app::ColorPickTarget::PlotStyle) => None, None => None, }; if let Some(m) = edit { diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 676b91586..d4ed02dd3 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1829,6 +1829,30 @@ impl OpenCADStudio { open_progress_layer, ]; + // If the Plot Style editor was launched from PLOT, keep the Plot dialog + // rendered underneath as its blocked parent. + let modal_underlay: Element<'_, Message> = + if self.active_modal == Some(super::ModalKind::Plotstyle) { + if let Some((plot_offset, plot_resize)) = + self.plotstyle_parent_plot_geometry.as_ref() + { + let plot_content = self.plot_modal_content(*plot_resize); + + crate::ui::modal::modal( + composed, + crate::tr!("modal", "plot"), + plot_content, + Message::CloseModal, + *plot_offset, + crate::ui::modal::ModalOptions::STANDARD, + ) + } else { + composed.into() + } + } else { + composed.into() + }; + // ── In-canvas modal dialogs (Plan B) ─────────────────────────────── // Former pop-up windows render as overlays here, so they work on both // the native (single main window) and web builds. @@ -1842,7 +1866,7 @@ impl OpenCADStudio { crate::ui::modal::ModalOptions::STANDARD }; crate::ui::modal::modal( - composed, + modal_underlay, self.modal_title(), content, Message::CloseModal, @@ -1850,7 +1874,7 @@ impl OpenCADStudio { modal_options, ) } - None => composed.into(), + None => modal_underlay, }; // Shared CAD colour picker. Indexed ACI colours use OpenCADStudio's own // dialog; True Color keeps the existing iced_aw gradient picker. diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 7eaa8b12a..9906580b9 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -50,7 +50,23 @@ impl OpenCADStudio { None => String::new(), } } - + pub(super) fn plot_modal_content<'s>( + &'s self, + extra: iced::Vector, + ) -> Element<'s, Message> { + sized_flow( + extra, + 760, + 540, + |flow| { + crate::ui::window::plot::view_window( + &self.plot_dialog, + self.print_all_options, + flow, + ) + }, + ) + } /// Build the currently-open modal dialog's content (Plan B), or `None`. /// Iced 0.15 measures the content first, so dialogs start at their natural /// size and overflowing regions become scrollable where a cap is supplied. @@ -280,20 +296,7 @@ impl OpenCADStudio { }) } } - super::super::ModalKind::Plot => { - sized_flow( - ex, - 760, - 540, - |flow| { - crate::ui::window::plot::view_window( - &self.plot_dialog, - self.print_all_options, - flow, - ) - }, - ) - } + super::super::ModalKind::Plot => self.plot_modal_content(ex), super::super::ModalKind::PrintAll => sized_flow( ex, 520, diff --git a/src/ui/color_select.rs b/src/ui/color_select.rs index da592bebe..ba4d4c2a1 100644 --- a/src/ui/color_select.rs +++ b/src/ui/color_select.rs @@ -92,7 +92,7 @@ pub fn color_display_name(c: AcadColor) -> String { } /// A small colour square. -fn swatch<'a>(bg: Color) -> Element<'a, Message> { +pub fn swatch<'a>(bg: Color) -> Element<'a, Message> { container(text("").width(13).height(13)) .style(move |theme: &Theme| container::Style { background: Some(Background::Color(bg)), diff --git a/src/ui/style/plotstyle.rs b/src/ui/style/plotstyle.rs index fbd98a8f8..97be90a7d 100644 --- a/src/ui/style/plotstyle.rs +++ b/src/ui/style/plotstyle.rs @@ -5,6 +5,38 @@ use iced::widget::{button, column, container, row, scrollable, text, text_input, use iced::{Background, Border, Element, Theme}; use crate::t; use std::borrow::Cow; +use std::fmt; + +#[derive(Clone, Debug, PartialEq)] +struct PlotLineweightItem { + index: u8, + label: String, +} + +impl fmt::Display for PlotLineweightItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.label) + } +} + +fn plot_lineweight_options() -> Vec { + let mut options = vec![PlotLineweightItem { + index: 255, + label: t!("Use object lineweight").into_owned(), + }]; + + options.extend( + crate::io::plot_style::LW_TABLE + .iter() + .enumerate() + .map(|(index, lw)| PlotLineweightItem { + index: index as u8, + label: format!("{lw:.2} mm"), + }), + ); + + options +} fn btn_s(accent: bool) -> impl Fn(&Theme, button::Status) -> button::Style { move |theme: &Theme, st| { @@ -128,6 +160,9 @@ pub fn view_window<'a>( let aci_items: Vec> = (1u8..=255) .map(|aci| { let is_sel = aci == selected_aci; + let (aci_color, _) = crate::ui::properties::acad_color_display( + acadrust::types::Color::Index(aci), + ); let has_override = table .and_then(|t| t.aci_entries.get(aci as usize)) .map(|e| e.color.is_some() || e.lineweight != 255 || e.screening != 100) @@ -153,8 +188,15 @@ pub fn view_window<'a>( } else { format!("{aci:>3} {}", t!("(default)")) }; - button(text(label).size(10).font(iced::Font::MONOSPACE)) - .on_press(Message::PlotStylePanelSelectAci(aci)) + button( + row![ + crate::ui::color_select::swatch(aci_color), + text(label).size(10).font(iced::Font::MONOSPACE), + ] + .spacing(6) + .align_y(iced::Center), + ) + .on_press(Message::PlotStylePanelSelectAci(aci)) .style(move |theme: &Theme, st| { let palette = theme.palette(); let pair = match (is_sel, st) { @@ -232,43 +274,134 @@ pub fn view_window<'a>( .unwrap_or_else(|| "—".into()); let lbl = |s: Cow<'static, str>| text(s).size(11).style(muted_style); + let lw_items = plot_lineweight_options(); + + let current_lw_index = lw_buf + .trim() + .parse::() + .unwrap_or(255); + + let current_lw = lw_items + .iter() + .find(|item| item.index == current_lw_index) + .cloned() + .unwrap_or_else(|| lw_items[0].clone()); let edit_panel = container( column![ row![ - text(t!("ACI:")).size(11).style(muted_style).width(100), + text(t!("ACI:")) + .size(11) + .style(muted_style) + .width(100), text(format!("{selected_aci}")).size(11), ] .spacing(8) .align_y(iced::Center), - lbl(t!("Color override (#RRGGBB):")), - text_input(t!("#RRGGBB or blank").as_ref(), color_buf) - .on_input(Message::PlotStylePanelColorBuf) - .style(field_style) - .size(11) - .padding([4, 8]), - lbl(t!("Lineweight index (0-24, 255=obj):")), - text_input("255", lw_buf) - .on_input(Message::PlotStylePanelLwBuf) - .style(field_style) - .size(11) - .padding([4, 8]), + + // ── Plot color ─────────────────────────────────────────────────── + lbl(t!("Plot color:")), + { + let selected_color = color_buf + .strip_prefix('#') + .filter(|hex| hex.len() == 6) + .and_then(|hex| u32::from_str_radix(hex, 16).ok()) + .map(|rgb| acadrust::types::Color::Rgb { + r: ((rgb >> 16) & 0xFF) as u8, + g: ((rgb >> 8) & 0xFF) as u8, + b: (rgb & 0xFF) as u8, + }); + + let display_color = selected_color + .unwrap_or(acadrust::types::Color::Index(selected_aci)); + + let (swatch_color, _) = + crate::ui::properties::acad_color_display(display_color); + + let display_text = if color_buf.is_empty() { + t!("Use object color").into_owned() + } else { + crate::ui::color_select::color_display_name(display_color) + }; + + row![ + container( + row![ + crate::ui::color_select::swatch(swatch_color), + text(display_text).size(11), + ] + .spacing(6) + .align_y(iced::Center) + ) + .padding([4, 8]) + .width(iced::Length::Fill), + + button(text(t!("Choose color…")).size(11)) + .on_press(Message::OpenColorWindow( + crate::app::ColorPickTarget::PlotStyle, + display_color, + )) + .style(btn_s(false)) + .padding([4, 10]), + + button(text(t!("Reset")).size(11)) + .on_press(Message::PlotStylePanelColorBuf(String::new())) + .style(btn_s(false)) + .padding([4, 10]), + ] + .spacing(6) + .align_y(iced::Center) + }, + + // ── Lineweight ─────────────────────────────────────────────────── + lbl(t!("Lineweight:")), + iced::widget::pick_list( + Some(current_lw), + lw_items, + |item| item.to_string(), + ) + .on_select(|item: PlotLineweightItem| { + Message::PlotStylePanelLwSet(item.index) + }) + .text_size(11) + .padding([3, 5]) + .width(iced::Length::Fill), + + // ── Screening ──────────────────────────────────────────────────── lbl(t!("Screening (0-100):")), text_input("100", screen_buf) .on_input(Message::PlotStylePanelScreenBuf) .style(field_style) .size(11) .padding([4, 8]), + Space::new().height(8), - text(t!("Current values:")).size(10).style(muted_style), - text(t!(" Color: %{cur_color}", cur_color = cur_color)).size(10), - text(t!(" Lineweight: %{cur_lw}", cur_lw = cur_lw)).size(10), - text(t!(" Screening: %{cur_scr}", cur_scr = cur_scr)).size(10), + + // ── Current values ─────────────────────────────────────────────── + text(t!("Current values:")) + .size(10) + .style(muted_style), + + text(t!( + " Color: %{cur_color}", + cur_color = cur_color + )) + .size(10), + + text(t!( + " Lineweight: %{cur_lw}", + cur_lw = cur_lw + )) + .size(10), + + text(t!( + " Screening: %{cur_scr}", + cur_scr = cur_scr + )) + .size(10), + Space::new().height(sizing.height), - button(text(t!("Apply to ACI")).size(11)) - .on_press(Message::PlotStylePanelApply) - .style(btn_s(true)) - .padding([5, 10]), + ] .spacing(8) .height(sizing.height), diff --git a/src/ui/window/plot.rs b/src/ui/window/plot.rs index e4ac7e993..1f8ff0d28 100644 --- a/src/ui/window/plot.rs +++ b/src/ui/window/plot.rs @@ -784,8 +784,9 @@ pub fn view_window( .on_press(Message::PlotDlg(PlotDlgMsg::LoadStyle)) .style(btn(false)) .padding([4, 10]), - button(text(t!("Save…")).size(11)) - .on_press(Message::PlotDlg(PlotDlgMsg::SaveStyle)) + + button(text(t!("Edit…")).size(11)) + .on_press(Message::PlotStylePanelOpen) .style(btn(false)) .padding([4, 10]), ]