Skip to main content

fission_core/ui/widgets/
grid.rs

1use crate::internal::InternalLower;
2use crate::lowering::{InternalIrBuilder, InternalLoweringCx};
3use crate::ui::Widget;
4use fission_ir::{
5    op::{GridPlacement, GridTrack, LayoutOp, Op},
6    WidgetId,
7};
8use serde::{Deserialize, Serialize};
9
10/// A CSS-grid-style layout container.
11///
12/// Define column and row tracks with [`GridTrack`] values (points, fractions,
13/// percentages, intrinsic tracks, `minmax`, fixed repeat, `auto_fit`, or
14/// `auto_fill`) and place children using [`GridItem`].
15///
16/// # Example
17///
18/// ```rust,ignore
19/// const CARD_MIN_WIDTH: f32 = 220.0;
20///
21/// Grid {
22///     columns: vec![GridTrack::auto_fit(GridTrack::minmax(
23///         GridTrack::Points(CARD_MIN_WIDTH),
24///         GridTrack::Fr(1.0),
25///     ))],
26///     rows: vec![GridTrack::Auto],
27///     column_gap: Some(tokens.spacing.m),
28///     row_gap: Some(tokens.spacing.m),
29///     children: widgets![CardA, CardB, CardC],
30///     ..Default::default()
31/// }
32/// ```
33#[derive(Debug, Default, Clone, Serialize, Deserialize)]
34pub struct Grid {
35    /// Explicit node identity.
36    pub id: Option<WidgetId>,
37    /// Grid children (typically [`GridItem`] nodes).
38    pub children: Vec<Widget>,
39    /// Column track definitions.
40    pub columns: Vec<GridTrack>,
41    /// Row track definitions.
42    pub rows: Vec<GridTrack>,
43    /// Horizontal gap between columns in layout points.
44    pub column_gap: Option<f32>,
45    /// Vertical gap between rows in layout points.
46    pub row_gap: Option<f32>,
47    /// Padding `[left, right, top, bottom]`.
48    pub padding: [f32; 4],
49}
50
51impl Grid {}
52
53impl InternalLower for Grid {
54    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
55        let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
56        cx.push_scope(id);
57
58        let mut builder = InternalIrBuilder::new(
59            id,
60            Op::Layout(LayoutOp::Grid {
61                columns: self.columns.clone(),
62                rows: self.rows.clone(),
63                column_gap: self.column_gap,
64                row_gap: self.row_gap,
65                padding: self.padding,
66            }),
67        );
68
69        for child in &self.children {
70            builder.add_child(child.lower(cx));
71        }
72
73        cx.pop_scope();
74        builder.build(cx)
75    }
76}
77
78/// A child placed within a [`Grid`] at a specific row/column position.
79///
80/// Use [`cell`](GridItem::cell) to set the row and column, and
81/// [`span`](GridItem::span) to span multiple tracks.
82///
83/// # Example
84///
85/// ```rust,ignore
86/// GridItem::new(content)
87///     .cell(2, 1)       // row 2, column 1
88///     .span(1, 2)        // span 1 row, 2 columns
89/// ```
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct GridItem {
92    /// Explicit node identity.
93    pub id: Option<WidgetId>,
94    /// The child widget placed in the grid cell.
95    pub child: Widget,
96    /// Starting row (1-indexed line or Auto).
97    pub row_start: GridPlacement,
98    /// Ending row (Auto or Span).
99    pub row_end: GridPlacement,
100    /// Starting column (1-indexed line or Auto).
101    pub col_start: GridPlacement,
102    /// Ending column (Auto or Span).
103    pub col_end: GridPlacement,
104}
105
106impl Default for GridItem {
107    fn default() -> Self {
108        Self {
109            id: None,
110            // Default child: empty Row
111            child: crate::ui::Row::default().into(),
112            row_start: GridPlacement::Auto,
113            row_end: GridPlacement::Auto,
114            col_start: GridPlacement::Auto,
115            col_end: GridPlacement::Auto,
116        }
117    }
118}
119
120impl GridItem {
121    pub fn new(child: impl Into<Widget>) -> Self {
122        Self {
123            child: child.into(),
124            ..Default::default()
125        }
126    }
127
128    pub fn cell(mut self, row: i16, col: i16) -> Self {
129        self.row_start = GridPlacement::Line(row);
130        self.col_start = GridPlacement::Line(col);
131        self
132    }
133
134    pub fn span(mut self, row_span: u16, col_span: u16) -> Self {
135        self.row_end = GridPlacement::Span(row_span);
136        self.col_end = GridPlacement::Span(col_span);
137        self
138    }
139}
140
141impl InternalLower for GridItem {
142    fn lower(&self, cx: &mut InternalLoweringCx) -> WidgetId {
143        let id = self.id.map(Into::into).unwrap_or_else(|| cx.next_node_id());
144        cx.push_scope(id);
145
146        let child_id = self.child.lower(cx);
147
148        cx.pop_scope();
149
150        let mut builder = InternalIrBuilder::new(
151            id,
152            Op::Layout(LayoutOp::GridItem {
153                row_start: self.row_start,
154                row_end: self.row_end,
155                col_start: self.col_start,
156                col_end: self.col_end,
157            }),
158        );
159        builder.add_child(child_id);
160        builder.build(cx)
161    }
162}