From 8f99ae19fd25e24eef5372670c1b3de176112003 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Thu, 30 Nov 2023 18:19:53 -0800 Subject: [PATCH] Implemented a basic grid Refs #14 --- examples/contacts.rs | 102 +++++ examples/focus-order.rs | 35 +- src/widget.rs | 2 +- src/widgets.rs | 1 + src/widgets/grid.rs | 912 ++++++++++++++++++++++++++++++++++++++++ src/widgets/stack.rs | 652 +--------------------------- 6 files changed, 1053 insertions(+), 651 deletions(-) create mode 100644 examples/contacts.rs create mode 100644 src/widgets/grid.rs diff --git a/examples/contacts.rs b/examples/contacts.rs new file mode 100644 index 0000000..4070d49 --- /dev/null +++ b/examples/contacts.rs @@ -0,0 +1,102 @@ +use std::collections::HashMap; + +use gooey::value::{Dynamic, MapEach}; +use gooey::widget::{Children, MakeWidget}; +use gooey::widgets::input::InputValue; +use gooey::Run; + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Contact { + pub id: u64, + pub first_name: String, + pub last_name: String, + pub title: String, +} + +fn main() -> gooey::Result { + let initial_contacts = vec![ + Contact { + id: 0, + first_name: String::from("John"), + last_name: String::from("Doe"), + title: String::from("Chef"), + }, + Contact { + id: 1, + first_name: String::from("Jane"), + last_name: String::from("Smith"), + title: String::from("Doctor"), + }, + ]; + let db = Dynamic::new( + initial_contacts + .into_iter() + .map(|contact| (contact.id, contact)) + .collect::>(), + ); + let selected_contact = Dynamic::new(None::); + let contact_list = db.map_each({ + let selected_contact = selected_contact.clone(); + move |contacts| { + let mut entries = contacts + .iter() + .map(|(id, contact)| (contact.last_name.clone(), contact.first_name.clone(), *id)) + .collect::>(); + entries.sort(); + entries + .into_iter() + .map(|(last, first, id)| { + selected_contact + .new_select(Some(id), format!("{first} {last}").align_left()) + .make_widget() + }) + .collect::() + } + }); + + let editing_contact = (&selected_contact, &db).map_each({ + let db = db.clone(); + move |(selected, contacts)| { + selected + .map(|id| edit_contact_form(&contacts[&id], &db).make_widget()) + .unwrap_or_else(|| "Select a contact".centered().make_widget()) + } + }); + contact_list + .into_rows() + .vertical_scroll() + .and(editing_contact.expand()) + .into_columns() + .run() +} + +fn edit_contact_form(contact: &Contact, db: &Dynamic>) -> impl MakeWidget { + let first = Dynamic::new(contact.first_name.clone()); + let last = Dynamic::new(contact.last_name.clone()); + let title = Dynamic::new(contact.title.clone()); + + "First Name" + .and(first.clone().into_input()) + .and("Last Name") + .and(last.clone().into_input()) + .and("Title") + .and(title.clone().into_input()) + .and( + "Save" + .into_button() + .on_click({ + let contact_id = contact.id; + let db = db.clone(); + move |()| { + let mut db = db.lock(); + let contact = db.get_mut(&contact_id).expect("missing contact"); + contact.first_name = first.get(); + contact.last_name = last.get(); + contact.title = title.get(); + } + }) + .into_default() + .align_right(), + ) + .into_rows() +} diff --git a/examples/focus-order.rs b/examples/focus-order.rs index a5d1a79..675b0b4 100644 --- a/examples/focus-order.rs +++ b/examples/focus-order.rs @@ -2,6 +2,7 @@ use std::process::exit; use gooey::value::{Dynamic, MapEach}; use gooey::widget::{MakeWidget, MakeWidgetWithId, WidgetTag}; +use gooey::widgets::grid::{Grid, GridDimension, GridWidgets}; use gooey::widgets::input::{InputValue, MaskedString}; use gooey::widgets::Expand; use gooey::Run; @@ -21,26 +22,15 @@ fn main() -> gooey::Result { let (cancel_tag, cancel_id) = WidgetTag::new(); let (username_tag, username_id) = WidgetTag::new(); - // TODO this should be a grid layout to ensure proper visual alignment. - let username_row = "Username" - .and( - username - .clone() - .into_input() - .make_with_id(username_tag) - .expand(), - ) - .into_columns(); + let username_row = ( + "Username", + username.clone().into_input().make_with_id(username_tag), + ); - let password_row = "Password" - .and( - password - .clone() - .into_input() - .with_next_focus(login_id) - .expand(), - ) - .into_columns(); + let password_row = ( + "Password", + password.clone().into_input().with_next_focus(login_id), + ); let buttons = "Cancel" .into_button() @@ -66,8 +56,11 @@ fn main() -> gooey::Result { ) .into_columns(); - username_row - .and(password_row) + Grid::from_rows(GridWidgets::from(username_row).and(password_row)) + .dimensions([ + GridDimension::FitContent, + GridDimension::Fractional { weight: 1 }, + ]) .and(buttons) .into_rows() .contain() diff --git a/src/widget.rs b/src/widget.rs index 1da6715..fac2d8d 100644 --- a/src/widget.rs +++ b/src/widget.rs @@ -1567,7 +1567,7 @@ impl WidgetGuard<'_> { } /// A list of [`Widget`]s. -#[derive(Debug, Default)] +#[derive(Debug, Default, Eq, PartialEq)] #[must_use] pub struct Children { ordered: Vec, diff --git a/src/widgets.rs b/src/widgets.rs index 05f8b8c..1c44e92 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -9,6 +9,7 @@ pub mod container; mod custom; mod data; mod expand; +pub mod grid; pub mod input; pub mod label; mod mode_switch; diff --git a/src/widgets/grid.rs b/src/widgets/grid.rs new file mode 100644 index 0000000..4b147a8 --- /dev/null +++ b/src/widgets/grid.rs @@ -0,0 +1,912 @@ +//! A Widget that arranges children into rows and columns. +// TODO on scale change, all `Lp` children need to resize + +use std::array; +use std::fmt::Debug; +use std::ops::{Deref, DerefMut}; + +use alot::{LotId, OrderedLots}; +use intentional::{Assert, Cast}; +use kludgine::figures::units::{Lp, UPx}; +use kludgine::figures::{ + Fraction, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, +}; + +use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; +use crate::styles::components::IntrinsicPadding; +use crate::styles::Dimension; +use crate::value::{Generation, IntoValue, Value}; +use crate::widget::{MakeWidget, ManagedWidget, Widget, WidgetInstance}; +use crate::ConstraintLimit; + +/// A 2D grid of widgets. +#[derive(Debug)] +pub struct Grid { + columns: Value<[GridDimension; ELEMENTS]>, + rows: Value>, + live_rows: Vec<[ManagedWidget; ELEMENTS]>, + layout: GridLayout, + layout_generation: Option, + spec_generation: Option, +} + +impl Grid { + fn new(orientation: Orientation, rows: impl IntoValue>) -> Self { + Self { + columns: Value::Constant(array::from_fn(|_| GridDimension::FitContent)), + rows: rows.into_value(), + live_rows: Vec::new(), + layout: GridLayout::new(orientation), + layout_generation: None, + spec_generation: None, + } + } + + /// Returns a grid that displays a list of rows of columns. The columns will + /// share dimensions, while each row will be measured individually. + #[must_use] + pub fn from_rows(rows: impl IntoValue>) -> Self { + Self::new(Orientation::Column, rows) + } + + /// Returns a grid that displays a list of columns of rows. The rows will + /// share dimensions, while each column will be measured individually. + #[must_use] + pub fn from_columns(columns: impl IntoValue>) -> Self { + Self::new(Orientation::Row, columns) + } + + /// Sets the dimensions for this grid and returns self. + /// + /// A grid is a 2d collection that orients itself either around rows or + /// columns. If this grid was created using [`Self::from_rows()`], + /// `dimensions` will control how the columns are measured. If this grid was + /// created using [`Self::from_columns()`], `dimensions` will control how + /// the rows are measured. + #[must_use] + pub fn dimensions(mut self, dimensions: impl IntoValue<[GridDimension; ELEMENTS]>) -> Self { + self.columns = dimensions.into_value(); + self + } + + fn synchronize_specs(&mut self, context: &mut EventContext<'_, '_>) { + let current_generation = self.columns.generation(); + if current_generation.map_or_else( + || self.layout.children.len() != ELEMENTS, + |gen| Some(gen) != self.spec_generation, + ) { + self.spec_generation = current_generation; + self.columns.map(|columns| { + self.layout.truncate(0); + + for (index, column) in columns.iter().enumerate() { + self.layout.insert(index, *column, context.kludgine.scale()); + } + }); + } + } + + fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) { + self.synchronize_specs(context); + let current_generation = self.rows.generation(); + self.rows.invalidate_when_changed(context); + if current_generation.map_or_else( + || self.rows.map(|rows| rows.len()) != self.live_rows.len(), + |gen| Some(gen) != self.layout_generation, + ) { + self.layout_generation = current_generation; + self.rows.map(|rows| { + self.layout.set_element_count(rows.len()); + for (index, row) in rows.iter().enumerate() { + if self.live_rows.get(index).map_or(true, |child| { + child.iter().zip(row.iter()).any(|(a, b)| a != b) + }) { + // These entries do not match. See if we can find the + // new id somewhere else, if so we can swap the entries. + if let Some((swap_index, _)) = self + .live_rows + .iter() + .enumerate() + .skip(index + 1) + .find(|(_, child)| child.iter().zip(row.iter()).all(|(a, b)| a == b)) + { + self.live_rows.swap(index, swap_index); + self.layout.swap(index, swap_index); + } else { + self.live_rows.insert( + index, + array::from_fn(|index| context.push_child(row[index].clone())), + ); + } + } + } + + // Any children remaining at the end of this process are ones + // that have been removed. + for removed in self.live_rows.drain(rows.len()..) { + for removed in removed { + context.remove_child(&removed); + } + } + }); + } + } +} + +impl Widget for Grid { + fn redraw(&mut self, context: &mut GraphicsContext<'_, '_, '_, '_, '_>) { + for (row, widgets) in self.live_rows.iter_mut().enumerate() { + if self.layout.others[row] > 0 { + for (column, cell) in widgets.iter().enumerate() { + if self.layout[column].size > 0 { + context.for_other(cell).redraw(); + } + } + } + } + } + + fn layout( + &mut self, + available_space: Size, + context: &mut LayoutContext<'_, '_, '_, '_, '_>, + ) -> Size { + self.synchronize_children(&mut context.as_event_context()); + + let content_size = self.layout.update( + available_space, + context.get(&IntrinsicPadding).into_upx(context.gfx.scale()), + context.gfx.scale(), + |row, column, constraints, persist| { + let mut context = context.for_other(&self.live_rows[column][row]); + if !persist { + context = context.as_temporary(); + } + context.layout(constraints) + }, + ); + + let mut other_offset = UPx::ZERO; + for (&other_size, row) in self.layout.others.iter().zip(&self.live_rows) { + if other_size > 0 { + for (layout, cell) in self.layout.iter().zip(row) { + if layout.size > 0 { + context.set_child_layout( + cell, + Rect::new( + self.layout + .orientation + .make_point(layout.offset, other_offset) + .into_signed(), + self.layout + .orientation + .make_size(layout.size, other_size) + .into_signed(), + ), + ); + } + } + other_offset = other_offset.saturating_add(other_size); + } + } + + content_size + } +} + +/// The orientation (Row/Column) of an [`Grid`] or +/// [`Stack`](crate::widgets::Stack) widget. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] + +pub enum Orientation { + /// The child widgets should be displayed as rows. + Row, + /// The child widgets should be displayed as columns. + Column, +} + +impl Orientation { + /// Splits a size into its measured and other parts. + pub(crate) fn split_size(self, s: Size) -> (U, U) { + match self { + Orientation::Row => (s.height, s.width), + Orientation::Column => (s.width, s.height), + } + } + + /// Combines split values into a [`Size`]. + pub(crate) fn make_size(self, measured: U, other: U) -> Size { + match self { + Orientation::Row => Size::new(other, measured), + Orientation::Column => Size::new(measured, other), + } + } + + /// Combines split values into a [`Point`]. + pub(crate) fn make_point(self, measured: U, other: U) -> Point { + match self { + Orientation::Row => Point::new(other, measured), + Orientation::Column => Point::new(measured, other), + } + } +} + +/// The strategy to use when laying a widget out inside of an [`Stack`]. +#[derive(Default, Debug, Clone, Copy)] +pub enum GridDimension { + /// Attempt to lay out the widget based on its contents. + #[default] + FitContent, + /// Use a fractional amount of the available space. + Fractional { + /// The weight to apply to this widget when dividing multiple widgets + /// fractionally. + weight: u8, + }, + /// Use a specified size for the widget. + Measured { + /// The size for the widget. + size: Dimension, + }, +} + +#[derive(Debug)] +pub(crate) struct GridLayout { + children: OrderedLots, + layouts: Vec, + pub elements_per_child: usize, + pub others: Vec, + total_weights: u32, + allocated_space: (UPx, Lp), + fractional: Vec<(LotId, u8)>, + fit_to_content: Vec, + premeasured: Vec, + pub orientation: Orientation, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub(crate) struct StackLayout { + pub offset: UPx, + pub size: UPx, +} + +impl GridLayout { + pub fn new(orientation: Orientation) -> Self { + Self { + orientation, + children: OrderedLots::new(), + layouts: Vec::new(), + elements_per_child: 1, + others: vec![UPx::ZERO], + total_weights: 0, + allocated_space: (UPx::ZERO, Lp::ZERO), + fractional: Vec::new(), + fit_to_content: Vec::new(), + premeasured: Vec::new(), + } + } + + pub fn set_element_count(&mut self, count: usize) { + self.others.resize(count, UPx::ZERO); + self.elements_per_child = count; + } + + #[cfg(test)] // only used in testing + pub fn push(&mut self, child: GridDimension, scale: Fraction) { + self.insert(self.len(), child, scale); + } + + pub fn remove(&mut self, index: usize) -> GridDimension { + let (id, dimension) = self.children.remove_by_index(index).expect("invalid index"); + self.layouts.remove(index); + + match dimension { + GridDimension::FitContent => { + self.fit_to_content.retain(|&measured| measured != id); + } + GridDimension::Fractional { weight } => { + self.fractional.retain(|(measured, _)| *measured != id); + self.total_weights -= u32::from(weight); + } + GridDimension::Measured { size: min, .. } => { + self.premeasured.retain(|&measured| measured != id); + match min { + Dimension::Px(pixels) => { + self.allocated_space.0 -= pixels.into_unsigned().ceil(); + } + Dimension::Lp(lp) => { + self.allocated_space.1 -= lp; + } + } + } + } + + dimension + } + + pub fn truncate(&mut self, new_length: usize) { + while self.len() > new_length { + self.remove(self.len() - 1); + } + } + + pub fn swap(&mut self, a: usize, b: usize) { + self.children.swap(a, b); + } + + pub fn insert(&mut self, index: usize, child: GridDimension, scale: Fraction) { + let id = self.children.insert(index, child); + let layout = match child { + GridDimension::FitContent => { + self.fit_to_content.push(id); + UPx::ZERO + } + GridDimension::Fractional { weight } => { + self.total_weights += u32::from(weight); + self.fractional.push((id, weight)); + UPx::ZERO + } + GridDimension::Measured { size: min, .. } => { + self.premeasured.push(id); + match min { + Dimension::Px(size) => self.allocated_space.0 += size.into_unsigned(), + Dimension::Lp(size) => self.allocated_space.1 += size, + } + min.into_upx(scale) + } + }; + self.layouts.insert( + index, + StackLayout { + offset: UPx::ZERO, + size: layout, + }, + ); + } + + #[allow(clippy::too_many_lines)] // TODO + pub fn update( + &mut self, + available: Size, + gutter: UPx, + scale: Fraction, + mut measure: impl FnMut(usize, usize, Size, bool) -> Size, + ) -> Size { + let (space_constraint, other_constraint) = self.orientation.split_size(available); + let available_space = space_constraint.max(); + let known_gutters = gutter.saturating_mul(UPx::new( + (self.children.len() - self.fit_to_content.len()) + .saturating_sub(1) + .cast::(), + )); + let allocated_space = + self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters; + let mut remaining = available_space.saturating_sub(allocated_space); + // If our `other_constraint` is not known, we will need to give child + // widgets an opportunity to lay themselves out in the full area. This + // requires one extra layout call, so we avoid persisting layouts during + // the first loop if this is the case. + let needs_final_layout = !matches!(other_constraint, ConstraintLimit::Fill(_)); + + // Measure the children that fit their content + for other in &mut self.others { + *other = UPx::ZERO; + } + let mut requires_gutter = false; + for &id in &self.fit_to_content { + let index = self.children.index_of_id(id).expect("child not found"); + + let mut max_measured = UPx::ZERO; + + for element in 0..self.elements_per_child { + let (measured, other) = self.orientation.split_size(measure( + index, + element, + self.orientation.make_size( + ConstraintLimit::SizeToFit(remaining.saturating_sub(if requires_gutter { + gutter + } else { + UPx::ZERO + })), + other_constraint, + ), + !needs_final_layout, + )); + + if measured > 0 { + max_measured = max_measured.max(measured); + self.others[element] = self.others[element].max(other); + } + } + self.layouts[index].size = max_measured; + if max_measured > 0 { + if requires_gutter { + remaining = remaining.saturating_sub(gutter); + } else { + requires_gutter = true; + } + } + remaining = remaining.saturating_sub(max_measured); + } + + // Measure measure the "other" dimension for children that we know their size already. + for &id in &self.premeasured { + let index = self.children.index_of_id(id).expect("child not found"); + for element in 0..self.elements_per_child { + let (_, other) = self.orientation.split_size(measure( + index, + element, + self.orientation.make_size( + ConstraintLimit::Fill(self.layouts[index].size), + other_constraint, + ), + !needs_final_layout, + )); + self.others[element] = self.others[element].max(other); + } + } + + // Measure the weighted children within the remaining space + if self.total_weights > 0 { + let mut needed_gutters = u32::try_from(self.fractional.len()).unwrap_or(u32::MAX); + if !requires_gutter { + needed_gutters -= 1; + } + let gutters = gutter * needed_gutters; + let space_per_weight = + ((remaining.saturating_sub(gutters)) / self.total_weights).floor(); + remaining = remaining.saturating_sub(space_per_weight * self.total_weights + gutters); + for (fractional_index, &(id, weight)) in self.fractional.iter().enumerate() { + let index = self.children.index_of_id(id).expect("child not found"); + let mut size = space_per_weight * u32::from(weight); + + // If we have fractional amounts remaining, divide the pixels + if remaining > 0 { + let from_end = u32::try_from(self.fractional.len() - fractional_index) + .expect("too many items"); + if remaining >= from_end { + let amount = (remaining / from_end).ceil().min(remaining); + remaining -= amount; + size += amount; + } + } + + self.layouts[index].size = size; + } + + // Now that we know the constrained sizes, we can measure the children + // to get the other measurement using the constrainted measurement. + for (id, _) in &self.fractional { + let index = self.children.index_of_id(*id).expect("child not found"); + for element in 0..self.elements_per_child { + let (_, measured) = self.orientation.split_size(measure( + index, + element, + self.orientation.make_size( + ConstraintLimit::Fill(self.layouts[index].size.into_upx(scale)), + other_constraint, + ), + !needs_final_layout, + )); + self.others[element] = self.others[element].max(measured); + } + } + } + + let mut total_other = self.total_other(); + if let ConstraintLimit::Fill(max) = other_constraint { + let remaining = max.saturating_sub(total_other); + if remaining > 0 { + let other_count = self.others.len().cast::(); + let amount_per = (remaining / other_count).floor(); + let rounding_error = remaining - amount_per * other_count; + self.others[0] += amount_per + rounding_error; + for other in &mut self.others[1..] { + *other += amount_per; + } + total_other = max; + } + } + + let measured = self.update_offsets(needs_final_layout, gutter, scale, measure); + + self.orientation.make_size(measured, total_other) + } + + fn total_other(&self) -> UPx { + self.others + .iter() + .fold(UPx::ZERO, |total, other| total.saturating_add(*other)) + } + + fn update_offsets( + &mut self, + needs_final_layout: bool, + gutter: UPx, + scale: Fraction, + mut measure: impl FnMut(usize, usize, Size, bool) -> Size, + ) -> UPx { + let mut offset = UPx::ZERO; + for index in 0..self.children.len() { + let visible = self.layouts[index].size > 0; + + if visible && offset > 0 { + offset += gutter; + } + + self.layouts[index].offset = offset; + + if visible { + offset += self.layouts[index].size; + if needs_final_layout { + for element in 0..self.elements_per_child { + measure( + index, + element, + self.orientation.make_size( + ConstraintLimit::Fill(self.layouts[index].size.into_upx(scale)), + ConstraintLimit::Fill(self.others[element]), + ), + true, + ); + } + } + } + } + offset + } +} + +impl Deref for GridLayout { + type Target = [StackLayout]; + + fn deref(&self) -> &Self::Target { + &self.layouts + } +} + +#[cfg(test)] +mod tests { + use std::cmp::Ordering; + + use kludgine::figures::units::UPx; + use kludgine::figures::{Fraction, IntoSigned, Size}; + + use super::{GridDimension, GridLayout, Orientation}; + use crate::styles::Dimension; + use crate::ConstraintLimit; + + struct Child { + size: UPx, + dimension: GridDimension, + other: UPx, + divisible_by: Option, + } + + impl Child { + pub fn new(size: impl Into, other: impl Into) -> Self { + Self { + size: size.into(), + dimension: GridDimension::FitContent, + other: other.into(), + divisible_by: None, + } + } + + pub fn fixed_size(mut self, size: UPx) -> Self { + self.dimension = GridDimension::Measured { + size: Dimension::Px(size.into_signed()), + }; + self + } + + pub fn weighted(mut self, weight: u8) -> Self { + self.dimension = GridDimension::Fractional { weight }; + self + } + + pub fn divisible_by(mut self, split_at: impl Into) -> Self { + self.divisible_by = Some(split_at.into()); + self + } + } + + fn assert_measured_children_in_orientation( + orientation: Orientation, + children: &[Child], + available: Size, + expected: &[UPx], + expected_size: Size, + ) { + assert_eq!(children.len(), expected.len()); + let mut flex = GridLayout::new(orientation); + for child in children { + flex.push(child.dimension, Fraction::ONE); + } + + let computed_size = flex.update( + available, + UPx::ZERO, + Fraction::ONE, + |index, _element, constraints, _persist| { + let (measured_constraint, _other_constraint) = orientation.split_size(constraints); + let child = &children[index]; + let maximum_measured = measured_constraint.max(); + let (measured, other) = + match (child.size.cmp(&maximum_measured), child.divisible_by) { + (Ordering::Greater, Some(divisible_by)) => { + let available_divided = maximum_measured / divisible_by; + let rows = ((child.size + divisible_by - 1) / divisible_by + + available_divided + - 1) + / available_divided; + (available_divided * divisible_by, child.other * rows) + } + _ => (child.size, child.other), + }; + orientation.make_size(measured, other) + }, + ); + assert_eq!(computed_size, expected_size); + let mut offset = UPx::ZERO; + for ((index, &child), &expected) in flex.iter().enumerate().zip(expected) { + assert_eq!( + child.size, + expected, + "child {index} measured to {}, expected {}", + child.size, + expected // TODO Display for UPx + ); + assert_eq!(child.offset, offset); + offset += child.size; + } + } + + fn assert_measured_children( + children: &[Child], + main_constraint: ConstraintLimit, + other_constraint: ConstraintLimit, + expected: &[UPx], + expected_measured: UPx, + expected_other: UPx, + ) { + assert_measured_children_in_orientation( + Orientation::Row, + children, + Orientation::Row.make_size(main_constraint, other_constraint), + expected, + Orientation::Row.make_size(expected_measured, expected_other), + ); + assert_measured_children_in_orientation( + Orientation::Column, + children, + Orientation::Column.make_size(main_constraint, other_constraint), + expected, + Orientation::Column.make_size(expected_measured, expected_other), + ); + } + + #[test] + fn size_to_fit() { + assert_measured_children( + &[Child::new(3, 1), Child::new(3, 1), Child::new(3, 1)], + ConstraintLimit::SizeToFit(UPx::new(10)), + ConstraintLimit::SizeToFit(UPx::new(10)), + &[UPx::new(3), UPx::new(3), UPx::new(3)], + UPx::new(9), + UPx::new(1), + ); + } + + #[test] + fn wrapping() { + // This tests some fun rounding edge cases. Because the total weights is + // 4 and the size is 10, we have inexact math to determine the pixel + // width of each child. + // + // In this particular example, it shows the weights are clamped so that + // each is credited for 2px. This is why the first child ends up with + // 4px. However, with 4 total weight, that leaves a remaining 2px to be + // assigned. The flex algorithm divides the remaining pixels amongst the + // remaining children. + assert_measured_children( + &[ + Child::new(20, 1).divisible_by(3).weighted(2), + Child::new(3, 1).weighted(1), + Child::new(3, 1).weighted(1), + ], + ConstraintLimit::Fill(UPx::new(10)), + ConstraintLimit::SizeToFit(UPx::new(10)), + &[UPx::new(4), UPx::new(3), UPx::new(3)], + UPx::new(10), + UPx::new(7), // 20 / 3 = 6.666, rounded up is 7 + ); + // Same as above, but with an 11px box. This creates a leftover of 3 px + // (11 % 4), adding 1px to all three children. + assert_measured_children( + &[ + Child::new(20, 1).divisible_by(3).weighted(2), + Child::new(3, 1).weighted(1), + Child::new(3, 1).weighted(1), + ], + ConstraintLimit::Fill(UPx::new(11)), + ConstraintLimit::SizeToFit(UPx::new(11)), + &[UPx::new(5), UPx::new(3), UPx::new(3)], + UPx::new(11), + UPx::new(7), // 20 / 3 = 6.666, rounded up is 7 + ); + // 12px box. This creates no leftover. + assert_measured_children( + &[ + Child::new(20, 1).divisible_by(3).weighted(2), + Child::new(3, 1).weighted(1), + Child::new(3, 1).weighted(1), + ], + ConstraintLimit::Fill(UPx::new(12)), + ConstraintLimit::SizeToFit(UPx::new(12)), + &[UPx::new(6), UPx::new(3), UPx::new(3)], + UPx::new(12), + UPx::new(4), // 20 / 6 = 3.666, rounded up is 4 + ); + // 13px box. This creates a leftover of 1 px (13 % 4), adding 1px only + // to the final child + assert_measured_children( + &[ + Child::new(20, 1).divisible_by(3).weighted(2), + Child::new(3, 1).weighted(1), + Child::new(3, 1).weighted(1), + ], + ConstraintLimit::Fill(UPx::new(13)), + ConstraintLimit::SizeToFit(UPx::new(13)), + &[UPx::new(6), UPx::new(3), UPx::new(4)], + UPx::new(13), + UPx::new(4), // 20 / 6 = 3.666, rounded up is 4 + ); + } + + #[test] + fn fixed_size() { + assert_measured_children( + &[ + Child::new(3, 1).fixed_size(UPx::new(7)), + Child::new(3, 1).weighted(1), + Child::new(3, 1).weighted(1), + ], + ConstraintLimit::Fill(UPx::new(15)), + ConstraintLimit::SizeToFit(UPx::new(15)), + &[UPx::new(7), UPx::new(4), UPx::new(4)], + UPx::new(15), + UPx::new(1), + ); + } +} + +/// A 2d collection of widgets for a [`Grid`]. +#[derive(Debug, Default, Eq, PartialEq)] +pub struct GridWidgets(Vec>); + +impl GridWidgets { + /// Returns an empty collection of widgets. + #[must_use] + pub const fn new() -> Self { + Self(Vec::new()) + } + + /// Pushes another `section` of widgets and returns the updated collection. + #[must_use] + pub fn and(mut self, section: impl Into>) -> Self { + self.push(section.into()); + self + } +} + +impl From for GridWidgets +where + T: Into>, +{ + fn from(value: T) -> Self { + Self(vec![value.into()]) + } +} + +impl Deref for GridWidgets { + type Target = Vec>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for GridWidgets { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// A single dimension of widgets within a [`GridWidgets`] collection. +#[derive(Debug, Eq, PartialEq)] +pub struct GridSection([WidgetInstance; N]); + +impl GridSection<0> { + /// Returns an empty section. + #[must_use] + pub const fn new() -> Self { + Self([]) + } + + /// Appends `other` to the end of this collection of widgets and + /// returns the updated collection. + #[must_use] + pub fn and(self, other: impl MakeWidget) -> GridSection<1> { + GridSection([other.make_widget()]) + } +} + +impl From for GridSection<1> +where + T: MakeWidget, +{ + fn from(value: T) -> Self { + Self([value.make_widget()]) + } +} + +impl Deref for GridSection { + type Target = [WidgetInstance; N]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for GridSection { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +macro_rules! impl_grid_widgets_and { + ($($var:ident $num:literal)+) => { + impl_grid_widgets_and!([] $($var $num)+ ); + }; + ([$($done:ident $done_num:literal)*] $cur:ident $cur_num:literal ) => {}; + ([$($done:ident $done_num:literal)*] $cur:ident $cur_num:literal $next:ident $next_num:literal $($var:ident $num:literal)* ) => { + impl GridSection<$cur_num> { + /// Appends `other` to the end of this collection of widgets and + /// returns the updated collection. + #[must_use] + pub fn and(self, other: impl MakeWidget) -> GridSection<$next_num> { + let mut items = self.0.into_iter(); + $( + let $done = items.next().assert("known size"); + )* + GridSection([ + $($done,)* + items.next().assert("known size"), + other.make_widget() + ]) + } + } + + impl_grid_widgets_and!([$($done $done_num)* $cur $cur_num] $next $next_num $($var $num)* ); + }; +} + +impl_grid_widgets_and!(a1 1 a2 2 a3 3 a4 4 a5 5 a6 6 a7 7 a8 8 a9 9 a10 10 a11 11 a12 12); + +macro_rules! impl_grid_widgets_from_tuple { + ($($type:ident $field:tt $var:ident),+) => { + impl<$($type),+> From<($($type,)+)> for GridSection<{ $crate::count!($($field),+;) }> + where + $($type: MakeWidget,)+ + { + fn from(tuple: ($($type,)+)) -> Self { + Self([ + $(tuple.$field.make_widget(),)+ + ]) + } + } + }; +} + +impl_all_tuples!(impl_grid_widgets_from_tuple); diff --git a/src/widgets/stack.rs b/src/widgets/stack.rs index cdd47cd..38ddd04 100644 --- a/src/widgets/stack.rs +++ b/src/widgets/stack.rs @@ -1,43 +1,36 @@ //! A widget that combines a collection of [`Children`] widgets into one. -// TODO on scale change, all `Lp` children need to resize -use std::ops::{Bound, Deref}; - -use alot::{LotId, OrderedLots}; -use intentional::Cast; -use kludgine::figures::units::{Lp, UPx}; -use kludgine::figures::{ - Fraction, IntoSigned, IntoUnsigned, Point, Rect, Round, ScreenScale, Size, -}; +use kludgine::figures::units::UPx; +use kludgine::figures::{IntoSigned, Rect, ScreenScale, Size}; use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext}; use crate::styles::components::IntrinsicPadding; -use crate::styles::Dimension; use crate::value::{Generation, IntoValue, Value}; use crate::widget::{Children, ManagedWidget, Widget, WidgetRef}; +use crate::widgets::grid::{GridDimension, GridLayout, Orientation}; use crate::widgets::{Expand, Resize}; use crate::ConstraintLimit; /// A widget that displays a collection of [`Children`] widgets in a -/// [direction](StackDirection). +/// [orientation](Orientation). #[derive(Debug)] pub struct Stack { - direction: StackDirection, + orientation: Orientation, /// The children widgets that belong to this array. pub children: Value, - layout: Layout, + layout: GridLayout, layout_generation: Option, // TODO Refactor synced_children into its own type. synced_children: Vec, } impl Stack { - /// Returns a new widget with the given direction and widgets. - pub fn new(direction: StackDirection, widgets: impl IntoValue) -> Self { + /// Returns a new widget with the given orientation and widgets. + pub fn new(orientation: Orientation, widgets: impl IntoValue) -> Self { Self { - direction, + orientation, children: widgets.into_value(), - layout: Layout::new(direction), + layout: GridLayout::new(orientation), layout_generation: None, synced_children: Vec::new(), } @@ -45,19 +38,19 @@ impl Stack { /// Returns a new instance that displays `widgets` in a series of columns. pub fn columns(widgets: impl IntoValue) -> Self { - Self::new(StackDirection::columns(), widgets) + Self::new(Orientation::Column, widgets) } /// Returns a new instance that displays `widgets` in a series of rows. pub fn rows(widgets: impl IntoValue) -> Self { - Self::new(StackDirection::rows(), widgets) + Self::new(Orientation::Row, widgets) } fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) { let current_generation = self.children.generation(); self.children.invalidate_when_changed(context); if current_generation.map_or_else( - || self.children.map(Children::len) != self.layout.children.len(), + || self.children.map(Children::len) != self.layout.len(), |gen| Some(gen) != self.layout_generation, ) { self.layout_generation = self.children.generation(); @@ -85,27 +78,18 @@ impl Stack { let (mut widget, dimension) = if let Some((weight, expand)) = guard.downcast_ref::().and_then(|expand| { expand - .weight(self.direction.orientation == StackOrientation::Row) + .weight(self.orientation == Orientation::Row) .map(|weight| (weight, expand)) }) { - ( - expand.child().clone(), - StackDimension::Fractional { weight }, - ) + (expand.child().clone(), GridDimension::Fractional { weight }) } else if let Some((child, size)) = guard.downcast_ref::().and_then(|r| { - let range = match self.layout.orientation.orientation { - StackOrientation::Row => r.height, - StackOrientation::Column => r.width, + let range = match self.layout.orientation { + Orientation::Row => r.height, + Orientation::Column => r.width, }; - range.minimum().map(|min| { - ( - r.child().clone(), - StackDimension::Measured { - min, - _max: range.end, - }, - ) + range.minimum().map(|size| { + (r.child().clone(), GridDimension::Measured { size }) }) }) { @@ -113,7 +97,7 @@ impl Stack { } else { ( WidgetRef::Unmounted(widget.clone()), - StackDimension::FitContent, + GridDimension::FitContent, ) }; drop(guard); @@ -156,7 +140,7 @@ impl Widget for Stack { available_space, context.get(&IntrinsicPadding).into_upx(context.gfx.scale()), context.gfx.scale(), - |child_index, constraints, persist| { + |child_index, _element, constraints, persist| { let mut context = context.for_other(&self.synced_children[child_index]); if !persist { context = context.as_temporary(); @@ -176,7 +160,7 @@ impl Widget for Stack { .into_signed(), self.layout .orientation - .make_size(layout.size, self.layout.other) + .make_size(layout.size, self.layout.others[0]) .into_signed(), ), ); @@ -186,593 +170,3 @@ impl Widget for Stack { content_size } } - -/// The direction of an [`Stack`] widget. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -pub struct StackDirection { - /// The orientation of the widgets. - pub orientation: StackOrientation, - /// If true, the widgets will be laid out in reverse order. - pub reverse: bool, -} - -impl StackDirection { - /// Display child widgets as columns. - #[must_use] - pub const fn columns() -> Self { - Self { - orientation: StackOrientation::Column, - reverse: false, - } - } - - /// Display child widgets as columns in reverse order. - #[must_use] - pub const fn columns_rev() -> Self { - Self { - orientation: StackOrientation::Column, - reverse: true, - } - } - - /// Display child widgets as rows. - #[must_use] - pub const fn rows() -> Self { - Self { - orientation: StackOrientation::Row, - reverse: false, - } - } - - /// Display child widgets as rows in reverse order. - #[must_use] - pub const fn rows_rev() -> Self { - Self { - orientation: StackOrientation::Row, - reverse: true, - } - } - - /// Splits a size into its measured and other parts. - pub(crate) fn split_size(self, s: Size) -> (U, U) { - match self.orientation { - StackOrientation::Row => (s.height, s.width), - StackOrientation::Column => (s.width, s.height), - } - } - - /// Combines split values into a [`Size`]. - pub(crate) fn make_size(self, measured: U, other: U) -> Size { - match self.orientation { - StackOrientation::Row => Size::new(other, measured), - StackOrientation::Column => Size::new(measured, other), - } - } - - /// Combines split values into a [`Point`]. - pub(crate) fn make_point(self, measured: U, other: U) -> Point { - match self.orientation { - StackOrientation::Row => Point::new(other, measured), - StackOrientation::Column => Point::new(measured, other), - } - } -} - -/// The orientation (Row/Column) of an [`Stack`] widget. -#[derive(Debug, Clone, Copy, Eq, PartialEq)] - -pub enum StackOrientation { - /// The child widgets should be displayed as rows. - Row, - /// The child widgets should be displayed as columns. - Column, -} - -/// The strategy to use when laying a widget out inside of an [`Stack`]. -#[derive(Debug, Clone, Copy)] -enum StackDimension { - /// Attempt to lay out the widget based on its contents. - FitContent, - /// Use a fractional amount of the available space. - Fractional { - /// The weight to apply to this widget when dividing multiple widgets - /// fractionally. - weight: u8, - }, - /// Use a range for this widget's size. - Measured { - /// The minimum size for the widget. - min: Dimension, - /// The optional maximum size for the widget. - _max: Bound, - }, -} - -#[derive(Debug)] -struct Layout { - children: OrderedLots, - layouts: Vec, - pub other: UPx, - total_weights: u32, - allocated_space: (UPx, Lp), - fractional: Vec<(LotId, u8)>, - fit_to_content: Vec, - premeasured: Vec, - pub orientation: StackDirection, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -struct StackLayout { - pub offset: UPx, - pub size: UPx, -} - -impl Layout { - pub const fn new(orientation: StackDirection) -> Self { - Self { - orientation, - children: OrderedLots::new(), - layouts: Vec::new(), - other: UPx::ZERO, - total_weights: 0, - allocated_space: (UPx::ZERO, Lp::ZERO), - fractional: Vec::new(), - fit_to_content: Vec::new(), - premeasured: Vec::new(), - } - } - - #[cfg(test)] // only used in testing - pub fn push(&mut self, child: StackDimension, scale: Fraction) { - self.insert(self.len(), child, scale); - } - - pub fn remove(&mut self, index: usize) -> StackDimension { - let (id, dimension) = self.children.remove_by_index(index).expect("invalid index"); - self.layouts.remove(index); - - match dimension { - StackDimension::FitContent => { - self.fit_to_content.retain(|&measured| measured != id); - } - StackDimension::Fractional { weight } => { - self.fractional.retain(|(measured, _)| *measured != id); - self.total_weights -= u32::from(weight); - } - StackDimension::Measured { min, .. } => { - self.premeasured.retain(|&measured| measured != id); - match min { - Dimension::Px(pixels) => { - self.allocated_space.0 -= pixels.into_unsigned().ceil(); - } - Dimension::Lp(lp) => { - self.allocated_space.1 -= lp; - } - } - } - } - - dimension - } - - pub fn truncate(&mut self, new_length: usize) { - while self.len() > new_length { - self.remove(self.len() - 1); - } - } - - pub fn swap(&mut self, a: usize, b: usize) { - self.children.swap(a, b); - } - - pub fn insert(&mut self, index: usize, child: StackDimension, scale: Fraction) { - let id = self.children.insert(index, child); - let layout = match child { - StackDimension::FitContent => { - self.fit_to_content.push(id); - UPx::ZERO - } - StackDimension::Fractional { weight } => { - self.total_weights += u32::from(weight); - self.fractional.push((id, weight)); - UPx::ZERO - } - StackDimension::Measured { min, .. } => { - self.premeasured.push(id); - match min { - Dimension::Px(size) => self.allocated_space.0 += size.into_unsigned(), - Dimension::Lp(size) => self.allocated_space.1 += size, - } - min.into_upx(scale) - } - }; - self.layouts.insert( - index, - StackLayout { - offset: UPx::ZERO, - size: layout, - }, - ); - } - - pub fn update( - &mut self, - available: Size, - gutter: UPx, - scale: Fraction, - mut measure: impl FnMut(usize, Size, bool) -> Size, - ) -> Size { - let (space_constraint, other_constraint) = self.orientation.split_size(available); - let available_space = space_constraint.max(); - let known_gutters = gutter.saturating_mul(UPx::new( - (self.children.len() - self.fit_to_content.len()) - .saturating_sub(1) - .cast::(), - )); - let allocated_space = - self.allocated_space.0 + self.allocated_space.1.into_upx(scale).ceil() + known_gutters; - let mut remaining = available_space.saturating_sub(allocated_space); - // If our `other_constraint` is not known, we will need to give child - // widgets an opportunity to lay themselves out in the full area. This - // requires one extra layout call, so we avoid persisting layouts during - // the first loop if this is the case. - let needs_final_layout = !matches!(other_constraint, ConstraintLimit::Fill(_)); - - // Measure the children that fit their content - self.other = UPx::ZERO; - let mut requires_gutter = false; - for &id in &self.fit_to_content { - let index = self.children.index_of_id(id).expect("child not found"); - - let (measured, other) = self.orientation.split_size(measure( - index, - self.orientation.make_size( - ConstraintLimit::SizeToFit(remaining.saturating_sub(if requires_gutter { - gutter - } else { - UPx::ZERO - })), - other_constraint, - ), - !needs_final_layout, - )); - self.layouts[index].size = measured; - if measured > 0 { - if requires_gutter { - remaining = remaining.saturating_sub(gutter); - } else { - requires_gutter = true; - } - self.other = self.other.max(other); - } - remaining = remaining.saturating_sub(measured); - } - - // Measure measure the "other" dimension for children that we know their size already. - for &id in &self.premeasured { - let index = self.children.index_of_id(id).expect("child not found"); - let (_, other) = self.orientation.split_size(measure( - index, - self.orientation.make_size( - ConstraintLimit::Fill(self.layouts[index].size), - other_constraint, - ), - !needs_final_layout, - )); - self.other = self.other.max(other); - } - - // Measure the weighted children within the remaining space - if self.total_weights > 0 { - let mut needed_gutters = u32::try_from(self.fractional.len()).unwrap_or(u32::MAX); - if !requires_gutter { - needed_gutters -= 1; - } - let gutters = gutter * needed_gutters; - let space_per_weight = - ((remaining.saturating_sub(gutters)) / self.total_weights).floor(); - remaining = remaining.saturating_sub(space_per_weight * self.total_weights + gutters); - for (fractional_index, &(id, weight)) in self.fractional.iter().enumerate() { - let index = self.children.index_of_id(id).expect("child not found"); - let mut size = space_per_weight * u32::from(weight); - - // If we have fractional amounts remaining, divide the pixels - if remaining > 0 { - let from_end = u32::try_from(self.fractional.len() - fractional_index) - .expect("too many items"); - if remaining >= from_end { - let amount = (remaining / from_end).ceil().min(remaining); - remaining -= amount; - size += amount; - } - } - - self.layouts[index].size = size; - } - - // Now that we know the constrained sizes, we can measure the children - // to get the other measurement using the constrainted measurement. - for (id, _) in &self.fractional { - let index = self.children.index_of_id(*id).expect("child not found"); - let (_, measured) = self.orientation.split_size(measure( - index, - self.orientation.make_size( - ConstraintLimit::Fill(self.layouts[index].size.into_upx(scale)), - other_constraint, - ), - !needs_final_layout, - )); - self.other = self.other.max(measured); - } - } - - self.other = match other_constraint { - ConstraintLimit::Fill(max) => self.other.max(max), - ConstraintLimit::SizeToFit(clip_limit) => self.other.min(clip_limit), - }; - - let measured = self.update_offsets(needs_final_layout, gutter, scale, measure); - - self.orientation.make_size(measured, self.other) - } - - fn update_offsets( - &mut self, - needs_final_layout: bool, - gutter: UPx, - scale: Fraction, - mut measure: impl FnMut(usize, Size, bool) -> Size, - ) -> UPx { - let mut offset = UPx::ZERO; - for index in 0..self.children.len() { - let visible = self.layouts[index].size > 0; - - if visible && offset > 0 { - offset += gutter; - } - - self.layouts[index].offset = offset; - - if visible { - offset += self.layouts[index].size; - if needs_final_layout { - measure( - index, - self.orientation.make_size( - ConstraintLimit::Fill(self.layouts[index].size.into_upx(scale)), - ConstraintLimit::Fill(self.other), - ), - true, - ); - } - } - } - offset - } -} - -impl Deref for Layout { - type Target = [StackLayout]; - - fn deref(&self) -> &Self::Target { - &self.layouts - } -} - -#[cfg(test)] -mod tests { - use std::cmp::Ordering; - use std::ops::Bound; - - use kludgine::figures::units::UPx; - use kludgine::figures::{Fraction, IntoSigned, Size}; - - use super::{Layout, StackDimension, StackDirection}; - use crate::styles::Dimension; - use crate::ConstraintLimit; - - struct Child { - size: UPx, - dimension: StackDimension, - other: UPx, - divisible_by: Option, - } - - impl Child { - pub fn new(size: impl Into, other: impl Into) -> Self { - Self { - size: size.into(), - dimension: StackDimension::FitContent, - other: other.into(), - divisible_by: None, - } - } - - pub fn fixed_size(mut self, size: UPx) -> Self { - self.dimension = StackDimension::Measured { - min: Dimension::Px(size.into_signed()), - _max: Bound::Unbounded, - }; - self - } - - pub fn weighted(mut self, weight: u8) -> Self { - self.dimension = StackDimension::Fractional { weight }; - self - } - - pub fn divisible_by(mut self, split_at: impl Into) -> Self { - self.divisible_by = Some(split_at.into()); - self - } - } - - fn assert_measured_children_in_orientation( - orientation: StackDirection, - children: &[Child], - available: Size, - expected: &[UPx], - expected_size: Size, - ) { - assert_eq!(children.len(), expected.len()); - let mut flex = Layout::new(orientation); - for child in children { - flex.push(child.dimension, Fraction::ONE); - } - - let computed_size = flex.update( - available, - UPx::ZERO, - Fraction::ONE, - |index, constraints, _persist| { - let (measured_constraint, _other_constraint) = orientation.split_size(constraints); - let child = &children[index]; - let maximum_measured = measured_constraint.max(); - let (measured, other) = - match (child.size.cmp(&maximum_measured), child.divisible_by) { - (Ordering::Greater, Some(divisible_by)) => { - let available_divided = maximum_measured / divisible_by; - let rows = ((child.size + divisible_by - 1) / divisible_by - + available_divided - - 1) - / available_divided; - (available_divided * divisible_by, child.other * rows) - } - _ => (child.size, child.other), - }; - orientation.make_size(measured, other) - }, - ); - assert_eq!(computed_size, expected_size); - let mut offset = UPx::ZERO; - for ((index, &child), &expected) in flex.iter().enumerate().zip(expected) { - assert_eq!( - child.size, - expected, - "child {index} measured to {}, expected {}", - child.size, - expected // TODO Display for UPx - ); - assert_eq!(child.offset, offset); - offset += child.size; - } - } - - fn assert_measured_children( - children: &[Child], - main_constraint: ConstraintLimit, - other_constraint: ConstraintLimit, - expected: &[UPx], - expected_measured: UPx, - expected_other: UPx, - ) { - assert_measured_children_in_orientation( - StackDirection::rows(), - children, - StackDirection::rows().make_size(main_constraint, other_constraint), - expected, - StackDirection::rows().make_size(expected_measured, expected_other), - ); - assert_measured_children_in_orientation( - StackDirection::columns(), - children, - StackDirection::columns().make_size(main_constraint, other_constraint), - expected, - StackDirection::columns().make_size(expected_measured, expected_other), - ); - } - - #[test] - fn size_to_fit() { - assert_measured_children( - &[Child::new(3, 1), Child::new(3, 1), Child::new(3, 1)], - ConstraintLimit::SizeToFit(UPx::new(10)), - ConstraintLimit::SizeToFit(UPx::new(10)), - &[UPx::new(3), UPx::new(3), UPx::new(3)], - UPx::new(9), - UPx::new(1), - ); - } - - #[test] - fn wrapping() { - // This tests some fun rounding edge cases. Because the total weights is - // 4 and the size is 10, we have inexact math to determine the pixel - // width of each child. - // - // In this particular example, it shows the weights are clamped so that - // each is credited for 2px. This is why the first child ends up with - // 4px. However, with 4 total weight, that leaves a remaining 2px to be - // assigned. The flex algorithm divides the remaining pixels amongst the - // remaining children. - assert_measured_children( - &[ - Child::new(20, 1).divisible_by(3).weighted(2), - Child::new(3, 1).weighted(1), - Child::new(3, 1).weighted(1), - ], - ConstraintLimit::Fill(UPx::new(10)), - ConstraintLimit::SizeToFit(UPx::new(10)), - &[UPx::new(4), UPx::new(3), UPx::new(3)], - UPx::new(10), - UPx::new(7), // 20 / 3 = 6.666, rounded up is 7 - ); - // Same as above, but with an 11px box. This creates a leftover of 3 px - // (11 % 4), adding 1px to all three children. - assert_measured_children( - &[ - Child::new(20, 1).divisible_by(3).weighted(2), - Child::new(3, 1).weighted(1), - Child::new(3, 1).weighted(1), - ], - ConstraintLimit::Fill(UPx::new(11)), - ConstraintLimit::SizeToFit(UPx::new(11)), - &[UPx::new(5), UPx::new(3), UPx::new(3)], - UPx::new(11), - UPx::new(7), // 20 / 3 = 6.666, rounded up is 7 - ); - // 12px box. This creates no leftover. - assert_measured_children( - &[ - Child::new(20, 1).divisible_by(3).weighted(2), - Child::new(3, 1).weighted(1), - Child::new(3, 1).weighted(1), - ], - ConstraintLimit::Fill(UPx::new(12)), - ConstraintLimit::SizeToFit(UPx::new(12)), - &[UPx::new(6), UPx::new(3), UPx::new(3)], - UPx::new(12), - UPx::new(4), // 20 / 6 = 3.666, rounded up is 4 - ); - // 13px box. This creates a leftover of 1 px (13 % 4), adding 1px only - // to the final child - assert_measured_children( - &[ - Child::new(20, 1).divisible_by(3).weighted(2), - Child::new(3, 1).weighted(1), - Child::new(3, 1).weighted(1), - ], - ConstraintLimit::Fill(UPx::new(13)), - ConstraintLimit::SizeToFit(UPx::new(13)), - &[UPx::new(6), UPx::new(3), UPx::new(4)], - UPx::new(13), - UPx::new(4), // 20 / 6 = 3.666, rounded up is 4 - ); - } - - #[test] - fn fixed_size() { - assert_measured_children( - &[ - Child::new(3, 1).fixed_size(UPx::new(7)), - Child::new(3, 1).weighted(1), - Child::new(3, 1).weighted(1), - ], - ConstraintLimit::Fill(UPx::new(15)), - ConstraintLimit::SizeToFit(UPx::new(15)), - &[UPx::new(7), UPx::new(4), UPx::new(4)], - UPx::new(15), - UPx::new(1), - ); - } -}