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    if cross_compress && some_fill_cross {
141        for (i, (child, tree)) in items.iter().zip(trees.iter_mut()).enumerate()
142        {
143            let (fill_main_factor, fill_cross_factor) = {
144                let size = child.as_widget().size();
145
146                axis.pack(size.width.fill_factor(), size.height.fill_factor())
147            };
148
149            if fill_main_factor == 0 && fill_cross_factor != 0 {
150                let (max_width, max_height) = axis.pack(available, cross);
151
152                let child_limits =
153                    Limits::new(Size::ZERO, Size::new(max_width, max_height));
154
155                let layout =
156                    child.as_widget().layout(tree, renderer, &child_limits);
157                let size = layout.size();
158
159                available -= axis.main(size);
160                cross = cross.max(axis.cross(size));
161
162                nodes[i] = layout;
163            }
164        }
165    }
166
167    let remaining = match axis {
168        Axis::Horizontal => match width {
169            Length::Shrink => 0.0,
170            _ => available.max(0.0),
171        },
172        Axis::Vertical => match height {
173            Length::Shrink => 0.0,
174            _ => available.max(0.0),
175        },
176    };
177
178    // THIRD PASS
179    // We only have the elements that are fluid in the main axis left.
180    // We use the remaining space to evenly allocate space based on fill factors.
181    for (i, (child, tree)) in items.iter().zip(trees).enumerate() {
182        let (fill_main_factor, fill_cross_factor) = {
183            let size = child.as_widget().size();
184
185            axis.pack(size.width.fill_factor(), size.height.fill_factor())
186        };
187
188        if fill_main_factor != 0 {
189            let max_main =
190                remaining * fill_main_factor as f32 / fill_main_sum as f32;
191
192            let max_main = if max_main.is_nan() {
193                f32::INFINITY
194            } else {
195                max_main
196            };
197
198            let min_main = if max_main.is_infinite() {
199                0.0
200            } else {
201                max_main
202            };
203
204            let (min_width, min_height) = axis.pack(min_main, 0.0);
205            let (max_width, max_height) = axis.pack(
206                max_main,
207                if fill_cross_factor == 0 {
208                    max_cross
209                } else {
210                    cross
211                },
212            );
213
214            let child_limits = Limits::new(
215                Size::new(min_width, min_height),
216                Size::new(max_width, max_height),
217            );
218
219            let layout =
220                child.as_widget().layout(tree, renderer, &child_limits);
221            cross = cross.max(axis.cross(layout.size()));
222
223            nodes[i] = layout;
224        }
225    }
226
227    let pad = axis.pack(padding.left, padding.top);
228    let mut main = pad.0;
229
230    // FOURTH PASS
231    // We align all the laid out nodes in the cross axis, if needed.
232    for (i, node) in nodes.iter_mut().enumerate() {
233        if i > 0 {
234            main += spacing;
235        }
236
237        let (x, y) = axis.pack(main, pad.1);
238
239        node.move_to_mut(Point::new(x, y));
240
241        match axis {
242            Axis::Horizontal => {
243                node.align_mut(
244                    Alignment::Start,
245                    align_items,
246                    Size::new(0.0, cross),
247                );
248            }
249            Axis::Vertical => {
250                node.align_mut(
251                    align_items,
252                    Alignment::Start,
253                    Size::new(cross, 0.0),
254                );
255            }
256        }
257
258        let size = node.size();
259
260        main += axis.main(size);
261    }
262
263    let (intrinsic_width, intrinsic_height) = axis.pack(main - pad.0, cross);
264    let size = limits.resolve(
265        width,
266        height,
267        Size::new(intrinsic_width, intrinsic_height),
268    );
269
270    Node::with_children(size.expand(padding), nodes)
271}