Skip to main content

ratatui_toolkit/
pane.rs

1use ratatui::layout::{Constraint, Direction, Layout, Rect};
2use ratatui::style::{Color, Modifier, Style};
3use ratatui::text::{Line, Span};
4use ratatui::widgets::{Block, BorderType, Borders, Paragraph, Widget};
5use ratatui::Frame;
6
7/// A styled panel component with title, icon, padding, and optional footer
8#[derive(Clone, Debug)]
9pub struct Pane<'a> {
10    /// Title text for the pane
11    pub title: String,
12
13    /// Icon to display before the title (optional)
14    pub icon: Option<String>,
15
16    /// Padding around the content (top, right, bottom, left)
17    pub padding: (u16, u16, u16, u16),
18
19    /// Simple text footer (optional) - displayed in border
20    pub text_footer: Option<Line<'a>>,
21
22    /// Height of the footer area when using widget footers
23    pub footer_height: u16,
24
25    // Styling
26    pub border_style: Style,
27    pub border_type: BorderType,
28    pub title_style: Style,
29    pub footer_style: Style,
30}
31
32impl<'a> Pane<'a> {
33    /// Create a new pane with the given title
34    pub fn new(title: impl Into<String>) -> Self {
35        Self {
36            title: title.into(),
37            icon: None,
38            padding: (0, 0, 0, 0),
39            text_footer: None,
40            footer_height: 0,
41            border_style: Style::default().fg(Color::White),
42            border_type: BorderType::Rounded,
43            title_style: Style::default().add_modifier(Modifier::BOLD),
44            footer_style: Style::default().fg(Color::DarkGray),
45        }
46    }
47
48    /// Set the icon for the title
49    pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
50        self.icon = Some(icon.into());
51        self
52    }
53
54    /// Set the padding (top, right, bottom, left)
55    pub fn with_padding(mut self, top: u16, right: u16, bottom: u16, left: u16) -> Self {
56        self.padding = (top, right, bottom, left);
57        self
58    }
59
60    /// Set uniform padding on all sides
61    pub fn with_uniform_padding(mut self, padding: u16) -> Self {
62        self.padding = (padding, padding, padding, padding);
63        self
64    }
65
66    /// Set a simple text footer (displayed in the border)
67    pub fn with_text_footer(mut self, footer: Line<'a>) -> Self {
68        self.text_footer = Some(footer);
69        self
70    }
71
72    /// Set the height for the footer area (when using widget footers)
73    pub fn with_footer_height(mut self, height: u16) -> Self {
74        self.footer_height = height;
75        self
76    }
77
78    /// Set the border style
79    pub fn border_style(mut self, style: Style) -> Self {
80        self.border_style = style;
81        self
82    }
83
84    /// Set the border type
85    pub fn border_type(mut self, border_type: BorderType) -> Self {
86        self.border_type = border_type;
87        self
88    }
89
90    /// Set the title style
91    pub fn title_style(mut self, style: Style) -> Self {
92        self.title_style = style;
93        self
94    }
95
96    /// Set the footer style
97    pub fn footer_style(mut self, style: Style) -> Self {
98        self.footer_style = style;
99        self
100    }
101
102    /// Build the title line with optional icon
103    fn build_title_line(&self) -> Line<'a> {
104        let mut spans = vec![Span::raw(" ")];
105
106        if let Some(ref icon) = self.icon {
107            spans.push(Span::styled(icon.clone(), self.title_style));
108            spans.push(Span::raw(" "));
109        }
110
111        spans.push(Span::styled(self.title.clone(), self.title_style));
112        spans.push(Span::raw(" "));
113
114        Line::from(spans)
115    }
116
117    /// Get the inner area after applying padding
118    fn get_padded_area(&self, area: Rect) -> Rect {
119        Rect {
120            x: area.x + self.padding.3,
121            y: area.y + self.padding.0,
122            width: area.width.saturating_sub(self.padding.1 + self.padding.3),
123            height: area.height.saturating_sub(self.padding.0 + self.padding.2),
124        }
125    }
126
127    /// Build the block with title and optional text footer
128    fn build_block(&self) -> Block<'a> {
129        let mut block = Block::default()
130            .borders(Borders::ALL)
131            .border_type(self.border_type)
132            .border_style(self.border_style)
133            .title(self.build_title_line());
134
135        // Add text footer if present
136        if let Some(ref footer) = self.text_footer {
137            block = block.title_bottom(footer.clone().style(self.footer_style));
138        }
139
140        block
141    }
142
143    /// Render the pane with the given content widget (no footer widget)
144    pub fn render<W>(&self, frame: &mut Frame, area: Rect, content: W)
145    where
146        W: Widget,
147    {
148        let padded_area = self.get_padded_area(area);
149        let block = self.build_block();
150        let inner = block.inner(padded_area);
151
152        frame.render_widget(block, padded_area);
153        frame.render_widget(content, inner);
154    }
155
156    /// Render the pane with content and a footer widget
157    /// The footer area will be `footer_height` lines tall at the bottom
158    pub fn render_with_footer<C, F>(&self, frame: &mut Frame, area: Rect, content: C, footer: F)
159    where
160        C: Widget,
161        F: Widget,
162    {
163        let padded_area = self.get_padded_area(area);
164        let block = self.build_block();
165        let inner = block.inner(padded_area);
166
167        frame.render_widget(block, padded_area);
168
169        // If footer height is 0, just render content
170        if self.footer_height == 0 {
171            frame.render_widget(content, inner);
172            return;
173        }
174
175        // Split the inner area into content and footer
176        let chunks = Layout::default()
177            .direction(Direction::Vertical)
178            .constraints([
179                Constraint::Min(0),                     // Content area
180                Constraint::Length(self.footer_height), // Footer area
181            ])
182            .split(inner);
183
184        frame.render_widget(content, chunks[0]);
185        frame.render_widget(footer, chunks[1]);
186    }
187
188    /// Render the pane with a paragraph content
189    pub fn render_paragraph(&self, frame: &mut Frame, area: Rect, content: Vec<Line<'a>>) {
190        let paragraph = Paragraph::new(content);
191        self.render(frame, area, paragraph);
192    }
193
194    /// Render the pane with paragraph content and a footer widget
195    pub fn render_paragraph_with_footer<F>(
196        &self,
197        frame: &mut Frame,
198        area: Rect,
199        content: Vec<Line<'a>>,
200        footer: F,
201    ) where
202        F: Widget,
203    {
204        let paragraph = Paragraph::new(content);
205        self.render_with_footer(frame, area, paragraph, footer);
206    }
207
208    /// Render just the pane block and return the inner area (useful for custom rendering)
209    /// Returns (content_area, optional_footer_area)
210    pub fn render_block(&self, frame: &mut Frame, area: Rect) -> (Rect, Option<Rect>) {
211        let padded_area = self.get_padded_area(area);
212        let block = self.build_block();
213        let inner = block.inner(padded_area);
214
215        frame.render_widget(block, padded_area);
216
217        // If no footer height, return just the content area
218        if self.footer_height == 0 {
219            return (inner, None);
220        }
221
222        // Split into content and footer areas
223        let chunks = Layout::default()
224            .direction(Direction::Vertical)
225            .constraints([Constraint::Min(0), Constraint::Length(self.footer_height)])
226            .split(inner);
227
228        (chunks[0], Some(chunks[1]))
229    }
230}