#![allow(clippy::iter_skip_next)]
use crate::query::{ParseError, Query};
use crate::render::{Cell, Modifier};
use crate::style::Color;
use crate::utils::{char_width, truncate_to_width};
use crate::widget::traits::{RenderContext, View, WidgetProps};
use crate::{impl_props_builders, impl_styled_view};
pub struct SearchBar {
input: String,
cursor: usize,
parsed_query: Option<Query>,
parse_error: Option<ParseError>,
placeholder: String,
width: u16,
focused: bool,
show_hints: bool,
icon: char,
bg_color: Color,
border_color: Color,
text_color: Color,
placeholder_color: Color,
error_color: Color,
props: WidgetProps,
}
impl SearchBar {
pub fn new() -> Self {
Self {
input: String::new(),
cursor: 0,
parsed_query: Some(Query::new()),
parse_error: None,
placeholder: "Search...".to_string(),
width: 40,
focused: false,
show_hints: true,
icon: '🔍',
bg_color: Color::rgb(30, 30, 40),
border_color: Color::rgb(80, 80, 100),
text_color: Color::WHITE,
placeholder_color: Color::rgb(100, 100, 120),
error_color: Color::RED,
props: WidgetProps::new(),
}
}
pub fn placeholder(mut self, text: impl Into<String>) -> Self {
self.placeholder = text.into();
self
}
pub fn width(mut self, width: u16) -> Self {
self.width = width.max(10);
self
}
pub fn icon(mut self, icon: char) -> Self {
self.icon = icon;
self
}
pub fn show_hints(mut self, show: bool) -> Self {
self.show_hints = show;
self
}
pub fn colors(mut self, bg: Color, border: Color, text: Color) -> Self {
self.bg_color = bg;
self.border_color = border;
self.text_color = text;
self
}
pub fn error_color(mut self, color: Color) -> Self {
self.error_color = color;
self
}
pub fn focus(&mut self) {
self.focused = true;
}
pub fn blur(&mut self) {
self.focused = false;
}
pub fn is_focused(&self) -> bool {
self.focused
}
pub fn get_input(&self) -> &str {
&self.input
}
pub fn set_query(&mut self, query: impl Into<String>) {
self.input = query.into();
self.cursor = self.input.chars().count();
self.parse_input();
}
pub fn clear(&mut self) {
self.input.clear();
self.cursor = 0;
self.parsed_query = Some(Query::new());
self.parse_error = None;
}
pub fn query(&self) -> Option<&Query> {
self.parsed_query.as_ref()
}
pub fn error(&self) -> Option<&ParseError> {
self.parse_error.as_ref()
}
pub fn is_valid(&self) -> bool {
self.parse_error.is_none()
}
pub fn input(&mut self, ch: char) {
let byte_idx = self
.input
.char_indices()
.nth(self.cursor)
.map_or(self.input.len(), |(i, _)| i);
self.input.insert(byte_idx, ch);
self.cursor += 1;
self.parse_input();
}
pub fn backspace(&mut self) {
if self.cursor > 0 {
self.cursor -= 1;
let byte_idx = self
.input
.char_indices()
.nth(self.cursor)
.map_or(self.input.len(), |(i, _)| i);
self.input.remove(byte_idx);
self.parse_input();
}
}
pub fn delete(&mut self) {
if self.cursor < self.input.chars().count() {
let byte_idx = self
.input
.char_indices()
.nth(self.cursor)
.map_or(self.input.len(), |(i, _)| i);
self.input.remove(byte_idx);
self.parse_input();
}
}
pub fn cursor_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1);
}
pub fn cursor_right(&mut self) {
self.cursor = (self.cursor + 1).min(self.input.chars().count());
}
pub fn cursor_home(&mut self) {
self.cursor = 0;
}
pub fn cursor_end(&mut self) {
self.cursor = self.input.chars().count();
}
pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
use crate::event::Key;
match key {
Key::Char(ch) => {
self.input(*ch);
true
}
Key::Backspace => {
self.backspace();
true
}
Key::Delete => {
self.delete();
true
}
Key::Left => {
self.cursor_left();
true
}
Key::Right => {
self.cursor_right();
true
}
Key::Home => {
self.cursor_home();
true
}
Key::End => {
self.cursor_end();
true
}
Key::Escape => {
self.clear();
true
}
_ => false,
}
}
fn parse_input(&mut self) {
match Query::parse(&self.input) {
Ok(query) => {
self.parsed_query = Some(query);
self.parse_error = None;
}
Err(err) => {
self.parsed_query = None;
self.parse_error = Some(err);
}
}
}
}
impl Default for SearchBar {
fn default() -> Self {
Self::new()
}
}
impl View for SearchBar {
fn render(&self, ctx: &mut RenderContext) {
let area = ctx.area;
let width = self.width.min(area.width);
if width < 5 || area.height < 1 {
return;
}
ctx.fill_row(0, width, None, Some(self.bg_color));
let mut left_border = Cell::new('│');
left_border.fg = Some(if self.focused {
Color::CYAN
} else {
self.border_color
});
ctx.set(0, 0, left_border);
let mut right_border = Cell::new('│');
right_border.fg = Some(if self.focused {
Color::CYAN
} else {
self.border_color
});
ctx.set(width - 1, 0, right_border);
let mut icon_cell = Cell::new(self.icon);
icon_cell.bg = Some(self.bg_color);
ctx.set(2, 0, icon_cell);
let input_x: u16 = 4;
let input_width = width.saturating_sub(6);
if self.input.is_empty() {
let ph = truncate_to_width(&self.placeholder, input_width as usize);
let mut dx: u16 = 0;
for ch in ph.chars() {
let cw = char_width(ch) as u16;
if dx + cw > input_width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(self.placeholder_color);
cell.bg = Some(self.bg_color);
ctx.set(input_x + dx, 0, cell);
dx += cw;
}
} else {
let chars: Vec<char> = self.input.chars().collect();
let mut cursor_display_width: u16 = 0;
for &ch in &chars[..self.cursor.min(chars.len())] {
cursor_display_width += char_width(ch) as u16;
}
let display_start = if cursor_display_width >= input_width {
let target_width = cursor_display_width - input_width + 1;
let mut w: u16 = 0;
let mut start = 0;
for (i, &ch) in chars.iter().enumerate() {
if w >= target_width {
start = i;
break;
}
w += char_width(ch) as u16;
start = i + 1;
}
start
} else {
0
};
let mut dx: u16 = 0;
for &ch in &chars[display_start..] {
let cw = char_width(ch) as u16;
if dx + cw > input_width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(if self.parse_error.is_some() {
self.error_color
} else {
self.text_color
});
cell.bg = Some(self.bg_color);
ctx.set(input_x + dx, 0, cell);
dx += cw;
}
}
if self.focused {
let cursor_display_x: u16 = self
.input
.chars()
.take(self.cursor)
.map(|ch| char_width(ch) as u16)
.sum();
let visible_start: u16 = if cursor_display_x >= input_width {
cursor_display_x - input_width + 1
} else {
0
};
let cursor_x = input_x + cursor_display_x.saturating_sub(visible_start);
if cursor_x < width - 1 {
let cursor_char = self.input.chars().skip(self.cursor).next().unwrap_or(' ');
let mut cursor_cell = Cell::new(cursor_char);
cursor_cell.fg = Some(self.bg_color);
cursor_cell.bg = Some(self.text_color);
ctx.set(cursor_x, 0, cursor_cell);
}
}
if self.parse_error.is_some() {
let mut error_cell = Cell::new('!');
error_cell.fg = Some(self.error_color);
error_cell.bg = Some(self.bg_color);
error_cell.modifier |= Modifier::BOLD;
ctx.set(width - 3, 0, error_cell);
}
if self.show_hints && area.height > 1 && self.focused {
let hint = if self.parse_error.is_some() {
"Invalid query syntax"
} else if self.input.is_empty() {
"Try: field:value, text~contains, age:>18"
} else {
""
};
if !hint.is_empty() {
for (i, ch) in hint.chars().enumerate() {
if i as u16 >= width {
break;
}
let mut cell = Cell::new(ch);
cell.fg = Some(if self.parse_error.is_some() {
self.error_color
} else {
self.placeholder_color
});
ctx.set(i as u16, 1, cell);
}
}
}
}
crate::impl_view_meta!("SearchBar");
}
impl_styled_view!(SearchBar);
impl_props_builders!(SearchBar);
pub fn search_bar() -> SearchBar {
SearchBar::new()
}