Skip to main content

tui_lipan/widgets/flow/
mod.rs

1mod layout;
2mod node;
3mod reconcile;
4
5pub(crate) use self::layout::{measure_flow, pack_rows};
6pub use self::node::FlowNode;
7pub(crate) use self::reconcile::reconcile_flow;
8
9use crate::core::element::{Element, ElementKind};
10use crate::style::{
11    Align, BorderStyle, Justify, LayoutConstraints, Length, Padding, ShrinkPriority, Style,
12};
13
14/// A horizontal wrapping container.
15///
16/// Packs children left-to-right, starting a new row whenever the next child
17/// would exceed the available width. Supports `gap`, `align` (cross-axis),
18/// `justify` (main-axis, applied per wrapped row), `padding`, and `border` -
19/// the same chrome primitives as [`crate::prelude::HStack`] /
20/// [`crate::prelude::VStack`].
21#[derive(Clone)]
22pub struct Flow {
23    pub(crate) children: Vec<Element>,
24    pub(crate) gap: u16,
25    /// Vertical gap between wrapped rows. When `None`, falls back to `gap` so
26    /// the spacing is symmetric by default.
27    pub(crate) row_gap: Option<u16>,
28    pub(crate) align: Align,
29    pub(crate) justify: Justify,
30    pub(crate) padding: Padding,
31    pub(crate) border: bool,
32    pub(crate) border_style: BorderStyle,
33    pub(crate) style: Style,
34    pub(crate) width: Length,
35    pub(crate) height: Length,
36    /// When `true`, a parent stack may shrink this Flow below its widest child
37    /// (its items then clip/ellipsize) so rigid siblings keep their width. When
38    /// `false` (default) the Flow reserves at least its widest child as a
39    /// main-axis floor and wraps onto more rows instead of truncating.
40    pub(crate) shrinkable: bool,
41}
42
43impl Default for Flow {
44    fn default() -> Self {
45        Self {
46            children: Vec::new(),
47            gap: 0,
48            row_gap: None,
49            align: Align::Start,
50            justify: Justify::Start,
51            padding: Padding::default(),
52            border: false,
53            border_style: BorderStyle::Plain,
54            style: Style::default(),
55            width: Length::Flex(1),
56            height: Length::Auto,
57            shrinkable: false,
58        }
59    }
60}
61
62impl Flow {
63    /// Create an empty Flow.
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Add a child.
69    pub fn child(mut self, child: impl Into<Element>) -> Self {
70        self.children.push(child.into());
71        self
72    }
73
74    /// Replace all children, discarding anything already added with
75    /// [`child`](Self::child). Call `child` repeatedly to append instead.
76    pub fn children(mut self, children: impl IntoIterator<Item = Element>) -> Self {
77        self.children = children.into_iter().collect();
78        self
79    }
80
81    /// Set gap between items and rows.
82    pub fn gap(mut self, gap: u16) -> Self {
83        self.gap = gap;
84        self
85    }
86
87    /// Set the vertical gap between wrapped rows, independent of the horizontal
88    /// item `gap`. Useful for hint/footer rows that want spacing between items
89    /// but tightly stacked rows when they wrap.
90    pub fn row_gap(mut self, row_gap: u16) -> Self {
91        self.row_gap = Some(row_gap);
92        self
93    }
94
95    /// Allow a parent stack to shrink this Flow below its widest child, clipping
96    /// or ellipsizing items, so rigid siblings keep their width. By default a
97    /// Flow reserves its widest child and wraps onto more rows instead. Use this
98    /// for the lower-priority group in a competing row (e.g. hints beside
99    /// must-stay-readable action buttons).
100    pub fn shrinkable(mut self, shrinkable: bool) -> Self {
101        self.shrinkable = shrinkable;
102        self
103    }
104
105    /// Set cross-axis alignment within each row.
106    pub fn align(mut self, align: Align) -> Self {
107        self.align = align;
108        self
109    }
110
111    /// Set main-axis distribution of items within each wrapped row.
112    ///
113    /// Applied per row: each row distributes its own leftover width, so
114    /// `SpaceBetween` pushes the first item of every row to the left edge and
115    /// the last to the right edge. Unlike stacks, Flow children are always
116    /// measured at their natural size, so the space variants work without any
117    /// explicit child sizing.
118    pub fn justify(mut self, justify: Justify) -> Self {
119        self.justify = justify;
120        self
121    }
122
123    /// Set padding around the inner content area.
124    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
125        self.padding = padding.into();
126        self
127    }
128
129    /// Draw a border around the container.
130    pub fn border(mut self, border: bool) -> Self {
131        self.border = border;
132        self
133    }
134
135    /// Set border style.
136    pub fn border_style(mut self, border_style: BorderStyle) -> Self {
137        self.border_style = border_style;
138        self
139    }
140
141    /// Set base style.
142    pub fn style(mut self, style: Style) -> Self {
143        self.style = style;
144        self
145    }
146
147    /// Override requested width.
148    pub fn width(mut self, width: Length) -> Self {
149        self.width = width;
150        self
151    }
152
153    /// Override requested height.
154    pub fn height(mut self, height: Length) -> Self {
155        self.height = height;
156        self
157    }
158}
159
160impl From<Flow> for Element {
161    fn from(value: Flow) -> Self {
162        let shrink_priority = if value.shrinkable {
163            ShrinkPriority::First
164        } else {
165            ShrinkPriority::Normal
166        };
167        Element::new(ElementKind::Flow(value)).with_layout(
168            LayoutConstraints::default()
169                .reflows(true)
170                .shrink_priority(shrink_priority),
171        )
172    }
173}
174
175impl crate::layout::hash::LayoutHash for Flow {
176    fn layout_hash(
177        &self,
178        hasher: &mut impl std::hash::Hasher,
179        recurse: &dyn Fn(&Element) -> Option<u64>,
180    ) -> Option<()> {
181        use std::hash::Hash;
182
183        self.gap.hash(hasher);
184        self.row_gap.hash(hasher);
185        self.align.hash(hasher);
186        self.justify.hash(hasher);
187        self.padding.hash(hasher);
188        self.border.hash(hasher);
189        self.border_style.hash(hasher);
190        self.width.hash(hasher);
191        self.height.hash(hasher);
192        self.shrinkable.hash(hasher);
193
194        let non_portal_count = self
195            .children
196            .iter()
197            .filter(|child| !matches!(child.kind, ElementKind::Portal(_)))
198            .count();
199        non_portal_count.hash(hasher);
200        for child in &self.children {
201            if matches!(child.kind, ElementKind::Portal(_)) {
202                continue;
203            }
204            recurse(child)?.hash(hasher);
205        }
206
207        Some(())
208    }
209}