use ratatui::{
buffer::Buffer,
layout::{Alignment, Rect},
style::{Color, Style},
text::{Line, Span},
widgets::{Paragraph, StatefulWidget, Widget},
};
use crate::gui_utils::{
clip_window_with_indicator_padded, compute_h_scroll_with_padding, display_cols_up_to,
display_width,
};
use super::state::CommandLineState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandLinePlacement {
Bottom,
Inline,
}
#[derive(Debug, Clone)]
pub struct CommandLine {
pub(crate) style: Style,
pub(crate) prompt_style: Style,
pub(crate) placeholder_style: Style,
pub(crate) overflow_indicator: char,
pub(crate) placement: CommandLinePlacement,
}
impl Default for CommandLine {
fn default() -> Self {
Self {
style: Style::default(),
prompt_style: Style::default().fg(Color::Yellow),
placeholder_style: Style::default().fg(Color::DarkGray),
overflow_indicator: '$',
placement: CommandLinePlacement::Bottom,
}
}
}
impl CommandLine {
pub(crate) fn placement_area(area: Rect, placement: CommandLinePlacement) -> Rect {
match placement {
CommandLinePlacement::Bottom => Rect {
x: area.x,
y: area.y.saturating_add(area.height.saturating_sub(1)),
width: area.width,
height: area.height.min(1),
},
CommandLinePlacement::Inline => area,
}
}
pub fn bottom(mut self) -> Self {
self.placement = CommandLinePlacement::Bottom;
self
}
pub fn inline(mut self) -> Self {
self.placement = CommandLinePlacement::Inline;
self
}
pub fn placement(mut self, placement: CommandLinePlacement) -> Self {
self.placement = placement;
self
}
pub fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
pub fn prompt_style(mut self, style: Style) -> Self {
self.prompt_style = style;
self
}
pub fn placeholder_style(mut self, style: Style) -> Self {
self.placeholder_style = style;
self
}
pub fn overflow_indicator(mut self, ch: char) -> Self {
self.overflow_indicator = ch;
self
}
}
impl StatefulWidget for CommandLine {
type State = CommandLineState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
if !state.is_active() {
return;
}
let area = Self::placement_area(area, self.placement);
let prompt = state.prompt();
let prompt_width = display_width(prompt);
let input_width = area.width.saturating_sub(prompt_width);
let input_area = Rect {
x: area.x.saturating_add(prompt_width),
y: area.y,
width: input_width,
height: area.height,
};
state.input.ensure_visible(input_area, None);
let text = state.input();
let input_line = if text.is_empty() {
Line::from(Span::styled("", self.placeholder_style))
} else {
let fits = display_width(text) <= input_width;
let start_cols = if fits {
state.input.h_scroll
} else {
let cursor_cols = display_cols_up_to(text, state.input.display_cursor_position());
let (target_h, _) =
compute_h_scroll_with_padding(cursor_cols, display_width(text), input_width);
target_h.max(state.input.h_scroll)
};
clip_window_with_indicator_padded(
text,
input_width,
self.overflow_indicator,
start_cols,
)
};
let mut spans = Vec::new();
spans.push(Span::styled(prompt.to_string(), self.prompt_style));
spans.extend(input_line.spans);
let paragraph = Paragraph::new(Line::from(spans))
.alignment(Alignment::Left)
.style(self.style);
paragraph.render(area, buf);
}
}