use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Layout, Position, Rect}; use ratatui::prelude::Text; use ratatui::style::{Style, Styled, Stylize}; use ratatui::widgets::{Block, Paragraph, Widget}; use ratatui::Frame; use crate::ui::styles::ManagarrStyle; use crate::ui::utils::{borderless_block, layout_block}; #[cfg(test)] #[path = "input_box_tests.rs"] mod input_box_tests; pub struct InputBox<'a> { content: &'a str, offset: usize, style: Style, block: Block<'a>, label: Option<&'a str>, cursor_after_string: bool, is_highlighted: Option, is_selected: Option, } impl<'a> InputBox<'a> { pub fn new(content: &'a str) -> InputBox<'a> { InputBox { content, offset: 0, style: Style::new().default(), block: layout_block(), label: None, cursor_after_string: true, is_highlighted: None, is_selected: None, } } pub fn style>(mut self, style: S) -> InputBox<'a> { self.style = style.into(); self } pub fn block(mut self, block: Block<'a>) -> InputBox<'a> { self.block = block; self } pub fn label(mut self, label: &'a str) -> InputBox<'a> { self.label = Some(label); self } pub fn offset(mut self, offset: usize) -> InputBox<'a> { self.offset = offset; self } pub fn cursor_after_string(mut self, cursor_after_string: bool) -> InputBox<'a> { self.cursor_after_string = cursor_after_string; self } pub fn highlighted(mut self, is_highlighted: bool) -> InputBox<'a> { self.is_highlighted = Some(is_highlighted); self } pub fn selected(mut self, is_selected: bool) -> InputBox<'a> { self.is_selected = Some(is_selected); self } pub fn is_selected(&self) -> bool { self.is_selected.unwrap_or_default() } pub fn show_cursor(&self, f: &mut Frame<'_>, area: Rect) { let area = if self.label.is_some() { Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).split(area)[1] } else { area }; if self.cursor_after_string { f.set_cursor_position(Position { x: area.x + (self.content.len() - self.offset) as u16 + 1, y: area.y + 1, }); } else { f.set_cursor_position(Position { x: area.x + 1u16, y: area.y + 1, }); } } fn render_input_box(self, area: Rect, buf: &mut Buffer) { let style = if matches!(self.is_highlighted, Some(true)) && matches!(self.is_selected, Some(false)) { Style::new().system_function().bold() } else { self.style }; let input_box_paragraph = Paragraph::new(Text::from(self.content)) .style(style) .block(self.block); if let Some(label) = self.label { let [label_area, text_box_area] = Layout::horizontal([Constraint::Percentage(48), Constraint::Percentage(48)]).areas(area); Paragraph::new(Text::from(format!("\n{label}: "))) .block(borderless_block()) .right_aligned() .primary() .render(label_area, buf); input_box_paragraph.render(text_box_area, buf); } else { input_box_paragraph.render(area, buf); } } } impl<'a> Widget for InputBox<'a> { fn render(self, area: Rect, buf: &mut Buffer) where Self: Sized, { self.render_input_box(area, buf); } } impl<'a> Styled for InputBox<'a> { type Item = InputBox<'a>; fn style(&self) -> Style { self.style } fn set_style>(self, style: S) -> Self::Item { self.style(style) } } #[macro_export] macro_rules! render_selectable_input_box { ($input_box:ident, $frame:ident, $area:ident) => { if $input_box.is_selected() { $input_box.show_cursor($frame, $area); } $frame.render_widget($input_box, $area); }; }