Skip to main content

ratatui_toolkit/pane/
mod.rs

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