grid_rs/
grid-rs.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: 2025 Fundament Research Institute <https://fundament.institute>
3
4use core::f32;
5use feather_macro::*;
6use feather_ui::color::sRGB;
7use feather_ui::component::button::Button;
8use feather_ui::component::gridbox::GridBox;
9use feather_ui::component::region::Region;
10use feather_ui::component::shape::{Shape, ShapeKind};
11use feather_ui::component::text::Text;
12use feather_ui::component::window::Window;
13use feather_ui::component::{ChildOf, mouse_area};
14use feather_ui::layout::{base, fixed, grid, leaf};
15use feather_ui::persist::{FnPersist2, FnPersistStore};
16use feather_ui::{
17    AbsPoint, AbsRect, App, DAbsPoint, DAbsRect, DRect, DValue, FILL_DRECT, InputResult, RelRect,
18    ScopeID, Slot, SourceID, UNSIZED_AXIS,
19};
20use std::sync::Arc;
21
22#[derive(PartialEq, Clone, Debug)]
23struct CounterState {
24    count: usize,
25}
26
27#[derive(Default, Empty, Area, Anchor, ZIndex)]
28struct FixedData {
29    area: DRect,
30    anchor: feather_ui::DPoint,
31    zindex: i32,
32}
33
34impl base::Padding for FixedData {}
35impl base::Limits for FixedData {}
36impl base::RLimits for FixedData {}
37impl fixed::Prop for FixedData {}
38impl fixed::Child for FixedData {}
39impl leaf::Prop for FixedData {}
40impl leaf::Padded for FixedData {}
41
42#[derive(Default, Empty, Area, Direction, RLimits, Padding)]
43struct GridData {
44    area: DRect,
45    direction: feather_ui::RowDirection,
46    rlimits: feather_ui::RelLimits,
47    rows: Vec<DValue>,
48    columns: Vec<DValue>,
49    spacing: feather_ui::DPoint,
50    padding: DAbsRect,
51}
52
53impl base::Anchor for GridData {}
54impl base::Limits for GridData {}
55impl fixed::Child for GridData {}
56
57impl grid::Prop for GridData {
58    fn rows(&self) -> &[DValue] {
59        &self.rows
60    }
61
62    fn columns(&self) -> &[DValue] {
63        &self.columns
64    }
65
66    fn spacing(&self) -> feather_ui::DPoint {
67        self.spacing
68    }
69}
70
71#[derive(Default, Empty, Area)]
72struct GridChild {
73    area: DRect,
74    x: usize,
75    y: usize,
76}
77
78impl base::Padding for GridChild {}
79impl base::Anchor for GridChild {}
80impl base::Limits for GridChild {}
81impl base::Margin for GridChild {}
82impl base::RLimits for GridChild {}
83impl base::Order for GridChild {}
84impl leaf::Prop for GridChild {}
85impl leaf::Padded for GridChild {}
86
87impl grid::Child for GridChild {
88    fn coord(&self) -> (usize, usize) {
89        (self.y, self.x)
90    }
91
92    fn span(&self) -> (usize, usize) {
93        (1, 1)
94    }
95}
96
97struct BasicApp {}
98
99impl FnPersistStore for BasicApp {
100    type Store = (CounterState, im::HashMap<Arc<SourceID>, Option<Window>>);
101}
102
103impl FnPersist2<CounterState, ScopeID<'_>, im::HashMap<Arc<SourceID>, Option<Window>>>
104    for BasicApp
105{
106    fn init(&self) -> Self::Store {
107        (CounterState { count: 99999999 }, im::HashMap::new())
108    }
109    fn call(
110        &mut self,
111        mut store: Self::Store,
112        args: CounterState,
113        mut scope: ScopeID<'_>,
114    ) -> (Self::Store, im::HashMap<Arc<SourceID>, Option<Window>>) {
115        if store.0 != args {
116            let button = {
117                let text = {
118                    Text::<FixedData> {
119                        id: scope.create(),
120                        props: FixedData {
121                            area: AbsRect::new(10.0, 15.0, 10.0, 15.0)
122                                + RelRect::new(0.0, 0.0, UNSIZED_AXIS, UNSIZED_AXIS),
123                            anchor: feather_ui::RelPoint::zero().into(),
124                            ..Default::default()
125                        }
126                        .into(),
127                        text: format!("Boxes: {}", args.count),
128                        font_size: 40.0,
129                        line_height: 56.0,
130                        ..Default::default()
131                    }
132                };
133
134                let rect = Shape::<DRect, { ShapeKind::RoundRect as u8 }>::new(
135                    scope.create(),
136                    feather_ui::FILL_DRECT,
137                    0.0,
138                    0.0,
139                    wide::f32x4::splat(10.0),
140                    sRGB::new(0.2, 0.7, 0.4, 1.0),
141                    sRGB::transparent(),
142                    DAbsPoint::zero(),
143                );
144
145                Button::<FixedData>::new(
146                    scope.create(),
147                    FixedData {
148                        area: AbsRect::new(0.0, 20.0, 0.0, 0.0)
149                            + RelRect::new(0.5, 0.0, UNSIZED_AXIS, UNSIZED_AXIS),
150                        anchor: feather_ui::RelPoint::new(0.5, 0.0).into(),
151                        zindex: 0,
152                    },
153                    Slot(feather_ui::APP_SOURCE_ID.into(), 0),
154                    feather_ui::children![fixed::Prop, rect, text],
155                )
156            };
157
158            const NUM_COLUMNS: usize = 5;
159            let rectgrid = {
160                let mut children: im::Vector<Option<Box<ChildOf<dyn grid::Prop>>>> =
161                    im::Vector::new();
162                {
163                    for (i, id) in scope.iter(0..args.count) {
164                        children.push_back(Some(Box::new(Shape::<
165                            GridChild,
166                            { ShapeKind::RoundRect as u8 },
167                        >::new(
168                            id,
169                            GridChild {
170                                area: FILL_DRECT,
171                                x: i % NUM_COLUMNS,
172                                y: i / NUM_COLUMNS,
173                            },
174                            0.0,
175                            0.0,
176                            wide::f32x4::splat(4.0),
177                            sRGB::new(
178                                (0.1 * i as f32) % 1.0,
179                                (0.65 * i as f32) % 1.0,
180                                (0.2 * i as f32) % 1.0,
181                                1.0,
182                            ),
183                            sRGB::transparent(),
184                            DAbsPoint::zero(),
185                        ))));
186                    }
187                }
188
189                GridBox::<GridData>::new(
190                    scope.create(),
191                    GridData {
192                        area: AbsRect::new(0.0, 200.0, 0.0, 0.0)
193                            + RelRect::new(0.0, 0.0, UNSIZED_AXIS, 1.0),
194
195                        rlimits: feather_ui::RelLimits::new(0.0..1.0, 0.0..),
196                        direction: feather_ui::RowDirection::LeftToRight,
197                        rows: [40.0, 20.0, 40.0, 20.0, 40.0, 20.0, 10.0]
198                            .map(DValue::from)
199                            .to_vec(),
200                        columns: [80.0, 40.0, 80.0, 40.0, 80.0].map(DValue::from).to_vec(),
201                        spacing: AbsPoint::new(4.0, 4.0).into(),
202                        padding: AbsRect::new(8.0, 8.0, 8.0, 8.0).into(),
203                    },
204                    children,
205                )
206            };
207
208            let region = Region::new(
209                scope.create(),
210                FixedData {
211                    area: FILL_DRECT,
212                    zindex: 0,
213                    ..Default::default()
214                },
215                feather_ui::children![fixed::Prop, button, rectgrid],
216            );
217            let window = Window::new(
218                scope.create(),
219                winit::window::Window::default_attributes()
220                    .with_title(env!("CARGO_CRATE_NAME"))
221                    .with_resizable(true),
222                Box::new(region),
223            );
224
225            store.1 = im::HashMap::new();
226            store.1.insert(window.id.clone(), Some(window));
227            store.0 = args.clone();
228        }
229        let windows = store.1.clone();
230        (store, windows)
231    }
232}
233
234use feather_ui::WrapEventEx;
235
236fn main() {
237    let onclick = Box::new(
238        |_: mouse_area::MouseAreaEvent,
239         mut appdata: feather_ui::AccessCell<CounterState>|
240         -> InputResult<()> {
241            {
242                appdata.count += 1;
243                InputResult::Consume(())
244            }
245        }
246        .wrap(),
247    );
248
249    let (mut app, event_loop, _, _) = App::<CounterState, BasicApp>::new::<()>(
250        CounterState { count: 0 },
251        vec![onclick],
252        BasicApp {},
253        |_| (),
254    )
255    .unwrap();
256
257    event_loop.run_app(&mut app).unwrap();
258}