mirror of
https://github.com/danbulant/cushy
synced 2026-07-08 12:40:36 +00:00
parent
8a4c66e73b
commit
8f99ae19fd
6 changed files with 1053 additions and 651 deletions
102
examples/contacts.rs
Normal file
102
examples/contacts.rs
Normal file
|
|
@ -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::<HashMap<_, _>>(),
|
||||||
|
);
|
||||||
|
let selected_contact = Dynamic::new(None::<u64>);
|
||||||
|
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::<Vec<_>>();
|
||||||
|
entries.sort();
|
||||||
|
entries
|
||||||
|
.into_iter()
|
||||||
|
.map(|(last, first, id)| {
|
||||||
|
selected_contact
|
||||||
|
.new_select(Some(id), format!("{first} {last}").align_left())
|
||||||
|
.make_widget()
|
||||||
|
})
|
||||||
|
.collect::<Children>()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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<HashMap<u64, Contact>>) -> 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()
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ use std::process::exit;
|
||||||
|
|
||||||
use gooey::value::{Dynamic, MapEach};
|
use gooey::value::{Dynamic, MapEach};
|
||||||
use gooey::widget::{MakeWidget, MakeWidgetWithId, WidgetTag};
|
use gooey::widget::{MakeWidget, MakeWidgetWithId, WidgetTag};
|
||||||
|
use gooey::widgets::grid::{Grid, GridDimension, GridWidgets};
|
||||||
use gooey::widgets::input::{InputValue, MaskedString};
|
use gooey::widgets::input::{InputValue, MaskedString};
|
||||||
use gooey::widgets::Expand;
|
use gooey::widgets::Expand;
|
||||||
use gooey::Run;
|
use gooey::Run;
|
||||||
|
|
@ -21,26 +22,15 @@ fn main() -> gooey::Result {
|
||||||
let (cancel_tag, cancel_id) = WidgetTag::new();
|
let (cancel_tag, cancel_id) = WidgetTag::new();
|
||||||
let (username_tag, username_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 = (
|
||||||
let username_row = "Username"
|
"Username",
|
||||||
.and(
|
username.clone().into_input().make_with_id(username_tag),
|
||||||
username
|
);
|
||||||
.clone()
|
|
||||||
.into_input()
|
|
||||||
.make_with_id(username_tag)
|
|
||||||
.expand(),
|
|
||||||
)
|
|
||||||
.into_columns();
|
|
||||||
|
|
||||||
let password_row = "Password"
|
let password_row = (
|
||||||
.and(
|
"Password",
|
||||||
password
|
password.clone().into_input().with_next_focus(login_id),
|
||||||
.clone()
|
);
|
||||||
.into_input()
|
|
||||||
.with_next_focus(login_id)
|
|
||||||
.expand(),
|
|
||||||
)
|
|
||||||
.into_columns();
|
|
||||||
|
|
||||||
let buttons = "Cancel"
|
let buttons = "Cancel"
|
||||||
.into_button()
|
.into_button()
|
||||||
|
|
@ -66,8 +56,11 @@ fn main() -> gooey::Result {
|
||||||
)
|
)
|
||||||
.into_columns();
|
.into_columns();
|
||||||
|
|
||||||
username_row
|
Grid::from_rows(GridWidgets::from(username_row).and(password_row))
|
||||||
.and(password_row)
|
.dimensions([
|
||||||
|
GridDimension::FitContent,
|
||||||
|
GridDimension::Fractional { weight: 1 },
|
||||||
|
])
|
||||||
.and(buttons)
|
.and(buttons)
|
||||||
.into_rows()
|
.into_rows()
|
||||||
.contain()
|
.contain()
|
||||||
|
|
|
||||||
|
|
@ -1567,7 +1567,7 @@ impl WidgetGuard<'_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A list of [`Widget`]s.
|
/// A list of [`Widget`]s.
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default, Eq, PartialEq)]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct Children {
|
pub struct Children {
|
||||||
ordered: Vec<WidgetInstance>,
|
ordered: Vec<WidgetInstance>,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ pub mod container;
|
||||||
mod custom;
|
mod custom;
|
||||||
mod data;
|
mod data;
|
||||||
mod expand;
|
mod expand;
|
||||||
|
pub mod grid;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod label;
|
pub mod label;
|
||||||
mod mode_switch;
|
mod mode_switch;
|
||||||
|
|
|
||||||
912
src/widgets/grid.rs
Normal file
912
src/widgets/grid.rs
Normal file
|
|
@ -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<const ELEMENTS: usize> {
|
||||||
|
columns: Value<[GridDimension; ELEMENTS]>,
|
||||||
|
rows: Value<GridWidgets<ELEMENTS>>,
|
||||||
|
live_rows: Vec<[ManagedWidget; ELEMENTS]>,
|
||||||
|
layout: GridLayout,
|
||||||
|
layout_generation: Option<Generation>,
|
||||||
|
spec_generation: Option<Generation>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const ELEMENTS: usize> Grid<ELEMENTS> {
|
||||||
|
fn new(orientation: Orientation, rows: impl IntoValue<GridWidgets<ELEMENTS>>) -> 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<GridWidgets<ELEMENTS>>) -> 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<GridWidgets<ELEMENTS>>) -> 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<const COLUMNS: usize> Widget for Grid<COLUMNS> {
|
||||||
|
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<ConstraintLimit>,
|
||||||
|
context: &mut LayoutContext<'_, '_, '_, '_, '_>,
|
||||||
|
) -> Size<UPx> {
|
||||||
|
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<U>(self, s: Size<U>) -> (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<U>(self, measured: U, other: U) -> Size<U> {
|
||||||
|
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<U>(self, measured: U, other: U) -> Point<U> {
|
||||||
|
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<GridDimension>,
|
||||||
|
layouts: Vec<StackLayout>,
|
||||||
|
pub elements_per_child: usize,
|
||||||
|
pub others: Vec<UPx>,
|
||||||
|
total_weights: u32,
|
||||||
|
allocated_space: (UPx, Lp),
|
||||||
|
fractional: Vec<(LotId, u8)>,
|
||||||
|
fit_to_content: Vec<LotId>,
|
||||||
|
premeasured: Vec<LotId>,
|
||||||
|
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<ConstraintLimit>,
|
||||||
|
gutter: UPx,
|
||||||
|
scale: Fraction,
|
||||||
|
mut measure: impl FnMut(usize, usize, Size<ConstraintLimit>, bool) -> Size<UPx>,
|
||||||
|
) -> Size<UPx> {
|
||||||
|
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::<u32>(),
|
||||||
|
));
|
||||||
|
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::<u32>();
|
||||||
|
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<ConstraintLimit>, bool) -> Size<UPx>,
|
||||||
|
) -> 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<UPx>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Child {
|
||||||
|
pub fn new(size: impl Into<UPx>, other: impl Into<UPx>) -> 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<UPx>) -> Self {
|
||||||
|
self.divisible_by = Some(split_at.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_measured_children_in_orientation(
|
||||||
|
orientation: Orientation,
|
||||||
|
children: &[Child],
|
||||||
|
available: Size<ConstraintLimit>,
|
||||||
|
expected: &[UPx],
|
||||||
|
expected_size: Size<UPx>,
|
||||||
|
) {
|
||||||
|
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<const N: usize>(Vec<GridSection<N>>);
|
||||||
|
|
||||||
|
impl<const N: usize> GridWidgets<N> {
|
||||||
|
/// 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<GridSection<N>>) -> Self {
|
||||||
|
self.push(section.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, const N: usize> From<T> for GridWidgets<N>
|
||||||
|
where
|
||||||
|
T: Into<GridSection<N>>,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(vec![value.into()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Deref for GridWidgets<N> {
|
||||||
|
type Target = Vec<GridSection<N>>;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> DerefMut for GridWidgets<N> {
|
||||||
|
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<const N: usize>([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<T> From<T> for GridSection<1>
|
||||||
|
where
|
||||||
|
T: MakeWidget,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self([value.make_widget()])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> Deref for GridSection<N> {
|
||||||
|
type Target = [WidgetInstance; N];
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize> DerefMut for GridSection<N> {
|
||||||
|
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);
|
||||||
|
|
@ -1,43 +1,36 @@
|
||||||
//! A widget that combines a collection of [`Children`] widgets into one.
|
//! 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 kludgine::figures::units::UPx;
|
||||||
|
use kludgine::figures::{IntoSigned, Rect, ScreenScale, Size};
|
||||||
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 crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
|
use crate::context::{AsEventContext, EventContext, GraphicsContext, LayoutContext};
|
||||||
use crate::styles::components::IntrinsicPadding;
|
use crate::styles::components::IntrinsicPadding;
|
||||||
use crate::styles::Dimension;
|
|
||||||
use crate::value::{Generation, IntoValue, Value};
|
use crate::value::{Generation, IntoValue, Value};
|
||||||
use crate::widget::{Children, ManagedWidget, Widget, WidgetRef};
|
use crate::widget::{Children, ManagedWidget, Widget, WidgetRef};
|
||||||
|
use crate::widgets::grid::{GridDimension, GridLayout, Orientation};
|
||||||
use crate::widgets::{Expand, Resize};
|
use crate::widgets::{Expand, Resize};
|
||||||
use crate::ConstraintLimit;
|
use crate::ConstraintLimit;
|
||||||
|
|
||||||
/// A widget that displays a collection of [`Children`] widgets in a
|
/// A widget that displays a collection of [`Children`] widgets in a
|
||||||
/// [direction](StackDirection).
|
/// [orientation](Orientation).
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Stack {
|
pub struct Stack {
|
||||||
direction: StackDirection,
|
orientation: Orientation,
|
||||||
/// The children widgets that belong to this array.
|
/// The children widgets that belong to this array.
|
||||||
pub children: Value<Children>,
|
pub children: Value<Children>,
|
||||||
layout: Layout,
|
layout: GridLayout,
|
||||||
layout_generation: Option<Generation>,
|
layout_generation: Option<Generation>,
|
||||||
// TODO Refactor synced_children into its own type.
|
// TODO Refactor synced_children into its own type.
|
||||||
synced_children: Vec<ManagedWidget>,
|
synced_children: Vec<ManagedWidget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Stack {
|
impl Stack {
|
||||||
/// Returns a new widget with the given direction and widgets.
|
/// Returns a new widget with the given orientation and widgets.
|
||||||
pub fn new(direction: StackDirection, widgets: impl IntoValue<Children>) -> Self {
|
pub fn new(orientation: Orientation, widgets: impl IntoValue<Children>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
direction,
|
orientation,
|
||||||
children: widgets.into_value(),
|
children: widgets.into_value(),
|
||||||
layout: Layout::new(direction),
|
layout: GridLayout::new(orientation),
|
||||||
layout_generation: None,
|
layout_generation: None,
|
||||||
synced_children: Vec::new(),
|
synced_children: Vec::new(),
|
||||||
}
|
}
|
||||||
|
|
@ -45,19 +38,19 @@ impl Stack {
|
||||||
|
|
||||||
/// Returns a new instance that displays `widgets` in a series of columns.
|
/// Returns a new instance that displays `widgets` in a series of columns.
|
||||||
pub fn columns(widgets: impl IntoValue<Children>) -> Self {
|
pub fn columns(widgets: impl IntoValue<Children>) -> Self {
|
||||||
Self::new(StackDirection::columns(), widgets)
|
Self::new(Orientation::Column, widgets)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new instance that displays `widgets` in a series of rows.
|
/// Returns a new instance that displays `widgets` in a series of rows.
|
||||||
pub fn rows(widgets: impl IntoValue<Children>) -> Self {
|
pub fn rows(widgets: impl IntoValue<Children>) -> Self {
|
||||||
Self::new(StackDirection::rows(), widgets)
|
Self::new(Orientation::Row, widgets)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) {
|
fn synchronize_children(&mut self, context: &mut EventContext<'_, '_>) {
|
||||||
let current_generation = self.children.generation();
|
let current_generation = self.children.generation();
|
||||||
self.children.invalidate_when_changed(context);
|
self.children.invalidate_when_changed(context);
|
||||||
if current_generation.map_or_else(
|
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,
|
|gen| Some(gen) != self.layout_generation,
|
||||||
) {
|
) {
|
||||||
self.layout_generation = self.children.generation();
|
self.layout_generation = self.children.generation();
|
||||||
|
|
@ -85,27 +78,18 @@ impl Stack {
|
||||||
let (mut widget, dimension) = if let Some((weight, expand)) =
|
let (mut widget, dimension) = if let Some((weight, expand)) =
|
||||||
guard.downcast_ref::<Expand>().and_then(|expand| {
|
guard.downcast_ref::<Expand>().and_then(|expand| {
|
||||||
expand
|
expand
|
||||||
.weight(self.direction.orientation == StackOrientation::Row)
|
.weight(self.orientation == Orientation::Row)
|
||||||
.map(|weight| (weight, expand))
|
.map(|weight| (weight, expand))
|
||||||
}) {
|
}) {
|
||||||
(
|
(expand.child().clone(), GridDimension::Fractional { weight })
|
||||||
expand.child().clone(),
|
|
||||||
StackDimension::Fractional { weight },
|
|
||||||
)
|
|
||||||
} else if let Some((child, size)) =
|
} else if let Some((child, size)) =
|
||||||
guard.downcast_ref::<Resize>().and_then(|r| {
|
guard.downcast_ref::<Resize>().and_then(|r| {
|
||||||
let range = match self.layout.orientation.orientation {
|
let range = match self.layout.orientation {
|
||||||
StackOrientation::Row => r.height,
|
Orientation::Row => r.height,
|
||||||
StackOrientation::Column => r.width,
|
Orientation::Column => r.width,
|
||||||
};
|
};
|
||||||
range.minimum().map(|min| {
|
range.minimum().map(|size| {
|
||||||
(
|
(r.child().clone(), GridDimension::Measured { size })
|
||||||
r.child().clone(),
|
|
||||||
StackDimension::Measured {
|
|
||||||
min,
|
|
||||||
_max: range.end,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
{
|
{
|
||||||
|
|
@ -113,7 +97,7 @@ impl Stack {
|
||||||
} else {
|
} else {
|
||||||
(
|
(
|
||||||
WidgetRef::Unmounted(widget.clone()),
|
WidgetRef::Unmounted(widget.clone()),
|
||||||
StackDimension::FitContent,
|
GridDimension::FitContent,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
drop(guard);
|
drop(guard);
|
||||||
|
|
@ -156,7 +140,7 @@ impl Widget for Stack {
|
||||||
available_space,
|
available_space,
|
||||||
context.get(&IntrinsicPadding).into_upx(context.gfx.scale()),
|
context.get(&IntrinsicPadding).into_upx(context.gfx.scale()),
|
||||||
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]);
|
let mut context = context.for_other(&self.synced_children[child_index]);
|
||||||
if !persist {
|
if !persist {
|
||||||
context = context.as_temporary();
|
context = context.as_temporary();
|
||||||
|
|
@ -176,7 +160,7 @@ impl Widget for Stack {
|
||||||
.into_signed(),
|
.into_signed(),
|
||||||
self.layout
|
self.layout
|
||||||
.orientation
|
.orientation
|
||||||
.make_size(layout.size, self.layout.other)
|
.make_size(layout.size, self.layout.others[0])
|
||||||
.into_signed(),
|
.into_signed(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -186,593 +170,3 @@ impl Widget for Stack {
|
||||||
content_size
|
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<U>(self, s: Size<U>) -> (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<U>(self, measured: U, other: U) -> Size<U> {
|
|
||||||
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<U>(self, measured: U, other: U) -> Point<U> {
|
|
||||||
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<Dimension>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct Layout {
|
|
||||||
children: OrderedLots<StackDimension>,
|
|
||||||
layouts: Vec<StackLayout>,
|
|
||||||
pub other: UPx,
|
|
||||||
total_weights: u32,
|
|
||||||
allocated_space: (UPx, Lp),
|
|
||||||
fractional: Vec<(LotId, u8)>,
|
|
||||||
fit_to_content: Vec<LotId>,
|
|
||||||
premeasured: Vec<LotId>,
|
|
||||||
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<ConstraintLimit>,
|
|
||||||
gutter: UPx,
|
|
||||||
scale: Fraction,
|
|
||||||
mut measure: impl FnMut(usize, Size<ConstraintLimit>, bool) -> Size<UPx>,
|
|
||||||
) -> Size<UPx> {
|
|
||||||
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::<u32>(),
|
|
||||||
));
|
|
||||||
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<ConstraintLimit>, bool) -> Size<UPx>,
|
|
||||||
) -> 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<UPx>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Child {
|
|
||||||
pub fn new(size: impl Into<UPx>, other: impl Into<UPx>) -> 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<UPx>) -> Self {
|
|
||||||
self.divisible_by = Some(split_at.into());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn assert_measured_children_in_orientation(
|
|
||||||
orientation: StackDirection,
|
|
||||||
children: &[Child],
|
|
||||||
available: Size<ConstraintLimit>,
|
|
||||||
expected: &[UPx],
|
|
||||||
expected_size: Size<UPx>,
|
|
||||||
) {
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue