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