1use ratatui::{
8 buffer::Buffer,
9 layout::Rect,
10 style::{Modifier, Style},
11 widgets::{Block, BorderType, Widget},
12};
13
14use crate::layout::LayoutMode;
15
16pub trait PanelStyleProvider {
24 fn default_style(&self) -> Style;
26
27 fn accent_style(&self) -> Style;
29
30 fn border_style(&self) -> Style;
32}
33
34pub struct Panel<'a, S: PanelStyleProvider> {
50 styles: &'a S,
51 title: Option<&'a str>,
52 active: bool,
53 mode: LayoutMode,
54 border_type: Option<BorderType>,
55}
56
57impl<'a, S: PanelStyleProvider> Panel<'a, S> {
58 pub fn new(styles: &'a S) -> Self {
60 Self {
61 styles,
62 title: None,
63 active: false,
64 mode: LayoutMode::Standard,
65 border_type: None,
66 }
67 }
68
69 #[must_use]
71 pub fn title(mut self, title: &'a str) -> Self {
72 self.title = Some(title);
73 self
74 }
75
76 #[must_use]
78 pub fn active(mut self, active: bool) -> Self {
79 self.active = active;
80 self
81 }
82
83 #[must_use]
85 pub fn mode(mut self, mode: LayoutMode) -> Self {
86 self.mode = mode;
87 self
88 }
89
90 #[must_use]
92 pub fn border_type(mut self, border_type: BorderType) -> Self {
93 self.border_type = Some(border_type);
94 self
95 }
96
97 pub fn render_and_get_inner(self, area: Rect, buf: &mut Buffer) -> Rect {
99 if !self.mode.show_borders() {
100 return area;
101 }
102
103 let border_style = if self.active {
104 self.styles.accent_style()
105 } else {
106 self.styles.border_style()
107 };
108
109 let border_type = self.border_type.unwrap_or(BorderType::Plain);
110
111 let mut block = Block::bordered()
112 .border_type(border_type)
113 .style(self.styles.default_style())
114 .border_style(border_style);
115
116 if self.mode.show_titles()
117 && let Some(title) = self.title
118 {
119 let title_style = if self.active {
120 self.styles.accent_style().add_modifier(Modifier::BOLD)
121 } else {
122 self.styles.default_style().add_modifier(Modifier::BOLD)
123 };
124 block = block.title(title).title_style(title_style);
125 }
126
127 let inner = block.inner(area);
128 block.render(area, buf);
129 inner
130 }
131}
132
133pub trait PanelStyles {
135 fn muted_style(&self) -> Style;
137
138 fn title_style(&self) -> Style;
140
141 fn border_active_style(&self) -> Style;
143
144 fn divider_style(&self) -> Style;
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 struct MockStyles {
153 default: Style,
154 accent: Style,
155 border: Style,
156 }
157
158 impl MockStyles {
159 fn new() -> Self {
160 Self {
161 default: Style::default(),
162 accent: Style::default().fg(ratatui::style::Color::Cyan),
163 border: Style::default().fg(ratatui::style::Color::Gray),
164 }
165 }
166 }
167
168 impl PanelStyleProvider for MockStyles {
169 fn default_style(&self) -> Style {
170 self.default
171 }
172 fn accent_style(&self) -> Style {
173 self.accent
174 }
175 fn border_style(&self) -> Style {
176 self.border
177 }
178 }
179
180 #[test]
181 fn compact_mode_returns_full_area() {
182 let styles = MockStyles::new();
183 let area = Rect::new(0, 0, 40, 10);
184 let mut buf = Buffer::empty(area);
185 let inner = Panel::new(&styles)
186 .mode(LayoutMode::Compact)
187 .render_and_get_inner(area, &mut buf);
188 assert_eq!(inner, area);
189 }
190
191 #[test]
192 fn standard_mode_returns_smaller_inner_area() {
193 let styles = MockStyles::new();
194 let area = Rect::new(0, 0, 80, 24);
195 let mut buf = Buffer::empty(area);
196 let inner = Panel::new(&styles)
197 .mode(LayoutMode::Standard)
198 .render_and_get_inner(area, &mut buf);
199 assert_eq!(inner.x, area.x + 1);
201 assert_eq!(inner.y, area.y + 1);
202 assert_eq!(inner.width, area.width - 2);
203 assert_eq!(inner.height, area.height - 2);
204 }
205
206 #[test]
207 fn wide_mode_with_title() {
208 let styles = MockStyles::new();
209 let area = Rect::new(0, 0, 120, 30);
210 let mut buf = Buffer::empty(area);
211 let inner = Panel::new(&styles)
212 .title("Test Panel")
213 .active(true)
214 .mode(LayoutMode::Wide)
215 .render_and_get_inner(area, &mut buf);
216 assert_eq!(inner.x, area.x + 1);
217 assert_eq!(inner.y, area.y + 1);
218 assert_eq!(inner.width, area.width - 2);
219 assert_eq!(inner.height, area.height - 2);
220 }
221
222 #[test]
223 fn active_panel_uses_accent_style() {
224 let styles = MockStyles::new();
225 let area = Rect::new(0, 0, 80, 24);
226 let mut buf = Buffer::empty(area);
227 Panel::new(&styles)
229 .active(true)
230 .mode(LayoutMode::Standard)
231 .render_and_get_inner(area, &mut buf);
232 }
233}