edit/
framebuffer.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! A shoddy framebuffer for terminal applications.
5
6use std::cell::Cell;
7use std::fmt::Write;
8use std::ops::{BitOr, BitXor};
9use std::ptr;
10use std::slice::ChunksExact;
11
12use crate::arena::{Arena, ArenaString};
13use crate::helpers::{CoordType, Point, Rect, Size};
14use crate::oklab::{oklab_blend, srgb_to_oklab};
15use crate::simd::{MemsetSafe, memset};
16use crate::unicode::MeasurementConfig;
17
18// Same constants as used in the PCG family of RNGs.
19#[cfg(target_pointer_width = "32")]
20const HASH_MULTIPLIER: usize = 747796405; // https://doi.org/10.1090/S0025-5718-99-00996-5, Table 5
21#[cfg(target_pointer_width = "64")]
22const HASH_MULTIPLIER: usize = 6364136223846793005; // Knuth's MMIX multiplier
23
24/// The size of our cache table. 1<<8 = 256.
25const CACHE_TABLE_LOG2_SIZE: usize = 8;
26const CACHE_TABLE_SIZE: usize = 1 << CACHE_TABLE_LOG2_SIZE;
27/// To index into the cache table, we use `color * HASH_MULTIPLIER` as the hash.
28/// Since the multiplication "shifts" the bits up, we don't just mask the lowest
29/// 8 bits out, but rather shift 56 bits down to get the best bits from the top.
30const CACHE_TABLE_SHIFT: usize = usize::BITS as usize - CACHE_TABLE_LOG2_SIZE;
31
32/// Standard 16 VT & default foreground/background colors.
33#[derive(Clone, Copy)]
34pub enum IndexedColor {
35    Black,
36    Red,
37    Green,
38    Yellow,
39    Blue,
40    Magenta,
41    Cyan,
42    White,
43    BrightBlack,
44    BrightRed,
45    BrightGreen,
46    BrightYellow,
47    BrightBlue,
48    BrightMagenta,
49    BrightCyan,
50    BrightWhite,
51
52    Background,
53    Foreground,
54}
55
56/// Number of indices used by [`IndexedColor`].
57pub const INDEXED_COLORS_COUNT: usize = 18;
58
59/// Fallback theme. Matches Windows Terminal's Ottosson theme.
60pub const DEFAULT_THEME: [u32; INDEXED_COLORS_COUNT] = [
61    0xff000000, // Black
62    0xff212cbe, // Red
63    0xff3aae3f, // Green
64    0xff4a9abe, // Yellow
65    0xffbe4d20, // Blue
66    0xffbe54bb, // Magenta
67    0xffb2a700, // Cyan
68    0xffbebebe, // White
69    0xff808080, // BrightBlack
70    0xff303eff, // BrightRed
71    0xff51ea58, // BrightGreen
72    0xff44c9ff, // BrightYellow
73    0xffff6a2f, // BrightBlue
74    0xffff74fc, // BrightMagenta
75    0xfff0e100, // BrightCyan
76    0xffffffff, // BrightWhite
77    // --------
78    0xff000000, // Background
79    0xffbebebe, // Foreground
80];
81
82/// A shoddy framebuffer for terminal applications.
83///
84/// The idea is that you create a [`Framebuffer`], draw a bunch of text and
85/// colors into it, and it takes care of figuring out what changed since the
86/// last rendering and sending the differences as VT to the terminal.
87///
88/// This is an improvement over how many other terminal applications work,
89/// as they fail to accurately track what changed. If you watch the output
90/// of `vim` for instance, you'll notice that it redraws unrelated parts of
91/// the screen all the time.
92pub struct Framebuffer {
93    /// Store the color palette.
94    indexed_colors: [u32; INDEXED_COLORS_COUNT],
95    /// Front and back buffers. Indexed by `frame_counter & 1`.
96    buffers: [Buffer; 2],
97    /// The current frame counter. Increments on every `flip` call.
98    frame_counter: usize,
99    /// The colors used for `contrast()`. It stores the default colors
100    /// of the palette as [dark, light], unless the palette is recognized
101    /// as a light them, in which case it swaps them.
102    auto_colors: [u32; 2],
103    /// A cache table for previously contrasted colors.
104    /// See: <https://fgiesen.wordpress.com/2019/02/11/cache-tables/>
105    contrast_colors: [Cell<(u32, u32)>; CACHE_TABLE_SIZE],
106    background_fill: u32,
107    foreground_fill: u32,
108}
109
110impl Framebuffer {
111    /// Creates a new framebuffer.
112    pub fn new() -> Self {
113        Self {
114            indexed_colors: DEFAULT_THEME,
115            buffers: Default::default(),
116            frame_counter: 0,
117            auto_colors: [
118                DEFAULT_THEME[IndexedColor::Black as usize],
119                DEFAULT_THEME[IndexedColor::BrightWhite as usize],
120            ],
121            contrast_colors: [const { Cell::new((0, 0)) }; CACHE_TABLE_SIZE],
122            background_fill: DEFAULT_THEME[IndexedColor::Background as usize],
123            foreground_fill: DEFAULT_THEME[IndexedColor::Foreground as usize],
124        }
125    }
126
127    /// Sets the base color palette.
128    ///
129    /// If you call this method, [`Framebuffer`] expects that you
130    /// successfully detect the light/dark mode of the terminal.
131    pub fn set_indexed_colors(&mut self, colors: [u32; INDEXED_COLORS_COUNT]) {
132        self.indexed_colors = colors;
133        self.background_fill = 0;
134        self.foreground_fill = 0;
135
136        self.auto_colors = [
137            self.indexed_colors[IndexedColor::Black as usize],
138            self.indexed_colors[IndexedColor::BrightWhite as usize],
139        ];
140        if !Self::is_dark(self.auto_colors[0]) {
141            self.auto_colors.swap(0, 1);
142        }
143    }
144
145    /// Begins a new frame with the given `size`.
146    pub fn flip(&mut self, size: Size) {
147        if size != self.buffers[0].bg_bitmap.size {
148            for buffer in &mut self.buffers {
149                buffer.text = LineBuffer::new(size);
150                buffer.bg_bitmap = Bitmap::new(size);
151                buffer.fg_bitmap = Bitmap::new(size);
152                buffer.attributes = AttributeBuffer::new(size);
153            }
154
155            let front = &mut self.buffers[self.frame_counter & 1];
156            // Trigger a full redraw. (Yes, it's a hack.)
157            front.fg_bitmap.fill(1);
158            // Trigger a cursor update as well, just to be sure.
159            front.cursor = Cursor::new_invalid();
160        }
161
162        self.frame_counter = self.frame_counter.wrapping_add(1);
163
164        let back = &mut self.buffers[self.frame_counter & 1];
165
166        back.text.fill_whitespace();
167        back.bg_bitmap.fill(self.background_fill);
168        back.fg_bitmap.fill(self.foreground_fill);
169        back.attributes.reset();
170        back.cursor = Cursor::new_disabled();
171    }
172
173    /// Replaces text contents in a single line of the framebuffer.
174    /// All coordinates are in viewport coordinates.
175    /// Assumes that control characters have been replaced or escaped.
176    pub fn replace_text(
177        &mut self,
178        y: CoordType,
179        origin_x: CoordType,
180        clip_right: CoordType,
181        text: &str,
182    ) {
183        let back = &mut self.buffers[self.frame_counter & 1];
184        back.text.replace_text(y, origin_x, clip_right, text)
185    }
186
187    /// Draws a scrollbar in the given `track` rectangle.
188    ///
189    /// Not entirely sure why I put it here instead of elsewhere.
190    ///
191    /// # Parameters
192    ///
193    /// * `clip_rect`: Clips the rendering to this rectangle.
194    ///   This is relevant when you have scrollareas inside scrollareas.
195    /// * `track`: The rectangle in which to draw the scrollbar.
196    ///   In absolute viewport coordinates.
197    /// * `content_offset`: The current offset of the scrollarea.
198    /// * `content_height`: The height of the scrollarea content.
199    pub fn draw_scrollbar(
200        &mut self,
201        clip_rect: Rect,
202        track: Rect,
203        content_offset: CoordType,
204        content_height: CoordType,
205    ) -> CoordType {
206        let track_clipped = track.intersect(clip_rect);
207        if track_clipped.is_empty() {
208            return 0;
209        }
210
211        let viewport_height = track.height();
212        // The content height is at least the viewport height.
213        let content_height = content_height.max(viewport_height);
214
215        // No need to draw a scrollbar if the content fits in the viewport.
216        let content_offset_max = content_height - viewport_height;
217        if content_offset_max == 0 {
218            return 0;
219        }
220
221        // The content offset must be at least one viewport height from the bottom.
222        // You don't want to scroll past the end after all...
223        let content_offset = content_offset.clamp(0, content_offset_max);
224
225        // In order to increase the visual resolution of the scrollbar,
226        // we'll use 1/8th blocks to represent the thumb.
227        // First, scale the offsets to get that 1/8th resolution.
228        let viewport_height = viewport_height as i64 * 8;
229        let content_offset_max = content_offset_max as i64 * 8;
230        let content_offset = content_offset as i64 * 8;
231        let content_height = content_height as i64 * 8;
232
233        // The proportional thumb height (0-1) is the fraction of viewport and
234        // content height. The taller the content, the smaller the thumb:
235        // = viewport_height / content_height
236        // We then scale that to the viewport height to get the height in 1/8th units.
237        // = viewport_height * viewport_height / content_height
238        // We add content_height/2 to round the integer division, which results in a numerator of:
239        // = viewport_height * viewport_height + content_height / 2
240        let numerator = viewport_height * viewport_height + content_height / 2;
241        let thumb_height = numerator / content_height;
242        // Ensure the thumb has a minimum size of 1 row.
243        let thumb_height = thumb_height.max(8);
244
245        // The proportional thumb top position (0-1) is:
246        // = content_offset / content_offset_max
247        // The maximum thumb top position is the viewport height minus the thumb height:
248        // = viewport_height - thumb_height
249        // To get the thumb top position in 1/8th units, we multiply both:
250        // = (viewport_height - thumb_height) * content_offset / content_offset_max
251        // We add content_offset_max/2 to round the integer division, which results in a numerator of:
252        // = (viewport_height - thumb_height) * content_offset + content_offset_max / 2
253        let numerator = (viewport_height - thumb_height) * content_offset + content_offset_max / 2;
254        let thumb_top = numerator / content_offset_max;
255        // The thumb bottom position is the thumb top position plus the thumb height.
256        let thumb_bottom = thumb_top + thumb_height;
257
258        // Shift to absolute coordinates.
259        let thumb_top = thumb_top + track.top as i64 * 8;
260        let thumb_bottom = thumb_bottom + track.top as i64 * 8;
261
262        // Clamp to the visible area.
263        let thumb_top = thumb_top.max(track_clipped.top as i64 * 8);
264        let thumb_bottom = thumb_bottom.min(track_clipped.bottom as i64 * 8);
265
266        // Calculate the height of the top/bottom cell of the thumb.
267        let top_fract = (thumb_top % 8) as CoordType;
268        let bottom_fract = (thumb_bottom % 8) as CoordType;
269
270        // Shift to absolute coordinates.
271        let thumb_top = ((thumb_top + 7) / 8) as CoordType;
272        let thumb_bottom = (thumb_bottom / 8) as CoordType;
273
274        self.blend_bg(track_clipped, self.indexed(IndexedColor::BrightBlack));
275        self.blend_fg(track_clipped, self.indexed(IndexedColor::BrightWhite));
276
277        // Draw the full blocks.
278        for y in thumb_top..thumb_bottom {
279            self.replace_text(y, track_clipped.left, track_clipped.right, "█");
280        }
281
282        // Draw the top/bottom cell of the thumb.
283        // U+2581 to U+2588, 1/8th block to 8/8th block elements glyphs: ▁▂▃▄▅▆▇█
284        // In UTF8: E2 96 81 to E2 96 88
285        let mut fract_buf = [0xE2, 0x96, 0x88];
286        if top_fract != 0 {
287            fract_buf[2] = (0x88 - top_fract) as u8;
288            self.replace_text(thumb_top - 1, track_clipped.left, track_clipped.right, unsafe {
289                std::str::from_utf8_unchecked(&fract_buf)
290            });
291        }
292        if bottom_fract != 0 {
293            fract_buf[2] = (0x88 - bottom_fract) as u8;
294            self.replace_text(thumb_bottom, track_clipped.left, track_clipped.right, unsafe {
295                std::str::from_utf8_unchecked(&fract_buf)
296            });
297            let rect = Rect {
298                left: track_clipped.left,
299                top: thumb_bottom,
300                right: track_clipped.right,
301                bottom: thumb_bottom + 1,
302            };
303            self.blend_bg(rect, self.indexed(IndexedColor::BrightWhite));
304            self.blend_fg(rect, self.indexed(IndexedColor::BrightBlack));
305        }
306
307        ((thumb_height + 4) / 8) as CoordType
308    }
309
310    #[inline]
311    pub fn indexed(&self, index: IndexedColor) -> u32 {
312        self.indexed_colors[index as usize]
313    }
314
315    /// Returns a color from the palette.
316    ///
317    /// To facilitate constant folding by the compiler,
318    /// alpha is given as a fraction (`numerator` / `denominator`).
319    #[inline]
320    pub fn indexed_alpha(&self, index: IndexedColor, numerator: u32, denominator: u32) -> u32 {
321        let c = self.indexed_colors[index as usize];
322        let a = 255 * numerator / denominator;
323        let r = (((c >> 16) & 0xFF) * numerator) / denominator;
324        let g = (((c >> 8) & 0xFF) * numerator) / denominator;
325        let b = ((c & 0xFF) * numerator) / denominator;
326        a << 24 | r << 16 | g << 8 | b
327    }
328
329    /// Returns a color opposite to the brightness of the given `color`.
330    pub fn contrasted(&self, color: u32) -> u32 {
331        let idx = (color as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
332        let slot = self.contrast_colors[idx].get();
333        if slot.0 == color { slot.1 } else { self.contrasted_slow(color) }
334    }
335
336    #[cold]
337    fn contrasted_slow(&self, color: u32) -> u32 {
338        let idx = (color as usize).wrapping_mul(HASH_MULTIPLIER) >> CACHE_TABLE_SHIFT;
339        let contrast = self.auto_colors[Self::is_dark(color) as usize];
340        self.contrast_colors[idx].set((color, contrast));
341        contrast
342    }
343
344    fn is_dark(color: u32) -> bool {
345        srgb_to_oklab(color).l < 0.5
346    }
347
348    /// Blends the given sRGB color onto the background bitmap.
349    ///
350    /// TODO: The current approach blends foreground/background independently,
351    /// but ideally `blend_bg` with semi-transparent dark should also darken text below it.
352    pub fn blend_bg(&mut self, target: Rect, bg: u32) {
353        let back = &mut self.buffers[self.frame_counter & 1];
354        back.bg_bitmap.blend(target, bg);
355    }
356
357    /// Blends the given sRGB color onto the foreground bitmap.
358    ///
359    /// TODO: The current approach blends foreground/background independently,
360    /// but ideally `blend_fg` should blend with the background color below it.
361    pub fn blend_fg(&mut self, target: Rect, fg: u32) {
362        let back = &mut self.buffers[self.frame_counter & 1];
363        back.fg_bitmap.blend(target, fg);
364    }
365
366    /// Reverses the foreground and background colors in the given rectangle.
367    pub fn reverse(&mut self, target: Rect) {
368        let back = &mut self.buffers[self.frame_counter & 1];
369
370        let target = target.intersect(back.bg_bitmap.size.as_rect());
371        if target.is_empty() {
372            return;
373        }
374
375        let top = target.top as usize;
376        let bottom = target.bottom as usize;
377        let left = target.left as usize;
378        let right = target.right as usize;
379        let stride = back.bg_bitmap.size.width as usize;
380
381        for y in top..bottom {
382            let beg = y * stride + left;
383            let end = y * stride + right;
384            let bg = &mut back.bg_bitmap.data[beg..end];
385            let fg = &mut back.fg_bitmap.data[beg..end];
386            bg.swap_with_slice(fg);
387        }
388    }
389
390    /// Replaces VT attributes in the given rectangle.
391    pub fn replace_attr(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
392        let back = &mut self.buffers[self.frame_counter & 1];
393        back.attributes.replace(target, mask, attr);
394    }
395
396    /// Sets the current visible cursor position and type.
397    ///
398    /// Call this when focus is inside an editable area and you want to show the cursor.
399    pub fn set_cursor(&mut self, pos: Point, overtype: bool) {
400        let back = &mut self.buffers[self.frame_counter & 1];
401        back.cursor.pos = pos;
402        back.cursor.overtype = overtype;
403    }
404
405    /// Renders the framebuffer contents accumulated since the
406    /// last call to `flip()` and returns them serialized as VT.
407    pub fn render<'a>(&mut self, arena: &'a Arena) -> ArenaString<'a> {
408        let idx = self.frame_counter & 1;
409        // Borrows the front/back buffers without letting Rust know that we have a reference to self.
410        // SAFETY: Well this is certainly correct, but whether Rust and its strict rules likes it is another question.
411        let (back, front) = unsafe {
412            let ptr = self.buffers.as_mut_ptr();
413            let back = &mut *ptr.add(idx);
414            let front = &*ptr.add(1 - idx);
415            (back, front)
416        };
417
418        let mut front_lines = front.text.lines.iter(); // hahaha
419        let mut front_bgs = front.bg_bitmap.iter();
420        let mut front_fgs = front.fg_bitmap.iter();
421        let mut front_attrs = front.attributes.iter();
422
423        let mut back_lines = back.text.lines.iter();
424        let mut back_bgs = back.bg_bitmap.iter();
425        let mut back_fgs = back.fg_bitmap.iter();
426        let mut back_attrs = back.attributes.iter();
427
428        let mut result = ArenaString::new_in(arena);
429        let mut last_bg = u64::MAX;
430        let mut last_fg = u64::MAX;
431        let mut last_attr = Attributes::None;
432
433        for y in 0..front.text.size.height {
434            // SAFETY: The only thing that changes the size of these containers,
435            // is the reset() method and it always resets front/back to the same size.
436            let front_line = unsafe { front_lines.next().unwrap_unchecked() };
437            let front_bg = unsafe { front_bgs.next().unwrap_unchecked() };
438            let front_fg = unsafe { front_fgs.next().unwrap_unchecked() };
439            let front_attr = unsafe { front_attrs.next().unwrap_unchecked() };
440
441            let back_line = unsafe { back_lines.next().unwrap_unchecked() };
442            let back_bg = unsafe { back_bgs.next().unwrap_unchecked() };
443            let back_fg = unsafe { back_fgs.next().unwrap_unchecked() };
444            let back_attr = unsafe { back_attrs.next().unwrap_unchecked() };
445
446            // TODO: Ideally, we should properly diff the contents and so if
447            // only parts of a line change, we should only update those parts.
448            if front_line == back_line
449                && front_bg == back_bg
450                && front_fg == back_fg
451                && front_attr == back_attr
452            {
453                continue;
454            }
455
456            let line_bytes = back_line.as_bytes();
457            let mut cfg = MeasurementConfig::new(&line_bytes);
458            let mut chunk_end = 0;
459
460            if result.is_empty() {
461                result.push_str("\x1b[m");
462            }
463            _ = write!(result, "\x1b[{};1H", y + 1);
464
465            while {
466                let bg = back_bg[chunk_end];
467                let fg = back_fg[chunk_end];
468                let attr = back_attr[chunk_end];
469
470                // Chunk into runs of the same color.
471                while {
472                    chunk_end += 1;
473                    chunk_end < back_bg.len()
474                        && back_bg[chunk_end] == bg
475                        && back_fg[chunk_end] == fg
476                        && back_attr[chunk_end] == attr
477                } {}
478
479                if last_bg != bg as u64 {
480                    last_bg = bg as u64;
481                    self.format_color(&mut result, false, bg);
482                }
483
484                if last_fg != fg as u64 {
485                    last_fg = fg as u64;
486                    self.format_color(&mut result, true, fg);
487                }
488
489                if last_attr != attr {
490                    let diff = last_attr ^ attr;
491                    if diff.is(Attributes::Italic) {
492                        if attr.is(Attributes::Italic) {
493                            result.push_str("\x1b[3m");
494                        } else {
495                            result.push_str("\x1b[23m");
496                        }
497                    }
498                    if diff.is(Attributes::Underlined) {
499                        if attr.is(Attributes::Underlined) {
500                            result.push_str("\x1b[4m");
501                        } else {
502                            result.push_str("\x1b[24m");
503                        }
504                    }
505                    last_attr = attr;
506                }
507
508                let beg = cfg.cursor().offset;
509                let end = cfg.goto_visual(Point { x: chunk_end as CoordType, y: 0 }).offset;
510                result.push_str(&back_line[beg..end]);
511
512                chunk_end < back_bg.len()
513            } {}
514        }
515
516        // If the cursor has changed since the last frame we naturally need to update it,
517        // but this also applies if the code above wrote to the screen,
518        // as it uses CUP sequences to reposition the cursor for writing.
519        if !result.is_empty() || back.cursor != front.cursor {
520            if back.cursor.pos.x >= 0 && back.cursor.pos.y >= 0 {
521                // CUP to the cursor position.
522                // DECSCUSR to set the cursor style.
523                // DECTCEM to show the cursor.
524                _ = write!(
525                    result,
526                    "\x1b[{};{}H\x1b[{} q\x1b[?25h",
527                    back.cursor.pos.y + 1,
528                    back.cursor.pos.x + 1,
529                    if back.cursor.overtype { 1 } else { 5 }
530                );
531            } else {
532                // DECTCEM to hide the cursor.
533                result.push_str("\x1b[?25l");
534            }
535        }
536
537        result
538    }
539
540    fn format_color(&self, dst: &mut ArenaString, fg: bool, mut color: u32) {
541        let typ = if fg { '3' } else { '4' };
542
543        // Some terminals support transparent backgrounds which are used
544        // if the default background color is active (CSI 49 m).
545        //
546        // If [`Framebuffer::set_indexed_colors`] was never called, we assume
547        // that the terminal doesn't support transparency and initialize the
548        // background bitmap with the `DEFAULT_THEME` default background color.
549        // Otherwise, we assume that the terminal supports transparency
550        // and initialize it with 0x00000000 (transparent).
551        //
552        // We also apply this to the foreground color, because it compresses
553        // the output slightly and ensures that we keep "default foreground"
554        // and "color that happens to be default foreground" separate.
555        // (This also applies to the background color by the way.)
556        if color == 0 {
557            _ = write!(dst, "\x1b[{typ}9m");
558            return;
559        }
560
561        if (color & 0xff000000) != 0xff000000 {
562            let idx = if fg { IndexedColor::Foreground } else { IndexedColor::Background };
563            let dst = self.indexed(idx);
564            color = oklab_blend(dst, color);
565        }
566
567        let r = color & 0xff;
568        let g = (color >> 8) & 0xff;
569        let b = (color >> 16) & 0xff;
570        _ = write!(dst, "\x1b[{typ}8;2;{r};{g};{b}m");
571    }
572}
573
574#[derive(Default)]
575struct Buffer {
576    text: LineBuffer,
577    bg_bitmap: Bitmap,
578    fg_bitmap: Bitmap,
579    attributes: AttributeBuffer,
580    cursor: Cursor,
581}
582
583/// A buffer for the text contents of the framebuffer.
584#[derive(Default)]
585struct LineBuffer {
586    lines: Vec<String>,
587    size: Size,
588}
589
590impl LineBuffer {
591    fn new(size: Size) -> Self {
592        Self { lines: vec![String::new(); size.height as usize], size }
593    }
594
595    fn fill_whitespace(&mut self) {
596        let width = self.size.width as usize;
597        for l in &mut self.lines {
598            l.clear();
599            l.reserve(width + width / 2);
600
601            let buf = unsafe { l.as_mut_vec() };
602            // Compiles down to `memset()`.
603            buf.extend(std::iter::repeat_n(b' ', width));
604        }
605    }
606
607    /// Replaces text contents in a single line of the framebuffer.
608    /// All coordinates are in viewport coordinates.
609    /// Assumes that control characters have been replaced or escaped.
610    fn replace_text(
611        &mut self,
612        y: CoordType,
613        origin_x: CoordType,
614        clip_right: CoordType,
615        text: &str,
616    ) {
617        let Some(line) = self.lines.get_mut(y as usize) else {
618            return;
619        };
620
621        let bytes = text.as_bytes();
622        let clip_right = clip_right.clamp(0, self.size.width);
623        let layout_width = clip_right - origin_x;
624
625        // Can't insert text that can't fit or is empty.
626        if layout_width <= 0 || bytes.is_empty() {
627            return;
628        }
629
630        let mut cfg = MeasurementConfig::new(&bytes);
631
632        // Check if the text intersects with the left edge of the framebuffer
633        // and figure out the parts that are inside.
634        let mut left = origin_x;
635        if left < 0 {
636            let mut cursor = cfg.goto_visual(Point { x: -left, y: 0 });
637
638            if left + cursor.visual_pos.x < 0 && cursor.offset < text.len() {
639                // `-left` must've intersected a wide glyph and since goto_visual stops _before_ reaching the target,
640                // we stopped before the wide glyph and thus must step forward to the next glyph.
641                cursor = cfg.goto_logical(Point { x: cursor.logical_pos.x + 1, y: 0 });
642            }
643
644            left += cursor.visual_pos.x;
645        }
646
647        // If the text still starts outside the framebuffer, we must've ran out of text above.
648        // Otherwise, if it starts outside the right edge to begin with, we can't insert it anyway.
649        if left < 0 || left >= clip_right {
650            return;
651        }
652
653        // Measure the width of the new text (= `res_new.visual_target.x`).
654        let beg_off = cfg.cursor().offset;
655        let end = cfg.goto_visual(Point { x: layout_width, y: 0 });
656
657        // Figure out at which byte offset the new text gets inserted.
658        let right = left + end.visual_pos.x;
659        let line_bytes = line.as_bytes();
660        let mut cfg_old = MeasurementConfig::new(&line_bytes);
661        let res_old_beg = cfg_old.goto_visual(Point { x: left, y: 0 });
662        let mut res_old_end = cfg_old.goto_visual(Point { x: right, y: 0 });
663
664        // Since the goto functions will always stop short of the target position,
665        // we need to manually step beyond it if we intersect with a wide glyph.
666        if res_old_end.visual_pos.x < right {
667            res_old_end = cfg_old.goto_logical(Point { x: res_old_end.logical_pos.x + 1, y: 0 });
668        }
669
670        // If we intersect a wide glyph, we need to pad the new text with spaces.
671        let src = &text[beg_off..end.offset];
672        let overlap_beg = (left - res_old_beg.visual_pos.x).max(0) as usize;
673        let overlap_end = (res_old_end.visual_pos.x - right).max(0) as usize;
674        let total_add = src.len() + overlap_beg + overlap_end;
675        let total_del = res_old_end.offset - res_old_beg.offset;
676
677        // This is basically a hand-written version of `Vec::splice()`,
678        // but for strings under the assumption that all inputs are valid.
679        // It also takes care of `overlap_beg` and `overlap_end` by inserting spaces.
680        unsafe {
681            // SAFETY: Our ucd code only returns valid UTF-8 offsets.
682            // If it didn't that'd be a priority -9000 bug for any text editor.
683            // And apart from that, all inputs are &str (= UTF8).
684            let dst = line.as_mut_vec();
685
686            let dst_len = dst.len();
687            let src_len = src.len();
688
689            // Make room for the new elements. NOTE that this must be done before
690            // we call as_mut_ptr, or else we risk accessing a stale pointer.
691            // We only need to reserve as much as the string actually grows by.
692            dst.reserve(total_add.saturating_sub(total_del));
693
694            // Move the pointer to the start of the insert.
695            let mut ptr = dst.as_mut_ptr().add(res_old_beg.offset);
696
697            // Move the tail end of the string by `total_add - total_del`-many bytes.
698            // This both effectively deletes the old text and makes room for the new text.
699            if total_add != total_del {
700                // Move the tail of the vector to make room for the new elements.
701                ptr::copy(
702                    ptr.add(total_del),
703                    ptr.add(total_add),
704                    dst_len - total_del - res_old_beg.offset,
705                );
706            }
707
708            // Pad left.
709            for _ in 0..overlap_beg {
710                ptr.write(b' ');
711                ptr = ptr.add(1);
712            }
713
714            // Copy the new elements into the vector.
715            ptr::copy_nonoverlapping(src.as_ptr(), ptr, src_len);
716            ptr = ptr.add(src_len);
717
718            // Pad right.
719            for _ in 0..overlap_end {
720                ptr.write(b' ');
721                ptr = ptr.add(1);
722            }
723
724            // Update the length of the vector.
725            dst.set_len(dst_len - total_del + total_add);
726        }
727    }
728}
729
730/// An sRGB bitmap.
731#[derive(Default)]
732struct Bitmap {
733    data: Vec<u32>,
734    size: Size,
735}
736
737impl Bitmap {
738    fn new(size: Size) -> Self {
739        Self { data: vec![0; (size.width * size.height) as usize], size }
740    }
741
742    fn fill(&mut self, color: u32) {
743        memset(&mut self.data, color);
744    }
745
746    /// Blends the given sRGB color onto the bitmap.
747    ///
748    /// This uses the `oklab` color space for blending so the
749    /// resulting colors may look different from what you'd expect.
750    fn blend(&mut self, target: Rect, color: u32) {
751        if (color & 0xff000000) == 0x00000000 {
752            return;
753        }
754
755        let target = target.intersect(self.size.as_rect());
756        if target.is_empty() {
757            return;
758        }
759
760        let top = target.top as usize;
761        let bottom = target.bottom as usize;
762        let left = target.left as usize;
763        let right = target.right as usize;
764        let stride = self.size.width as usize;
765
766        for y in top..bottom {
767            let beg = y * stride + left;
768            let end = y * stride + right;
769            let data = &mut self.data[beg..end];
770
771            if (color & 0xff000000) == 0xff000000 {
772                memset(data, color);
773            } else {
774                let end = data.len();
775                let mut off = 0;
776
777                while {
778                    let c = data[off];
779
780                    // Chunk into runs of the same color, so that we only call alpha_blend once per run.
781                    let chunk_beg = off;
782                    while {
783                        off += 1;
784                        off < end && data[off] == c
785                    } {}
786                    let chunk_end = off;
787
788                    let c = oklab_blend(c, color);
789                    memset(&mut data[chunk_beg..chunk_end], c);
790
791                    off < end
792                } {}
793            }
794        }
795    }
796
797    /// Iterates over each row in the bitmap.
798    fn iter(&self) -> ChunksExact<'_, u32> {
799        self.data.chunks_exact(self.size.width as usize)
800    }
801}
802
803/// A bitfield for VT text attributes.
804///
805/// It being a bitfield allows for simple diffing.
806#[repr(transparent)]
807#[derive(Default, Clone, Copy, PartialEq, Eq)]
808pub struct Attributes(u8);
809
810#[allow(non_upper_case_globals)]
811impl Attributes {
812    pub const None: Self = Self(0);
813    pub const Italic: Self = Self(0b1);
814    pub const Underlined: Self = Self(0b10);
815    pub const All: Self = Self(0b11);
816
817    pub const fn is(self, attr: Self) -> bool {
818        (self.0 & attr.0) == attr.0
819    }
820}
821
822unsafe impl MemsetSafe for Attributes {}
823
824impl BitOr for Attributes {
825    type Output = Self;
826
827    fn bitor(self, rhs: Self) -> Self::Output {
828        Self(self.0 | rhs.0)
829    }
830}
831
832impl BitXor for Attributes {
833    type Output = Self;
834
835    fn bitxor(self, rhs: Self) -> Self::Output {
836        Self(self.0 ^ rhs.0)
837    }
838}
839
840/// Stores VT attributes for the framebuffer.
841#[derive(Default)]
842struct AttributeBuffer {
843    data: Vec<Attributes>,
844    size: Size,
845}
846
847impl AttributeBuffer {
848    fn new(size: Size) -> Self {
849        Self { data: vec![Default::default(); (size.width * size.height) as usize], size }
850    }
851
852    fn reset(&mut self) {
853        memset(&mut self.data, Default::default());
854    }
855
856    fn replace(&mut self, target: Rect, mask: Attributes, attr: Attributes) {
857        let target = target.intersect(self.size.as_rect());
858        if target.is_empty() {
859            return;
860        }
861
862        let top = target.top as usize;
863        let bottom = target.bottom as usize;
864        let left = target.left as usize;
865        let right = target.right as usize;
866        let stride = self.size.width as usize;
867
868        for y in top..bottom {
869            let beg = y * stride + left;
870            let end = y * stride + right;
871            let dst = &mut self.data[beg..end];
872
873            if mask == Attributes::All {
874                memset(dst, attr);
875            } else {
876                for a in dst {
877                    *a = Attributes(a.0 & !mask.0 | attr.0);
878                }
879            }
880        }
881    }
882
883    /// Iterates over each row in the bitmap.
884    fn iter(&self) -> ChunksExact<'_, Attributes> {
885        self.data.chunks_exact(self.size.width as usize)
886    }
887}
888
889/// Stores cursor position and type for the framebuffer.
890#[derive(Default, PartialEq, Eq)]
891struct Cursor {
892    pos: Point,
893    overtype: bool,
894}
895
896impl Cursor {
897    const fn new_invalid() -> Self {
898        Self { pos: Point::MIN, overtype: false }
899    }
900
901    const fn new_disabled() -> Self {
902        Self { pos: Point { x: -1, y: -1 }, overtype: false }
903    }
904}