Skip to main content

altui_core/layout/
layout.rs

1use std::time::Instant;
2#[cfg(feature = "layout-cache")]
3use std::{cell::RefCell, collections::HashMap};
4
5use cassowary::strength::{MEDIUM, REQUIRED, STRONG, WEAK};
6use cassowary::WeightedRelation::*;
7use cassowary::{Constraint as CassowaryConstraint, Solver};
8
9use crate::layout::elements::{LineBox, Variables};
10use crate::layout::{
11    elements::{ElConstraint, Element},
12    flex::Flex,
13    rect::Rect,
14};
15
16const SUPER: f64 = 1_001_001_001_000.0;
17
18#[derive(Default)]
19struct Line {
20    elements: Vec<Element>,
21}
22
23#[derive(Copy, Clone)]
24pub(crate) enum Attr {
25    Start,
26    CrossStart,
27    Size,
28    CrossSize,
29    Gap,
30}
31
32#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
33pub enum Corner {
34    TopLeft,
35    TopRight,
36    BottomRight,
37    BottomLeft,
38}
39
40#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
41pub enum Direction {
42    Horizontal,
43    Vertical,
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
47pub enum Constraint {
48    // TODO: enforce range 0 - 100
49    Percentage(u16),
50    Ratio(u32, u32),
51    Length(u16),
52    Max(u16),
53    Min(u16),
54}
55
56impl Constraint {
57    pub fn apply(&self, length: u16) -> u16 {
58        match *self {
59            Constraint::Percentage(p) => length * p / 100,
60            Constraint::Ratio(num, den) => {
61                let r = num * u32::from(length) / den;
62                r as u16
63            }
64            Constraint::Length(l) => length.min(l),
65            Constraint::Max(m) => length.min(m),
66            Constraint::Min(m) => length.max(m),
67        }
68    }
69}
70
71#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
72pub struct Margin {
73    pub vertical: u16,
74    pub horizontal: u16,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78pub enum Alignment {
79    Left,
80    Center,
81    Right,
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, Hash)]
85pub struct Layout {
86    direction: Direction,
87    margin: Margin,
88    constraints: Vec<Constraint>,
89    flex: Flex,
90    cross_flex: Flex,
91    cross_size: Constraint,
92    overlap: u16,
93    cross_overlap: u16,
94    boxed: bool,
95    external_scroll: bool,
96    wrap: bool,
97}
98
99#[cfg(feature = "layout-cache")]
100thread_local! {
101    static LAYOUT_CACHE: RefCell<HashMap<(Rect, Layout, u16), Vec<Rect>>> = RefCell::new(HashMap::new());
102}
103
104impl Default for Layout {
105    fn default() -> Layout {
106        Layout {
107            direction: Direction::Vertical,
108            margin: Margin {
109                horizontal: 0,
110                vertical: 0,
111            },
112            constraints: Vec::new(),
113            flex: Flex::Start,
114            cross_flex: Flex::Start,
115            cross_size: Constraint::Percentage(100),
116            overlap: 0,
117            cross_overlap: 0,
118            boxed: false,
119            external_scroll: false,
120            wrap: false,
121        }
122    }
123}
124
125impl Layout {
126    /// Creates a vertical layout with the given constraints.
127    ///
128    /// A vertical layout splits the available area from top to bottom
129    /// into multiple horizontal slices.
130    ///
131    /// In other words:
132    /// * `Constraint` values are applied to the `height`
133    /// * All resulting rectangles share the same `x` and `width`
134    /// * Rectangles are stacked along the `Y axis`
135    ///
136    /// This method is equivalent to `Layout::default().direction(Direction::Vertical)`
137    /// and effectively replaces the use of `Layout::new(...)` for vertical layouts.
138    ///
139    /// ## Visual representation
140    /// ```text
141    /// +----------------------+
142    /// |        Rect 0        |  ← Constraint[0]
143    /// +----------------------+
144    /// |        Rect 1        |  ← Constraint[1]
145    /// +----------------------+
146    /// |        Rect 2        |  ← Constraint[2]
147    /// +----------------------+
148    /// ```
149    ///
150    /// ## Example
151    ///
152    /// ```
153    /// use altui_core::layout::{Rect, Constraint::*, Layout};
154    ///
155    /// let chunks = Layout::vertical([Length(2),Min(0)])
156    ///     .split(Rect {
157    ///         x: 0,
158    ///         y: 0,
159    ///         width: 10,
160    ///         height: 6,
161    ///     });
162    ///
163    /// // Splits the area into two rows:
164    /// // height = 2 and height = 4
165    /// assert_eq!(chunks.len(), 2);
166    /// assert_eq!(chunks[0].height, 2);
167    /// assert_eq!(chunks[1].height, 4);
168    /// ```
169    /// See also [`Layout::horizontal`] for left-to-right layouts.
170    pub fn vertical<C>(constraints: C) -> Self
171    where
172        C: Into<Vec<Constraint>>,
173    {
174        Layout {
175            constraints: constraints.into(),
176            ..Default::default()
177        }
178    }
179
180    /// Creates a horizontal layout with the given constraints.
181    ///
182    /// A horizontal layout splits the available area from left to right
183    /// into multiple vertical slices.
184    ///
185    /// In other words:
186    /// * Constraint values are applied to the `width`
187    /// * All resulting rectangles share the same `y` and `height`
188    /// * Rectangles are stacked along the `X axis`
189    ///
190    /// This method is equivalent to
191    /// `Layout::default().direction(Direction::Horizontal)`
192    /// and effectively replaces the use of `Layout::new(...)` for horizontal layouts.
193    ///
194    /// ## Visual representation
195    /// ```text
196    /// +---------+---------+---------+
197    /// | Rect 0  | Rect 1  | Rect 2  |
198    /// |         |         |         |
199    /// +---------+---------+---------+
200    ///   ↑           ↑
201    /// Constraint[0] Constraint[1]
202    /// ```
203    ///
204    /// ## Example
205    ///
206    /// ```
207    /// use altui_core::layout::{Rect, Constraint::*, Layout};
208    ///
209    /// let chunks = Layout::horizontal([Length(3),Min(0)])
210    ///     .split(Rect {
211    ///         x: 0,
212    ///         y: 0,
213    ///         width: 10,
214    ///         height: 4,
215    ///     });
216    ///
217    /// // Splits the area into two columns:
218    /// // width = 3 and width = 7
219    /// assert_eq!(chunks.len(), 2);
220    /// assert_eq!(chunks[0].width, 3);
221    /// assert_eq!(chunks[1].width, 7);
222    /// ```
223    /// See also [`Layout::vertical`] for top-to-bottom layouts.
224    pub fn horizontal<C>(constraints: C) -> Self
225    where
226        C: Into<Vec<Constraint>>,
227    {
228        Layout {
229            direction: Direction::Horizontal,
230            constraints: constraints.into(),
231            ..Default::default()
232        }
233    }
234
235    pub fn constraints<C>(mut self, constraints: C) -> Layout
236    where
237        C: Into<Vec<Constraint>>,
238    {
239        self.constraints = constraints.into();
240        self
241    }
242
243    pub fn margin(mut self, margin: u16) -> Layout {
244        self.margin = Margin {
245            horizontal: margin,
246            vertical: margin,
247        };
248        self
249    }
250
251    pub fn horizontal_margin(mut self, horizontal: u16) -> Layout {
252        self.margin.horizontal = horizontal;
253        self
254    }
255
256    pub fn vertical_margin(mut self, vertical: u16) -> Layout {
257        self.margin.vertical = vertical;
258        self
259    }
260
261    pub fn direction(mut self, direction: Direction) -> Layout {
262        self.direction = direction;
263        self
264    }
265
266    /// Sets the amount of allowed overlap between adjacent elements
267    /// along the main axis.
268    ///
269    /// The `overlap` value specifies how many neighboring characters are
270    /// allowed to overlap between adjacent layout items.
271    ///
272    /// Useful for compact layouts with shared borders.
273    ///
274    /// ```text
275    /// ┌────────┌────────┌────────┐
276    /// │ Item 1 │ Item 2 │ Item 3 │
277    /// └────────└────────└────────┘
278    /// ```
279    ///
280    /// # Warning: Unstable API
281    ///
282    /// ⚠️ **Known Issue**: This method may cause incorrect rendering
283    /// when used together with [`Layout::split_ext`], [`Flex::SpaceBetween`]
284    /// or [`Flex::SpaceAround`].
285    pub fn overlap(mut self, overlap: u16) -> Layout {
286        self.overlap = overlap;
287        self
288    }
289
290    /// Sets the amount of allowed overlap between adjacent elements
291    /// along the cross axis.
292    ///
293    /// The `overlap` value specifies how many neighboring symbols are
294    /// allowed to overlap between adjacent layout items.
295    ///
296    /// Useful for compact layouts with shared borders.
297    ///
298    /// ```text
299    /// ┌────────┐
300    /// │ Item 1 │
301    /// ┌────────┐
302    /// │ Item 2 │
303    /// ┌────────┐
304    /// │ Item 3 │
305    /// └────────┘
306    /// ```
307    ///
308    /// # Warning: Unstable API
309    ///
310    /// ⚠️ **Known Issue**: This method may cause incorrect rendering
311    /// when used together with [`Layout::split_ext`], [`Flex::SpaceBetween`]
312    /// or [`Flex::SpaceAround`].
313    pub fn cross_overlap(mut self, cross_overlap: u16) -> Layout {
314        self.cross_overlap = cross_overlap;
315        self
316    }
317
318    /// Sets how free space is distributed between elements along the main axis.
319    ///
320    /// `Flex` controls the alignment and spacing of elements **after all constraints
321    /// have been resolved**.
322    ///
323    /// This affects only the main axis:
324    /// * horizontal layouts → X axis
325    /// * vertical layouts → Y axis
326    ///
327    /// The default value is [`Flex::Start`].
328    ///
329    /// See [`Flex`] for a detailed description of each mode.
330    pub fn flex(mut self, flex: Flex) -> Layout {
331        self.flex = flex;
332        self
333    }
334
335    /// Sets how free space is distributed between elements along the cross axis.
336    ///
337    /// `Flex` controls the alignment and spacing of elements **after all constraints
338    /// have been resolved**.
339    ///
340    /// This affects only the cross axis:
341    /// * horizontal layouts → Y axis
342    /// * vertical layouts → X axis
343    ///
344    /// The default value is [`Flex::Start`].
345    pub fn cross_flex(mut self, flex: Flex) -> Layout {
346        self.cross_flex = flex;
347        self
348    }
349
350    /// Enables or disables *boxed layout mode*.
351    ///
352    /// When `boxed` is set to `true`, fixed-size constraints
353    /// (`Constraint::Length`, `Constraint::Min`) take precedence and are resolved first.
354    ///
355    /// Remaining space is then distributed between:
356    /// * `Constraint::Ratio`
357    /// * `Constraint::Percentage`
358    ///
359    /// When boxed is `false` (default), all constraints participate in layout
360    /// solving on equal terms.
361    ///
362    /// ## Important notes
363    ///
364    /// * Using `boxed = true` together with scrolling or wrapping may produce
365    ///   unintuitive results.
366    /// * Percentage and ratio constraints are strongly discouraged when
367    ///   `boxed` is enabled.
368    ///
369    /// ## Default
370    ///
371    /// `false`
372    pub fn boxed(mut self, boxed: bool) -> Layout {
373        self.boxed = boxed;
374        self
375    }
376
377    /// Marks the layout as externally scroll-controlled.
378    ///
379    /// When enabled, the layout does not compute its own scroll bounds.
380    /// Instead, the scroll size is assumed to be managed by the caller.
381    ///
382    /// This is mainly useful when:
383    /// * multiple layouts share a single scroll state
384    /// * scroll limits are computed elsewhere
385    ///
386    /// This option is only meaningful when using [`Layout::split_ext`].
387    pub fn external_scroll(mut self) -> Layout {
388        self.external_scroll = true;
389        self
390    }
391
392    /// Enables wrapping into multiple lines when the accumulated main-axis length
393    /// exceeds the given value. Sets line/column size on cross-axis.
394    ///
395    /// Wrapping splits elements into multiple lines/columns, similar to text wrapping.
396    ///
397    /// * Wrapping is performed based on main-axis length
398    /// * Line/column size along the cross-axis is fixed (Min/Max Constraints work as Length)
399    /// * Enabling wrapping implicitly changes the scroll direction
400    ///
401    /// ## Scroll direction
402    ///
403    /// * Without wrapping:
404    ///   * horizontal layout → horizontal scroll
405    ///   * vertical layout → vertical scroll
406    ///
407    /// * With wrapping enabled:
408    ///   * horizontal layout → vertical scroll
409    ///   * vertical layout → horizontal scroll
410    ///
411    /// ## Example
412    ///
413    /// Wrapping a horizontal layout into multiple rows:
414    ///
415    /// ```text
416    /// [A][B][C]  → wrap
417    /// [A][B]
418    /// [C]
419    /// ```
420    /// See also [`cross_size`](Layout::cross_size).
421    pub fn wrap(mut self, cross_size: Constraint) -> Layout {
422        self.wrap = true;
423        self.cross_size = cross_size;
424        self
425    }
426
427    /// Sets the size of Rect along the cross-axis (or cross-direction) without wrapping
428    ///
429    /// Note!
430    ///  * `Min/Max` constraints work as `Length`
431    ///  * Default is `Percentage(100)`
432    pub fn cross_size(mut self, cross_size: Constraint) -> Layout {
433        self.cross_size = cross_size;
434        self
435    }
436
437    #[cfg(not(feature = "layout-cache"))]
438    pub fn cache_eq(&self, other: &Layout) -> bool {
439        self.constraints == other.constraints
440            && self.cross_size == other.cross_size
441            && self.flex == other.flex
442            && self.cross_flex == other.cross_flex
443            && self.direction == other.direction
444            && self.margin == other.margin
445            && self.boxed == other.boxed
446            && self.wrap == other.wrap
447            && self.external_scroll == other.external_scroll
448    }
449
450    /// Splits the given area into multiple [`Rect`]s according to the layout
451    /// configuration, direction, wrapping rules and constraints.
452    ///
453    /// This function is a high-level wrapper around an internal Cassowary-based
454    /// solver. The layout process consists of two distinct phases:
455    ///
456    /// 1. Line/column construction (preparation phase)
457    /// 2. Constraint solving (layout phase)
458    ///
459    /// ## Wrapping behavior
460    ///
461    /// * Wrapping is enabled when [`Layout::wrap`] method is used.
462    /// * Each line/column has a fixed cross size determined in the method parameter.
463    /// * When wrapping is enabled, a new line/column is started whenever adding the
464    ///   next constraint would exceed the available main axis (direction) size.
465    /// * Elements with `Max` constraint and `Percentage` and `Ratio` constraints
466    ///   if layout is `boxed` do not contribute to line/column size and will be
467    ///   collapsed by the solver.
468    ///
469    /// # Examples
470    ///
471    /// ### Vertical layout
472    /// ```
473    /// use altui_core::layout::{Rect, Constraint, Layout};
474    ///
475    /// let chunks = Layout::vertical([Constraint::Length(5), Constraint::Min(0)])
476    ///     .split(Rect {
477    ///         x: 2,
478    ///         y: 2,
479    ///         width: 10,
480    ///         height: 10,
481    ///     });
482    /// assert_eq!(
483    ///     chunks,
484    ///     vec![
485    ///         Rect {
486    ///             x: 2,
487    ///             y: 2,
488    ///             width: 10,
489    ///             height: 5
490    ///         },
491    ///         Rect {
492    ///             x: 2,
493    ///             y: 7,
494    ///             width: 10,
495    ///             height: 5
496    ///         }
497    ///     ]
498    /// );
499    /// ```
500    ///
501    /// ## Horizontal layout with ratios
502    /// ```
503    /// use altui_core::layout::{Rect, Constraint, Layout};
504    ///
505    /// let chunks = Layout::horizontal([Constraint::Ratio(1, 3), Constraint::Ratio(2, 3)])
506    ///     .split(Rect {
507    ///         x: 0,
508    ///         y: 0,
509    ///         width: 9,
510    ///         height: 2,
511    ///     });
512    /// assert_eq!(
513    ///     chunks,
514    ///     vec![
515    ///         Rect {
516    ///             x: 0,
517    ///             y: 0,
518    ///             width: 3,
519    ///             height: 2
520    ///         },
521    ///         Rect {
522    ///             x: 3,
523    ///             y: 0,
524    ///             width: 6,
525    ///             height: 2
526    ///         }
527    ///     ]
528    /// );
529    /// ```
530    ///
531    /// ### Wrapping into multiple lines by length
532    /// ```
533    /// use altui_core::layout::{Rect, Constraint, Layout};
534    ///
535    /// let chunks = Layout::horizontal([
536    ///         Constraint::Length(3),
537    ///         Constraint::Length(3),
538    ///         Constraint::Length(3),
539    ///     ])
540    ///     .wrap(Constraint::Length(2))
541    ///     .split(Rect {
542    ///         x: 0,
543    ///         y: 0,
544    ///         width: 6,
545    ///         height: 4,
546    ///     });
547    ///
548    /// // Produces two rows with height `2` each:
549    /// // [3][3]
550    /// // [3]
551    ///
552    /// assert_eq!(
553    ///     chunks,
554    ///     vec![
555    ///         Rect { x: 0, y: 0, width: 3, height: 2 },
556    ///         Rect { x: 3, y: 0, width: 3, height: 2 },
557    ///         Rect { x: 0, y: 2, width: 3, height: 2 },
558    ///     ]
559    /// );
560    /// ```
561    ///
562    /// ### Wrapping into multiple lines by percentage
563    /// ```
564    /// use altui_core::layout::{Rect, Constraint, Layout};
565    ///
566    /// let chunks = Layout::horizontal([
567    ///         Constraint::Length(3),
568    ///         Constraint::Length(3),
569    ///         Constraint::Length(3),
570    ///     ])
571    ///     .wrap(Constraint::Percentage(50))
572    ///     .split(Rect {
573    ///         x: 0,
574    ///         y: 0,
575    ///         width: 6,
576    ///         height: 8,
577    ///     });
578    ///
579    /// // Produces two rows with height `4` each:
580    /// // [3][3]
581    /// // [3]
582    ///
583    /// assert_eq!(
584    ///     chunks,
585    ///     vec![
586    ///         Rect { x: 0, y: 0, width: 3, height: 4 },
587    ///         Rect { x: 3, y: 0, width: 3, height: 4 },
588    ///         Rect { x: 0, y: 4, width: 3, height: 4 },
589    ///     ]
590    /// );
591    /// ```
592    pub fn split(&self, area: Rect) -> Vec<Rect> {
593        let dest_area = area.inner(&self.margin);
594
595        // TODO: Maybe use a fixed size cache ?
596        #[cfg(feature = "layout-cache")]
597        {
598            LAYOUT_CACHE.with(|c| {
599                c.borrow_mut()
600                    .entry((dest_area, self.clone(), 0))
601                    .or_insert_with(|| self.split_inner(dest_area))
602                    .clone()
603            })
604        }
605
606        #[cfg(not(feature = "layout-cache"))]
607        self.split_inner(dest_area)
608    }
609
610    fn split_inner(&self, dest_area: Rect) -> Vec<Rect> {
611        let stopwatch = Instant::now();
612        let (lines, _, line_cross_size, _, variables) = self.split_preparations(&dest_area);
613
614        let result = Results::new(self.direction, self.constraints.len()).split(
615            dest_area,
616            self,
617            0.0,
618            lines,
619            line_cross_size,
620            variables,
621        );
622
623        tracing::trace!(
624            "Altui split: {} µs",
625            stopwatch.elapsed().as_nanos() as f64 / 1_000.0
626        );
627        result
628    }
629
630    /// Splits the given area into multiple [`Rect`]s with scrolling support.
631    ///
632    /// This method extends [`Layout::split`] by introducing a scroll offset and
633    /// automatically computing the scrollable range based on the layout
634    /// configuration.
635    ///
636    /// The layout process is identical to split, but the final positioning
637    /// is shifted by a computed scroll offset along the active scroll axis.
638    ///
639    /// ## Scrolling model
640    ///
641    /// Scrolling is single-axis and its direction depends on whether wrapping
642    /// is enabled:
643    ///
644    /// * Without wrapping:
645    ///   * Scrolling occurs along the main axis of the layout.
646    ///   * For example:
647    ///     * Direction::Horizontal → horizontal scrolling
648    ///     * Direction::Vertical → vertical scrolling
649    ///
650    /// * With wrapping enabled:
651    ///   * Scrolling occurs along the cross axis.
652    ///   * The layout grows by adding multiple lines/columns, and scrolling moves
653    ///     between those lines/columns.
654    ///
655    /// This behavior mirrors common UI patterns where wrapping turns a
656    /// one-dimensional layout into a multi-line structure with perpendicular
657    /// scrolling.
658    ///
659    /// ## Scroll parameters
660    ///
661    /// * scroll — total scrollable length (output)
662    ///   * Updated by this function unless external_scroll is enabled.
663    ///   * Represents the full scroll range in layout units.
664    ///
665    /// * scrollstate — current scroll position (input/output)
666    ///   * Clamped automatically to the valid scroll range.
667    ///   * Used to compute the effective scroll offset.
668    ///
669    /// ## Wrapping and scrolling interaction
670    ///
671    /// * When wrapping is enabled:
672    ///   * The scroll range is computed from the total cross-axis size of all
673    ///     lines/columns.
674    ///
675    /// * When wrapping is disabled:
676    ///   * The scroll range is computed from the total content length along the
677    ///     main axis.
678    ///
679    /// ## External scrolling
680    ///
681    /// If external_scroll is enabled:
682    /// * The scroll range is taken from the scroll parameter.
683    /// * This allows integrating the layout with an external scrolling container.
684    ///
685    /// ## Constraints and limitations
686    ///
687    /// ⚠️ Important:
688    /// Do not use the following constraint combinations with split_ext:
689    ///
690    /// * [`Constraint::Percentage`] or [`Constraint::Ratio`] when the layout is boxed
691    /// * [`Constraint::Max`] when relying on internal scrolling
692    ///
693    /// These combinations may produce undefined or unintuitive scrolling behavior.
694    ///
695    /// ## Notes
696    ///
697    /// * Line construction and wrapping are performed before scrolling is
698    ///   applied.
699    /// * The solver does not decide the scroll direction.
700    /// * Margins are applied before layout and scrolling.
701    /// * Scrolling offsets are quantized to integer values.
702    ///
703    /// # Examples
704    ///
705    /// ### Horizontal scrolling without wrapping
706    ///
707    ///```
708    /// use altui_core::layout::{Rect, Constraint, Layout};
709    ///
710    /// let mut scroll = 0;
711    /// let mut scrollstate = 0;
712    ///
713    /// let chunks = Layout::horizontal([
714    ///         Constraint::Length(5),
715    ///         Constraint::Length(5),
716    ///         Constraint::Length(5),
717    ///     ])
718    ///     .split_ext(
719    ///         Rect { x: 0, y: 0, width: 8, height: 2 },
720    ///         &mut scrollstate,
721    ///         &mut scroll,
722    ///     );
723    ///
724    /// // Content width exceeds available width, so scrolling is horizontal.
725    /// assert!(scroll == 15);
726    /// ```
727    ///
728    /// ### Vertical scrolling with wrapping enabled
729    /// ```
730    /// use altui_core::layout::{Rect, Constraint, Layout};
731    ///
732    /// let mut scroll = 0;
733    /// let mut scrollstate = 0;
734    ///
735    /// let chunks = Layout::horizontal([
736    ///         Constraint::Length(3),
737    ///         Constraint::Length(3),
738    ///         Constraint::Length(3),
739    ///     ])
740    ///     .wrap(Constraint::Length(8))
741    ///     .split_ext(
742    ///         Rect { x: 0, y: 0, width: 6, height: 8 },
743    ///         &mut scrollstate,
744    ///         &mut scroll,
745    ///     );
746    ///
747    /// // Wrapping creates multiple rows; scrolling is now vertical.
748    /// assert!(scroll == 16);
749    /// ```
750    /// See [`Layout::split`] for a non-scrolling variant.
751    pub fn split_ext(&self, area: Rect, scrollstate: &mut u16, scroll: &mut u16) -> Vec<Rect> {
752        let dest_area = area.inner(&self.margin);
753
754        if *scroll > 0 && scrollstate > scroll {
755            *scrollstate = *scroll;
756        }
757
758        // TODO: Maybe use a fixed size cache ?
759        #[cfg(feature = "layout-cache")]
760        {
761            LAYOUT_CACHE.with(|c| {
762                c.borrow_mut()
763                    .entry((dest_area, self.clone(), *scrollstate))
764                    .or_insert_with(|| self.split_ext_inner(dest_area, scrollstate, scroll))
765                    .clone()
766            })
767        }
768
769        #[cfg(not(feature = "layout-cache"))]
770        self.split_ext_inner(dest_area, scrollstate, scroll)
771    }
772
773    fn split_ext_inner(
774        &self,
775        dest_area: Rect,
776        scrollstate: &mut u16,
777        scroll: &mut u16,
778    ) -> Vec<Rect> {
779        let stopwatch = std::time::Instant::now();
780        let gap: f64;
781        let (lines, size, line_cross_size, content_length, variables) =
782            self.split_preparations(&dest_area);
783
784        if !self.external_scroll && !self.wrap {
785            gap = (content_length - size).max(0.0).floor();
786            *scroll = content_length.max(size).floor() as u16;
787        } else if !self.external_scroll {
788            let cross_size = dest_area.cross_end(self.direction);
789            let cross_length = *line_cross_size * lines.len() as f64;
790            gap = (cross_length - cross_size).max(0.0).floor();
791            *scroll = cross_length.max(cross_size).floor() as u16;
792        } else {
793            gap = (f64::from(*scroll) - size).max(0.0);
794        }
795
796        let scroll_inner = match gap == 0.0 {
797            true => 0.0,
798            false => f64::from(*scrollstate) / f64::from(*scroll) * gap,
799        };
800
801        let result = Results::new(self.direction, self.constraints.len()).split(
802            dest_area,
803            self,
804            scroll_inner.floor(),
805            lines,
806            line_cross_size,
807            variables,
808        );
809
810        tracing::trace!(
811            "Altui scrollable split: {} µs",
812            stopwatch.elapsed().as_nanos() as f64 / 1_000.0
813        );
814        result
815    }
816
817    #[inline(always)]
818    fn split_preparations(
819        &self,
820        dest_area: &Rect,
821    ) -> (Vec<Line>, f64, ElConstraint, f64, Variables) {
822        use Constraint::*;
823        let mut content_length = 0.0;
824        let mut variables = Variables::default();
825        let size = dest_area.size(self.direction);
826        let line_cross_size = match self.cross_size {
827            Percentage(100) => ElConstraint::Length(dest_area.cross_size(self.direction)),
828            Percentage(v) => {
829                ElConstraint::Length(dest_area.cross_size(self.direction) * v as f64 / 100.0)
830            }
831            Ratio(n, d) => ElConstraint::Ratio(size * f64::from(n) / f64::from(d)),
832            Length(v) | Min(v) | Max(v) => ElConstraint::Length(f64::from(v)),
833        };
834        let mut lines = Vec::new();
835        let mut line = Line::default();
836
837        let boxedf64 = if self.boxed { STRONG } else { MEDIUM };
838        let mut used = 0.0;
839
840        for el in &self.constraints {
841            let (boxed, strength, constraint) = match el {
842                Length(v) => (false, boxedf64, ElConstraint::Length(f64::from(*v))),
843                Min(v) => (false, boxedf64, ElConstraint::Min(f64::from(*v))),
844                Percentage(v) => (
845                    self.boxed,
846                    MEDIUM,
847                    ElConstraint::Percentage(size * f64::from(*v) / 100.0),
848                ),
849                Max(v) => (true, WEAK, ElConstraint::Max(f64::from(*v))),
850                Ratio(n, d) => (
851                    self.boxed,
852                    MEDIUM,
853                    ElConstraint::Ratio(size * f64::from(*n) / f64::from(*d)),
854                ),
855            };
856
857            let w = if boxed { 0.0 } else { *constraint };
858            content_length += w;
859
860            if self.wrap && used > 0.0 && used + w > size {
861                lines.push(std::mem::take(&mut line));
862                used = 0.0;
863            }
864
865            line.elements
866                .push(Element::new(strength, constraint, &mut variables));
867            used += w;
868        }
869
870        if !line.elements.is_empty() {
871            lines.push(line);
872        }
873
874        (lines, size, line_cross_size, content_length, variables)
875    }
876}
877
878#[derive(Debug)]
879struct Results {
880    v: Vec<Rect>,
881    axis: Direction,
882}
883
884impl Results {
885    fn new(axis: Direction, len: usize) -> Self {
886        Results {
887            v: vec![Rect::default(); len],
888            axis,
889        }
890    }
891
892    #[inline(always)]
893    fn main_axis(&mut self, index: usize, attr: Attr, value: u16) {
894        let r = &mut self.v[index];
895        match (self.axis, attr) {
896            (Direction::Horizontal, Attr::Start) => r.x = value,
897            (Direction::Horizontal, Attr::Size) => r.width = value,
898            (Direction::Vertical, Attr::Start) => r.y = value,
899            (Direction::Vertical, Attr::Size) => r.height = value,
900            _ => {}
901        }
902    }
903
904    #[inline(always)]
905    fn cross_axis(&mut self, index: usize, line: usize, lines: &[(u16, u16)]) {
906        let r = &mut self.v[index];
907        let (start, size) = lines[line];
908        match self.axis {
909            Direction::Horizontal => {
910                r.y = start;
911                r.height = size;
912            }
913            Direction::Vertical => {
914                r.x = start;
915                r.width = size;
916            }
917        }
918    }
919
920    #[inline(always)]
921    fn split(
922        mut self,
923        dest_area: Rect,
924        layout: &Layout,
925        mut offset: f64,
926        lines: Vec<Line>,
927        cross_size: ElConstraint,
928        mut variables: Variables,
929    ) -> Vec<Rect> {
930        use crate::layout::Flex::*;
931
932        // Gap constraints for each line + rows constraints + flex constraints
933        let mut ccs: Vec<CassowaryConstraint> =
934            Vec::with_capacity(lines.len() + layout.constraints.len() * 6 + lines.len() * 2);
935        // Cross gap + line/column constraints + flex constraints
936        let mut cross_ccs: Vec<CassowaryConstraint> = Vec::with_capacity(1 + lines.len() * 5 + 2);
937
938        // Calc quantity of Cassowary Variables, that must be eq with the index of the last Variable:
939        // Constraints Variables + Lines Variables + Gaps Variables + Cross gap variable
940        let vars_indexes = layout.constraints.len() * 2 + lines.len() * 2 + lines.len() + 1;
941        // Line, index, attr
942        let mut ivars: Vec<Option<(usize, usize, Attr)>> = vec![None; vars_indexes];
943
944        // Lines processing
945        let elmlines = &lines
946            .iter()
947            .map(|_| LineBox::new(&mut variables))
948            .collect::<Vec<LineBox>>();
949
950        let overlap = (layout.overlap as f64).min(dest_area.size(self.axis));
951        let cross_overlap = (layout.cross_overlap as f64).min(dest_area.cross_size(self.axis));
952
953        // Cross axis gap for lines
954        let cross_gap = variables.add();
955        ivars[variables.get_id(cross_gap)] = Some((0, 0, Attr::Gap));
956        cross_ccs.push(match layout.cross_flex {
957            SpaceBetween | SpaceAround => cross_gap | GE(REQUIRED) | 0f64,
958            _ => cross_gap | EQ(REQUIRED) | 0f64,
959        });
960
961        let mut j: usize = 0;
962        // Lines
963        for ((l, line), elmline) in lines.iter().enumerate().zip(elmlines) {
964            elmline.vars(&mut ivars, l, &variables);
965            // Main axis gap constraint for SpaceBetween or SpaceAround
966            let gap = variables.add();
967            ivars[variables.get_id(gap)] = Some((0, 0, Attr::Gap));
968            ccs.push(match layout.flex {
969                SpaceBetween | SpaceAround => gap | GE(REQUIRED) | 0f64,
970                _ => gap | EQ(SUPER) | 0f64,
971            });
972
973            // calc offset for cross axis scroll
974            let cross_size_with_offset = if layout.wrap {
975                scroll(*cross_size, &mut offset)
976            } else {
977                *cross_size
978            };
979
980            // spacing between lines
981            if let Some(next) = elmlines.get(l + 1) {
982                cross_ccs.push(
983                    next.cross_start - elmline.cross_end()
984                        | EQ(REQUIRED)
985                        | cross_gap - cross_overlap,
986                );
987            }
988
989            cross_ccs.extend([
990                elmline.cross_size | GE(REQUIRED) | 0.0,
991                elmline.cross_start | GE(REQUIRED) | dest_area.cross_start(self.axis),
992                elmline.cross_end() | LE(REQUIRED) | dest_area.cross_end(self.axis),
993                elmline.cross_size | EQ(MEDIUM) | cross_size_with_offset,
994            ]);
995            // Elements
996            for (i, elm) in line.elements.iter().enumerate() {
997                elm.vars(&mut ivars, l, j, &variables);
998
999                // calc offset for main axis scroll
1000                let v = match elm.constraint {
1001                    ElConstraint::Min(v) => {
1002                        ccs.push(elm.size | EQ(WEAK) | dest_area.size(self.axis));
1003                        if layout.wrap {
1004                            v
1005                        } else {
1006                            scroll(v, &mut offset)
1007                        }
1008                    }
1009                    ElConstraint::Percentage(v)
1010                    | ElConstraint::Ratio(v)
1011                    | ElConstraint::Length(v) => {
1012                        if layout.wrap {
1013                            v
1014                        } else {
1015                            scroll(v, &mut offset)
1016                        }
1017                    }
1018                    ElConstraint::Max(v) => v,
1019                };
1020
1021                // spacing between elements
1022                if let Some(next_elm) = line.elements.get(i + 1) {
1023                    ccs.push(next_elm.start - elm.end() | EQ(REQUIRED) | gap - overlap);
1024                }
1025
1026                ccs.extend([
1027                    elm.size | GE(REQUIRED) | 0.0,
1028                    elm.start | GE(REQUIRED) | dest_area.start(self.axis),
1029                    elm.end() | LE(REQUIRED) | dest_area.end(self.axis),
1030                    match elm.constraint {
1031                        ElConstraint::Min(_) => elm.size | GE(elm.strength) | v,
1032                        _ => elm.size | EQ(elm.strength) | v,
1033                    },
1034                ]);
1035                j += 1;
1036            }
1037            // flex constraints
1038            if let (Some(first), Some(last)) = (line.elements.first(), line.elements.last()) {
1039                match layout.flex {
1040                    Legacy | SpaceBetween => ccs.extend([
1041                        first.start | EQ(SUPER) | dest_area.start(self.axis),
1042                        last.end() | EQ(SUPER) | dest_area.end(self.axis),
1043                    ]),
1044                    Start => ccs.push(first.start | EQ(SUPER) | dest_area.start(self.axis)),
1045                    End => ccs.push(last.end() | EQ(SUPER) | dest_area.end(self.axis)),
1046                    Center => ccs.push(
1047                        first.start + last.size + last.start
1048                            | EQ(SUPER)
1049                            | dest_area.start(self.axis) + dest_area.end(self.axis),
1050                    ),
1051                    SpaceAround => ccs.extend([
1052                        first.start - dest_area.start(self.axis) | EQ(SUPER) | gap,
1053                        dest_area.end(self.axis) - last.end() | EQ(SUPER) | gap,
1054                    ]),
1055                }
1056            }
1057        }
1058
1059        if let (Some(first), Some(last)) = (elmlines.first(), elmlines.last()) {
1060            match layout.cross_flex {
1061                Legacy | Start => {
1062                    cross_ccs.push(first.cross_start | EQ(SUPER) | dest_area.cross_start(self.axis))
1063                }
1064                End => {
1065                    cross_ccs.push(last.cross_end() | EQ(SUPER) | dest_area.cross_end(self.axis))
1066                }
1067                Center => cross_ccs.push(
1068                    first.cross_start + last.cross_size + last.cross_start
1069                        | EQ(SUPER)
1070                        | dest_area.cross_start(self.axis) + dest_area.cross_end(self.axis),
1071                ),
1072                SpaceBetween => cross_ccs.extend([
1073                    first.cross_start | EQ(SUPER) | dest_area.cross_start(self.axis),
1074                    last.cross_end() | EQ(SUPER) | dest_area.cross_end(self.axis),
1075                ]),
1076                SpaceAround => cross_ccs.extend([
1077                    first.cross_start - dest_area.cross_start(self.axis) | EQ(SUPER) | cross_gap,
1078                    dest_area.cross_end(self.axis) - last.cross_end() | EQ(SUPER) | cross_gap,
1079                ]),
1080            }
1081        }
1082
1083        assert!(ccs.len() < layout.constraints.len() * 13 + lines.len() + 1);
1084
1085        let mut solver = Solver::new();
1086        solver.add_constraints(&cross_ccs).unwrap();
1087        // (Start, Size)
1088        let mut lines = vec![(0, 0); lines.len()];
1089        for &(var, value) in solver.fetch_changes() {
1090            let (line, index, attr) = ivars
1091                .get(variables.get_id(var))
1092                .expect("Cassowary variable and usize must have the same size and alignment")
1093                .expect("Total quantity of Cassowary Variables used in altui must be eq the last Variable index");
1094            // We have one gap var in line, that can be eq 0, but indexs can not
1095            assert!(index == 0);
1096            let value = norm(value);
1097
1098            match attr {
1099                Attr::CrossStart => lines[line].0 = value,
1100                Attr::CrossSize => lines[line].1 = value,
1101                _ => {}
1102            }
1103        }
1104
1105        solver.reset();
1106
1107        solver.add_constraints(&ccs).unwrap();
1108        for &(var, value) in solver.fetch_changes() {
1109            let (line, index, attr) = ivars
1110                .get(variables.get_id(var))
1111                .expect("Cassowary variable and usize must have the same size and alignment")
1112                .expect("Total quantity of Cassowary Variables used in altui must be eq the last Variable index");
1113            let value = norm(value);
1114
1115            self.main_axis(index, attr, value);
1116            self.cross_axis(index, line, &lines);
1117        }
1118
1119        self.v
1120    }
1121}
1122
1123#[inline]
1124fn norm(v: f64) -> u16 {
1125    if v.is_sign_negative() {
1126        0
1127    } else {
1128        v as u16
1129    }
1130}
1131
1132#[inline(always)]
1133fn scroll(mut x: f64, scroll: &mut f64) -> f64 {
1134    if *scroll > 0.0 {
1135        if *scroll >= x {
1136            *scroll -= x;
1137            return 0.0;
1138        } else {
1139            x -= *scroll;
1140            *scroll = 0.0;
1141        }
1142    }
1143    x
1144}
1145
1146#[cfg(test)]
1147mod tests {
1148    use super::*;
1149
1150    #[test]
1151    fn test_boxed() {
1152        let err_msg = "Failed to resolve area with boxed param in Layout";
1153        let area = Rect {
1154            x: 0,
1155            y: 0,
1156            width: 10,
1157            height: 10,
1158        };
1159
1160        let layout = Layout::vertical([Constraint::Percentage(50), Constraint::Length(8)])
1161            .boxed(true)
1162            .split(area);
1163
1164        assert_eq!(layout[0].height, 2, "{}, {}", err_msg, "Percentage");
1165        assert_eq!(layout[1].height, 8, "{}, {}", err_msg, "Length");
1166    }
1167
1168    #[test]
1169    fn test_vertical_split_by_height() {
1170        let target = Rect {
1171            x: 2,
1172            y: 2,
1173            width: 10,
1174            height: 10,
1175        };
1176
1177        let chunks = Layout::vertical([
1178            Constraint::Percentage(10),
1179            Constraint::Max(5),
1180            Constraint::Min(1),
1181        ])
1182        .split(target);
1183
1184        assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
1185        chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
1186    }
1187
1188    #[test]
1189    fn test_rect_size_truncation() {
1190        for width in 256u16..300u16 {
1191            for height in 256u16..300u16 {
1192                let rect = Rect::new(0, 0, width, height);
1193                rect.area(); // Should not panic.
1194                assert!(rect.width < width || rect.height < height);
1195                // The target dimensions are rounded down so the math will not be too precise
1196                // but let's make sure the ratios don't diverge crazily.
1197                assert!(
1198                    (f64::from(rect.width) / f64::from(rect.height)
1199                        - f64::from(width) / f64::from(height))
1200                    .abs()
1201                        < 1.0
1202                )
1203            }
1204        }
1205
1206        // One dimension below 255, one above. Area above max u16.
1207        let width = 900;
1208        let height = 100;
1209        let rect = Rect::new(0, 0, width, height);
1210        assert_ne!(rect.width, 900);
1211        assert_ne!(rect.height, 100);
1212        assert!(rect.width < width || rect.height < height);
1213    }
1214
1215    #[test]
1216    fn test_rect_size_preservation() {
1217        for width in 0..256u16 {
1218            for height in 0..256u16 {
1219                let rect = Rect::new(0, 0, width, height);
1220                rect.area(); // Should not panic.
1221                assert_eq!(rect.width, width);
1222                assert_eq!(rect.height, height);
1223            }
1224        }
1225
1226        // One dimension below 255, one above. Area below max u16.
1227        let rect = Rect::new(0, 0, 300, 100);
1228        assert_eq!(rect.width, 300);
1229        assert_eq!(rect.height, 100);
1230    }
1231}