binocular/ui/search/
bar.rs1use crate::app::{InputMode, Mode};
2use crate::ui::indicators::mode_indicator;
3use crate::ui::shortcuts::{render_hints_line, search_bar_hints};
4use ratatui::{
5 layout::Rect,
6 style::{Color, Modifier, Style},
7 text::{Line, Span},
8 widgets::{Block, BorderType, Borders, Paragraph},
9 Frame,
10};
11
12use super::search_border_style;
13
14pub struct SearchBarView<'a> {
15 pub app_mode: Mode,
16 pub search_mode: InputMode,
17 pub preview_search_active: bool,
18 pub preview_search_query: &'a str,
19 pub preview_search_cursor: usize,
20 pub query_text: &'a str,
21 pub query_cursor: usize,
22 pub search_label: &'a str,
23 pub match_mode_label: &'static str,
24}
25
26pub fn render_search_bar(f: &mut Frame, view: &SearchBarView<'_>, area: Rect) {
27 if view.app_mode == Mode::Preview && view.preview_search_active {
28 render_preview_search_bar(f, view, area);
29 return;
30 }
31
32 render_main_search_bar(f, view, area);
33}
34
35fn render_preview_search_bar(f: &mut Frame, view: &SearchBarView<'_>, area: Rect) {
36 let input_text = format!("/{}", view.preview_search_query);
37 let input = Paragraph::new(input_text.as_str())
38 .style(Style::default().fg(Color::Blue))
39 .block(
40 Block::default()
41 .borders(Borders::ALL)
42 .border_type(BorderType::Rounded)
43 .title(" Search Preview "),
44 );
45 f.render_widget(input, area);
46 f.set_cursor_position((
47 area.x + 1 + view.preview_search_cursor as u16 + 1,
48 area.y + 1,
49 ));
50}
51
52fn render_main_search_bar(f: &mut Frame, view: &SearchBarView<'_>, area: Rect) {
53 let is_insert_mode = view.search_mode == InputMode::Insert;
54 let input_line = build_query_line(view.query_text, view.query_cursor, is_insert_mode);
55 let search_mode = if is_insert_mode {
56 InputMode::Insert
57 } else {
58 InputMode::Normal
59 };
60
61 let center_title = Line::from(vec![
62 Span::raw(" "),
63 Span::styled(
64 view.search_label,
65 Style::default().add_modifier(Modifier::BOLD),
66 ),
67 Span::raw(" "),
68 ])
69 .centered();
70
71 let mode_title = Line::from(vec![Span::raw(" "), mode_indicator(&search_mode, None)]);
72
73 let hints = render_hints_line(search_bar_hints(view.app_mode, view.search_mode));
74 let mut block = Block::default()
75 .borders(Borders::ALL)
76 .border_type(BorderType::Rounded)
77 .title(center_title)
78 .title(mode_title)
79 .title_bottom(hints.right_aligned())
80 .border_style(search_border_style(view.app_mode, view.search_mode));
81 block = block.title(
82 Line::from(vec![Span::styled(
83 view.match_mode_label,
84 Style::default().add_modifier(Modifier::BOLD),
85 )])
86 .right_aligned(),
87 );
88 let input = Paragraph::new(input_line)
89 .style(Style::default().fg(Color::White))
90 .block(block);
91
92 f.render_widget(input, area);
93}
94
95fn build_query_line(query: &str, cursor: usize, is_insert_mode: bool) -> Line<'static> {
96 let query_chars: Vec<char> = query.chars().collect();
97 let cursor_pos = cursor.min(query_chars.len());
98
99 if query_chars.is_empty() {
100 return Line::from(vec![empty_cursor_span(is_insert_mode)]);
101 }
102
103 let mut spans = Vec::new();
104
105 if cursor_pos > 0 {
106 let before: String = query_chars[..cursor_pos].iter().collect();
107 spans.push(Span::raw(before));
108 }
109
110 let cursor_char: String = if cursor_pos < query_chars.len() {
111 query_chars[cursor_pos..=cursor_pos].iter().collect()
112 } else {
113 " ".to_string()
114 };
115 spans.push(cursor_span(cursor_char, is_insert_mode));
116
117 if cursor_pos + 1 < query_chars.len() {
118 let after: String = query_chars[cursor_pos + 1..].iter().collect();
119 spans.push(Span::raw(after));
120 }
121
122 Line::from(spans)
123}
124
125fn empty_cursor_span(is_insert_mode: bool) -> Span<'static> {
126 if is_insert_mode {
127 Span::styled(" ", Style::default().add_modifier(Modifier::UNDERLINED))
128 } else {
129 Span::styled(" ", Style::default().bg(Color::White).fg(Color::Black))
130 }
131}
132
133fn cursor_span(content: String, is_insert_mode: bool) -> Span<'static> {
134 if is_insert_mode {
135 Span::styled(
136 content,
137 Style::default()
138 .add_modifier(Modifier::UNDERLINED)
139 .fg(Color::LightGreen),
140 )
141 } else {
142 Span::styled(content, Style::default().bg(Color::White).fg(Color::Black))
143 }
144}