1use crate::cell::Cell;
2use crate::rect::Rect;
3use crate::style::Style;
4use unicode_width::UnicodeWidthChar;
5
6pub struct Buffer {
15 pub area: Rect,
17 pub content: Vec<Cell>,
19 pub(crate) clip_stack: Vec<Rect>,
20}
21
22impl Buffer {
23 pub fn empty(area: Rect) -> Self {
25 let size = area.area() as usize;
26 Self {
27 area,
28 content: vec![Cell::default(); size],
29 clip_stack: Vec::new(),
30 }
31 }
32
33 pub fn push_clip(&mut self, rect: Rect) {
39 let effective = if let Some(current) = self.clip_stack.last() {
40 intersect_rects(*current, rect)
41 } else {
42 rect
43 };
44 self.clip_stack.push(effective);
45 }
46
47 pub fn pop_clip(&mut self) {
52 self.clip_stack.pop();
53 }
54
55 fn effective_clip(&self) -> Option<&Rect> {
56 self.clip_stack.last()
57 }
58
59 #[inline]
60 fn index_of(&self, x: u32, y: u32) -> usize {
61 ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
62 }
63
64 #[inline]
66 pub fn in_bounds(&self, x: u32, y: u32) -> bool {
67 x >= self.area.x && x < self.area.right() && y >= self.area.y && y < self.area.bottom()
68 }
69
70 #[inline]
74 pub fn get(&self, x: u32, y: u32) -> &Cell {
75 &self.content[self.index_of(x, y)]
76 }
77
78 #[inline]
82 pub fn get_mut(&mut self, x: u32, y: u32) -> &mut Cell {
83 let idx = self.index_of(x, y);
84 &mut self.content[idx]
85 }
86
87 pub fn set_string(&mut self, mut x: u32, y: u32, s: &str, style: Style) {
94 if y >= self.area.bottom() {
95 return;
96 }
97 let clip = self.effective_clip().copied();
98 for ch in s.chars() {
99 if x >= self.area.right() {
100 break;
101 }
102 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
103 if char_width == 0 {
104 if x > self.area.x {
107 let prev_in_clip = clip.map_or(true, |clip| {
108 (x - 1) >= clip.x
109 && (x - 1) < clip.right()
110 && y >= clip.y
111 && y < clip.bottom()
112 });
113 if prev_in_clip {
114 self.get_mut(x - 1, y).symbol.push(ch);
115 }
116 }
117 continue;
118 }
119
120 let in_clip = clip.map_or(true, |clip| {
121 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
122 });
123
124 if !in_clip {
125 x = x.saturating_add(char_width);
126 continue;
127 }
128
129 let cell = self.get_mut(x, y);
130 cell.set_char(ch);
131 cell.set_style(style);
132
133 if char_width > 1 {
135 let next_x = x + 1;
136 if next_x < self.area.right() {
137 let next = self.get_mut(next_x, y);
138 next.symbol.clear();
139 next.style = style;
140 }
141 }
142
143 x = x.saturating_add(char_width);
144 }
145 }
146
147 pub fn set_string_linked(&mut self, mut x: u32, y: u32, s: &str, style: Style, url: &str) {
152 if y >= self.area.bottom() {
153 return;
154 }
155 let clip = self.effective_clip().copied();
156 let link = Some(url.to_string());
157 for ch in s.chars() {
158 if x >= self.area.right() {
159 break;
160 }
161 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
162 if char_width == 0 {
163 if x > self.area.x {
164 let prev_in_clip = clip.map_or(true, |clip| {
165 (x - 1) >= clip.x
166 && (x - 1) < clip.right()
167 && y >= clip.y
168 && y < clip.bottom()
169 });
170 if prev_in_clip {
171 self.get_mut(x - 1, y).symbol.push(ch);
172 }
173 }
174 continue;
175 }
176
177 let in_clip = clip.map_or(true, |clip| {
178 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
179 });
180
181 if !in_clip {
182 x = x.saturating_add(char_width);
183 continue;
184 }
185
186 let cell = self.get_mut(x, y);
187 cell.set_char(ch);
188 cell.set_style(style);
189 cell.hyperlink = link.clone();
190
191 if char_width > 1 {
192 let next_x = x + 1;
193 if next_x < self.area.right() {
194 let next = self.get_mut(next_x, y);
195 next.symbol.clear();
196 next.style = style;
197 next.hyperlink = link.clone();
198 }
199 }
200
201 x = x.saturating_add(char_width);
202 }
203 }
204
205 pub fn set_char(&mut self, x: u32, y: u32, ch: char, style: Style) {
209 let in_clip = self.effective_clip().map_or(true, |clip| {
210 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
211 });
212 if !self.in_bounds(x, y) || !in_clip {
213 return;
214 }
215 let cell = self.get_mut(x, y);
216 cell.set_char(ch);
217 cell.set_style(style);
218 }
219
220 pub fn diff<'a>(&'a self, other: &'a Buffer) -> Vec<(u32, u32, &'a Cell)> {
226 let mut updates = Vec::new();
227 for y in self.area.y..self.area.bottom() {
228 for x in self.area.x..self.area.right() {
229 let cur = self.get(x, y);
230 let prev = other.get(x, y);
231 if cur != prev {
232 updates.push((x, y, cur));
233 }
234 }
235 }
236 updates
237 }
238
239 pub fn reset(&mut self) {
241 for cell in &mut self.content {
242 cell.reset();
243 }
244 self.clip_stack.clear();
245 }
246
247 pub fn resize(&mut self, area: Rect) {
252 self.area = area;
253 let size = area.area() as usize;
254 self.content.resize(size, Cell::default());
255 self.reset();
256 }
257}
258
259fn intersect_rects(a: Rect, b: Rect) -> Rect {
260 let x = a.x.max(b.x);
261 let y = a.y.max(b.y);
262 let right = a.right().min(b.right());
263 let bottom = a.bottom().min(b.bottom());
264 let width = right.saturating_sub(x);
265 let height = bottom.saturating_sub(y);
266 Rect::new(x, y, width, height)
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn clip_stack_intersects_nested_regions() {
275 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
276 buf.push_clip(Rect::new(1, 1, 6, 3));
277 buf.push_clip(Rect::new(4, 0, 6, 4));
278
279 buf.set_char(3, 2, 'x', Style::new());
280 buf.set_char(4, 2, 'y', Style::new());
281
282 assert_eq!(buf.get(3, 2).symbol, " ");
283 assert_eq!(buf.get(4, 2).symbol, "y");
284 }
285
286 #[test]
287 fn set_string_advances_even_when_clipped() {
288 let mut buf = Buffer::empty(Rect::new(0, 0, 8, 1));
289 buf.push_clip(Rect::new(2, 0, 6, 1));
290
291 buf.set_string(0, 0, "abcd", Style::new());
292
293 assert_eq!(buf.get(2, 0).symbol, "c");
294 assert_eq!(buf.get(3, 0).symbol, "d");
295 }
296
297 #[test]
298 fn pop_clip_restores_previous_clip() {
299 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
300 buf.push_clip(Rect::new(0, 0, 2, 1));
301 buf.push_clip(Rect::new(4, 0, 2, 1));
302
303 buf.set_char(1, 0, 'a', Style::new());
304 buf.pop_clip();
305 buf.set_char(1, 0, 'b', Style::new());
306
307 assert_eq!(buf.get(1, 0).symbol, "b");
308 }
309
310 #[test]
311 fn reset_clears_clip_stack() {
312 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
313 buf.push_clip(Rect::new(0, 0, 0, 0));
314 buf.reset();
315 buf.set_char(0, 0, 'z', Style::new());
316
317 assert_eq!(buf.get(0, 0).symbol, "z");
318 }
319}