feat(ribbon): tighten + squeeze collapsed panels, content-driven height

Adaptive tool-panel row now keeps degrading past the collapsed state instead
of overflowing:

- Panels still shrink from the right one at a time (full -> compact ->
  collapsed). Once every panel is a collapsed button and the row still
  overflows, each button drops its large icon to a small one (tight) together,
  then the buttons are pulled together up to 8px per gap (reclaiming edge
  padding) before anything is clipped.
- Tool-area height now tracks the tallest shown panel, so the ribbon shrinks
  as its panels collapse; the measured height is reported for dropdown
  anchoring.
- Tabs squeeze their inter-tab gaps shut before wrapping to a new row
  (WrapFlow min_spacing_x).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-07-05 23:58:18 +03:00
1 parent ced42daff5
commit 8e6146fd91
4 files changed
+282 -102

No files matched your search

+101 -48
View File
@@ -66,8 +66,9 @@ pub struct Ribbon {
/// Measured tab-bar height (28 on one row, 56 when tabs wrap). Written by
/// the `WrapBar` widget during layout, read when anchoring dropdowns.
tab_bar_h: Arc<AtomicU32>,
/// Measured tool-area height (TOOL_BAR_H on one row, taller when the panels
/// wrap). Written by the `DensitySwap` widget, read when anchoring dropdowns.
/// Measured tool-area height (TOOL_BAR_H at full density, shorter once every
/// panel is collapsed). Written by `CollapsePanels`, read when anchoring
/// dropdowns below the ribbon.
tool_bar_h: Arc<AtomicU32>,
}
@@ -429,7 +430,11 @@ impl Ribbon {
},
);
let tabs = WrapFlow::new(tab_items).spacing_x(6.0).row_h(28.0);
// Tabs may squeeze their gaps shut (down to 0px) to fit before wrapping.
let tabs = WrapFlow::new(tab_items)
.spacing_x(6.0)
.min_spacing_x(0.0)
.row_h(28.0);
let tab_bar = container(
WrapBar::new(lead.into(), tabs.into())
@@ -520,6 +525,23 @@ impl Ribbon {
&self.active_linetype,
self.active_lineweight,
&style_ctx,
false,
),
tight: collapse_button(
g,
self.last_panel_tool.get(g.title).copied(),
&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,
true,
),
flyout: container(render_group(
false,
@@ -549,6 +571,7 @@ impl Ribbon {
})
.collect();
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H, BORDER_DARK)
.report_height(self.tool_bar_h.clone())
.into()
} else {
text("").into()
@@ -1264,8 +1287,9 @@ fn representative<'g>(group: &'g RibbonGroup, last_used: Option<&str>) -> Option
}
/// A collapsed panel: its representative tool (a live button that updates to the
/// last-used tool) plus a title + ▾ opener for the full flyout. Fixed
/// `TOOL_BAR_H` tall so it lines up with inline panels.
/// last-used tool) plus a title + ▾ opener for the full flyout. Its height is its
/// natural content height (shorter than a full 3-row panel), so `CollapsePanels`
/// can shrink the ribbon once every panel is collapsed.
#[allow(clippy::too_many_arguments)]
fn collapse_button<'a>(
group: &RibbonGroup,
@@ -1281,43 +1305,65 @@ fn collapse_button<'a>(
active_linetype: &'a str,
active_lineweight: LineWeight,
style_ctx: &StyleContext,
// When set, the button shows a SMALL representative icon instead of the large
// one — the `tight` step used once even the all-collapsed row overflows.
compact: bool,
) -> Element<'a, Message> {
let title = group.title;
// Show the representative tool with a large icon. For a Properties panel the
// representative is its Match button, rendered as a large tool.
let face: Element<'_, Message> = match representative(group, last_used) {
Some(RibbonItem::PropertiesGroup { match_prop }) => render_large(
&RibbonItem::LargeTool(match_prop.clone()),
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
layer_infos,
active_layer,
active_color,
active_linetype,
active_lineweight,
style_ctx,
false,
),
Some(item) => render_large(
item,
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
layer_infos,
active_layer,
active_color,
active_linetype,
active_lineweight,
style_ctx,
false,
),
None => text("").into(),
// The representative tool's face. Normally a large icon; in the tight step it
// shrinks to the small icon (SMALL_W vs LARGE_W) to reclaim width. For a
// Properties panel the representative is its Match button.
let rep = representative(group, last_used);
let face: Element<'_, Message> = if compact {
match rep {
Some(RibbonItem::PropertiesGroup { match_prop }) => render_small(
&RibbonItem::LargeTool(match_prop.clone()),
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
),
Some(item) => {
render_small(item, active_tool, open_dd, last_cmd, wireframe, ortho_mode)
}
None => text("").into(),
}
} else {
match rep {
Some(RibbonItem::PropertiesGroup { match_prop }) => render_large(
&RibbonItem::LargeTool(match_prop.clone()),
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
layer_infos,
active_layer,
active_color,
active_linetype,
active_lineweight,
style_ctx,
false,
),
Some(item) => render_large(
item,
active_tool,
open_dd,
last_cmd,
wireframe,
ortho_mode,
layer_infos,
active_layer,
active_color,
active_linetype,
active_lineweight,
style_ctx,
false,
),
None => text("").into(),
}
};
let opener = button(
@@ -1347,15 +1393,22 @@ fn collapse_button<'a>(
})
.padding([1, 4]);
column![
container(face).height(Fill).align_y(iced::Center),
opener,
]
.align_x(iced::Center)
.spacing(2)
.padding([3u16, 4])
.height(Length::Fixed(TOOL_BAR_H))
.into()
// The large face fills a fixed slot; the small (compact) face keeps its own
// natural height. Either way the whole button is natural height, so a
// collapsed / tight panel is shorter than a full one and the row shrinks.
let face_box = if compact {
container(face).align_y(iced::Center)
} else {
container(face)
.height(Length::Fixed(COLLAPSED_FACE_H))
.align_y(iced::Center)
};
column![face_box, opener]
.align_x(iced::Center)
.spacing(2)
.padding([3u16, 4])
.into()
}
impl Default for Ribbon {