diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 48d414395..c92a80924 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -16,13 +16,13 @@ use iced::widget::{button, column, container, mouse_area, row, scrollable, svg, use iced::{Background, Border, Color, Element, Fill, Length, Padding, Theme}; use crate::app::Message; -use crate::modules::{CadModule, IconKind, RibbonItem}; +use crate::modules::{CadModule, IconKind, RibbonGroup, RibbonItem}; use crate::plugin::all_ribbon_modules; use crate::ui::properties::{lw_options, LinetypeItem}; mod widgets; use widgets::{StyleContext, *}; -use crate::ui::wrap_bar::{WrapBar, WrapFlow}; +use crate::ui::wrap_bar::{DensitySwap, WrapBar, WrapFlow}; // ── Ribbon state ─────────────────────────────────────────────────────────── @@ -365,16 +365,6 @@ impl Ribbon { let tool_area: Element<'_, Message> = if let Some(module) = self.modules.get(effective_active) { let groups = module.ribbon_groups(); - let wireframe = self.wireframe; - let ortho_mode = self.ortho_mode; - let active_tool = self.active_tool.clone(); - let open_dd = self.open_dropdown.clone(); - let last_cmd = &self.last_cmd; - let layer_infos = &self.layer_infos; - let active_layer = &self.active_layer; - 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(), @@ -386,92 +376,40 @@ impl Ribbon { active_table_style: self.active_table_style.clone(), }; - let mut widgets: Vec> = Vec::new(); - let mut first_group = true; - - for group in groups { - if !first_group { - widgets.push( - container(text("")) - .width(1) - .height(Fill) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER_DARK)), - ..Default::default() - }) - .into(), - ); - } - first_group = false; - - let mut items_row: Vec> = Vec::new(); - let mut small_buf: Vec> = Vec::new(); - - for item in &group.tools { - let is_large = matches!( - &item, - RibbonItem::LargeTool(_) - | RibbonItem::LargeDropdown { .. } - | RibbonItem::LayerComboGroup { .. } - | RibbonItem::PropertiesGroup { .. } - | RibbonItem::StyleComboGroup { .. } - ); - - if is_large { - flush_small_col(&mut small_buf, &mut items_row); - items_row.push(render_large( - item, - &active_tool, - &open_dd, - last_cmd, - wireframe, - ortho_mode, - layer_infos, - active_layer, - active_color, - active_linetype, - active_lineweight, - &style_ctx, - )); - } else { - small_buf.push(render_small( - item, - &active_tool, - &open_dd, - last_cmd, - wireframe, - ortho_mode, - )); - if small_buf.len() == 3 { - flush_small_col(&mut small_buf, &mut items_row); - } - } - } - flush_small_col(&mut small_buf, &mut items_row); - - let tools_el = items_row - .into_iter() - .fold(row![].spacing(2).height(Fill).align_y(iced::Top), |r, e| { - r.push(e) - }); - - widgets.push( - column![ - tools_el, - container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]), - ] - .align_x(iced::Center) - .spacing(0) - .padding([3u16, 4]) - .height(Length::Fill) - .into(), - ); - } - - widgets - .into_iter() - .fold(row![].spacing(0).height(Length::Fill), |r, w| r.push(w)) - .into() + // Two density variants: full-size panels, and a compact one where + // large buttons shrink to icon-only columns. DensitySwap shows the + // widest that fits the current ribbon width. + let full = build_tool_row( + false, + &groups, + &self.active_tool, + &self.open_dropdown, + &self.last_cmd, + self.wireframe, + self.ortho_mode, + &self.layer_infos, + &self.active_layer, + self.active_color, + &self.active_linetype, + self.active_lineweight, + &style_ctx, + ); + let compact = build_tool_row( + true, + &groups, + &self.active_tool, + &self.open_dropdown, + &self.last_cmd, + self.wireframe, + self.ortho_mode, + &self.layer_infos, + &self.active_layer, + self.active_color, + &self.active_linetype, + self.active_lineweight, + &style_ctx, + ); + DensitySwap::new(vec![full, compact]).into() } else { text("").into() }; @@ -1136,6 +1074,112 @@ impl Ribbon { } } +/// Render one ribbon tool row for the active module's groups. When `compact`, +/// large tools/dropdowns are drawn as small icon columns so the whole row is +/// narrower; the combo groups (layer / properties / style) always stay full. +#[allow(clippy::too_many_arguments)] +fn build_tool_row<'a>( + compact: bool, + groups: &[RibbonGroup], + active_tool: &Option, + open_dd: &Option, + last_cmd: &HashMap<&'static str, &'static str>, + wireframe: bool, + ortho_mode: bool, + layer_infos: &'a [LayerInfo], + active_layer: &'a str, + active_color: AcadColor, + active_linetype: &'a str, + active_lineweight: LineWeight, + style_ctx: &StyleContext, +) -> Element<'a, Message> { + let mut widgets: Vec> = Vec::new(); + let mut first_group = true; + + for group in groups { + if !first_group { + widgets.push( + container(text("")) + .width(1) + .height(Fill) + .style(|_: &Theme| container::Style { + background: Some(Background::Color(BORDER_DARK)), + ..Default::default() + }) + .into(), + ); + } + first_group = false; + + let mut items_row: Vec> = Vec::new(); + let mut small_buf: Vec> = Vec::new(); + + for item in &group.tools { + let is_large = match item { + RibbonItem::LargeTool(_) | RibbonItem::LargeDropdown { .. } => !compact, + RibbonItem::LayerComboGroup { .. } + | RibbonItem::PropertiesGroup { .. } + | RibbonItem::StyleComboGroup { .. } => true, + _ => false, + }; + + if is_large { + flush_small_col(&mut small_buf, &mut items_row); + items_row.push(render_large( + item, + active_tool, + open_dd, + last_cmd, + wireframe, + ortho_mode, + layer_infos, + active_layer, + active_color, + active_linetype, + active_lineweight, + style_ctx, + )); + } else { + small_buf.push(render_small( + item, + active_tool, + open_dd, + last_cmd, + wireframe, + ortho_mode, + )); + if small_buf.len() == 3 { + flush_small_col(&mut small_buf, &mut items_row); + } + } + } + flush_small_col(&mut small_buf, &mut items_row); + + let tools_el = items_row + .into_iter() + .fold(row![].spacing(2).height(Fill).align_y(iced::Top), |r, e| { + r.push(e) + }); + + widgets.push( + column![ + tools_el, + container(text(group.title).size(9).color(GROUP_LABEL)).padding([1, 4]), + ] + .align_x(iced::Center) + .spacing(0) + .padding([3u16, 4]) + .height(Length::Fill) + .into(), + ); + } + + widgets + .into_iter() + .fold(row![].spacing(0).height(Length::Fill), |r, w| r.push(w)) + .into() +} + impl Default for Ribbon { fn default() -> Self { Self::new() diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 07ef34feb..61b9b3ba9 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -344,7 +344,9 @@ pub(super) fn render_small<'a>( ortho_mode: bool, ) -> Element<'a, Message> { match item { - RibbonItem::Tool(t) => { + // Large variants render small too, so the ribbon can shrink a panel of + // large buttons to icon-only columns when the width is tight. + RibbonItem::Tool(t) | RibbonItem::LargeTool(t) => { let active = is_active_tool(t.id, active_tool, wireframe, ortho_mode); let event = t.event.clone(); let tool_id = t.id.to_string(); @@ -368,6 +370,13 @@ pub(super) fn render_small<'a>( items, default, .. + } + | RibbonItem::LargeDropdown { + id, + icon, + items, + default, + .. } => { let active = active_tool.as_deref() == Some(*id) || items diff --git a/src/ui/wrap_bar.rs b/src/ui/wrap_bar.rs index 4a5da31df..65bee069f 100644 --- a/src/ui/wrap_bar.rs +++ b/src/ui/wrap_bar.rs @@ -15,6 +15,7 @@ //! anchor overlays below the possibly-taller bar. use std::sync::atomic::{AtomicU32, Ordering}; +use std::cell::Cell; use std::sync::Arc; use iced::advanced::layout::{self, Layout}; @@ -612,3 +613,180 @@ impl<'a> From> for Element<'a, Message> { Element::new(w) } } + +/// Picks the widest variant that fits the available width and shows only that +/// one — used by the ribbon tool area to swap a full-size panel row for a +/// compact (icon-only) one when the window is too narrow. Variants must be +/// ordered widest-first; the last is shown when none fit. +pub struct DensitySwap<'a> { + variants: Vec>, + chosen: Cell, +} + +impl<'a> DensitySwap<'a> { + pub fn new(variants: Vec>) -> Self { + Self { + variants, + chosen: Cell::new(0), + } + } +} + +impl<'a> Widget for DensitySwap<'a> { + fn children(&self) -> Vec { + self.variants.iter().map(widget::Tree::new).collect() + } + + fn diff(&self, tree: &mut widget::Tree) { + let refs: Vec<_> = self.variants.iter().map(|e| e.as_widget()).collect(); + tree.diff_children(&refs); + } + + fn size(&self) -> Size { + Size::new(Length::Shrink, Length::Fill) + } + + fn layout( + &mut self, + tree: &mut widget::Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let max_w = limits.max().width; + let natural = + layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, limits.max().height)); + + // Widest-first: keep the first variant whose natural width fits; else the + // last (narrowest). + let mut pick = self.variants.len().saturating_sub(1); + for (i, v) in self.variants.iter_mut().enumerate() { + let n = v.as_widget_mut().layout(&mut tree.children[i], renderer, &natural); + if n.size().width <= max_w { + pick = i; + break; + } + } + self.chosen.set(pick); + + let node = + self.variants[pick] + .as_widget_mut() + .layout(&mut tree.children[pick], renderer, limits); + let sz = node.size(); + layout::Node::with_children(sz, vec![node]) + } + + fn update( + &mut self, + tree: &mut widget::Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + let i = self.chosen.get(); + if let Some(child_layout) = layout.children().next() { + self.variants[i].as_widget_mut().update( + &mut tree.children[i], + event, + child_layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + + fn mouse_interaction( + &self, + tree: &widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let i = self.chosen.get(); + layout + .children() + .next() + .map(|child_layout| { + self.variants[i].as_widget().mouse_interaction( + &tree.children[i], + child_layout, + cursor, + viewport, + renderer, + ) + }) + .unwrap_or_default() + } + + fn operate( + &mut self, + tree: &mut widget::Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn widget::Operation, + ) { + let i = self.chosen.get(); + if let Some(child_layout) = layout.children().next() { + self.variants[i] + .as_widget_mut() + .operate(&mut tree.children[i], child_layout, renderer, operation); + } + } + + fn draw( + &self, + tree: &widget::Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let i = self.chosen.get(); + if let Some(child_layout) = layout.children().next() { + self.variants[i].as_widget().draw( + &tree.children[i], + renderer, + theme, + style, + child_layout, + cursor, + viewport, + ); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut widget::Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + let i = self.chosen.get(); + let child_layout = layout.children().next()?; + self.variants[i].as_widget_mut().overlay( + &mut tree.children[i], + child_layout, + renderer, + viewport, + translation, + ) + } +} + +impl<'a> From> for Element<'a, Message> { + fn from(w: DensitySwap<'a>) -> Self { + Element::new(w) + } +}