reratui_core/
layout.rs

1//! Layout wrapper components for RSX macro support
2//!
3//! This module provides wrapper components that enable ratatui's Layout and Block
4//! to work with nested children in the RSX macro system.
5
6use crate::vnode::Element;
7use ratatui::{
8    buffer::Buffer,
9    layout::{Constraint, Layout, Rect},
10    widgets::{Block, List, Paragraph, Widget},
11};
12
13/// An enum that can hold different types of widgets
14#[derive(Clone)]
15pub enum AnyWidget {
16    /// A layout wrapper widget
17    Layout(LayoutWrapper),
18    /// A block wrapper widget
19    Block(BlockWrapper),
20    /// A paragraph widget
21    Paragraph(Paragraph<'static>),
22    /// A list widget
23    List(List<'static>),
24    /// A VNode widget
25    VNode(Element),
26}
27
28impl Widget for AnyWidget {
29    fn render(self, area: Rect, buffer: &mut Buffer) {
30        match self {
31            AnyWidget::Layout(layout) => layout.render(area, buffer),
32            AnyWidget::Block(block) => block.render(area, buffer),
33            AnyWidget::Paragraph(paragraph) => paragraph.render(area, buffer),
34            AnyWidget::List(list) => list.render(area, buffer),
35            AnyWidget::VNode(vnode) => vnode.render(area, buffer),
36        }
37    }
38}
39
40impl From<LayoutWrapper> for AnyWidget {
41    fn from(layout: LayoutWrapper) -> Self {
42        AnyWidget::Layout(layout)
43    }
44}
45
46impl From<BlockWrapper> for AnyWidget {
47    fn from(block: BlockWrapper) -> Self {
48        AnyWidget::Block(block)
49    }
50}
51
52impl From<Paragraph<'static>> for AnyWidget {
53    fn from(paragraph: Paragraph<'static>) -> Self {
54        AnyWidget::Paragraph(paragraph)
55    }
56}
57
58impl From<List<'static>> for AnyWidget {
59    fn from(list: List<'static>) -> Self {
60        AnyWidget::List(list)
61    }
62}
63
64impl From<Element> for AnyWidget {
65    fn from(vnode: Element) -> Self {
66        AnyWidget::VNode(vnode)
67    }
68}
69
70impl From<Block<'static>> for AnyWidget {
71    fn from(block: Block<'static>) -> Self {
72        AnyWidget::Block(BlockWrapper::new(block, vec![]))
73    }
74}
75
76impl From<String> for AnyWidget {
77    fn from(text: String) -> Self {
78        AnyWidget::VNode(Element::text(text))
79    }
80}
81
82impl From<&str> for AnyWidget {
83    fn from(text: &str) -> Self {
84        AnyWidget::VNode(Element::text(text.to_string()))
85    }
86}
87
88impl From<&String> for AnyWidget {
89    fn from(text: &String) -> Self {
90        AnyWidget::VNode(Element::text(text.clone()))
91    }
92}
93
94impl From<ratatui::text::Span<'static>> for AnyWidget {
95    fn from(span: ratatui::text::Span<'static>) -> Self {
96        // Wrap the span in a paragraph to make it renderable
97        use ratatui::text::Line;
98        let line = Line::from(vec![span]);
99        let paragraph = Paragraph::new(vec![line]);
100        AnyWidget::Paragraph(paragraph)
101    }
102}
103
104impl From<ratatui::text::Line<'static>> for AnyWidget {
105    fn from(line: ratatui::text::Line<'static>) -> Self {
106        let paragraph = Paragraph::new(vec![line]);
107        AnyWidget::Paragraph(paragraph)
108    }
109}
110
111/// A wrapper around ratatui's Layout that can render children in split areas
112#[derive(Clone)]
113pub struct LayoutWrapper {
114    layout: Layout,
115    children: Vec<AnyWidget>,
116    constraints: Option<Vec<Constraint>>,
117}
118
119impl LayoutWrapper {
120    /// Creates a new LayoutWrapper
121    pub fn new(layout: Layout, children: Vec<AnyWidget>) -> Self {
122        Self {
123            layout,
124            children,
125            constraints: None,
126        }
127    }
128
129    /// Creates a new LayoutWrapper with custom constraints
130    pub fn with_constraints(
131        layout: Layout,
132        children: Vec<AnyWidget>,
133        constraints: Vec<Constraint>,
134    ) -> Self {
135        Self {
136            layout,
137            children,
138            constraints: Some(constraints),
139        }
140    }
141}
142
143impl Widget for LayoutWrapper {
144    fn render(self, area: Rect, buffer: &mut Buffer) {
145        // Use custom constraints if provided, otherwise create default constraints
146        let constraints = if let Some(custom_constraints) = self.constraints {
147            custom_constraints
148        } else {
149            // Create better default constraints based on the number of children
150            match self.children.len() {
151                0 => vec![],
152                1 => vec![Constraint::Percentage(100)],
153                2 => vec![Constraint::Percentage(50), Constraint::Percentage(50)],
154                3 => vec![
155                    Constraint::Percentage(33),
156                    Constraint::Percentage(33),
157                    Constraint::Percentage(34),
158                ],
159                4 => vec![Constraint::Percentage(25); 4],
160                5 => vec![Constraint::Percentage(20); 5],
161                _ => {
162                    // For more than 5 children, use equal distribution
163                    let percentage = 100 / self.children.len() as u16;
164                    let remainder = 100 % self.children.len() as u16;
165                    let mut constraints =
166                        vec![Constraint::Percentage(percentage); self.children.len()];
167                    if remainder > 0 && !constraints.is_empty() {
168                        // Add the remainder to the last constraint
169                        if let Some(last) = constraints.last_mut() {
170                            *last = Constraint::Percentage(percentage + remainder);
171                        }
172                    }
173                    constraints
174                }
175            }
176        };
177
178        // Create a new layout with the constraints
179        let layout = self.layout.constraints(constraints);
180        let chunks = layout.split(area);
181
182        // Render each child in its corresponding chunk
183        for (i, child) in self.children.into_iter().enumerate() {
184            if i < chunks.len() {
185                child.render(chunks[i], buffer);
186            }
187        }
188    }
189}
190
191/// A wrapper around ratatui's Block that can render children inside the block
192#[derive(Clone)]
193pub struct BlockWrapper {
194    block: Block<'static>,
195    children: Vec<AnyWidget>,
196}
197
198impl BlockWrapper {
199    /// Creates a new BlockWrapper
200    pub fn new(block: Block<'static>, children: Vec<AnyWidget>) -> Self {
201        Self { block, children }
202    }
203}
204
205impl Widget for BlockWrapper {
206    fn render(self, area: Rect, buffer: &mut Buffer) {
207        // Calculate the inner area before rendering the block
208        let inner_area = self.block.inner(area);
209
210        // Render the block border
211        self.block.render(area, buffer);
212
213        // Render children in the inner area
214        if !self.children.is_empty() {
215            if self.children.len() == 1 {
216                // Single child - render directly in the inner area
217                self.children
218                    .into_iter()
219                    .next()
220                    .unwrap()
221                    .render(inner_area, buffer);
222            } else {
223                // Multiple children - create a vertical layout
224                let constraints: Vec<Constraint> = (0..self.children.len())
225                    .map(|_| Constraint::Min(0))
226                    .collect();
227
228                let layout = Layout::default()
229                    .direction(ratatui::layout::Direction::Vertical)
230                    .constraints(constraints);
231
232                let chunks = layout.split(inner_area);
233
234                for (i, child) in self.children.into_iter().enumerate() {
235                    if i < chunks.len() {
236                        child.render(chunks[i], buffer);
237                    }
238                }
239            }
240        }
241    }
242}