diff --git a/src/app/layers.rs b/src/app/layers.rs index f96d3a6e..6fe888cf 100644 --- a/src/app/layers.rs +++ b/src/app/layers.rs @@ -39,5 +39,53 @@ impl H7CAD { .collect(); self.tabs[i].layers.sync_linetypes(lt_items.clone()); self.ribbon.set_available_linetypes(lt_items); + self.sync_ribbon_styles(); + } + + pub(super) fn sync_ribbon_styles(&mut self) { + let i = self.active_tab; + let doc = &self.tabs[i].scene.document; + + let text_names: Vec = doc.text_styles.iter().map(|s| s.name.clone()).collect(); + let active_text = doc.header.current_text_style_name.clone(); + let active_text = if text_names.contains(&active_text) { active_text } else { + text_names.first().cloned().unwrap_or_else(|| "Standard".to_string()) + }; + + let dim_names: Vec = doc.dim_styles.iter().map(|s| s.name.clone()).collect(); + let active_dim = doc.header.current_dimstyle_name.clone(); + let active_dim = if dim_names.contains(&active_dim) { active_dim } else { + dim_names.first().cloned().unwrap_or_else(|| "Standard".to_string()) + }; + + let mleader_names: Vec = doc.objects.values().filter_map(|o| { + if let acadrust::objects::ObjectType::MultiLeaderStyle(mls) = o { + Some(mls.name.clone()) + } else { + None + } + }).collect(); + let active_mleader = self.tabs[i].active_mleader_style.clone(); + let active_mleader = if mleader_names.contains(&active_mleader) { active_mleader } else { + mleader_names.first().cloned().unwrap_or_else(|| "Standard".to_string()) + }; + + let table_names: Vec = doc.objects.values().filter_map(|o| { + if let acadrust::objects::ObjectType::TableStyle(ts) = o { + Some(ts.name.clone()) + } else { + None + } + }).collect(); + let active_table = table_names.first().cloned().unwrap_or_else(|| "Standard".to_string()); + + let active_mleader2 = active_mleader.clone(); + let active_table2 = active_table.clone(); + self.ribbon.set_styles( + text_names, &active_text, + dim_names, &active_dim, + mleader_names, &active_mleader2, + table_names, &active_table2, + ); } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 25c551ea..a4cebf5c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -295,6 +295,8 @@ pub enum Message { RibbonLinetypeChanged(String), /// User changed the active lineweight in the Properties toolbar. RibbonLineweightChanged(LineWeight), + /// User selected a style from a style combobox in the ribbon. + RibbonStyleChanged { key: crate::modules::StyleKey, name: String }, // ── Properties panel ────────────────────────────────────────────────── /// User selected a layer from the layer pick_list in the Properties panel. diff --git a/src/app/update.rs b/src/app/update.rs index 07180421..fafd66fb 100644 --- a/src/app/update.rs +++ b/src/app/update.rs @@ -1929,6 +1929,43 @@ impl H7CAD { Task::none() } + Message::RibbonStyleChanged { key, name } => { + use crate::modules::StyleKey; + self.ribbon.close_dropdown(); + match key { + StyleKey::TextStyle => { + self.ribbon.active_text_style = name.clone(); + let i = self.active_tab; + let found = self.tabs[i].scene.document.text_styles.iter() + .find(|s| s.name == name) + .map(|ts| ts.handle); + if let Some(h) = found { + self.tabs[i].scene.document.header.current_text_style_handle = h; + self.tabs[i].scene.document.header.current_text_style_name = name; + } + } + StyleKey::DimStyle => { + self.ribbon.active_dim_style = name.clone(); + let i = self.active_tab; + let found = self.tabs[i].scene.document.dim_styles.get(&name) + .map(|ds| ds.handle); + if let Some(h) = found { + self.tabs[i].scene.document.header.current_dimstyle_handle = h; + self.tabs[i].scene.document.header.current_dimstyle_name = name; + } + } + StyleKey::MLeaderStyle => { + self.ribbon.active_mleader_style = name.clone(); + let i = self.active_tab; + self.tabs[i].active_mleader_style = name; + } + StyleKey::TableStyle => { + self.ribbon.active_table_style = name; + } + } + Task::none() + } + Message::PropLayerChanged(layer) => { let i = self.active_tab; let handles = self.property_target_handles(i); diff --git a/src/modules/annotate/mod.rs b/src/modules/annotate/mod.rs index 30166d98..af2b7270 100644 --- a/src/modules/annotate/mod.rs +++ b/src/modules/annotate/mod.rs @@ -23,7 +23,7 @@ pub mod table_cmd; pub mod text; pub mod tolerance_cmd; -use crate::modules::{CadModule, RibbonGroup, RibbonItem}; +use crate::modules::{CadModule, RibbonGroup, RibbonItem, StyleKey}; pub struct AnnotateModule; @@ -45,7 +45,7 @@ impl CadModule for AnnotateModule { tools: vec![ RibbonItem::LargeDropdown { id: "ANNOTATE_TEXT", - label: "Text", + label: "Multiline\nText", icon: mtext::ICON, items: vec![ (mtext::tool().id, mtext::tool().label, mtext::tool().icon), @@ -54,18 +54,21 @@ impl CadModule for AnnotateModule { ], default: "MTEXT", }, - RibbonItem::Tool(crate::modules::ToolDef { - id: "STYLE", - label: "Text Style", - icon: crate::modules::IconKind::Svg(include_bytes!("../../../assets/icons/text_style.svg")), - event: crate::modules::ModuleEvent::Command("STYLE".to_string()), - }), - RibbonItem::Tool(crate::modules::ToolDef { - id: "FIND", - label: "Find", - icon: crate::modules::IconKind::Svg(include_bytes!("../../../assets/icons/find.svg")), - event: crate::modules::ModuleEvent::Command("FIND".to_string()), - }), + RibbonItem::StyleComboGroup { + style_key: StyleKey::TextStyle, + combo_id: "TEXT_STYLE_COMBO", + manager_cmd: Some("STYLE"), + rows: vec![ + vec![ + crate::modules::ToolDef { + id: "FIND", + label: "Find", + icon: crate::modules::IconKind::Svg(include_bytes!("../../../assets/icons/find.svg")), + event: crate::modules::ModuleEvent::Command("FIND".to_string()), + }, + ], + ], + }, ], }, // ── Dimensions ─────────────────────────────────────────────── @@ -87,20 +90,26 @@ impl CadModule for AnnotateModule { ], default: "DIMLINEAR", }, - RibbonItem::Tool(dim_continue::tool()), - RibbonItem::Tool(dim_baseline::tool()), - RibbonItem::Tool(tolerance_cmd::tool()), - RibbonItem::Tool(dimedit::tool()), - RibbonItem::Tool(dimtedit::tool()), - RibbonItem::Tool(dimbreak::tool()), - RibbonItem::Tool(dimspace::tool()), - RibbonItem::Tool(dimjogline::tool()), - RibbonItem::Tool(crate::modules::ToolDef { - id: "DIMSTYLE", - label: "Dim Style", - icon: crate::modules::IconKind::Svg(include_bytes!("../../../assets/icons/dim_style.svg")), - event: crate::modules::ModuleEvent::Command("DIMSTYLE".to_string()), - }), + RibbonItem::StyleComboGroup { + style_key: StyleKey::DimStyle, + combo_id: "DIM_STYLE_COMBO", + manager_cmd: Some("DIMSTYLE"), + rows: vec![ + vec![ + qdim::tool(), + dim_continue::tool(), + dim_baseline::tool(), + ], + vec![ + tolerance_cmd::tool(), + dimedit::tool(), + dimtedit::tool(), + dimbreak::tool(), + dimspace::tool(), + dimjogline::tool(), + ], + ], + }, ], }, // ── Leaders ────────────────────────────────────────────────── @@ -117,10 +126,21 @@ impl CadModule for AnnotateModule { ], default: "MLEADER", }, - RibbonItem::Tool(mleader_edit::tool_add()), - RibbonItem::Tool(mleader_edit::tool_remove()), - RibbonItem::Tool(mleader_edit::tool_align()), - RibbonItem::Tool(mleader_edit::tool_collect()), + RibbonItem::StyleComboGroup { + style_key: StyleKey::MLeaderStyle, + combo_id: "MLEADER_STYLE_COMBO", + manager_cmd: Some("MLEADERSTYLE"), + rows: vec![ + vec![ + mleader_edit::tool_add(), + mleader_edit::tool_remove(), + ], + vec![ + mleader_edit::tool_align(), + mleader_edit::tool_collect(), + ], + ], + }, ], }, // ── Tables ─────────────────────────────────────────────────── @@ -128,6 +148,12 @@ impl CadModule for AnnotateModule { title: "Tables", tools: vec![ RibbonItem::LargeTool(table_cmd::tool()), + RibbonItem::StyleComboGroup { + style_key: StyleKey::TableStyle, + combo_id: "TABLE_STYLE_COMBO", + manager_cmd: Some("TABLESTYLE"), + rows: vec![], + }, ], }, // ── Markup ─────────────────────────────────────────────────── diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 5b4bf431..8cec0dc3 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -79,6 +79,27 @@ pub enum RibbonItem { LayerComboGroup { row2: Vec, row3: Vec }, /// Match Properties (large button) + Color / Linetype / Lineweight combos on the right. PropertiesGroup { match_prop: ToolDef }, + /// A style selector combobox (text / dim / mleader / table style) with + /// optional small tool rows below it. + StyleComboGroup { + /// Which style domain this combo controls. + style_key: StyleKey, + /// Unique dropdown id (must be unique across the ribbon). + combo_id: &'static str, + /// Optional command to run when the user opens the style manager. + manager_cmd: Option<&'static str>, + /// Small tool rows rendered below the combo (0–2 rows). + rows: Vec>, + }, +} + +/// Identifies which style list a `StyleComboGroup` refers to. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum StyleKey { + TextStyle, + DimStyle, + MLeaderStyle, + TableStyle, } impl From for RibbonItem { diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 51ff7c38..1872dba3 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -18,7 +18,7 @@ use crate::ui::properties::{LinetypeItem, color_picker_dropdown, lw_options}; use crate::app::Message; mod widgets; -use widgets::*; +use widgets::{StyleContext, *}; // ── Ribbon state ─────────────────────────────────────────────────────────── @@ -43,6 +43,15 @@ pub struct Ribbon { pub available_linetypes: Vec, /// Whether the full ACI palette is expanded inside the color picker overlay. pub prop_color_palette_open: bool, + // ── Style selector state ────────────────────────────────────────────── + pub text_style_names: Vec, + pub active_text_style: String, + pub dim_style_names: Vec, + pub active_dim_style: String, + pub mleader_style_names: Vec, + pub active_mleader_style: String, + pub table_style_names: Vec, + pub active_table_style: String, } /// Per-layer display data shown in the ribbon layer dropdown. @@ -82,9 +91,34 @@ impl Ribbon { art: String::new(), }], prop_color_palette_open: false, + text_style_names: vec!["Standard".to_string()], + active_text_style: "Standard".to_string(), + dim_style_names: vec!["Standard".to_string()], + active_dim_style: "Standard".to_string(), + mleader_style_names: vec!["Standard".to_string()], + active_mleader_style: "Standard".to_string(), + table_style_names: vec!["Standard".to_string()], + active_table_style: "Standard".to_string(), } } + pub fn set_styles( + &mut self, + text: Vec, active_text: &str, + dim: Vec, active_dim: &str, + mleader: Vec, active_mleader: &str, + table: Vec, active_table: &str, + ) { + self.text_style_names = text; + self.active_text_style = active_text.to_string(); + self.dim_style_names = dim; + self.active_dim_style = active_dim.to_string(); + self.mleader_style_names = mleader; + self.active_mleader_style = active_mleader.to_string(); + self.table_style_names = table; + self.active_table_style = active_table.to_string(); + } + pub fn set_layers(&mut self, infos: Vec, active: &str) { self.active_layer = active.to_string(); self.layer_names = infos.iter().map(|l| l.name.clone()).collect(); @@ -274,6 +308,16 @@ impl Ribbon { let active_color = self.active_color; let active_linetype = &self.active_linetype; let active_lineweight = self.active_lineweight; + let style_ctx = StyleContext { + text_style_names: self.text_style_names.clone(), + active_text_style: self.active_text_style.clone(), + dim_style_names: self.dim_style_names.clone(), + active_dim_style: self.active_dim_style.clone(), + mleader_style_names: self.mleader_style_names.clone(), + active_mleader_style: self.active_mleader_style.clone(), + table_style_names: self.table_style_names.clone(), + active_table_style: self.active_table_style.clone(), + }; let mut widgets: Vec> = Vec::new(); let mut first_group = true; @@ -303,6 +347,7 @@ impl Ribbon { | RibbonItem::LargeDropdown { .. } | RibbonItem::LayerComboGroup { .. } | RibbonItem::PropertiesGroup { .. } + | RibbonItem::StyleComboGroup { .. } ); if is_large { @@ -319,6 +364,7 @@ impl Ribbon { active_color, active_linetype, active_lineweight, + &style_ctx, )); } else { small_buf.push(render_small( diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index a1926a1b..83b21142 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -6,11 +6,11 @@ use std::time::Duration; use acadrust::types::{Color as AcadColor, LineWeight}; use iced::widget::tooltip::Position as TipPos; -use iced::widget::{button, column, container, row, svg, text, tooltip}; +use iced::widget::{button, column, container, row, scrollable, svg, text, tooltip}; use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::app::Message; -use crate::modules::{IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef}; +use crate::modules::{IconKind, ModuleEvent, RibbonGroup, RibbonItem, StyleKey, ToolDef}; use crate::ui::properties::{LwItem, acad_color_display}; use super::LayerInfo; @@ -70,6 +70,38 @@ pub(super) const ICON_COLOR: Color = Color { r: 0.25, g: 0.75, b: 0.45, a: 1.0 } pub(super) const LABEL_ON: Color = Color { r: 0.92, g: 0.92, b: 0.92, a: 1.0 }; pub(super) const LABEL_OFF: Color = Color { r: 0.72, g: 0.72, b: 0.72, a: 1.0 }; +// ── Style context (passed from Ribbon to render_large) ──────────────────── + +pub(super) struct StyleContext { + pub text_style_names: Vec, + pub active_text_style: String, + pub dim_style_names: Vec, + pub active_dim_style: String, + pub mleader_style_names: Vec, + pub active_mleader_style: String, + pub table_style_names: Vec, + pub active_table_style: String, +} + +impl StyleContext { + fn names_for(&self, key: StyleKey) -> &[String] { + match key { + StyleKey::TextStyle => &self.text_style_names, + StyleKey::DimStyle => &self.dim_style_names, + StyleKey::MLeaderStyle => &self.mleader_style_names, + StyleKey::TableStyle => &self.table_style_names, + } + } + fn active_for(&self, key: StyleKey) -> &str { + match key { + StyleKey::TextStyle => &self.active_text_style, + StyleKey::DimStyle => &self.active_dim_style, + StyleKey::MLeaderStyle => &self.active_mleader_style, + StyleKey::TableStyle => &self.active_table_style, + } + } +} + // ── Layout helpers ───────────────────────────────────────────────────────── /// Flush up-to-3 small items as a vertical column into the group row. @@ -260,7 +292,7 @@ pub(super) fn render_small<'a>( // ── Large item renderer ──────────────────────────────────────────────────── -/// Render a full-height large button (LargeTool, LargeDropdown, or LayerCombo). +/// Render a full-height large button (LargeTool, LargeDropdown, LayerCombo, StyleCombo). pub(super) fn render_large<'a>( item: RibbonItem, active_tool: &Option, @@ -273,6 +305,7 @@ pub(super) fn render_large<'a>( active_color: AcadColor, active_linetype: &'a str, active_lineweight: LineWeight, + style_ctx: &StyleContext, ) -> Element<'a, Message> { match item { RibbonItem::LargeTool(t) => { @@ -595,6 +628,185 @@ pub(super) fn render_large<'a>( .into() } + RibbonItem::StyleComboGroup { style_key, combo_id, manager_cmd, rows } => { + const STYLE_COMBO_W: f32 = LARGE_W * 2.3; + let names: Vec = style_ctx.names_for(style_key).to_vec(); + let active: String = style_ctx.active_for(style_key).to_string(); + let is_open = open_dd.as_deref() == Some(combo_id); + + // ── combo button ── + let combo_btn = button( + row![ + container(text(active.clone()).size(11).color(Color::WHITE)) + .width(Fill) + .clip(true), + text(if is_open { "▲" } else { "▾" }) + .size(9) + .color(Color { r: 0.7, g: 0.7, b: 0.7, a: 1.0 }), + ] + .spacing(4) + .align_y(iced::Center), + ) + .on_press(Message::ToggleRibbonDropdown(combo_id.to_string())) + .style(move |_: &Theme, status| button::Style { + background: Some(Background::Color(match (is_open, status) { + (true, _) => Color { r: 0.14, g: 0.14, b: 0.14, a: 1.0 }, + (_, button::Status::Hovered) => Color { r: 0.26, g: 0.26, b: 0.26, a: 1.0 }, + _ => Color { r: 0.18, g: 0.18, b: 0.18, a: 1.0 }, + })), + border: Border { + radius: 3.0.into(), + width: 1.0, + color: if is_open { + Color { r: 0.45, g: 0.65, b: 0.90, a: 1.0 } + } else { + Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 } + }, + }, + ..Default::default() + }) + .padding([3, 8]) + .width(Fill); + + // ── style items panel (when open) ── + let items_panel: Element = if is_open { + let items_col: Vec> = names + .into_iter() + .map(|name| { + let is_sel = name.as_str() == active.as_str(); + let n = name.clone(); + let key = style_key; + button( + row![ + text(if is_sel { "✓" } else { " " }) + .size(10) + .color(if is_sel { + Color { r: 0.2, g: 0.8, b: 0.4, a: 1.0 } + } else { + Color::TRANSPARENT + }), + text(name).size(11).color(Color::WHITE), + ] + .spacing(6) + .align_y(iced::Center), + ) + .on_press(Message::RibbonStyleChanged { key, name: n }) + .style(move |_: &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 } + } + _ if is_sel => Color { r: 0.20, g: 0.35, b: 0.55, a: 1.0 }, + _ => Color { r: 0.16, g: 0.16, b: 0.16, a: 1.0 }, + })), + ..Default::default() + }) + .padding([4, 10]) + .width(Fill) + .into() + }) + .collect(); + + // Optional "Open Manager…" row + let mut full_col = items_col; + if let Some(mgr_cmd) = manager_cmd { + full_col.push( + button( + text(format!("Manage…")).size(10) + .color(Color { r: 0.5, g: 0.8, b: 1.0, a: 1.0 }), + ) + .on_press(Message::Command(mgr_cmd.to_string())) + .style(|_: &Theme, status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered => Color { r: 0.24, g: 0.24, b: 0.24, a: 1.0 }, + _ => Color { r: 0.13, g: 0.13, b: 0.13, a: 1.0 }, + })), + ..Default::default() + }) + .padding([4, 10]) + .width(Fill) + .into(), + ); + } + + container( + scrollable( + container(column(full_col).spacing(1)) + .width(Fill) + .padding(4), + ) + .height(Length::Shrink), + ) + .max_height(180.0) + .width(Length::Fixed(STYLE_COMBO_W)) + .style(|_: &Theme| container::Style { + background: Some(Background::Color( + Color { r: 0.14, g: 0.14, b: 0.14, a: 0.98 }, + )), + border: Border { + color: Color { r: 0.35, g: 0.35, b: 0.35, a: 1.0 }, + width: 1.0, + radius: 4.0.into(), + }, + ..Default::default() + }) + .into() + } else { + iced::widget::Space::new().width(0).height(0).into() + }; + + // ── tool rows below combo ── + let make_tool_row = |tools: Vec| -> Element { + let btns: Vec> = tools + .into_iter() + .map(|t| { + let is_active = active_tool.as_deref() == Some(t.id); + let tip = t.label; + let event = t.event.clone(); + let icon_el: Element = match t.icon { + IconKind::Glyph(g) => text(g).size(13).color(Color::WHITE).into(), + IconKind::Svg(bytes) => iced::widget::svg( + iced::widget::svg::Handle::from_memory(bytes), + ) + .width(16) + .height(16) + .into(), + }; + let msg = module_event_to_message(event); + tooltip( + button(icon_el) + .on_press(msg) + .style(move |_: &Theme, status| tool_btn_style(is_active, status)) + .padding([2, 5]), + make_tip(tip.to_string()), + TipPos::Bottom, + ) + .gap(4.0) + .delay(Duration::from_millis(400)) + .style(tip_style) + .into() + }) + .collect(); + row(btns).spacing(2).align_y(iced::Center).into() + }; + + let mut col_items: Vec> = vec![ + container(row![combo_btn, items_panel].spacing(0)) + .width(Fill) + .into(), + ]; + for row_tools in rows { + col_items.push(make_tool_row(row_tools)); + } + + container(column(col_items).spacing(3).align_x(iced::Left)) + .width(Length::Fixed(STYLE_COMBO_W)) + .height(Fill) + .align_y(iced::Center) + .padding(Padding { top: 4.0, bottom: 4.0, left: 4.0, right: 4.0 }) + .into() + } + _ => text("").into(), } } @@ -636,6 +848,7 @@ pub(super) fn compute_dropdown_left( | RibbonItem::LargeDropdown { .. } | RibbonItem::LayerComboGroup { .. } | RibbonItem::PropertiesGroup { .. } + | RibbonItem::StyleComboGroup { .. } ); let id: &str = match item { RibbonItem::LargeTool(t) => t.id, @@ -644,11 +857,13 @@ pub(super) fn compute_dropdown_left( RibbonItem::Dropdown { id, .. } => *id, RibbonItem::LayerComboGroup { .. } => LAYER_COMBO_ID, RibbonItem::PropertiesGroup { match_prop } => match_prop.id, + RibbonItem::StyleComboGroup { combo_id, .. } => combo_id, }; let item_w = match item { RibbonItem::LargeTool(_) | RibbonItem::LargeDropdown { .. } => LARGE_W, RibbonItem::LayerComboGroup { .. } => LARGE_W * 2.5, RibbonItem::PropertiesGroup { .. } => LARGE_W + 4.0 + 130.0, + RibbonItem::StyleComboGroup { .. } => LARGE_W * 2.3, RibbonItem::Dropdown { .. } => SMALL_W + ARROW_W, _ => SMALL_W, };