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