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:
parent
ced42daff5
commit
8e6146fd91
4 changed files with 282 additions and 102 deletions
|
|
@ -1,11 +1,15 @@
|
||||||
//! Adaptive ribbon panels.
|
//! Adaptive ribbon panels.
|
||||||
//!
|
//!
|
||||||
//! Lays the active module's panels on one row. When they don't all fit, panels
|
//! Lays the active module's panels on one row. When they don't all fit, the row
|
||||||
//! degrade **from the right**: first a panel's large buttons shrink to compact
|
//! degrades **from the right**, one panel at a time: first a panel shrinks to
|
||||||
//! icon columns, then — if it still doesn't fit — the panel collapses to a title
|
//! compact icon columns, then it collapses to a title button. If even the
|
||||||
//! button whose click opens the full panel as a flyout overlay.
|
//! 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::cell::RefCell;
|
||||||
|
use std::sync::atomic::{AtomicU32, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use iced::advanced::layout::{self, Layout};
|
use iced::advanced::layout::{self, Layout};
|
||||||
use iced::advanced::widget::{self, Widget};
|
use iced::advanced::widget::{self, Widget};
|
||||||
|
|
@ -17,31 +21,49 @@ use iced::{
|
||||||
|
|
||||||
use crate::app::Message;
|
use crate::app::Message;
|
||||||
|
|
||||||
/// One panel in its four renderings.
|
/// 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 struct Panel<'a> {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub full: Element<'a, Message>,
|
pub full: Element<'a, Message>,
|
||||||
pub compact: Element<'a, Message>,
|
pub compact: Element<'a, Message>,
|
||||||
pub button: Element<'a, Message>,
|
pub button: Element<'a, Message>,
|
||||||
|
pub tight: Element<'a, Message>,
|
||||||
pub flyout: 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
|
// Per-panel degradation level; also the offset of the shown element within the
|
||||||
// panel's 4 trees ([full, compact, button, flyout]).
|
// 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 FULL: u8 = 0;
|
||||||
const COMPACT: u8 = 1;
|
const COMPACT: u8 = 1;
|
||||||
const COLLAPSED: u8 = 2;
|
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> {
|
pub struct CollapsePanels<'a> {
|
||||||
panels: Vec<Panel<'a>>,
|
panels: Vec<Panel<'a>>,
|
||||||
/// Title of the panel whose flyout is open (if any).
|
/// Title of the panel whose flyout is open (if any).
|
||||||
open: Option<String>,
|
open: Option<String>,
|
||||||
/// Row height (a full ribbon tool-area height).
|
/// Fallback row height, used only when there are no panels to measure.
|
||||||
row_h: f32,
|
row_h: f32,
|
||||||
/// Colour of the 1px divider drawn between panels.
|
/// Colour of the 1px divider drawn between panels.
|
||||||
divider: Color,
|
divider: Color,
|
||||||
/// Chosen degradation level per panel; set during layout.
|
/// Chosen degradation level per panel; set during layout.
|
||||||
levels: RefCell<Vec<u8>>,
|
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> {
|
impl<'a> CollapsePanels<'a> {
|
||||||
|
|
@ -53,14 +75,22 @@ impl<'a> CollapsePanels<'a> {
|
||||||
row_h,
|
row_h,
|
||||||
divider,
|
divider,
|
||||||
levels: RefCell::new(vec![FULL; n]),
|
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> {
|
fn shown(&self, i: usize, level: u8) -> &Element<'a, Message> {
|
||||||
match level {
|
match level {
|
||||||
FULL => &self.panels[i].full,
|
FULL => &self.panels[i].full,
|
||||||
COMPACT => &self.panels[i].compact,
|
COMPACT => &self.panels[i].compact,
|
||||||
_ => &self.panels[i].button,
|
COLLAPSED => &self.panels[i].button,
|
||||||
|
_ => &self.panels[i].tight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -68,7 +98,8 @@ impl<'a> CollapsePanels<'a> {
|
||||||
match level {
|
match level {
|
||||||
FULL => &mut self.panels[i].full,
|
FULL => &mut self.panels[i].full,
|
||||||
COMPACT => &mut self.panels[i].compact,
|
COMPACT => &mut self.panels[i].compact,
|
||||||
_ => &mut self.panels[i].button,
|
COLLAPSED => &mut self.panels[i].button,
|
||||||
|
_ => &mut self.panels[i].tight,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,11 +112,12 @@ impl<'a> CollapsePanels<'a> {
|
||||||
|
|
||||||
impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
fn children(&self) -> Vec<widget::Tree> {
|
fn children(&self) -> Vec<widget::Tree> {
|
||||||
let mut v = Vec::with_capacity(self.panels.len() * 4);
|
let mut v = Vec::with_capacity(self.panels.len() * SLOTS);
|
||||||
for p in &self.panels {
|
for p in &self.panels {
|
||||||
v.push(widget::Tree::new(&p.full));
|
v.push(widget::Tree::new(&p.full));
|
||||||
v.push(widget::Tree::new(&p.compact));
|
v.push(widget::Tree::new(&p.compact));
|
||||||
v.push(widget::Tree::new(&p.button));
|
v.push(widget::Tree::new(&p.button));
|
||||||
|
v.push(widget::Tree::new(&p.tight));
|
||||||
v.push(widget::Tree::new(&p.flyout));
|
v.push(widget::Tree::new(&p.flyout));
|
||||||
}
|
}
|
||||||
v
|
v
|
||||||
|
|
@ -97,6 +129,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
refs.push(p.full.as_widget());
|
refs.push(p.full.as_widget());
|
||||||
refs.push(p.compact.as_widget());
|
refs.push(p.compact.as_widget());
|
||||||
refs.push(p.button.as_widget());
|
refs.push(p.button.as_widget());
|
||||||
|
refs.push(p.tight.as_widget());
|
||||||
refs.push(p.flyout.as_widget());
|
refs.push(p.flyout.as_widget());
|
||||||
}
|
}
|
||||||
tree.diff_children(&refs);
|
tree.diff_children(&refs);
|
||||||
|
|
@ -116,58 +149,109 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
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();
|
let n = self.panels.len();
|
||||||
|
|
||||||
// Measure each panel at all three densities.
|
// 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 full_w = vec![0.0f32; n];
|
||||||
let mut compact_w = vec![0.0f32; n];
|
let mut compact_w = vec![0.0f32; n];
|
||||||
let mut button_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 {
|
for i in 0..n {
|
||||||
full_w[i] = self.panels[i].full.as_widget_mut()
|
let s = self.panels[i].full.as_widget_mut()
|
||||||
.layout(&mut tree.children[4 * i], renderer, &natural)
|
.layout(&mut tree.children[SLOTS * i], renderer, &natural)
|
||||||
.size()
|
.size();
|
||||||
.width;
|
full_w[i] = s.width;
|
||||||
compact_w[i] = self.panels[i].compact.as_widget_mut()
|
full_h[i] = s.height;
|
||||||
.layout(&mut tree.children[4 * i + 1], renderer, &natural)
|
let s = self.panels[i].compact.as_widget_mut()
|
||||||
.size()
|
.layout(&mut tree.children[SLOTS * i + 1], renderer, &natural)
|
||||||
.width;
|
.size();
|
||||||
button_w[i] = self.panels[i].button.as_widget_mut()
|
compact_w[i] = s.width;
|
||||||
.layout(&mut tree.children[4 * i + 2], renderer, &natural)
|
compact_h[i] = s.height;
|
||||||
.size()
|
let s = self.panels[i].button.as_widget_mut()
|
||||||
.width;
|
.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 {
|
let width_of = |lv: u8, i: usize| -> f32 {
|
||||||
match lv {
|
match lv {
|
||||||
FULL => full_w[i],
|
FULL => full_w[i],
|
||||||
COMPACT => compact_w[i],
|
COMPACT => compact_w[i],
|
||||||
_ => button_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() };
|
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
|
// Degrade from the RIGHT, one panel at a time (gradual): first FULL →
|
||||||
// to compact one at a time; phase 2 (only if compact still overflows)
|
// COMPACT, then COMPACT → COLLAPSED, each phase only while the row still
|
||||||
// collapses them to buttons.
|
// 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];
|
let mut levels = vec![FULL; n];
|
||||||
for i in (0..n).rev() {
|
for degraded in [COMPACT, COLLAPSED] {
|
||||||
if total(&levels) <= max_w {
|
for i in (0..n).rev() {
|
||||||
break;
|
if total(&levels) <= max_w {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
levels[i] = degraded;
|
||||||
}
|
}
|
||||||
levels[i] = COMPACT;
|
|
||||||
}
|
}
|
||||||
for i in (0..n).rev() {
|
if total(&levels) > max_w {
|
||||||
if total(&levels) <= max_w {
|
levels
|
||||||
break;
|
.iter_mut()
|
||||||
}
|
.filter(|l| **l == COLLAPSED)
|
||||||
levels[i] = COLLAPSED;
|
.for_each(|l| *l = TIGHT);
|
||||||
}
|
}
|
||||||
*self.levels.borrow_mut() = levels.clone();
|
*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.
|
// Place the chosen element for each panel left-to-right.
|
||||||
let mut children: Vec<layout::Node> = Vec::with_capacity(n);
|
let mut children: Vec<layout::Node> = Vec::with_capacity(n);
|
||||||
let mut x = 0.0f32;
|
let mut x = 0.0f32;
|
||||||
for i in 0..n {
|
for i in 0..n {
|
||||||
|
if i > 0 {
|
||||||
|
x -= squeeze;
|
||||||
|
}
|
||||||
let level = levels[i];
|
let level = levels[i];
|
||||||
let tree_idx = 4 * i + level as usize;
|
let tree_idx = SLOTS * i + level as usize;
|
||||||
let node = self.shown_mut(i, level).as_widget_mut().layout(
|
let node = self.shown_mut(i, level).as_widget_mut().layout(
|
||||||
&mut tree.children[tree_idx],
|
&mut tree.children[tree_idx],
|
||||||
renderer,
|
renderer,
|
||||||
|
|
@ -175,12 +259,16 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
);
|
);
|
||||||
let h = node.size().height;
|
let h = node.size().height;
|
||||||
let w = node.size().width;
|
let w = node.size().width;
|
||||||
let y = ((self.row_h - h) / 2.0).max(0.0);
|
let y = ((row_h - h) / 2.0).max(0.0);
|
||||||
children.push(node.move_to(Point::new(x, y)));
|
children.push(node.move_to(Point::new(x, y)));
|
||||||
x += w;
|
x += w;
|
||||||
}
|
}
|
||||||
|
|
||||||
layout::Node::with_children(Size::new(x, self.row_h), children)
|
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(
|
fn update(
|
||||||
|
|
@ -197,7 +285,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
let levels = self.levels_snapshot(self.panels.len());
|
let levels = self.levels_snapshot(self.panels.len());
|
||||||
for (i, child_layout) in layout.children().enumerate() {
|
for (i, child_layout) in layout.children().enumerate() {
|
||||||
let level = levels[i];
|
let level = levels[i];
|
||||||
let tree_idx = 4 * i + level as usize;
|
let tree_idx = SLOTS * i + level as usize;
|
||||||
self.shown_mut(i, level).as_widget_mut().update(
|
self.shown_mut(i, level).as_widget_mut().update(
|
||||||
&mut tree.children[tree_idx],
|
&mut tree.children[tree_idx],
|
||||||
event,
|
event,
|
||||||
|
|
@ -223,7 +311,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
let mut interaction = mouse::Interaction::default();
|
let mut interaction = mouse::Interaction::default();
|
||||||
for (i, child_layout) in layout.children().enumerate() {
|
for (i, child_layout) in layout.children().enumerate() {
|
||||||
let level = levels[i];
|
let level = levels[i];
|
||||||
let tree_idx = 4 * i + level as usize;
|
let tree_idx = SLOTS * i + level as usize;
|
||||||
let it = self.shown(i, level).as_widget().mouse_interaction(
|
let it = self.shown(i, level).as_widget().mouse_interaction(
|
||||||
&tree.children[tree_idx],
|
&tree.children[tree_idx],
|
||||||
child_layout,
|
child_layout,
|
||||||
|
|
@ -248,7 +336,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
let levels = self.levels_snapshot(self.panels.len());
|
let levels = self.levels_snapshot(self.panels.len());
|
||||||
for (i, child_layout) in layout.children().enumerate() {
|
for (i, child_layout) in layout.children().enumerate() {
|
||||||
let level = levels[i];
|
let level = levels[i];
|
||||||
let tree_idx = 4 * i + level as usize;
|
let tree_idx = SLOTS * i + level as usize;
|
||||||
self.shown_mut(i, level).as_widget_mut().operate(
|
self.shown_mut(i, level).as_widget_mut().operate(
|
||||||
&mut tree.children[tree_idx],
|
&mut tree.children[tree_idx],
|
||||||
child_layout,
|
child_layout,
|
||||||
|
|
@ -271,7 +359,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
let levels = self.levels_snapshot(self.panels.len());
|
let levels = self.levels_snapshot(self.panels.len());
|
||||||
for (i, child_layout) in layout.children().enumerate() {
|
for (i, child_layout) in layout.children().enumerate() {
|
||||||
let level = levels[i];
|
let level = levels[i];
|
||||||
let tree_idx = 4 * i + level as usize;
|
let tree_idx = SLOTS * i + level as usize;
|
||||||
self.shown(i, level).as_widget().draw(
|
self.shown(i, level).as_widget().draw(
|
||||||
&tree.children[tree_idx],
|
&tree.children[tree_idx],
|
||||||
renderer,
|
renderer,
|
||||||
|
|
@ -283,12 +371,13 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1px divider between adjacent panels, except between two collapsed
|
// 1px divider between adjacent panels, except between two button-form
|
||||||
// panels (whose buttons read better with no line between them).
|
// 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 bounds: Vec<Rectangle> = layout.children().map(|l| l.bounds()).collect();
|
||||||
let wb = layout.bounds();
|
let wb = layout.bounds();
|
||||||
for i in 0..self.panels.len().saturating_sub(1) {
|
for i in 0..self.panels.len().saturating_sub(1) {
|
||||||
if levels[i] == COLLAPSED && levels[i + 1] == COLLAPSED {
|
if is_btn(levels[i]) && is_btn(levels[i + 1]) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let x = bounds[i + 1].x;
|
let x = bounds[i + 1].x;
|
||||||
|
|
@ -320,8 +409,9 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
let levels = self.levels_snapshot(self.panels.len());
|
let levels = self.levels_snapshot(self.panels.len());
|
||||||
let open_id = self.open.clone()?;
|
let open_id = self.open.clone()?;
|
||||||
let p = self.panels.iter().position(|pan| pan.id == open_id)?;
|
let p = self.panels.iter().position(|pan| pan.id == open_id)?;
|
||||||
// Only a collapsed panel shows a flyout.
|
// Only a button-form panel (collapsed or tight) shows a flyout.
|
||||||
if levels.get(p).copied().unwrap_or(FULL) != COLLAPSED {
|
let lvl = levels.get(p).copied().unwrap_or(FULL);
|
||||||
|
if lvl != COLLAPSED && lvl != TIGHT {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,7 +421,7 @@ impl<'a> Widget<Message, Theme, Renderer> for CollapsePanels<'a> {
|
||||||
|
|
||||||
Some(overlay::Element::new(Box::new(FlyoutOverlay {
|
Some(overlay::Element::new(Box::new(FlyoutOverlay {
|
||||||
flyout: &mut self.panels[p].flyout,
|
flyout: &mut self.panels[p].flyout,
|
||||||
tree: &mut tree.children[4 * p + 3],
|
tree: &mut tree.children[SLOTS * p + 4],
|
||||||
anchor,
|
anchor,
|
||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,9 @@ pub struct Ribbon {
|
||||||
/// Measured tab-bar height (28 on one row, 56 when tabs wrap). Written by
|
/// Measured tab-bar height (28 on one row, 56 when tabs wrap). Written by
|
||||||
/// the `WrapBar` widget during layout, read when anchoring dropdowns.
|
/// the `WrapBar` widget during layout, read when anchoring dropdowns.
|
||||||
tab_bar_h: Arc<AtomicU32>,
|
tab_bar_h: Arc<AtomicU32>,
|
||||||
/// Measured tool-area height (TOOL_BAR_H on one row, taller when the panels
|
/// Measured tool-area height (TOOL_BAR_H at full density, shorter once every
|
||||||
/// wrap). Written by the `DensitySwap` widget, read when anchoring dropdowns.
|
/// panel is collapsed). Written by `CollapsePanels`, read when anchoring
|
||||||
|
/// dropdowns below the ribbon.
|
||||||
tool_bar_h: Arc<AtomicU32>,
|
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(
|
let tab_bar = container(
|
||||||
WrapBar::new(lead.into(), tabs.into())
|
WrapBar::new(lead.into(), tabs.into())
|
||||||
|
|
@ -520,6 +525,23 @@ impl Ribbon {
|
||||||
&self.active_linetype,
|
&self.active_linetype,
|
||||||
self.active_lineweight,
|
self.active_lineweight,
|
||||||
&style_ctx,
|
&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(
|
flyout: container(render_group(
|
||||||
false,
|
false,
|
||||||
|
|
@ -549,6 +571,7 @@ impl Ribbon {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H, BORDER_DARK)
|
CollapsePanels::new(panels, self.collapsed_open.clone(), TOOL_BAR_H, BORDER_DARK)
|
||||||
|
.report_height(self.tool_bar_h.clone())
|
||||||
.into()
|
.into()
|
||||||
} else {
|
} else {
|
||||||
text("").into()
|
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
|
/// 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
|
/// last-used tool) plus a title + ▾ opener for the full flyout. Its height is its
|
||||||
/// `TOOL_BAR_H` tall so it lines up with inline panels.
|
/// 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn collapse_button<'a>(
|
fn collapse_button<'a>(
|
||||||
group: &RibbonGroup,
|
group: &RibbonGroup,
|
||||||
|
|
@ -1281,43 +1305,65 @@ fn collapse_button<'a>(
|
||||||
active_linetype: &'a str,
|
active_linetype: &'a str,
|
||||||
active_lineweight: LineWeight,
|
active_lineweight: LineWeight,
|
||||||
style_ctx: &StyleContext,
|
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> {
|
) -> Element<'a, Message> {
|
||||||
let title = group.title;
|
let title = group.title;
|
||||||
|
|
||||||
// Show the representative tool with a large icon. For a Properties panel the
|
// The representative tool's face. Normally a large icon; in the tight step it
|
||||||
// representative is its Match button, rendered as a large tool.
|
// shrinks to the small icon (SMALL_W vs LARGE_W) to reclaim width. For a
|
||||||
let face: Element<'_, Message> = match representative(group, last_used) {
|
// Properties panel the representative is its Match button.
|
||||||
Some(RibbonItem::PropertiesGroup { match_prop }) => render_large(
|
let rep = representative(group, last_used);
|
||||||
&RibbonItem::LargeTool(match_prop.clone()),
|
let face: Element<'_, Message> = if compact {
|
||||||
active_tool,
|
match rep {
|
||||||
open_dd,
|
Some(RibbonItem::PropertiesGroup { match_prop }) => render_small(
|
||||||
last_cmd,
|
&RibbonItem::LargeTool(match_prop.clone()),
|
||||||
wireframe,
|
active_tool,
|
||||||
ortho_mode,
|
open_dd,
|
||||||
layer_infos,
|
last_cmd,
|
||||||
active_layer,
|
wireframe,
|
||||||
active_color,
|
ortho_mode,
|
||||||
active_linetype,
|
),
|
||||||
active_lineweight,
|
Some(item) => {
|
||||||
style_ctx,
|
render_small(item, active_tool, open_dd, last_cmd, wireframe, ortho_mode)
|
||||||
false,
|
}
|
||||||
),
|
None => text("").into(),
|
||||||
Some(item) => render_large(
|
}
|
||||||
item,
|
} else {
|
||||||
active_tool,
|
match rep {
|
||||||
open_dd,
|
Some(RibbonItem::PropertiesGroup { match_prop }) => render_large(
|
||||||
last_cmd,
|
&RibbonItem::LargeTool(match_prop.clone()),
|
||||||
wireframe,
|
active_tool,
|
||||||
ortho_mode,
|
open_dd,
|
||||||
layer_infos,
|
last_cmd,
|
||||||
active_layer,
|
wireframe,
|
||||||
active_color,
|
ortho_mode,
|
||||||
active_linetype,
|
layer_infos,
|
||||||
active_lineweight,
|
active_layer,
|
||||||
style_ctx,
|
active_color,
|
||||||
false,
|
active_linetype,
|
||||||
),
|
active_lineweight,
|
||||||
None => text("").into(),
|
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(
|
let opener = button(
|
||||||
|
|
@ -1347,15 +1393,22 @@ fn collapse_button<'a>(
|
||||||
})
|
})
|
||||||
.padding([1, 4]);
|
.padding([1, 4]);
|
||||||
|
|
||||||
column![
|
// The large face fills a fixed slot; the small (compact) face keeps its own
|
||||||
container(face).height(Fill).align_y(iced::Center),
|
// natural height. Either way the whole button is natural height, so a
|
||||||
opener,
|
// collapsed / tight panel is shorter than a full one and the row shrinks.
|
||||||
]
|
let face_box = if compact {
|
||||||
.align_x(iced::Center)
|
container(face).align_y(iced::Center)
|
||||||
.spacing(2)
|
} else {
|
||||||
.padding([3u16, 4])
|
container(face)
|
||||||
.height(Length::Fixed(TOOL_BAR_H))
|
.height(Length::Fixed(COLLAPSED_FACE_H))
|
||||||
.into()
|
.align_y(iced::Center)
|
||||||
|
};
|
||||||
|
|
||||||
|
column![face_box, opener]
|
||||||
|
.align_x(iced::Center)
|
||||||
|
.spacing(2)
|
||||||
|
.padding([3u16, 4])
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Ribbon {
|
impl Default for Ribbon {
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,10 @@ pub(super) const ARROW_W: f32 = ROW_H * 0.4;
|
||||||
pub(super) const LARGE_ARR: f32 = ROW_H * 0.55;
|
pub(super) const LARGE_ARR: f32 = ROW_H * 0.55;
|
||||||
/// Total ribbon tool-area height = 3 × ROW_H + 6 px v-padding + 12 px group-label.
|
/// Total ribbon tool-area height = 3 × ROW_H + 6 px v-padding + 12 px group-label.
|
||||||
pub(super) const TOOL_BAR_H: f32 = 3.0 * ROW_H + 18.0;
|
pub(super) const TOOL_BAR_H: f32 = 3.0 * ROW_H + 18.0;
|
||||||
|
/// Height of a collapsed panel button's large representative face (big icon +
|
||||||
|
/// its label). A collapsed button is this face plus the title opener, so it is
|
||||||
|
/// shorter than a full 3-row panel — the ribbon height follows it down.
|
||||||
|
pub(super) const COLLAPSED_FACE_H: f32 = LARGE_ICON + 20.0;
|
||||||
|
|
||||||
// ── Tab-bar constants ──────────────────────────────────────────────────────
|
// ── Tab-bar constants ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -431,6 +431,9 @@ impl<'a> From<WrapBar<'a>> for Element<'a, Message> {
|
||||||
pub struct WrapFlow<'a> {
|
pub struct WrapFlow<'a> {
|
||||||
items: Vec<Element<'a, Message>>,
|
items: Vec<Element<'a, Message>>,
|
||||||
spacing_x: f32,
|
spacing_x: f32,
|
||||||
|
/// Smallest inter-item gap when the row overflows. `INFINITY` (the default)
|
||||||
|
/// means "never compress" — strictly opt-in per flow.
|
||||||
|
min_spacing_x: f32,
|
||||||
spacing_y: f32,
|
spacing_y: f32,
|
||||||
row_h: f32,
|
row_h: f32,
|
||||||
}
|
}
|
||||||
|
|
@ -440,6 +443,7 @@ impl<'a> WrapFlow<'a> {
|
||||||
Self {
|
Self {
|
||||||
items,
|
items,
|
||||||
spacing_x: 2.0,
|
spacing_x: 2.0,
|
||||||
|
min_spacing_x: f32::INFINITY,
|
||||||
spacing_y: 0.0,
|
spacing_y: 0.0,
|
||||||
row_h: 28.0,
|
row_h: 28.0,
|
||||||
}
|
}
|
||||||
|
|
@ -450,6 +454,13 @@ impl<'a> WrapFlow<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Allow the inter-item gap to shrink to `s` (from `spacing_x`) when the
|
||||||
|
/// items don't fit, so they get a bit closer before wrapping to a new row.
|
||||||
|
pub fn min_spacing_x(mut self, s: f32) -> Self {
|
||||||
|
self.min_spacing_x = s;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn row_h(mut self, h: f32) -> Self {
|
pub fn row_h(mut self, h: f32) -> Self {
|
||||||
self.row_h = h;
|
self.row_h = h;
|
||||||
self
|
self
|
||||||
|
|
@ -480,13 +491,35 @@ impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> {
|
||||||
let natural =
|
let natural =
|
||||||
layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY));
|
layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY));
|
||||||
|
|
||||||
let mut nodes: Vec<layout::Node> = Vec::with_capacity(self.items.len());
|
// Measure each item exactly once and total their widths.
|
||||||
|
let mut measured: Vec<layout::Node> = Vec::with_capacity(self.items.len());
|
||||||
|
let mut sum_w = 0.0f32;
|
||||||
|
for (item, state) in self.items.iter_mut().zip(tree.children.iter_mut()) {
|
||||||
|
let node = item.as_widget_mut().layout(state, renderer, &natural);
|
||||||
|
sum_w += node.size().width;
|
||||||
|
measured.push(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this flow opted into compression and the items don't fit one row at
|
||||||
|
// the normal gap, shrink the gap (down to min_spacing_x) so they get a
|
||||||
|
// bit closer before wrapping. Only fires when max_w is a real bound
|
||||||
|
// (i.e. the flow is already width-constrained, e.g. overflowing tabs).
|
||||||
|
let n = measured.len();
|
||||||
|
let mut eff_gap = self.spacing_x;
|
||||||
|
if self.min_spacing_x < self.spacing_x && max_w.is_finite() && n > 1 {
|
||||||
|
let line_w = sum_w + (n - 1) as f32 * self.spacing_x;
|
||||||
|
if line_w > max_w {
|
||||||
|
eff_gap =
|
||||||
|
((max_w - sum_w) / (n - 1) as f32).clamp(self.min_spacing_x, self.spacing_x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut nodes: Vec<layout::Node> = Vec::with_capacity(n);
|
||||||
let mut x = 0.0f32;
|
let mut x = 0.0f32;
|
||||||
let mut y = 0.0f32;
|
let mut y = 0.0f32;
|
||||||
let mut used_w = 0.0f32;
|
let mut used_w = 0.0f32;
|
||||||
|
|
||||||
for (item, state) in self.items.iter_mut().zip(tree.children.iter_mut()) {
|
for node in measured {
|
||||||
let node = item.as_widget_mut().layout(state, renderer, &natural);
|
|
||||||
let sz = node.size();
|
let sz = node.size();
|
||||||
if x > 0.0 && x + sz.width > max_w {
|
if x > 0.0 && x + sz.width > max_w {
|
||||||
x = 0.0;
|
x = 0.0;
|
||||||
|
|
@ -494,8 +527,8 @@ impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> {
|
||||||
}
|
}
|
||||||
let cy = y + ((self.row_h - sz.height) / 2.0).max(0.0);
|
let cy = y + ((self.row_h - sz.height) / 2.0).max(0.0);
|
||||||
nodes.push(node.move_to(Point::new(x, cy)));
|
nodes.push(node.move_to(Point::new(x, cy)));
|
||||||
x += sz.width + self.spacing_x;
|
x += sz.width + eff_gap;
|
||||||
used_w = used_w.max(x - self.spacing_x);
|
used_w = used_w.max(x - eff_gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
let total_h = if self.items.is_empty() {
|
let total_h = if self.items.is_empty() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue