vtui_core/
canvas.rs

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        // Source origin inside temp buffer
239        let src_x0 = (clip.x - rect.x) as usize;
240        let src_y0 = (clip.y - rect.y) as usize;
241
242        // Destination origin inside canvas buffer
243        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}