1use ratatui::{buffer::Buffer, layout::Rect, style::Style, widgets::Widget};
2use unicode_segmentation::UnicodeSegmentation;
3use unicode_width::UnicodeWidthStr;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub struct LogicalRect {
7 pub x: i32,
8 pub y: i32,
9 pub width: i32,
10 pub height: i32,
11}
12
13impl From<Rect> for LogicalRect {
14 fn from(value: Rect) -> Self {
15 Self::new(value.x as i32, value.y as i32, value.width, value.height)
16 }
17}
18
19impl LogicalRect {
20 pub fn new(x: i32, y: i32, width: u16, height: u16) -> Self {
21 Self {
22 x,
23 y,
24 width: width as i32,
25 height: height as i32,
26 }
27 }
28
29 pub fn intersection(self, other: Self) -> Self {
30 if !self.intersects(other) {
31 return Self {
32 x: 0,
33 y: 0,
34 width: 0,
35 height: 0,
36 };
37 }
38
39 let x1 = self.x.max(other.x);
40 let y1 = self.y.max(other.y);
41 let x2 = self.right().min(other.right());
42 let y2 = self.bottom().min(other.bottom());
43
44 if x2 <= x1 || y2 <= y1 {
45 LogicalRect {
46 x: x1,
47 y: y1,
48 width: 0,
49 height: 0,
50 }
51 } else {
52 LogicalRect {
53 x: x1,
54 y: y1,
55 width: x2 - x1,
56 height: y2 - y1,
57 }
58 }
59 }
60
61 #[inline(always)]
62 pub fn intersects(self, other: Self) -> bool {
63 self.y < other.y + other.height
64 && self.y + self.height > other.y
65 && self.x < other.x + other.width
66 && self.x + self.width > other.x
67 }
68
69 pub fn with_offset(mut self, offset_x: i32, offset_y: i32) -> Self {
70 self.x -= offset_x;
71 self.y -= offset_y;
72 self
73 }
74
75 pub const fn area(self) -> i64 {
76 (self.width as i64) * (self.height as i64)
77 }
78
79 pub const fn left(self) -> i32 {
80 self.x
81 }
82
83 pub const fn right(self) -> i32 {
84 self.x.saturating_add(self.width)
85 }
86
87 pub const fn top(self) -> i32 {
88 self.y
89 }
90
91 pub const fn bottom(self) -> i32 {
92 self.y.saturating_add(self.height)
93 }
94}
95
96pub struct Canvas<'a> {
97 buf: &'a mut Buffer,
98 rect: LogicalRect,
99 offset_x: i32,
100 offset_y: i32,
101}
102
103impl<'a> Canvas<'a> {
104 pub fn new(rect: LogicalRect, buf: &'a mut Buffer) -> Self {
105 Self {
106 rect,
107 buf,
108 offset_x: 0,
109 offset_y: 0,
110 }
111 }
112}
113
114impl Canvas<'_> {
115 fn set_stringn<T, S>(&mut self, x: i32, y: i32, text: T, max_width: usize, style: S)
116 where
117 T: AsRef<str>,
118 S: Into<Style>,
119 {
120 let buffer_area = {
121 let inner = LogicalRect::from(self.buf.area);
122
123 if self.clipped() {
124 inner.intersection(self.rect)
125 } else {
126 inner
127 }
128 };
129
130 let buf_x = self.get_buf_column(x);
131 let buf_y = self.get_buf_row(y);
132 let max_width = max_width.try_into().unwrap_or(i32::MAX);
133
134 if buf_y < buffer_area.top() || buf_y >= buffer_area.bottom() {
135 return;
136 }
137
138 let start = buf_x.max(buffer_area.left());
139
140 if start >= buffer_area.right() {
141 return;
142 }
143
144 let mut remaining = (buffer_area.right() - start).clamp(0, max_width) as u16;
145 let mut skip_width = (start - buf_x).max(0) as u16;
146 let mut cursor = start as u16;
147 let row = buf_y as u16;
148
149 let style = style.into();
150
151 for g in UnicodeSegmentation::graphemes(text.as_ref(), true) {
152 if g.contains(char::is_control) {
153 continue;
154 }
155
156 let width = g.width() as u16;
157
158 if width == 0 || skip_width > 0 {
159 skip_width = skip_width.saturating_sub(width);
160 continue;
161 }
162
163 if remaining < width {
164 break;
165 }
166
167 self.buf[(cursor, row)].set_symbol(g).set_style(style);
168
169 let end = cursor + width;
170 cursor += 1;
171
172 while cursor < end {
173 self.buf[(cursor, row)].reset();
174 cursor += 1;
175 }
176
177 remaining -= width;
178 }
179 }
180
181 fn get_buf_column(&self, x: i32) -> i32 {
182 self.rect.x + x - self.offset_x
183 }
184
185 fn get_buf_row(&self, y: i32) -> i32 {
186 self.rect.y + y - self.offset_y
187 }
188
189 fn clipped(&self) -> bool {
190 false
191 }
192}
193
194impl Canvas<'_> {
195 pub fn buffer_mut(&mut self) -> &mut Buffer {
196 self.buf
197 }
198
199 pub fn rect(&self) -> LogicalRect {
200 self.rect
201 }
202
203 pub fn area(&self) -> LogicalRect {
204 LogicalRect::new(0, 0, self.rect.width as u16, self.rect.height as u16)
205 }
206
207 pub fn set_offset(&mut self, offset_x: i32, offset_y: i32) {
208 self.offset_x = offset_x;
209 self.offset_y = offset_y;
210 }
211
212 pub fn text<T, S>(&mut self, x: i32, y: i32, text: T, style: S)
213 where
214 T: AsRef<str>,
215 S: Into<Style>,
216 {
217 self.set_stringn(x, y, text, usize::MAX, style)
218 }
219
220 pub fn render_widget(&mut self, rect: LogicalRect, widget: impl Widget) {
221 let rect = rect.with_offset(self.offset_x - self.rect.x, self.offset_y - self.rect.y);
222
223 if !rect.intersects(self.rect) {
224 return;
225 }
226
227 let clip = rect.intersection(self.rect);
229
230 let buffer_bounds = LogicalRect::from(self.buf.area);
232 let final_clip = clip.intersection(buffer_bounds);
233
234 if final_clip.width <= 0 || final_clip.height <= 0 {
236 return;
237 }
238
239 let temp_rect = Rect {
244 x: 0,
245 y: 0,
246 width: rect.width.min(u16::MAX as i32) as u16,
247 height: rect.height.min(u16::MAX as i32) as u16,
248 };
249
250 let mut temp_buf = Buffer::empty(temp_rect);
251 widget.render(temp_rect, &mut temp_buf);
252
253 let src_x0 = (final_clip.x - rect.x) as usize;
256 let src_y0 = (final_clip.y - rect.y) as usize;
257
258 let dst_x0 = (final_clip.x - self.buf.area.x as i32) as usize;
261 let dst_y0 = (final_clip.y - self.buf.area.y as i32) as usize;
262
263 let src_stride = rect.width as usize;
264 let dst_stride = self.buf.area.width as usize;
265 let row_len = final_clip.width as usize;
266
267 for row in 0..final_clip.height as usize {
269 let src_row = (src_y0 + row) * src_stride + src_x0;
270 let dst_row = (dst_y0 + row) * dst_stride + dst_x0;
271
272 let src = &temp_buf.content[src_row..src_row + row_len];
273 let dst = &mut self.buf.content[dst_row..dst_row + row_len];
274 dst.clone_from_slice(src);
275 }
276 }
277}