1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, BorderType, Borders, Paragraph, Widget},
7};
8
9use crate::engine;
10use crate::input::editor::Editor;
11use crate::ui::theme;
12
13pub struct TestInput<'a> {
14 pub editor: &'a Editor,
15 pub focused: bool,
16 pub matches: &'a [engine::Match],
17 pub show_whitespace: bool,
18 pub border_type: BorderType,
19}
20
21impl<'a> Widget for TestInput<'a> {
22 fn render(self, area: Rect, buf: &mut Buffer) {
23 let border_style = if self.focused {
24 Style::default().fg(theme::BLUE)
25 } else {
26 Style::default().fg(theme::OVERLAY)
27 };
28
29 let block = Block::default()
30 .borders(Borders::ALL)
31 .border_type(self.border_type)
32 .border_style(border_style)
33 .title(Span::styled(
34 " Test String ",
35 Style::default().fg(theme::TEXT),
36 ));
37
38 let content = self.editor.content();
39 let flat_spans = build_highlighted_spans(content, self.matches);
40 let flat_spans = if self.show_whitespace {
41 visualize_whitespace(flat_spans)
42 } else {
43 flat_spans
44 };
45 let lines = split_spans_into_lines(flat_spans);
46
47 let v_scroll = self.editor.vertical_scroll();
49 let inner_height = (area.height as usize).saturating_sub(2); let visible_lines: Vec<Line> = lines
51 .into_iter()
52 .skip(v_scroll)
53 .take(inner_height)
54 .collect();
55
56 let paragraph = Paragraph::new(visible_lines)
57 .block(block)
58 .style(Style::default().bg(theme::BASE));
59
60 paragraph.render(area, buf);
61
62 if self.focused {
64 let (cursor_line, cursor_col) = self.editor.cursor_line_col();
65 let visual_col = cursor_col.saturating_sub(self.editor.scroll_offset());
66 let visual_row = cursor_line.saturating_sub(v_scroll);
67 let cursor_x = area.x + 1 + visual_col as u16;
68 let cursor_y = area.y + 1 + visual_row as u16;
69 if cursor_x < area.x + area.width - 1 && cursor_y < area.y + area.height - 1 {
70 if let Some(cell) = buf.cell_mut((cursor_x, cursor_y)) {
71 cell.set_style(
72 Style::default()
73 .fg(theme::BASE)
74 .bg(theme::TEXT)
75 .add_modifier(Modifier::BOLD),
76 );
77 }
78 }
79 }
80 }
81}
82
83pub fn split_spans_into_lines<'a>(spans: Vec<Span<'a>>) -> Vec<Line<'a>> {
85 let mut lines: Vec<Line<'a>> = Vec::new();
86 let mut current_spans: Vec<Span<'a>> = Vec::new();
87
88 for span in spans {
89 let style = span.style;
90 let text: &str = span.content.as_ref();
91
92 let mut remaining = text;
93 while let Some(nl_pos) = remaining.find('\n') {
94 let before = &remaining[..nl_pos];
95 if !before.is_empty() {
96 current_spans.push(Span::styled(before.to_string(), style));
97 }
98 lines.push(Line::from(std::mem::take(&mut current_spans)));
99 remaining = &remaining[nl_pos + 1..];
100 }
101 if !remaining.is_empty() {
102 current_spans.push(Span::styled(remaining.to_string(), style));
103 }
104 }
105
106 lines.push(Line::from(current_spans));
108 lines
109}
110
111fn visualize_whitespace<'a>(spans: Vec<Span<'a>>) -> Vec<Span<'a>> {
113 let mut result = Vec::new();
114 for span in spans {
115 let style = span.style;
116 let text: &str = span.content.as_ref();
117 let ws_style = Style::default().fg(theme::OVERLAY);
118 let mut segment = String::new();
119
120 for c in text.chars() {
121 match c {
122 ' ' => {
123 if !segment.is_empty() {
124 result.push(Span::styled(std::mem::take(&mut segment), style));
125 }
126 result.push(Span::styled("\u{00b7}", ws_style)); }
128 '\n' => {
129 if !segment.is_empty() {
130 result.push(Span::styled(std::mem::take(&mut segment), style));
131 }
132 result.push(Span::styled("\u{21b5}\n", ws_style)); }
134 '\t' => {
135 if !segment.is_empty() {
136 result.push(Span::styled(std::mem::take(&mut segment), style));
137 }
138 result.push(Span::styled("\u{2192}", ws_style)); }
140 _ => {
141 segment.push(c);
142 }
143 }
144 }
145 if !segment.is_empty() {
146 result.push(Span::styled(segment, style));
147 }
148 }
149 result
150}
151
152fn build_highlighted_spans<'a>(text: &'a str, matches: &[engine::Match]) -> Vec<Span<'a>> {
153 if matches.is_empty() || text.is_empty() {
154 return vec![Span::styled(text, Style::default().fg(theme::TEXT))];
155 }
156
157 let mut spans = Vec::new();
158 let mut pos = 0;
159
160 for (match_index, m) in matches.iter().enumerate() {
161 let bg = theme::match_bg(match_index);
162
163 if m.start > pos {
164 spans.push(Span::styled(
165 &text[pos..m.start],
166 Style::default().fg(theme::TEXT),
167 ));
168 }
169
170 if m.captures.is_empty() {
171 spans.push(Span::styled(
172 &text[m.start..m.end],
173 Style::default()
174 .fg(theme::TEXT)
175 .bg(bg)
176 .add_modifier(Modifier::BOLD),
177 ));
178 } else {
179 let mut inner_pos = m.start;
181 for cap in &m.captures {
182 if cap.start > inner_pos {
183 spans.push(Span::styled(
184 &text[inner_pos..cap.start],
185 Style::default()
186 .fg(theme::TEXT)
187 .bg(bg)
188 .add_modifier(Modifier::BOLD),
189 ));
190 }
191 let color = theme::capture_color(cap.index.saturating_sub(1));
192 spans.push(Span::styled(
193 &text[cap.start..cap.end],
194 Style::default()
195 .fg(theme::BASE)
196 .bg(color)
197 .add_modifier(Modifier::BOLD),
198 ));
199 inner_pos = cap.end;
200 }
201 if inner_pos < m.end {
202 spans.push(Span::styled(
203 &text[inner_pos..m.end],
204 Style::default()
205 .fg(theme::TEXT)
206 .bg(bg)
207 .add_modifier(Modifier::BOLD),
208 ));
209 }
210 }
211
212 pos = m.end;
213 }
214
215 if pos < text.len() {
216 spans.push(Span::styled(&text[pos..], Style::default().fg(theme::TEXT)));
217 }
218
219 spans
220}