ratatui_toolkit/primitives/termtui/
widget.rs1use crate::primitives::termtui::copy_mode::{CopyMode, CopyPos};
4use crate::primitives::termtui::screen::Screen;
5use ratatui::buffer::Buffer;
6use ratatui::layout::Rect;
7use ratatui::style::{Color, Modifier, Style};
8use ratatui::widgets::Widget;
9
10pub struct TermTuiWidget<'a> {
12 screen: &'a Screen,
14 scroll_offset: usize,
16 copy_mode: Option<&'a CopyMode>,
18}
19
20impl<'a> TermTuiWidget<'a> {
21 pub fn new(screen: &'a Screen) -> Self {
23 Self {
24 screen,
25 scroll_offset: 0,
26 copy_mode: None,
27 }
28 }
29
30 pub fn scroll_offset(mut self, offset: usize) -> Self {
32 self.scroll_offset = offset;
33 self
34 }
35
36 pub fn copy_mode(mut self, mode: &'a CopyMode) -> Self {
38 self.copy_mode = Some(mode);
39 self
40 }
41}
42
43impl Widget for TermTuiWidget<'_> {
44 fn render(self, area: Rect, buf: &mut Buffer) {
45 let size = self.screen.size();
46 let _screen_rows = size.rows as usize;
47 let screen_cols = size.cols as usize;
48
49 let selection = self.copy_mode.and_then(|m| m.get_selection());
51 let copy_cursor = self.copy_mode.and_then(|m| m.cursor());
52
53 for (row_idx, row) in self.screen.visible_rows().enumerate() {
55 if row_idx >= area.height as usize {
56 break;
57 }
58
59 let y = area.y + row_idx as u16;
60
61 for (col_idx, cell) in row.cells().enumerate() {
63 if col_idx >= area.width as usize || col_idx >= screen_cols {
64 break;
65 }
66
67 let x = area.x + col_idx as u16;
68
69 if cell.is_wide_continuation() {
71 continue;
72 }
73
74 let mut style = cell.attrs().to_ratatui();
76
77 if let Some((start, end)) = &selection {
79 let cell_y = row_idx as i32 - self.scroll_offset as i32;
80 let cell_x = col_idx as i32;
81
82 if is_in_selection(cell_x, cell_y, start, end) {
83 style = Style::default()
84 .bg(Color::Rgb(70, 130, 180))
85 .fg(Color::White);
86 }
87 }
88
89 let ch = cell.text().chars().next().unwrap_or(' ');
91
92 if let Some(buf_cell) = buf.cell_mut((x, y)) {
93 buf_cell.set_char(ch).set_style(style);
94 }
95 }
96 }
97
98 let cursor_pos = self.screen.cursor_pos();
100
101 if let Some(copy_cursor) = copy_cursor {
103 let cursor_row = (copy_cursor.y + self.scroll_offset as i32) as u16;
104 let cursor_col = copy_cursor.x as u16;
105
106 if cursor_row < area.height && cursor_col < area.width {
107 let x = area.x + cursor_col;
108 let y = area.y + cursor_row;
109
110 if let Some(cell) = buf.cell_mut((x, y)) {
111 let cursor_style = Style::default()
112 .bg(Color::Yellow)
113 .fg(Color::Black)
114 .add_modifier(Modifier::BOLD);
115 cell.set_style(cursor_style);
116 }
117 }
118 } else if self.screen.cursor_visible() && self.scroll_offset == 0 {
119 let cursor_row = cursor_pos.row;
121 let cursor_col = cursor_pos.col;
122
123 if cursor_row < area.height && cursor_col < area.width {
124 let x = area.x + cursor_col;
125 let y = area.y + cursor_row;
126
127 if let Some(cell) = buf.cell_mut((x, y)) {
128 let cursor_style = Style::default()
129 .bg(Color::White)
130 .fg(Color::Black)
131 .add_modifier(Modifier::REVERSED);
132
133 if cell.symbol() == " " {
135 cell.set_char('█');
136 }
137 cell.set_style(cursor_style);
138 }
139 }
140 }
141 }
142}
143
144fn is_in_selection(x: i32, y: i32, start: &CopyPos, end: &CopyPos) -> bool {
146 let (low, high) = CopyPos::to_low_high(start, end);
147
148 if y < low.y || y > high.y {
149 return false;
150 }
151
152 if y == low.y && y == high.y {
153 x >= low.x && x <= high.x
155 } else if y == low.y {
156 x >= low.x
158 } else if y == high.y {
159 x <= high.x
161 } else {
162 true
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_is_in_selection_single_line() {
173 let start = CopyPos::new(5, 10);
174 let end = CopyPos::new(15, 10);
175
176 assert!(is_in_selection(5, 10, &start, &end));
177 assert!(is_in_selection(10, 10, &start, &end));
178 assert!(is_in_selection(15, 10, &start, &end));
179 assert!(!is_in_selection(4, 10, &start, &end));
180 assert!(!is_in_selection(16, 10, &start, &end));
181 assert!(!is_in_selection(10, 9, &start, &end));
182 }
183
184 #[test]
185 fn test_is_in_selection_multi_line() {
186 let start = CopyPos::new(5, 10);
187 let end = CopyPos::new(15, 12);
188
189 assert!(is_in_selection(5, 10, &start, &end));
191 assert!(is_in_selection(50, 10, &start, &end)); assert!(!is_in_selection(4, 10, &start, &end));
193
194 assert!(is_in_selection(0, 11, &start, &end));
196 assert!(is_in_selection(50, 11, &start, &end));
197
198 assert!(is_in_selection(0, 12, &start, &end));
200 assert!(is_in_selection(15, 12, &start, &end));
201 assert!(!is_in_selection(16, 12, &start, &end));
202 }
203
204 #[test]
205 fn test_is_in_selection_reversed() {
206 let start = CopyPos::new(15, 12);
208 let end = CopyPos::new(5, 10);
209
210 assert!(is_in_selection(10, 11, &start, &end));
212 }
213}