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) kitty_clip_info: Option<(u32, u32)>,
81}
82
83impl Buffer {
84 pub fn empty(area: Rect) -> Self {
86 let size = area.area() as usize;
87 Self {
88 area,
89 content: vec![Cell::default(); size],
90 clip_stack: Vec::new(),
91 raw_sequences: Vec::new(),
92 kitty_placements: Vec::new(),
93 kitty_clip_info: None,
94 }
95 }
96
97 pub fn raw_sequence(&mut self, x: u32, y: u32, seq: String) {
102 if let Some(clip) = self.effective_clip() {
103 if x >= clip.right() || y >= clip.bottom() {
104 return;
105 }
106 }
107 self.raw_sequences.push((x, y, seq));
108 }
109
110 pub(crate) fn kitty_place(&mut self, mut p: KittyPlacement) {
116 if let Some(clip) = self.effective_clip() {
118 if p.x >= clip.right()
119 || p.y >= clip.bottom()
120 || p.x + p.cols <= clip.x
121 || p.y + p.rows <= clip.y
122 {
123 return;
124 }
125 }
126
127 if let Some((top_clip_rows, original_height)) = self.kitty_clip_info {
129 if original_height > 0 && (top_clip_rows > 0 || p.rows < original_height) {
130 let ratio = p.src_height as f64 / original_height as f64;
131 p.crop_y = (top_clip_rows as f64 * ratio) as u32;
132 let bottom_clip = original_height.saturating_sub(top_clip_rows + p.rows);
133 let bottom_pixels = (bottom_clip as f64 * ratio) as u32;
134 p.crop_h = p.src_height.saturating_sub(p.crop_y + bottom_pixels);
135 }
136 }
137
138 self.kitty_placements.push(p);
139 }
140
141 pub fn push_clip(&mut self, rect: Rect) {
147 let effective = if let Some(current) = self.clip_stack.last() {
148 intersect_rects(*current, rect)
149 } else {
150 rect
151 };
152 self.clip_stack.push(effective);
153 }
154
155 pub fn pop_clip(&mut self) {
160 self.clip_stack.pop();
161 }
162
163 fn effective_clip(&self) -> Option<&Rect> {
164 self.clip_stack.last()
165 }
166
167 #[inline]
168 fn index_of(&self, x: u32, y: u32) -> usize {
169 ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
170 }
171
172 #[inline]
174 pub fn in_bounds(&self, x: u32, y: u32) -> bool {
175 x >= self.area.x && x < self.area.right() && y >= self.area.y && y < self.area.bottom()
176 }
177
178 #[inline]
182 pub fn get(&self, x: u32, y: u32) -> &Cell {
183 debug_assert!(
184 self.in_bounds(x, y),
185 "Buffer::get({x}, {y}) out of bounds for area {:?}",
186 self.area
187 );
188 &self.content[self.index_of(x, y)]
189 }
190
191 #[inline]
195 pub fn get_mut(&mut self, x: u32, y: u32) -> &mut Cell {
196 debug_assert!(
197 self.in_bounds(x, y),
198 "Buffer::get_mut({x}, {y}) out of bounds for area {:?}",
199 self.area
200 );
201 let idx = self.index_of(x, y);
202 &mut self.content[idx]
203 }
204
205 pub fn set_string(&mut self, mut x: u32, y: u32, s: &str, style: Style) {
212 if y >= self.area.bottom() {
213 return;
214 }
215 let clip = self.effective_clip().copied();
216 for ch in s.chars() {
217 if x >= self.area.right() {
218 break;
219 }
220 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
221 if char_width == 0 {
222 if x > self.area.x {
225 let prev_in_clip = clip.map_or(true, |clip| {
226 (x - 1) >= clip.x
227 && (x - 1) < clip.right()
228 && y >= clip.y
229 && y < clip.bottom()
230 });
231 if prev_in_clip {
232 self.get_mut(x - 1, y).symbol.push(ch);
233 }
234 }
235 continue;
236 }
237
238 let in_clip = clip.map_or(true, |clip| {
239 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
240 });
241
242 if !in_clip {
243 x = x.saturating_add(char_width);
244 continue;
245 }
246
247 let cell = self.get_mut(x, y);
248 cell.set_char(ch);
249 cell.set_style(style);
250
251 if char_width > 1 {
253 let next_x = x + 1;
254 if next_x < self.area.right() {
255 let next = self.get_mut(next_x, y);
256 next.symbol.clear();
257 next.style = style;
258 }
259 }
260
261 x = x.saturating_add(char_width);
262 }
263 }
264
265 pub fn set_string_linked(&mut self, mut x: u32, y: u32, s: &str, style: Style, url: &str) {
270 if y >= self.area.bottom() {
271 return;
272 }
273 let clip = self.effective_clip().copied();
274 let link = Some(compact_str::CompactString::new(url));
275 for ch in s.chars() {
276 if x >= self.area.right() {
277 break;
278 }
279 let char_width = UnicodeWidthChar::width(ch).unwrap_or(0) as u32;
280 if char_width == 0 {
281 if x > self.area.x {
282 let prev_in_clip = clip.map_or(true, |clip| {
283 (x - 1) >= clip.x
284 && (x - 1) < clip.right()
285 && y >= clip.y
286 && y < clip.bottom()
287 });
288 if prev_in_clip {
289 self.get_mut(x - 1, y).symbol.push(ch);
290 }
291 }
292 continue;
293 }
294
295 let in_clip = clip.map_or(true, |clip| {
296 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
297 });
298
299 if !in_clip {
300 x = x.saturating_add(char_width);
301 continue;
302 }
303
304 let cell = self.get_mut(x, y);
305 cell.set_char(ch);
306 cell.set_style(style);
307 cell.hyperlink = link.clone();
308
309 if char_width > 1 {
310 let next_x = x + 1;
311 if next_x < self.area.right() {
312 let next = self.get_mut(next_x, y);
313 next.symbol.clear();
314 next.style = style;
315 next.hyperlink = link.clone();
316 }
317 }
318
319 x = x.saturating_add(char_width);
320 }
321 }
322
323 pub fn set_char(&mut self, x: u32, y: u32, ch: char, style: Style) {
327 let in_clip = self.effective_clip().map_or(true, |clip| {
328 x >= clip.x && x < clip.right() && y >= clip.y && y < clip.bottom()
329 });
330 if !self.in_bounds(x, y) || !in_clip {
331 return;
332 }
333 let cell = self.get_mut(x, y);
334 cell.set_char(ch);
335 cell.set_style(style);
336 }
337
338 pub fn diff<'a>(&'a self, other: &'a Buffer) -> Vec<(u32, u32, &'a Cell)> {
344 let mut updates = Vec::new();
345 for y in self.area.y..self.area.bottom() {
346 for x in self.area.x..self.area.right() {
347 let cur = self.get(x, y);
348 let prev = other.get(x, y);
349 if cur != prev {
350 updates.push((x, y, cur));
351 }
352 }
353 }
354 updates
355 }
356
357 pub fn reset(&mut self) {
359 for cell in &mut self.content {
360 cell.reset();
361 }
362 self.clip_stack.clear();
363 self.raw_sequences.clear();
364 self.kitty_placements.clear();
365 self.kitty_clip_info = None;
366 }
367
368 pub fn reset_with_bg(&mut self, bg: crate::style::Color) {
370 for cell in &mut self.content {
371 cell.reset();
372 cell.style.bg = Some(bg);
373 }
374 self.clip_stack.clear();
375 self.raw_sequences.clear();
376 self.kitty_placements.clear();
377 self.kitty_clip_info = None;
378 }
379
380 pub fn resize(&mut self, area: Rect) {
385 self.area = area;
386 let size = area.area() as usize;
387 self.content.resize(size, Cell::default());
388 self.reset();
389 }
390}
391
392fn intersect_rects(a: Rect, b: Rect) -> Rect {
393 let x = a.x.max(b.x);
394 let y = a.y.max(b.y);
395 let right = a.right().min(b.right());
396 let bottom = a.bottom().min(b.bottom());
397 let width = right.saturating_sub(x);
398 let height = bottom.saturating_sub(y);
399 Rect::new(x, y, width, height)
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn clip_stack_intersects_nested_regions() {
408 let mut buf = Buffer::empty(Rect::new(0, 0, 10, 5));
409 buf.push_clip(Rect::new(1, 1, 6, 3));
410 buf.push_clip(Rect::new(4, 0, 6, 4));
411
412 buf.set_char(3, 2, 'x', Style::new());
413 buf.set_char(4, 2, 'y', Style::new());
414
415 assert_eq!(buf.get(3, 2).symbol, " ");
416 assert_eq!(buf.get(4, 2).symbol, "y");
417 }
418
419 #[test]
420 fn set_string_advances_even_when_clipped() {
421 let mut buf = Buffer::empty(Rect::new(0, 0, 8, 1));
422 buf.push_clip(Rect::new(2, 0, 6, 1));
423
424 buf.set_string(0, 0, "abcd", Style::new());
425
426 assert_eq!(buf.get(2, 0).symbol, "c");
427 assert_eq!(buf.get(3, 0).symbol, "d");
428 }
429
430 #[test]
431 fn pop_clip_restores_previous_clip() {
432 let mut buf = Buffer::empty(Rect::new(0, 0, 6, 1));
433 buf.push_clip(Rect::new(0, 0, 2, 1));
434 buf.push_clip(Rect::new(4, 0, 2, 1));
435
436 buf.set_char(1, 0, 'a', Style::new());
437 buf.pop_clip();
438 buf.set_char(1, 0, 'b', Style::new());
439
440 assert_eq!(buf.get(1, 0).symbol, "b");
441 }
442
443 #[test]
444 fn reset_clears_clip_stack() {
445 let mut buf = Buffer::empty(Rect::new(0, 0, 4, 1));
446 buf.push_clip(Rect::new(0, 0, 0, 0));
447 buf.reset();
448 buf.set_char(0, 0, 'z', Style::new());
449
450 assert_eq!(buf.get(0, 0).symbol, "z");
451 }
452}