cranpose_ui/widgets/grid.rs
1//! `Grid`: children in rows and columns whose sizes the grid's tracks set.
2
3use std::ops::RangeInclusive;
4
5use cranpose_core::NodeId;
6use smallvec::SmallVec;
7
8use super::layout::{LayoutEnvironment, compose_layout};
9use crate::{
10 composable,
11 layout::{core::Alignment, grid::GridMeasurePolicy},
12 modifier::Modifier,
13};
14
15/// The row or column index that leaves the choice to the grid. Compose's
16/// `GridScope.GridIndexUnspecified`.
17pub const GRID_INDEX_UNSPECIFIED: i32 = 0;
18
19/// The largest row or column index and span; larger values count as this.
20/// Compose's `GridScope.MaxGridIndex`.
21pub const MAX_GRID_INDEX: i32 = 1000;
22
23/// A share of the room a grid's other tracks leave. Compose's `Fr`.
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct Fr(pub f32);
26
27/// The size of a row or a column of a [`Grid`]. Compose's `GridTrackSize`.
28/// Negative sizes count as zero.
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub enum GridTrackSize {
31 /// A size in dp.
32 Fixed(f32),
33 /// A fraction, from 0 to 1, of the grid's room after its gaps. With no
34 /// bound on the axis the track takes its content's size, as
35 /// [`GridTrackSize::MaxContent`].
36 Percentage(f32),
37 /// A share of the room the other tracks leave, in proportion to the
38 /// weight, and never smaller than its content's smallest size.
39 Flex(Fr),
40 /// At least the dp size, then a share of the room left as
41 /// [`GridTrackSize::Flex`]. It never asks its content for a size, so a
42 /// lazy list can sit in it.
43 MinMax(f32, Fr),
44 /// The smallest size the content takes.
45 MinContent,
46 /// The size the content takes with no limit.
47 MaxContent,
48 /// At least the content's smallest size, growing toward the size it
49 /// takes with no limit while the grid has room.
50 Auto,
51}
52
53/// The way children without a cell fill a [`Grid`]. Compose's `GridFlow`.
54#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
55pub enum GridFlow {
56 /// Across the first row, then the next.
57 #[default]
58 Row,
59 /// Down the first column, then the next.
60 Column,
61}
62
63/// A named area of a grid.
64#[derive(Clone, Copy, Debug, PartialEq)]
65pub(crate) struct GridAreaDef {
66 pub(crate) name: &'static str,
67 pub(crate) row: i32,
68 pub(crate) column: i32,
69 pub(crate) row_span: u32,
70 pub(crate) column_span: u32,
71}
72
73/// The tracks, gaps, flow and named areas of a [`Grid`]. Compose's
74/// `GridConfigurationScope`: a track call adds the next row or column, and
75/// the last gap call for an axis sets it.
76#[derive(Clone, Debug, Default, PartialEq)]
77pub struct GridConfig {
78 pub(crate) columns: SmallVec<[GridTrackSize; 4]>,
79 pub(crate) rows: SmallVec<[GridTrackSize; 4]>,
80 pub(crate) row_gap: f32,
81 pub(crate) column_gap: f32,
82 pub(crate) flow: GridFlow,
83 pub(crate) areas: SmallVec<[GridAreaDef; 2]>,
84}
85
86impl GridConfig {
87 /// A grid with no explicit tracks or gaps, whose children fill rows.
88 pub fn new() -> Self {
89 Self::default()
90 }
91
92 /// Adds a column.
93 pub fn column(mut self, size: GridTrackSize) -> Self {
94 self.columns.push(size);
95 self
96 }
97
98 /// Adds a column for each size, in order.
99 pub fn columns(mut self, sizes: impl IntoIterator<Item = GridTrackSize>) -> Self {
100 self.columns.extend(sizes);
101 self
102 }
103
104 /// Adds a row.
105 pub fn row(mut self, size: GridTrackSize) -> Self {
106 self.rows.push(size);
107 self
108 }
109
110 /// Adds a row for each size, in order.
111 pub fn rows(mut self, sizes: impl IntoIterator<Item = GridTrackSize>) -> Self {
112 self.rows.extend(sizes);
113 self
114 }
115
116 /// Sets the gap between rows and between columns, in dp.
117 pub fn gap(self, all: f32) -> Self {
118 self.row_gap(all).column_gap(all)
119 }
120
121 /// Sets the gap between rows, in dp.
122 pub fn row_gap(mut self, gap: f32) -> Self {
123 self.row_gap = gap.max(0.0);
124 self
125 }
126
127 /// Sets the gap between columns, in dp.
128 pub fn column_gap(mut self, gap: f32) -> Self {
129 self.column_gap = gap.max(0.0);
130 self
131 }
132
133 /// Sets the way children without a cell fill the grid.
134 pub fn flow(mut self, flow: GridFlow) -> Self {
135 self.flow = flow;
136 self
137 }
138
139 /// Names an area that children take with [`GridItem::area`]. Rows and
140 /// columns count from 1, and from the last explicit track when
141 /// negative. With [`GRID_INDEX_UNSPECIFIED`] on one axis the area is
142 /// that whole row or column, and its children fill it one after
143 /// another. Naming an area again moves it.
144 pub fn area(
145 mut self,
146 name: &'static str,
147 row: i32,
148 column: i32,
149 row_span: u32,
150 column_span: u32,
151 ) -> Self {
152 let area = GridAreaDef {
153 name,
154 row: clamp_index(row),
155 column: clamp_index(column),
156 row_span: clamp_span(row_span),
157 column_span: clamp_span(column_span),
158 };
159 match self.areas.iter_mut().find(|known| known.name == name) {
160 Some(known) => *known = area,
161 None => self.areas.push(area),
162 }
163 self
164 }
165
166 /// Names the area of the rows and columns of two ranges:
167 /// `area_ranges("footer", 2..=3, 1..=2)` is
168 /// `area("footer", 2, 1, 2, 2)`.
169 pub fn area_ranges(
170 self,
171 name: &'static str,
172 rows: RangeInclusive<i32>,
173 columns: RangeInclusive<i32>,
174 ) -> Self {
175 let (row, row_span) = start_and_span(rows);
176 let (column, column_span) = start_and_span(columns);
177 self.area(name, row, column, row_span, column_span)
178 }
179}
180
181/// Where a child of a [`Grid`] sits and how it aligns in its cell. Compose's
182/// `GridScope.gridItem` arguments; give it with [`Modifier::grid_item`].
183/// A child without one takes the next free cell.
184#[derive(Clone, Copy, Debug, PartialEq)]
185pub struct GridItem {
186 pub(crate) row: i32,
187 pub(crate) column: i32,
188 pub(crate) row_span: u32,
189 pub(crate) column_span: u32,
190 pub(crate) area: Option<&'static str>,
191 pub(crate) alignment: Alignment,
192}
193
194impl GridItem {
195 /// A child that takes the next free cell, at the top start of it.
196 pub const fn auto() -> Self {
197 Self {
198 row: GRID_INDEX_UNSPECIFIED,
199 column: GRID_INDEX_UNSPECIFIED,
200 row_span: 1,
201 column_span: 1,
202 area: None,
203 alignment: Alignment::TOP_START,
204 }
205 }
206
207 /// A child in `row` and `column`, which count from 1, and from the
208 /// last explicit track when negative (-1 is the last one). With
209 /// [`GRID_INDEX_UNSPECIFIED`] on one axis the grid picks the first free
210 /// cell along that axis. An index past the explicit tracks adds tracks
211 /// sized [`GridTrackSize::Auto`].
212 pub const fn at(row: i32, column: i32) -> Self {
213 Self {
214 row: clamp_index(row),
215 column: clamp_index(column),
216 ..Self::auto()
217 }
218 }
219
220 /// A child over the rows and columns of two ranges:
221 /// `GridItem::ranges(2..=3, 1..=2)` is `GridItem::at(2, 1).span(2, 2)`.
222 pub fn ranges(rows: RangeInclusive<i32>, columns: RangeInclusive<i32>) -> Self {
223 let (row, row_span) = start_and_span(rows);
224 let (column, column_span) = start_and_span(columns);
225 Self::at(row, column).span(row_span, column_span)
226 }
227
228 /// A child in the area [`GridConfig::area`] named `name`. A child of an
229 /// unknown area takes the next free cell.
230 pub const fn area(name: &'static str) -> Self {
231 Self {
232 area: Some(name),
233 ..Self::auto()
234 }
235 }
236
237 /// Sets how many rows and columns the child covers; a span counts as at
238 /// least 1. A child the grid places itself spans no more columns than a
239 /// row flow has, or rows than a column flow has.
240 pub const fn span(mut self, rows: u32, columns: u32) -> Self {
241 self.row_span = clamp_span(rows);
242 self.column_span = clamp_span(columns);
243 self
244 }
245
246 /// Sets where the child sits in its cell when it is smaller.
247 pub const fn alignment(mut self, alignment: Alignment) -> Self {
248 self.alignment = alignment;
249 self
250 }
251}
252
253impl Default for GridItem {
254 fn default() -> Self {
255 Self::auto()
256 }
257}
258
259impl Modifier {
260 /// Places this child of a [`Grid`]. Compose's `GridScope.gridItem`.
261 pub fn grid_item(self, item: GridItem) -> Self {
262 self.layout_data(item)
263 }
264}
265
266const fn clamp_index(index: i32) -> i32 {
267 if index > MAX_GRID_INDEX {
268 MAX_GRID_INDEX
269 } else if index < -MAX_GRID_INDEX {
270 -MAX_GRID_INDEX
271 } else {
272 index
273 }
274}
275
276const fn clamp_span(span: u32) -> u32 {
277 if span == 0 {
278 1
279 } else if span > MAX_GRID_INDEX as u32 {
280 MAX_GRID_INDEX as u32
281 } else {
282 span
283 }
284}
285
286/// The first index of a range and the count of indices in it, at least 1.
287fn start_and_span(range: RangeInclusive<i32>) -> (i32, u32) {
288 let (start, end) = range.into_inner();
289 (
290 start,
291 end.saturating_sub(start).saturating_add(1).max(1) as u32,
292 )
293}
294
295/// Lays children out in rows and columns. The config sets the tracks, the
296/// gaps and the named areas; each child takes the cell its
297/// [`Modifier::grid_item`] names, or the next free one. Rows and columns
298/// that children reach past the explicit tracks size to their content.
299/// Compose's `Grid`.
300///
301/// # Example
302///
303/// ```rust
304/// use cranpose_ui::{
305/// widgets::{Fr, Grid, GridConfig, GridItem, GridTrackSize},
306/// *,
307/// };
308///
309/// #[composable]
310/// fn Dashboard() {
311/// Grid(
312/// GridConfig::new()
313/// .columns([GridTrackSize::Fixed(120.0), GridTrackSize::Flex(Fr(1.0))])
314/// .gap(8.0),
315/// Modifier::empty().fill_max_width(),
316/// || {
317/// Text(
318/// "Title",
319/// Modifier::empty().grid_item(GridItem::at(1, 1).span(1, 2)),
320/// TextStyle::default(),
321/// );
322/// Text("Name", Modifier::empty(), TextStyle::default());
323/// Text("Ada", Modifier::empty(), TextStyle::default());
324/// },
325/// );
326/// }
327/// ```
328#[composable]
329pub fn Grid<F>(config: GridConfig, modifier: Modifier, content: F) -> NodeId
330where
331 F: FnMut() + 'static,
332{
333 let environment = LayoutEnvironment::current();
334 let policy = GridMeasurePolicy {
335 config,
336 layout_direction: environment.direction,
337 density: environment.density.density(),
338 };
339 compose_layout(modifier, policy, environment, content)
340}