1use std::sync::Arc;
7
8use super::component::Component;
9use super::vstack::StackAlign;
10
11#[derive(Debug, Clone, Copy)]
13pub struct LayoutViewport {
14 pub width: usize,
15 pub height: usize,
16}
17
18impl Default for LayoutViewport {
19 fn default() -> Self {
20 Self {
21 width: 80,
22 height: 24,
23 }
24 }
25}
26
27#[derive(Clone)]
29pub enum LayoutNode {
30 VStack(VStackNode),
32 HStack(HStackNode),
34 Scroll(ScrollNode),
36}
37
38#[derive(Clone)]
40pub struct VStackNode {
41 pub entries: Vec<StackLayoutEntry>,
42 pub gap: usize,
43 pub align: StackAlign,
44}
45
46#[derive(Clone)]
48pub struct HStackNode {
49 pub entries: Vec<StackLayoutEntry>,
50 pub gap: usize,
51 pub align: StackAlign,
52}
53
54#[derive(Clone)]
56pub struct ScrollNode {
57 pub component: Arc<dyn Component>,
58 pub get_scroll_top: fn() -> usize,
60}
61
62impl ScrollNode {
63 pub fn new(component: Arc<dyn Component>) -> Self {
65 Self {
66 component,
67 get_scroll_top: || 0,
68 }
69 }
70
71 pub fn with_scroll_top(component: Arc<dyn Component>, get_scroll_top: fn() -> usize) -> Self {
73 Self {
74 component,
75 get_scroll_top,
76 }
77 }
78}
79
80#[derive(Clone)]
82pub struct StackLayoutEntry {
83 pub component: Arc<dyn Component>,
84 pub basis: Option<usize>,
85 pub grow: usize,
86 pub shrink: usize,
87 pub min_size: usize,
88 pub max_size: usize,
89 pub visible: Option<fn(LayoutViewport) -> bool>,
90}
91
92impl StackLayoutEntry {
93 pub fn new(component: Arc<dyn Component>) -> Self {
95 Self {
96 component,
97 basis: None,
98 grow: 0,
99 shrink: 1,
100 min_size: 0,
101 max_size: usize::MAX,
102 visible: None,
103 }
104 }
105
106 pub fn with_options(
108 component: Arc<dyn Component>,
109 basis: Option<usize>,
110 grow: usize,
111 shrink: usize,
112 min_size: usize,
113 max_size: usize,
114 ) -> Self {
115 Self {
116 component,
117 basis,
118 grow,
119 shrink,
120 min_size,
121 max_size,
122 visible: None,
123 }
124 }
125}
126
127pub trait LayoutNodeProvider: Component {
129 fn layout_node(&self) -> Option<LayoutNode>;
131}
132
133pub fn allocate_stack_sizes(
135 entries: &[StackLayoutEntry],
136 intrinsic_sizes: &[usize],
137 available_size: Option<usize>,
138 gap: usize,
139) -> Vec<usize> {
140 let n = entries.len();
141 if n == 0 {
142 return Vec::new();
143 }
144
145 let total_gap = if n > 1 { gap * (n - 1) } else { 0 };
146
147 let mut sizes: Vec<usize> = entries
149 .iter()
150 .enumerate()
151 .map(|(i, entry)| {
152 let basis = entry
153 .basis
154 .unwrap_or_else(|| intrinsic_sizes.get(i).copied().unwrap_or(0));
155 basis.clamp(entry.min_size, entry.max_size)
156 })
157 .collect();
158
159 if let Some(available) = available_size {
160 let content_available = available.saturating_sub(total_gap);
161 let total_basis: usize = sizes.iter().sum();
162
163 if total_basis < content_available {
164 let remaining = content_available - total_basis;
166 let total_grow: usize = entries.iter().map(|e| e.grow).sum();
167
168 if total_grow > 0 {
169 let mut to_distribute = remaining;
170
171 for (i, entry) in entries.iter().enumerate() {
173 if entry.grow > 0 && to_distribute > 0 {
174 let share = (remaining * entry.grow / total_grow).min(to_distribute);
175 let new_size = (sizes[i] + share).min(entry.max_size);
176 let added = new_size - sizes[i];
177 sizes[i] = new_size;
178 to_distribute -= added;
179 }
180 }
181
182 for (i, entry) in entries.iter().enumerate() {
184 if to_distribute == 0 {
185 break;
186 }
187 if entry.grow > 0 && sizes[i] < entry.max_size {
188 sizes[i] += 1;
189 to_distribute -= 1;
190 }
191 }
192 }
193 } else if total_basis > content_available {
194 let overflow = total_basis - content_available;
196 let mut to_shrink = overflow;
197
198 let shrinkable: Vec<(usize, usize)> = entries
200 .iter()
201 .enumerate()
202 .filter(|(i, e)| e.shrink > 0 && sizes[*i] > e.min_size)
203 .map(|(i, e)| (i, e.shrink * sizes[i].max(1)))
204 .collect();
205
206 let total_shrink: usize = shrinkable.iter().map(|(_, s)| *s).sum();
207
208 if total_shrink > 0 {
209 for (i, weight) in &shrinkable {
210 if to_shrink == 0 {
211 break;
212 }
213 let share = (overflow * weight / total_shrink).min(to_shrink);
214 let entry = &entries[*i];
215 let new_size = sizes[*i].saturating_sub(share).max(entry.min_size);
216 let removed = sizes[*i] - new_size;
217 sizes[*i] = new_size;
218 to_shrink -= removed;
219 }
220 }
221 }
222 }
223
224 sizes
225}
226
227pub fn visible_stack_entries(
229 entries: &[StackLayoutEntry],
230 viewport: LayoutViewport,
231) -> Vec<StackLayoutEntry> {
232 entries
233 .iter()
234 .filter(|entry| entry.visible.map(|v| v(viewport)).unwrap_or(true))
235 .cloned()
236 .collect()
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242 use crate::Text;
243
244 #[test]
245 fn test_allocate_stack_sizes_auto() {
246 let entries = vec![
247 StackLayoutEntry::new(Arc::new(Text::new("A", 0, 0))),
248 StackLayoutEntry::new(Arc::new(Text::new("B", 0, 0))),
249 ];
250
251 let intrinsic = vec![3, 3]; let sizes = allocate_stack_sizes(&entries, &intrinsic, None, 0);
253 assert_eq!(sizes, vec![3, 3]);
254 }
255
256 #[test]
257 fn test_allocate_stack_sizes_grow() {
258 let entries = vec![
259 StackLayoutEntry::with_options(
260 Arc::new(Text::new("A", 0, 0)),
261 Some(2),
262 1,
263 1,
264 0,
265 usize::MAX,
266 ),
267 StackLayoutEntry::with_options(
268 Arc::new(Text::new("B", 0, 0)),
269 Some(2),
270 1,
271 1,
272 0,
273 usize::MAX,
274 ),
275 ];
276
277 let intrinsic = vec![2, 2];
278 let sizes = allocate_stack_sizes(&entries, &intrinsic, Some(10), 0);
279 assert_eq!(sizes.iter().sum::<usize>(), 10);
280 }
281
282 #[test]
283 fn test_allocate_stack_sizes_shrink() {
284 let entries = vec![
285 StackLayoutEntry::with_options(
286 Arc::new(Text::new("A", 0, 0)),
287 Some(10),
288 0,
289 1,
290 2,
291 usize::MAX,
292 ),
293 StackLayoutEntry::with_options(
294 Arc::new(Text::new("B", 0, 0)),
295 Some(10),
296 0,
297 1,
298 2,
299 usize::MAX,
300 ),
301 ];
302
303 let intrinsic = vec![10, 10];
304 let sizes = allocate_stack_sizes(&entries, &intrinsic, Some(10), 0);
305 assert!(sizes[0] >= 2);
307 assert!(sizes[1] >= 2);
308 assert_eq!(sizes.iter().sum::<usize>(), 10);
309 }
310}