iced_widget/
grid.rs

1//! Distribute content on a grid.
2use crate::core::layout::{self, Layout};
3use crate::core::mouse;
4use crate::core::overlay;
5use crate::core::renderer;
6use crate::core::widget::{Operation, Tree};
7use crate::core::{
8    Clipboard, Element, Event, Length, Pixels, Rectangle, Shell, Size, Vector,
9    Widget,
10};
11
12/// A container that distributes its contents on a responsive grid.
13pub struct Grid<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer> {
14    spacing: f32,
15    columns: Constraint,
16    width: Option<Pixels>,
17    height: Sizing,
18    children: Vec<Element<'a, Message, Theme, Renderer>>,
19}
20
21enum Constraint {
22    MaxWidth(Pixels),
23    Amount(usize),
24}
25
26impl<'a, Message, Theme, Renderer> Grid<'a, Message, Theme, Renderer>
27where
28    Renderer: crate::core::Renderer,
29{
30    /// Creates an empty [`Grid`].
31    pub fn new() -> Self {
32        Self::from_vec(Vec::new())
33    }
34
35    /// Creates a [`Grid`] with the given capacity.
36    pub fn with_capacity(capacity: usize) -> Self {
37        Self::from_vec(Vec::with_capacity(capacity))
38    }
39
40    /// Creates a [`Grid`] with the given elements.
41    pub fn with_children(
42        children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
43    ) -> Self {
44        let iterator = children.into_iter();
45
46        Self::with_capacity(iterator.size_hint().0).extend(iterator)
47    }
48
49    /// Creates a [`Grid`] from an already allocated [`Vec`].
50    pub fn from_vec(
51        children: Vec<Element<'a, Message, Theme, Renderer>>,
52    ) -> Self {
53        Self {
54            spacing: 0.0,
55            columns: Constraint::Amount(3),
56            width: None,
57            height: Sizing::AspectRatio(1.0),
58            children,
59        }
60    }
61
62    /// Sets the spacing _between_ cells in the [`Grid`].
63    pub fn spacing(mut self, amount: impl Into<Pixels>) -> Self {
64        self.spacing = amount.into().0;
65        self
66    }
67
68    /// Sets the width of the [`Grid`] in [`Pixels`].
69    ///
70    /// By default, a [`Grid`] will [`Fill`] its parent.
71    ///
72    /// [`Fill`]: Length::Fill
73    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
74        self.width = Some(width.into());
75        self
76    }
77
78    /// Sets the height of the [`Grid`].
79    ///
80    /// By default, a [`Grid`] uses a cell aspect ratio of `1.0` (i.e. squares).
81    pub fn height(mut self, height: impl Into<Sizing>) -> Self {
82        self.height = height.into();
83        self
84    }
85
86    /// Sets the amount of columns in the [`Grid`].
87    pub fn columns(mut self, column: usize) -> Self {
88        self.columns = Constraint::Amount(column);
89        self
90    }
91
92    /// Makes the amount of columns dynamic in the [`Grid`], never
93    /// exceeding the provided `max_width`.
94    pub fn fluid(mut self, max_width: impl Into<Pixels>) -> Self {
95        self.columns = Constraint::MaxWidth(max_width.into());
96        self
97    }
98
99    /// Adds an [`Element`] to the [`Grid`].
100    pub fn push(
101        mut self,
102        child: impl Into<Element<'a, Message, Theme, Renderer>>,
103    ) -> Self {
104        self.children.push(child.into());
105        self
106    }
107
108    /// Adds an element to the [`Grid`], if `Some`.
109    pub fn push_maybe(
110        self,
111        child: Option<impl Into<Element<'a, Message, Theme, Renderer>>>,
112    ) -> Self {
113        if let Some(child) = child {
114            self.push(child)
115        } else {
116            self
117        }
118    }
119
120    /// Extends the [`Grid`] with the given children.
121    pub fn extend(
122        self,
123        children: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
124    ) -> Self {
125        children.into_iter().fold(self, Self::push)
126    }
127}
128
129impl<Message, Renderer> Default for Grid<'_, Message, Renderer>
130where
131    Renderer: crate::core::Renderer,
132{
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl<'a, Message, Theme, Renderer: crate::core::Renderer>
139    FromIterator<Element<'a, Message, Theme, Renderer>>
140    for Grid<'a, Message, Theme, Renderer>
141{
142    fn from_iter<
143        T: IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
144    >(
145        iter: T,
146    ) -> Self {
147        Self::with_children(iter)
148    }
149}
150
151impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
152    for Grid<'_, Message, Theme, Renderer>
153where
154    Renderer: crate::core::Renderer,
155{
156    fn children(&self) -> Vec<Tree> {
157        self.children.iter().map(Tree::new).collect()
158    }
159
160    fn diff(&self, tree: &mut Tree) {
161        tree.diff_children(&self.children);
162    }
163
164    fn size(&self) -> Size<Length> {
165        Size {
166            width: self
167                .width
168                .map(|pixels| Length::Fixed(pixels.0))
169                .unwrap_or(Length::Fill),
170            height: match self.height {
171                Sizing::AspectRatio(_) => Length::Shrink,
172                Sizing::EvenlyDistribute(length) => length,
173            },
174        }
175    }
176
177    fn layout(
178        &mut self,
179        tree: &mut Tree,
180        renderer: &Renderer,
181        limits: &layout::Limits,
182    ) -> layout::Node {
183        let size = self.size();
184        let limits = limits.width(size.width).height(size.height);
185        let available = limits.max();
186
187        let cells_per_row = match self.columns {
188            // width = n * (cell + spacing) - spacing, given n > 0
189            Constraint::MaxWidth(pixels) => ((available.width + self.spacing)
190                / (pixels.0 + self.spacing))
191                .ceil() as usize,
192            Constraint::Amount(amount) => amount,
193        };
194
195        let cell_width = (available.width
196            - self.spacing * (cells_per_row - 1) as f32)
197            / cells_per_row as f32;
198
199        let cell_height = match self.height {
200            Sizing::AspectRatio(ratio) => Some(cell_width / ratio),
201            Sizing::EvenlyDistribute(Length::Shrink) => None,
202            Sizing::EvenlyDistribute(_) => {
203                let total_rows = self.children.len().div_ceil(cells_per_row);
204                Some(
205                    (available.height - self.spacing * (total_rows - 1) as f32)
206                        / total_rows as f32,
207                )
208            }
209        };
210
211        let cell_limits = layout::Limits::new(
212            Size::new(cell_width, cell_height.unwrap_or(0.0)),
213            Size::new(cell_width, cell_height.unwrap_or(available.height)),
214        );
215
216        let mut nodes = Vec::with_capacity(self.children.len());
217        let mut x = 0.0;
218        let mut y = 0.0;
219        let mut row_height = 0.0f32;
220
221        for (i, (child, tree)) in
222            self.children.iter_mut().zip(&mut tree.children).enumerate()
223        {
224            let node = child
225                .as_widget_mut()
226                .layout(tree, renderer, &cell_limits)
227                .move_to((x, y));
228
229            let size = node.size();
230
231            x += size.width + self.spacing;
232            row_height = row_height.max(size.height);
233
234            if (i + 1) % cells_per_row == 0 {
235                y += cell_height.unwrap_or(row_height) + self.spacing;
236                x = 0.0;
237                row_height = 0.0;
238            }
239
240            nodes.push(node);
241        }
242
243        if x == 0.0 {
244            y -= self.spacing;
245        } else {
246            y += cell_height.unwrap_or(row_height);
247        }
248
249        layout::Node::with_children(Size::new(available.width, y), nodes)
250    }
251
252    fn operate(
253        &mut self,
254        tree: &mut Tree,
255        layout: Layout<'_>,
256        renderer: &Renderer,
257        operation: &mut dyn Operation,
258    ) {
259        operation.container(None, layout.bounds());
260        operation.traverse(&mut |operation| {
261            self.children
262                .iter_mut()
263                .zip(&mut tree.children)
264                .zip(layout.children())
265                .for_each(|((child, state), layout)| {
266                    child
267                        .as_widget_mut()
268                        .operate(state, layout, renderer, operation);
269                });
270        });
271    }
272
273    fn update(
274        &mut self,
275        tree: &mut Tree,
276        event: &Event,
277        layout: Layout<'_>,
278        cursor: mouse::Cursor,
279        renderer: &Renderer,
280        clipboard: &mut dyn Clipboard,
281        shell: &mut Shell<'_, Message>,
282        viewport: &Rectangle,
283    ) {
284        for ((child, state), layout) in self
285            .children
286            .iter_mut()
287            .zip(&mut tree.children)
288            .zip(layout.children())
289        {
290            child.as_widget_mut().update(
291                state, event, layout, cursor, renderer, clipboard, shell,
292                viewport,
293            );
294        }
295    }
296
297    fn mouse_interaction(
298        &self,
299        tree: &Tree,
300        layout: Layout<'_>,
301        cursor: mouse::Cursor,
302        viewport: &Rectangle,
303        renderer: &Renderer,
304    ) -> mouse::Interaction {
305        self.children
306            .iter()
307            .zip(&tree.children)
308            .zip(layout.children())
309            .map(|((child, state), layout)| {
310                child.as_widget().mouse_interaction(
311                    state, layout, cursor, viewport, renderer,
312                )
313            })
314            .max()
315            .unwrap_or_default()
316    }
317
318    fn draw(
319        &self,
320        tree: &Tree,
321        renderer: &mut Renderer,
322        theme: &Theme,
323        style: &renderer::Style,
324        layout: Layout<'_>,
325        cursor: mouse::Cursor,
326        viewport: &Rectangle,
327    ) {
328        if let Some(viewport) = layout.bounds().intersection(viewport) {
329            for ((child, state), layout) in self
330                .children
331                .iter()
332                .zip(&tree.children)
333                .zip(layout.children())
334                .filter(|(_, layout)| layout.bounds().intersects(&viewport))
335            {
336                child.as_widget().draw(
337                    state, renderer, theme, style, layout, cursor, &viewport,
338                );
339            }
340        }
341    }
342
343    fn overlay<'b>(
344        &'b mut self,
345        tree: &'b mut Tree,
346        layout: Layout<'b>,
347        renderer: &Renderer,
348        viewport: &Rectangle,
349        translation: Vector,
350    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
351        overlay::from_children(
352            &mut self.children,
353            tree,
354            layout,
355            renderer,
356            viewport,
357            translation,
358        )
359    }
360}
361
362impl<'a, Message, Theme, Renderer> From<Grid<'a, Message, Theme, Renderer>>
363    for Element<'a, Message, Theme, Renderer>
364where
365    Message: 'a,
366    Theme: 'a,
367    Renderer: crate::core::Renderer + 'a,
368{
369    fn from(row: Grid<'a, Message, Theme, Renderer>) -> Self {
370        Self::new(row)
371    }
372}
373
374/// The sizing strategy of a [`Grid`].
375#[derive(Debug, Clone, Copy, PartialEq)]
376pub enum Sizing {
377    /// The [`Grid`] will ensure each cell follows the given aspect ratio and the
378    /// total size will be the sum of the cells and the spacing between them.
379    ///
380    /// The ratio is the amount of horizontal pixels per each vertical pixel of a cell
381    /// in the [`Grid`].
382    AspectRatio(f32),
383
384    /// The [`Grid`] will evenly distribute the space available in the given [`Length`]
385    /// for each cell.
386    EvenlyDistribute(Length),
387}
388
389impl From<f32> for Sizing {
390    fn from(height: f32) -> Self {
391        Self::EvenlyDistribute(Length::from(height))
392    }
393}
394
395impl From<Length> for Sizing {
396    fn from(height: Length) -> Self {
397        Self::EvenlyDistribute(height)
398    }
399}
400
401/// Creates a new [`Sizing`] strategy that maintains the given aspect ratio.
402pub fn aspect_ratio(
403    width: impl Into<Pixels>,
404    height: impl Into<Pixels>,
405) -> Sizing {
406    Sizing::AspectRatio(width.into().0 / height.into().0)
407}