Files
OneCAD-Draft/src/ui/ribbon/collapse.rs
T
Hakan SevenandClaude Opus 4.8 8e6146fd91 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>
2026-07-05 23:58:18 +03:00

533 lines
18 KiB
Rust

//! Adaptive ribbon panels.
//!
//! Lays the active module's panels on one row. When they don't all fit, the row
//! degrades **from the right**, one panel at a time: first a panel shrinks to
//! compact icon columns, then it collapses to a title button. If even the
//! all-collapsed row overflows, every button drops to its small icon together,
//! then the buttons are squeezed. The row's height tracks the tallest shown
//! panel, so it shrinks as the panels do.
use std::cell::RefCell;
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::Arc;
use iced::advanced::layout::{self, Layout};
use iced::advanced::widget::{self, Widget};
use iced::advanced::{mouse, overlay, renderer, Clipboard, Renderer as _, Shell};
use iced::{
Background, Border, Color, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size,
Theme, Vector,
};
use crate::app::Message;
/// One panel in its five renderings. `tight` is the collapsed button with a
/// small representative icon instead of the large one — the last step before the
/// buttons are squeezed together.
pub struct Panel<'a> {
pub id: String,
pub full: Element<'a, Message>,
pub compact: Element<'a, Message>,
pub button: Element<'a, Message>,
pub tight: Element<'a, Message>,
pub flyout: Element<'a, Message>,
}
// Number of tree slots per panel: [full, compact, button, tight, flyout].
const SLOTS: usize = 5;
// Per-panel degradation level; also the offset of the shown element within the
// panel's tree slots. Degrade order (from the right): FULL → COMPACT → COLLAPSED
// (big-icon button) → TIGHT (small-icon button). `flyout` (slot 4) is overlay
// only, never a level.
const FULL: u8 = 0;
const COMPACT: u8 = 1;
const COLLAPSED: u8 = 2;
const TIGHT: u8 = 3;
/// When even the all-tight row still overflows, the collapsed buttons are pulled
/// together by up to this many px per gap — reclaiming their edge padding —
/// before anything is clipped. Mirrors the tab bar squeezing its gaps shut
/// before wrapping.
const MAX_PANEL_SQUEEZE: f32 = 8.0;
pub struct CollapsePanels<'a> {
panels: Vec<Panel<'a>>,
/// Title of the panel whose flyout is open (if any).
open: Option<String>,
/// Fallback row height, used only when there are no panels to measure.
row_h: f32,
/// Colour of the 1px divider drawn between panels.
divider: Color,
/// Chosen degradation level per panel; set during layout.
levels: RefCell<Vec<u8>>,
/// If set, the measured row height is written here each layout (read when
/// anchoring dropdowns below the ribbon).
height_out: Option<Arc<AtomicU32>>,
}
impl<'a> CollapsePanels<'a> {
pub fn new(panels: Vec<Panel<'a>>, open: Option<String>, row_h: f32, divider: Color) -> Self {
let n = panels.len();
Self {
panels,
open,
row_h,
divider,
levels: RefCell::new(vec![FULL; n]),
height_out: None,
}
}
/// Report the measured row height into `out` on every layout.
pub fn report_height(mut self, out: Arc<AtomicU32>) -> Self {
self.height_out = Some(out);
self
}
fn shown(&self, i: usize, level: u8) -> &Element<'a, Message> {
match level {
FULL => &self.panels[i].full,
COMPACT => &self.panels[i].compact,
COLLAPSED => &self.panels[i].button,
_ => &self.panels[i].tight,
}
}
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,
COLLAPSED => &mut self.panels[i].button,
_ => &mut self.panels[i].tight,
}
}
fn levels_snapshot(&self, n: usize) -> Vec<u8> {
let mut v = self.levels.borrow().clone();
v.resize(n, FULL);
v
}
}
impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
fn children(&self) -> Vec<widget::Tree> {
let mut v = Vec::with_capacity(self.panels.len() * SLOTS);
for p in &self.panels {
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.tight));
v.push(widget::Tree::new(&p.flyout));
}
v
}
fn diff(&self, tree: &mut widget::Tree) {
let mut refs: Vec<&dyn Widget<Message, Theme, Renderer>> = Vec::new();
for p in &self.panels {
refs.push(p.full.as_widget());
refs.push(p.compact.as_widget());
refs.push(p.button.as_widget());
refs.push(p.tight.as_widget());
refs.push(p.flyout.as_widget());
}
tree.diff_children(&refs);
}
fn size(&self) -> Size<Length> {
Size::new(Length::Shrink, Length::Shrink)
}
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, f32::INFINITY));
let n = self.panels.len();
// Measure each panel at all four densities. Width drives the degradation
// decision; height drives the row height.
let mut full_w = vec![0.0f32; n];
let mut compact_w = vec![0.0f32; n];
let mut button_w = vec![0.0f32; n];
let mut tight_w = vec![0.0f32; n];
let mut full_h = vec![0.0f32; n];
let mut compact_h = vec![0.0f32; n];
let mut button_h = vec![0.0f32; n];
let mut tight_h = vec![0.0f32; n];
for i in 0..n {
let s = self.panels[i].full.as_widget_mut()
.layout(&mut tree.children[SLOTS * i], renderer, &natural)
.size();
full_w[i] = s.width;
full_h[i] = s.height;
let s = self.panels[i].compact.as_widget_mut()
.layout(&mut tree.children[SLOTS * i + 1], renderer, &natural)
.size();
compact_w[i] = s.width;
compact_h[i] = s.height;
let s = self.panels[i].button.as_widget_mut()
.layout(&mut tree.children[SLOTS * i + 2], renderer, &natural)
.size();
button_w[i] = s.width;
button_h[i] = s.height;
let s = self.panels[i].tight.as_widget_mut()
.layout(&mut tree.children[SLOTS * i + 3], renderer, &natural)
.size();
tight_w[i] = s.width;
tight_h[i] = s.height;
}
let width_of = |lv: u8, i: usize| -> f32 {
match lv {
FULL => full_w[i],
COMPACT => compact_w[i],
COLLAPSED => button_w[i],
_ => tight_w[i],
}
};
let height_of = |lv: u8, i: usize| -> f32 {
match lv {
FULL => full_h[i],
COMPACT => compact_h[i],
COLLAPSED => button_h[i],
_ => tight_h[i],
}
};
let total = |levels: &[u8]| -> f32 { (0..n).map(|i| width_of(levels[i], i)).sum() };
// Degrade from the RIGHT, one panel at a time (gradual): first FULL →
// COMPACT, then COMPACT → COLLAPSED, each phase only while the row still
// overflows. If even the all-collapsed row overflows, every button drops
// to its small icon (tight) together, then the buttons are squeezed.
let mut levels = vec![FULL; n];
for degraded in [COMPACT, COLLAPSED] {
for i in (0..n).rev() {
if total(&levels) <= max_w {
break;
}
levels[i] = degraded;
}
}
if total(&levels) > max_w {
levels
.iter_mut()
.filter(|l| **l == COLLAPSED)
.for_each(|l| *l = TIGHT);
}
*self.levels.borrow_mut() = levels.clone();
// Same idea as the tab bar squeezing its gaps before wrapping: once every
// panel is at its tightest and the row STILL overflows, pull the buttons
// together (up to MAX_PANEL_SQUEEZE per gap, reclaiming their edge
// padding) so more of them stay on-screen before anything is clipped.
// `total(&levels) > max_w` only remains true when nothing more can be
// degraded, so this never overlaps a full/compact panel.
let squeeze = if n > 1 && total(&levels) > max_w {
((total(&levels) - max_w) / (n - 1) as f32).min(MAX_PANEL_SQUEEZE)
} else {
0.0
};
// The row is as tall as the tallest shown panel, so the ribbon height
// shrinks as its panels degrade to shorter collapsed / tight buttons.
let row_h = if n == 0 {
self.row_h
} else {
(0..n)
.map(|i| height_of(levels[i], i))
.fold(0.0f32, f32::max)
};
// Place the chosen element for each panel left-to-right.
let mut children: Vec<layout::Node> = Vec::with_capacity(n);
let mut x = 0.0f32;
for i in 0..n {
if i > 0 {
x -= squeeze;
}
let level = levels[i];
let tree_idx = SLOTS * 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 w = node.size().width;
let y = ((row_h - h) / 2.0).max(0.0);
children.push(node.move_to(Point::new(x, y)));
x += w;
}
if let Some(out) = &self.height_out {
out.store(row_h.to_bits(), Ordering::Relaxed);
}
layout::Node::with_children(Size::new(x, row_h), children)
}
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 levels = self.levels_snapshot(self.panels.len());
for (i, child_layout) in layout.children().enumerate() {
let level = levels[i];
let tree_idx = SLOTS * i + level as usize;
self.shown_mut(i, level).as_widget_mut().update(
&mut tree.children[tree_idx],
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 levels = self.levels_snapshot(self.panels.len());
let mut interaction = mouse::Interaction::default();
for (i, child_layout) in layout.children().enumerate() {
let level = levels[i];
let tree_idx = SLOTS * i + level as usize;
let it = self.shown(i, level).as_widget().mouse_interaction(
&tree.children[tree_idx],
child_layout,
cursor,
viewport,
renderer,
);
if it != mouse::Interaction::default() {
interaction = it;
}
}
interaction
}
fn operate(
&mut self,
tree: &mut widget::Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
let levels = self.levels_snapshot(self.panels.len());
for (i, child_layout) in layout.children().enumerate() {
let level = levels[i];
let tree_idx = SLOTS * i + level as usize;
self.shown_mut(i, level).as_widget_mut().operate(
&mut tree.children[tree_idx],
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 levels = self.levels_snapshot(self.panels.len());
for (i, child_layout) in layout.children().enumerate() {
let level = levels[i];
let tree_idx = SLOTS * i + level as usize;
self.shown(i, level).as_widget().draw(
&tree.children[tree_idx],
renderer,
theme,
style,
child_layout,
cursor,
viewport,
);
}
// 1px divider between adjacent panels, except between two button-form
// panels (collapsed or tight — they read better with no line between).
let is_btn = |lv: u8| lv == COLLAPSED || lv == TIGHT;
let bounds: Vec<Rectangle> = layout.children().map(|l| l.bounds()).collect();
let wb = layout.bounds();
for i in 0..self.panels.len().saturating_sub(1) {
if is_btn(levels[i]) && is_btn(levels[i + 1]) {
continue;
}
let x = bounds[i + 1].x;
renderer.fill_quad(
renderer::Quad {
bounds: Rectangle {
x,
y: wb.y,
width: 1.0,
height: wb.height,
},
border: Border::default(),
shadow: Shadow::default(),
snap: true,
},
Background::Color(self.divider),
);
}
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut widget::Tree,
layout: Layout<'b>,
_renderer: &Renderer,
_viewport: &Rectangle,
translation: Vector,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
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 a button-form panel (collapsed or tight) shows a flyout.
let lvl = levels.get(p).copied().unwrap_or(FULL);
if lvl != COLLAPSED && lvl != TIGHT {
return None;
}
let child_layout = layout.children().nth(p)?;
let b = child_layout.bounds();
let anchor = Point::new(b.x + translation.x, b.y + b.height + translation.y);
Some(overlay::Element::new(Box::new(FlyoutOverlay {
flyout: &mut self.panels[p].flyout,
tree: &mut tree.children[SLOTS * p + 4],
anchor,
})))
}
}
impl<'a> From<CollapsePanels<'a>> for Element<'a, Message> {
fn from(w: CollapsePanels<'a>) -> Self {
Element::new(w)
}
}
/// Overlay that renders an open panel's flyout anchored below its button and
/// closes it when the user presses outside.
struct FlyoutOverlay<'a, 'b> {
flyout: &'b mut Element<'a, Message>,
tree: &'b mut widget::Tree,
anchor: Point,
}
impl overlay::Overlay<Message, Theme, Renderer> for FlyoutOverlay<'_, '_> {
fn layout(&mut self, renderer: &Renderer, bounds: Size) -> layout::Node {
let viewport = Rectangle::with_size(bounds);
let limits = layout::Limits::new(Size::ZERO, viewport.size());
let node = self
.flyout
.as_widget_mut()
.layout(self.tree, renderer, &limits);
let size = node.size();
let mut x = self.anchor.x;
let mut y = self.anchor.y;
if x + size.width > viewport.width {
x = (viewport.width - size.width).max(0.0);
}
if y + size.height > viewport.height {
y = (self.anchor.y - size.height).max(0.0);
}
layout::Node::with_children(size, vec![node]).translate(Vector::new(x, y))
}
fn draw(
&self,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
) {
let child = layout.children().next().unwrap();
self.flyout.as_widget().draw(
self.tree,
renderer,
theme,
style,
child,
cursor,
&child.bounds(),
);
}
fn update(
&mut self,
event: &Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) {
let child = layout.children().next().unwrap();
let vp = child.bounds();
if let Event::Mouse(mouse::Event::ButtonPressed(_)) = event {
if !cursor.is_over(vp) {
shell.publish(Message::CloseRibbonDropdown);
shell.capture_event();
return;
}
}
self.flyout
.as_widget_mut()
.update(self.tree, event, child, cursor, renderer, clipboard, shell, &vp);
}
fn operate(
&mut self,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn widget::Operation,
) {
let child = layout.children().next().unwrap();
self.flyout
.as_widget_mut()
.operate(self.tree, child, renderer, operation);
}
fn mouse_interaction(
&self,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &Renderer,
) -> mouse::Interaction {
let child = layout.children().next().unwrap();
self.flyout
.as_widget()
.mouse_interaction(self.tree, child, cursor, &child.bounds(), renderer)
}
}