1use ratatui::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Modifier, Style},
5 text::{Line, Span},
6 widgets::{Block, 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}
18
19impl<'a> Widget for TestInput<'a> {
20 fn render(self, area: Rect, buf: &mut Buffer) {
21 let border_style = if self.focused {
22 Style::default().fg(theme::BLUE)
23 } else {
24 Style::default().fg(theme::OVERLAY)
25 };
26
27 let block = Block::default()
28 .borders(Borders::ALL)
29 .border_style(border_style)
30 .title(Span::styled(
31 " Test String ",
32 Style::default().fg(theme::TEXT),
33 ));
34
35 let content = self.editor.content();
36 let flat_spans = build_highlighted_spans(content, self.matches);
37 let lines = split_spans_into_lines(flat_spans);
38
39 let v_scroll = self.editor.vertical_scroll();
41 let inner_height = (area.height as usize).saturating_sub(2); let visible_lines: Vec<Line> = lines
43 .into_iter()
44 .skip(v_scroll)
45 .take(inner_height)
46 .collect();
47
48 let paragraph = Paragraph::new(visible_lines)
49 .block(block)
50 .style(Style::default().bg(theme::BASE));
51
52 paragraph.render(area, buf);
53
54 if self.focused {
56 let (cursor_line, cursor_col) = self.editor.cursor_line_col();
57 let visual_col = cursor_col.saturating_sub(self.editor.scroll_offset());
58 let visual_row = cursor_line.saturating_sub(v_scroll);
59 let cursor_x = area.x + 1 + visual_col as u16;
60 let cursor_y = area.y + 1 + visual_row as u16;
61 if cursor_x < area.x + area.width - 1 && cursor_y < area.y + area.height - 1 {
62 if let Some(cell) = buf.cell_mut((cursor_x, cursor_y)) {
63 cell.set_style(
64 Style::default()
65 .fg(theme::BASE)
66 .bg(theme::TEXT)
67 .add_modifier(Modifier::BOLD),
68 );
69 }
70 }
71 }
72 }
73}
74
75pub fn split_spans_into_lines<'a>(spans: Vec<Span<'a>>) -> Vec<Line<'a>> {
77 let mut lines: Vec<Line<'a>> = Vec::new();
78 let mut current_spans: Vec<Span<'a>> = Vec::new();
79
80 for span in spans {
81 let style = span.style;
82 let text: &str = span.content.as_ref();
83
84 let mut remaining = text;
85 while let Some(nl_pos) = remaining.find('\n') {
86 let before = &remaining[..nl_pos];
87 if !before.is_empty() {
88 current_spans.push(Span::styled(before.to_string(), style));
89 }
90 lines.push(Line::from(std::mem::take(&mut current_spans)));
91 remaining = &remaining[nl_pos + 1..];
92 }
93 if !remaining.is_empty() {
94 current_spans.push(Span::styled(remaining.to_string(), style));
95 }
96 }
97
98 lines.push(Line::from(current_spans));
100 lines
101}
102
103fn build_highlighted_spans<'a>(text: &'a str, matches: &[engine::Match]) -> Vec<Span<'a>> {
104 if matches.is_empty() || text.is_empty() {
105 return vec![Span::styled(text, Style::default().fg(theme::TEXT))];
106 }
107
108 let mut spans = Vec::new();
109 let mut pos = 0;
110
111 for m in matches {
112 if m.start > pos {
113 spans.push(Span::styled(
114 &text[pos..m.start],
115 Style::default().fg(theme::TEXT),
116 ));
117 }
118
119 if m.captures.is_empty() {
120 spans.push(Span::styled(
121 &text[m.start..m.end],
122 Style::default()
123 .fg(theme::TEXT)
124 .bg(theme::MATCH_BG)
125 .add_modifier(Modifier::BOLD),
126 ));
127 } else {
128 let mut inner_pos = m.start;
130 for cap in &m.captures {
131 if cap.start > inner_pos {
132 spans.push(Span::styled(
133 &text[inner_pos..cap.start],
134 Style::default()
135 .fg(theme::TEXT)
136 .bg(theme::MATCH_BG)
137 .add_modifier(Modifier::BOLD),
138 ));
139 }
140 let color = theme::capture_color(cap.index.saturating_sub(1));
141 spans.push(Span::styled(
142 &text[cap.start..cap.end],
143 Style::default()
144 .fg(theme::BASE)
145 .bg(color)
146 .add_modifier(Modifier::BOLD),
147 ));
148 inner_pos = cap.end;
149 }
150 if inner_pos < m.end {
151 spans.push(Span::styled(
152 &text[inner_pos..m.end],
153 Style::default()
154 .fg(theme::TEXT)
155 .bg(theme::MATCH_BG)
156 .add_modifier(Modifier::BOLD),
157 ));
158 }
159 }
160
161 pos = m.end;
162 }
163
164 if pos < text.len() {
165 spans.push(Span::styled(&text[pos..], Style::default().fg(theme::TEXT)));
166 }
167
168 spans
169}