Skip to main content

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::length;
23use crate::widget;
24use crate::{Alignment, Length, Padding, Size};
25
26/// The main axis of a flex layout.
27#[derive(Debug)]
28pub enum Axis {
29    /// The horizontal axis
30    Horizontal,
31
32    /// The vertical axis
33    Vertical,
34}
35
36impl Axis {
37    fn main(&self, size: Size) -> f32 {
38        match self {
39            Axis::Horizontal => size.width,
40            Axis::Vertical => size.height,
41        }
42    }
43
44    fn cross(&self, size: Size) -> f32 {
45        match self {
46            Axis::Horizontal => size.height,
47            Axis::Vertical => size.width,
48        }
49    }
50
51    fn pack<T>(&self, main: T, cross: T) -> (T, T) {
52        match self {
53            Axis::Horizontal => (main, cross),
54            Axis::Vertical => (cross, main),
55        }
56    }
57}
58
59/// Computes the flex layout with the given axis and limits, applying spacing,
60/// padding and alignment to the items as needed.
61///
62/// It returns a new layout [`Node`].
63pub fn resolve<Message, Theme, Renderer>(
64    axis: Axis,
65    renderer: &Renderer,
66    limits: &Limits,
67    width: Length,
68    height: Length,
69    padding: Padding,
70    spacing: f32,
71    align_items: Alignment,
72    items: &mut [Element<'_, Message, Theme, Renderer>],
73    trees: &mut [widget::Tree],
74) -> Node
75where
76    Renderer: crate::Renderer,
77{
78    let limits = limits.width(width).height(height).shrink(padding);
79    let total_spacing = spacing * items.len().saturating_sub(1) as f32;
80    let max_cross = axis.cross(limits.max());
81
82    let (main_compress, cross_compress) = {
83        let compression = limits.compression();
84        axis.pack(compression.width, compression.height)
85    };
86
87    let compression = {
88        let (compress_x, compress_y) = axis.pack(main_compress, false);
89        Size::new(compress_x, compress_y)
90    };
91
92    let mut fill_main_sum = 0;
93    let mut some_fill_cross = false;
94    let mut some_fill_max = false;
95    let mut some_fill_min = false;
96    let mut min_total = 0.0;
97    let mut min_factors = 0;
98    let mut cross = 0.0;
99    let mut available = axis.main(limits.max()) - total_spacing;
100
101    let mut nodes: Vec<Node> = Vec::with_capacity(items.len());
102    nodes.resize(items.len(), Node::default());
103
104    #[derive(Debug, Clone, Copy)]
105    struct Meta {
106        main: Length,
107        cross: Length,
108        category: Category,
109        resolved: bool,
110    }
111
112    #[derive(Debug, Clone, Copy)]
113    enum Category {
114        Static,
115        CrossFluid,
116        CrossFluidDeferred(f32),
117        MainFluid,
118    }
119
120    let mut metas = Vec::with_capacity(items.len());
121
122    // STATIC PASS
123    // We lay out non-fluid elements in the main axis.
124    // If we need to compress the cross axis, then we skip any of these elements
125    // that are also fluid in the cross axis.
126    for (i, child) in items.iter_mut().enumerate() {
127        let size = child.as_widget().size();
128        let (size_main, size_cross) = axis.pack(size.width, size.height);
129
130        let fill_main_factor = size_main.fill_factor();
131        let fill_cross_factor = size_cross.fill_factor();
132        let main_is_static = main_compress || fill_main_factor == 0;
133
134        let category = match (main_is_static, cross_compress, fill_cross_factor == 0) {
135            (true, false, _) | (true, _, true) => Category::Static,
136            (true, true, false) => {
137                if let Length::Fixed(main) = size_main {
138                    available -= main;
139                    Category::CrossFluidDeferred(main)
140                } else {
141                    Category::CrossFluid
142                }
143            }
144            (false, _, _) => Category::MainFluid,
145        };
146
147        let meta = Meta {
148            main: size_main,
149            cross: size_cross,
150            resolved: false,
151            category,
152        };
153
154        metas.push(meta);
155
156        match meta.main {
157            Length::Bounded {
158                sizing: length::Sizing::Fill(_),
159                bounds: length::Bounds::Min(min),
160            }
161            | Length::Fluid(length::Constraint::Min(min)) => {
162                min_total += min;
163                min_factors += fill_main_factor;
164                some_fill_min = true;
165            }
166            Length::Bounded {
167                sizing: length::Sizing::Fill(_),
168                bounds: length::Bounds::Max(_),
169            }
170            | Length::Fluid(length::Constraint::Max) => {
171                some_fill_max = true;
172            }
173            Length::Bounded {
174                sizing: length::Sizing::Fill(_),
175                bounds: length::Bounds::Both { .. },
176            } => {
177                some_fill_max = true;
178                some_fill_min = true;
179            }
180            _ => {}
181        }
182
183        let Category::Static = meta.category else {
184            fill_main_sum += fill_main_factor;
185            some_fill_cross = some_fill_cross || fill_cross_factor != 0;
186            continue;
187        };
188
189        let (max_width, max_height) = axis.pack(
190            available,
191            if !cross_compress || fill_cross_factor == 0 {
192                max_cross
193            } else {
194                cross
195            },
196        );
197
198        let child_limits =
199            Limits::with_compression(Size::ZERO, Size::new(max_width, max_height), compression);
200
201        let layout = child
202            .as_widget_mut()
203            .layout(&mut trees[i], renderer, &child_limits);
204
205        let size = layout.size();
206
207        available -= axis.main(size);
208        cross = cross.max(axis.cross(size));
209        nodes[i] = layout;
210    }
211
212    // CROSS FLUID PASS
213    // If we must compress the cross axis and there are fluid elements in the
214    // cross axis, we lay out any of these elements that are also non-fluid in
215    // the main axis (i.e. the ones we deliberately skipped in the first pass).
216    //
217    // We use the maximum cross length obtained in the first pass as the maximum
218    // cross limit.
219    //
220    // We can defer the layout of any elements that have a fixed size in the main axis,
221    // allowing them to use the cross calculations of the next pass.
222    if cross_compress && some_fill_cross {
223        for (i, child) in items.iter_mut().enumerate() {
224            let meta = metas[i];
225
226            let Category::CrossFluid = meta.category else {
227                continue;
228            };
229
230            let (max_width, max_height) =
231                axis.pack(available, if cross_compress { cross } else { max_cross });
232
233            let child_limits =
234                Limits::with_compression(Size::ZERO, Size::new(max_width, max_height), compression);
235
236            let layout = child
237                .as_widget_mut()
238                .layout(&mut trees[i], renderer, &child_limits);
239
240            let size = layout.size();
241
242            available -= axis.main(size);
243            cross = cross.max(axis.cross(size));
244            nodes[i] = layout;
245        }
246    }
247
248    let mut remaining = available.max(0.0);
249
250    // MIN / MAX PASSES
251    // We lay out any elements that are either bounded or fluid with some min / max constraints.
252    //
253    // First, we lay out any elements with `Max` bounds to potentially free up space; then
254    // another pass lays out elements with `Min` bounds.
255    //
256    // Since the remaining space may change mid-iteration, we need to revisit previous elements
257    // until they stabilize. Still, `layout` is only called once per element.
258    #[derive(Debug, Clone, Copy)]
259    enum Stage {
260        Max,
261        Min,
262    }
263
264    let mut step = if main_compress {
265        None
266    } else if some_fill_max {
267        Some(Stage::Max)
268    } else if some_fill_min {
269        Some(Stage::Min)
270    } else {
271        None
272    };
273
274    while let Some(stage) = step {
275        let current = remaining;
276        let (reserved_space, reserved_factors) = match stage {
277            Stage::Max => (min_total, min_factors),
278            Stage::Min => (0.0, 0),
279        };
280
281        for (i, child) in items.iter_mut().enumerate() {
282            let meta = &mut metas[i];
283
284            if meta.resolved {
285                continue;
286            }
287
288            let fill_main_factor = meta.main.fill_factor();
289
290            if fill_main_factor == 0 {
291                continue;
292            }
293
294            let bounds = match stage {
295                Stage::Max => match meta.main {
296                    Length::Bounded {
297                        bounds: bounds @ (length::Bounds::Max(_) | length::Bounds::Both { .. }),
298                        ..
299                    } => bounds,
300                    Length::Fluid(length::Constraint::Max) => length::Bounds::Min(0.0),
301                    _ => continue,
302                },
303                Stage::Min => match meta.main {
304                    Length::Bounded {
305                        bounds: bounds @ (length::Bounds::Min(_) | length::Bounds::Both { .. }),
306                        ..
307                    } => bounds,
308                    Length::Fluid(length::Constraint::Min(min)) => length::Bounds::Min(min),
309                    _ => continue,
310                },
311            };
312
313            let max_available = (remaining - reserved_space) * fill_main_factor as f32
314                / (fill_main_sum - reserved_factors) as f32;
315
316            let max_available = if max_available.is_nan() {
317                f32::INFINITY
318            } else {
319                max_available
320            };
321
322            let (min, max) = match bounds {
323                length::Bounds::Max(max) => (0.0, max),
324                length::Bounds::Both { min, max } => (min, max),
325                length::Bounds::Min(min) => (min, max_available),
326            };
327
328            match stage {
329                Stage::Max if max > max_available => continue,
330                Stage::Min if min < max_available => continue,
331                _ => {}
332            }
333
334            let min = min.min(remaining);
335            let max = max.min(max_available).max(min);
336
337            let (min_width, min_height) = axis.pack(min, 0.0);
338            let (max_width, max_height) = axis.pack(
339                max,
340                if !cross_compress || meta.cross.fill_factor() == 0 {
341                    max_cross
342                } else {
343                    cross
344                },
345            );
346
347            let child_limits = Limits::with_compression(
348                Size::new(min_width, min_height),
349                Size::new(max_width, max_height),
350                compression,
351            );
352
353            let layout = child
354                .as_widget_mut()
355                .layout(&mut trees[i], renderer, &child_limits);
356
357            cross = cross.max(axis.cross(layout.size()));
358            remaining -= axis.main(layout.size());
359            fill_main_sum -= fill_main_factor;
360            nodes[i] = layout;
361            meta.resolved = true;
362        }
363
364        if remaining == current {
365            step = match stage {
366                Stage::Max if some_fill_min => Some(Stage::Min),
367                _ => None,
368            };
369        }
370    }
371
372    // MAIN FLUID PASS (conditional)
373    // We lay out the elements that are fluid in the main axis.
374    // We use the remaining space to evenly allocate space based on fill factors.
375    if !main_compress {
376        for (i, child) in items.iter_mut().enumerate() {
377            let meta = &mut metas[i];
378
379            if meta.resolved {
380                continue;
381            }
382
383            let Category::MainFluid = meta.category else {
384                continue;
385            };
386
387            let max_main = remaining * meta.main.fill_factor() as f32 / fill_main_sum as f32;
388
389            let max_main = if max_main.is_nan() {
390                f32::INFINITY
391            } else {
392                max_main
393            };
394
395            let min_main = if max_main.is_infinite() {
396                0.0
397            } else {
398                max_main
399            };
400
401            let (min_width, min_height) = axis.pack(min_main, 0.0);
402            let (max_width, max_height) = axis.pack(
403                max_main,
404                if !cross_compress || meta.cross.fill_factor() == 0 {
405                    max_cross
406                } else {
407                    cross
408                },
409            );
410
411            let child_limits = Limits::with_compression(
412                Size::new(min_width, min_height),
413                Size::new(max_width, max_height),
414                compression,
415            );
416
417            let layout = child
418                .as_widget_mut()
419                .layout(&mut trees[i], renderer, &child_limits);
420
421            cross = cross.max(axis.cross(layout.size()));
422            nodes[i] = layout;
423        }
424    }
425
426    // CROSS FLUID DEFERRED PASS (conditional)
427    // We lay out any elements that were deferred in the second pass.
428    // These are elements that must be compressed in their cross axis and have
429    // a fixed length in the main axis.
430    if cross_compress && some_fill_cross {
431        for (i, child) in items.iter_mut().enumerate() {
432            let meta = metas[i];
433
434            let Category::CrossFluidDeferred(main) = meta.category else {
435                continue;
436            };
437
438            let (max_width, max_height) = axis.pack(main, cross);
439            let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height));
440
441            let layout = child
442                .as_widget_mut()
443                .layout(&mut trees[i], renderer, &child_limits);
444
445            let size = layout.size();
446
447            cross = cross.max(axis.cross(size));
448            nodes[i] = layout;
449        }
450    }
451
452    let pad = axis.pack(padding.left, padding.top);
453    let mut main = pad.0;
454
455    let cross = match axis {
456        Axis::Horizontal => limits.resolve_height(height, cross),
457        Axis::Vertical => limits.resolve_width(width, cross),
458    };
459
460    // ALIGNMENT PASS
461    // We align all the laid out nodes in the cross axis, if needed.
462    for (i, node) in nodes.iter_mut().enumerate() {
463        if i > 0 {
464            main += spacing;
465        }
466
467        node.move_to_mut(axis.pack(main, pad.1));
468
469        match axis {
470            Axis::Horizontal => {
471                node.align_mut(Alignment::Start, align_items, Size::new(0.0, cross));
472            }
473            Axis::Vertical => {
474                node.align_mut(align_items, Alignment::Start, Size::new(cross, 0.0));
475            }
476        }
477
478        main += axis.main(node.size());
479    }
480
481    let main = match axis {
482        Axis::Horizontal => limits.resolve_width(width, main - pad.0),
483        Axis::Vertical => limits.resolve_height(height, main - pad.0),
484    };
485
486    let size = Size::from(axis.pack(main, cross));
487
488    Node::with_children(size.expand(padding), nodes)
489}