1use std::hash::{Hash, Hasher};
8use std::sync::Arc;
9
10use crate::cell::Cell;
11use crate::rect::Rect;
12use crate::style::Style;
13use unicode_width::UnicodeWidthChar;
14
15#[derive(Clone, Debug)]
21#[allow(dead_code)]
22pub(crate) struct KittyPlacement {
23 pub content_hash: u64,
25 pub rgba: Arc<Vec<u8>>,
27 pub src_width: u32,
29 pub src_height: u32,
31 pub x: u32,
33 pub y: u32,
34 pub cols: u32,
36 pub rows: u32,
37 pub crop_y: u32,
39 pub crop_h: u32,
41}
42
43pub(crate) fn hash_rgba(data: &[u8]) -> u64 {
45 let mut hasher = std::collections::hash_map::DefaultHasher::new();
46 data.hash(&mut hasher);
47 hasher.finish()
48}
49
50impl PartialEq for KittyPlacement {
51 fn eq(&self, other: &Self) -> bool {
52 self.content_hash == other.content_hash
53 && self.x == other.x
54 && self.y == other.y
55 && self.cols == other.cols
56 && self.rows == other.rows
57 && self.crop_y == other.crop_y
58 && self.crop_h == other.crop_h
59 }
60}
61
62pub struct Buffer {
71 pub area: Rect,
73 pub content: Vec<Cell>,
75 pub(crate) clip_stack: Vec<Rect>,
76 pub(crate) raw_sequences: Vec<(u32, u32, String)>,
77 pub(crate) kitty_placements: Vec<KittyPlacement>,
78 pub(crate) cursor_pos: Option<(u32, u32)>,
79 pub(crate) kitty_clip_info: Option<(u32, u32)>,
82}
83
84impl Buffer {
85 pub fn empty(area: Rect) -> Self {
87 let size = area.area() as usize;
88 Self {
89 area,
90 content: vec![Cell::default(); size],
91 clip_stack: Vec::new(),
92 raw_sequences: Vec::new(),
93 kitty_placements: Vec::new(),
94 cursor_pos: None,
95 kitty_clip_info: None,
96 }
97 }
98
99 pub(crate) fn set_cursor_pos(&mut self, x: u32, y: u32) {
100 self.cursor_pos = Some((x, y));
101 }
102
103 #[cfg(feature = "crossterm")]
104 pub(crate) fn cursor_pos(&self) -> Option<(u32, u32)> {
105 self.cursor_pos
106 }
107
108 pub fn raw_sequence(&mut self, x: u32, y: u32, seq: String) {
113 if let Some(clip) = self.effective_clip() {
114 if x >= clip.right() || y >= clip.bottom() {
115 return;
116 }
117 }
118 self.raw_sequences.push((x, y, seq));
119 }
120
121 pub(crate) fn kitty_place(&mut self, mut p: KittyPlacement) {
127 if let Some(clip) = self.effective_clip() {
129 if p.x >= clip.right()
130 || p.y >= clip.bottom()
131 || p.x + p.cols <= clip.x
132 || p.y + p.rows <= clip.y
133 {
134 return;
135 }
136 }
137
138 if let Some((top_clip_rows, original_height)) = self.kitty_clip_info {
140 if original_height > 0 && (top_clip_rows > 0 || p.rows < original_height) {
141 let ratio = p.src_height as f64 / original_height as f64;
142 p.crop_y = (top_clip_rows as f64 * ratio) as u32;
143 let bottom_clip = original_height.saturating_sub(top_clip_rows + p.rows);
144 let bottom_pixels = (bottom_clip as f64 * ratio) as u32;
145 p.crop_h = p.src_height.saturating_sub(p.crop_y + bottom_pixels);
146 }
147 }
148
149 self.kitty_placements.push(p);
150 }
151
152 pub fn push_clip(&mut self, rect: Rect) {
158 let effective = if let Some(current) = self.clip_stack.last() {
159 intersect_rects(*current, rect)
160 } else {
161 rect
162 };
163 self.clip_stack.push(effective);
164 }
165
166 pub fn pop_clip(&mut self) {
171 self.clip_stack.pop();
172 }
173
174 fn effective_clip(&self) -> Option<&Rect> {
175 self.clip_stack.last()
176 }
177
178 #[inline]
179 fn index_of(&self, x: u32, y: u32) -> usize {
180 ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
181 }
182
183 #[inline]
185 pub fn in_bounds(&self, x: u32, y: u32) -> bool {
186 x >= self.area.x && x < self.area.right() && y >= self.area.y && y < self.area.bottom()
187 }
188
189 #[inline]
193 pub fn get(&self, x: u32, y: u32) -> &Cell {
194 debug_assert!(
195 self.in_bounds(x, y),
196 "Buffer::get({x}, {y}) out of bounds for area {:?}",
197 self.area
198 );
199 &self.content[self.index_of(x, y)]
200 }
201
202 #[inline]
206 pub fn get_mut(&mut self, x: u32, y: u32) -> &mut Cell {
207 debug_assert!(
208 self.in_bounds(x, y),
209 "Buffer::get_mut({x}, {y}) out of bounds for area {:?}",
210 self.area
211 );
212 let idx = self.index_of(x, y);
213 &mut self.content[idx]
214 }
215
216 pub fn set_string(&mut self, mut x: u32, y: u32, s: &str, style: Style) {
223 if y >= self.area.bottom() {
224 return;
225 }
226 let clip = self.effective_clip().copied();
227 for ch in s.chars() {
228 if x >= self.area.right() {
229 break;
230 }
231 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
232 if char_width == 0 {
233 if x > self.area.x {
236 let prev_in_clip = clip.map_or(true, |clip| {
237 (x - 1) >= clip.x
238 && (x - 1) < clip.right()
239 && y >= clip.y
240 && y < clip.bottom()
241 });
242 if prev_in_clip {
243 self.get_mut(x - 1, y).symbol.push(ch);
244 }
245 }
246 continue;
247 }
248
249 let in_clip = clip.map_or(true, |clip| {
250 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
251 });
252
253 if !in_clip {
254 x = x.saturating_add(char_width);
255 continue;
256 }
257
258 let cell = self.get_mut(x, y);
259 cell.set_char(ch);
260 cell.set_style(style);
261
262 if char_width > 1 {
264 let next_x = x + 1;
265 if next_x < self.area.right() {
266 let next = self.get_mut(next_x, y);
267 next.symbol.clear();
268 next.style = style;
269 }
270 }
271
272 x = x.saturating_add(char_width);
273 }
274 }
275
276 pub fn set_string_linked(&mut self, mut x: u32, y: u32, s: &str, style: Style, url: &str) {
281 if y >= self.area.bottom() {
282 return;
283 }
284 let clip = self.effective_clip().copied();
285 let link = Some(compact_str::CompactString::new(url));
286 for ch in s.chars() {
287 if x >= self.area.right() {
288 break;
289 }
290 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
291 if char_width == 0 {
292 if x > self.area.x {
293 let prev_in_clip = clip.map_or(true, |clip| {
294 (x - 1) >= clip.x
295 && (x - 1) < clip.right()
296 && y >= clip.y
297 && y < clip.bottom()
298 });
299 if prev_in_clip {
300 self.get_mut(x - 1, y).symbol.push(ch);
301 }
302 }
303 continue;
304 }
305
306 let in_clip = clip.map_or(true, |clip| {
307 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
308 });
309
310 if !in_clip {
311 x = x.saturating_add(char_width);
312 continue;
313 }
314
315 let cell = self.get_mut(x, y);
316 cell.set_char(ch);
317 cell.set_style(style);
318 cell.hyperlink = link.clone();
319
320 if char_width > 1 {
321 let next_x = x + 1;
322 if next_x < self.area.right() {
323 let next = self.get_mut(next_x, y);
324 next.symbol.clear();
325 next.style = style;
326 next.hyperlink = link.clone();
327 }
328 }
329
330 x = x.saturating_add(char_width);
331 }
332 }
333
334 pub fn set_char(&mut self, x: u32, y: u32, ch: char, style: Style) {
338 let in_clip = self.effective_clip().map_or(true, |clip| {
339 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
340 });
341 if !self.in_bounds(x, y) || !in_clip {
342 return;
343 }
344 let cell = self.get_mut(x, y);
345 cell.set_char(ch);
346 cell.set_style(style);
347 }
348
349 pub fn diff<'a>(&'a self, other: &'a Buffer) -> Vec<(u32, u32, &'a Cell)> {
355 let mut updates = Vec::new();
356 for y in self.area.y..self.area.bottom() {
357 for x in self.area.x..self.area.right() {
358 let cur = self.get(x, y);
359 let prev = other.get(x, y);
360 if cur != prev {
361 updates.push((x, y, cur));
362 }
363 }
364 }
365 updates
366 }
367
368 pub fn reset(&mut self) {
370 for cell in &mut self.content {
371 cell.reset();
372 }
373 self.clip_stack.clear();
374 self.raw_sequences.clear();
375 self.kitty_placements.clear();
376 self.cursor_pos = None;
377 self.kitty_clip_info = None;
378 }
379
380 pub fn reset_with_bg(&mut self, bg: crate::style::Color) {
382 for cell in &mut self.content {
383 cell.reset();
384 cell.style.bg = Some(bg);
385 }
386 self.clip_stack.clear();
387 self.raw_sequences.clear();
388 self.kitty_placements.clear();
389 self.cursor_pos = None;
390 self.kitty_clip_info = None;
391 }
392
393 pub fn resize(&mut self, area: Rect) {
398 self.area = area;
399 let size = area.area() as usize;
400 self.content.resize(size, Cell::default());
401 self.reset();
402 }
403}
404
405fn intersect_rects(a: Rect, b: Rect) -> Rect {
406 let x = a.x.max(b.x);
407 let y = a.y.max(b.y);
408 let right = a.right().min(b.right());
409 let bottom = a.bottom().min(b.bottom());
410 let width = right.saturating_sub(x);
411 let height = bottom.saturating_sub(y);
412 Rect::new(x, y, width, height)
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn clip_stack_intersects_nested_regions() {
421 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
422 buf.push_clip(Rect::new(1, 1, 6, 3));
423 buf.push_clip(Rect::new(4, 0, 6, 4));
424
425 buf.set_char(3, 2, 'x', Style::new());
426 buf.set_char(4, 2, 'y', Style::new());
427
428 assert_eq!(buf.get(3, 2).symbol, " ");
429 assert_eq!(buf.get(4, 2).symbol, "y");
430 }
431
432 #[test]
433 fn set_string_advances_even_when_clipped() {
434 let mut buf = Buffer::empty(Rect::new(0, 0, 8, 1));
435 buf.push_clip(Rect::new(2, 0, 6, 1));
436
437 buf.set_string(0, 0, "abcd", Style::new());
438
439 assert_eq!(buf.get(2, 0).symbol, "c");
440 assert_eq!(buf.get(3, 0).symbol, "d");
441 }
442
443 #[test]
444 fn pop_clip_restores_previous_clip() {
445 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
446 buf.push_clip(Rect::new(0, 0, 2, 1));
447 buf.push_clip(Rect::new(4, 0, 2, 1));
448
449 buf.set_char(1, 0, 'a', Style::new());
450 buf.pop_clip();
451 buf.set_char(1, 0, 'b', Style::new());
452
453 assert_eq!(buf.get(1, 0).symbol, "b");
454 }
455
456 #[test]
457 fn reset_clears_clip_stack() {
458 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
459 buf.push_clip(Rect::new(0, 0, 0, 0));
460 buf.reset();
461 buf.set_char(0, 0, 'z', Style::new());
462
463 assert_eq!(buf.get(0, 0).symbol, "z");
464 }
465}