From 5164dc13874b5f1bfe9bcf2b684e3634b77c676e Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 5 Jul 2026 19:04:47 +0300 Subject: [PATCH] feat(ribbon): degrade tool panels per-panel from the right instead of all at once MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool area no longer swaps every panel between a full and a compact row together. CollapsePanels is now the whole tool area: it measures each panel at full, compact and collapsed widths and degrades from the right — first shrinking a panel's large buttons to compact icon columns one panel at a time until the row fits, then (only if all-compact still overflows) collapsing the rightmost panels to their ▾ flyout buttons. Left panels stay full as long as possible. Removes the global DensitySwap full/compact/collapse variant selection and the now-unused build_tool_groups / tool_divider helpers. Co-Authored-By: Claude Opus 4.8 --- src/ui/ribbon/collapse.rs | 185 +++++++++++++++++++++----------------- src/ui/ribbon/mod.rs | 127 +++++--------------------- 2 files changed, 123 insertions(+), 189 deletions(-) diff --git a/src/ui/ribbon/collapse.rs b/src/ui/ribbon/collapse.rs index 6897ed21f..52c60c36d 100644 --- a/src/ui/ribbon/collapse.rs +++ b/src/ui/ribbon/collapse.rs @@ -1,12 +1,11 @@ -//! Collapsing ribbon panels. +//! Adaptive ribbon panels. //! -//! Given a list of panels (each with an inline element, a collapsed title -//! button, and a full flyout), this widget lays out as many inline panels as -//! fit the width from the left and collapses the rest into their title buttons. -//! Clicking a collapsed button opens that panel's flyout as an overlay anchored -//! just below the button. +//! Lays the active module's panels on one row. When they don't all fit, panels +//! degrade **from the right**: first a panel's large buttons shrink to compact +//! icon columns, then — if it still doesn't fit — the panel collapses to a title +//! button whose click opens the full panel as a flyout overlay. -use std::cell::Cell; +use std::cell::RefCell; use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Widget}; @@ -15,67 +14,71 @@ use iced::{Element, Event, Length, Point, Rectangle, Renderer, Size, Theme, Vect use crate::app::Message; -/// One ribbon panel in its three renderings. +/// One panel in its four renderings. pub struct Panel<'a> { pub id: String, - pub inline: Element<'a, Message>, + pub full: Element<'a, Message>, + pub compact: Element<'a, Message>, pub button: Element<'a, Message>, pub flyout: Element<'a, Message>, } +// Per-panel degradation level; also the offset of the shown element within the +// panel's 4 trees ([full, compact, button, flyout]). +const FULL: u8 = 0; +const COMPACT: u8 = 1; +const COLLAPSED: u8 = 2; + pub struct CollapsePanels<'a> { panels: Vec>, /// Title of the panel whose flyout is open (if any). open: Option, /// Row height (a full ribbon tool-area height). row_h: f32, - /// Number of panels shown inline from the left; set during layout. - split: Cell, + /// Chosen degradation level per panel; set during layout. + levels: RefCell>, } impl<'a> CollapsePanels<'a> { pub fn new(panels: Vec>, open: Option, row_h: f32) -> Self { + let n = panels.len(); Self { panels, open, row_h, - split: Cell::new(0), + levels: RefCell::new(vec![FULL; n]), } } - /// Element used inline for panel `i` given the split `k`. - fn shown(&self, i: usize, k: usize) -> &Element<'a, Message> { - if i < k { - &self.panels[i].inline - } else { - &self.panels[i].button + fn shown(&self, i: usize, level: u8) -> &Element<'a, Message> { + match level { + FULL => &self.panels[i].full, + COMPACT => &self.panels[i].compact, + _ => &self.panels[i].button, } } - fn shown_mut(&mut self, i: usize, k: usize) -> &mut Element<'a, Message> { - if i < k { - &mut self.panels[i].inline - } else { - &mut self.panels[i].button + fn shown_mut(&mut self, i: usize, level: u8) -> &mut Element<'a, Message> { + match level { + FULL => &mut self.panels[i].full, + COMPACT => &mut self.panels[i].compact, + _ => &mut self.panels[i].button, } } - /// Tree index of the element shown inline for panel `i` given split `k`. - fn shown_tree(i: usize, k: usize) -> usize { - // trees are laid out [inline_0, button_0, flyout_0, inline_1, …] - if i < k { - 3 * i - } else { - 3 * i + 1 - } + fn levels_snapshot(&self, n: usize) -> Vec { + let mut v = self.levels.borrow().clone(); + v.resize(n, FULL); + v } } impl<'a> Widget for CollapsePanels<'a> { fn children(&self) -> Vec { - let mut v = Vec::with_capacity(self.panels.len() * 3); + let mut v = Vec::with_capacity(self.panels.len() * 4); for p in &self.panels { - v.push(widget::Tree::new(&p.inline)); + v.push(widget::Tree::new(&p.full)); + v.push(widget::Tree::new(&p.compact)); v.push(widget::Tree::new(&p.button)); v.push(widget::Tree::new(&p.flyout)); } @@ -85,7 +88,8 @@ impl<'a> Widget for CollapsePanels<'a> { fn diff(&self, tree: &mut widget::Tree) { let mut refs: Vec<&dyn Widget> = Vec::new(); for p in &self.panels { - refs.push(p.inline.as_widget()); + refs.push(p.full.as_widget()); + refs.push(p.compact.as_widget()); refs.push(p.button.as_widget()); refs.push(p.flyout.as_widget()); } @@ -103,57 +107,69 @@ impl<'a> Widget for CollapsePanels<'a> { limits: &layout::Limits, ) -> layout::Node { let max_w = limits.max().width; - let natural = - layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY)); + let natural = layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY)); let n = self.panels.len(); - // Measure every inline panel and every collapsed button. - let mut inline_w = vec![0.0f32; n]; + // Measure each panel at all three densities. + let mut full_w = vec![0.0f32; n]; + let mut compact_w = vec![0.0f32; n]; let mut button_w = vec![0.0f32; n]; for i in 0..n { - let inline_node = self.panels[i].inline.as_widget_mut().layout( - &mut tree.children[3 * i], - renderer, - &natural, - ); - let button_node = self.panels[i].button.as_widget_mut().layout( - &mut tree.children[3 * i + 1], - renderer, - &natural, - ); - inline_w[i] = inline_node.size().width; - button_w[i] = button_node.size().width; + full_w[i] = self.panels[i].full.as_widget_mut() + .layout(&mut tree.children[4 * i], renderer, &natural) + .size() + .width; + compact_w[i] = self.panels[i].compact.as_widget_mut() + .layout(&mut tree.children[4 * i + 1], renderer, &natural) + .size() + .width; + button_w[i] = self.panels[i].button.as_widget_mut() + .layout(&mut tree.children[4 * i + 2], renderer, &natural) + .size() + .width; } - // Expand a prefix of panels from the left while the row still fits with - // the remaining panels collapsed to buttons. - let all_buttons: f32 = button_w.iter().sum(); - let mut used = all_buttons; - let mut k = 0; - for i in 0..n { - let candidate = used - button_w[i] + inline_w[i]; - if candidate <= max_w { - used = candidate; - k = i + 1; - } else { + let width_of = |lv: u8, i: usize| -> f32 { + match lv { + FULL => full_w[i], + COMPACT => compact_w[i], + _ => button_w[i], + } + }; + let total = |levels: &[u8]| -> f32 { (0..n).map(|i| width_of(levels[i], i)).sum() }; + + // Start all full; degrade from the RIGHT. Phase 1 shrinks large panels + // to compact one at a time; phase 2 (only if compact still overflows) + // collapses them to buttons. + let mut levels = vec![FULL; n]; + for i in (0..n).rev() { + if total(&levels) <= max_w { break; } + levels[i] = COMPACT; } - self.split.set(k); + for i in (0..n).rev() { + if total(&levels) <= max_w { + break; + } + levels[i] = COLLAPSED; + } + *self.levels.borrow_mut() = levels.clone(); // Place the chosen element for each panel left-to-right. let mut children: Vec = Vec::with_capacity(n); let mut x = 0.0f32; for i in 0..n { - let tree_idx = Self::shown_tree(i, k); - let node = self.shown_mut(i, k).as_widget_mut().layout( + let level = levels[i]; + let tree_idx = 4 * i + level as usize; + let node = self.shown_mut(i, level).as_widget_mut().layout( &mut tree.children[tree_idx], renderer, &natural, ); let h = node.size().height; - let y = ((self.row_h - h) / 2.0).max(0.0); let w = node.size().width; + let y = ((self.row_h - h) / 2.0).max(0.0); children.push(node.move_to(Point::new(x, y))); x += w; } @@ -172,10 +188,11 @@ impl<'a> Widget for CollapsePanels<'a> { shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - let k = self.split.get(); + let levels = self.levels_snapshot(self.panels.len()); for (i, child_layout) in layout.children().enumerate() { - let tree_idx = Self::shown_tree(i, k); - self.shown_mut(i, k).as_widget_mut().update( + let level = levels[i]; + let tree_idx = 4 * i + level as usize; + self.shown_mut(i, level).as_widget_mut().update( &mut tree.children[tree_idx], event, child_layout, @@ -196,11 +213,12 @@ impl<'a> Widget for CollapsePanels<'a> { viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let k = self.split.get(); + let levels = self.levels_snapshot(self.panels.len()); let mut interaction = mouse::Interaction::default(); for (i, child_layout) in layout.children().enumerate() { - let tree_idx = Self::shown_tree(i, k); - let it = self.shown(i, k).as_widget().mouse_interaction( + let level = levels[i]; + let tree_idx = 4 * i + level as usize; + let it = self.shown(i, level).as_widget().mouse_interaction( &tree.children[tree_idx], child_layout, cursor, @@ -221,10 +239,11 @@ impl<'a> Widget for CollapsePanels<'a> { renderer: &Renderer, operation: &mut dyn widget::Operation, ) { - let k = self.split.get(); + let levels = self.levels_snapshot(self.panels.len()); for (i, child_layout) in layout.children().enumerate() { - let tree_idx = Self::shown_tree(i, k); - self.shown_mut(i, k).as_widget_mut().operate( + let level = levels[i]; + let tree_idx = 4 * i + level as usize; + self.shown_mut(i, level).as_widget_mut().operate( &mut tree.children[tree_idx], child_layout, renderer, @@ -243,10 +262,11 @@ impl<'a> Widget for CollapsePanels<'a> { cursor: mouse::Cursor, viewport: &Rectangle, ) { - let k = self.split.get(); + let levels = self.levels_snapshot(self.panels.len()); for (i, child_layout) in layout.children().enumerate() { - let tree_idx = Self::shown_tree(i, k); - self.shown(i, k).as_widget().draw( + let level = levels[i]; + let tree_idx = 4 * i + level as usize; + self.shown(i, level).as_widget().draw( &tree.children[tree_idx], renderer, theme, @@ -266,11 +286,11 @@ impl<'a> Widget for CollapsePanels<'a> { _viewport: &Rectangle, translation: Vector, ) -> Option> { - let k = self.split.get(); + let levels = self.levels_snapshot(self.panels.len()); let open_id = self.open.clone()?; let p = self.panels.iter().position(|pan| pan.id == open_id)?; - // Only collapsed panels (index >= k) show a flyout. - if p < k { + // Only a collapsed panel shows a flyout. + if levels.get(p).copied().unwrap_or(FULL) != COLLAPSED { return None; } @@ -280,7 +300,7 @@ impl<'a> Widget for CollapsePanels<'a> { Some(overlay::Element::new(Box::new(FlyoutOverlay { flyout: &mut self.panels[p].flyout, - tree: &mut tree.children[3 * p + 2], + tree: &mut tree.children[4 * p + 3], anchor, }))) } @@ -352,9 +372,6 @@ impl overlay::Overlay for FlyoutOverlay<'_, '_> { let child = layout.children().next().unwrap(); let vp = child.bounds(); - // Close when a press lands outside the flyout. Capture the event so the - // press doesn't also reach the collapsed button underneath (which would - // immediately re-open the flyout). if let Event::Mouse(mouse::Event::ButtonPressed(_)) = event { if !cursor.is_over(vp) { shell.publish(Message::CloseRibbonDropdown); diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 127ea7803..7b174b3ce 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -24,7 +24,7 @@ mod widgets; use widgets::{StyleContext, *}; mod collapse; use collapse::{CollapsePanels, Panel}; -use crate::ui::wrap_bar::{DensitySwap, WrapBar, WrapFlow}; +use crate::ui::wrap_bar::{WrapBar, WrapFlow}; // ── Ribbon state ─────────────────────────────────────────────────────────── @@ -468,47 +468,30 @@ impl Ribbon { active_table_style: self.active_table_style.clone(), }; - // Adaptive tool area, widest→narrowest: - // 1. full — full-size panels on one row - // 2. compact — large buttons shrink to icon-only columns - // 3. collapse — panels that still don't fit collapse to title - // buttons that open the full panel as a flyout - // DensitySwap shows the widest variant that fits the width. - let build = |compact: bool| { - build_tool_groups( - compact, - &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 full: Element<'_, Message> = build(false) - .into_iter() - .fold(row![].spacing(0).height(Length::Fixed(TOOL_BAR_H)), |r, w| { - r.push(w) - }) - .into(); - let compact: Element<'_, Message> = build(true) - .into_iter() - .fold(row![].spacing(0).height(Length::Fixed(TOOL_BAR_H)), |r, w| { - r.push(w) - }) - .into(); - + // Adaptive tool area: panels sit on one row; when they don't all + // fit they degrade from the right — a panel's large buttons first + // shrink to compact icon columns, then it collapses to a ▾ flyout + // button. See `CollapsePanels`. let panels: Vec> = groups .iter() .map(|g| Panel { id: g.title.to_string(), - inline: render_group( + full: render_group( + false, + g, + &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, + ), + compact: render_group( true, g, &self.active_tool, @@ -565,12 +548,7 @@ impl Ribbon { .into(), }) .collect(); - let collapse: Element<'_, Message> = - CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H).into(); - - DensitySwap::new(vec![full, compact, collapse]) - .report_height(self.tool_bar_h.clone()) - .into() + CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H).into() } else { text("").into() }; @@ -1179,67 +1157,6 @@ impl Ribbon { } } -/// Render the active module's ribbon panels as a list of group elements (with -/// 1px dividers between them). When `compact`, large tools/dropdowns are drawn -/// as small icon columns so each panel is narrower; the combo groups (layer / -/// properties / style) always stay full. Each panel is a fixed `TOOL_BAR_H` -/// tall so the list can be laid out in a row or flex-wrapped across rows. -#[allow(clippy::too_many_arguments)] -fn build_tool_groups<'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, -) -> Vec> { - let mut widgets: Vec> = Vec::new(); - let mut first_group = true; - - for group in groups { - if !first_group { - widgets.push(tool_divider()); - } - first_group = false; - widgets.push(render_group( - compact, - group, - active_tool, - open_dd, - last_cmd, - wireframe, - ortho_mode, - layer_infos, - active_layer, - active_color, - active_linetype, - active_lineweight, - style_ctx, - )); - } - - widgets -} - -/// A 1px vertical divider between ribbon panels, full tool-area height. -fn tool_divider<'a>() -> Element<'a, Message> { - container(text("")) - .width(1) - .height(Length::Fixed(TOOL_BAR_H)) - .style(|_: &Theme| container::Style { - background: Some(Background::Color(BORDER_DARK)), - ..Default::default() - }) - .into() -} - /// Render a single ribbon panel (tools + group label), fixed `TOOL_BAR_H` tall. /// When `compact`, large tools/dropdowns are drawn as small icon columns. #[allow(clippy::too_many_arguments)]