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: &mut [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 (main_compress, cross_compress) = {
82        let compression = limits.compression();
83        axis.pack(compression.width, compression.height)
84    };
85
86    let compression = {
87        let (compress_x, compress_y) = axis.pack(main_compress, false);
88        Size::new(compress_x, compress_y)
89    };
90
91    let mut fill_main_sum = 0;
92    let mut some_fill_cross = false;
93    let mut cross = if cross_compress { 0.0 } else { max_cross };
94    let mut available = axis.main(limits.max()) - total_spacing;
95
96    let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
97    nodes.resize(items.len(), Node::default());
98
99    // FIRST PASS
100    // We lay out non-fluid elements in the main axis.
101    // If we need to compress the cross axis, then we skip any of these elements
102    // that are also fluid in the cross axis.
103    for (i, (child, tree)) in items.iter_mut().zip(trees.iter_mut()).enumerate() {
104        let (fill_main_factor, fill_cross_factor) = {
105            let size = child.as_widget().size();
106
107            axis.pack(size.width.fill_factor(), size.height.fill_factor())
108        };
109
110        if (main_compress || fill_main_factor == 0) && (!cross_compress || fill_cross_factor == 0) {
111            let (max_width, max_height) = axis.pack(
112                available,
113                if fill_cross_factor == 0 {
114                    max_cross
115                } else {
116                    cross
117                },
118            );
119
120            let child_limits =
121                Limits::with_compression(Size::ZERO, Size::new(max_width, max_height), compression);
122
123            let layout = child.as_widget_mut().layout(tree, renderer, &child_limits);
124            let size = layout.size();
125
126            available -= axis.main(size);
127            cross = cross.max(axis.cross(size));
128
129            nodes[i] = layout;
130        } else {
131            fill_main_sum += fill_main_factor;
132            some_fill_cross = some_fill_cross || fill_cross_factor != 0;
133        }
134    }
135
136    // SECOND PASS (conditional)
137    // If we must compress the cross axis and there are fluid elements in the
138    // cross axis, we lay out any of these elements that are also non-fluid in
139    // the main axis (i.e. the ones we deliberately skipped in the first pass).
140    //
141    // We use the maximum cross length obtained in the first pass as the maximum
142    // cross limit.
143    //
144    // We can defer the layout of any elements that have a fixed size in the main axis,
145    // allowing them to use the cross calculations of the next pass.
146    if cross_compress && some_fill_cross {
147        for (i, (child, tree)) in items.iter_mut().zip(trees.iter_mut()).enumerate() {
148            let (main_size, cross_size) = {
149                let size = child.as_widget().size();
150
151                axis.pack(size.width, size.height)
152            };
153
154            if (main_compress || main_size.fill_factor() == 0) && cross_size.fill_factor() != 0 {
155                if let Length::Fixed(main) = main_size {
156                    available -= main;
157                    continue;
158                }
159
160                let (max_width, max_height) = axis.pack(available, cross);
161
162                let child_limits = Limits::with_compression(
163                    Size::ZERO,
164                    Size::new(max_width, max_height),
165                    compression,
166                );
167
168                let layout = child.as_widget_mut().layout(tree, renderer, &child_limits);
169                let size = layout.size();
170
171                available -= axis.main(size);
172                cross = cross.max(axis.cross(size));
173
174                nodes[i] = layout;
175            }
176        }
177    }
178
179    let remaining = available.max(0.0);
180
181    // THIRD PASS (conditional)
182    // We lay out the elements that are fluid in the main axis.
183    // We use the remaining space to evenly allocate space based on fill factors.
184    if !main_compress {
185        for (i, (child, tree)) in items.iter_mut().zip(trees.iter_mut()).enumerate() {
186            let (fill_main_factor, fill_cross_factor) = {
187                let size = child.as_widget().size();
188
189                axis.pack(size.width.fill_factor(), size.height.fill_factor())
190            };
191
192            if fill_main_factor != 0 {
193                let max_main = remaining * fill_main_factor as f32 / fill_main_sum as f32;
194
195                let max_main = if max_main.is_nan() {
196                    f32::INFINITY
197                } else {
198                    max_main
199                };
200
201                let min_main = if max_main.is_infinite() {
202                    0.0
203                } else {
204                    max_main
205                };
206
207                let (min_width, min_height) = axis.pack(min_main, 0.0);
208                let (max_width, max_height) = axis.pack(
209                    max_main,
210                    if fill_cross_factor == 0 {
211                        max_cross
212                    } else {
213                        cross
214                    },
215                );
216
217                let child_limits = Limits::with_compression(
218                    Size::new(min_width, min_height),
219                    Size::new(max_width, max_height),
220                    compression,
221                );
222
223                let layout = child.as_widget_mut().layout(tree, renderer, &child_limits);
224                cross = cross.max(axis.cross(layout.size()));
225
226                nodes[i] = layout;
227            }
228        }
229    }
230
231    // FOURTH PASS (conditional)
232    // We lay out any elements that were deferred in the second pass.
233    // These are elements that must be compressed in their cross axis and have
234    // a fixed length in the main axis.
235    if cross_compress && some_fill_cross {
236        for (i, (child, tree)) in items.iter_mut().zip(trees).enumerate() {
237            let (main_size, cross_size) = {
238                let size = child.as_widget().size();
239
240                axis.pack(size.width, size.height)
241            };
242
243            if cross_size.fill_factor() != 0 {
244                let Length::Fixed(main) = main_size else {
245                    continue;
246                };
247
248                let (max_width, max_height) = axis.pack(main, cross);
249
250                let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height));
251
252                let layout = child.as_widget_mut().layout(tree, renderer, &child_limits);
253                let size = layout.size();
254
255                cross = cross.max(axis.cross(size));
256
257                nodes[i] = layout;
258            }
259        }
260    }
261
262    let pad = axis.pack(padding.left, padding.top);
263    let mut main = pad.0;
264
265    // FIFTH PASS
266    // We align all the laid out nodes in the cross axis, if needed.
267    for (i, node) in nodes.iter_mut().enumerate() {
268        if i > 0 {
269            main += spacing;
270        }
271
272        let (x, y) = axis.pack(main, pad.1);
273
274        node.move_to_mut(Point::new(x, y));
275
276        match axis {
277            Axis::Horizontal => {
278                node.align_mut(Alignment::Start, align_items, Size::new(0.0, cross));
279            }
280            Axis::Vertical => {
281                node.align_mut(align_items, Alignment::Start, Size::new(cross, 0.0));
282            }
283        }
284
285        let size = node.size();
286
287        main += axis.main(size);
288    }
289
290    let (intrinsic_width, intrinsic_height) = axis.pack(main - pad.0, cross);
291    let size = limits.resolve(width, height, Size::new(intrinsic_width, intrinsic_height));
292
293    Node::with_children(size.expand(padding), nodes)
294}