feat: implement Component trait for modular UI components and refactor TUI views to use it
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
//! Chat input box and autocomplete dropdown rendering.
|
||||
//! Implements the Component trait.
|
||||
|
||||
use super::theme::Theme;
|
||||
use crate::components::Component;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
|
||||
use ratatui::Frame;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct InputComponent;
|
||||
|
||||
impl Component for InputComponent {
|
||||
fn draw(&mut self, frame: &mut Frame, area: Rect, state: &crate::state::AppStateRest) {
|
||||
if state.input.autocomplete_visible && !state.input.autocomplete_candidates.is_empty() {
|
||||
let n = state.input.autocomplete_candidates.len().min(10) as u16;
|
||||
let dropdown_height = n + 2;
|
||||
let dropdown_area = Rect {
|
||||
x: area.x,
|
||||
y: area.y.saturating_sub(dropdown_height),
|
||||
width: area.width.min(45),
|
||||
height: dropdown_height,
|
||||
};
|
||||
let dropdown_title = match state.input.autocomplete_kind {
|
||||
crate::state::AutocompleteKind::Command => " ⌘ Commands ",
|
||||
crate::state::AutocompleteKind::FileMention => " 📁 Files ",
|
||||
};
|
||||
let dropdown_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(Theme::BORDER))
|
||||
.title(Span::styled(
|
||||
dropdown_title,
|
||||
Style::default().fg(Theme::PRIMARY).add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.style(Style::default().bg(Theme::SURFACE_ELEVATED));
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
let selected = state.input.autocomplete_idx;
|
||||
for (i, candidate) in state
|
||||
.input
|
||||
.autocomplete_candidates
|
||||
.iter()
|
||||
.enumerate()
|
||||
.take(10)
|
||||
{
|
||||
let prefix = if i == selected { " ▸ " } else { " " };
|
||||
let style = if i == selected {
|
||||
Style::default()
|
||||
.fg(Theme::TEXT)
|
||||
.bg(Theme::HIGHLIGHT_DIM)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default().fg(Theme::TEXT)
|
||||
};
|
||||
let label = format!("{prefix}{candidate}");
|
||||
lines.push(Line::from(Span::styled(label, style)));
|
||||
}
|
||||
let dropdown = Paragraph::new(lines).block(dropdown_block);
|
||||
frame.render_widget(ratatui::widgets::Clear, dropdown_area);
|
||||
frame.render_widget(dropdown, dropdown_area);
|
||||
}
|
||||
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.border_style(Style::default().fg(Theme::BORDER_FOCUSED))
|
||||
.style(Style::default().bg(Theme::BG));
|
||||
|
||||
let input_text = &state.input.buffer;
|
||||
let cursor_pos = state.input.cursor;
|
||||
|
||||
let prompt = Span::styled(
|
||||
" ❯ ",
|
||||
Style::default()
|
||||
.fg(Theme::PRIMARY)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
|
||||
let mut spans = vec![prompt];
|
||||
|
||||
if input_text.is_empty() {
|
||||
spans.push(Span::styled(
|
||||
"Type a message, @file, or /command...",
|
||||
Style::default()
|
||||
.fg(Theme::TEXT_DIM)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
));
|
||||
} else {
|
||||
let (before, after) = input_text.split_at(cursor_pos);
|
||||
spans.push(Span::raw(before.to_string()));
|
||||
let (cursor_char, after_char) = if after.is_empty() {
|
||||
(" ".to_string(), "")
|
||||
} else {
|
||||
let c = after.chars().next().unwrap();
|
||||
let char_len = c.len_utf8();
|
||||
(c.to_string(), &after[char_len..])
|
||||
};
|
||||
|
||||
// Blinking cursor logic based on tick count
|
||||
let cursor_style = if state.misc.tick_count % 10 < 5 {
|
||||
Style::default()
|
||||
.bg(Theme::PRIMARY)
|
||||
.fg(Theme::BG)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::default()
|
||||
.bg(Theme::TEXT_DIM)
|
||||
.fg(Theme::BG)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
spans.push(Span::styled(cursor_char, cursor_style));
|
||||
if !after_char.is_empty() {
|
||||
spans.push(Span::raw(after_char.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
let line = Line::from(spans);
|
||||
let paragraph = Paragraph::new(line).block(block);
|
||||
frame.render_widget(paragraph, area);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user