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}