From e0c5db9e0cbddfb95c354b4c26ee3262e83f5cf5 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Wed, 3 Jun 2026 20:25:14 +0300 Subject: [PATCH] feat(mleaderstyle): add Multileader Style editor window New dedicated style-manager window for MultiLeaderStyle objects (the existing mlstyle window edits MLineStyle, not multileader). Opened via the MLEADERSTYLE command / ribbon manager entry. Editable across Leader Format / Structure / Content sections: - path/content/text-angle/text-alignment enums (cycle buttons) - line & text color (ACI), arrowhead & break-gap sizes - landing distance/gap, max points, segment angles, scale factor - default text, text height - enable landing/dogleg, text frame, text-always-left, annotative New/Delete/Set-Current style management. Co-Authored-By: Claude Opus 4.8 --- src/app/commands.rs | 5 +- src/app/mod.rs | 49 ++++++ src/app/update.rs | 356 +++++++++++++++++++++++++++++++++++++++++ src/app/view.rs | 46 ++++++ src/ui/mleaderstyle.rs | 340 +++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 1 + 6 files changed, 796 insertions(+), 1 deletion(-) create mode 100644 src/ui/mleaderstyle.rs diff --git a/src/app/commands.rs b/src/app/commands.rs index 6472f5634..f53a01a4d 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -3983,7 +3983,10 @@ impl OpenCADStudio { 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() { - "" | "LIST" | "?" => { + "" | "DIALOG" | "UI" => { + return Task::done(Message::MLeaderStyleDialogOpen); + } + "LIST" | "?" => { let styles: Vec = self.tabs[i] .scene .document diff --git a/src/app/mod.rs b/src/app/mod.rs index a17e09c6d..b2991788a 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -234,6 +234,7 @@ pub(super) struct OpenCADStudio { textstyle_window: Option, tablestyle_window: Option, mlstyle_window: Option, + mleaderstyle_window: Option, layout_manager_window: Option, plotstyle_window: Option, dimstyle_window: Option, @@ -294,6 +295,21 @@ pub(super) struct OpenCADStudio { // ── MLineStyle Dialog ───────────────────────────────────────────────── mlstyle_selected: String, + // ── MLeaderStyle Dialog ─────────────────────────────────────────────── + mleaderstyle_selected: String, + mls_landing_distance: String, + mls_landing_gap: String, + mls_arrowhead_size: String, + mls_text_height: String, + mls_scale_factor: String, + mls_break_gap: String, + mls_first_seg_angle: String, + mls_second_seg_angle: String, + mls_max_points: String, + mls_default_text: String, + mls_line_color: String, + mls_text_color: String, + // ── TableStyle Dialog ───────────────────────────────────────────────── tablestyle_selected: String, /// Edit buffers for the table style's general margins. @@ -1005,6 +1021,21 @@ pub enum Message { MlStyleDialogSetCurrent, MlStyleDialogNew, MlStyleDialogDelete, + // ── MLeaderStyle Dialog ─────────────────────────────────────────────── + MLeaderStyleDialogOpen, + #[allow(dead_code)] + MLeaderStyleDialogClose, + MLeaderStyleDialogSelect(String), + MLeaderStyleDialogSetCurrent, + MLeaderStyleDialogNew, + MLeaderStyleDialogDelete, + MLeaderStyleEdit { + field: &'static str, + value: String, + }, + MLeaderStyleToggle(&'static str), + MLeaderStyleCycle(&'static str), + MLeaderStyleApply, // ── DimStyle Dialog ─────────────────────────────────────────────────── DimStyleDialogOpen, DimStyleDialogClose, @@ -1108,6 +1139,7 @@ impl OpenCADStudio { textstyle_window: None, tablestyle_window: None, mlstyle_window: None, + mleaderstyle_window: None, layout_manager_window: None, plotstyle_window: None, dimstyle_window: None, @@ -1172,6 +1204,20 @@ impl OpenCADStudio { ts_vmargin: "1.5".to_string(), // MLineStyle dialog mlstyle_selected: "Standard".to_string(), + // MLeaderStyle dialog + mleaderstyle_selected: "Standard".to_string(), + mls_landing_distance: String::new(), + mls_landing_gap: String::new(), + mls_arrowhead_size: String::new(), + mls_text_height: String::new(), + mls_scale_factor: String::new(), + mls_break_gap: String::new(), + mls_first_seg_angle: String::new(), + mls_second_seg_angle: String::new(), + mls_max_points: String::new(), + mls_default_text: String::new(), + mls_line_color: String::new(), + mls_text_color: String::new(), // DimStyle dialog dimstyle_selected: "Standard".to_string(), dimstyle_tab: 0, @@ -1308,6 +1354,9 @@ pub fn run() -> iced::Result { if Some(window_id) == state.mlstyle_window { return "Multiline Style".into(); } + if Some(window_id) == state.mleaderstyle_window { + return "Multileader Style".into(); + } if Some(window_id) == state.layout_manager_window { return "Layout Manager".into(); } diff --git a/src/app/update.rs b/src/app/update.rs index a12e97c56..8cfc0958e 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1428,6 +1428,10 @@ impl OpenCADStudio { self.mlstyle_window = None; self.ribbon.deactivate_tool_if("MLSTYLE"); } + if self.mleaderstyle_window == Some(id) { + self.mleaderstyle_window = None; + self.ribbon.deactivate_tool_if("MLEADERSTYLE"); + } if self.layout_manager_window == Some(id) { self.layout_manager_window = None; self.ribbon.deactivate_tool_if("LAYOUTMANAGER"); @@ -6045,6 +6049,302 @@ impl OpenCADStudio { Task::none() } + // ── MLeaderStyle Dialog ─────────────────────────────────────────── + Message::MLeaderStyleDialogOpen => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let cur = self.tabs[i].active_mleader_style.clone(); + let exists = self.tabs[i] + .scene + .document + .objects + .values() + .any(|o| matches!(o, ObjectType::MultiLeaderStyle(s) if s.name == cur)); + self.mleaderstyle_selected = if exists { + cur + } else { + self.tabs[i] + .scene + .document + .objects + .values() + .find_map(|o| { + if let ObjectType::MultiLeaderStyle(s) = o { + Some(s.name.clone()) + } else { + None + } + }) + .unwrap_or_else(|| "Standard".to_string()) + }; + self.load_mleaderstyle_bufs(i); + if let Some(id) = self.mleaderstyle_window { + return window::gain_focus(id); + } + let (id, task) = window::open(window::Settings { + size: iced::Size::new(560.0, 560.0), + resizable: true, + level: window::Level::AlwaysOnTop, + ..Default::default() + }); + self.mleaderstyle_window = Some(id); + task.map(|_| Message::Noop) + } + Message::MLeaderStyleDialogClose => { + if let Some(id) = self.mleaderstyle_window.take() { + window::close(id) + } else { + Task::none() + } + } + Message::MLeaderStyleDialogSelect(name) => { + self.mleaderstyle_selected = name; + let i = self.active_tab; + self.load_mleaderstyle_bufs(i); + Task::none() + } + Message::MLeaderStyleDialogSetCurrent => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let name = self.mleaderstyle_selected.clone(); + let exists = self.tabs[i] + .scene + .document + .objects + .values() + .any(|o| matches!(o, ObjectType::MultiLeaderStyle(s) if s.name == name)); + if exists { + self.tabs[i].active_mleader_style = name.clone(); + self.ribbon.active_mleader_style = name.clone(); + self.command_line + .push_output(&format!("Current multileader style: {}", name)); + } + Task::none() + } + Message::MLeaderStyleDialogNew => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let doc = &self.tabs[i].scene.document; + let mut n = 1u32; + let new_name = loop { + let candidate = format!("MLeader{}", n); + let taken = doc.objects.values().any(|o| { + matches!(o, ObjectType::MultiLeaderStyle(s) if s.name.eq_ignore_ascii_case(&candidate)) + }); + if !taken { + break candidate; + } + n += 1; + }; + self.push_undo_snapshot(i, "MLEADERSTYLE NEW"); + let mut style = acadrust::objects::MultiLeaderStyle::new(&new_name); + let nh = acadrust::Handle::new(self.tabs[i].scene.document.next_handle()); + style.handle = nh; + self.tabs[i] + .scene + .document + .objects + .insert(nh, ObjectType::MultiLeaderStyle(style)); + self.mleaderstyle_selected = new_name; + self.load_mleaderstyle_bufs(i); + self.tabs[i].dirty = true; + Task::none() + } + Message::MLeaderStyleDialogDelete => { + use acadrust::objects::ObjectType; + let i = self.active_tab; + let name = self.mleaderstyle_selected.clone(); + if name.eq_ignore_ascii_case("Standard") { + self.command_line + .push_error("Cannot delete the Standard style."); + return Task::none(); + } + let handle = self.tabs[i] + .scene + .document + .objects + .iter() + .find_map(|(&h, o)| match o { + ObjectType::MultiLeaderStyle(s) if s.name == name => Some(h), + _ => None, + }); + if let Some(h) = handle { + self.push_undo_snapshot(i, "MLEADERSTYLE DEL"); + self.tabs[i].scene.document.objects.remove(&h); + self.mleaderstyle_selected = self.tabs[i] + .scene + .document + .objects + .values() + .find_map(|o| { + if let ObjectType::MultiLeaderStyle(s) = o { + Some(s.name.clone()) + } else { + None + } + }) + .unwrap_or_else(|| "Standard".to_string()); + self.load_mleaderstyle_bufs(i); + self.tabs[i].dirty = true; + } + Task::none() + } + Message::MLeaderStyleEdit { field, value } => { + match field { + "landing_distance" => self.mls_landing_distance = value, + "landing_gap" => self.mls_landing_gap = value, + "arrowhead_size" => self.mls_arrowhead_size = value, + "text_height" => self.mls_text_height = value, + "scale_factor" => self.mls_scale_factor = value, + "break_gap" => self.mls_break_gap = value, + "first_seg_angle" => self.mls_first_seg_angle = value, + "second_seg_angle" => self.mls_second_seg_angle = value, + "max_points" => self.mls_max_points = value, + "default_text" => self.mls_default_text = value, + "line_color" => self.mls_line_color = value, + "text_color" => self.mls_text_color = value, + _ => {} + } + Task::none() + } + Message::MLeaderStyleToggle(field) => { + let i = self.active_tab; + if let Some(s) = self.mleaderstyle_mut(i) { + match field { + "enable_landing" => s.enable_landing = !s.enable_landing, + "enable_dogleg" => s.enable_dogleg = !s.enable_dogleg, + "text_frame" => s.text_frame = !s.text_frame, + "text_always_left" => s.text_always_left = !s.text_always_left, + "annotative" => s.is_annotative = !s.is_annotative, + _ => {} + } + self.push_undo_snapshot(i, "MLEADERSTYLE EDIT"); + self.tabs[i].dirty = true; + self.tabs[i].scene.bump_geometry(); + } + Task::none() + } + Message::MLeaderStyleCycle(field) => { + use acadrust::objects::{ + LeaderContentType, MultiLeaderPathType, TextAlignmentType, TextAngleType, + }; + let i = self.active_tab; + if let Some(s) = self.mleaderstyle_mut(i) { + match field { + "path_type" => { + s.path_type = match s.path_type { + MultiLeaderPathType::Invisible => { + MultiLeaderPathType::StraightLineSegments + } + MultiLeaderPathType::StraightLineSegments => { + MultiLeaderPathType::Spline + } + MultiLeaderPathType::Spline => MultiLeaderPathType::Invisible, + }; + } + "content_type" => { + s.content_type = match s.content_type { + LeaderContentType::None => LeaderContentType::Block, + LeaderContentType::Block => LeaderContentType::MText, + LeaderContentType::MText => LeaderContentType::Tolerance, + LeaderContentType::Tolerance => LeaderContentType::None, + }; + } + "text_angle_type" => { + s.text_angle_type = match s.text_angle_type { + TextAngleType::ParallelToLastLeaderLine => TextAngleType::Horizontal, + TextAngleType::Horizontal => TextAngleType::Optimized, + TextAngleType::Optimized => { + TextAngleType::ParallelToLastLeaderLine + } + }; + } + "text_alignment" => { + s.text_alignment = match s.text_alignment { + TextAlignmentType::Left => TextAlignmentType::Center, + TextAlignmentType::Center => TextAlignmentType::Right, + TextAlignmentType::Right => TextAlignmentType::Left, + }; + } + _ => {} + } + self.push_undo_snapshot(i, "MLEADERSTYLE EDIT"); + self.tabs[i].dirty = true; + self.tabs[i].scene.bump_geometry(); + } + Task::none() + } + Message::MLeaderStyleApply => { + let i = self.active_tab; + let ( + ld, + lg, + asz, + th, + sf, + bg, + fsa, + ssa, + mp, + dt, + lc, + tc, + ) = ( + self.mls_landing_distance.parse::().ok(), + self.mls_landing_gap.parse::().ok(), + self.mls_arrowhead_size.parse::().ok(), + self.mls_text_height.parse::().ok(), + self.mls_scale_factor.parse::().ok(), + self.mls_break_gap.parse::().ok(), + self.mls_first_seg_angle.parse::().ok(), + self.mls_second_seg_angle.parse::().ok(), + self.mls_max_points.parse::().ok(), + self.mls_default_text.clone(), + self.mls_line_color.parse::().ok(), + self.mls_text_color.parse::().ok(), + ); + if let Some(s) = self.mleaderstyle_mut(i) { + if let Some(v) = ld { + s.landing_distance = v; + } + if let Some(v) = lg { + s.landing_gap = v; + } + if let Some(v) = asz { + s.arrowhead_size = v; + } + if let Some(v) = th { + s.text_height = v; + } + if let Some(v) = sf { + s.scale_factor = v; + } + if let Some(v) = bg { + s.break_gap_size = v; + } + if let Some(v) = fsa { + s.first_segment_angle = v; + } + if let Some(v) = ssa { + s.second_segment_angle = v; + } + if let Some(v) = mp { + s.max_leader_points = v; + } + s.default_text = dt; + if let Some(v) = lc { + s.line_color = acadrust::types::Color::from_index(v); + } + if let Some(v) = tc { + s.text_color = acadrust::types::Color::from_index(v); + } + self.push_undo_snapshot(i, "MLEADERSTYLE EDIT"); + self.tabs[i].dirty = true; + self.tabs[i].scene.bump_geometry(); + } + Task::none() + } + // ── DimStyle Dialog ─────────────────────────────────────────────── Message::DimStyleDialogOpen => { let i = self.active_tab; @@ -6544,6 +6844,62 @@ impl OpenCADStudio { Some(task) } + /// Mutable access to the currently selected multileader style. + fn mleaderstyle_mut( + &mut self, + tab: usize, + ) -> Option<&mut acadrust::objects::MultiLeaderStyle> { + use acadrust::objects::ObjectType; + let name = self.mleaderstyle_selected.clone(); + self.tabs[tab] + .scene + .document + .objects + .values_mut() + .find_map(|o| match o { + ObjectType::MultiLeaderStyle(s) if s.name == name => Some(s), + _ => None, + }) + } + + /// Populate all edit buffers from the currently selected multileader style. + fn load_mleaderstyle_bufs(&mut self, tab: usize) { + use acadrust::objects::ObjectType; + let name = self.mleaderstyle_selected.clone(); + let Some(s) = self.tabs[tab] + .scene + .document + .objects + .values() + .find_map(|o| match o { + ObjectType::MultiLeaderStyle(s) if s.name == name => Some(s), + _ => None, + }) + else { + return; + }; + self.mls_landing_distance = format!("{:.4}", s.landing_distance); + self.mls_landing_gap = format!("{:.4}", s.landing_gap); + self.mls_arrowhead_size = format!("{:.4}", s.arrowhead_size); + self.mls_text_height = format!("{:.4}", s.text_height); + self.mls_scale_factor = format!("{:.4}", s.scale_factor); + self.mls_break_gap = format!("{:.4}", s.break_gap_size); + self.mls_first_seg_angle = format!("{:.4}", s.first_segment_angle); + self.mls_second_seg_angle = format!("{:.4}", s.second_segment_angle); + self.mls_max_points = s.max_leader_points.to_string(); + self.mls_default_text = s.default_text.clone(); + self.mls_line_color = s + .line_color + .index() + .map(|c| c.to_string()) + .unwrap_or_default(); + self.mls_text_color = s + .text_color + .index() + .map(|c| c.to_string()) + .unwrap_or_default(); + } + /// Populate all edit buffers from the currently selected dim style. fn load_dimstyle_bufs(&mut self, tab: usize) { let doc = &self.tabs[tab].scene.document; diff --git a/src/app/view.rs b/src/app/view.rs index b6c1653d3..967646e86 100644 --- a/src/app/view.rs +++ b/src/app/view.rs @@ -155,6 +155,52 @@ impl OpenCADStudio { tab.scene.document.header.multiline_style.clone(), ); } + if Some(window_id) == self.mleaderstyle_window { + 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::MultiLeaderStyle(s) = o { + Some(s.name.clone()) + } else { + None + } + }) + .collect(); + let selected_style = tab.scene.document.objects.values().find_map(|o| { + if let ObjectType::MultiLeaderStyle(s) = o { + if s.name == self.mleaderstyle_selected { + Some(s) + } else { + None + } + } else { + None + } + }); + return crate::ui::mleaderstyle::view_window(crate::ui::mleaderstyle::MLeaderStyleView { + styles, + selected: &self.mleaderstyle_selected, + style: selected_style, + current: tab.active_mleader_style.clone(), + landing_distance: &self.mls_landing_distance, + landing_gap: &self.mls_landing_gap, + arrowhead_size: &self.mls_arrowhead_size, + text_height: &self.mls_text_height, + scale_factor: &self.mls_scale_factor, + break_gap: &self.mls_break_gap, + first_seg_angle: &self.mls_first_seg_angle, + second_seg_angle: &self.mls_second_seg_angle, + max_points: &self.mls_max_points, + default_text: &self.mls_default_text, + line_color: &self.mls_line_color, + text_color: &self.mls_text_color, + }); + } if Some(window_id) == self.layout_manager_window { let i = self.active_tab; let layouts = self.tabs[i].scene.layout_names(); diff --git a/src/ui/mleaderstyle.rs b/src/ui/mleaderstyle.rs new file mode 100644 index 000000000..2e97d4879 --- /dev/null +++ b/src/ui/mleaderstyle.rs @@ -0,0 +1,340 @@ +//! Multileader Style Manager window — fills the entire OS window. + +use crate::app::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 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() +} + +/// View-model borrowed from the app for one render of the dialog. +pub struct MLeaderStyleView<'a> { + pub styles: Vec, + pub selected: &'a str, + pub style: Option<&'a acadrust::objects::MultiLeaderStyle>, + pub current: String, + pub landing_distance: &'a str, + pub landing_gap: &'a str, + pub arrowhead_size: &'a str, + pub text_height: &'a str, + pub scale_factor: &'a str, + pub break_gap: &'a str, + pub first_seg_angle: &'a str, + pub second_seg_angle: &'a str, + pub max_points: &'a str, + pub default_text: &'a str, + pub line_color: &'a str, + pub text_color: &'a str, +} + +fn section<'a>(label: &'static str) -> Element<'a, Message> { + text(label).size(11).color(ACCENT).into() +} + +fn num_row<'a>( + label: &'static str, + placeholder: &'static str, + value: &'a str, + field: &'static str, +) -> Element<'a, Message> { + row![ + text(label).size(11).color(DIM).width(150), + text_input(placeholder, value) + .on_input(move |v| Message::MLeaderStyleEdit { field, value: v }) + .size(11) + .width(110), + ] + .spacing(8) + .align_y(iced::Center) + .into() +} + +fn enum_row<'a>( + label: &'static str, + val: String, + field: &'static str, +) -> Element<'a, Message> { + row![ + text(label).size(11).color(DIM).width(150), + text(val).size(11).width(150), + button(text("Cycle").size(10)) + .on_press(Message::MLeaderStyleCycle(field)) + .style(btn_s(false)) + .padding([2, 8]), + ] + .spacing(8) + .align_y(iced::Center) + .into() +} + +fn chk<'a>(label: &'static str, val: bool, field: &'static str) -> Element<'a, Message> { + checkbox(val) + .label(label) + .on_toggle(move |_| Message::MLeaderStyleToggle(field)) + .size(14) + .text_size(11) + .into() +} + +pub fn view_window<'a>(v: MLeaderStyleView<'a>) -> Element<'a, Message> { + // ── Toolbar ─────────────────────────────────────────────────────────── + let toolbar = container( + row![ + button(text("New").size(11)) + .on_press(Message::MLeaderStyleDialogNew) + .style(btn_s(true)) + .padding([4, 10]), + button(text("Delete").size(11)) + .on_press(Message::MLeaderStyleDialogDelete) + .style(btn_s(false)) + .padding([4, 10]), + button(text("Set Current").size(11)) + .on_press(Message::MLeaderStyleDialogSetCurrent) + .style(btn_s(false)) + .padding([4, 10]), + ] + .spacing(4) + .align_y(iced::Center), + ) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(TB)), + ..Default::default() + }) + .width(Fill) + .padding([5, 8]); + + // ── Left: Style list ────────────────────────────────────────────────── + let style_items: Vec> = v + .styles + .iter() + .map(|name| { + let is_sel = name.as_str() == v.selected; + button(text(name.clone()).size(11)) + .on_press(Message::MLeaderStyleDialogSelect(name.clone())) + .style(list_item(is_sel)) + .padding([4, 8]) + .width(Fill) + .into() + }) + .collect(); + + let style_list = container( + column![ + text(format!("Current: {}", v.current)).size(10).color(DIM), + container(scrollable(column(style_items).spacing(2)).height(Fill)) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(LIST)), + border: Border { + color: BORDER, + width: 1.0, + radius: 3.0.into() + }, + ..Default::default() + }) + .width(Fill) + .height(Fill) + .padding(2), + ] + .spacing(4) + .height(Fill), + ) + .width(200) + .height(Fill) + .padding(iced::Padding { + top: 12.0, + right: 8.0, + bottom: 12.0, + left: 12.0, + }); + + // ── Right: Details panel ────────────────────────────────────────────── + let details: Element<'_, Message> = if let Some(s) = v.style { + scrollable( + column![ + row![ + text("Name:").size(11).color(DIM).width(150), + text(s.name.clone()).size(11), + ] + .spacing(8), + // Leader Format + section("Leader Format"), + enum_row("Path type:", format!("{:?}", s.path_type), "path_type"), + num_row("Line color (ACI):", "256", v.line_color, "line_color"), + num_row("Arrowhead size:", "0.18", v.arrowhead_size, "arrowhead_size"), + num_row("Break gap size:", "0.125", v.break_gap, "break_gap"), + // Leader Structure + section("Leader Structure"), + chk("Enable landing", s.enable_landing, "enable_landing"), + chk("Enable dogleg", s.enable_dogleg, "enable_dogleg"), + num_row("Landing distance:", "8.0", v.landing_distance, "landing_distance"), + num_row("Landing gap:", "0.09", v.landing_gap, "landing_gap"), + num_row("Max leader points:", "2", v.max_points, "max_points"), + num_row("First seg. angle:", "0", v.first_seg_angle, "first_seg_angle"), + num_row("Second seg. angle:", "0", v.second_seg_angle, "second_seg_angle"), + num_row("Scale factor:", "1.0", v.scale_factor, "scale_factor"), + chk("Annotative", s.is_annotative, "annotative"), + // Content + section("Content"), + enum_row("Content type:", format!("{:?}", s.content_type), "content_type"), + num_row("Default text:", "", v.default_text, "default_text"), + num_row("Text height:", "0.18", v.text_height, "text_height"), + num_row("Text color (ACI):", "256", v.text_color, "text_color"), + enum_row( + "Text angle:", + format!("{:?}", s.text_angle_type), + "text_angle_type" + ), + enum_row( + "Text alignment:", + format!("{:?}", s.text_alignment), + "text_alignment" + ), + chk("Text frame", s.text_frame, "text_frame"), + chk("Text always left", s.text_always_left, "text_always_left"), + button(text("Apply").size(11)) + .on_press(Message::MLeaderStyleApply) + .style(btn_s(true)) + .padding([4, 14]), + ] + .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 93df7944d..a3ce298cd 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -9,6 +9,7 @@ pub mod command_line; pub mod dimstyle; pub mod layers; pub mod layout_manager; +pub mod mleaderstyle; pub mod mlstyle; pub mod open_progress; pub mod overlay;