diff --git a/src/app/view.rs b/src/app/view.rs index da9aa8e68..3670b5c59 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -5,7 +5,7 @@ use super::helpers::grid_plane_from_camera; use crate::scene::{VIEWCUBE_DRAW_PX, VIEWCUBE_PAD}; use crate::scene::grip::grips_to_screen; use crate::ui::overlay; -use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, text_input, Row, Space}; +use iced::widget::{button, column, container, mouse_area, row, shader, stack, text, Row, Space}; use iced::window; use iced::{keyboard, Background, Border, Color, Element, Fill, Subscription, Task, Theme}; @@ -19,7 +19,7 @@ impl H7CAD { return tab.layers.view_window(); } if Some(window_id) == self.page_setup_window { - return page_setup_overlay( + return crate::ui::page_setup::view_window( &self.page_setup_w, &self.page_setup_h, &self.page_setup_plot_area, self.page_setup_center, &self.page_setup_offset_x, &self.page_setup_offset_y, @@ -30,7 +30,7 @@ impl H7CAD { let tab = &self.tabs[self.active_tab]; let styles: Vec = tab.scene.document.text_styles .iter().map(|s| s.name.clone()).collect(); - return textstyle_overlay(styles, &self.textstyle_selected, + return crate::ui::textstyle::view_window(styles, &self.textstyle_selected, &self.textstyle_font, &self.textstyle_width, &self.textstyle_oblique); } if Some(window_id) == self.tablestyle_window { @@ -43,7 +43,7 @@ impl H7CAD { .find_map(|o| if let ObjectType::TableStyle(s) = o { if s.name == self.tablestyle_selected { Some(s) } else { None } } else { None }); - return tablestyle_overlay(styles, &self.tablestyle_selected, selected_style); + return crate::ui::tablestyle::view_window(styles, &self.tablestyle_selected, selected_style); } if Some(window_id) == self.mlstyle_window { use acadrust::objects::ObjectType; @@ -55,18 +55,18 @@ impl H7CAD { .find_map(|o| if let ObjectType::MLineStyle(s) = o { if s.name == self.mlstyle_selected { Some(s) } else { None } } else { None }); - return mlstyle_overlay(styles, &self.mlstyle_selected, selected_style, + return crate::ui::mlstyle::view_window(styles, &self.mlstyle_selected, selected_style, tab.scene.document.header.multiline_style.clone()); } if Some(window_id) == self.layout_manager_window { let i = self.active_tab; let layouts = self.tabs[i].scene.layout_names(); let current = self.tabs[i].scene.current_layout.clone(); - return layout_manager_overlay(layouts, &self.layout_manager_selected, + return crate::ui::layout_manager::view_window(layouts, &self.layout_manager_selected, &self.layout_manager_rename_buf, current); } if Some(window_id) == self.plotstyle_window { - return plotstyle_panel_overlay( + return crate::ui::plotstyle::view_window( self.active_plot_style.as_ref(), self.plotstyle_panel_aci, &self.ps_color_buf, &self.ps_lineweight_buf, &self.ps_screening_buf, ); @@ -75,10 +75,26 @@ impl H7CAD { let tab = &self.tabs[self.active_tab]; let styles: Vec = tab.scene.document.dim_styles .iter().map(|s| s.name.clone()).collect(); - return dimstyle_overlay(styles, &self.dimstyle_selected, self.dimstyle_tab, self); + return crate::ui::dimstyle::view_window( + styles, &self.dimstyle_selected, self.dimstyle_tab, + crate::ui::dimstyle::DimStyleValues { + dimdle: &self.ds_dimdle, dimdli: &self.ds_dimdli, dimgap: &self.ds_dimgap, + dimexe: &self.ds_dimexe, dimexo: &self.ds_dimexo, + dimsd1: self.ds_dimsd1, dimsd2: self.ds_dimsd2, + dimse1: self.ds_dimse1, dimse2: self.ds_dimse2, + dimasz: &self.ds_dimasz, dimcen: &self.ds_dimcen, dimtsz: &self.ds_dimtsz, + dimtxt: &self.ds_dimtxt, dimtxsty: &self.ds_dimtxsty, dimtad: &self.ds_dimtad, + dimtih: self.ds_dimtih, dimtoh: self.ds_dimtoh, + dimscale: &self.ds_dimscale, dimlfac: &self.ds_dimlfac, + dimlunit: &self.ds_dimlunit, dimdec: &self.ds_dimdec, dimpost: &self.ds_dimpost, + dimtol: self.ds_dimtol, dimlim: self.ds_dimlim, + dimtp: &self.ds_dimtp, dimtm: &self.ds_dimtm, + dimtdec: &self.ds_dimtdec, dimtfac: &self.ds_dimtfac, + }, + ); } if Some(window_id) == self.shortcuts_window { - return shortcuts_panel_overlay(&self.shortcut_overrides); + return crate::ui::shortcuts::view_window(&self.shortcut_overrides); } let i = self.active_tab; @@ -692,1625 +708,3 @@ fn layout_context_menu_overlay(name: &str) -> Element<'_, Message> { stack![catcher, positioned].into() } -// ── Page Setup overlay ────────────────────────────────────────────────────── - -/// Modal panel for editing paper width / height of the current layout. -// ── Paper size presets ──────────────────────────────────────────────────── - -#[allow(dead_code)] -const PAPER_PRESETS: &[(&str, f64, f64)] = &[ - ("A4 Portrait", 210.0, 297.0), - ("A4 Landscape", 297.0, 210.0), - ("A3 Portrait", 297.0, 420.0), - ("A3 Landscape", 420.0, 297.0), - ("A2 Portrait", 420.0, 594.0), - ("A2 Landscape", 594.0, 420.0), - ("A1 Portrait", 594.0, 841.0), - ("A1 Landscape", 841.0, 594.0), - ("A0 Portrait", 841.0, 1189.0), - ("A0 Landscape", 1189.0, 841.0), - ("Letter Portrait", 215.9, 279.4), - ("Letter Landscape", 279.4, 215.9), - ("Custom", 0.0, 0.0), -]; - -fn page_setup_overlay<'a>( - w_buf: &'a str, - h_buf: &'a str, - plot_area: &'a str, - center: bool, - offset_x: &'a str, - offset_y: &'a str, - rotation: &'a str, - scale: &'a str, -) -> Element<'a, Message> { - const PANEL_BG: Color = Color { r: 0.15, g: 0.15, b: 0.15, a: 1.0 }; - const BORDER_COL: Color = Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }; - const TEXT_COLOR: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const DIM_COLOR: 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_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; - - let lbl = |s: &'static str| text(s).size(11).color(DIM_COLOR).width(110); - - let field_style = |_: &Theme, _: text_input::Status| text_input::Style { - background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }), - border: Border { color: BORDER_COL, width: 1.0, radius: 3.0.into() }, - icon: TEXT_COLOR, - placeholder: Color { r: 0.45, g: 0.45, b: 0.45, a: 1.0 }, - value: TEXT_COLOR, - selection: ACCENT, - }; - - let btn_style = |accent: bool| { - move |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed if accent => { - Color { r: 0.20, g: 0.42, b: 0.72, a: 1.0 } - } - button::Status::Hovered | button::Status::Pressed => { - Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 } - } - _ if accent => ACCENT, - _ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }, - })), - text_color: TEXT_COLOR, - border: Border { color: BORDER_COL, width: 1.0, radius: 4.0.into() }, - shadow: iced::Shadow::default(), - snap: false, - } - }; - - let pill_style = |active: bool| { - move |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match (active, status) { - (true, _) => ACTIVE_BG, - (false, button::Status::Hovered | button::Status::Pressed) => { - Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 } - } - _ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 }, - })), - text_color: TEXT_COLOR, - border: Border { color: BORDER_COL, width: 1.0, radius: 3.0.into() }, - shadow: iced::Shadow::default(), - snap: false, - } - }; - - let divider = || container(Space::new().width(Fill).height(1)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER_COL)), - ..Default::default() - }) - .width(Fill) - .height(1); - - // ── Paper size presets ──────────────────────────────────────────────── - let preset_row1 = row![ - button(text("A4 P").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPreset("A4 Portrait".into())) - .style(pill_style(false)).padding([3, 6]), - button(text("A4 L").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPreset("A4 Landscape".into())) - .style(pill_style(false)).padding([3, 6]), - button(text("A3 P").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPreset("A3 Portrait".into())) - .style(pill_style(false)).padding([3, 6]), - button(text("A3 L").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPreset("A3 Landscape".into())) - .style(pill_style(false)).padding([3, 6]), - ].spacing(4); - - let preset_row2 = row![ - button(text("A2 L").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPreset("A2 Landscape".into())) - .style(pill_style(false)).padding([3, 6]), - button(text("A1 L").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPreset("A1 Landscape".into())) - .style(pill_style(false)).padding([3, 6]), - button(text("A0 L").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPreset("A0 Landscape".into())) - .style(pill_style(false)).padding([3, 6]), - button(text("Letter").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPreset("Letter Landscape".into())) - .style(pill_style(false)).padding([3, 6]), - ].spacing(4); - - // ── Plot area buttons ───────────────────────────────────────────────── - let area_row = row![ - button(text("Layout").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPlotArea("Layout".into())) - .style(pill_style(plot_area == "Layout")).padding([3, 8]), - button(text("Extents").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupPlotArea("Extents".into())) - .style(pill_style(plot_area == "Extents")).padding([3, 8]), - ].spacing(6); - - // ── Center toggle ───────────────────────────────────────────────────── - let center_btn = button( - text(if center { "✓ Center on page" } else { " Center on page" }).size(11).color(TEXT_COLOR) - ) - .on_press(Message::PageSetupCenterToggle) - .style(pill_style(center)) - .padding([4, 10]); - - // ── Rotation buttons ────────────────────────────────────────────────── - let rot_row = row![ - button(text("0°").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupRotation("0".into())) - .style(pill_style(rotation == "0")).padding([3, 8]), - button(text("90°").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupRotation("90".into())) - .style(pill_style(rotation == "90")).padding([3, 8]), - button(text("180°").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupRotation("180".into())) - .style(pill_style(rotation == "180")).padding([3, 8]), - button(text("270°").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupRotation("270".into())) - .style(pill_style(rotation == "270")).padding([3, 8]), - ].spacing(4); - - let panel = container( - column![ - text("Page Setup").size(14).color(TEXT_COLOR), - divider(), - // Paper size - text("Paper Size").size(11).color(DIM_COLOR), - preset_row1, - preset_row2, - row![ - lbl("Width (mm)"), - text_input("297", w_buf) - .on_input(Message::PageSetupWidthEdit) - .on_submit(Message::PageSetupCommit) - .style(field_style) - .width(80).size(12), - ].spacing(6).align_y(iced::Alignment::Center), - row![ - lbl("Height (mm)"), - text_input("210", h_buf) - .on_input(Message::PageSetupHeightEdit) - .on_submit(Message::PageSetupCommit) - .style(field_style) - .width(80).size(12), - ].spacing(6).align_y(iced::Alignment::Center), - divider(), - // Plot area - text("Plot Area").size(11).color(DIM_COLOR), - area_row, - divider(), - // Position - text("Position").size(11).color(DIM_COLOR), - center_btn, - row![ - lbl("Offset X (mm)"), - text_input("0", offset_x) - .on_input(Message::PageSetupOffsetXEdit) - .style(field_style) - .width(80).size(12), - ].spacing(6).align_y(iced::Alignment::Center), - row![ - lbl("Offset Y (mm)"), - text_input("0", offset_y) - .on_input(Message::PageSetupOffsetYEdit) - .style(field_style) - .width(80).size(12), - ].spacing(6).align_y(iced::Alignment::Center), - divider(), - // Rotation - text("Rotation").size(11).color(DIM_COLOR), - rot_row, - divider(), - // Plot Scale - text("Plot Scale").size(11).color(DIM_COLOR), - row![ - button(text("Fit").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("Fit".into())) - .style(pill_style(scale == "Fit")).padding([3, 8]), - button(text("1:1").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("1:1".into())) - .style(pill_style(scale == "1:1")).padding([3, 8]), - button(text("1:2").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("1:2".into())) - .style(pill_style(scale == "1:2")).padding([3, 8]), - button(text("1:5").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("1:5".into())) - .style(pill_style(scale == "1:5")).padding([3, 8]), - button(text("1:10").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("1:10".into())) - .style(pill_style(scale == "1:10")).padding([3, 8]), - ].spacing(4), - row![ - button(text("1:20").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("1:20".into())) - .style(pill_style(scale == "1:20")).padding([3, 8]), - button(text("1:50").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("1:50".into())) - .style(pill_style(scale == "1:50")).padding([3, 8]), - button(text("1:100").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("1:100".into())) - .style(pill_style(scale == "1:100")).padding([3, 8]), - button(text("2:1").size(10).color(TEXT_COLOR)) - .on_press(Message::PageSetupScale("2:1".into())) - .style(pill_style(scale == "2:1")).padding([3, 8]), - ].spacing(4), - divider(), - // Buttons - row![ - button(text("Cancel").size(12).color(TEXT_COLOR)) - .on_press(Message::PageSetupClose) - .style(btn_style(false)) - .padding([5, 14]), - Space::new().width(Fill).height(0), - button(text("OK").size(12).color(TEXT_COLOR)) - .on_press(Message::PageSetupCommit) - .style(btn_style(true)) - .padding([5, 20]), - ] - .align_y(iced::Alignment::Center), - ] - .spacing(8) - .padding(16) - .width(290), - ) - .style(move |_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER_COL, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }); - - // Click-catcher to close on outside click. - let catcher = mouse_area( - container(Space::new().width(Fill).height(Fill)) - .width(Fill) - .height(Fill), - ) - .on_press(Message::PageSetupClose); - - // Center the panel on screen. - let positioned = container(panel) - .width(Fill) - .height(Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - stack![catcher, positioned].into() -} - -// ── DimStyle Dialog overlay ───────────────────────────────────────────────── - -fn dimstyle_overlay<'a>( - styles: Vec, - selected: &'a str, - tab: u8, - app: &'a super::H7CAD, -) -> Element<'a, Message> { - use super::DsField; - use iced::widget::checkbox; - use iced::Length::Shrink; - - const PANEL_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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const DIM_COL: 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_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; - - let lbl = |s: &'static str| text(s).size(11).color(DIM_COL).width(150); - - let field_style = |_: &Theme, _: text_input::Status| text_input::Style { - background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - icon: TEXT_COL, placeholder: DIM_COL, value: TEXT_COL, selection: ACCENT, - }; - - let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> { - text_input("", val) - .on_input(move |s| Message::DsEdit(fld.clone(), s)) - .style(field_style) - .size(11) - .width(90) - .into() - }; - - let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match (accent, status) { - (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_COL, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }; - - let tab_btn = |label: &'static str, idx: u8| { - let active = tab == idx; - button(text(label).size(11).color(TEXT_COL)) - .on_press(Message::DimStyleDialogTab(idx)) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (true, _) => ACTIVE_BG, - (false, button::Status::Hovered | button::Status::Pressed) => - Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }, - _ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 }, - })), - text_color: TEXT_COL, - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - ..Default::default() - }) - .padding([4, 10]) - }; - - // ── Style list ──────────────────────────────────────────────────────── - let style_list: Element<'_, Message> = { - let mut col = column![].spacing(2); - for name in styles { - let active = name == selected; - col = col.push( - button(text(name.clone()).size(11).color(TEXT_COL)) - .on_press(Message::DimStyleDialogSelect(name)) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (active, st) { - (true, _) => ACTIVE_BG, - (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_COL, - border: Border { color: BORDER, width: 0.0, radius: 3.0.into() }, - ..Default::default() - }) - .padding([3, 8]) - .width(Fill) - ); - } - iced::widget::scrollable(col).height(160).into() - }; - - let style_panel = column![ - text("Styles").size(11).color(DIM_COL), - container(style_list) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - ..Default::default() - }) - .width(150) - .padding(2), - row![ - button(text("New").size(10).color(TEXT_COL)) - .on_press(Message::DimStyleDialogNew) - .style(btn_style(false)).padding([3, 8]), - button(text("Delete").size(10).color(TEXT_COL)) - .on_press(Message::DimStyleDialogDelete) - .style(btn_style(false)).padding([3, 8]), - ].spacing(4), - button(text("Set Current").size(10).color(TEXT_COL)) - .on_press(Message::DimStyleDialogSetCurrent) - .style(btn_style(false)).padding([3, 8]).width(Fill), - ].spacing(6).width(150); - - // ── Tab bar ─────────────────────────────────────────────────────────── - let tabs = row![ - tab_btn("Lines", 0), - tab_btn("Arrows", 1), - tab_btn("Text", 2), - tab_btn("Scale/Units", 3), - tab_btn("Tolerances", 4), - ].spacing(2); - - // ── Checkbox helper ─────────────────────────────────────────────────── - let chk = |label: &'static str, val: bool, fld: DsField| -> Element<'a, Message> { - checkbox(val) - .label(label) - .on_toggle(move |_| Message::DsToggle(fld.clone())) - .size(14) - .text_size(11) - .into() - }; - - // ── Tab content ─────────────────────────────────────────────────────── - let tab_content: Element<'_, Message> = match tab { - 0 => column![ - text("Dimension Line").size(11).color(ACCENT), - row![lbl("Extension (DIMDLE)"), mk_field(DsField::Dimdle, &app.ds_dimdle)].spacing(8).align_y(iced::Center), - row![lbl("Spacing (DIMDLI)"), mk_field(DsField::Dimdli, &app.ds_dimdli)].spacing(8).align_y(iced::Center), - row![lbl("Text gap (DIMGAP)"), mk_field(DsField::Dimgap, &app.ds_dimgap)].spacing(8).align_y(iced::Center), - chk("Suppress 1st line (DIMSD1)", app.ds_dimsd1, DsField::Dimsd1), - chk("Suppress 2nd line (DIMSD2)", app.ds_dimsd2, DsField::Dimsd2), - text("Extension Line").size(11).color(ACCENT), - row![lbl("Extension (DIMEXE)"), mk_field(DsField::Dimexe, &app.ds_dimexe)].spacing(8).align_y(iced::Center), - row![lbl("Offset (DIMEXO)"), mk_field(DsField::Dimexo, &app.ds_dimexo)].spacing(8).align_y(iced::Center), - chk("Suppress 1st line (DIMSE1)", app.ds_dimse1, DsField::Dimse1), - chk("Suppress 2nd line (DIMSE2)", app.ds_dimse2, DsField::Dimse2), - ].spacing(6).into(), - - 1 => column![ - text("Arrows").size(11).color(ACCENT), - row![lbl("Arrow size (DIMASZ)"), mk_field(DsField::Dimasz, &app.ds_dimasz)].spacing(8).align_y(iced::Center), - row![lbl("Center mark (DIMCEN)"), mk_field(DsField::Dimcen, &app.ds_dimcen)].spacing(8).align_y(iced::Center), - row![lbl("Tick size (DIMTSZ)"), mk_field(DsField::Dimtsz, &app.ds_dimtsz)].spacing(8).align_y(iced::Center), - ].spacing(6).into(), - - 2 => column![ - text("Text").size(11).color(ACCENT), - row![lbl("Height (DIMTXT)"), mk_field(DsField::Dimtxt, &app.ds_dimtxt)].spacing(8).align_y(iced::Center), - row![lbl("Style (DIMTXSTY)"), mk_field(DsField::Dimtxsty, &app.ds_dimtxsty)].spacing(8).align_y(iced::Center), - row![lbl("Vertical pos (DIMTAD)"), mk_field(DsField::Dimtad, &app.ds_dimtad)].spacing(8).align_y(iced::Center), - chk("Horizontal inside (DIMTIH)", app.ds_dimtih, DsField::Dimtih), - chk("Horizontal outside (DIMTOH)", app.ds_dimtoh, DsField::Dimtoh), - ].spacing(6).into(), - - 3 => column![ - text("Scale").size(11).color(ACCENT), - row![lbl("Overall scale (DIMSCALE)"), mk_field(DsField::Dimscale, &app.ds_dimscale)].spacing(8).align_y(iced::Center), - row![lbl("Linear factor (DIMLFAC)"), mk_field(DsField::Dimlfac, &app.ds_dimlfac)].spacing(8).align_y(iced::Center), - text("Units").size(11).color(ACCENT), - row![lbl("Format (DIMLUNIT)"), mk_field(DsField::Dimlunit, &app.ds_dimlunit)].spacing(8).align_y(iced::Center), - row![lbl("Decimals (DIMDEC)"), mk_field(DsField::Dimdec, &app.ds_dimdec)].spacing(8).align_y(iced::Center), - row![lbl("Suffix (DIMPOST)"), mk_field(DsField::Dimpost, &app.ds_dimpost)].spacing(8).align_y(iced::Center), - ].spacing(6).into(), - - _ => column![ - text("Tolerances").size(11).color(ACCENT), - chk("Generate tolerances (DIMTOL)", app.ds_dimtol, DsField::Dimtol), - chk("Limits generation (DIMLIM)", app.ds_dimlim, DsField::Dimlim), - row![lbl("Plus tolerance (DIMTP)"), mk_field(DsField::Dimtp, &app.ds_dimtp)].spacing(8).align_y(iced::Center), - row![lbl("Minus tolerance (DIMTM)"), mk_field(DsField::Dimtm, &app.ds_dimtm)].spacing(8).align_y(iced::Center), - row![lbl("Tol. decimals (DIMTDEC)"), mk_field(DsField::Dimtdec, &app.ds_dimtdec)].spacing(8).align_y(iced::Center), - row![lbl("Tol. scale (DIMTFAC)"), mk_field(DsField::Dimtfac, &app.ds_dimtfac)].spacing(8).align_y(iced::Center), - ].spacing(6).into(), - }; - - // ── Right panel ─────────────────────────────────────────────────────── - let right_panel = column![ - text(format!("Editing: {selected}")).size(12).color(TEXT_COL), - tabs, - container( - iced::widget::scrollable( - container(tab_content).padding(8) - ).height(220) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - ..Default::default() - }) - .width(Fill), - row![ - button(text("Apply").size(11).color(TEXT_COL)) - .on_press(Message::DimStyleDialogApply) - .style(btn_style(true)).padding([5, 16]), - button(text("Close").size(11).color(TEXT_COL)) - .on_press(Message::DimStyleDialogClose) - .style(btn_style(false)).padding([5, 12]), - ].spacing(8), - ].spacing(8).width(Fill); - - // ── Main panel ──────────────────────────────────────────────────────── - let panel = container( - column![ - row![ - text("Dimension Style Manager").size(14).color(TEXT_COL), - Space::new().width(Fill), - button(text("✕").size(12).color(DIM_COL)) - .on_press(Message::DimStyleDialogClose) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - text_color: DIM_COL, - ..Default::default() - }) - .padding([2, 6]), - ].align_y(iced::Center), - row![style_panel, right_panel].spacing(12).align_y(iced::Top), - ].spacing(10).padding(16) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) - .width(Shrink); - - let catcher = mouse_area( - container(iced::widget::Space::new().width(Fill).height(Fill)) - ).on_press(Message::DimStyleDialogClose); - - let positioned = container(panel) - .width(Fill).height(Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - stack![catcher, positioned].into() -} - -// ── TextStyle Font Browser overlay ───────────────────────────────────────── - -/// Built-in CXF font file names (relative to assets/fonts/). -const BUILTIN_FONTS: &[&str] = &[ - "CourierCad.cxf", "Cursive.cxf", "GothGBT.cxf", "GothGRT.cxf", "GothITT.cxf", - "GreekC.cxf", "GreekS.cxf", "ItalicC.cxf", "ItalicT.cxf", - "RomanC.cxf", "RomanD.cxf", "RomanS.cxf", "RomanT.cxf", - "SansND.cxf", "SansNS.cxf", "ScriptC.cxf", "ScriptS.cxf", - "Standard.cxf", "Unicode.cxf", "SymbolCad.cxf", -]; - -fn textstyle_overlay<'a>( - styles: Vec, - selected: &'a str, - font_buf: &'a str, - width_buf: &'a str, - oblique_buf: &'a str, -) -> Element<'a, Message> { - use iced::Length::Shrink; - - const PANEL_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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const DIM_COL: 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_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; - - let field_style = |_: &Theme, _: iced::widget::text_input::Status| iced::widget::text_input::Style { - background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - icon: TEXT_COL, placeholder: DIM_COL, value: TEXT_COL, selection: ACCENT, - }; - - let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match (accent, status) { - (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_COL, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }; - - // Left: style list. - let style_items: Vec> = styles.iter().map(|name| { - let is_sel = name.as_str() == selected; - button(text(name.clone()).size(11).color(TEXT_COL)) - .on_press(Message::TextStyleDialogSelect(name.clone())) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (is_sel, st) { - (true, _) => ACTIVE_BG, - (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_COL, - ..Default::default() - }) - .padding([3, 8]) - .width(Fill) - .into() - }).collect(); - - let style_panel = container( - column(style_items).spacing(2) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding(4) - .width(150) - .height(280); - - // Middle: font file list (built-in CXF fonts). - let font_items: Vec> = BUILTIN_FONTS.iter().map(|&f| { - let is_sel = font_buf == f; - button(text(f).size(10).color(TEXT_COL)) - .on_press(Message::TextStyleFontPick(f.to_string())) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (is_sel, st) { - (true, _) => ACTIVE_BG, - (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_COL, - ..Default::default() - }) - .padding([3, 8]) - .width(Fill) - .into() - }).collect(); - - let font_panel = column![ - text("Font File:").size(11).color(DIM_COL), - container( - iced::widget::scrollable(column(font_items).spacing(1)) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding(4) - .width(180) - .height(160), - text_input("font file…", font_buf) - .on_input(|v| Message::TextStyleEdit { field: "font", value: v }) - .style(field_style) - .size(11) - .width(180), - ] - .spacing(4); - - // Right: properties + preview. - let props = column![ - text("Properties").size(12).color(ACCENT), - row![ - text("Width Factor:").size(11).color(DIM_COL).width(110), - text_input("1.0", width_buf) - .on_input(|v| Message::TextStyleEdit { field: "width", value: v }) - .style(field_style) - .size(11) - .width(80), - ].spacing(6).align_y(iced::Center), - row![ - text("Oblique (°):").size(11).color(DIM_COL).width(110), - text_input("0.0", oblique_buf) - .on_input(|v| Message::TextStyleEdit { field: "oblique", value: v }) - .style(field_style) - .size(11) - .width(80), - ].spacing(6).align_y(iced::Center), - Space::new().height(8), - text("Preview:").size(11).color(DIM_COL), - container( - text("AaBbCc 0123").size(20).color(TEXT_COL) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding(10) - .width(Fill), - Space::new().height(Fill), - row![ - button(text("Apply").size(11)) - .on_press(Message::TextStyleApply) - .style(btn_style(true)) - .padding([5, 10]), - button(text("Set Current").size(11)) - .on_press(Message::TextStyleDialogSetCurrent) - .style(btn_style(false)) - .padding([5, 10]), - ].spacing(6), - ] - .spacing(8) - .width(220); - - let panel = container( - column![ - row![ - text("Text Style Font Browser").size(13).color(TEXT_COL), - Space::new().width(Fill), - row![ - button(text("New").size(11)) - .on_press(Message::TextStyleDialogNew) - .style(btn_style(false)) - .padding([3, 8]), - button(text("Delete").size(11)) - .on_press(Message::TextStyleDialogDelete) - .style(btn_style(false)) - .padding([3, 8]), - button(text("✕").size(12).color(DIM_COL)) - .on_press(Message::TextStyleDialogClose) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - text_color: DIM_COL, - ..Default::default() - }) - .padding([2, 6]), - ].spacing(4), - ].align_y(iced::Center), - row![style_panel, font_panel, props].spacing(12).align_y(iced::Top), - ].spacing(10).padding(16) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) - .width(Shrink); - - let catcher = mouse_area( - container(iced::widget::Space::new().width(Fill).height(Fill)) - ).on_press(Message::TextStyleDialogClose); - - let positioned = container(panel) - .width(Fill).height(Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - stack![catcher, positioned].into() -} - -// ── TableStyle Dialog overlay ─────────────────────────────────────────────── - -fn tablestyle_overlay<'a>( - styles: Vec, - selected: &'a str, - selected_style: Option<&'a acadrust::objects::TableStyle>, -) -> Element<'a, Message> { - use iced::Length::Shrink; - - const PANEL_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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const DIM_COL: 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_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; - - let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match (accent, status) { - (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_COL, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }; - - // Style list. - let style_items: Vec> = styles.iter().map(|name| { - let is_sel = name.as_str() == selected; - button(text(name.clone()).size(11).color(TEXT_COL)) - .on_press(Message::TableStyleDialogSelect(name.clone())) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (is_sel, st) { - (true, _) => ACTIVE_BG, - (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_COL, - ..Default::default() - }) - .padding([3, 8]) - .width(Fill) - .into() - }).collect(); - - let style_panel = container( - column(style_items).spacing(2) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding(4) - .width(160) - .height(240); - - // Right panel: details. - let details: Element<'_, Message> = if let Some(s) = selected_style { - let info_row = |label: &'static str, val: String| -> Element<'_, Message> { - row![ - text(label).size(11).color(DIM_COL).width(140), - text(val).size(11).color(TEXT_COL), - ].spacing(8).align_y(iced::Center).into() - }; - let row_info = |row_label: &'static str, rs: &acadrust::objects::RowCellStyle| -> Element<'_, Message> { - column![ - text(row_label).size(11).color(ACCENT), - info_row(" Text Style:", rs.text_style_name.clone()), - info_row(" Text Height:", format!("{:.4}", rs.text_height)), - info_row(" Alignment:", format!("{:?}", rs.alignment)), - ].spacing(3).into() - }; - column![ - info_row("Name:", s.name.clone()), - info_row("H Margin:", format!("{:.4}", s.horizontal_margin)), - info_row("V Margin:", format!("{:.4}", s.vertical_margin)), - info_row("Title Suppressed:", s.title_suppressed.to_string()), - info_row("Header Suppressed:", s.header_suppressed.to_string()), - row_info("Data Row:", &s.data_row_style), - row_info("Header Row:", &s.header_row_style), - row_info("Title Row:", &s.title_row_style), - ].spacing(5).into() - } else { - text("No style selected.").size(11).color(DIM_COL).into() - }; - - let right_panel = column![ - details, - Space::new().height(Fill), - row![ - button(text("New").size(11)) - .on_press(Message::TableStyleDialogNew) - .style(btn_style(true)) - .padding([5, 10]), - button(text("Delete").size(11)) - .on_press(Message::TableStyleDialogDelete) - .style(btn_style(false)) - .padding([5, 10]), - ].spacing(6), - ] - .spacing(10) - .width(280) - .height(240); - - let panel = container( - column![ - row![ - text("Table Style Manager").size(13).color(TEXT_COL), - Space::new().width(Fill), - button(text("✕").size(12).color(DIM_COL)) - .on_press(Message::TableStyleDialogClose) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - text_color: DIM_COL, - ..Default::default() - }) - .padding([2, 6]), - ].align_y(iced::Center), - row![style_panel, right_panel].spacing(12).align_y(iced::Top), - ].spacing(10).padding(16) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) - .width(Shrink); - - let catcher = mouse_area( - container(iced::widget::Space::new().width(Fill).height(Fill)) - ).on_press(Message::TableStyleDialogClose); - - let positioned = container(panel) - .width(Fill).height(Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - stack![catcher, positioned].into() -} - -// ── MLineStyle Dialog overlay ─────────────────────────────────────────────── - -fn mlstyle_overlay<'a>( - styles: Vec, - selected: &'a str, - selected_style: Option<&'a acadrust::objects::MLineStyle>, - current_style: String, -) -> Element<'a, Message> { - use iced::Length::Shrink; - - const PANEL_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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const DIM_COL: 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_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; - - let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match (accent, status) { - (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_COL, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }; - - // 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).color(TEXT_COL)) - .on_press(Message::MlStyleDialogSelect(name.clone())) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (is_sel, st) { - (true, _) => ACTIVE_BG, - (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_COL, - ..Default::default() - }) - .padding([3, 8]) - .width(Fill) - .into() - }).collect(); - - let style_panel = container( - column(style_items).spacing(2) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding(4) - .width(160) - .height(240); - - // Right panel: details for selected style. - let details: Element<'_, Message> = if let Some(s) = selected_style { - let info_row = |label: &'static str, val: String| -> Element<'_, Message> { - row![ - text(label).size(11).color(DIM_COL).width(110), - text(val).size(11).color(TEXT_COL), - ].spacing(8).align_y(iced::Center).into() - }; - let elem_rows: Vec> = s.elements.iter().enumerate().map(|(idx, e)| { - let color_str: String = match &e.color { - acadrust::types::Color::ByLayer => "ByLayer".into(), - acadrust::types::Color::ByBlock => "ByBlock".into(), - acadrust::types::Color::Index(i) => format!("ACI {}", i), - acadrust::types::Color::Rgb { r, g, b } => format!("#{:02X}{:02X}{:02X}", r, g, b), - }; - let lt = if e.linetype.is_empty() { "ByLayer" } else { e.linetype.as_str() }; - row![ - text(format!(" {}:", idx)).size(10).color(DIM_COL).width(20), - text(format!("{:+.3}", e.offset)).size(10).color(TEXT_COL).width(60), - text(color_str).size(10).color(TEXT_COL).width(80), - text(lt).size(10).color(TEXT_COL), - ].spacing(4).align_y(iced::Center).into() - }).collect(); - - let mut col_items: Vec> = vec![ - info_row("Name:", s.name.clone()), - info_row("Elements:", s.elements.len().to_string()), - text(" Off Color Ltype").size(10).color(DIM_COL).into(), - ]; - col_items.extend(elem_rows); - column(col_items).spacing(6).into() - } else { - text("No style selected.").size(11).color(DIM_COL).into() - }; - - let right_panel = column![ - details, - Space::new().height(Fill), - row![ - button(text("Set Current").size(11)) - .on_press(Message::MlStyleDialogSetCurrent) - .style(btn_style(true)) - .padding([5, 10]), - button(text("New").size(11)) - .on_press(Message::MlStyleDialogNew) - .style(btn_style(false)) - .padding([5, 10]), - button(text("Delete").size(11)) - .on_press(Message::MlStyleDialogDelete) - .style(btn_style(false)) - .padding([5, 10]), - ].spacing(6), - ] - .spacing(10) - .width(280) - .height(240); - - let panel = container( - column![ - row![ - text("Multiline Style Manager").size(13).color(TEXT_COL), - Space::new().width(Fill), - button(text("✕").size(12).color(DIM_COL)) - .on_press(Message::MlStyleDialogClose) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - text_color: DIM_COL, - ..Default::default() - }) - .padding([2, 6]), - ].align_y(iced::Center), - row![style_panel, right_panel].spacing(12).align_y(iced::Top), - ].spacing(10).padding(16) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) - .width(Shrink); - - let catcher = mouse_area( - container(iced::widget::Space::new().width(Fill).height(Fill)) - ).on_press(Message::MlStyleDialogClose); - - let positioned = container(panel) - .width(Fill).height(Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - stack![catcher, positioned].into() -} - -// ── Plot Style Panel ─────────────────────────────────────────────────────── - -fn plotstyle_panel_overlay<'a>( - table: Option<&'a crate::io::plot_style::PlotStyleTable>, - selected_aci: u8, - color_buf: &'a str, - lw_buf: &'a str, - screen_buf: &'a str, -) -> Element<'a, Message> { - use iced::Length::Fill; - use iced::Color; - use iced::widget::scrollable; - - const PANEL_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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const DIM_COL: 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_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; - - let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match (accent, status) { - (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_COL, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }; - - let field_style = |_: &Theme, _: text_input::Status| text_input::Style { - background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - icon: TEXT_COL, - placeholder: DIM_COL, - value: TEXT_COL, - selection: ACCENT, - }; - - // ACI color list (1..=255) - let aci_items: Vec> = (1u8..=255).map(|aci| { - let is_sel = aci == selected_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) - .unwrap_or(false); - let lw_str = table.and_then(|t| t.aci_entries.get(aci as usize)) - .and_then(|e| { - if e.lineweight != 255 { - crate::io::plot_style::LW_TABLE.get(e.lineweight as usize) - .map(|lw| format!("{:.2}mm", lw)) - } else { None } - }).unwrap_or_default(); - let color_str = table.and_then(|t| t.aci_entries.get(aci as usize)) - .and_then(|e| e.color.map(|[r, g, b]| format!("#{:02X}{:02X}{:02X}", r, g, b))) - .unwrap_or_default(); - let label = if has_override { - format!("{aci:>3} {color_str:<9} {lw_str}") - } else { - format!("{aci:>3} (default)") - }; - button(text(label).size(10).color(TEXT_COL).font(iced::Font::MONOSPACE)) - .on_press(Message::PlotStylePanelSelectAci(aci)) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (is_sel, st) { - (true, _) => ACTIVE_BG, - (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_COL, - ..Default::default() - }) - .padding([2, 8]) - .width(Fill) - .into() - }).collect(); - - let list_panel = container( - scrollable(column(aci_items).spacing(1)).height(300) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding(4) - .width(260); - - // Right panel: edit selected entry - let entry = table.and_then(|t| t.aci_entries.get(selected_aci as usize)); - let edit_panel: Element<'_, Message> = { - let lbl = |s: &'static str| text(s).size(11).color(DIM_COL); - column![ - row![ - text("ACI:").size(11).color(DIM_COL).width(90), - text(format!("{selected_aci}")).size(11).color(TEXT_COL), - ].spacing(8).align_y(iced::Center), - lbl("Color override (#RRGGBB):"), - text_input("#RRGGBB or blank", color_buf) - .on_input(Message::PlotStylePanelColorBuf) - .style(field_style) - .size(11) - .padding([4, 8]), - lbl("Lineweight index (0-24, 255=obj):"), - text_input("255", lw_buf) - .on_input(Message::PlotStylePanelLwBuf) - .style(field_style) - .size(11) - .padding([4, 8]), - lbl("Screening (0-100):"), - text_input("100", screen_buf) - .on_input(Message::PlotStylePanelScreenBuf) - .style(field_style) - .size(11) - .padding([4, 8]), - Space::new().height(8), - { - let cur_color = entry.and_then(|e| e.color.map(|[r,g,b]| format!("#{:02X}{:02X}{:02X}", r,g,b))).unwrap_or("(none)".into()); - let cur_lw = entry.map(|e| if e.lineweight == 255 { "object".into() } else { - crate::io::plot_style::LW_TABLE.get(e.lineweight as usize) - .map(|lw| format!("{:.2}mm (idx {})", lw, e.lineweight)) - .unwrap_or_else(|| format!("idx {}", e.lineweight)) - }).unwrap_or("—".into()); - let cur_scr = entry.map(|e| format!("{}%", e.screening)).unwrap_or("—".into()); - let vals: Element<'_, Message> = column![ - text("Current values:").size(10).color(DIM_COL), - text(format!(" Color: {cur_color}")).size(10).color(TEXT_COL), - text(format!(" Lweight: {cur_lw}")).size(10).color(TEXT_COL), - text(format!(" Screening: {cur_scr}")).size(10).color(TEXT_COL), - ].spacing(3).into(); - vals - }, - Space::new().height(Fill), - button(text("Apply to ACI").size(11)) - .on_press(Message::PlotStylePanelApply) - .style(btn_style(true)) - .padding([5, 10]), - ] - .spacing(6) - .width(220) - .height(300) - .into() - }; - - let table_name = table.map(|t| t.name.as_str()).unwrap_or("(no table loaded)"); - - let panel = container( - column![ - row![ - text("Plot Style Table Editor").size(13).color(TEXT_COL), - Space::new().width(Fill), - button(text("✕").size(12).color(DIM_COL)) - .on_press(Message::PlotStylePanelClose) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - text_color: DIM_COL, - ..Default::default() - }) - .padding([2, 6]), - ].align_y(iced::Center), - text(format!("Table: {table_name}")).size(11).color(DIM_COL), - Space::new().height(8), - row![ - list_panel, - Space::new().width(12), - edit_panel, - ].align_y(iced::alignment::Vertical::Top), - Space::new().height(8), - row![ - button(text("Load CTB/STB").size(11)) - .on_press(Message::PlotStyleLoad) - .style(btn_style(false)) - .padding([5, 10]), - button(text("Save As…").size(11)) - .on_press(Message::PlotStylePanelSave) - .style(btn_style(false)) - .padding([5, 10]), - button(text("Clear Table").size(11)) - .on_press(Message::PlotStyleClear) - .style(btn_style(false)) - .padding([5, 10]), - Space::new().width(Fill), - button(text("Close").size(11)) - .on_press(Message::PlotStylePanelClose) - .style(btn_style(false)) - .padding([5, 10]), - ].spacing(8), - ] - .spacing(6) - .padding(16) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) - .width(520) - .height(430); - - let catcher = mouse_area(container(Space::new().width(Fill).height(Fill)) - .width(Fill).height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.45 })), - ..Default::default() - }) - ).on_press(Message::PlotStylePanelClose); - - let positioned = container(panel) - .width(Fill).height(Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - stack![catcher, positioned].into() -} - -// ── Layout Manager Panel ─────────────────────────────────────────────────── - -fn layout_manager_overlay<'a>( - layouts: Vec, - selected: &'a str, - rename_buf: &'a str, - current: String, -) -> Element<'a, Message> { - use iced::Length::Fill; - use iced::Color; - - const PANEL_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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const DIM_COL: 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_BG: Color = Color { r: 0.20, g: 0.40, b: 0.70, a: 1.0 }; - const WARN_COL: Color = Color { r: 0.80, g: 0.35, b: 0.25, a: 1.0 }; - - let btn_style = |accent: bool| move |_: &Theme, status: button::Status| button::Style { - background: Some(Background::Color(match (accent, status) { - (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_COL, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }; - - let field_style = |_: &Theme, _: text_input::Status| text_input::Style { - background: Background::Color(Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }), - border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, - icon: TEXT_COL, - placeholder: DIM_COL, - value: TEXT_COL, - selection: ACCENT, - }; - - // Layout list - let list_items: Vec> = layouts.iter().map(|name| { - let is_sel = name.as_str() == selected; - let is_cur = name.as_str() == current.as_str(); - let label = if is_cur { format!("{} ◀", name) } else { name.clone() }; - button(text(label).size(12).color(TEXT_COL)) - .on_press(Message::LayoutManagerSelect(name.clone())) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match (is_sel, st) { - (true, _) => ACTIVE_BG, - (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_COL, - ..Default::default() - }) - .padding([5, 10]) - .width(Fill) - .into() - }).collect(); - - let list_panel = container( - column(list_items).spacing(2) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding(4) - .width(200) - .height(260); - - let is_model = selected == "Model"; - - let right_panel = column![ - text(if is_model { "Model Space" } else { "Paper Space Layout" }) - .size(12).color(TEXT_COL), - Space::new().height(4), - row![ - text("Name:").size(11).color(DIM_COL).width(70), - text(selected).size(11).color(TEXT_COL), - ].spacing(8).align_y(iced::Center), - row![ - text("Status:").size(11).color(DIM_COL).width(70), - text(if selected == current.as_str() { "Active ◀" } else { "Inactive" }) - .size(11) - .color(if selected == current.as_str() { ACCENT } else { DIM_COL }), - ].spacing(8).align_y(iced::Center), - Space::new().height(12), - text("Rename:").size(11).color(DIM_COL), - row![ - text_input("New name…", rename_buf) - .on_input(Message::LayoutManagerRenameBuf) - .on_submit(Message::LayoutManagerRenameCommit) - .style(field_style) - .size(11) - .padding([4, 8]), - button(text("OK").size(11)) - .on_press(Message::LayoutManagerRenameCommit) - .style(btn_style(true)) - .padding([4, 8]), - ].spacing(6).align_y(iced::Center), - Space::new().height(Fill), - row![ - button(text("◀").size(11)) - .on_press(Message::LayoutManagerMoveLeft) - .style(btn_style(false)) - .padding([4, 8]), - button(text("▶").size(11)) - .on_press(Message::LayoutManagerMoveRight) - .style(btn_style(false)) - .padding([4, 8]), - Space::new().width(Fill), - button(text("Set Current").size(11)) - .on_press(Message::LayoutManagerSetCurrent) - .style(btn_style(true)) - .padding([5, 10]), - ].spacing(6).align_y(iced::Center), - ] - .spacing(6) - .width(240) - .height(260); - - let panel = container( - column![ - row![ - text("Layout Manager").size(13).color(TEXT_COL), - Space::new().width(Fill), - button(text("✕").size(12).color(DIM_COL)) - .on_press(Message::LayoutManagerClose) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - text_color: DIM_COL, - ..Default::default() - }) - .padding([2, 6]), - ].align_y(iced::Center), - Space::new().height(8), - row![ - list_panel, - Space::new().width(12), - right_panel, - ].align_y(iced::alignment::Vertical::Top), - Space::new().height(8), - row![ - button(text("New Layout").size(11)) - .on_press(Message::LayoutManagerNew) - .style(btn_style(false)) - .padding([5, 10]), - button(text("Delete").size(11)) - .on_press(Message::LayoutManagerDelete) - .style(move |_: &Theme, st| button::Style { - background: Some(Background::Color(match st { - button::Status::Hovered | button::Status::Pressed => - Color { r: 0.60, g: 0.20, b: 0.18, a: 1.0 }, - _ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }, - })), - text_color: if is_model { DIM_COL } else { WARN_COL }, - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding([5, 10]), - Space::new().width(Fill), - button(text("Close").size(11)) - .on_press(Message::LayoutManagerClose) - .style(btn_style(false)) - .padding([5, 10]), - ].spacing(8), - ] - .spacing(6) - .padding(16) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) - .width(490) - .height(370); - - let catcher = mouse_area(container(Space::new().width(Fill).height(Fill)) - .width(Fill).height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.45 })), - ..Default::default() - }) - ).on_press(Message::LayoutManagerClose); - - let positioned = container(panel) - .width(Fill).height(Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - stack![catcher, positioned].into() -} - -// ── Keyboard Shortcuts Reference Panel ──────────────────────────────────── - -fn shortcuts_panel_overlay<'a>( - overrides: &'a std::collections::HashMap, -) -> Element<'a, Message> { - use iced::Length::Fill; - use iced::Color; - use iced::widget::scrollable; - - const PANEL_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_COL: Color = Color { r: 0.88, g: 0.88, b: 0.88, a: 1.0 }; - const DIM_COL: Color = Color { r: 0.55, g: 0.55, b: 0.55, a: 1.0 }; - const HEAD_COL: Color = Color { r: 0.40, g: 0.70, b: 1.00, a: 1.0 }; - - let shortcut_row = |key: &'static str, action: &'static str| -> Element<'_, Message> { - row![ - text(key).size(11).color(HEAD_COL).font(iced::Font::MONOSPACE).width(120), - text(action).size(11).color(TEXT_COL), - ].spacing(8).align_y(iced::Center).padding([2, 0]).into() - }; - - let section = |title: &'static str| -> Element<'_, Message> { - text(title).size(12).color(DIM_COL).into() - }; - - let builtins: Vec> = vec![ - section("── Function Keys ──────────────────────────────"), - shortcut_row("F3", "Toggle Object Snap"), - shortcut_row("F7", "Toggle Grid"), - shortcut_row("F8", "Toggle Ortho"), - shortcut_row("F9", "Toggle Grid Snap"), - shortcut_row("F10", "Toggle Polar Tracking"), - shortcut_row("F11", "Toggle Object Snap Tracking"), - shortcut_row("F12", "Toggle Dynamic Input"), - section("── Ctrl Shortcuts ─────────────────────────────"), - shortcut_row("Ctrl+N", "New Drawing"), - shortcut_row("Ctrl+O", "Open File"), - shortcut_row("Ctrl+S", "Save"), - shortcut_row("Ctrl+Shift+S","Save As"), - shortcut_row("Ctrl+Z", "Undo"), - shortcut_row("Ctrl+Shift+Z / Ctrl+Y", "Redo"), - shortcut_row("Ctrl+C", "Copy to Clipboard"), - shortcut_row("Ctrl+X", "Cut to Clipboard"), - shortcut_row("Ctrl+V", "Paste from Clipboard"), - section("── Other Keys ──────────────────────────────────"), - shortcut_row("Enter / Space", "Finalize command / Repeat last"), - shortcut_row("Escape", "Cancel active command"), - shortcut_row("Delete", "Delete selected entities"), - shortcut_row("↑ / ↓", "Command history navigation"), - ]; - - // User-defined overrides - let mut override_rows: Vec> = vec![ - section("── Custom Overrides (SHORTCUTS SET) ─────────"), - ]; - if overrides.is_empty() { - override_rows.push( - text(" (none — use: SHORTCUTS SET )") - .size(11).color(DIM_COL).into() - ); - } else { - let mut sorted: Vec<_> = overrides.iter().collect(); - sorted.sort_by_key(|(k, _)| k.as_str()); - for (key, cmd) in sorted { - let label: &str = key.as_str(); - let cmd_str: &str = cmd.as_str(); - override_rows.push( - row![ - text(label).size(11).color(HEAD_COL).font(iced::Font::MONOSPACE).width(120), - text(cmd_str).size(11).color(TEXT_COL), - ].spacing(8).align_y(iced::Center).padding([2, 0]).into() - ); - } - } - - let mut all_rows = builtins; - all_rows.extend(override_rows); - - let panel = container( - column![ - row![ - text("Keyboard Shortcuts").size(13).color(TEXT_COL), - Space::new().width(Fill), - button(text("✕").size(12).color(DIM_COL)) - .on_press(Message::ShortcutsPanelClose) - .style(|_: &Theme, _| button::Style { - background: Some(Background::Color(Color::TRANSPARENT)), - text_color: DIM_COL, - ..Default::default() - }) - .padding([2, 6]), - ].align_y(iced::Center), - text("Type SHORTCUTS SET to add custom shortcuts.") - .size(10).color(DIM_COL), - Space::new().height(8), - container( - scrollable( - column(all_rows).spacing(4).padding([0, 8]) - ).height(360) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 })), - border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, - ..Default::default() - }) - .padding(8), - Space::new().height(8), - row![ - Space::new().width(Fill), - button(text("Close").size(11)) - .on_press(Message::ShortcutsPanelClose) - .style(|_: &Theme, status| button::Style { - background: Some(Background::Color(match status { - button::Status::Hovered | button::Status::Pressed => - Color { r: 0.28, g: 0.28, b: 0.28, 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: 4.0.into() }, - ..Default::default() - }) - .padding([5, 12]), - ], - ] - .spacing(6) - .padding(16) - ) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(PANEL_BG)), - border: Border { color: BORDER, width: 1.0, radius: 6.0.into() }, - ..Default::default() - }) - .width(520) - .height(480); - - let catcher = mouse_area(container(Space::new().width(Fill).height(Fill)) - .width(Fill).height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(Color { r: 0.0, g: 0.0, b: 0.0, a: 0.45 })), - ..Default::default() - }) - ).on_press(Message::ShortcutsPanelClose); - - let positioned = container(panel) - .width(Fill).height(Fill) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); - - stack![catcher, positioned].into() -} diff --git a/src/ui/dimstyle.rs b/src/ui/dimstyle.rs new file mode 100644 index 000000000..1cc514f93 --- /dev/null +++ b/src/ui/dimstyle.rs @@ -0,0 +1,263 @@ +//! Dimension Style Manager window — fills the entire OS window. + +use crate::app::{DsField, Message}; +use iced::widget::{button, checkbox, column, container, row, scrollable, text, text_input, 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 FIELD: Color = Color { r: 0.10, g: 0.10, 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> { + pub dimdle: &'a str, pub dimdli: &'a str, pub dimgap: &'a str, + pub dimexe: &'a str, pub dimexo: &'a str, + pub dimsd1: bool, pub dimsd2: bool, + pub dimse1: bool, pub dimse2: bool, + pub dimasz: &'a str, pub dimcen: &'a str, pub dimtsz: &'a str, + pub dimtxt: &'a str, pub dimtxsty: &'a str, pub dimtad: &'a str, + pub dimtih: bool, pub dimtoh: bool, + pub dimscale: &'a str, pub dimlfac: &'a str, + pub dimlunit: &'a str, pub dimdec: &'a str, pub dimpost: &'a str, + pub dimtol: bool, pub dimlim: bool, + pub dimtp: &'a str, pub dimtm: &'a str, + pub dimtdec: &'a str, pub dimtfac: &'a str, +} + +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) { + (true, _) => ACTIVE, + (false, button::Status::Hovered | button::Status::Pressed) => Color { r: 0.28, g: 0.28, b: 0.28, a: 1.0 }, + _ => Color { r: 0.20, g: 0.20, b: 0.20, a: 1.0 }, + })), + text_color: TEXT, + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + ..Default::default() + } +} + +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), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + icon: TEXT, placeholder: DIM, value: TEXT, selection: ACCENT, + } +} + +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() +} + +pub fn view_window<'a>( + styles: Vec, + selected: &'a str, + tab: u8, + vals: DimStyleValues<'a>, +) -> 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)).on_press(Message::DimStyleDialogTab(0)).style(tab_btn_style(tab == 0)).padding([4, 10]), + button(text("Arrows").size(11)).on_press(Message::DimStyleDialogTab(1)).style(tab_btn_style(tab == 1)).padding([4, 10]), + button(text("Text").size(11)).on_press(Message::DimStyleDialogTab(2)).style(tab_btn_style(tab == 2)).padding([4, 10]), + button(text("Scale/Units").size(11)).on_press(Message::DimStyleDialogTab(3)).style(tab_btn_style(tab == 3)).padding([4, 10]), + button(text("Tolerances").size(11)).on_press(Message::DimStyleDialogTab(4)).style(tab_btn_style(tab == 4)).padding([4, 10]), + ].spacing(2); + + let lbl = |s: &'static str| text(s).size(11).color(DIM).width(180); + + let mk_field = |fld: DsField, val: &'a str| -> Element<'a, Message> { + text_input("", val) + .on_input(move |s| Message::DsEdit(fld.clone(), s)) + .style(field_style) + .size(11) + .width(100) + .into() + }; + + let chk = |label: &'static str, val: bool, fld: DsField| -> Element<'a, Message> { + checkbox(val) + .label(label) + .on_toggle(move |_| Message::DsToggle(fld.clone())) + .size(14) + .text_size(11) + .into() + }; + + let tab_content: Element<'_, Message> = match tab { + 0 => column![ + text("Dimension Line").size(11).color(ACCENT), + row![lbl("Extension (DIMDLE)"), mk_field(DsField::Dimdle, vals.dimdle)].spacing(8).align_y(iced::Center), + row![lbl("Spacing (DIMDLI)"), mk_field(DsField::Dimdli, vals.dimdli)].spacing(8).align_y(iced::Center), + row![lbl("Text gap (DIMGAP)"), mk_field(DsField::Dimgap, vals.dimgap)].spacing(8).align_y(iced::Center), + chk("Suppress 1st line (DIMSD1)", vals.dimsd1, DsField::Dimsd1), + chk("Suppress 2nd line (DIMSD2)", vals.dimsd2, DsField::Dimsd2), + text("Extension Line").size(11).color(ACCENT), + row![lbl("Extension (DIMEXE)"), mk_field(DsField::Dimexe, vals.dimexe)].spacing(8).align_y(iced::Center), + row![lbl("Offset (DIMEXO)"), mk_field(DsField::Dimexo, vals.dimexo)].spacing(8).align_y(iced::Center), + chk("Suppress 1st line (DIMSE1)", vals.dimse1, DsField::Dimse1), + chk("Suppress 2nd line (DIMSE2)", vals.dimse2, DsField::Dimse2), + ].spacing(7).into(), + 1 => column![ + text("Arrows").size(11).color(ACCENT), + row![lbl("Arrow size (DIMASZ)"), mk_field(DsField::Dimasz, vals.dimasz)].spacing(8).align_y(iced::Center), + row![lbl("Center mark (DIMCEN)"), mk_field(DsField::Dimcen, vals.dimcen)].spacing(8).align_y(iced::Center), + row![lbl("Tick size (DIMTSZ)"), mk_field(DsField::Dimtsz, vals.dimtsz)].spacing(8).align_y(iced::Center), + ].spacing(7).into(), + 2 => column![ + text("Text").size(11).color(ACCENT), + row![lbl("Height (DIMTXT)"), mk_field(DsField::Dimtxt, vals.dimtxt)].spacing(8).align_y(iced::Center), + row![lbl("Style (DIMTXSTY)"), mk_field(DsField::Dimtxsty, vals.dimtxsty)].spacing(8).align_y(iced::Center), + row![lbl("Vertical pos (DIMTAD)"), mk_field(DsField::Dimtad, vals.dimtad)].spacing(8).align_y(iced::Center), + chk("Horizontal inside (DIMTIH)", vals.dimtih, DsField::Dimtih), + chk("Horizontal outside (DIMTOH)", vals.dimtoh, DsField::Dimtoh), + ].spacing(7).into(), + 3 => column![ + text("Scale").size(11).color(ACCENT), + row![lbl("Overall scale (DIMSCALE)"), mk_field(DsField::Dimscale, vals.dimscale)].spacing(8).align_y(iced::Center), + row![lbl("Linear factor (DIMLFAC)"), mk_field(DsField::Dimlfac, vals.dimlfac)].spacing(8).align_y(iced::Center), + text("Units").size(11).color(ACCENT), + row![lbl("Format (DIMLUNIT)"), mk_field(DsField::Dimlunit, vals.dimlunit)].spacing(8).align_y(iced::Center), + row![lbl("Decimals (DIMDEC)"), mk_field(DsField::Dimdec, vals.dimdec)].spacing(8).align_y(iced::Center), + row![lbl("Suffix (DIMPOST)"), mk_field(DsField::Dimpost, vals.dimpost)].spacing(8).align_y(iced::Center), + ].spacing(7).into(), + _ => column![ + text("Tolerances").size(11).color(ACCENT), + chk("Generate tolerances (DIMTOL)", vals.dimtol, DsField::Dimtol), + chk("Limits generation (DIMLIM)", vals.dimlim, DsField::Dimlim), + row![lbl("Plus tolerance (DIMTP)"), mk_field(DsField::Dimtp, vals.dimtp)].spacing(8).align_y(iced::Center), + row![lbl("Minus tolerance (DIMTM)"), mk_field(DsField::Dimtm, vals.dimtm)].spacing(8).align_y(iced::Center), + row![lbl("Tol. decimals (DIMTDEC)"), mk_field(DsField::Dimtdec, vals.dimtdec)].spacing(8).align_y(iced::Center), + row![lbl("Tol. scale (DIMTFAC)"), mk_field(DsField::Dimtfac, vals.dimtfac)].spacing(8).align_y(iced::Center), + ].spacing(7).into(), + }; + + // ── Right panel: tabs + scrollable content ──────────────────────────── + let right_panel = container( + column![ + text(format!("Editing: {selected}")).size(11).color(DIM), + tabs, + hdivider(), + scrollable( + container(tab_content).padding([12, 12]) + ).height(Fill), + ] + .spacing(6) + .height(Fill) + ) + .height(Fill) + .width(Fill) + .padding(iced::Padding { top: 12.0, right: 12.0, bottom: 12.0, 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() +} diff --git a/src/ui/layout_manager.rs b/src/ui/layout_manager.rs new file mode 100644 index 000000000..c92671ea0 --- /dev/null +++ b/src/ui/layout_manager.rs @@ -0,0 +1,192 @@ +//! Layout Manager window — fills the entire OS window. + +use crate::app::Message; +use iced::widget::{button, column, container, row, scrollable, text, text_input, 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 FIELD: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }; +const LIST: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 }; +const WARN: Color = Color { r: 0.80, g: 0.35, b: 0.25, 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 field_style(_: &Theme, _: text_input::Status) -> text_input::Style { + text_input::Style { + background: Background::Color(FIELD), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + icon: TEXT, placeholder: DIM, value: TEXT, selection: ACCENT, + } +} + +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>( + layouts: Vec, + selected: &'a str, + rename_buf: &'a str, + current: String, +) -> Element<'a, Message> { + let is_model = selected == "Model"; + + // ── Toolbar ─────────────────────────────────────────────────────────── + let toolbar = container( + row![ + button(text("New Layout").size(11)).on_press(Message::LayoutManagerNew).style(btn_s(false)).padding([4, 10]), + button(text("Delete").size(11)) + .on_press(Message::LayoutManagerDelete) + .style(move |_: &Theme, st| button::Style { + background: Some(Background::Color(match st { + button::Status::Hovered | button::Status::Pressed => Color { r: 0.60, g: 0.20, b: 0.18, a: 1.0 }, + _ => Color { r: 0.22, g: 0.22, b: 0.22, a: 1.0 }, + })), + text_color: if is_model { DIM } else { WARN }, + border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, + ..Default::default() + }) + .padding([4, 10]), + Space::new().width(Fill), + button(text("◀ Move Left").size(11)).on_press(Message::LayoutManagerMoveLeft).style(btn_s(false)).padding([4, 8]), + button(text("Move Right ▶").size(11)).on_press(Message::LayoutManagerMoveRight).style(btn_s(false)).padding([4, 8]), + button(text("Set Current").size(11)).on_press(Message::LayoutManagerSetCurrent).style(btn_s(true)).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: Layout list ───────────────────────────────────────────────── + let list_items: Vec> = layouts.iter().map(|name| { + let is_sel = name.as_str() == selected; + let is_cur = name.as_str() == current.as_str(); + let label = if is_cur { format!("{name} ◀") } else { name.clone() }; + button(text(label).size(12)) + .on_press(Message::LayoutManagerSelect(name.clone())) + .style(list_item(is_sel)) + .padding([5, 10]) + .width(Fill) + .into() + }).collect(); + + let layout_list = container( + column![ + text("Layouts").size(10).color(DIM), + container(scrollable(column(list_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(220) + .height(Fill) + .padding(iced::Padding { top: 12.0, right: 8.0, bottom: 12.0, left: 12.0 }); + + // ── Right: Details + rename ─────────────────────────────────────────── + let details = container( + column![ + text(if is_model { "Model Space" } else { "Paper Space Layout" }) + .size(13).color(TEXT), + Space::new().height(8), + row![ + text("Name:").size(11).color(DIM).width(80), + text(selected).size(11), + ].spacing(8).align_y(iced::Center), + row![ + text("Status:").size(11).color(DIM).width(80), + text(if selected == current.as_str() { "Active ◀" } else { "Inactive" }) + .size(11) + .color(if selected == current.as_str() { ACCENT } else { DIM }), + ].spacing(8).align_y(iced::Center), + Space::new().height(16), + text("Rename").size(10).color(DIM), + row![ + text_input("New name…", rename_buf) + .on_input(Message::LayoutManagerRenameBuf) + .on_submit(Message::LayoutManagerRenameCommit) + .style(field_style) + .size(11) + .padding([4, 8]), + button(text("OK").size(11)) + .on_press(Message::LayoutManagerRenameCommit) + .style(btn_s(true)) + .padding([4, 10]), + ].spacing(6).align_y(iced::Center), + ] + .spacing(8) + ) + .width(Fill) + .padding([12, 12]); + + let body = row![layout_list, vsep(), details].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() +} diff --git a/src/ui/mlstyle.rs b/src/ui/mlstyle.rs new file mode 100644 index 000000000..2d37cee42 --- /dev/null +++ b/src/ui/mlstyle.rs @@ -0,0 +1,170 @@ +//! 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}; + +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, +) -> 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![ + text(label).size(11).color(DIM).width(120), + text(val).size(11), + ].spacing(8).align_y(iced::Center).into() + }; + + let details: Element<'_, Message> = if let Some(s) = selected_style { + let elem_rows: Vec> = s.elements.iter().enumerate().map(|(idx, e)| { + let color_str = match &e.color { + acadrust::types::Color::ByLayer => "ByLayer".into(), + acadrust::types::Color::ByBlock => "ByBlock".into(), + acadrust::types::Color::Index(i) => format!("ACI {i}"), + acadrust::types::Color::Rgb { r, g, b } => format!("#{r:02X}{g:02X}{b:02X}"), + }; + let lt = if e.linetype.is_empty() { "ByLayer" } else { &e.linetype }; + row![ + text(format!(" {idx}:")).size(10).color(DIM).width(24), + text(format!("{:+.3}", e.offset)).size(10).width(70), + text(color_str).size(10).width(90), + text(lt).size(10), + ].spacing(4).align_y(iced::Center).into() + }).collect(); + + let mut col_items: Vec> = vec![ + info_row("Name:", s.name.clone()), + info_row("Elements:", s.elements.len().to_string()), + text(" Off Color Ltype").size(10).color(DIM).into(), + ]; + col_items.extend(elem_rows); + scrollable(column(col_items).spacing(6).padding([12, 12])).height(Fill).into() + } else { + container(text("Select a style to view details.").size(11).color(DIM)).padding([12, 12]).into() + }; + + 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() +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index a69efe28f..7806c9240 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -4,12 +4,20 @@ pub const ROW_H: f32 = 26.0; pub mod app_menu; pub mod command_line; +pub mod dimstyle; pub mod layers; +pub mod layout_manager; +pub mod mlstyle; pub mod overlay; +pub mod page_setup; +pub mod plotstyle; pub mod properties; pub mod ribbon; +pub mod shortcuts; pub mod snap_popup; pub mod statusbar; +pub mod tablestyle; +pub mod textstyle; pub use app_menu::AppMenu; pub use command_line::CommandLine; diff --git a/src/ui/page_setup.rs b/src/ui/page_setup.rs new file mode 100644 index 000000000..5c18b68ef --- /dev/null +++ b/src/ui/page_setup.rs @@ -0,0 +1,199 @@ +//! Page Setup window — fills the entire OS window. + +use crate::app::Message; +use iced::widget::{button, column, container, row, scrollable, text, text_input, 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 FIELD: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }; + +fn btn(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() }, + shadow: iced::Shadow::default(), + snap: false, + } +} + +fn pill(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.20, g: 0.20, b: 0.20, a: 1.0 }, + })), + text_color: TEXT, + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + shadow: iced::Shadow::default(), + snap: false, + } +} + +fn field_style(_: &Theme, _: text_input::Status) -> text_input::Style { + text_input::Style { + background: Background::Color(FIELD), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + icon: TEXT, placeholder: DIM, value: TEXT, selection: ACCENT, + } +} + +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 section_label<'a>(s: &'static str) -> Element<'a, Message> { + text(s).size(11).color(DIM).into() +} + +pub fn view_window<'a>( + w_buf: &'a str, + h_buf: &'a str, + plot_area: &'a str, + center: bool, + offset_x: &'a str, + offset_y: &'a str, + rotation: &'a str, + scale: &'a str, +) -> Element<'a, Message> { + // ── Toolbar ─────────────────────────────────────────────────────────── + let toolbar = container( + row![ + button(text("Cancel").size(12)) + .on_press(Message::PageSetupClose) + .style(btn(false)) + .padding([4, 14]), + Space::new().width(Fill), + button(text("OK").size(12)) + .on_press(Message::PageSetupCommit) + .style(btn(true)) + .padding([4, 20]), + ] + .align_y(iced::Center) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(TB)), + ..Default::default() + }) + .width(Fill) + .padding([5, 10]); + + let lbl = |s: &'static str| text(s).size(11).color(DIM).width(130); + + // ── Paper size presets ──────────────────────────────────────────────── + let presets1 = row![ + button(text("A4 P").size(10)).on_press(Message::PageSetupPreset("A4 Portrait".into())).style(pill(false)).padding([3, 6]), + button(text("A4 L").size(10)).on_press(Message::PageSetupPreset("A4 Landscape".into())).style(pill(false)).padding([3, 6]), + button(text("A3 P").size(10)).on_press(Message::PageSetupPreset("A3 Portrait".into())).style(pill(false)).padding([3, 6]), + button(text("A3 L").size(10)).on_press(Message::PageSetupPreset("A3 Landscape".into())).style(pill(false)).padding([3, 6]), + ].spacing(4); + + let presets2 = row![ + button(text("A2 L").size(10)).on_press(Message::PageSetupPreset("A2 Landscape".into())).style(pill(false)).padding([3, 6]), + button(text("A1 L").size(10)).on_press(Message::PageSetupPreset("A1 Landscape".into())).style(pill(false)).padding([3, 6]), + button(text("A0 L").size(10)).on_press(Message::PageSetupPreset("A0 Landscape".into())).style(pill(false)).padding([3, 6]), + button(text("Letter").size(10)).on_press(Message::PageSetupPreset("Letter Landscape".into())).style(pill(false)).padding([3, 6]), + ].spacing(4); + + // ── Rotation buttons ────────────────────────────────────────────────── + let rot_row = row![ + button(text("0°").size(10)).on_press(Message::PageSetupRotation("0".into())).style(pill(rotation == "0")).padding([3, 8]), + button(text("90°").size(10)).on_press(Message::PageSetupRotation("90".into())).style(pill(rotation == "90")).padding([3, 8]), + button(text("180°").size(10)).on_press(Message::PageSetupRotation("180".into())).style(pill(rotation == "180")).padding([3, 8]), + button(text("270°").size(10)).on_press(Message::PageSetupRotation("270".into())).style(pill(rotation == "270")).padding([3, 8]), + ].spacing(4); + + // ── Scale buttons ───────────────────────────────────────────────────── + let scale_row1 = row![ + button(text("Fit").size(10)).on_press(Message::PageSetupScale("Fit".into())).style(pill(scale == "Fit")).padding([3, 8]), + button(text("1:1").size(10)).on_press(Message::PageSetupScale("1:1".into())).style(pill(scale == "1:1")).padding([3, 8]), + button(text("1:2").size(10)).on_press(Message::PageSetupScale("1:2".into())).style(pill(scale == "1:2")).padding([3, 8]), + button(text("1:5").size(10)).on_press(Message::PageSetupScale("1:5".into())).style(pill(scale == "1:5")).padding([3, 8]), + button(text("1:10").size(10)).on_press(Message::PageSetupScale("1:10".into())).style(pill(scale == "1:10")).padding([3, 8]), + ].spacing(4); + + let scale_row2 = row![ + button(text("1:20").size(10)).on_press(Message::PageSetupScale("1:20".into())).style(pill(scale == "1:20")).padding([3, 8]), + button(text("1:50").size(10)).on_press(Message::PageSetupScale("1:50".into())).style(pill(scale == "1:50")).padding([3, 8]), + button(text("1:100").size(10)).on_press(Message::PageSetupScale("1:100".into())).style(pill(scale == "1:100")).padding([3, 8]), + button(text("2:1").size(10)).on_press(Message::PageSetupScale("2:1".into())).style(pill(scale == "2:1")).padding([3, 8]), + ].spacing(4); + + // ── Main scrollable form ────────────────────────────────────────────── + let form = column![ + section_label("Paper Size"), + presets1, + presets2, + row![ + lbl("Width (mm)"), + text_input("297", w_buf).on_input(Message::PageSetupWidthEdit).on_submit(Message::PageSetupCommit).style(field_style).size(12).width(90), + ].spacing(8).align_y(iced::Center), + row![ + lbl("Height (mm)"), + text_input("210", h_buf).on_input(Message::PageSetupHeightEdit).on_submit(Message::PageSetupCommit).style(field_style).size(12).width(90), + ].spacing(8).align_y(iced::Center), + hdivider(), + section_label("Plot Area"), + row![ + button(text("Layout").size(10)).on_press(Message::PageSetupPlotArea("Layout".into())).style(pill(plot_area == "Layout")).padding([3, 8]), + button(text("Extents").size(10)).on_press(Message::PageSetupPlotArea("Extents".into())).style(pill(plot_area == "Extents")).padding([3, 8]), + ].spacing(6), + hdivider(), + section_label("Position"), + button( + text(if center { "✓ Center on page" } else { " Center on page" }).size(11) + ) + .on_press(Message::PageSetupCenterToggle) + .style(pill(center)) + .padding([4, 10]), + row![ + lbl("Offset X (mm)"), + text_input("0", offset_x).on_input(Message::PageSetupOffsetXEdit).style(field_style).size(12).width(90), + ].spacing(8).align_y(iced::Center), + row![ + lbl("Offset Y (mm)"), + text_input("0", offset_y).on_input(Message::PageSetupOffsetYEdit).style(field_style).size(12).width(90), + ].spacing(8).align_y(iced::Center), + hdivider(), + section_label("Rotation"), + rot_row, + hdivider(), + section_label("Plot Scale"), + scale_row1, + scale_row2, + ] + .spacing(10) + .padding(16) + .width(Fill); + + let content = scrollable(form).width(Fill).height(Fill); + + container( + column![toolbar, hdivider(), content].spacing(0) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BG)), + ..Default::default() + }) + .width(Fill) + .height(Fill) + .into() +} diff --git a/src/ui/plotstyle.rs b/src/ui/plotstyle.rs new file mode 100644 index 000000000..868f6a2d7 --- /dev/null +++ b/src/ui/plotstyle.rs @@ -0,0 +1,206 @@ +//! Plot Style Table Editor window — fills the entire OS window. + +use crate::app::Message; +use iced::widget::{button, column, container, row, scrollable, text, text_input, 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 FIELD: Color = Color { r: 0.10, g: 0.10, b: 0.10, 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 field_style(_: &Theme, _: text_input::Status) -> text_input::Style { + text_input::Style { + background: Background::Color(FIELD), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + icon: TEXT, placeholder: DIM, value: TEXT, selection: ACCENT, + } +} + +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>( + table: Option<&'a crate::io::plot_style::PlotStyleTable>, + selected_aci: u8, + color_buf: &'a str, + lw_buf: &'a str, + screen_buf: &'a str, +) -> Element<'a, Message> { + let table_name = table.map(|t| t.name.as_str()).unwrap_or("(no table loaded)"); + + // ── Toolbar ─────────────────────────────────────────────────────────── + let toolbar = container( + row![ + button(text("Load CTB/STB").size(11)).on_press(Message::PlotStyleLoad).style(btn_s(false)).padding([4, 10]), + button(text("Save As…").size(11)).on_press(Message::PlotStylePanelSave).style(btn_s(false)).padding([4, 10]), + button(text("Clear Table").size(11)).on_press(Message::PlotStyleClear).style(btn_s(false)).padding([4, 10]), + Space::new().width(Fill), + text(table_name).size(10).color(DIM), + ] + .spacing(4) + .align_y(iced::Center) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(TB)), + ..Default::default() + }) + .width(Fill) + .padding([5, 8]); + + // ── Left: ACI list ──────────────────────────────────────────────────── + let aci_items: Vec> = (1u8..=255).map(|aci| { + let is_sel = aci == selected_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) + .unwrap_or(false); + let lw_str = table.and_then(|t| t.aci_entries.get(aci as usize)) + .and_then(|e| { + if e.lineweight != 255 { + crate::io::plot_style::LW_TABLE.get(e.lineweight as usize) + .map(|lw| format!("{lw:.2}mm")) + } else { None } + }).unwrap_or_default(); + let color_str = table.and_then(|t| t.aci_entries.get(aci as usize)) + .and_then(|e| e.color.map(|[r, g, b]| format!("#{r:02X}{g:02X}{b:02X}"))) + .unwrap_or_default(); + let label = if has_override { + format!("{aci:>3} {color_str:<9} {lw_str}") + } else { + format!("{aci:>3} (default)") + }; + button(text(label).size(10).font(iced::Font::MONOSPACE)) + .on_press(Message::PlotStylePanelSelectAci(aci)) + .style(move |_: &Theme, st| button::Style { + background: Some(Background::Color(match (is_sel, 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() + }) + .padding([2, 8]) + .width(Fill) + .into() + }).collect(); + + let aci_list = container( + column![ + text("ACI Color Index").size(10).color(DIM), + container( + scrollable(column(aci_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(280) + .height(Fill) + .padding(iced::Padding { top: 12.0, right: 8.0, bottom: 12.0, left: 12.0 }); + + // ── Right: Edit panel ───────────────────────────────────────────────── + let entry = table.and_then(|t| t.aci_entries.get(selected_aci as usize)); + let cur_color = entry.and_then(|e| e.color.map(|[r,g,b]| format!("#{r:02X}{g:02X}{b:02X}"))).unwrap_or_else(|| "(none)".into()); + let cur_lw = entry.map(|e| if e.lineweight == 255 { "object".into() } else { + crate::io::plot_style::LW_TABLE.get(e.lineweight as usize) + .map(|lw| format!("{lw:.2}mm (idx {})", e.lineweight)) + .unwrap_or_else(|| format!("idx {}", e.lineweight)) + }).unwrap_or_else(|| "—".into()); + let cur_scr = entry.map(|e| format!("{}%", e.screening)).unwrap_or_else(|| "—".into()); + + let lbl = |s: &'static str| text(s).size(11).color(DIM); + + let edit_panel = container( + column![ + row![ + text("ACI:").size(11).color(DIM).width(100), + text(format!("{selected_aci}")).size(11), + ].spacing(8).align_y(iced::Center), + lbl("Color override (#RRGGBB):"), + text_input("#RRGGBB or blank", color_buf) + .on_input(Message::PlotStylePanelColorBuf) + .style(field_style).size(11).padding([4, 8]), + lbl("Lineweight index (0-24, 255=obj):"), + text_input("255", lw_buf) + .on_input(Message::PlotStylePanelLwBuf) + .style(field_style).size(11).padding([4, 8]), + lbl("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("Current values:").size(10).color(DIM), + text(format!(" Color: {cur_color}")).size(10), + text(format!(" Lineweight: {cur_lw}")).size(10), + text(format!(" Screening: {cur_scr}")).size(10), + Space::new().height(Fill), + button(text("Apply to ACI").size(11)) + .on_press(Message::PlotStylePanelApply) + .style(btn_s(true)) + .padding([5, 10]), + ] + .spacing(8) + .height(Fill) + ) + .width(Fill) + .height(Fill) + .padding([12, 12]); + + let body = row![aci_list, vsep(), edit_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() +} diff --git a/src/ui/shortcuts.rs b/src/ui/shortcuts.rs new file mode 100644 index 000000000..64d1ce9c7 --- /dev/null +++ b/src/ui/shortcuts.rs @@ -0,0 +1,141 @@ +//! Keyboard Shortcuts Reference window — fills the entire OS window. + +use crate::app::Message; +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 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 KEY: Color = Color { r: 0.40, g: 0.70, b: 1.00, a: 1.0 }; + +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 shortcut_row<'a>(key: &'static str, action: &'static str) -> Element<'a, Message> { + row![ + text(key).size(11).color(KEY).font(iced::Font::MONOSPACE).width(160), + text(action).size(11), + ] + .spacing(8) + .align_y(iced::Center) + .padding([2, 0]) + .into() +} + +fn section<'a>(title: &'static str) -> Element<'a, Message> { + container(text(title).size(11).color(DIM)) + .padding(iced::Padding { top: 6.0, right: 0.0, bottom: 2.0, left: 0.0 }) + .into() +} + +pub fn view_window<'a>( + overrides: &'a std::collections::HashMap, +) -> Element<'a, Message> { + // ── Toolbar ─────────────────────────────────────────────────────────── + let toolbar = container( + row![ + text("Type SHORTCUTS SET to add custom shortcuts.") + .size(10) + .color(DIM), + ] + .align_y(iced::Center) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(TB)), + ..Default::default() + }) + .width(Fill) + .padding([5, 10]); + + // ── Shortcut entries ────────────────────────────────────────────────── + let mut rows: Vec> = vec![ + section("── Function Keys ──────────────────────────────────────"), + shortcut_row("F3", "Toggle Object Snap"), + shortcut_row("F7", "Toggle Grid"), + shortcut_row("F8", "Toggle Ortho"), + shortcut_row("F9", "Toggle Grid Snap"), + shortcut_row("F10", "Toggle Polar Tracking"), + shortcut_row("F11", "Toggle Object Snap Tracking"), + shortcut_row("F12", "Toggle Dynamic Input"), + section("── Ctrl Shortcuts ──────────────────────────────────────"), + shortcut_row("Ctrl+N", "New Drawing"), + shortcut_row("Ctrl+O", "Open File"), + shortcut_row("Ctrl+S", "Save"), + shortcut_row("Ctrl+Shift+S", "Save As"), + shortcut_row("Ctrl+Z", "Undo"), + shortcut_row("Ctrl+Shift+Z / Ctrl+Y","Redo"), + shortcut_row("Ctrl+C", "Copy to Clipboard"), + shortcut_row("Ctrl+X", "Cut to Clipboard"), + shortcut_row("Ctrl+V", "Paste from Clipboard"), + section("── Other Keys ──────────────────────────────────────────"), + shortcut_row("Enter / Space", "Finalize command / Repeat last"), + shortcut_row("Escape", "Cancel active command"), + shortcut_row("Delete", "Delete selected entities"), + shortcut_row("↑ / ↓", "Command history navigation"), + ]; + + // Custom overrides section + rows.push(section("── Custom Overrides (SHORTCUTS SET) ──────────────────")); + if overrides.is_empty() { + rows.push( + text(" (none — use: SHORTCUTS SET )") + .size(11).color(DIM).into() + ); + } else { + let mut sorted: Vec<_> = overrides.iter().collect(); + sorted.sort_by_key(|(k, _)| k.as_str()); + for (key, cmd) in sorted { + rows.push( + row![ + text(key.as_str()).size(11).color(KEY).font(iced::Font::MONOSPACE).width(160), + text(cmd.as_str()).size(11), + ] + .spacing(8).align_y(iced::Center).padding([2, 0]) + .into() + ); + } + } + + // ── Section headers styled separately ──────────────────────────────── + let content = scrollable( + column(rows).spacing(3).padding([12, 16]) + ) + .width(Fill) + .height(Fill); + + // ── Header row with accent ──────────────────────────────────────────── + let header = container( + row![ + text("Key").size(10).color(ACCENT).width(160), + text("Action").size(10).color(ACCENT), + ] + .spacing(8) + .padding([4, 16]) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(Color { r: 0.13, g: 0.13, b: 0.18, a: 1.0 })), + ..Default::default() + }) + .width(Fill); + + container( + column![toolbar, hdivider(), header, hdivider(), content].spacing(0) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BG)), + ..Default::default() + }) + .width(Fill) + .height(Fill) + .into() +} diff --git a/src/ui/tablestyle.rs b/src/ui/tablestyle.rs new file mode 100644 index 000000000..8adbef6e8 --- /dev/null +++ b/src/ui/tablestyle.rs @@ -0,0 +1,166 @@ +//! Table 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}; + +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::TableStyle>, +) -> 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![ + text(label).size(11).color(DIM).width(160), + text(val).size(11), + ].spacing(8).align_y(iced::Center).into() + }; + + let row_info = |row_label: &'static str, rs: &acadrust::objects::RowCellStyle| -> Element<'_, Message> { + column![ + text(row_label).size(11).color(ACCENT), + info_row(" Text Style:", rs.text_style_name.clone()), + info_row(" Text Height:", format!("{:.4}", rs.text_height)), + info_row(" Alignment:", format!("{:?}", rs.alignment)), + ].spacing(3).into() + }; + + let details: Element<'_, Message> = if let Some(s) = selected_style { + scrollable( + column![ + info_row("Name:", s.name.clone()), + info_row("H Margin:", format!("{:.4}", s.horizontal_margin)), + info_row("V Margin:", format!("{:.4}", s.vertical_margin)), + info_row("Title Suppressed:", s.title_suppressed.to_string()), + info_row("Header Suppressed:", s.header_suppressed.to_string()), + row_info("Data Row:", &s.data_row_style), + row_info("Header Row:", &s.header_row_style), + row_info("Title Row:", &s.title_row_style), + ].spacing(6).padding([12, 12]) + ).height(Fill).into() + } else { + container( + text("Select a style to view details.").size(11).color(DIM) + ).padding([12, 12]).into() + }; + + 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() +} diff --git a/src/ui/textstyle.rs b/src/ui/textstyle.rs new file mode 100644 index 000000000..da7329566 --- /dev/null +++ b/src/ui/textstyle.rs @@ -0,0 +1,220 @@ +//! Text Style Font Browser window — fills the entire OS window. + +use crate::app::Message; +use iced::widget::{button, column, container, row, scrollable, text, text_input, 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 FIELD: Color = Color { r: 0.10, g: 0.10, b: 0.10, a: 1.0 }; +const LIST: Color = Color { r: 0.12, g: 0.12, b: 0.12, a: 1.0 }; + +const BUILTIN_FONTS: &[&str] = &[ + "CourierCad.cxf", "Cursive.cxf", "GothGBT.cxf", "GothGRT.cxf", "GothITT.cxf", + "GreekC.cxf", "GreekS.cxf", "ItalicC.cxf", "ItalicT.cxf", + "RomanC.cxf", "RomanD.cxf", "RomanS.cxf", "RomanT.cxf", + "SansND.cxf", "SansNS.cxf", "ScriptC.cxf", "ScriptS.cxf", + "Standard.cxf", "Unicode.cxf", "SymbolCad.cxf", +]; + +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 field_style(_: &Theme, _: text_input::Status) -> text_input::Style { + text_input::Style { + background: Background::Color(FIELD), + border: Border { color: BORDER, width: 1.0, radius: 3.0.into() }, + icon: TEXT, placeholder: DIM, value: TEXT, selection: ACCENT, + } +} + +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, + font_buf: &'a str, + width_buf: &'a str, + oblique_buf: &'a str, +) -> Element<'a, Message> { + // ── 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().map(|&f| { + let is_sel = font_buf == f; + button(text(f).size(10)) + .on_press(Message::TextStyleFontPick(f.to_string())) + .style(list_item(is_sel)) + .padding([3, 8]) + .width(Fill) + .into() + }).collect(); + + let font_panel = container( + column![ + text("Font File").size(10).color(DIM), + container(scrollable(column(font_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), + text_input("font file…", font_buf) + .on_input(|v| Message::TextStyleEdit { field: "font", value: v }) + .style(field_style) + .size(11) + .width(Fill), + ] + .spacing(6) + .height(Fill) + ) + .width(190) + .height(Fill) + .padding([12, 8]); + + // ── Right: Properties ───────────────────────────────────────────────── + let props_panel = container( + column![ + text("Properties").size(11).color(ACCENT), + row![ + text("Width Factor:").size(11).color(DIM).width(120), + text_input("1.0", width_buf) + .on_input(|v| Message::TextStyleEdit { field: "width", value: v }) + .style(field_style).size(11).width(90), + ].spacing(6).align_y(iced::Center), + row![ + text("Oblique (°):").size(11).color(DIM).width(120), + text_input("0.0", oblique_buf) + .on_input(|v| Message::TextStyleEdit { field: "oblique", value: v }) + .style(field_style).size(11).width(90), + ].spacing(6).align_y(iced::Center), + Space::new().height(8), + text("Preview").size(10).color(DIM), + container( + text("AaBbCc 0123").size(20) + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(FIELD)), + border: Border { color: BORDER, width: 1.0, radius: 4.0.into() }, + ..Default::default() + }) + .padding(12) + .width(Fill), + ] + .spacing(10) + .height(Fill) + ) + .width(Fill) + .height(Fill) + .padding(iced::Padding { top: 12.0, right: 12.0, bottom: 12.0, left: 8.0 }); + + let body = row![style_panel, vsep(), 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() +}