From 73497850e28d70c4db490e6fc4d82571c5b22deb Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 25 Jun 2026 18:35:49 +0300 Subject: [PATCH] fix(ribbon): float annotate style dropdowns so they aren't clipped (#153) The text / dimension / multileader / table style combos rendered their open list inline inside the ribbon row, so the popup was clipped to the fixed ribbon-row height and got cut off even with only two entries. The Draw-tab dropdowns and the layer combo avoid this by rendering as floating overlays. Render the style list as an overlay too: the combo now draws only its button inline, and a new `style_combo_overlay` builds the list as a floating panel that grows to fit its entries, positioned with the existing `compute_dropdown_left` (which already accounts for StyleComboGroup widths) and mirroring `layer_combo_overlay`. Co-Authored-By: Claude Opus 4.8 --- src/ui/ribbon/mod.rs | 132 ++++++++++++++++++++++++++++++++++++ src/ui/ribbon/widgets.rs | 141 +++------------------------------------ 2 files changed, 141 insertions(+), 132 deletions(-) diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index ced0d5908..b0aa11557 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -567,6 +567,13 @@ impl Ribbon { return self.prop_lw_overlay(); } + // Style combo dropdowns (annotate tab) float as overlays so the list + // isn't clipped by the fixed ribbon-row height, the way the Draw-tab + // dropdowns already are. (#153) + if let Some(ov) = self.style_combo_overlay(open_id) { + return Some(ov); + } + let module = self.modules.get(self.active)?; let groups = module.ribbon_groups(); let mut items_list: Option> = None; @@ -804,6 +811,131 @@ impl Ribbon { ) } + /// Floating popup for an annotate-tab style combo (text / dimension / + /// multileader / table style). Returns `None` when `open_id` is not a + /// style combo. Built as an overlay — like the layer combo and the + /// Draw-tab dropdowns — so the list grows to fit its entries instead of + /// being clipped to the ribbon-row height. (#153) + fn style_combo_overlay(&self, open_id: &str) -> Option> { + let groups = self.modules.get(self.active)?.ribbon_groups(); + + // Locate the open style combo; capture its style key + manager command. + let mut found: Option<(crate::modules::StyleKey, Option<&'static str>)> = None; + 'outer: for group in &groups { + for item in &group.tools { + if let RibbonItem::StyleComboGroup { + style_key, + combo_id, + manager_cmd, + .. + } = item + { + if *combo_id == open_id { + found = Some((*style_key, *manager_cmd)); + break 'outer; + } + } + } + } + let (style_key, manager_cmd) = found?; + + let 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 active = ctx.active_for(style_key).to_string(); + + let row_style = |_: &Theme, status: button::Status| button::Style { + background: Some(Background::Color(match status { + button::Status::Hovered | button::Status::Pressed => ROW_HOVER, + _ => Color::TRANSPARENT, + })), + ..Default::default() + }; + + let mut rows: Vec> = ctx + .names_for(style_key) + .iter() + .map(|name| { + let is_sel = name.as_str() == active.as_str(); + let n = name.clone(); + let checkmark: Element = container(if is_sel { + crate::ui::icons::tinted(crate::ui::icons::CHECK, 11.0, CHECK_COLOR) + } else { + iced::widget::Space::new().width(0).into() + }) + .width(Length::Fixed(14.0)) + .into(); + button( + row![ + checkmark, + text(name.clone()) + .size(11) + .color(if is_sel { LABEL_ON } else { LABEL_OFF }), + ] + .spacing(4) + .align_y(iced::Center), + ) + .on_press(Message::RibbonStyleChanged { + key: style_key, + name: n, + }) + .style(row_style) + .width(Fill) + .padding([4, 10]) + .into() + }) + .collect(); + + if let Some(mgr) = manager_cmd { + rows.push( + button(text("Manage…").size(11).color(LABEL_ON)) + .on_press(Message::Command(mgr.to_string())) + .style(row_style) + .width(Fill) + .padding([4, 10]) + .into(), + ); + } + + let panel = container(column(rows)) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(PANEL_BG)), + border: Border { + color: PANEL_BORDER, + width: 1.0, + radius: 3.0.into(), + }, + ..Default::default() + }) + .width(Length::Fixed(LARGE_W * 2.3)); + + let top_offset = 28.0 + TOOL_BAR_H; + let left_offset = compute_dropdown_left(&groups, open_id); + let positioned = container(panel) + .align_left(Fill) + .align_top(Fill) + .padding(Padding { + top: top_offset, + left: left_offset, + ..Default::default() + }) + .width(Fill) + .height(Fill); + + Some( + mouse_area(positioned) + .on_press(Message::CloseRibbonDropdown) + .into(), + ) + } + fn prop_color_overlay(&self) -> Option> { let picker = crate::ui::color_select::color_list( crate::ui::color_select::ColorExtras { diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 806a64cc1..b02fc5a68 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -8,7 +8,7 @@ use acadrust::types::{Color as AcadColor, LineWeight}; // Ribbon tooltips anchor to the right of their button so the cursor — which // rests on the button itself — never covers the tip text. (#143) use iced::widget::tooltip::Position as TipPos; -use iced::widget::{button, column, container, row, scrollable, svg, text, tooltip}; +use iced::widget::{button, column, container, row, svg, text, tooltip}; use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::app::Message; @@ -177,7 +177,7 @@ pub(super) struct StyleContext { } impl StyleContext { - fn names_for(&self, key: StyleKey) -> &[String] { + pub(super) fn names_for(&self, key: StyleKey) -> &[String] { match key { StyleKey::TextStyle => &self.text_style_names, StyleKey::DimStyle => &self.dim_style_names, @@ -185,7 +185,7 @@ impl StyleContext { StyleKey::TableStyle => &self.table_style_names, } } - fn active_for(&self, key: StyleKey) -> &str { + pub(super) fn active_for(&self, key: StyleKey) -> &str { match key { StyleKey::TextStyle => &self.active_text_style, StyleKey::DimStyle => &self.active_dim_style, @@ -859,11 +859,10 @@ pub(super) fn render_large<'a>( 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); @@ -933,133 +932,11 @@ pub(super) fn render_large<'a>( .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![ - container(if is_sel { - icons::tinted( - icons::CHECK, - 10.0, - Color { - r: 0.2, - g: 0.8, - b: 0.4, - a: 1.0, - }, - ) - } else { - iced::widget::Space::new().width(0).into() - }) - .width(12), - 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() - }; + // The open style list renders as a floating overlay + // (`Ribbon::style_combo_overlay`) so it isn't clipped by the fixed + // ribbon-row height — matching the Draw-tab dropdowns. (#153) + let items_panel: Element = + iced::widget::Space::new().width(0).height(0).into(); // ── tool rows below combo ── let make_tool_row = |tools: Vec| -> Element {