Skip to main content

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