Skip to main content

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    /// Creates a new LayoutWrapper from Elements
143    pub fn from_elements(layout: Layout, children: Vec<Element>) -> Self {
144        Self {
145            layout,
146            children: children.into_iter().map(AnyWidget::from).collect(),
147            constraints: None,
148        }
149    }
150
151    /// Creates a new LayoutWrapper from Elements with custom constraints
152    pub fn from_elements_with_constraints(
153        layout: Layout,
154        children: Vec<Element>,
155        constraints: Vec<Constraint>,
156    ) -> Self {
157        Self {
158            layout,
159            children: children.into_iter().map(AnyWidget::from).collect(),
160            constraints: Some(constraints),
161        }
162    }
163}
164
165impl Widget for LayoutWrapper {
166    fn render(self, area: Rect, buffer: &mut Buffer) {
167        // Use custom constraints if provided, otherwise create default constraints
168        let constraints = if let Some(custom_constraints) = self.constraints {
169            custom_constraints
170        } else {
171            // Create better default constraints based on the number of children
172            match self.children.len() {
173                0 => vec![],
174                1 => vec![Constraint::Percentage(100)],
175                2 => vec![Constraint::Percentage(50), Constraint::Percentage(50)],
176                3 => vec![
177                    Constraint::Percentage(33),
178                    Constraint::Percentage(33),
179                    Constraint::Percentage(34),
180                ],
181                4 => vec![Constraint::Percentage(25); 4],
182                5 => vec![Constraint::Percentage(20); 5],
183                _ => {
184                    // For more than 5 children, use equal distribution
185                    let percentage = 100 / self.children.len() as u16;
186                    let remainder = 100 % self.children.len() as u16;
187                    let mut constraints =
188                        vec![Constraint::Percentage(percentage); self.children.len()];
189                    if remainder > 0 && !constraints.is_empty() {
190                        // Add the remainder to the last constraint
191                        if let Some(last) = constraints.last_mut() {
192                            *last = Constraint::Percentage(percentage + remainder);
193                        }
194                    }
195                    constraints
196                }
197            }
198        };
199
200        // Create a new layout with the constraints
201        let layout = self.layout.constraints(constraints);
202        let chunks = layout.split(area);
203
204        // Render each child in its corresponding chunk
205        for (i, child) in self.children.into_iter().enumerate() {
206            if i < chunks.len() {
207                child.render(chunks[i], buffer);
208            }
209        }
210    }
211}
212
213/// A wrapper around ratatui's Block that can render children inside the block
214#[derive(Clone)]
215pub struct BlockWrapper {
216    block: Block<'static>,
217    children: Vec<AnyWidget>,
218}
219
220impl BlockWrapper {
221    /// Creates a new BlockWrapper
222    pub fn new(block: Block<'static>, children: Vec<AnyWidget>) -> Self {
223        Self { block, children }
224    }
225}
226
227impl Widget for BlockWrapper {
228    fn render(self, area: Rect, buffer: &mut Buffer) {
229        // Calculate the inner area before rendering the block
230        let inner_area = self.block.inner(area);
231
232        // Render the block border
233        self.block.render(area, buffer);
234
235        // Render children in the inner area
236        if !self.children.is_empty() {
237            if self.children.len() == 1 {
238                // Single child - render directly in the inner area
239                self.children
240                    .into_iter()
241                    .next()
242                    .unwrap()
243                    .render(inner_area, buffer);
244            } else {
245                // Multiple children - create a vertical layout
246                let constraints: Vec<Constraint> = (0..self.children.len())
247                    .map(|_| Constraint::Min(0))
248                    .collect();
249
250                let layout = Layout::default()
251                    .direction(ratatui::layout::Direction::Vertical)
252                    .constraints(constraints);
253
254                let chunks = layout.split(inner_area);
255
256                for (i, child) in self.children.into_iter().enumerate() {
257                    if i < chunks.len() {
258                        child.render(chunks[i], buffer);
259                    }
260                }
261            }
262        }
263    }
264}