Skip to main content

vtcode_design/
panel.rs

1//! Base panel widget primitive.
2//!
3//! `Panel` renders standardized chrome (borders, titles) and returns the inner
4//! area for child widgets. It is decoupled from any specific session or styling
5//! type via the [`PanelStyleProvider`] trait.
6
7use ratatui::{
8    buffer::Buffer,
9    layout::Rect,
10    style::{Modifier, Style},
11    widgets::{Block, BorderType, Widget},
12};
13
14use crate::layout::LayoutMode;
15
16/// Trait for providing panel styles. Decouples `Panel` from any specific
17/// session or theme type.
18///
19/// Implementors provide the three core styles needed to render a panel:
20/// - `default_style`: the base style for the panel background
21/// - `accent_style`: the style for active/focused borders
22/// - `border_style`: the style for inactive borders
23pub trait PanelStyleProvider {
24    /// Base style for the panel background.
25    fn default_style(&self) -> Style;
26
27    /// Style for active/focused borders and titles.
28    fn accent_style(&self) -> Style;
29
30    /// Style for inactive borders.
31    fn border_style(&self) -> Style;
32}
33
34/// A consistent panel wrapper that applies standardized chrome (borders, titles).
35///
36/// This widget ensures visual consistency across all panels in the UI by
37/// providing a unified border and title style based on the active theme.
38///
39/// # Example
40/// ```ignore
41/// let inner = Panel::new(&styles)
42///     .title("Transcript")
43///     .active(true)
44///     .mode(layout_mode)
45///     .border_type(BorderType::Rounded)
46///     .render_and_get_inner(area, buf);
47/// // Render child widget into `inner`
48/// ```
49pub 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    /// Create a new panel with required style reference.
59    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    /// Set the panel title (displayed in the border).
70    #[must_use]
71    pub fn title(mut self, title: &'a str) -> Self {
72        self.title = Some(title);
73        self
74    }
75
76    /// Mark the panel as active (highlighted border).
77    #[must_use]
78    pub fn active(mut self, active: bool) -> Self {
79        self.active = active;
80        self
81    }
82
83    /// Set the layout mode (affects border visibility).
84    #[must_use]
85    pub fn mode(mut self, mode: LayoutMode) -> Self {
86        self.mode = mode;
87        self
88    }
89
90    /// Override the border type.
91    #[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    /// Render the panel and return the inner area for child widgets.
98    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
133/// Extended style methods for panels and visual hierarchy.
134pub trait PanelStyles {
135    /// Style for muted/secondary content.
136    fn muted_style(&self) -> Style;
137
138    /// Style for panel titles.
139    fn title_style(&self) -> Style;
140
141    /// Style for active/focused borders.
142    fn border_active_style(&self) -> Style;
143
144    /// Style for dividers between sections.
145    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        // Borders reduce inner area by 1 on each side
200        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        // Should not panic and should render with accent style
228        Panel::new(&styles)
229            .active(true)
230            .mode(LayoutMode::Standard)
231            .render_and_get_inner(area, &mut buf);
232    }
233}