Skip to main content

vtcode_ui/tui/core_tui/widgets/
header.rs

1use ratatui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::{Modifier, Style},
5    text::Line,
6    widgets::{Clear, Widget},
7};
8
9use crate::tui::ui::tui::session::Session;
10
11/// Widget for rendering the header area with session metadata
12///
13/// This widget displays:
14/// - Provider and model information
15/// - Reasoning mode status
16/// - Tool policy summary
17/// - Workspace trust level
18/// - Git status
19/// - Plan progress (if applicable)
20/// - Suggestions or highlights
21///
22/// # Example
23/// ```ignore
24/// HeaderWidget::new(session)
25///     .lines(header_lines)
26///     .custom_style(style)
27///     .render(area, buf);
28/// ```
29pub struct HeaderWidget<'a> {
30    session: &'a mut Session,
31    lines: std::sync::Arc<Vec<Line<'static>>>,
32    custom_style: Option<Style>,
33}
34
35impl<'a> HeaderWidget<'a> {
36    /// Create a new HeaderWidget with required parameters
37    pub(crate) fn new(session: &'a mut Session) -> Self {
38        Self {
39            session,
40            lines: std::sync::Arc::new(Vec::new()),
41            custom_style: None,
42        }
43    }
44
45    /// Set the header lines to display
46    #[must_use]
47    pub(crate) fn lines(mut self, lines: std::sync::Arc<Vec<Line<'static>>>) -> Self {
48        self.lines = lines;
49        self
50    }
51
52    /// Set a custom style for the header
53    #[must_use]
54    pub fn custom_style(mut self, style: Style) -> Self {
55        self.custom_style = Some(style);
56        self
57    }
58}
59
60impl<'a> Widget for HeaderWidget<'a> {
61    #[cfg_attr(feature = "profiling", hotpath::measure)]
62    fn render(self, area: Rect, buf: &mut Buffer) {
63        Clear.render(area, buf);
64
65        if area.height == 0 || area.width == 0 {
66            return;
67        }
68
69        // Paint header rows via set_span — avoids Paragraph wrap + title
70        // rebuild every frame (hotpath: header::render was ~22KB/frame).
71        let hide_header = self.session.appearance.hide_header;
72        let text_style = self.session.header_primary_style().add_modifier(Modifier::DIM);
73
74        if hide_header {
75            // Paragraph used text_style as the base for the whole area.
76            buf.set_style(area, text_style);
77            for (y, line) in (area.y..).zip(self.lines.iter().take(usize::from(area.height))) {
78                let mut x = area.x;
79                for span in &line.spans {
80                    if x >= area.right() {
81                        break;
82                    }
83                    let merged = text_style.patch(span.style);
84                    let (end_x, _) =
85                        buf.set_stringn(x, y, span.content.as_ref(), area.right().saturating_sub(x) as usize, merged);
86                    x = end_x;
87                }
88            }
89            return;
90        }
91
92        let mut border_style = Style::default();
93        if let Some(accent) = self
94            .session
95            .theme
96            .tool_accent
97            .or(self.session.theme.primary)
98            .or(self.session.theme.foreground)
99        {
100            border_style = border_style.fg(crate::tui::core_tui::style::ratatui_color_from_ansi(accent));
101        }
102        let title = self.session.header_block_title_cached();
103        let block = ratatui::widgets::Block::bordered()
104            .title(title)
105            .border_type(crate::tui::core_tui::session::terminal_capabilities::get_border_type())
106            .border_style(border_style)
107            .style(self.session.styles.default_style());
108        let inner = block.inner(area);
109        block.render(area, buf);
110
111        if inner.width == 0 || inner.height == 0 {
112            return;
113        }
114        for (y, line) in (inner.y..).zip(self.lines.iter().take(usize::from(inner.height))) {
115            let mut x = inner.x;
116            for span in &line.spans {
117                if x >= inner.right() {
118                    break;
119                }
120                // Paragraph: text_style is the base, span style patches on top.
121                let merged = text_style.patch(span.style);
122                let (end_x, _) =
123                    buf.set_stringn(x, y, span.content.as_ref(), inner.right().saturating_sub(x) as usize, merged);
124                x = end_x;
125            }
126        }
127    }
128}