fix(statusbar): keep pills right-aligned

Reuse free space on the final wrapped layout-tab row before adding another status-bar row.

Closes #527
This commit is contained in:
Hakan Seven 2026-07-27 21:33:25 +03:00
commit ebed0e6e62
2 changed files with 94 additions and 21 deletions

View file

@ -363,12 +363,15 @@ impl StatusBar {
) )
.into(), .into(),
); );
let right_status = WrapFlow::new(pills).spacing_x(2.0).row_h(30.0); let right_status = WrapFlow::new(pills)
.spacing_x(2.0)
.row_h(30.0)
.justify_end(true);
// Left area: hamburger menu + Model/layout tabs in a flex-wrap flow, so // Left area: hamburger menu + Model/layout tabs in a flex-wrap flow, so
// they spill onto lower rows when narrow (no scroll arrows). The pills // they spill onto lower rows when narrow (no scroll arrows). The pills
// wrap in their own flow; WrapBar stacks the two areas so a wrapped tab // use the remaining space on the final tab row when they fit; otherwise
// never shares a row with a pill. // WrapBar adds another right-aligned row.
let mut left: Vec<Element<'_, Message>> = Vec::new(); let mut left: Vec<Element<'_, Message>> = Vec::new();
left.push(PosReport::new(SB_LAYOUTLIST_ID, menu_btn).into()); left.push(PosReport::new(SB_LAYOUTLIST_ID, menu_btn).into());
if show_layout_tabs { if show_layout_tabs {

View file

@ -8,8 +8,9 @@
//! • one row → `lead` at the left; `trail` packed after it (`justify_end` //! • one row → `lead` at the left; `trail` packed after it (`justify_end`
//! false) or flush to the right edge (`justify_end` true); //! false) or flush to the right edge (`justify_end` true);
//! an optional Fill `middle` stretches through the gap. //! an optional Fill `middle` stretches through the gap.
//! • two rows → `lead` (+ `middle` filling the rest) on row 1; `trail` on //! • wrapped → `trail` first uses free space on `lead`'s final row; only
//! row 2, left- or right-aligned per `justify_end`. //! when it does not fit is another row added. Alignment follows
//! `justify_end`.
//! //!
//! The measured total height is written to `height_out` (if set) so callers can //! The measured total height is written to `height_out` (if set) so callers can
//! anchor overlays below the possibly-taller bar. //! anchor overlays below the possibly-taller bar.
@ -201,7 +202,8 @@ impl<'a> Widget<Message, Theme, Renderer> for WrapBar<'a> {
total_h = row_h + trail_sz.height; total_h = row_h + trail_sz.height;
} else { } else {
// 2-slot dual-wrap: lead and trail each wrap within their OWN row // 2-slot dual-wrap: lead and trail each wrap within their OWN row
// band, stacked so a wrapped lead item never lands on a trail row. // band. When the lead itself wrapped, first try the unused space on
// its final row before adding another row for the trail.
lead_node = lead_node =
self.lead self.lead
.as_widget_mut() .as_widget_mut()
@ -209,19 +211,58 @@ impl<'a> Widget<Message, Theme, Renderer> for WrapBar<'a> {
lead_sz = lead_node.size(); lead_sz = lead_node.size();
let lead_h = lead_sz.height.max(self.min_row_h); let lead_h = lead_sz.height.max(self.min_row_h);
trail_node = self.trail.as_widget_mut().layout(
&mut tree.children[trail_idx],
renderer,
&bounded,
);
trail_sz = trail_node.size();
let trail_h = trail_sz.height.max(self.min_row_h);
lead_pos = Point::new(0.0, (lead_h - lead_sz.height) / 2.0); lead_pos = Point::new(0.0, (lead_h - lead_sz.height) / 2.0);
trail_pos = Point::new(0.0, lead_h + (trail_h - trail_sz.height) / 2.0);
middle_x = 0.0; middle_x = 0.0;
middle_gap = 0.0; middle_gap = 0.0;
total_h = lead_h + trail_h;
let shared_last_row = if self.justify_end && trail_sz.width <= width {
let mut last_center: Option<f32> = None;
for child in lead_node.children() {
let bounds = child.bounds();
let center = bounds.y + bounds.height / 2.0;
last_center = Some(last_center.map_or(center, |value| value.max(center)));
}
last_center.and_then(|center| {
let last_right = lead_node
.children()
.iter()
.filter_map(|child| {
let bounds = child.bounds();
let child_center = bounds.y + bounds.height / 2.0;
((child_center - center).abs() < 0.5)
.then_some(bounds.x + bounds.width)
})
.fold(0.0f32, f32::max);
let trail_x = (width - trail_sz.width).max(0.0);
(last_right + self.spacing <= trail_x).then_some((
trail_x,
lead_pos.y + center - trail_sz.height / 2.0,
))
})
} else {
None
};
if let Some((trail_x, trail_y)) = shared_last_row {
trail_pos = Point::new(trail_x, trail_y);
total_h = lead_h.max(trail_y + trail_sz.height);
} else {
trail_node = self.trail.as_widget_mut().layout(
&mut tree.children[trail_idx],
renderer,
&bounded,
);
trail_sz = trail_node.size();
let trail_h = trail_sz.height.max(self.min_row_h);
let trail_x = if self.justify_end {
(width - trail_sz.width).max(0.0)
} else {
0.0
};
trail_pos =
Point::new(trail_x, lead_h + (trail_h - trail_sz.height) / 2.0);
total_h = lead_h + trail_h;
}
} }
let mut children: Vec<layout::Node> = Vec::with_capacity(3); let mut children: Vec<layout::Node> = Vec::with_capacity(3);
@ -436,6 +477,8 @@ pub struct WrapFlow<'a> {
min_spacing_x: f32, min_spacing_x: f32,
spacing_y: f32, spacing_y: f32,
row_h: f32, row_h: f32,
/// Align every wrapped row to the right edge of the available width.
justify_end: bool,
} }
impl<'a> WrapFlow<'a> { impl<'a> WrapFlow<'a> {
@ -446,6 +489,7 @@ impl<'a> WrapFlow<'a> {
min_spacing_x: f32::INFINITY, min_spacing_x: f32::INFINITY,
spacing_y: 0.0, spacing_y: 0.0,
row_h: 28.0, row_h: 28.0,
justify_end: false,
} }
} }
@ -465,6 +509,11 @@ impl<'a> WrapFlow<'a> {
self.row_h = h; self.row_h = h;
self self
} }
pub fn justify_end(mut self, justify: bool) -> Self {
self.justify_end = justify;
self
}
} }
impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> { impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> {
@ -514,29 +563,50 @@ impl<'a> Widget<Message, Theme, Renderer> for WrapFlow<'a> {
} }
} }
let mut nodes: Vec<layout::Node> = Vec::with_capacity(n); let mut positioned: Vec<(layout::Node, f32, f32, usize)> =
Vec::with_capacity(n);
let mut row_widths = vec![0.0f32];
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 row_index = 0usize;
for node in measured { for node in measured {
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;
y += self.row_h + self.spacing_y; y += self.row_h + self.spacing_y;
row_index += 1;
row_widths.push(0.0);
} }
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))); positioned.push((node, x, cy, row_index));
row_widths[row_index] = x + sz.width;
x += sz.width + eff_gap; x += sz.width + eff_gap;
used_w = used_w.max(x - eff_gap);
} }
let used_w = row_widths.iter().copied().fold(0.0f32, f32::max);
let flow_w = if self.justify_end && max_w.is_finite() {
max_w.max(0.0)
} else {
used_w
};
let nodes = positioned
.into_iter()
.map(|(node, x, cy, row)| {
let offset = if self.justify_end {
(flow_w - row_widths[row]).max(0.0)
} else {
0.0
};
node.move_to(Point::new(x + offset, cy))
})
.collect();
let total_h = if self.items.is_empty() { let total_h = if self.items.is_empty() {
self.row_h self.row_h
} else { } else {
y + self.row_h y + self.row_h
}; };
layout::Node::with_children(Size::new(used_w.max(0.0), total_h), nodes) layout::Node::with_children(Size::new(flow_w.max(0.0), total_h), nodes)
} }
fn update( fn update(