//! 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::{AtomicBool, AtomicU32, Ordering}; use std::sync::Arc; use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Widget}; use iced::advanced::{mouse, overlay, renderer, Renderer as _, Shell}; use iced::{ Background, Border, Element, Event, Length, Point, Rectangle, Renderer, Shadow, Size, Theme, Vector, }; use crate::app::Message; /// One panel in its four density 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. An open flyout draws the panel's /// own `full` rendering; see `FlyoutOverlay`. `elements` is indexed by `Level`. pub(crate) struct Panel<'a> { pub(crate) id: String, pub(crate) elements: [Element<'a, Message>; 4], } // Number of tree slots per panel: [full, compact, button, tight]. const SLOTS: usize = 4; /// A panel's degradation level; also the offset of its shown element within the /// panel's tree slots. Degrade order (from the right): `Full` → `Compact` → /// `Collapsed` (big-icon button) → `Tight` (small-icon button). #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum Level { Full = 0, Compact = 1, Collapsed = 2, Tight = 3, } /// Tree-slot index of panel `i`'s rendering at `level` /// (`SLOTS` slots per panel: `[full, compact, button, tight]`). fn slot(i: usize, level: Level) -> usize { i * SLOTS + level as usize } /// 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; /// How the ribbon tool panels are sized. `Auto` adapts to the window width (the /// step-by-step degradation); the others pin every panel to one density so the /// user can override the automatic choice. The selection is persisted. #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize)] pub enum CollapseMode { /// Size panels to the window: degrade from the right as space runs out. #[default] Auto, /// Always full-size panels (large buttons), even if they overflow. Full, /// Always compact panels (small icon columns). Compact, /// Always collapsed to title buttons. Collapsed, } impl CollapseMode { /// Every mode, in dropdown order. pub const ALL: &'static [CollapseMode] = &[ CollapseMode::Auto, CollapseMode::Full, CollapseMode::Compact, CollapseMode::Collapsed, ]; /// Label shown in the dropdown. pub fn label(self) -> &'static str { match self { CollapseMode::Auto => "Auto", CollapseMode::Full => "Full", CollapseMode::Compact => "Compact", CollapseMode::Collapsed => "Collapsed", } } /// The degradation level every panel is pinned to, or `None` for `Auto`. fn forced_level(self) -> Option { match self { CollapseMode::Auto => None, CollapseMode::Full => Some(Level::Full), CollapseMode::Compact => Some(Level::Compact), CollapseMode::Collapsed => Some(Level::Collapsed), } } } impl std::fmt::Display for CollapseMode { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.label()) } } /// Natural widths of one panel at each degradation level. Only the widths drive /// the degradation decision; the decision itself is pure and tested. #[derive(Clone, Copy, Debug, Default)] pub(crate) struct Widths { /// Width of the full-size panel (`Level::Full`). pub full: f32, /// Width of the compact icon-column panel (`Level::Compact`). pub compact: f32, /// Width of the collapsed title button (`Level::Collapsed`). pub button: f32, /// Width of the tightest small-icon button (`Level::Tight`). pub tight: f32, } /// Choose the degradation level per panel. `Auto` degrades from the right, one /// panel at a time: first Full -> Compact, then Compact -> Collapsed, each phase /// only while the row still overflows; if even the all-collapsed row overflows, /// every collapsed panel drops to Tight at once. Forced modes pin everything. pub(crate) fn decide_levels(mode: CollapseMode, widths: &[Widths], max_w: f32) -> Vec { if let Some(level) = mode.forced_level() { return vec![level; widths.len()]; } let width_of = |lv: Level, i: usize| match lv { Level::Full => widths[i].full, Level::Compact => widths[i].compact, Level::Collapsed => widths[i].button, Level::Tight => widths[i].tight, }; let total = |levels: &[Level]| -> f32 { (0..levels.len()).map(|i| width_of(levels[i], i)).sum() }; let mut levels = vec![Level::Full; widths.len()]; for degraded in [Level::Compact, Level::Collapsed] { for i in (0..widths.len()).rev() { if total(&levels) <= max_w { break; } levels[i] = degraded; } } if total(&levels) > max_w { levels .iter_mut() .filter(|l| **l == Level::Collapsed) .for_each(|l| *l = Level::Tight); } levels } pub(crate) struct CollapsePanels<'a> { panels: Vec>, /// Title of the panel whose flyout is open (if any). open: Option, /// Fallback row height, used only when there are no panels to measure. row_h: f32, /// Chosen degradation level per panel; set during layout. levels: RefCell>, /// If set, the measured row height is written here each layout (read when /// anchoring dropdowns below the ribbon). height_out: Option>, /// If set, `true` is written here whenever the row is in the tight state /// (some panel dropped to its small icon), so the tab bar can react. tight_out: Option>, /// User-chosen density. `Auto` runs the width-based degradation; the others /// pin every panel to one level regardless of the window width. mode: CollapseMode, } impl<'a> CollapsePanels<'a> { pub fn new(panels: Vec>, open: Option, row_h: f32) -> Self { let n = panels.len(); Self { panels, open, row_h, levels: RefCell::new(vec![Level::Full; n]), height_out: None, tight_out: None, mode: CollapseMode::Auto, } } /// Report the measured row height into `out` on every layout. pub fn report_height(mut self, out: Arc) -> Self { self.height_out = Some(out); self } /// Report whether the row is in the tight state into `out` on every layout. pub fn report_tight(mut self, out: Arc) -> Self { self.tight_out = Some(out); self } /// Pin the panels to a density (or `Auto` to size by window width). pub fn mode(mut self, mode: CollapseMode) -> Self { self.mode = mode; self } /// The shown element of panel `i` at `level` — an index into its `elements` /// array, which the tree slots mirror (`slot(i, level)`). fn shown(&self, i: usize, level: Level) -> &Element<'a, Message> { &self.panels[i].elements[level as usize] } fn shown_mut(&mut self, i: usize, level: Level) -> &mut Element<'a, Message> { &mut self.panels[i].elements[level as usize] } fn levels_snapshot(&self, n: usize) -> Vec { let mut v = self.levels.borrow().clone(); v.resize(n, Level::Full); v } } impl<'a> Widget for CollapsePanels<'a> { fn diff(&mut self, tree: &mut widget::Tree) { let mut refs = Vec::with_capacity(self.panels.len() * SLOTS); for p in &mut self.panels { refs.extend(p.elements.iter_mut()); } tree.diff_children(&mut refs); } fn size(&self) -> Size { 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(); // Auto needs each panel's natural width at all four densities to pick the // degradation; heights drive the row height. A forced mode pins every // panel to one level, so it measures nothing at all. let mut widths = Vec::with_capacity(n); let mut heights = Vec::with_capacity(n); let auto = self.mode == CollapseMode::Auto; if auto { for i in 0..n { let f = self.panels[i].elements[Level::Full as usize] .as_widget_mut() .layout(&mut tree.children[slot(i, Level::Full)], renderer, &natural) .size(); let c = self.panels[i].elements[Level::Compact as usize] .as_widget_mut() .layout( &mut tree.children[slot(i, Level::Compact)], renderer, &natural, ) .size(); let b = self.panels[i].elements[Level::Collapsed as usize] .as_widget_mut() .layout( &mut tree.children[slot(i, Level::Collapsed)], renderer, &natural, ) .size(); let t = self.panels[i].elements[Level::Tight as usize] .as_widget_mut() .layout( &mut tree.children[slot(i, Level::Tight)], renderer, &natural, ) .size(); widths.push(Widths { full: f.width, compact: c.width, button: b.width, tight: t.width, }); heights.push([f.height, c.height, b.height, t.height]); } } let levels = if let Some(level) = self.mode.forced_level() { vec![level; n] } else { decide_levels(self.mode, &widths, max_w) }; // The row is "tight" once any panel has dropped to its small icon — the // last, most cramped state. The tab bar hides its mode selector then. if let Some(out) = &self.tight_out { out.store(levels.iter().any(|&l| l == Level::Tight), Ordering::Relaxed); } *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. In // Auto this only fires when everything is already tight, so it never // overlaps a full/compact panel; a forced density is left to overflow. let squeeze = if auto { let width_of = |lv: Level, i: usize| match lv { Level::Full => widths[i].full, Level::Compact => widths[i].compact, Level::Collapsed => widths[i].button, Level::Tight => widths[i].tight, }; let total: f32 = (0..n).map(|i| width_of(levels[i], i)).sum(); if n > 1 && total > max_w { ((total - max_w) / (n - 1) as f32).min(MAX_PANEL_SQUEEZE) } else { 0.0 } } else { 0.0 }; // Place the chosen element for each panel left-to-right. let mut placed: Vec<(layout::Node, f32, f32)> = 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 = slot(i, level); 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; placed.push((node, x, h)); x += w; } // The row is as tall as the tallest shown panel, so the ribbon height // shrinks as its panels degrade to shorter collapsed / tight buttons. // Auto centers against the heights it measured; a forced mode derives the // height from the placed panels (nothing was measured). let row_h = if auto { if n == 0 { self.row_h } else { (0..n) .map(|i| heights[i][levels[i] as usize]) .fold(0.0f32, f32::max) } } else { placed.iter().map(|(_, _, h)| *h).fold(self.row_h, f32::max) }; let children: Vec = placed .into_iter() .map(|(node, node_x, h)| { let y = ((row_h - h) / 2.0).max(0.0); node.move_to(Point::new(node_x, y)) }) .collect(); 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, 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 = slot(i, level); self.shown_mut(i, level).as_widget_mut().update( &mut tree.children[tree_idx], event, child_layout, cursor, renderer, 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 = slot(i, level); 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 = slot(i, level); 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 = slot(i, level); 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: Level| lv == Level::Collapsed || lv == Level::Tight; let bounds: Vec = 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(theme.palette().background.neutral.color), ); } } fn overlay<'b>( &'b mut self, tree: &'b mut widget::Tree, layout: Layout<'b>, _renderer: &Renderer, _viewport: &Rectangle, translation: Vector, ) -> Option> { let levels = self.levels_snapshot(self.panels.len()); // An open flyout owns the overlay slot. if let Some(open_id) = self.open.clone() { if let Some(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(Level::Full); if (lvl == Level::Collapsed || lvl == Level::Tight) && layout.children().nth(p).is_some() { let child_layout = layout.children().nth(p).unwrap(); let b = child_layout.bounds(); let anchor = Point::new(b.x + translation.x, b.y + b.height + translation.y); // The flyout is the panel's own `full` rendering (the row // never shows slot 0 while the panel is button-form, so it // is free to double as the overlay content). return Some(overlay::Element::new(Box::new(FlyoutOverlay { content: &mut self.panels[p].elements[Level::Full as usize], tree: &mut tree.children[slot(p, Level::Full)], anchor, }))); } } } // No flyout: forward the SHOWN children's overlays. Every ribbon // tooltip is an iced overlay produced inside these children, so // returning None here left all of them permanently dead (#411 — a // regression from the day this widget replaced the plain row). // Split borrows so each shown child and its tree slot are disjoint. let mut overlays = Vec::new(); let mut tree_rest = tree.children.as_mut_slice(); for ((i, panel), child_layout) in self.panels.iter_mut().enumerate().zip(layout.children()) { if tree_rest.len() < SLOTS { break; } let (chunk, rest) = tree_rest.split_at_mut(SLOTS); tree_rest = rest; let level = levels.get(i).copied().unwrap_or(Level::Full); let child = &mut panel.elements[level as usize]; if let Some(o) = child.as_widget_mut().overlay( &mut chunk[level as usize], child_layout, _renderer, _viewport, translation, ) { overlays.push(o); } } if overlays.is_empty() { None } else { Some(overlay::Group::with_children(overlays).overlay()) } } } impl<'a> From> 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. Draws the panel's `full` rendering /// (`content`) inside a thin styled box. struct FlyoutOverlay<'a, 'b> { content: &'b mut Element<'a, Message>, tree: &'b mut widget::Tree, anchor: Point, } impl overlay::Overlay 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 .content .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 b = layout.bounds(); renderer.fill_quad( renderer::Quad { bounds: b, border: Border { color: theme.palette().background.neutral.color, width: 1.0, radius: 0.0.into(), }, shadow: Shadow::default(), snap: true, }, Background::Color(theme.palette().background.weakest.color), ); let child = layout.children().next().unwrap(); self.content.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, 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.content .as_widget_mut() .update(self.tree, event, child, cursor, renderer, shell, &vp); } fn operate( &mut self, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn widget::Operation, ) { let child = layout.children().next().unwrap(); self.content .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.content.as_widget().mouse_interaction( self.tree, child, cursor, &child.bounds(), renderer, ) } } #[cfg(test)] mod tests { use super::*; fn w(full: f32, compact: f32, button: f32, tight: f32) -> Widths { Widths { full, compact, button, tight, } } /// Forced modes pin every panel to the same level, regardless of width. #[test] fn forced_modes_pin_every_panel() { let widths = [w(200.0, 150.0, 100.0, 50.0), w(180.0, 130.0, 90.0, 40.0)]; assert_eq!( decide_levels(CollapseMode::Full, &widths, 10.0), vec![Level::Full, Level::Full] ); assert_eq!( decide_levels(CollapseMode::Compact, &widths, 10.0), vec![Level::Compact, Level::Compact] ); assert_eq!( decide_levels(CollapseMode::Collapsed, &widths, 10.0), vec![Level::Collapsed, Level::Collapsed] ); } /// A huge Auto width keeps every panel full. #[test] fn auto_huge_width_keeps_everything_full() { let widths = [w(200.0, 150.0, 100.0, 50.0), w(180.0, 130.0, 90.0, 40.0)]; assert_eq!( decide_levels(CollapseMode::Auto, &widths, 1e9), vec![Level::Full, Level::Full] ); } /// Auto degrades from the right: the overflowing rightmost panel shrinks to /// compact first, and stops as soon as the row fits. #[test] fn auto_degrades_from_the_right_one_panel_at_a_time() { let widths = [w(50.0, 40.0, 30.0, 20.0), w(200.0, 100.0, 60.0, 30.0)]; // Full row = 250 > 240; rightmost compact => 50 + 100 = 150 <= 240. assert_eq!( decide_levels(CollapseMode::Auto, &widths, 240.0), vec![Level::Full, Level::Compact] ); } /// The row only keeps degrading left while it still overflows. #[test] fn auto_stops_degrading_once_the_row_fits() { let widths = [w(50.0, 25.0, 20.0, 15.0), w(50.0, 25.0, 20.0, 15.0)]; // Full row 100 > 75; rightmost compact => 75 <= 75; left panel stays FULL. assert_eq!( decide_levels(CollapseMode::Auto, &widths, 75.0), vec![Level::Full, Level::Compact] ); } /// A single panel past its collapse limit and still overflowing drops right /// through to a tight small-icon button. #[test] fn auto_escalates_a_single_panel_past_collapsed_to_tight() { let widths = [w(50.0, 40.0, 30.0, 20.0)]; // full 50 > 25 -> compact 40 > 25 -> collapsed 30 > 25 -> all-collapsed // (still) overflows -> TIGHT. assert_eq!( decide_levels(CollapseMode::Auto, &widths, 25.0), vec![Level::Tight] ); } /// When even the all-collapsed row overflows, every collapsed panel drops to /// tight at once. #[test] fn auto_cascade_drops_every_collapsed_panel_to_tight() { let widths = [w(100.0, 90.0, 50.0, 30.0), w(100.0, 90.0, 50.0, 30.0)]; assert_eq!( decide_levels(CollapseMode::Auto, &widths, 20.0), vec![Level::Tight, Level::Tight] ); } /// The first (compact) phase completes across the whole row before the /// second (collapsed) phase starts: the left panel ends up compact even /// though at FULL it would have fit next to the eventually-collapsed right /// panel (30 + 40 = 70 <= 100). #[test] fn auto_runs_the_compact_phase_across_the_row_first() { let widths = [w(30.0, 26.0, 15.0, 12.0), w(100.0, 80.0, 40.0, 20.0)]; // phase compact: right (30+100=130>100 -> compact), left (26+80=110>100 -> compact) // phase collapsed: right (26+40=66<=100 after right->collapsed is 26+80=106>100), left (66<=100 -> stop) assert_eq!( decide_levels(CollapseMode::Auto, &widths, 100.0), vec![Level::Compact, Level::Collapsed] ); } /// No panels means no levels, in Auto and every forced mode. #[test] fn no_panels_stays_empty() { assert_eq!(decide_levels(CollapseMode::Auto, &[], 1e9), vec![]); assert_eq!(decide_levels(CollapseMode::Full, &[], 1e9), vec![]); assert_eq!(decide_levels(CollapseMode::Collapsed, &[], 1e9), vec![]); } /// A single panel walked the whole ladder as the width shrinks: FULL, /// COMPACT, COLLAPSED, then TIGHT once it still overflows. #[test] fn single_panel_walks_the_whole_ladder() { let widths = [w(100.0, 60.0, 40.0, 20.0)]; assert_eq!( decide_levels(CollapseMode::Auto, &widths, 1e9), vec![Level::Full] ); assert_eq!( decide_levels(CollapseMode::Auto, &widths, 100.0), vec![Level::Full] ); assert_eq!( decide_levels(CollapseMode::Auto, &widths, 75.0), vec![Level::Compact] ); assert_eq!( decide_levels(CollapseMode::Auto, &widths, 45.0), vec![Level::Collapsed] ); assert_eq!( decide_levels(CollapseMode::Auto, &widths, 20.0), vec![Level::Tight] ); } /// Zero natural widths never overflow, even against a zero-width row: the /// first total (0) satisfies `<= max_w` immediately. #[test] fn zero_widths_never_overflow() { let widths = [w(0.0, 0.0, 0.0, 0.0), w(0.0, 0.0, 0.0, 0.0)]; assert_eq!( decide_levels(CollapseMode::Auto, &widths, 0.0), vec![Level::Full, Level::Full] ); } /// An endless row (max_w = 0) degrades everything to tight, same as positive /// widths against an impossible budget. #[test] fn endless_row_degrades_everything_to_tight() { let widths = [w(50.0, 30.0, 20.0, 10.0), w(50.0, 30.0, 20.0, 10.0)]; assert_eq!( decide_levels(CollapseMode::Auto, &widths, 0.0), vec![Level::Tight, Level::Tight] ); } /// An unbounded row keeps every panel full. #[test] fn unbounded_row_keeps_everything_full() { let widths = [w(50.0, 30.0, 20.0, 10.0), w(50.0, 30.0, 20.0, 10.0)]; assert_eq!( decide_levels(CollapseMode::Auto, &widths, f32::INFINITY), vec![Level::Full, Level::Full] ); } /// A NaN budget is not a number, so every `<=` / `>` comparison is false: /// both phases fully degrade (all Collapsed) but the tight cascade never /// fires. Degenerate input; the point is it must not panic and must be /// deterministic. #[test] fn nan_budget_degrades_both_phases_but_no_tight_cascade() { let widths = [w(50.0, 30.0, 20.0, 10.0), w(50.0, 30.0, 20.0, 10.0)]; assert_eq!( decide_levels(CollapseMode::Auto, &widths, f32::NAN), vec![Level::Collapsed, Level::Collapsed] ); } /// A row that fits exactly at full width stays full (<=, not <). #[test] fn exact_fit_at_full_stays_full() { let widths = [w(30.0, 20.0, 15.0, 10.0), w(40.0, 25.0, 15.0, 10.0)]; assert_eq!( decide_levels(CollapseMode::Auto, &widths, 70.0), vec![Level::Full, Level::Full] ); } /// The refactor must be a no-op: `decide_levels` equals the pre-refactor /// inline `u8` algorithm (two rightmost-first phases + the tight cascade) /// on a deterministic sweep of widths and budgets, including the degenerate /// 0 / 0 / inf / NaN corner cases. #[test] fn matches_the_pre_refactor_algorithm() { fn legacy(mode: CollapseMode, widths: &[Widths], max_w: f32) -> Vec { let forced = match mode { CollapseMode::Auto => None, CollapseMode::Full => Some(0), CollapseMode::Compact => Some(1), CollapseMode::Collapsed => Some(2), }; if let Some(level) = forced { return vec![level; widths.len()]; } let width_of = |lv: u8, i: usize| match lv { 0 => widths[i].full, 1 => widths[i].compact, 2 => widths[i].button, _ => widths[i].tight, }; let total = |levels: &[u8]| -> f32 { (0..levels.len()).map(|i| width_of(levels[i], i)).sum() }; let mut levels = vec![0u8; widths.len()]; for degraded in [1u8, 2u8] { for i in (0..widths.len()).rev() { if total(&levels) <= max_w { break; } levels[i] = degraded; } } if total(&levels) > max_w { levels.iter_mut().filter(|l| **l == 2).for_each(|l| *l = 3); } levels } let mut rng: u64 = 0x243F_6A88_85A3_08D3; for n in 0..=6usize { for _ in 0..40 { let mut widths = Vec::with_capacity(n); for _ in 0..n { rng = rng .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); let full = (rng % 2000) as f32 / 10.0; rng = rng .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); let compact = (rng % (full as u64 * 10 + 1)) as f32 / 10.0; rng = rng .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); let button = (rng % (compact as u64 * 10 + 1)) as f32 / 10.0; rng = rng .wrapping_mul(6364136223846793005) .wrapping_add(1442695040888963407); let tight = (rng % (button as u64 * 10 + 1)) as f32 / 10.0; widths.push(Widths { full, compact, button, tight, }); } let budgets = [ 0.0, 0.5, 1.0, (rng % 3000) as f32 / 10.0, 800.0, 1e9, f32::INFINITY, f32::NAN, ]; for mw in budgets { let now: Vec = decide_levels(CollapseMode::Auto, &widths, mw) .iter() .map(|&l| l as u8) .collect(); assert_eq!( now, legacy(CollapseMode::Auto, &widths, mw), "n={n} widths={widths:?} max_w={mw}" ); } } } } }