use super::Layout;
use crate::compat::{Any, Box, Vec};
use crate::core::{ObjectId, Rect, Size};
use crate::widget::Widget;
use core::fmt;
struct FlowChild {
widget_id: ObjectId,
widget: Option<Box<dyn Widget>>,
default_size: Size,
}
impl FlowChild {
fn size_hint(&self) -> Size {
self.widget.as_ref().map(|w| w.size_hint()).unwrap_or(self.default_size)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FlowDirection {
#[default]
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum FlowAlignment {
#[default]
Start,
Center,
End,
SpaceBetween,
SpaceAround,
}
#[derive(Debug, Clone, Copy)]
pub struct FlowLayoutConfig {
pub direction: FlowDirection,
pub alignment: FlowAlignment,
pub spacing: i32,
pub padding: i32,
pub wrap: bool,
}
impl Default for FlowLayoutConfig {
fn default() -> Self {
Self {
direction: FlowDirection::Horizontal,
alignment: FlowAlignment::Start,
spacing: 8,
padding: 8,
wrap: false,
}
}
}
pub struct FlowLayout {
config: FlowLayoutConfig,
children: Vec<FlowChild>,
}
impl fmt::Debug for FlowLayout {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FlowLayout")
.field("config", &self.config)
.field("children", &format_args!("{} children", self.children.len()))
.finish()
}
}
impl FlowLayout {
pub fn new() -> Self {
Self { config: FlowLayoutConfig::default(), children: Vec::new() }
}
pub fn with_config(mut config: FlowLayoutConfig) -> Self {
if config.padding < 0 {
log::warn!(
"FlowLayout: negative padding {} is undefined; treating it as 0",
config.padding
);
config.padding = 0;
}
Self { config, children: Vec::new() }
}
pub fn add_child(&mut self, child: Box<dyn Widget>) {
let widget_id = child.id();
let default_size = child.size_hint();
self.children.push(FlowChild { widget_id, widget: Some(child), default_size });
}
pub fn remove_child(&mut self, index: usize) -> Option<Box<dyn Widget>> {
if index < self.children.len() {
self.children.remove(index).widget
} else {
None
}
}
pub fn set_child_size(&mut self, widget_id: ObjectId, size: Size) {
if let Some(child) = self.children.iter_mut().find(|c| c.widget_id == widget_id) {
child.default_size = size;
}
}
pub fn clear_children(&mut self) {
self.children.clear();
}
pub fn child_count(&self) -> usize {
self.children.len()
}
pub fn layout(&self, available_rect: Rect) -> Vec<Rect> {
let padding = self.config.padding.max(0);
let content_rect = Rect::new(
available_rect.x.saturating_add(padding),
available_rect.y.saturating_add(padding),
available_rect.width.saturating_sub(2 * padding as u32),
available_rect.height.saturating_sub(2 * padding as u32),
);
match self.config.direction {
FlowDirection::Horizontal => self.layout_horizontal(&content_rect),
FlowDirection::Vertical => self.layout_vertical(&content_rect),
}
}
fn layout_horizontal(&self, content_rect: &Rect) -> Vec<Rect> {
let mut positions = Vec::new();
let mut line_starts = vec![0usize];
let mut current_x = content_rect.x;
let mut current_y = content_rect.y;
let mut row_height = 0i32;
for child in &self.children {
let size = child.size_hint();
let child_width = size.width as i32;
let child_height = size.height as i32;
if self.config.wrap
&& current_x + child_width > content_rect.x + content_rect.width as i32
{
current_x = content_rect.x;
current_y += row_height + self.config.spacing;
row_height = 0;
line_starts.push(positions.len());
}
positions.push(Rect::new(
current_x,
current_y,
child_width as u32,
child_height as u32,
));
current_x += child_width + self.config.spacing;
row_height = row_height.max(child_height);
}
self.apply_alignment(&mut positions, &line_starts, content_rect);
positions
}
fn layout_vertical(&self, content_rect: &Rect) -> Vec<Rect> {
let mut positions = Vec::new();
let mut line_starts = vec![0usize];
let mut current_x = content_rect.x;
let mut current_y = content_rect.y;
let mut column_width = 0i32;
for child in &self.children {
let size = child.size_hint();
let child_width = size.width as i32;
let child_height = size.height as i32;
if self.config.wrap
&& current_y + child_height > content_rect.y + content_rect.height as i32
{
current_y = content_rect.y;
current_x += column_width + self.config.spacing;
column_width = 0;
line_starts.push(positions.len());
}
positions.push(Rect::new(
current_x,
current_y,
child_width as u32,
child_height as u32,
));
current_y += child_height + self.config.spacing;
column_width = column_width.max(child_width);
}
self.apply_alignment(&mut positions, &line_starts, content_rect);
positions
}
fn apply_alignment(&self, positions: &mut [Rect], line_starts: &[usize], content_rect: &Rect) {
if self.config.alignment == FlowAlignment::Start {
return;
}
let major_is_x = self.config.direction == FlowDirection::Horizontal;
let origin = if major_is_x { content_rect.x } else { content_rect.y };
let mut lines: Vec<core::ops::Range<usize>> = Vec::with_capacity(line_starts.len());
for (i, &start) in line_starts.iter().enumerate() {
let end = line_starts.get(i + 1).copied().unwrap_or(positions.len());
if start < end {
lines.push(start..end);
}
}
if lines.is_empty() && !positions.is_empty() {
lines.push(0..positions.len());
}
for line in &lines {
let items = &mut positions[line.start..line.end];
if items.is_empty() {
continue;
}
let available =
if major_is_x { content_rect.width as i32 } else { content_rect.height as i32 };
let sizes: i32 = if major_is_x {
items.iter().map(|r| r.width as i32).sum()
} else {
items.iter().map(|r| r.height as i32).sum()
};
let n = items.len() as i32;
let gap = match self.config.alignment {
FlowAlignment::Center | FlowAlignment::End => self.config.spacing,
FlowAlignment::SpaceBetween => {
if n > 1 {
(available - sizes) / (n - 1)
} else {
self.config.spacing
}
}
FlowAlignment::SpaceAround => {
if n > 1 {
self.config.spacing
+ (available - sizes - (n - 1) * self.config.spacing) / n
} else {
self.config.spacing
}
}
FlowAlignment::Start => self.config.spacing,
};
let painted = sizes + (n - 1) * gap;
let slack = available - painted;
let start_offset = match self.config.alignment {
FlowAlignment::Center => slack.max(0) / 2,
FlowAlignment::End => slack,
FlowAlignment::SpaceBetween => 0,
FlowAlignment::SpaceAround => {
slack.max(0) / 2
}
FlowAlignment::Start => 0,
};
let mut cursor = origin + start_offset;
for item in items.iter_mut() {
if major_is_x {
item.x = cursor;
cursor += item.width as i32 + gap;
} else {
item.y = cursor;
cursor += item.height as i32 + gap;
}
}
}
let minor_total: i32 = lines
.iter()
.map(|line| {
positions[line.start..line.end]
.iter()
.map(|r| if major_is_x { r.height as i32 } else { r.width as i32 })
.max()
.unwrap_or(0)
})
.sum::<i32>()
+ (lines.len().saturating_sub(1) as i32) * self.config.spacing;
let minor_available =
if major_is_x { content_rect.height as i32 } else { content_rect.width as i32 };
let minor_offset = match self.config.alignment {
FlowAlignment::Center => (minor_available - minor_total) / 2,
FlowAlignment::End => minor_available - minor_total,
_ => 0,
};
if minor_offset != 0 {
for pos in positions.iter_mut() {
if major_is_x {
pos.y += minor_offset;
} else {
pos.x += minor_offset;
}
}
}
}
pub fn preferred_size(&self) -> Size {
let count = self.children.len() as i32;
let gap_total: i64 =
if count > 1 { (count - 1) as i64 * self.config.spacing as i64 } else { 0 };
let padding = self.config.padding.max(0) as i64;
let clamp_extent = |v: i64| -> u32 { v.clamp(0, u32::MAX as i64) as u32 };
match self.config.direction {
FlowDirection::Horizontal => {
let width: i64 =
self.children.iter().map(|c| c.size_hint().width as i64).sum::<i64>()
+ gap_total;
let height: i64 =
self.children.iter().map(|c| c.size_hint().height as i64).max().unwrap_or(0);
Size::new(clamp_extent(width + 2 * padding), clamp_extent(height + 2 * padding))
}
FlowDirection::Vertical => {
let height: i64 =
self.children.iter().map(|c| c.size_hint().height as i64).sum::<i64>()
+ gap_total;
let width: i64 =
self.children.iter().map(|c| c.size_hint().width as i64).max().unwrap_or(0);
Size::new(clamp_extent(width + 2 * padding), clamp_extent(height + 2 * padding))
}
}
}
}
crate::impl_default_via_new!(FlowLayout);
impl Layout for FlowLayout {
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn add_widget(&mut self, widget_id: ObjectId, _stretch: u32) {
if !self.children.iter().any(|c| c.widget_id == widget_id) {
self.children.push(FlowChild {
widget_id,
widget: None,
default_size: Size::new(100, 100),
});
}
}
fn remove_widget(&mut self, widget_id: ObjectId) {
self.children.retain(|c| c.widget_id != widget_id);
}
fn update(&self, rect: Rect, widgets: &mut dyn FnMut(ObjectId, Rect)) {
let positions = self.layout(rect);
for (i, child_rect) in positions.iter().enumerate() {
if let Some(child) = self.children.get(i) {
widgets(child.widget_id, *child_rect);
}
}
}
fn child_ids(&self) -> Vec<ObjectId> {
self.children.iter().map(|c| c.widget_id).collect()
}
fn has_child(&self, id: ObjectId) -> bool {
self.children.iter().any(|c| c.widget_id == id)
}
fn clear(&mut self) {
self.clear_children();
}
}
#[cfg(test)]
mod tests {
use super::*;
struct TestWidget {
id: ObjectId,
size: Size,
}
impl TestWidget {
fn new(id: ObjectId, width: u32, height: u32) -> Self {
Self { id, size: Size::new(width, height) }
}
}
impl crate::event::EventHandler for TestWidget {
fn handle_event(&mut self, _event: &crate::event::Event) {}
}
impl Widget for TestWidget {
fn id(&self) -> ObjectId {
self.id
}
fn size_hint(&self) -> Size {
self.size
}
}
#[test]
fn test_empty_layout_returns_empty() {
let layout = FlowLayout::new();
let positions = layout.layout(Rect::new(0, 0, 300, 200));
assert!(positions.is_empty());
}
#[test]
fn test_empty_layout_center_alignment() {
let mut layout = FlowLayout::new();
layout.config.alignment = FlowAlignment::Center;
let positions = layout.layout(Rect::new(0, 0, 300, 200));
assert!(positions.is_empty());
}
#[test]
fn test_horizontal_positions_children_in_a_row() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.spacing = 10;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 40, 20)));
layout.add_child(Box::new(TestWidget::new(2, 60, 30)));
let positions = layout.layout(Rect::new(0, 0, 300, 200));
assert_eq!(positions.len(), 2);
assert_eq!(positions[0], Rect::new(0, 0, 40, 20));
assert_eq!(positions[1], Rect::new(50, 0, 60, 30));
}
#[test]
fn test_vertical_positions_children_in_a_column() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Vertical;
layout.config.spacing = 5;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 40, 20)));
layout.add_child(Box::new(TestWidget::new(2, 30, 50)));
let positions = layout.layout(Rect::new(0, 0, 300, 200));
assert_eq!(positions.len(), 2);
assert_eq!(positions[0], Rect::new(0, 0, 40, 20));
assert_eq!(positions[1], Rect::new(0, 25, 30, 50));
}
#[test]
fn test_horizontal_wrap_to_next_row() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.spacing = 0;
layout.config.padding = 0;
layout.config.wrap = true;
layout.add_child(Box::new(TestWidget::new(1, 60, 20)));
layout.add_child(Box::new(TestWidget::new(2, 60, 20)));
layout.add_child(Box::new(TestWidget::new(3, 60, 20)));
let positions = layout.layout(Rect::new(0, 0, 125, 100));
assert_eq!(positions.len(), 3);
assert_eq!(positions[0], Rect::new(0, 0, 60, 20)); assert_eq!(positions[1], Rect::new(60, 0, 60, 20)); assert_eq!(positions[2], Rect::new(0, 20, 60, 20)); }
#[test]
fn test_vertical_wrap_to_next_column() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Vertical;
layout.config.spacing = 0;
layout.config.padding = 0;
layout.config.wrap = true;
layout.add_child(Box::new(TestWidget::new(1, 30, 50)));
layout.add_child(Box::new(TestWidget::new(2, 30, 50)));
layout.add_child(Box::new(TestWidget::new(3, 30, 50)));
let positions = layout.layout(Rect::new(0, 0, 200, 105));
assert_eq!(positions.len(), 3);
assert_eq!(positions[0], Rect::new(0, 0, 30, 50)); assert_eq!(positions[1], Rect::new(0, 50, 30, 50)); assert_eq!(positions[2], Rect::new(30, 0, 30, 50)); }
#[test]
fn test_layout_honors_padding() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.spacing = 0;
layout.config.padding = 10;
layout.add_child(Box::new(TestWidget::new(1, 40, 20)));
let positions = layout.layout(Rect::new(0, 0, 100, 50));
assert_eq!(positions.len(), 1);
assert_eq!(positions[0], Rect::new(10, 10, 40, 20));
}
#[test]
fn test_horizontal_preserves_child_taller_than_container() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.spacing = 0;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 50, 30)));
let positions = layout.layout(Rect::new(0, 0, 200, 20));
assert_eq!(positions, vec![Rect::new(0, 0, 50, 30)]);
}
#[test]
fn test_horizontal_preserves_children_after_height_overflow() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.spacing = 0;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 50, 20)));
layout.add_child(Box::new(TestWidget::new(2, 50, 30)));
let positions = layout.layout(Rect::new(0, 0, 200, 25));
assert_eq!(positions.len(), 2);
assert_eq!(positions[0], Rect::new(0, 0, 50, 20));
assert_eq!(positions[1], Rect::new(50, 0, 50, 30));
}
#[test]
fn test_add_widget_sets_default_size() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.padding = 0;
layout.config.spacing = 0;
layout.add_widget(42, 0);
let positions = layout.layout(Rect::new(0, 0, 300, 200));
assert_eq!(positions.len(), 1);
assert_eq!(positions[0], Rect::new(0, 0, 100, 100));
}
#[test]
fn test_set_child_size_overrides_default() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.padding = 0;
layout.config.spacing = 0;
layout.add_widget(42, 0);
layout.set_child_size(42, Size::new(50, 30));
let positions = layout.layout(Rect::new(0, 0, 300, 200));
assert_eq!(positions.len(), 1);
assert_eq!(positions[0], Rect::new(0, 0, 50, 30));
}
#[test]
fn test_preferred_size_with_children() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.spacing = 10;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 40, 20)));
layout.add_child(Box::new(TestWidget::new(2, 60, 30)));
assert_eq!(layout.preferred_size(), Size::new(110, 30));
}
#[test]
fn wrapped_rows_are_aligned_per_line_not_against_the_whole_set() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.alignment = FlowAlignment::End;
layout.config.spacing = 0;
layout.config.padding = 0;
layout.config.wrap = true;
for id in 1..=4 {
layout.add_child(Box::new(TestWidget::new(id, 40, 10)));
}
let positions = layout.layout(Rect::new(0, 0, 100, 200));
assert_eq!(positions.len(), 4);
for row in 0..2 {
let first = positions[row * 2];
let second = positions[row * 2 + 1];
assert_eq!(first.x, 20, "row {row} is not right-aligned: {first:?}");
assert_eq!(second.x, 60, "row {row} is not right-aligned: {second:?}");
assert_eq!(
second.x + second.width as i32,
100,
"row {row} does not reach the right edge"
);
}
assert_eq!(positions[0].y, positions[1].y, "the first row is one line");
assert_eq!(positions[2].y, positions[3].y, "the second row is one line");
assert!(positions[2].y > positions[0].y, "the second line is below the first");
}
#[test]
fn a_single_line_still_uses_the_whole_set_alignment() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.alignment = FlowAlignment::Center;
layout.config.spacing = 10;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 40, 20)));
layout.add_child(Box::new(TestWidget::new(2, 40, 20)));
let positions = layout.layout(Rect::new(0, 0, 200, 100));
assert_eq!(positions[0].x, 55);
assert_eq!(positions[1].x, 105);
}
#[test]
fn space_around_is_symmetric_and_in_bounds() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.alignment = FlowAlignment::SpaceAround;
layout.config.spacing = 0;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 10, 10)));
layout.add_child(Box::new(TestWidget::new(2, 10, 10)));
let positions = layout.layout(Rect::new(0, 0, 100, 40));
assert_eq!(positions[0], Rect::new(20, 0, 10, 10));
assert_eq!(positions[1], Rect::new(70, 0, 10, 10));
assert_eq!(positions[0].x, 100 - (positions[1].x + positions[1].width as i32));
}
#[test]
fn space_around_pays_the_base_gap_and_handles_remainders() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.alignment = FlowAlignment::SpaceAround;
layout.config.spacing = 10;
layout.config.padding = 0;
for id in [1, 2, 3] {
layout.add_child(Box::new(TestWidget::new(id, 30, 10)));
}
let positions = layout.layout(Rect::new(0, 0, 160, 40));
assert_eq!(positions.len(), 3);
let lead = positions[0].x;
let trail = 160 - (positions[2].x + positions[2].width as i32);
assert_eq!(lead, trail, "the run is centred, not left-padded");
for (i, pos) in positions.iter().enumerate() {
assert!(
pos.x >= 0 && pos.x + pos.width as i32 <= 160,
"item {i} ran off the line: {pos:?}"
);
}
}
#[test]
fn space_around_single_item_and_vertical_are_symmetric() {
let mut horizontal = FlowLayout::new();
horizontal.config.direction = FlowDirection::Horizontal;
horizontal.config.alignment = FlowAlignment::SpaceAround;
horizontal.config.spacing = 0;
horizontal.config.padding = 0;
horizontal.add_child(Box::new(TestWidget::new(1, 10, 10)));
let positions = horizontal.layout(Rect::new(0, 0, 100, 40));
assert_eq!(positions[0].x, 45, "a lone item is centred");
let mut vertical = FlowLayout::new();
vertical.config.direction = FlowDirection::Vertical;
vertical.config.alignment = FlowAlignment::SpaceAround;
vertical.config.spacing = 0;
vertical.config.padding = 0;
vertical.add_child(Box::new(TestWidget::new(1, 10, 10)));
vertical.add_child(Box::new(TestWidget::new(2, 10, 10)));
let positions = vertical.layout(Rect::new(0, 0, 40, 100));
assert_eq!(positions[0], Rect::new(0, 20, 10, 10));
assert_eq!(positions[1], Rect::new(0, 70, 10, 10));
}
#[test]
fn minor_axis_centering_uses_the_lines_max_extent() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.alignment = FlowAlignment::Center;
layout.config.spacing = 0;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 10, 10)));
layout.add_child(Box::new(TestWidget::new(2, 10, 10)));
let positions = layout.layout(Rect::new(0, 0, 100, 100));
assert_eq!(positions[0].y, 45);
assert_eq!(positions[1].y, 45);
}
#[test]
fn wrapped_minor_axis_centering_uses_real_line_heights() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.alignment = FlowAlignment::Center;
layout.config.spacing = 0;
layout.config.padding = 0;
layout.config.wrap = true;
layout.add_child(Box::new(TestWidget::new(1, 40, 10)));
layout.add_child(Box::new(TestWidget::new(2, 40, 30)));
layout.add_child(Box::new(TestWidget::new(3, 40, 20)));
let positions = layout.layout(Rect::new(0, 0, 100, 100));
assert_eq!(positions[0].y, 25);
assert_eq!(positions[1].y, 25);
assert_eq!(positions[2].y, 55);
}
#[test]
fn negative_spacing_measures_and_places_consistently() {
let mut layout = FlowLayout::new();
layout.config.direction = FlowDirection::Horizontal;
layout.config.spacing = -1;
layout.config.padding = 0;
layout.add_child(Box::new(TestWidget::new(1, 10, 10)));
layout.add_child(Box::new(TestWidget::new(2, 10, 10)));
assert_eq!(layout.preferred_size(), Size::new(19, 10));
let positions = layout.layout(Rect::new(0, 0, 100, 40));
assert_eq!(positions[1].x, 9, "the second child overlaps by the negative spacing");
}
#[test]
fn negative_spacing_with_space_between_keeps_one_line() {
let mut layout = FlowLayout::with_config(FlowLayoutConfig {
direction: FlowDirection::Horizontal,
alignment: FlowAlignment::SpaceBetween,
spacing: -10,
wrap: false,
padding: 0,
});
layout.add_child(Box::new(TestWidget::new(1, 10, 10)));
layout.add_child(Box::new(TestWidget::new(2, 10, 10)));
let positions = layout.layout(Rect::new(0, 0, 100, 40));
assert_eq!(positions.len(), 2);
assert_eq!(positions[0].y, positions[1].y, "both children stay on one line");
assert_eq!(positions[0].x, 0);
assert_eq!(positions[1].x, 90, "SpaceBetween spreads the single line's free space");
}
#[test]
fn negative_spacing_with_space_between_keeps_one_column() {
let mut layout = FlowLayout::with_config(FlowLayoutConfig {
direction: FlowDirection::Vertical,
alignment: FlowAlignment::SpaceBetween,
spacing: -10,
wrap: false,
padding: 0,
});
layout.add_child(Box::new(TestWidget::new(1, 10, 10)));
layout.add_child(Box::new(TestWidget::new(2, 10, 10)));
let positions = layout.layout(Rect::new(0, 0, 40, 100));
assert_eq!(positions[0].x, positions[1].x, "both children stay in one column");
assert_eq!(positions[0].y, 0);
assert_eq!(positions[1].y, 90);
}
#[test]
fn wrapping_still_splits_lines_with_negative_spacing() {
let mut layout = FlowLayout::with_config(FlowLayoutConfig {
direction: FlowDirection::Horizontal,
alignment: FlowAlignment::Start,
spacing: -5,
wrap: true,
padding: 0,
});
for id in 1..=3 {
layout.add_child(Box::new(TestWidget::new(id, 10, 10)));
}
let positions = layout.layout(Rect::new(0, 0, 15, 100));
assert_eq!(positions[0].y, 0);
assert_eq!(positions[1].y, 0, "the second child still fits on the first line");
assert!(positions[2].y > 0, "the third child wraps to a new line");
}
#[test]
fn negative_padding_is_clamped_to_zero_at_construction() {
let layout = FlowLayout::with_config(FlowLayoutConfig {
direction: FlowDirection::Horizontal,
alignment: FlowAlignment::Start,
spacing: 0,
padding: -1,
wrap: false,
});
assert_eq!(layout.preferred_size(), Size::new(0, 0));
}
#[test]
fn a_directly_mutated_negative_padding_does_not_panic() {
let mut layout = FlowLayout::new();
layout.config.padding = -1;
layout.config.spacing = 0;
layout.add_child(Box::new(TestWidget::new(1, 10, 10)));
let positions = layout.layout(Rect::new(0, 0, 100, 50));
assert_eq!(positions[0], Rect::new(0, 0, 10, 10));
}
#[test]
fn a_huge_padding_saturates_instead_of_wrapping() {
let mut layout = FlowLayout::new();
layout.config.padding = i32::MAX;
layout.config.spacing = 0;
layout.add_child(Box::new(TestWidget::new(1, 10, 10)));
let size = layout.preferred_size();
assert!(size.width >= 10 && size.height >= 10);
}
#[test]
fn dyn_clear_removes_flow_children() {
let mut layout: Box<dyn Layout> = Box::new(FlowLayout::new());
layout.add_widget(1, 0);
assert!(layout.has_child(1));
layout.clear();
assert!(!layout.has_child(1));
assert!(layout.child_ids().is_empty());
layout.clear();
assert!(layout.child_ids().is_empty());
}
}