steer_tui/tui/widgets/input_panel/
textarea.rs

1//! Text area widget with scrollbar support
2
3use ratatui::layout::Rect;
4use ratatui::prelude::{Buffer, StatefulWidget, Widget};
5use ratatui::widgets::{Block, Scrollbar, ScrollbarOrientation, ScrollbarState};
6use tui_textarea::TextArea;
7
8use crate::tui::InputMode;
9use crate::tui::theme::{Component, Theme};
10
11/// Widget wrapper for TextArea with scrollbar support
12#[derive(Debug)]
13pub struct TextAreaWidget<'a> {
14    textarea: &'a mut TextArea<'static>,
15    theme: &'a Theme,
16    block: Option<Block<'a>>,
17    mode: Option<InputMode>,
18}
19
20impl<'a> TextAreaWidget<'a> {
21    /// Create a new text area widget
22    pub fn new(textarea: &'a mut TextArea<'static>, theme: &'a Theme) -> Self {
23        Self {
24            textarea,
25            theme,
26            block: None,
27            mode: None,
28        }
29    }
30
31    /// Set the block for the text area
32    pub fn with_block(mut self, block: Block<'a>) -> Self {
33        self.block = Some(block);
34        self
35    }
36
37    /// Set the input mode for styling
38    pub fn with_mode(mut self, mode: InputMode) -> Self {
39        self.mode = Some(mode);
40        self
41    }
42}
43
44impl<'a> Widget for TextAreaWidget<'a> {
45    fn render(self, area: Rect, buf: &mut Buffer) {
46        // Take ownership of block to avoid borrow issues
47        let (inner_area, theme, _mode) = if let Some(block) = self.block {
48            let styled_block = if let Some(mode) = self.mode {
49                apply_mode_styling(block, mode, self.theme)
50            } else {
51                block
52            };
53            let inner = styled_block.inner(area);
54            styled_block.render(area, buf);
55            (inner, self.theme, self.mode)
56        } else {
57            (area, self.theme, self.mode)
58        };
59
60        // Calculate if we need scrollbar before rendering textarea
61        let textarea_height = inner_area.height;
62        let content_lines = self.textarea.lines().len();
63        let needs_scrollbar = content_lines > textarea_height as usize;
64        let cursor_row = self.textarea.cursor().0;
65
66        // Render the text area without its own block
67        self.textarea.set_block(Block::default());
68        self.textarea.render(inner_area, buf);
69
70        // Render scrollbar if needed
71        if needs_scrollbar {
72            let mut scrollbar_state = ScrollbarState::new(content_lines)
73                .position(cursor_row)
74                .viewport_content_length(textarea_height as usize);
75
76            let scrollbar = Scrollbar::new(ScrollbarOrientation::VerticalRight)
77                .begin_symbol(Some("▲"))
78                .end_symbol(Some("▼"))
79                .thumb_style(theme.style(Component::DimText));
80
81            let scrollbar_area = Rect {
82                x: area.x + area.width - 1,
83                y: area.y + 1,
84                width: 1,
85                height: area.height - 2,
86            };
87
88            scrollbar.render(scrollbar_area, buf, &mut scrollbar_state);
89        }
90    }
91}
92
93/// Apply mode-specific styling to a block
94fn apply_mode_styling<'a>(mut block: Block<'a>, mode: InputMode, theme: &Theme) -> Block<'a> {
95    match mode {
96        InputMode::Simple | InputMode::VimInsert => {
97            // Active border and text style
98            let active = theme.style(Component::InputPanelBorderActive);
99            block = block.style(active).border_style(active);
100        }
101        InputMode::VimNormal => {
102            // Keep text style the same as VimInsert (active) but dim the border
103            let text_style = theme.style(Component::InputPanelBorderActive);
104            let border_dim = theme.style(Component::InputPanelBorder);
105            block = block.style(text_style).border_style(border_dim);
106        }
107        InputMode::BashCommand => {
108            let style = theme.style(Component::InputPanelBorderCommand);
109            block = block.style(style).border_style(style);
110        }
111        InputMode::ConfirmExit => {
112            let style = theme.style(Component::InputPanelBorderError);
113            block = block.style(style).border_style(style);
114        }
115        InputMode::EditMessageSelection => {
116            let style = theme.style(Component::InputPanelBorderCommand);
117            block = block.style(style).border_style(style);
118        }
119        InputMode::FuzzyFinder => {
120            let style = theme.style(Component::InputPanelBorderActive);
121            block = block.style(style).border_style(style);
122        }
123        _ => {}
124    }
125    block
126}