Skip to main content

rpi_tui/
vstack.rs

1//! VStack - Vertical stack layout component.
2//!
3//! Implements the vertical stack layout as described in `tui-plan.md`.
4
5use std::any::Any;
6use std::sync::{Arc, Mutex};
7
8use super::component::Component;
9use super::layout_node::{LayoutNode, LayoutNodeProvider, StackLayoutEntry, VStackNode};
10
11/// Options for a stack entry.
12#[derive(Debug, Clone, Default)]
13pub struct StackEntryOptions {
14    /// Initial size on the stack's main axis. Defaults to "auto" (None = auto).
15    pub basis: Option<usize>,
16    /// Share of positive remaining space. Defaults to 0.
17    pub grow: usize,
18    /// Relative willingness to shrink when content overflows. Defaults to 1.
19    pub shrink: usize,
20    /// Minimum allocated size on the main axis. Defaults to 0.
21    pub min_size: usize,
22    /// Maximum allocated size on the main axis. None = no limit.
23    pub max_size: Option<usize>,
24}
25
26impl StackEntryOptions {
27    /// Create options with basis (fixed size).
28    pub fn with_basis(size: usize) -> Self {
29        Self {
30            basis: Some(size),
31            ..Default::default()
32        }
33    }
34
35    /// Create options with auto sizing.
36    pub fn auto() -> Self {
37        Self::default()
38    }
39
40    /// Set the basis (fixed size on main axis).
41    pub fn basis(mut self, size: usize) -> Self {
42        self.basis = Some(size);
43        self
44    }
45
46    /// Set the grow factor.
47    pub fn grow(mut self, factor: usize) -> Self {
48        self.grow = factor;
49        self
50    }
51
52    /// Set the shrink factor.
53    pub fn shrink(mut self, factor: usize) -> Self {
54        self.shrink = factor;
55        self
56    }
57
58    /// Set the minimum size.
59    pub fn min_size(mut self, size: usize) -> Self {
60        self.min_size = size;
61        self
62    }
63
64    /// Set the maximum size.
65    pub fn max_size(mut self, size: usize) -> Self {
66        self.max_size = Some(size);
67        self
68    }
69}
70
71/// A child entry in a stack.
72#[derive(Clone)]
73pub struct StackEntry {
74    pub component: Arc<dyn Component>,
75    pub options: StackEntryOptions,
76}
77
78impl StackEntry {
79    /// Create a new stack entry with default options.
80    pub fn new(component: Arc<dyn Component>) -> Self {
81        Self {
82            component,
83            options: StackEntryOptions::default(),
84        }
85    }
86
87    /// Create a new stack entry with options.
88    pub fn with_options(component: Arc<dyn Component>, options: StackEntryOptions) -> Self {
89        Self { component, options }
90    }
91
92    /// Set the basis (fixed size on main axis).
93    pub fn basis(mut self, size: usize) -> Self {
94        self.options.basis = Some(size);
95        self
96    }
97
98    /// Set the grow factor.
99    pub fn grow(mut self, factor: usize) -> Self {
100        self.options.grow = factor;
101        self
102    }
103
104    /// Set the shrink factor.
105    pub fn shrink(mut self, factor: usize) -> Self {
106        self.options.shrink = factor;
107        self
108    }
109
110    /// Set the minimum size.
111    pub fn min_size(mut self, size: usize) -> Self {
112        self.options.min_size = size;
113        self
114    }
115
116    /// Convert to a StackLayoutEntry for the layout system.
117    pub fn to_layout_entry(&self) -> StackLayoutEntry {
118        StackLayoutEntry {
119            component: self.component.clone(),
120            basis: self.options.basis,
121            grow: self.options.grow,
122            shrink: self.options.shrink,
123            min_size: self.options.min_size,
124            max_size: self.options.max_size.unwrap_or(usize::MAX),
125            visible: None,
126        }
127    }
128}
129
130/// A child that can be either a component or a stack entry.
131pub enum StackChild {
132    Component(Arc<dyn Component>),
133    Entry(StackEntry),
134}
135
136impl From<Arc<dyn Component>> for StackChild {
137    fn from(component: Arc<dyn Component>) -> Self {
138        StackChild::Component(component)
139    }
140}
141
142impl From<StackEntry> for StackChild {
143    fn from(entry: StackEntry) -> Self {
144        StackChild::Entry(entry)
145    }
146}
147
148/// Options for a stack.
149#[derive(Debug, Clone, Default)]
150pub struct StackOptions {
151    /// Gap between children in rows/columns.
152    pub gap: usize,
153    /// Alignment on the cross axis.
154    pub align: StackAlign,
155}
156
157/// Alignment options for stack children.
158#[derive(Debug, Clone, Copy, Default)]
159pub enum StackAlign {
160    #[default]
161    Stretch,
162    Start,
163    Center,
164    End,
165}
166
167/// VStack - Vertical stack layout.
168///
169/// Children are arranged from top to bottom.
170#[derive(Clone)]
171pub struct VStack {
172    children: Arc<Mutex<Vec<StackEntry>>>,
173    options: StackOptions,
174}
175
176impl VStack {
177    /// Create a new empty VStack.
178    pub fn new() -> Self {
179        Self {
180            children: Arc::new(Mutex::new(Vec::new())),
181            options: StackOptions::default(),
182        }
183    }
184
185    /// Create a new VStack with options.
186    pub fn with_options(options: StackOptions) -> Self {
187        Self {
188            children: Arc::new(Mutex::new(Vec::new())),
189            options,
190        }
191    }
192
193    /// Create a VStack from a vector of children.
194    pub fn from_children(children: Vec<StackChild>) -> Self {
195        let entries: Vec<StackEntry> = children
196            .into_iter()
197            .map(|child| match child {
198                StackChild::Component(c) => StackEntry::new(c),
199                StackChild::Entry(e) => e,
200            })
201            .collect();
202
203        Self {
204            children: Arc::new(Mutex::new(entries)),
205            options: StackOptions::default(),
206        }
207    }
208
209    /// Create a VStack from a vector of stack entries.
210    pub fn from_entries(children: Vec<StackEntry>) -> Self {
211        Self {
212            children: Arc::new(Mutex::new(children)),
213            options: StackOptions::default(),
214        }
215    }
216
217    /// Add a child with default options.
218    pub fn add_child(&self, component: Arc<dyn Component>) {
219        if let Ok(mut children) = self.children.lock() {
220            children.push(StackEntry::new(component));
221        }
222    }
223
224    /// Add a child with options.
225    pub fn add_child_with_options(
226        &self,
227        component: Arc<dyn Component>,
228        options: StackEntryOptions,
229    ) {
230        if let Ok(mut children) = self.children.lock() {
231            children.push(StackEntry::with_options(component, options));
232        }
233    }
234
235    /// Remove a child.
236    pub fn remove_child(&self, component: &Arc<dyn Component>) {
237        if let Ok(mut children) = self.children.lock() {
238            children.retain(|e| !Arc::ptr_eq(&e.component, component));
239        }
240    }
241
242    /// Clear all children.
243    pub fn clear(&self) {
244        if let Ok(mut children) = self.children.lock() {
245            children.clear();
246        }
247    }
248
249    /// Get the gap between children.
250    pub fn gap(&self) -> usize {
251        self.options.gap
252    }
253
254    /// Get the alignment.
255    pub fn align(&self) -> StackAlign {
256        self.options.align
257    }
258
259    /// Get a clone of the children list.
260    pub fn get_children(&self) -> Vec<StackEntry> {
261        self.children.lock().map(|c| c.clone()).unwrap_or_default()
262    }
263
264    /// Get layout entries for the layout system.
265    pub fn get_layout_entries(&self) -> Vec<StackLayoutEntry> {
266        self.children
267            .lock()
268            .map(|c| c.iter().map(|e| e.to_layout_entry()).collect())
269            .unwrap_or_default()
270    }
271
272    /// Calculate the intrinsic height of all children.
273    #[allow(dead_code)]
274    fn intrinsic_height(&self, width: usize) -> usize {
275        if let Ok(children) = self.children.lock() {
276            let mut total = 0;
277            for (i, entry) in children.iter().enumerate() {
278                let child_height = entry.component.render(width).len();
279                total += child_height.max(entry.options.min_size);
280                // Add gap between children
281                if i > 0 && self.options.gap > 0 {
282                    total += self.options.gap;
283                }
284            }
285            total
286        } else {
287            0
288        }
289    }
290}
291
292impl Default for VStack {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298impl Component for VStack {
299    fn render(&self, width: usize) -> Vec<String> {
300        let mut lines = Vec::new();
301
302        if let Ok(children) = self.children.lock() {
303            // Simple rendering: render each child and concatenate
304            // Full constraint-based layout would be implemented for TuiAltScreen
305            for (i, entry) in children.iter().enumerate() {
306                let child_lines = entry.component.render(width);
307                lines.extend(child_lines);
308
309                // Add gap between children
310                if i < children.len() - 1 && self.options.gap > 0 {
311                    for _ in 0..self.options.gap {
312                        lines.push(String::new());
313                    }
314                }
315            }
316        }
317
318        lines
319    }
320
321    fn invalidate(&self) {
322        if let Ok(children) = self.children.lock() {
323            for entry in children.iter() {
324                entry.component.invalidate();
325            }
326        }
327    }
328
329    fn as_any(&self) -> &dyn Any {
330        self
331    }
332}
333
334impl LayoutNodeProvider for VStack {
335    fn layout_node(&self) -> Option<LayoutNode> {
336        Some(LayoutNode::VStack(VStackNode {
337            entries: self.get_layout_entries(),
338            gap: self.options.gap,
339            align: self.options.align,
340        }))
341    }
342}
343
344/// Constrained layout calculation for VStack.
345///
346/// Given a fixed height, distribute space among children based on their
347/// basis, grow, shrink, and min_size options.
348///
349/// Intrinsic heights come from [`Component::render`]; callers that already
350/// have a render cache should use [`layout_vstack_constrained_with`] so a
351/// component measured here is not rendered a second time when it is painted.
352pub fn layout_vstack_constrained(
353    children: &[StackEntry],
354    width: usize,
355    height: usize,
356    gap: usize,
357) -> Vec<(Arc<dyn Component>, usize)> {
358    layout_vstack_constrained_with(children, width, height, gap, |component, width| {
359        component.render(width).len()
360    })
361}
362
363/// Like [`layout_vstack_constrained`], but measures auto-basis children through
364/// the supplied closure (usually a frame-local render cache).
365///
366/// Mirrors upstream routing every `measureHeight` through `renderCached`
367/// (`packages/tui/src/layout.ts`).
368pub(crate) fn layout_vstack_constrained_with<F>(
369    children: &[StackEntry],
370    width: usize,
371    height: usize,
372    gap: usize,
373    mut measure: F,
374) -> Vec<(Arc<dyn Component>, usize)>
375where
376    F: FnMut(&Arc<dyn Component>, usize) -> usize,
377{
378    let n = children.len();
379    if n == 0 {
380        return Vec::new();
381    }
382
383    let total_gap = if n > 1 { gap * (n - 1) } else { 0 };
384    let available = height.saturating_sub(total_gap);
385
386    // First pass: calculate basis sizes and intrinsic heights
387    let mut sizes: Vec<usize> = Vec::with_capacity(n);
388    let mut total_basis = 0usize;
389    let mut total_grow = 0usize;
390    let mut grow_indices = Vec::new();
391
392    for (i, entry) in children.iter().enumerate() {
393        // An explicit basis does not depend on intrinsic content. The
394        // transcript scroll view uses basis 0 + grow 1, so measuring it here
395        // used to render the complete history before its actual paint.
396        let basis = entry
397            .options
398            .basis
399            .unwrap_or_else(|| measure(&entry.component, width));
400        let min_size = entry.options.min_size;
401        let max_size = entry.options.max_size.unwrap_or(usize::MAX);
402
403        let size = basis.clamp(min_size, max_size);
404        sizes.push(size);
405        total_basis += size;
406
407        if entry.options.grow > 0 {
408            total_grow += entry.options.grow;
409            grow_indices.push(i);
410        }
411    }
412
413    // Distribute remaining space or shrink
414    if total_basis < available && total_grow > 0 {
415        // Distribute remaining space
416        let remaining = available - total_basis;
417        let mut to_distribute = remaining;
418
419        for &i in &grow_indices {
420            if total_grow == 0 {
421                break;
422            }
423            let entry = &children[i];
424            let share = (remaining * entry.options.grow) / total_grow;
425            let max_size = entry.options.max_size.unwrap_or(usize::MAX);
426            let new_size = (sizes[i] + share).min(max_size);
427            let added = new_size - sizes[i];
428            sizes[i] = new_size;
429            to_distribute = to_distribute.saturating_sub(added);
430        }
431
432        // Distribute any leftover
433        for &i in &grow_indices {
434            if to_distribute == 0 {
435                break;
436            }
437            let entry = &children[i];
438            let max_size = entry.options.max_size.unwrap_or(usize::MAX);
439            if sizes[i] < max_size {
440                sizes[i] += 1;
441                to_distribute -= 1;
442            }
443        }
444    } else if total_basis > available {
445        // Need to shrink
446        let overflow = total_basis - available;
447        let mut to_shrink = overflow;
448
449        // Find shrinkable entries
450        let shrinkable: Vec<(usize, usize)> = children
451            .iter()
452            .enumerate()
453            .filter(|(i, e)| e.options.shrink > 0 && sizes[*i] > e.options.min_size)
454            .map(|(i, e)| (i, e.options.shrink))
455            .collect();
456
457        let total_shrink: usize = shrinkable.iter().map(|(_, s)| *s).sum();
458
459        if total_shrink > 0 {
460            for (i, shrink) in &shrinkable {
461                if to_shrink == 0 {
462                    break;
463                }
464                let share = (overflow * *shrink) / total_shrink;
465                let min_size = children[*i].options.min_size;
466                let new_size = sizes[*i].saturating_sub(share).max(min_size);
467                let removed = sizes[*i] - new_size;
468                sizes[*i] = new_size;
469                to_shrink = to_shrink.saturating_sub(removed);
470            }
471        }
472    }
473
474    // Return component and size pairs
475    children
476        .iter()
477        .zip(sizes.into_iter())
478        .map(|(entry, size)| (entry.component.clone(), size))
479        .collect()
480}