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#[derive(Clone, Debug)]
9pub struct Pane<'a> {
10 pub title: String,
12
13 pub icon: Option<String>,
15
16 pub padding: (u16, u16, u16, u16),
18
19 pub text_footer: Option<Line<'a>>,
21
22 pub footer_height: u16,
24
25 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 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 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
50 self.icon = Some(icon.into());
51 self
52 }
53
54 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 pub fn with_uniform_padding(mut self, padding: u16) -> Self {
62 self.padding = (padding, padding, padding, padding);
63 self
64 }
65
66 pub fn with_text_footer(mut self, footer: Line<'a>) -> Self {
68 self.text_footer = Some(footer);
69 self
70 }
71
72 pub fn with_footer_height(mut self, height: u16) -> Self {
74 self.footer_height = height;
75 self
76 }
77
78 pub fn border_style(mut self, style: Style) -> Self {
80 self.border_style = style;
81 self
82 }
83
84 pub fn border_type(mut self, border_type: BorderType) -> Self {
86 self.border_type = border_type;
87 self
88 }
89
90 pub fn title_style(mut self, style: Style) -> Self {
92 self.title_style = style;
93 self
94 }
95
96 pub fn footer_style(mut self, style: Style) -> Self {
98 self.footer_style = style;
99 self
100 }
101
102 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 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 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 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 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 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 self.footer_height == 0 {
171 frame.render_widget(content, inner);
172 return;
173 }
174
175 let chunks = Layout::default()
177 .direction(Direction::Vertical)
178 .constraints([
179 Constraint::Min(0), Constraint::Length(self.footer_height), ])
182 .split(inner);
183
184 frame.render_widget(content, chunks[0]);
185 frame.render_widget(footer, chunks[1]);
186 }
187
188 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 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 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 self.footer_height == 0 {
219 return (inner, None);
220 }
221
222 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}