iced_core/layout/
flex.rs

1//! Distribute elements using a flex-based layout.
2// This code is heavily inspired by the [`druid`] codebase.
3//
4// [`druid`]: https://github.com/xi-editor/druid
5//
6// Copyright 2018 The xi-editor Authors, Héctor Ramón
7//
8// Licensed under the Apache License, Version 2.0 (the "License");
9// you may not use this file except in compliance with the License.
10// You may obtain a copy of the License at
11//
12//     http://www.apache.org/licenses/LICENSE-2.0
13//
14// Unless required by applicable law or agreed to in writing, software
15// distributed under the License is distributed on an "AS IS" BASIS,
16// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17// See the License for the specific language governing permissions and
18// limitations under the License.
19use crate::Element;
20
21use crate::layout::{Limits, Node};
22use crate::widget;
23use crate::{Alignment, Length, Padding, Point, Size};
24
25/// The main axis of a flex layout.
26#[derive(Debug)]
27pub enum Axis {
28    /// The horizontal axis
29    Horizontal,
30
31    /// The vertical axis
32    Vertical,
33}
34
35impl Axis {
36    fn main(&self, size: Size) -> f32 {
37        match self {
38            Axis::Horizontal => size.width,
39            Axis::Vertical => size.height,
40        }
41    }
42
43    fn cross(&self, size: Size) -> f32 {
44        match self {
45            Axis::Horizontal => size.height,
46            Axis::Vertical => size.width,
47        }
48    }
49
50    fn pack<T>(&self, main: T, cross: T) -> (T, T) {
51        match self {
52            Axis::Horizontal => (main, cross),
53            Axis::Vertical => (cross, main),
54        }
55    }
56}
57
58/// Computes the flex layout with the given axis and limits, applying spacing,
59/// padding and alignment to the items as needed.
60///
61/// It returns a new layout [`Node`].
62pub fn resolve<Message, Theme, Renderer>(
63    axis: Axis,
64    renderer: &Renderer,
65    limits: &Limits,
66    width: Length,
67    height: Length,
68    padding: Padding,
69    spacing: f32,
70    align_items: Alignment,
71    items: &[Element<'_, Message, Theme, Renderer>],
72    trees: &mut [widget::Tree],
73) -> Node
74where
75    Renderer: crate::Renderer,
76{
77    let limits = limits.width(width).height(height).shrink(padding);
78    let total_spacing = spacing * items.len().saturating_sub(1) as f32;
79    let max_cross = axis.cross(limits.max());
80
81    let mut fill_main_sum = 0;
82    let mut some_fill_cross = false;
83    let (mut cross, cross_compress) = match axis {
84        Axis::Vertical if width == Length::Shrink => (0.0, true),
85        Axis::Horizontal if height == Length::Shrink => (0.0, true),
86        _ => (max_cross, false),
87    };
88
89    let mut available = axis.main(limits.max()) - total_spacing;
90
91    let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
92    nodes.resize(items.len(), Node::default());
93
94    // FIRST PASS
95    // We lay out non-fluid elements in the main axis.
96    // If we need to compress the cross axis, then we skip any of these elements
97    // that are also fluid in the cross axis.
98    for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() {
99        let (fill_main_factor, fill_cross_factor) = {
100            let size = child.as_widget().size();
101
102            axis.pack(size.width.fill_factor(), size.height.fill_factor())
103        };
104
105        if fill_main_factor == 0 && (!cross_compress || fill_cross_factor == 0)
106        {
107            let (max_width, max_height) = axis.pack(
108                available,
109                if fill_cross_factor == 0 {
110                    max_cross
111                } else {
112                    cross
113                },
114            );
115
116            let child_limits =
117                Limits::new(Size::ZERO, Size::new(max_width, max_height));
118
119            let layout =
120                child.as_widget().layout(tree, renderer, &child_limits);
121            let size = layout.size();
122
123            available -= axis.main(size);
124            cross = cross.max(axis.cross(size));
125
126            nodes[i] = layout;
127        } else {
128            fill_main_sum += fill_main_factor;
129            some_fill_cross = some_fill_cross || fill_cross_factor != 0;
130        }
131    }
132
133    // SECOND PASS (conditional)
134    // If we must compress the cross axis and there are fluid elements in the
135    // cross axis, we lay out any of these elements that are also non-fluid in
136    // the main axis (i.e. the ones we deliberately skipped in the first pass).
137    //
138    // We use the maximum cross length obtained in the first pass as the maximum
139    // cross limit.
140    //
141    // We can defer the layout of any elements that have a fixed size in the main axis,
142    // allowing them to use the cross calculations of the next pass.
143    if cross_compress && some_fill_cross {
144        for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate()
145        {
146            let (main_size, cross_size) = {
147                let size = child.as_widget().size();
148
149                axis.pack(size.width, size.height)
150            };
151
152            if main_size.fill_factor() == 0 && cross_size.fill_factor() != 0 {
153                if let Length::Fixed(main) = main_size {
154                    available -= main;
155                    continue;
156                }
157
158                let (max_width, max_height) = axis.pack(available, cross);
159
160                let child_limits =
161                    Limits::new(Size::ZERO, Size::new(max_width, max_height));
162
163                let layout =
164                    child.as_widget().layout(tree, renderer, &child_limits);
165                let size = layout.size();
166
167                available -= axis.main(size);
168                cross = cross.max(axis.cross(size));
169
170                nodes[i] = layout;
171            }
172        }
173    }
174
175    let remaining = match axis {
176        Axis::Horizontal => match width {
177            Length::Shrink => 0.0,
178            _ => available.max(0.0),
179        },
180        Axis::Vertical => match height {
181            Length::Shrink => 0.0,
182            _ => available.max(0.0),
183        },
184    };
185
186    // THIRD PASS
187    // We lay out the elements that are fluid in the main axis.
188    // We use the remaining space to evenly allocate space based on fill factors.
189    for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate() {
190        let (fill_main_factor, fill_cross_factor) = {
191            let size = child.as_widget().size();
192
193            axis.pack(size.width.fill_factor(), size.height.fill_factor())
194        };
195
196        if fill_main_factor != 0 {
197            let max_main =
198                remaining * fill_main_factor as f32 / fill_main_sum as f32;
199
200            let max_main = if max_main.is_nan() {
201                f32::INFINITY
202            } else {
203                max_main
204            };
205
206            let min_main = if max_main.is_infinite() {
207                0.0
208            } else {
209                max_main
210            };
211
212            let (min_width, min_height) = axis.pack(min_main, 0.0);
213            let (max_width, max_height) = axis.pack(
214                max_main,
215                if fill_cross_factor == 0 {
216                    max_cross
217                } else {
218                    cross
219                },
220            );
221
222            let child_limits = Limits::new(
223                Size::new(min_width, min_height),
224                Size::new(max_width, max_height),
225            );
226
227            let layout =
228                child.as_widget().layout(tree, renderer, &child_limits);
229            cross = cross.max(axis.cross(layout.size()));
230
231            nodes[i] = layout;
232        }
233    }
234
235    // FOURTH PASS (conditional)
236    // We lay out any elements that were deferred in the second pass.
237    // These are elements that must be compressed in their cross axis and have
238    // a fixed length in the main axis.
239    if cross_compress && some_fill_cross {
240        for (i, (child, tree)) in items.iter().zip(trees).enumerate() {
241            let (main_size, cross_size) = {
242                let size = child.as_widget().size();
243
244                axis.pack(size.width, size.height)
245            };
246
247            if cross_size.fill_factor() != 0 {
248                let Length::Fixed(main) = main_size else {
249                    continue;
250                };
251
252                let (max_width, max_height) = axis.pack(main, cross);
253
254                let child_limits =
255                    Limits::new(Size::ZERO, Size::new(max_width, max_height));
256
257                let layout =
258                    child.as_widget().layout(tree, renderer, &child_limits);
259                let size = layout.size();
260
261                cross = cross.max(axis.cross(size));
262
263                nodes[i] = layout;
264            }
265        }
266    }
267
268    let pad = axis.pack(padding.left, padding.top);
269    let mut main = pad.0;
270
271    // FIFTH PASS
272    // We align all the laid out nodes in the cross axis, if needed.
273    for (i, node) in nodes.iter_mut().enumerate() {
274        if i > 0 {
275            main += spacing;
276        }
277
278        let (x, y) = axis.pack(main, pad.1);
279
280        node.move_to_mut(Point::new(x, y));
281
282        match axis {
283            Axis::Horizontal => {
284                node.align_mut(
285                    Alignment::Start,
286                    align_items,
287                    Size::new(0.0, cross),
288                );
289            }
290            Axis::Vertical => {
291                node.align_mut(
292                    align_items,
293                    Alignment::Start,
294                    Size::new(cross, 0.0),
295                );
296            }
297        }
298
299        let size = node.size();
300
301        main += axis.main(size);
302    }
303
304    let (intrinsic_width, intrinsic_height) = axis.pack(main - pad.0, cross);
305    let size = limits.resolve(
306        width,
307        height,
308        Size::new(intrinsic_width, intrinsic_height),
309    );
310
311    Node::with_children(size.expand(padding), nodes)
312}