From 552ab7eb6a2c0a3215ba07f7e68f1bcc187fbe38 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 6 Apr 2026 14:57:11 +0300 Subject: [PATCH] feat: TableStyle manager (TABLESTYLE command + dialog) - TABLESTYLE/TS command: LIST, NEW subcommands for managing table styles - Table Style Manager dialog: style list with details panel showing name, margins, suppression flags, and row styles (data/header/title) with text style, height, and alignment per row type - New / Delete buttons in the dialog Co-Authored-By: Claude Sonnet 4.6 --- src/app/commands.rs | 53 ++++++++++++++ src/app/mod.rs | 13 ++++ src/app/update.rs | 67 ++++++++++++++++++ src/app/view.rs | 163 +++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 295 insertions(+), 1 deletion(-) diff --git a/src/app/commands.rs b/src/app/commands.rs index 696df755f..b9ba1782e 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -2085,6 +2085,59 @@ impl H7CAD { } // ── DimStyle management ─────────────────────────────────────── + // TABLESTYLE — Table Style Manager. + cmd if cmd == "TABLESTYLE" || cmd == "TS" || cmd.starts_with("TABLESTYLE ") => { + use acadrust::objects::{TableStyle, ObjectType}; + let raw_rest = cmd.split_once(' ').map(|(_, r)| r.trim()).unwrap_or(""); + let parts: Vec<&str> = raw_rest.split_whitespace().collect(); + let sub = parts.first().map(|s| s.to_uppercase()).unwrap_or_default(); + match sub.as_str() { + "" | "DIALOG" | "UI" => { + return Task::done(Message::TableStyleDialogOpen); + } + "LIST" | "?" => { + let doc = &self.tabs[i].scene.document; + let styles: Vec = doc.objects.values() + .filter_map(|o| if let ObjectType::TableStyle(s) = o { Some(s) } else { None }) + .map(|s| format!("{} (h_margin:{:.2} v_margin:{:.2})", s.name, s.horizontal_margin, s.vertical_margin)) + .collect(); + if styles.is_empty() { + self.command_line.push_output("No table styles."); + } else { + self.command_line.push_output(&format!("TableStyles:\n {}", styles.join("\n "))); + } + } + "NEW" | "N" => { + let name = parts.get(1).copied().unwrap_or("").to_string(); + if name.is_empty() { + self.command_line.push_error("Usage: TABLESTYLE NEW "); + } else { + let doc = &self.tabs[i].scene.document; + let exists = doc.objects.values().any(|o| { + matches!(o, ObjectType::TableStyle(s) if s.name.eq_ignore_ascii_case(&name)) + }); + if exists { + self.command_line.push_error(&format!("TABLESTYLE: '{}' already exists.", name)); + } else { + self.push_undo_snapshot(i, "TABLESTYLE NEW"); + let mut style = TableStyle::standard(); + style.name = name.clone(); + let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle()); + style.handle = nh; + self.tabs[i].scene.document.objects.insert(nh, ObjectType::TableStyle(style)); + self.tabs[i].dirty = true; + self.command_line.push_output(&format!("TABLESTYLE: '{}' created.", name)); + } + } + } + _ => { + self.command_line.push_error( + "Usage: TABLESTYLE [LIST|NEW ]" + ); + } + } + } + // MLSTYLE — Multiline Style Manager. // Usage: // MLSTYLE — open dialog diff --git a/src/app/mod.rs b/src/app/mod.rs index 2d9fadfc6..9f1398c80 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -94,6 +94,10 @@ pub(super) struct H7CAD { mlstyle_open: bool, mlstyle_selected: String, + // ── TableStyle Dialog ───────────────────────────────────────────────── + tablestyle_open: bool, + tablestyle_selected: String, + // ── DimStyle Dialog ─────────────────────────────────────────────────── dimstyle_open: bool, /// Name of the style currently shown in the dialog. @@ -339,6 +343,12 @@ pub enum Message { PlotStyleLoaded(Option), /// Clear the active plot style table. PlotStyleClear, + // ── TableStyle Dialog ───────────────────────────────────────────────── + TableStyleDialogOpen, + TableStyleDialogClose, + TableStyleDialogSelect(String), + TableStyleDialogNew, + TableStyleDialogDelete, // ── MLineStyle Dialog ───────────────────────────────────────────────── MlStyleDialogOpen, MlStyleDialogClose, @@ -407,6 +417,9 @@ impl H7CAD { page_setup_scale: "Fit".to_string(), // Plot style active_plot_style: None, + // TableStyle dialog + tablestyle_open: false, + tablestyle_selected: "Standard".to_string(), // MLineStyle dialog mlstyle_open: false, mlstyle_selected: "Standard".to_string(), diff --git a/src/app/update.rs b/src/app/update.rs index 8546979f2..1761b0a2e 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -2126,6 +2126,73 @@ impl H7CAD { Task::none() } + // ── TableStyle Dialog ───────────────────────────────────────────── + Message::TableStyleDialogOpen => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + self.tablestyle_selected = self.tabs[i].scene.document.objects.values() + .find_map(|o| if let ObjectType::TableStyle(s) = o { Some(s.name.clone()) } else { None }) + .unwrap_or_else(|| "Standard".to_string()); + self.tablestyle_open = true; + Task::none() + } + Message::TableStyleDialogClose => { + self.tablestyle_open = false; + Task::none() + } + Message::TableStyleDialogSelect(name) => { + self.tablestyle_selected = name; + Task::none() + } + + Message::TableStyleDialogNew => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let doc = &self.tabs[i].scene.document; + let mut n = 1u32; + let new_name = loop { + let candidate = format!("TS{}", n); + let taken = doc.objects.values().any(|o| { + matches!(o, ObjectType::TableStyle(s) if s.name.eq_ignore_ascii_case(&candidate)) + }); + if !taken { break candidate; } + n += 1; + }; + self.push_undo_snapshot(i, "TABLESTYLE NEW"); + let mut style = acadrust::objects::TableStyle::standard(); + style.name = new_name.clone(); + let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle()); + style.handle = nh; + self.tabs[i].scene.document.objects.insert(nh, ObjectType::TableStyle(style)); + self.tablestyle_selected = new_name; + self.tabs[i].dirty = true; + Task::none() + } + Message::TableStyleDialogDelete => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let name = self.tablestyle_selected.clone(); + if name.eq_ignore_ascii_case("Standard") { + self.command_line.push_error("Cannot delete the Standard style."); + return Task::none(); + } + let handle = self.tabs[i].scene.document.objects.iter() + .find_map(|(&h, o)| { + if let ObjectType::TableStyle(s) = o { + if s.name == name { Some(h) } else { None } + } else { None } + }); + if let Some(h) = handle { + self.push_undo_snapshot(i, "TABLESTYLE DEL"); + self.tabs[i].scene.document.objects.remove(&h); + self.tablestyle_selected = self.tabs[i].scene.document.objects.values() + .find_map(|o| if let ObjectType::TableStyle(s) = o { Some(s.name.clone()) } else { None }) + .unwrap_or_else(|| "Standard".to_string()); + self.tabs[i].dirty = true; + } + Task::none() + } + // ── MLineStyle Dialog ───────────────────────────────────────────── Message::MlStyleDialogOpen => { use acadrust::objects::ObjectType; diff --git a/src/app/view.rs b/src/app/view.rs index afa7d7e9c..cebe33c7e 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -227,6 +227,21 @@ impl H7CAD { iced::widget::Space::new().width(0).height(0).into() }; + let tablestyle_layer: Element<'_, Message> = if self.tablestyle_open { + use acadrust::objects::ObjectType; + let tab = &self.tabs[self.active_tab]; + let styles: Vec = tab.scene.document.objects.values() + .filter_map(|o| if let ObjectType::TableStyle(s) = o { Some(s.name.clone()) } else { None }) + .collect(); + let selected_style = tab.scene.document.objects.values() + .find_map(|o| if let ObjectType::TableStyle(s) = o { + if s.name == self.tablestyle_selected { Some(s) } else { None } + } else { None }); + tablestyle_overlay(styles, &self.tablestyle_selected, selected_style) + } else { + iced::widget::Space::new().width(0).height(0).into() + }; + let mlstyle_layer: Element<'_, Message> = if self.mlstyle_open { use acadrust::objects::ObjectType; let tab = &self.tabs[self.active_tab]; @@ -256,7 +271,7 @@ impl H7CAD { iced::widget::Space::new().width(0).height(0).into() }; - stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, mlstyle_layer, dimstyle_layer].into() + stack![main_ui, self.app_menu.view(), snap_layer, dropdown_layer, layout_ctx_layer, page_setup_layer, tablestyle_layer, mlstyle_layer, dimstyle_layer].into() } pub fn subscription(&self) -> Subscription { @@ -1037,6 +1052,152 @@ fn dimstyle_overlay<'a>( 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>(