vtcode_ui/design/
panel.rs1use ratatui::{
8 buffer::Buffer,
9 layout::Rect,
10 style::{Modifier, Style},
11 widgets::{Block, BorderType, Widget},
12};
13
14use crate::design::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(crate) 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(crate) 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(crate) fn title(mut self, title: &'a str) -> Self {
72 self.title = Some(title);
73 self
74 }
75
76 #[must_use]
78 pub(crate) fn active(mut self, active: bool) -> Self {
79 self.active = active;
80 self
81 }
82
83 #[must_use]
85 pub(crate) fn mode(mut self, mode: LayoutMode) -> Self {
86 self.mode = mode;
87 self
88 }
89
90 #[must_use]
92 pub(crate) fn border_type(mut self, border_type: BorderType) -> Self {
93 self.border_type = Some(border_type);
94 self
95 }
96
97 pub(crate) fn render_and_get_inner(self, area: Rect, buf: &mut Buffer) -> Rect {
99 if area.is_empty() {
100 return area;
101 }
102 if !self.mode.show_borders() {
103 return area;
104 }
105
106 let border_style = if self.active {
107 self.styles.accent_style()
108 } else {
109 self.styles.border_style()
110 };
111
112 let border_type = self.border_type.unwrap_or(BorderType::Plain);
113
114 let mut block = Block::bordered()
115 .border_type(border_type)
116 .style(self.styles.default_style())
117 .border_style(border_style);
118
119 if self.mode.show_titles()
120 && let Some(title) = self.title
121 {
122 let title_style = if self.active {
123 self.styles.accent_style().add_modifier(Modifier::BOLD)
124 } else {
125 self.styles.default_style().add_modifier(Modifier::BOLD)
126 };
127 block = block.title(title).title_style(title_style);
128 }
129
130 let inner = block.inner(area);
131 block.render(area, buf);
132 inner
133 }
134}
135
136pub trait PanelStyles {
138 fn muted_style(&self) -> Style;
140
141 fn title_style(&self) -> Style;
143
144 fn border_active_style(&self) -> Style;
146
147 fn divider_style(&self) -> Style;
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154
155 struct MockStyles {
156 default: Style,
157 accent: Style,
158 border: Style,
159 }
160
161 impl MockStyles {
162 fn new() -> Self {
163 Self {
164 default: Style::default(),
165 accent: Style::default().fg(ratatui::style::Color::Cyan),
166 border: Style::default().fg(ratatui::style::Color::Gray),
167 }
168 }
169 }
170
171 impl PanelStyleProvider for MockStyles {
172 fn default_style(&self) -> Style {
173 self.default
174 }
175 fn accent_style(&self) -> Style {
176 self.accent
177 }
178 fn border_style(&self) -> Style {
179 self.border
180 }
181 }
182
183 #[test]
184 fn zero_area_returns_unchanged_without_painting() {
185 let styles = MockStyles::new();
186 let area = Rect::new(5, 5, 0, 0);
187 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 10));
188 let before = buf.clone();
189 let inner = Panel::new(&styles)
190 .mode(LayoutMode::Standard)
191 .render_and_get_inner(area, &mut buf);
192 assert_eq!(inner, area);
193 assert_eq!(buf, before, "zero-area panel must not touch the buffer");
194 }
195
196 #[test]
197 fn compact_mode_returns_full_area() {
198 let styles = MockStyles::new();
199 let area = Rect::new(0, 0, 40, 10);
200 let mut buf = Buffer::empty(area);
201 let inner = Panel::new(&styles)
202 .mode(LayoutMode::Compact)
203 .render_and_get_inner(area, &mut buf);
204 assert_eq!(inner, area);
205 }
206
207 #[test]
208 fn standard_mode_returns_smaller_inner_area() {
209 let styles = MockStyles::new();
210 let area = Rect::new(0, 0, 80, 24);
211 let mut buf = Buffer::empty(area);
212 let inner = Panel::new(&styles)
213 .mode(LayoutMode::Standard)
214 .render_and_get_inner(area, &mut buf);
215 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 wide_mode_with_title() {
224 let styles = MockStyles::new();
225 let area = Rect::new(0, 0, 120, 30);
226 let mut buf = Buffer::empty(area);
227 let inner = Panel::new(&styles)
228 .title("Test Panel")
229 .active(true)
230 .mode(LayoutMode::Wide)
231 .render_and_get_inner(area, &mut buf);
232 assert_eq!(inner.x, area.x + 1);
233 assert_eq!(inner.y, area.y + 1);
234 assert_eq!(inner.width, area.width - 2);
235 assert_eq!(inner.height, area.height - 2);
236 }
237
238 #[test]
239 fn active_panel_uses_accent_style() {
240 let styles = MockStyles::new();
241 let area = Rect::new(0, 0, 80, 24);
242 let mut buf = Buffer::empty(area);
243 Panel::new(&styles)
245 .active(true)
246 .mode(LayoutMode::Standard)
247 .render_and_get_inner(area, &mut buf);
248 }
249}