steer_tui/tui/widgets/input_panel/
textarea.rs1use 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#[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 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 pub fn with_block(mut self, block: Block<'a>) -> Self {
36 self.block = Some(block);
37 self
38 }
39
40 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 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 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 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 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
107fn 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 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 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}