#![allow(dead_code)]
use crate::tui::theme::Theme;
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
pub struct InputProps<'a> {
pub value: &'a str,
pub placeholder: &'a str,
pub mode_line: &'a str,
pub focused: bool,
}
pub fn render(frame: &mut Frame, area: Rect, props: &InputProps, theme: &Theme) {
if area.height == 0 || area.width == 0 {
return;
}
let has_mode_line = !props.mode_line.is_empty();
let (field_area, mode_area) = if has_mode_line && area.height >= 2 {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(1), Constraint::Length(1)])
.split(area);
(chunks[0], Some(chunks[1]))
} else {
(area, None)
};
let border_style = if props.focused {
Style::default().fg(theme.primary)
} else {
Style::default().fg(theme.text_muted)
};
let block = Block::default()
.borders(Borders::ALL)
.border_style(border_style);
let prompt = Span::styled("> ", Style::default().fg(theme.text_muted));
let content: Span = if props.value.is_empty() {
Span::styled(
props.placeholder.to_owned(),
Style::default()
.fg(theme.text_muted)
.add_modifier(Modifier::ITALIC),
)
} else {
Span::styled(
props.value.to_owned(),
Style::default().fg(theme.text_strong),
)
};
let mut spans: Vec<Span> = vec![prompt, content];
if props.focused {
spans.push(Span::styled(
"▏",
Style::default()
.fg(theme.primary)
.add_modifier(Modifier::BOLD),
));
}
let field_paragraph = Paragraph::new(Line::from(spans))
.block(block)
.wrap(Wrap { trim: false });
frame.render_widget(field_paragraph, field_area);
if let Some(mode_rect) = mode_area {
if !props.mode_line.is_empty() {
let mode_paragraph = Paragraph::new(Line::from(vec![Span::styled(
props.mode_line.to_owned(),
Style::default().fg(theme.text_muted),
)]));
frame.render_widget(mode_paragraph, mode_rect);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::theme::{theme, ColorDepth};
use ratatui::{backend::TestBackend, Terminal};
fn make_terminal(width: u16, height: u16) -> Terminal<TestBackend> {
Terminal::new(TestBackend::new(width, height)).unwrap()
}
#[test]
fn render_empty_value_focused_80x5_sem_panic() {
let mut terminal = make_terminal(80, 5);
let t = theme(ColorDepth::TrueColor);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 80, 5);
render(
f,
area,
&InputProps {
value: "",
placeholder: "Digite sua ideia…",
mode_line: "Build · GPT-5.3 · medium",
focused: true,
},
&t,
);
})
.unwrap();
}
#[test]
fn render_empty_value_unfocused_80x5_sem_panic() {
let mut terminal = make_terminal(80, 5);
let t = theme(ColorDepth::Ansi256);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 80, 5);
render(
f,
area,
&InputProps {
value: "",
placeholder: "Digite sua ideia…",
mode_line: "Build · GPT-5.3 · medium",
focused: false,
},
&t,
);
})
.unwrap();
}
#[test]
fn render_with_value_focused_80x5_sem_panic() {
let mut terminal = make_terminal(80, 5);
let t = theme(ColorDepth::TrueColor);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 80, 5);
render(
f,
area,
&InputProps {
value: "Adicionar comando sdd review",
placeholder: "Digite sua ideia…",
mode_line: "Build · GPT-5.3 · medium",
focused: true,
},
&t,
);
})
.unwrap();
}
#[test]
fn render_with_value_unfocused_80x5_sem_panic() {
let mut terminal = make_terminal(80, 5);
let t = theme(ColorDepth::Ansi16);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 80, 5);
render(
f,
area,
&InputProps {
value: "Adicionar comando sdd review",
placeholder: "Digite sua ideia…",
mode_line: "Build · GPT-5.3 · medium",
focused: false,
},
&t,
);
})
.unwrap();
}
#[test]
fn render_empty_value_focused_40x5_sem_panic() {
let mut terminal = make_terminal(40, 5);
let t = theme(ColorDepth::TrueColor);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 40, 5);
render(
f,
area,
&InputProps {
value: "",
placeholder: "Digite…",
mode_line: "medium",
focused: true,
},
&t,
);
})
.unwrap();
}
#[test]
fn render_with_value_unfocused_40x5_sem_panic() {
let mut terminal = make_terminal(40, 5);
let t = theme(ColorDepth::Ansi256);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 40, 5);
render(
f,
area,
&InputProps {
value: "Texto de teste",
placeholder: "Digite…",
mode_line: "medium",
focused: false,
},
&t,
);
})
.unwrap();
}
#[test]
fn render_area_zero_sem_panic() {
let mut terminal = make_terminal(10, 3);
let t = theme(ColorDepth::Ansi16);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 0, 0);
render(
f,
area,
&InputProps {
value: "x",
placeholder: "p",
mode_line: "m",
focused: true,
},
&t,
);
})
.unwrap();
}
#[test]
fn render_area_uma_linha_sem_panic() {
let mut terminal = make_terminal(40, 1);
let t = theme(ColorDepth::TrueColor);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 40, 1);
render(
f,
area,
&InputProps {
value: "",
placeholder: "p",
mode_line: "m",
focused: true,
},
&t,
);
})
.unwrap();
}
#[test]
fn mode_line_vazio_usa_area_inteira_do_campo() {
let mut terminal = make_terminal(40, 3);
let t = theme(ColorDepth::TrueColor);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 40, 3);
render(
f,
area,
&InputProps {
value: "",
placeholder: "Digite…",
mode_line: "",
focused: true,
},
&t,
);
})
.unwrap();
let buffer = terminal.backend().buffer().clone();
let bottom_row_has_border =
(0..40).any(|x| matches!(buffer[(x, 2)].symbol(), "└" | "┘" | "─"));
assert!(
bottom_row_has_border,
"campo deve ocupar a altura total quando mode_line está vazio"
);
}
#[test]
fn cursor_ausente_quando_unfocused() {
let mut terminal = make_terminal(40, 5);
let t = theme(ColorDepth::TrueColor);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 40, 5);
render(
f,
area,
&InputProps {
value: "texto",
placeholder: "p",
mode_line: "m",
focused: false,
},
&t,
);
})
.unwrap();
let buffer = terminal.backend().buffer().clone();
let has_cursor = buffer.content().iter().any(|cell| cell.symbol() == "▏");
assert!(!has_cursor, "cursor não deve aparecer quando unfocused");
}
#[test]
fn cursor_presente_quando_focused() {
let mut terminal = make_terminal(40, 5);
let t = theme(ColorDepth::TrueColor);
terminal
.draw(|f| {
let area = Rect::new(0, 0, 40, 5);
render(
f,
area,
&InputProps {
value: "texto",
placeholder: "p",
mode_line: "m",
focused: true,
},
&t,
);
})
.unwrap();
let buffer = terminal.backend().buffer().clone();
let has_cursor = buffer.content().iter().any(|cell| cell.symbol() == "▏");
assert!(has_cursor, "cursor deve aparecer quando focused");
}
}