use crate::menu::{Align, Flex};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SegmentMetrics {
pub intrinsic_width: f32,
pub flex: Flex,
pub align: Align,
}
impl SegmentMetrics {
pub fn new(intrinsic_width: f32, flex: Flex, align: Align) -> Self {
SegmentMetrics {
intrinsic_width,
flex,
align,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SegmentBox {
pub x: f32,
pub width: f32,
pub text_x: f32,
}
pub fn resolve_segments(segments: &[SegmentMetrics], available_width: f32) -> Vec<SegmentBox> {
let total_intrinsic: f32 = segments.iter().map(|s| s.intrinsic_width).sum();
let leftover = (available_width - total_intrinsic).max(0.0);
let grow_count = segments.iter().filter(|s| s.flex == Flex::Grow).count();
let per_grow = if grow_count > 0 {
leftover / grow_count as f32
} else {
0.0
};
let mut boxes = Vec::with_capacity(segments.len());
let mut cursor = 0.0_f32;
for seg in segments {
let width = match seg.flex {
Flex::Grow => seg.intrinsic_width + per_grow,
Flex::Fixed => seg.intrinsic_width,
};
let slack = (width - seg.intrinsic_width).max(0.0);
let text_x = match seg.align {
Align::Left => cursor,
Align::Center => cursor + slack / 2.0,
Align::Right => cursor + slack,
};
boxes.push(SegmentBox {
x: cursor,
width,
text_x,
});
cursor += width;
}
boxes
}
pub fn consumed_width(boxes: &[SegmentBox]) -> f32 {
boxes.last().map(|b| b.x + b.width).unwrap_or(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f32 = 1e-4;
fn approx(a: f32, b: f32) {
assert!((a - b).abs() < EPS, "expected {b}, got {a}");
}
#[test]
fn grow_label_then_right_value_is_flush_right() {
let segs = [
SegmentMetrics::new(50.0, Flex::Grow, Align::Left),
SegmentMetrics::new(40.0, Flex::Fixed, Align::Right),
];
let boxes = resolve_segments(&segs, 200.0);
approx(boxes[0].x, 0.0);
approx(boxes[0].width, 160.0);
approx(boxes[0].text_x, 0.0); approx(boxes[1].x, 160.0);
approx(boxes[1].width, 40.0);
approx(boxes[1].text_x, 160.0);
approx(consumed_width(&boxes), 200.0);
}
#[test]
fn two_grow_segments_split_leftover_evenly() {
let segs = [
SegmentMetrics::new(20.0, Flex::Grow, Align::Left),
SegmentMetrics::new(20.0, Flex::Grow, Align::Left),
];
let boxes = resolve_segments(&segs, 120.0);
approx(boxes[0].width, 60.0);
approx(boxes[1].width, 60.0);
approx(boxes[1].x, 60.0);
}
#[test]
fn no_grow_segments_are_left_packed() {
let segs = [
SegmentMetrics::new(30.0, Flex::Fixed, Align::Left),
SegmentMetrics::new(30.0, Flex::Fixed, Align::Left),
];
let boxes = resolve_segments(&segs, 200.0);
approx(boxes[0].x, 0.0);
approx(boxes[1].x, 30.0);
approx(consumed_width(&boxes), 60.0); }
#[test]
fn center_alignment_centers_within_box() {
let segs = [SegmentMetrics::new(40.0, Flex::Grow, Align::Center)];
let boxes = resolve_segments(&segs, 100.0);
approx(boxes[0].width, 100.0);
approx(boxes[0].text_x, 30.0);
}
#[test]
fn overflow_clamps_leftover_to_zero() {
let segs = [
SegmentMetrics::new(100.0, Flex::Grow, Align::Left),
SegmentMetrics::new(50.0, Flex::Fixed, Align::Right),
];
let boxes = resolve_segments(&segs, 100.0);
approx(boxes[0].width, 100.0); approx(boxes[1].width, 50.0);
approx(boxes[1].text_x, 100.0); }
#[test]
fn empty_input_is_empty_output() {
assert!(resolve_segments(&[], 100.0).is_empty());
approx(consumed_width(&[]), 0.0);
}
}