From 8e6146fd91927ee79894e1563c3ec55f7ecf6081 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sun, 5 Jul 2026 23:58:18 +0300 Subject: [PATCH] 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 --- src/ui/ribbon/collapse.rs | 188 ++++++++++++++++++++++++++++---------- src/ui/ribbon/mod.rs | 149 ++++++++++++++++++++---------- src/ui/ribbon/widgets.rs | 4 + src/ui/wrap_bar.rs | 43 ++++++++- 4 files changed, 282 insertions(+), 102 deletions(-) diff --git a/src/ui/ribbon/collapse.rs b/src/ui/ribbon/collapse.rs index 958cd3e0a..b97ed1e15 100644 --- a/src/ui/ribbon/collapse.rs +++ b/src/ui/ribbon/collapse.rs @@ -1,11 +1,15 @@ //! Adaptive ribbon panels. //! -//! 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. +//! 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}; @@ -17,31 +21,49 @@ use iced::{ 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 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 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 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>, /// Title of the panel whose flyout is open (if any). open: Option, - /// Row height (a full ribbon tool-area height). + /// 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>, + /// If set, the measured row height is written here each layout (read when + /// anchoring dropdowns below the ribbon). + height_out: Option>, } impl<'a> CollapsePanels<'a> { @@ -53,14 +75,22 @@ impl<'a> CollapsePanels<'a> { 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) -> 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, - _ => &self.panels[i].button, + COLLAPSED => &self.panels[i].button, + _ => &self.panels[i].tight, } } @@ -68,7 +98,8 @@ impl<'a> CollapsePanels<'a> { match level { FULL => &mut self.panels[i].full, 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 for CollapsePanels<'a> { fn children(&self) -> Vec { - 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 { 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 @@ -97,6 +129,7 @@ impl<'a> Widget for CollapsePanels<'a> { 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); @@ -116,58 +149,109 @@ impl<'a> Widget for CollapsePanels<'a> { let natural = layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY)); 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 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 { - 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; + 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], - _ => 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() }; - // 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. + // 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 i in (0..n).rev() { - if total(&levels) <= max_w { - break; + for degraded in [COMPACT, COLLAPSED] { + for i in (0..n).rev() { + if total(&levels) <= max_w { + break; + } + levels[i] = degraded; } - levels[i] = COMPACT; } - for i in (0..n).rev() { - if total(&levels) <= max_w { - break; - } - levels[i] = COLLAPSED; + 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 = 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 = 4 * i + level as usize; + 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, @@ -175,12 +259,16 @@ impl<'a> Widget for CollapsePanels<'a> { ); let h = node.size().height; 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))); 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( @@ -197,7 +285,7 @@ impl<'a> Widget for CollapsePanels<'a> { let levels = self.levels_snapshot(self.panels.len()); for (i, child_layout) in layout.children().enumerate() { 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( &mut tree.children[tree_idx], event, @@ -223,7 +311,7 @@ impl<'a> Widget for CollapsePanels<'a> { let mut interaction = mouse::Interaction::default(); for (i, child_layout) in layout.children().enumerate() { 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( &tree.children[tree_idx], child_layout, @@ -248,7 +336,7 @@ impl<'a> Widget for CollapsePanels<'a> { let levels = self.levels_snapshot(self.panels.len()); for (i, child_layout) in layout.children().enumerate() { 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( &mut tree.children[tree_idx], child_layout, @@ -271,7 +359,7 @@ impl<'a> Widget for CollapsePanels<'a> { let levels = self.levels_snapshot(self.panels.len()); for (i, child_layout) in layout.children().enumerate() { 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( &tree.children[tree_idx], renderer, @@ -283,12 +371,13 @@ impl<'a> Widget for CollapsePanels<'a> { ); } - // 1px divider between adjacent panels, except between two collapsed - // panels (whose buttons read better with no line between them). + // 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 = layout.children().map(|l| l.bounds()).collect(); let wb = layout.bounds(); 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; } let x = bounds[i + 1].x; @@ -320,8 +409,9 @@ impl<'a> Widget for CollapsePanels<'a> { 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 collapsed panel shows a flyout. - if levels.get(p).copied().unwrap_or(FULL) != COLLAPSED { + // 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; } @@ -331,7 +421,7 @@ impl<'a> Widget for CollapsePanels<'a> { Some(overlay::Element::new(Box::new(FlyoutOverlay { flyout: &mut self.panels[p].flyout, - tree: &mut tree.children[4 * p + 3], + tree: &mut tree.children[SLOTS * p + 4], anchor, }))) } diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index 93e1d2fcf..123d44f54 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -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, - /// 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, } @@ -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 { diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index bc661fcae..ff88d9989 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -37,6 +37,10 @@ pub(super) const ARROW_W: f32 = ROW_H * 0.4; 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. 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 ────────────────────────────────────────────────────── diff --git a/src/ui/wrap_bar.rs b/src/ui/wrap_bar.rs index f8b791474..58446ffda 100644 --- a/src/ui/wrap_bar.rs +++ b/src/ui/wrap_bar.rs @@ -431,6 +431,9 @@ impl<'a> From> for Element<'a, Message> { pub struct WrapFlow<'a> { items: Vec>, 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, row_h: f32, } @@ -440,6 +443,7 @@ impl<'a> WrapFlow<'a> { Self { items, spacing_x: 2.0, + min_spacing_x: f32::INFINITY, spacing_y: 0.0, row_h: 28.0, } @@ -450,6 +454,13 @@ impl<'a> WrapFlow<'a> { 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 { self.row_h = h; self @@ -480,13 +491,35 @@ impl<'a> Widget for WrapFlow<'a> { let natural = layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, f32::INFINITY)); - let mut nodes: Vec = Vec::with_capacity(self.items.len()); + // Measure each item exactly once and total their widths. + let mut measured: Vec = 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 = Vec::with_capacity(n); let mut x = 0.0f32; let mut y = 0.0f32; let mut used_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); + for node in measured { let sz = node.size(); if x > 0.0 && x + sz.width > max_w { x = 0.0; @@ -494,8 +527,8 @@ impl<'a> Widget for WrapFlow<'a> { } let cy = y + ((self.row_h - sz.height) / 2.0).max(0.0); nodes.push(node.move_to(Point::new(x, cy))); - x += sz.width + self.spacing_x; - used_w = used_w.max(x - self.spacing_x); + x += sz.width + eff_gap; + used_w = used_w.max(x - eff_gap); } let total_h = if self.items.is_empty() {