1use 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#[derive(Clone, Debug)]
13pub struct Pane<'a> {
14 pub title: String,
16
17 pub icon: Option<String>,
19
20 pub padding: (u16, u16, u16, u16),
22
23 pub text_footer: Option<Line<'a>>,
25
26 pub footer_height: u16,
28
29 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 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 pub fn with_icon(mut self, icon: impl Into<String>) -> Self {
54 self.icon = Some(icon.into());
55 self
56 }
57
58 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 pub fn with_uniform_padding(mut self, padding: u16) -> Self {
66 self.padding = (padding, padding, padding, padding);
67 self
68 }
69
70 pub fn with_text_footer(mut self, footer: Line<'a>) -> Self {
72 self.text_footer = Some(footer);
73 self
74 }
75
76 pub fn with_footer_height(mut self, height: u16) -> Self {
78 self.footer_height = height;
79 self
80 }
81
82 pub fn border_style(mut self, style: Style) -> Self {
84 self.border_style = style;
85 self
86 }
87
88 pub fn border_type(mut self, border_type: BorderType) -> Self {
90 self.border_type = border_type;
91 self
92 }
93
94 pub fn title_style(mut self, style: Style) -> Self {
96 self.title_style = style;
97 self
98 }
99
100 pub fn footer_style(mut self, style: Style) -> Self {
102 self.footer_style = style;
103 self
104 }
105
106 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 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 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 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 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 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 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 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}