Skip to main content

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(no_skip)]
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}