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