Skip to main content

strat9_kernel/arch/x86_64/vga/
writer.rs

1use super::{
2    api::current_ui_scale,
3    cursor::{CursorManager, CURSOR_H, CURSOR_PIXELS, CURSOR_W, TEXT_CURSOR_MAX_DIM},
4    font::{parse_psf, parse_psf2_unicode_map, FontInfo, FONT_PSF},
5    scrollback::{SbCell, ScrollbackBuffer, MAX_SCROLLBACK},
6    types::*,
7};
8use crate::framebuffer::{CanvasBuffer, DirtyRectSet, MAX_DIRTY_RECTS};
9use alloc::{string::String, vec, vec::Vec};
10use core::{
11    fmt,
12    sync::atomic::{AtomicBool, AtomicU64, Ordering},
13};
14
15pub(crate) static PRESENTED_FRAMES: AtomicU64 = AtomicU64::new(0);
16pub(crate) static DOUBLE_BUFFER_MODE: AtomicBool = AtomicBool::new(false);
17pub(crate) static VGA_PRESENT_REGION_COUNT: AtomicU64 = AtomicU64::new(0);
18pub(crate) static VGA_PRESENT_PIXEL_COUNT: AtomicU64 = AtomicU64::new(0);
19
20/// Core framebuffer terminal writer: owns the VGA text-mode state machine,
21/// glyph cache, scrollback ring-buffer, hardware/software cursors, and
22/// pixel-level rendering helpers.
23const SCROLLBAR_W: usize = 12;
24
25use crate::framebuffer::RgbColor;
26
27#[derive(Clone, Copy)]
28pub(crate) struct ClipRect {
29    x: usize,
30    y: usize,
31    w: usize,
32    h: usize,
33}
34
35// Mouse cursor (X arrow, 12×16) ==================================================================================================================================
36const PRESENT_MIN_TICKS: u64 = 1;
37
38// Selection clipboard ==========================================================================================================================================================================
39const CLIPBOARD_CAP: usize = 8192;
40static CLIPBOARD: spin::Mutex<([u8; CLIPBOARD_CAP], usize)> =
41    spin::Mutex::new(([0u8; CLIPBOARD_CAP], 0));
42
43pub struct VgaWriter {
44    pub(crate) enabled: bool,
45    /// Shared double-buffered framebuffer canvas (None before init).
46    pub(crate) canvas: Option<CanvasBuffer>,
47    /// Pixel format (pack_rgb/unpack_color helpers).
48    pub(crate) fmt: PixelFormat,
49
50    pub(crate) cols: usize,
51    pub(crate) rows: usize,
52    pub(crate) col: usize,
53    pub(crate) row: usize,
54
55    pub(crate) fg: u32,
56    pub(crate) bg: u32,
57
58    pub(crate) font: &'static [u8],
59    pub(crate) font_info: FontInfo,
60    pub(crate) glyph_mask_cache: Vec<u8>,
61    pub(crate) unicode_map: Vec<(u32, usize)>,
62    pub(crate) status_bar_height: usize,
63    pub(crate) clip: ClipRect,
64
65    /// Scrollback ring-buffer.
66    pub(crate) sb: ScrollbackBuffer,
67    pub(crate) scroll_offset: usize,
68
69    /// Mouse + text cursor manager.
70    pub(crate) cursor: CursorManager,
71
72    // Text selection
73    pub(crate) sel_active: bool,
74    pub(crate) sel_start_row: usize,
75    pub(crate) sel_start_col: usize,
76    pub(crate) sel_end_row: usize,
77    pub(crate) sel_end_col: usize,
78    pub(crate) prepared_row_cells: Vec<(usize, u32, u32)>,
79}
80
81unsafe impl Send for VgaWriter {}
82
83/// Short helpers to access the optional canvas without repeating unwrap.
84impl VgaWriter {
85    #[inline]
86    fn can(&self) -> &CanvasBuffer {
87        self.canvas
88            .as_ref()
89            .expect("VgaWriter canvas not initialized")
90    }
91
92    #[inline]
93    fn canm(&mut self) -> &mut CanvasBuffer {
94        self.canvas
95            .as_mut()
96            .expect("VgaWriter canvas not initialized")
97    }
98}
99
100impl VgaWriter {
101    /// Maximum capacity of the scrollback ring buffer.
102    pub(crate) fn sb_capacity(&self) -> usize {
103        self.sb.capacity(self.rows)
104    }
105
106    pub(crate) fn sb_row_at(&self, logical_idx: usize) -> Option<&Vec<SbCell>> {
107        self.sb.row_at(logical_idx)
108    }
109
110    pub(crate) fn sb_push_row(&mut self, row: Vec<SbCell>) {
111        let rows = self.rows;
112        self.sb.push_row(row, rows);
113    }
114
115    pub(crate) fn build_glyph_mask_cache(font: &'static [u8], info: &FontInfo) -> Vec<u8> {
116        let glyph_pixels = info.glyph_w.saturating_mul(info.glyph_h);
117        let total_pixels = info.glyph_count.saturating_mul(glyph_pixels);
118        let mut cache = Vec::with_capacity(total_pixels);
119        if glyph_pixels == 0 || info.glyph_count == 0 {
120            return cache;
121        }
122
123        let row_bytes = info.glyph_w.div_ceil(8);
124        for glyph_index in 0..info.glyph_count {
125            let start = info.data_offset + glyph_index * info.bytes_per_glyph;
126            let end = start.saturating_add(info.bytes_per_glyph);
127            if end > font.len() {
128                cache.resize(cache.len().saturating_add(glyph_pixels), 0);
129                continue;
130            }
131            let glyph_ptr = font[start..end].as_ptr();
132            for gy in 0..info.glyph_h {
133                for gx in 0..info.glyph_w {
134                    let byte = unsafe { *glyph_ptr.add(gy * row_bytes + gx / 8) };
135                    let mask = 0x80u8 >> (gx % 8);
136                    cache.push(if (byte & mask) != 0 { 1 } else { 0 });
137                }
138            }
139        }
140        cache
141    }
142
143    pub(crate) fn glyph_mask_slice(&self, glyph_index: usize) -> Option<&[u8]> {
144        let glyph_pixels = self
145            .font_info
146            .glyph_w
147            .saturating_mul(self.font_info.glyph_h);
148        if glyph_pixels == 0 {
149            return None;
150        }
151        let start = glyph_index.checked_mul(glyph_pixels)?;
152        let end = start.checked_add(glyph_pixels)?;
153        self.glyph_mask_cache.get(start..end)
154    }
155
156    /// Creates a new instance.
157    pub const fn new() -> Self {
158        Self {
159            enabled: false,
160            canvas: None,
161            fmt: PixelFormat {
162                bpp: 0,
163                red_size: 0,
164                red_shift: 0,
165                green_size: 0,
166                green_shift: 0,
167                blue_size: 0,
168                blue_shift: 0,
169            },
170            cols: 0,
171            rows: 0,
172            col: 0,
173            row: 0,
174            fg: 0,
175            bg: 0,
176            font: &[],
177            font_info: FontInfo {
178                glyph_count: 0,
179                bytes_per_glyph: 0,
180                glyph_w: 8,
181                glyph_h: 16,
182                data_offset: 0,
183                unicode_table_offset: None,
184            },
185            glyph_mask_cache: Vec::new(),
186            unicode_map: Vec::new(),
187            status_bar_height: 0,
188            clip: ClipRect {
189                x: 0,
190                y: 0,
191                w: 0,
192                h: 0,
193            },
194            sb: ScrollbackBuffer::new(),
195            scroll_offset: 0,
196            cursor: CursorManager::new(),
197            sel_active: false,
198            sel_start_row: 0,
199            sel_start_col: 0,
200            sel_end_row: 0,
201            sel_end_col: 0,
202            prepared_row_cells: Vec::new(),
203        }
204    }
205
206    /// Performs the configure operation.
207    pub(crate) fn configure(
208        &mut self,
209        fb_addr: *mut u8,
210        fb_width: usize,
211        fb_height: usize,
212        pitch: usize,
213        fmt: PixelFormat,
214    ) -> bool {
215        let Some(font_info) = parse_psf(FONT_PSF) else {
216            return false;
217        };
218        let status_bar_height = font_info.glyph_h;
219        let text_height = fb_height.saturating_sub(status_bar_height);
220        let cols = fb_width / font_info.glyph_w;
221        let rows = text_height / font_info.glyph_h;
222        if cols == 0 || rows == 0 {
223            return false;
224        }
225        let (fr, fg, fb) = color_to_rgb(Color::LightGrey);
226        let (br, bg, bb) = color_to_rgb(Color::Black);
227
228        self.enabled = true;
229        self.canvas = Some(CanvasBuffer {
230            addr: fb_addr,
231            width: fb_width,
232            height: fb_height,
233            pitch,
234            bpp: fmt.bpp,
235            back_buffer: None,
236            draw_to_back: false,
237            dirty: DirtyRectSet::empty(),
238            track_dirty: false,
239            present_pending: false,
240            last_present_tick: 0,
241            ops: crate::framebuffer::FramebufferOps::detect(),
242        });
243        self.fmt = fmt;
244        self.cols = cols;
245        self.rows = rows;
246        self.col = 0;
247        self.row = 0;
248        self.fg = fmt.pack_rgb(fr, fg, fb);
249        self.bg = fmt.pack_rgb(br, bg, bb);
250        self.font = FONT_PSF;
251        self.font_info = font_info;
252        self.glyph_mask_cache = Self::build_glyph_mask_cache(FONT_PSF, &self.font_info);
253        self.unicode_map = parse_psf2_unicode_map(FONT_PSF, &self.font_info);
254        self.status_bar_height = status_bar_height;
255        self.clip = ClipRect {
256            x: 0,
257            y: 0,
258            w: fb_width,
259            h: fb_height,
260        };
261        self.sb = ScrollbackBuffer::new();
262        self.scroll_offset = 0;
263        self.cursor = CursorManager::new();
264        self.cursor.tc_visible = false;
265        self.cursor.tc_col = 0;
266        self.cursor.tc_row = 0;
267        self.cursor.tc_w = 0;
268        self.cursor.tc_h = 0;
269        self.cursor.tc_color = 0;
270        self.sel_active = false;
271        self.prepared_row_cells = Vec::new();
272        true
273    }
274
275    /// Performs the pack color operation.
276    #[inline]
277    pub(crate) fn pack_color(&self, color: RgbColor) -> u32 {
278        self.fmt.pack_rgb(color.r, color.g, color.b)
279    }
280
281    /// Performs the unpack color operation.
282    pub(crate) fn unpack_color(&self, value: u32) -> RgbColor {
283        /// Performs the unscale operation.
284        fn unscale(v: u32, bits: u8) -> u8 {
285            if bits == 0 {
286                return 0;
287            }
288            let max = (1u32 << bits) - 1;
289            ((v * 255) / max) as u8
290        }
291
292        let r = unscale(
293            (value >> self.fmt.red_shift) & ((1u32 << self.fmt.red_size) - 1),
294            self.fmt.red_size,
295        );
296        let g = unscale(
297            (value >> self.fmt.green_shift) & ((1u32 << self.fmt.green_size) - 1),
298            self.fmt.green_size,
299        );
300        let b = unscale(
301            (value >> self.fmt.blue_shift) & ((1u32 << self.fmt.blue_size) - 1),
302            self.fmt.blue_size,
303        );
304        RgbColor::new(r, g, b)
305    }
306
307    /// Sets color.
308    pub fn set_color(&mut self, fg: Color, bg: Color) {
309        self.set_rgb_color(fg.into(), bg.into());
310    }
311
312    /// Sets rgb color.
313    pub fn set_rgb_color(&mut self, fg: RgbColor, bg: RgbColor) {
314        self.fg = self.pack_color(fg);
315        self.bg = self.pack_color(bg);
316    }
317
318    /// Performs the text colors operation.
319    pub fn text_colors(&self) -> (RgbColor, RgbColor) {
320        (self.unpack_color(self.fg), self.unpack_color(self.bg))
321    }
322
323    /// Performs the width operation.
324    pub fn width(&self) -> usize {
325        self.can().width
326    }
327
328    /// Performs the height operation.
329    pub fn height(&self) -> usize {
330        self.can().height
331    }
332
333    /// Performs the cols operation.
334    pub fn cols(&self) -> usize {
335        self.cols
336    }
337
338    /// Performs the rows operation.
339    pub fn rows(&self) -> usize {
340        self.rows
341    }
342
343    /// Performs the glyph size operation.
344    pub fn glyph_size(&self) -> (usize, usize) {
345        (self.font_info.glyph_w, self.font_info.glyph_h)
346    }
347
348    /// Sets cursor cell.
349    pub fn set_cursor_cell(&mut self, col: usize, row: usize) {
350        if !self.enabled || self.cols == 0 || self.rows == 0 {
351            return;
352        }
353        if self.cursor.tc_visible {
354            self.text_cursor_erase_hw();
355            self.cursor.tc_visible = false;
356        }
357        self.col = core::cmp::min(col, self.cols - 1);
358        self.row = core::cmp::min(row, self.rows - 1);
359    }
360
361    /// Performs the text area height operation.
362    pub(crate) fn text_area_height(&self) -> usize {
363        self.can().height.saturating_sub(self.status_bar_height)
364    }
365
366    /// Performs the enabled operation.
367    pub fn enabled(&self) -> bool {
368        self.enabled
369    }
370
371    /// Performs the framebuffer info operation.
372    pub fn framebuffer_info(&self) -> FramebufferInfo {
373        FramebufferInfo {
374            available: self.enabled,
375            width: self.can().width,
376            height: self.can().height,
377            pitch: self.can().pitch,
378            bpp: self.fmt.bpp,
379            red_size: self.fmt.red_size,
380            red_shift: self.fmt.red_shift,
381            green_size: self.fmt.green_size,
382            green_shift: self.fmt.green_shift,
383            blue_size: self.fmt.blue_size,
384            blue_shift: self.fmt.blue_shift,
385            text_cols: self.cols,
386            text_rows: self.rows,
387            glyph_w: self.font_info.glyph_w,
388            glyph_h: self.font_info.glyph_h,
389            double_buffer_mode: DOUBLE_BUFFER_MODE.load(Ordering::Relaxed),
390            double_buffer_enabled: self.can().draw_to_back && self.can().back_buffer.is_some(),
391            ui_scale: current_ui_scale(),
392        }
393    }
394
395    /// Performs the in clip operation.
396    #[inline]
397    pub(crate) fn in_clip(&self, x: usize, y: usize) -> bool {
398        x >= self.clip.x
399            && y >= self.clip.y
400            && x < self.clip.x.saturating_add(self.clip.w)
401            && y < self.clip.y.saturating_add(self.clip.h)
402    }
403
404    /// Performs the clipped rect operation.
405    pub(crate) fn clipped_rect(
406        &self,
407        x: usize,
408        y: usize,
409        width: usize,
410        height: usize,
411    ) -> Option<(usize, usize, usize, usize)> {
412        if width == 0 || height == 0 || !self.enabled {
413            return None;
414        }
415        let src_x2 = core::cmp::min(x.saturating_add(width), self.can().width);
416        let src_y2 = core::cmp::min(y.saturating_add(height), self.can().height);
417        let clip_x2 = self.clip.x.saturating_add(self.clip.w);
418        let clip_y2 = self.clip.y.saturating_add(self.clip.h);
419
420        let sx = core::cmp::max(x, self.clip.x);
421        let sy = core::cmp::max(y, self.clip.y);
422        let ex = core::cmp::min(src_x2, clip_x2);
423        let ey = core::cmp::min(src_y2, clip_y2);
424        if ex <= sx || ey <= sy {
425            return None;
426        }
427        Some((sx, sy, ex - sx, ey - sy))
428    }
429
430    /// Performs the clear dirty operation.
431    pub(crate) fn clear_dirty(&mut self) {
432        self.canm().dirty.clear();
433    }
434
435    /// Performs the mark dirty rect operation.
436    pub(crate) fn mark_dirty_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
437        let canvas = self.can();
438        if !canvas.track_dirty || width == 0 || height == 0 || !self.enabled {
439            return;
440        }
441        let fw = canvas.width;
442        let fh = canvas.height;
443        let src_x2 = core::cmp::min(x.saturating_add(width), fw);
444        let src_y2 = core::cmp::min(y.saturating_add(height), fh);
445        let clip_x2 = self.clip.x.saturating_add(self.clip.w);
446        let clip_y2 = self.clip.y.saturating_add(self.clip.h);
447        let sx = core::cmp::max(x, self.clip.x);
448        let sy = core::cmp::max(y, self.clip.y);
449        let ex = core::cmp::min(src_x2, clip_x2);
450        let ey = core::cmp::min(src_y2, clip_y2);
451        if ex <= sx || ey <= sy {
452            return;
453        }
454        drop(canvas);
455        self.canm()
456            .dirty
457            .include(sx as u32, sy as u32, (ex - sx) as u32, (ey - sy) as u32);
458    }
459
460    /// Sets clip rect.
461    pub fn set_clip_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
462        let x_end = core::cmp::min(x.saturating_add(width), self.can().width);
463        let y_end = core::cmp::min(y.saturating_add(height), self.can().height);
464        self.clip = ClipRect {
465            x,
466            y,
467            w: x_end.saturating_sub(x),
468            h: y_end.saturating_sub(y),
469        };
470    }
471
472    /// Performs the reset clip rect operation.
473    pub fn reset_clip_rect(&mut self) {
474        self.clip = ClipRect {
475            x: 0,
476            y: 0,
477            w: self.can().width,
478            h: self.can().height,
479        };
480    }
481
482    /// Performs the draw to back buffer operation.
483    pub(crate) fn draw_to_back_buffer(&self) -> bool {
484        self.can().draw_to_back && self.can().back_buffer.is_some()
485    }
486
487    /// Enables double buffer.
488    pub fn enable_double_buffer(&mut self) -> bool {
489        if !self.enabled {
490            return false;
491        }
492        if self.can().back_buffer.is_none() {
493            let mut buf = Vec::with_capacity(self.can().width.saturating_mul(self.can().height));
494            for y in 0..self.can().height {
495                for x in 0..self.can().width {
496                    buf.push(self.read_hw_pixel_packed(x, y));
497                }
498            }
499            self.canm().back_buffer = Some(buf);
500        }
501        self.canm().draw_to_back = true;
502        self.canm().track_dirty = true;
503        self.clear_dirty();
504        true
505    }
506
507    /// Disables double buffer.
508    pub fn disable_double_buffer(&mut self, present: bool) {
509        if present {
510            self.present();
511        }
512        self.canm().draw_to_back = false;
513        self.canm().track_dirty = false;
514        self.clear_dirty();
515    }
516
517    /// Performs the present operation.
518    pub fn present(&mut self) {
519        if !self.enabled {
520            return;
521        }
522
523        // Extract all info through can() first to avoid borrow conflicts
524        let bpp = self.fmt.bpp;
525        let fb_addr = self.can().addr;
526        let pitch = self.can().pitch;
527        let fb_width = self.can().width;
528        let track_dirty = self.can().track_dirty;
529
530        // Early return: nothing dirty to present
531        if track_dirty && self.can().dirty.len == 0 {
532            return;
533        }
534
535        // Copy dirty rects to local array (no borrow on self after this)
536        let region_count;
537        let mut regions = [ClipRect {
538            x: 0,
539            y: 0,
540            w: 0,
541            h: 0,
542        }; MAX_DIRTY_RECTS];
543        if track_dirty {
544            region_count = self.can().dirty.len;
545            for idx in 0..region_count {
546                let r = self.can().dirty.rects[idx];
547                if r.is_valid() {
548                    regions[idx] = ClipRect {
549                        x: r.x0 as usize,
550                        y: r.y0 as usize,
551                        w: r.width() as usize,
552                        h: r.height() as usize,
553                    };
554                }
555            }
556        } else {
557            region_count = 1;
558            regions[0] = ClipRect {
559                x: 0,
560                y: 0,
561                w: fb_width,
562                h: self.can().height,
563            };
564        }
565
566        // Scoped block: get raw ptr to back_buffer then release immutable borrow
567        let buf_ptr = {
568            let Some(buf) = self.can().back_buffer.as_ref() else {
569                return;
570            };
571            buf.as_ptr()
572        };
573
574        // Pixel copy loop using local vars only (no live borrow on self)
575        for region_idx in 0..region_count {
576            let region = regions[region_idx];
577            if region.w == 0 || region.h == 0 {
578                continue;
579            }
580
581            if bpp == 32 {
582                let row_bytes = region.w * 4;
583                for y in region.y..(region.y + region.h) {
584                    let src = unsafe { buf_ptr.add(y * fb_width + region.x) as *const u8 };
585                    let dst_off = y * pitch + region.x * 4;
586                    unsafe {
587                        core::ptr::copy_nonoverlapping(src, fb_addr.add(dst_off), row_bytes);
588                    }
589                }
590            } else {
591                // 24bpp: convert row-by-row to packed bytes, then bulk-copy.
592                let row_bytes = region.w * 3;
593                let mut row_buf = alloc::vec![0u8; row_bytes];
594                for y in region.y..(region.y + region.h) {
595                    let src_row = y * fb_width + region.x;
596                    for x in 0..region.w {
597                        let packed = unsafe { *buf_ptr.add(src_row + x) };
598                        let off = x * 3;
599                        row_buf[off] = packed as u8;
600                        row_buf[off + 1] = (packed >> 8) as u8;
601                        row_buf[off + 2] = (packed >> 16) as u8;
602                    }
603                    let dst_off = y * pitch + region.x * 3;
604                    unsafe {
605                        core::ptr::copy_nonoverlapping(
606                            row_buf.as_ptr(),
607                            fb_addr.add(dst_off),
608                            row_bytes,
609                        );
610                    }
611                }
612            }
613
614            VGA_PRESENT_REGION_COUNT.fetch_add(1, Ordering::Relaxed);
615            VGA_PRESENT_PIXEL_COUNT
616                .fetch_add(region.w.saturating_mul(region.h) as u64, Ordering::Relaxed);
617        }
618
619        PRESENTED_FRAMES.fetch_add(1, Ordering::Relaxed);
620        self.clear_dirty();
621        self.canm().present_pending = false;
622        self.canm().last_present_tick = crate::process::scheduler::ticks();
623
624        if crate::hardware::virtio::gpu::is_available() {
625            if let Some(gpu) = crate::hardware::virtio::gpu::get_gpu() {
626                gpu.flush_now();
627            }
628        }
629
630        if self.cursor.mc_visible {
631            self.mc_save_hw();
632            self.mc_draw_hw();
633        }
634        if self.cursor.tc_visible {
635            self.text_cursor_save_hw();
636            self.text_cursor_draw_hw();
637        }
638    }
639
640    pub(crate) fn request_present(&mut self) {
641        if !self.draw_to_back_buffer() {
642            return;
643        }
644        self.canm().present_pending = true;
645        self.present_if_due(false);
646    }
647
648    pub(crate) fn present_if_due(&mut self, force: bool) {
649        if !self.canm().present_pending || !self.draw_to_back_buffer() {
650            return;
651        }
652        let now = crate::process::scheduler::ticks();
653        if force || now.saturating_sub(self.canm().last_present_tick) >= PRESENT_MIN_TICKS {
654            self.present();
655        }
656    }
657
658    // Mouse cursor ====================
659
660    /// Performs the mc save hw operation.
661    /// Uses read_pixel_packed (back-buffer aware) instead of read_hw_pixel_packed.
662    pub(crate) fn mc_save_hw(&mut self) {
663        let x = self.cursor.mc_x;
664        let y = self.cursor.mc_y;
665        let fw = self.can().width;
666        let fh = self.can().height;
667        for cy in 0..CURSOR_H {
668            for cx in 0..CURSOR_W {
669                let px = x + cx as i32;
670                let py = y + cy as i32;
671                if px < 0 || py < 0 || px as usize >= fw || py as usize >= fh {
672                    self.cursor.mc_save[cy * CURSOR_W + cx] = 0;
673                    continue;
674                }
675                self.cursor.mc_save[cy * CURSOR_W + cx] =
676                    self.read_pixel_packed(px as usize, py as usize);
677            }
678        }
679    }
680
681    /// Performs the mc draw hw operation.
682    /// Uses put_pixel_raw (back-buffer aware) instead of write_hw_pixel_packed.
683    pub(crate) fn mc_draw_hw(&mut self) {
684        let x = self.cursor.mc_x;
685        let y = self.cursor.mc_y;
686        let black = self.pack_color(RgbColor::BLACK);
687        let white = self.pack_color(RgbColor::WHITE);
688        for cy in 0..CURSOR_H {
689            for cx in 0..CURSOR_W {
690                let p = CURSOR_PIXELS[cy * CURSOR_W + cx];
691                if p == 0 {
692                    continue;
693                }
694                let px = x + cx as i32;
695                let py = y + cy as i32;
696                if px < 0
697                    || py < 0
698                    || px as usize >= self.can().width
699                    || py as usize >= self.can().height
700                {
701                    continue;
702                }
703                let color = if p == 1 { black } else { white };
704                self.put_pixel_raw(px as usize, py as usize, color);
705            }
706        }
707    }
708
709    /// Performs the mc erase hw operation.
710    /// Uses put_pixel_raw (back-buffer aware) instead of write_hw_pixel_packed.
711    pub(crate) fn mc_erase_hw(&mut self) {
712        let x = self.cursor.mc_x;
713        let y = self.cursor.mc_y;
714        for cy in 0..CURSOR_H {
715            for cx in 0..CURSOR_W {
716                if CURSOR_PIXELS[cy * CURSOR_W + cx] == 0 {
717                    continue;
718                }
719                let px = x + cx as i32;
720                let py = y + cy as i32;
721                if px < 0
722                    || py < 0
723                    || px as usize >= self.can().width
724                    || py as usize >= self.can().height
725                {
726                    continue;
727                }
728                self.put_pixel_raw(
729                    px as usize,
730                    py as usize,
731                    self.cursor.mc_save[cy * CURSOR_W + cx],
732                );
733            }
734        }
735    }
736
737    /// Updates mouse cursor.
738    pub fn update_mouse_cursor(&mut self, x: i32, y: i32) {
739        if !self.enabled {
740            return;
741        }
742        if self.cursor.mc_visible && self.cursor.mc_x == x && self.cursor.mc_y == y {
743            self.present_if_due(false);
744            return;
745        }
746        if self.cursor.mc_visible {
747            self.mc_erase_hw();
748        }
749        self.cursor.mc_x = x;
750        self.cursor.mc_y = y;
751        self.mc_save_hw();
752        self.mc_draw_hw();
753        self.cursor.mc_visible = true;
754        self.present_if_due(false);
755    }
756
757    /// Performs the hide mouse cursor operation.
758    pub fn hide_mouse_cursor(&mut self) {
759        if self.cursor.mc_visible {
760            self.mc_erase_hw();
761            self.cursor.mc_visible = false;
762        }
763        self.present_if_due(false);
764    }
765
766    pub(crate) fn text_cursor_rect(&self) -> Option<(usize, usize, usize, usize)> {
767        if !self.enabled {
768            return None;
769        }
770        let (gw, gh) = self.glyph_size();
771        if gw == 0 || gh == 0 {
772            return None;
773        }
774        let x = self.cursor.tc_col.saturating_mul(gw);
775        let y = self.cursor.tc_row.saturating_mul(gh);
776        if x >= self.can().width || y >= self.can().height {
777            return None;
778        }
779        Some((
780            x,
781            y,
782            gw.min(TEXT_CURSOR_MAX_DIM).min(self.can().width - x),
783            gh.min(TEXT_CURSOR_MAX_DIM).min(self.can().height - y),
784        ))
785    }
786
787    /// Save text cursor pixels. Uses read_pixel_packed (back-buffer aware).
788    pub(crate) fn text_cursor_save_hw(&mut self) {
789        let Some((x, y, w, h)) = self.text_cursor_rect() else {
790            self.cursor.tc_w = 0;
791            self.cursor.tc_h = 0;
792            return;
793        };
794        self.cursor.tc_w = w;
795        self.cursor.tc_h = h;
796        for cy in 0..h {
797            for cx in 0..w {
798                self.cursor.tc_save[cy * TEXT_CURSOR_MAX_DIM + cx] =
799                    self.read_pixel_packed(x + cx, y + cy);
800            }
801        }
802    }
803
804    /// Draw text cursor. Uses put_pixel_raw (back-buffer aware).
805    pub(crate) fn text_cursor_draw_hw(&mut self) {
806        let Some((x, y, w, h)) = self.text_cursor_rect() else {
807            return;
808        };
809        for cy in 0..h {
810            for cx in 0..w {
811                self.put_pixel_raw(x + cx, y + cy, self.cursor.tc_color);
812            }
813        }
814    }
815
816    /// Erase text cursor (restore saved pixels). Uses put_pixel_raw (back-buffer aware).
817    pub(crate) fn text_cursor_erase_hw(&mut self) {
818        let Some((x, y, w, h)) = self.text_cursor_rect() else {
819            return;
820        };
821        let restore_w = w.min(self.cursor.tc_w);
822        let restore_h = h.min(self.cursor.tc_h);
823        for cy in 0..restore_h {
824            for cx in 0..restore_w {
825                self.put_pixel_raw(
826                    x + cx,
827                    y + cy,
828                    self.cursor.tc_save[cy * TEXT_CURSOR_MAX_DIM + cx],
829                );
830            }
831        }
832    }
833
834    pub(crate) fn draw_text_cursor_overlay(&mut self, color: RgbColor) {
835        if !self.enabled {
836            return;
837        }
838        let packed = self.pack_color(color);
839        if self.cursor.tc_visible {
840            self.text_cursor_erase_hw();
841            self.cursor.tc_visible = false;
842        }
843        if packed == self.bg {
844            self.present();
845            return;
846        }
847        self.cursor.tc_col = self.col;
848        self.cursor.tc_row = self.row;
849        self.cursor.tc_color = packed;
850        self.text_cursor_save_hw();
851        self.text_cursor_draw_hw();
852        self.cursor.tc_visible = true;
853        // Present immediately so the cursor is always visible after this call.
854        // Using present() instead of present_if_due() avoids the race where
855        // vgabuf_flush_to_framebuffer presents between draw and next blink.
856        self.present();
857    }
858
859    /// Performs the hide text cursor operation on the writer.
860    pub fn hide_text_cursor(&mut self) {
861        if self.cursor.tc_visible {
862            self.text_cursor_erase_hw();
863            self.cursor.tc_visible = false;
864            self.present();
865        }
866    }
867
868    //  Text selection ==========
869
870    /// Performs the sel normalized operation.
871    pub(crate) fn sel_normalized(&self) -> (usize, usize, usize, usize) {
872        let (sr, sc, er, ec) = (
873            self.sel_start_row,
874            self.sel_start_col,
875            self.sel_end_row,
876            self.sel_end_col,
877        );
878        if sr < er || (sr == er && sc <= ec) {
879            (sr, sc, er, ec)
880        } else {
881            (er, ec, sr, sc)
882        }
883    }
884
885    /// Performs the pixel to sb pos operation.
886    pub fn pixel_to_sb_pos(&self, px: usize, py: usize) -> Option<(usize, usize)> {
887        if !self.enabled {
888            return None;
889        }
890        let gw = self.font_info.glyph_w;
891        let gh = self.font_info.glyph_h;
892        if gw == 0 || gh == 0 {
893            return None;
894        }
895        let text_h = self.text_area_height();
896        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
897        if px >= text_w || py >= text_h {
898            return None;
899        }
900        let vis_row = py / gh;
901        let vis_col = px / gw;
902        if vis_col >= self.cols {
903            return None;
904        }
905        let total_complete = self.sb.rows.len();
906        let has_partial = !self.sb.cur_row.is_empty();
907        let total_virtual = total_complete + if has_partial { 1 } else { 0 };
908        if total_virtual == 0 {
909            return None;
910        }
911        let view_end = total_virtual.saturating_sub(self.scroll_offset);
912        let view_start = view_end.saturating_sub(self.rows);
913        let display_len = view_end.saturating_sub(view_start);
914        if vis_row >= display_len {
915            return None;
916        }
917        Some((view_start + vis_row, vis_col))
918    }
919
920    /// Starts selection.
921    pub fn start_selection(&mut self, px: usize, py: usize) {
922        if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
923            self.sel_start_row = row;
924            self.sel_start_col = col;
925            self.sel_end_row = row;
926            self.sel_end_col = col;
927            self.sel_active = true;
928            self.render_viewport_full();
929        }
930    }
931
932    /// Updates selection.
933    pub fn update_selection(&mut self, px: usize, py: usize) {
934        if !self.sel_active {
935            return;
936        }
937        if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
938            if row == self.sel_end_row && col == self.sel_end_col {
939                return;
940            }
941            self.sel_end_row = row;
942            self.sel_end_col = col;
943            self.render_viewport_full();
944        }
945    }
946
947    /// Performs the end selection operation.
948    pub fn end_selection(&mut self) {
949        if !self.sel_active {
950            return;
951        }
952        let (start_row, start_col, end_row, end_col) = self.sel_normalized();
953        let mut bytes: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
954        for row in start_row..=end_row {
955            let len = if row < self.sb.rows.len() {
956                self.sb_row_at(row).map(|r| r.len()).unwrap_or(0)
957            } else if row == self.sb.rows.len() {
958                self.sb.cur_row.len()
959            } else {
960                break;
961            };
962            let c0 = if row == start_row {
963                start_col.min(len)
964            } else {
965                0
966            };
967            let c1 = if row == end_row {
968                end_col.min(len)
969            } else {
970                len
971            };
972            for col in c0..c1 {
973                let ch = if row < self.sb.rows.len() {
974                    self.sb_row_at(row)
975                        .and_then(|r| r.get(col))
976                        .map(|cell| cell.ch)
977                        .unwrap_or(' ')
978                } else {
979                    self.sb.cur_row[col].ch
980                };
981                let mut buf = [0u8; 4];
982                bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
983            }
984            if row < end_row {
985                bytes.push(b'\n');
986            }
987        }
988        if let Some(mut clip) = CLIPBOARD.try_lock() {
989            let n = bytes.len().min(CLIPBOARD_CAP);
990            clip.0[..n].copy_from_slice(&bytes[..n]);
991            clip.1 = n;
992        }
993    }
994
995    /// Performs the clear selection operation.
996    pub fn clear_selection(&mut self) {
997        if self.sel_active {
998            self.sel_active = false;
999            self.render_viewport_full();
1000        }
1001    }
1002
1003    /// Performs the clear with operation.
1004    pub fn clear_with(&mut self, color: RgbColor) {
1005        if !self.enabled {
1006            return;
1007        }
1008        let packed = self.pack_color(color);
1009        let canvas = self.can();
1010        let (fw, fh) = (canvas.width, canvas.height);
1011
1012        if canvas.draw_to_back && canvas.back_buffer.is_some() {
1013            drop(canvas);
1014            if let Some(buf) = self.canm().back_buffer.as_mut() {
1015                buf.fill(packed);
1016            }
1017            self.mark_dirty_rect(0, 0, fw, fh);
1018            self.col = 0;
1019            self.row = 0;
1020            return;
1021        }
1022
1023        drop(canvas);
1024        // Direct HW path: bulk fill per row.
1025        if self.fmt.bpp == 32 {
1026            let canvas = self.can();
1027            let row_bytes = fw * 4;
1028            for y in 0..fh {
1029                let off = y * canvas.pitch;
1030                unsafe {
1031                    let ptr = canvas.addr.add(off) as *mut u32;
1032                    for x in 0..fw {
1033                        core::ptr::write_volatile(ptr.add(x), packed);
1034                    }
1035                }
1036            }
1037        } else {
1038            for y in 0..fh {
1039                for x in 0..fw {
1040                    self.write_hw_pixel_packed(x, y, packed);
1041                }
1042            }
1043        }
1044        self.col = 0;
1045        self.row = 0;
1046    }
1047
1048    /// Performs the clear operation.
1049    pub fn clear(&mut self) {
1050        self.clear_with(self.unpack_color(self.bg));
1051    }
1052
1053    /// Performs the pixel offset operation.
1054    #[inline]
1055    pub(crate) fn pixel_offset(&self, x: usize, y: usize) -> Option<usize> {
1056        if x >= self.can().width || y >= self.can().height {
1057            return None;
1058        }
1059        let bytes_pp = self.fmt.bpp as usize / 8;
1060        let row = y.checked_mul(self.can().pitch)?;
1061        let col = x.checked_mul(bytes_pp)?;
1062        row.checked_add(col)
1063    }
1064
1065    /// Writes hw pixel packed.
1066    pub(crate) fn write_hw_pixel_packed(&mut self, x: usize, y: usize, color: u32) {
1067        let Some(off) = self.pixel_offset(x, y) else {
1068            return;
1069        };
1070        unsafe {
1071            match self.fmt.bpp {
1072                32 => {
1073                    core::ptr::write_volatile(self.can().addr.add(off) as *mut u32, color);
1074                }
1075                24 => {
1076                    core::ptr::write_volatile(self.can().addr.add(off), (color & 0xFF) as u8);
1077                    core::ptr::write_volatile(
1078                        self.can().addr.add(off + 1),
1079                        ((color >> 8) & 0xFF) as u8,
1080                    );
1081                    core::ptr::write_volatile(
1082                        self.can().addr.add(off + 2),
1083                        ((color >> 16) & 0xFF) as u8,
1084                    );
1085                }
1086                _ => {}
1087            }
1088        }
1089    }
1090
1091    /// Reads hw pixel packed.
1092    pub(crate) fn read_hw_pixel_packed(&self, x: usize, y: usize) -> u32 {
1093        let Some(off) = self.pixel_offset(x, y) else {
1094            return 0;
1095        };
1096        unsafe {
1097            match self.fmt.bpp {
1098                32 => core::ptr::read_volatile(self.can().addr.add(off) as *const u32),
1099                24 => {
1100                    let b0 = core::ptr::read_volatile(self.can().addr.add(off)) as u32;
1101                    let b1 = core::ptr::read_volatile(self.can().addr.add(off + 1)) as u32;
1102                    let b2 = core::ptr::read_volatile(self.can().addr.add(off + 2)) as u32;
1103                    b0 | (b1 << 8) | (b2 << 16)
1104                }
1105                _ => 0,
1106            }
1107        }
1108    }
1109
1110    /// Reads pixel packed.
1111    pub(crate) fn read_pixel_packed(&self, x: usize, y: usize) -> u32 {
1112        if x >= self.can().width || y >= self.can().height {
1113            return 0;
1114        }
1115        if self.draw_to_back_buffer() {
1116            if let Some(buf) = self.can().back_buffer.as_ref() {
1117                return buf[y * self.can().width + x];
1118            }
1119        }
1120        self.read_hw_pixel_packed(x, y)
1121    }
1122
1123    /// Performs the put pixel raw operation.
1124    pub(crate) fn put_pixel_raw(&mut self, x: usize, y: usize, color: u32) {
1125        if !self.enabled {
1126            return;
1127        }
1128        // Fast path : inline canvas bounds + clip check in one go.
1129        let canvas = self.can();
1130        let (fw, fh) = (canvas.width, canvas.height);
1131        if x >= fw || y >= fh || !self.in_clip(x, y) {
1132            return;
1133        }
1134        if canvas.draw_to_back && canvas.back_buffer.is_some() {
1135            drop(canvas);
1136            if let Some(buf) = self.canm().back_buffer.as_mut() {
1137                buf[y * fw + x] = color;
1138            }
1139            self.mark_dirty_rect(x, y, 1, 1);
1140            return;
1141        }
1142        drop(canvas);
1143        self.write_hw_pixel_packed(x, y, color);
1144    }
1145
1146    /// Performs the draw pixel operation.
1147    pub fn draw_pixel(&mut self, x: usize, y: usize, color: RgbColor) {
1148        self.put_pixel_raw(x, y, self.pack_color(color));
1149    }
1150
1151    /// Performs the draw pixel alpha operation.
1152    pub fn draw_pixel_alpha(&mut self, x: usize, y: usize, color: RgbColor, alpha: u8) {
1153        if !self.enabled
1154            || alpha == 0
1155            || x >= self.can().width
1156            || y >= self.can().height
1157            || !self.in_clip(x, y)
1158        {
1159            return;
1160        }
1161        if alpha == 255 {
1162            self.put_pixel_raw(x, y, self.pack_color(color));
1163            return;
1164        }
1165        let dst = self.unpack_color(self.read_pixel_packed(x, y));
1166        let inv = (255u16).saturating_sub(alpha as u16);
1167        let a = alpha as u16;
1168        let blended = RgbColor::new(
1169            ((color.r as u16 * a + dst.r as u16 * inv + 127) / 255) as u8,
1170            ((color.g as u16 * a + dst.g as u16 * inv + 127) / 255) as u8,
1171            ((color.b as u16 * a + dst.b as u16 * inv + 127) / 255) as u8,
1172        );
1173        self.put_pixel_raw(x, y, self.pack_color(blended));
1174    }
1175
1176    /// Performs the draw line operation.
1177    pub fn draw_line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
1178        let mut x = x0;
1179        let mut y = y0;
1180        let dx = (x1 - x0).abs();
1181        let sx = if x0 < x1 { 1 } else { -1 };
1182        let dy = -(y1 - y0).abs();
1183        let sy = if y0 < y1 { 1 } else { -1 };
1184        let mut err = dx + dy;
1185        let packed = self.pack_color(color);
1186
1187        loop {
1188            if x >= 0 && y >= 0 {
1189                self.put_pixel_raw(x as usize, y as usize, packed);
1190            }
1191            if x == x1 && y == y1 {
1192                break;
1193            }
1194            let e2 = 2 * err;
1195            if e2 >= dy {
1196                err += dy;
1197                x += sx;
1198            }
1199            if e2 <= dx {
1200                err += dx;
1201                y += sy;
1202            }
1203        }
1204    }
1205
1206    /// Performs the draw rect operation.
1207    pub fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1208        if width == 0 || height == 0 {
1209            return;
1210        }
1211        let x2 = x.saturating_add(width - 1);
1212        let y2 = y.saturating_add(height - 1);
1213        self.draw_line(x as isize, y as isize, x2 as isize, y as isize, color);
1214        self.draw_line(x as isize, y as isize, x as isize, y2 as isize, color);
1215        self.draw_line(x2 as isize, y as isize, x2 as isize, y2 as isize, color);
1216        self.draw_line(x as isize, y2 as isize, x2 as isize, y2 as isize, color);
1217    }
1218
1219    /// Performs the fill rect operation.
1220    pub fn fill_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1221        let Some((sx, sy, sw, sh)) = self.clipped_rect(x, y, width, height) else {
1222            return;
1223        };
1224        let sw = sw.min(self.can().width.saturating_sub(sx));
1225        let sh = sh.min(self.can().height.saturating_sub(sy));
1226        if sw == 0 || sh == 0 {
1227            return;
1228        }
1229        let packed = self.pack_color(color);
1230
1231        if self.draw_to_back_buffer() {
1232            let fw = self.can().width;
1233            let wrote = self
1234                .canm()
1235                .back_buffer
1236                .as_mut()
1237                .map(|buf| {
1238                    for py in sy..(sy + sh) {
1239                        let row = py * fw;
1240                        let start = row + sx;
1241                        let end = start + sw;
1242                        buf[start..end].fill(packed);
1243                    }
1244                })
1245                .is_some();
1246            if wrote {
1247                self.mark_dirty_rect(sx, sy, sw, sh);
1248            }
1249            return;
1250        }
1251
1252        if self.fmt.bpp == 32 {
1253            for py in sy..(sy + sh) {
1254                let Some(row_off) = py
1255                    .checked_mul(self.can().pitch)
1256                    .and_then(|v| v.checked_add(sx * 4))
1257                else {
1258                    continue;
1259                };
1260                let count = sw;
1261                unsafe {
1262                    let ptr = self.can().addr.add(row_off) as *mut u32;
1263                    for i in 0..count {
1264                        core::ptr::write_volatile(ptr.add(i), packed);
1265                    }
1266                }
1267            }
1268            return;
1269        }
1270
1271        for py in sy..(sy + sh) {
1272            for px in sx..(sx + sw) {
1273                self.write_hw_pixel_packed(px, py, packed);
1274            }
1275        }
1276    }
1277
1278    /// Performs the fill rect alpha operation.
1279    pub fn fill_rect_alpha(
1280        &mut self,
1281        x: usize,
1282        y: usize,
1283        width: usize,
1284        height: usize,
1285        color: RgbColor,
1286        alpha: u8,
1287    ) {
1288        if !self.enabled || width == 0 || height == 0 || alpha == 0 {
1289            return;
1290        }
1291        if alpha == 255 {
1292            self.fill_rect(x, y, width, height, color);
1293            return;
1294        }
1295        let x_end = core::cmp::min(x.saturating_add(width), self.can().width);
1296        let y_end = core::cmp::min(y.saturating_add(height), self.can().height);
1297        for py in y..y_end {
1298            for px in x..x_end {
1299                self.draw_pixel_alpha(px, py, color, alpha);
1300            }
1301        }
1302    }
1303
1304    /// Performs the blit rgb operation.
1305    pub fn blit_rgb(
1306        &mut self,
1307        dst_x: usize,
1308        dst_y: usize,
1309        src_width: usize,
1310        src_height: usize,
1311        pixels: &[RgbColor],
1312    ) -> bool {
1313        let len = src_width.saturating_mul(src_height);
1314        if !self.enabled || src_width == 0 || src_height == 0 || pixels.len() < len {
1315            return false;
1316        }
1317        let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.can().width);
1318        let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.can().height);
1319        if x_end <= dst_x || y_end <= dst_y {
1320            return true;
1321        }
1322        let copy_w = x_end - dst_x;
1323        let copy_h = y_end - dst_y;
1324
1325        let fmt = self.fmt;
1326        if self.draw_to_back_buffer() {
1327            let fb_width = self.can().width;
1328            let wrote = self
1329                .canm()
1330                .back_buffer
1331                .as_mut()
1332                .map(|buf| {
1333                    for row in 0..copy_h {
1334                        let src_row = row * src_width;
1335                        let dst_row = (dst_y + row) * fb_width + dst_x;
1336                        for col in 0..copy_w {
1337                            buf[dst_row + col] = fmt.pack_rgb(
1338                                pixels[src_row + col].r,
1339                                pixels[src_row + col].g,
1340                                pixels[src_row + col].b,
1341                            );
1342                        }
1343                    }
1344                })
1345                .is_some();
1346            if wrote {
1347                self.mark_dirty_rect(dst_x, dst_y, copy_w, copy_h);
1348                return true;
1349            }
1350        }
1351
1352        if fmt.bpp == 32 {
1353            let mut row_buf = alloc::vec![0u32; copy_w];
1354            for row in 0..copy_h {
1355                let src_row = row * src_width;
1356                for col in 0..copy_w {
1357                    row_buf[col] = self.pack_color(pixels[src_row + col]);
1358                }
1359                if let Some(off) = self.pixel_offset(dst_x, dst_y + row) {
1360                    unsafe {
1361                        core::ptr::copy_nonoverlapping(
1362                            row_buf.as_ptr(),
1363                            self.can().addr.add(off) as *mut u32,
1364                            copy_w,
1365                        );
1366                    }
1367                }
1368            }
1369        } else {
1370            for row in 0..copy_h {
1371                let src_row = row * src_width;
1372                for col in 0..copy_w {
1373                    self.draw_pixel(dst_x + col, dst_y + row, pixels[src_row + col]);
1374                }
1375            }
1376        }
1377        true
1378    }
1379
1380    /// Performs the blit rgb24 operation.
1381    pub fn blit_rgb24(
1382        &mut self,
1383        dst_x: usize,
1384        dst_y: usize,
1385        src_width: usize,
1386        src_height: usize,
1387        bytes: &[u8],
1388    ) -> bool {
1389        let needed = src_width.saturating_mul(src_height).saturating_mul(3);
1390        if !self.enabled || src_width == 0 || src_height == 0 || bytes.len() < needed {
1391            return false;
1392        }
1393        let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.can().width);
1394        let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.can().height);
1395        if x_end <= dst_x || y_end <= dst_y {
1396            return true;
1397        }
1398        let copy_w = x_end - dst_x;
1399        let copy_h = y_end - dst_y;
1400
1401        let fmt = self.fmt;
1402        if self.draw_to_back_buffer() {
1403            let fb_width = self.can().width;
1404            let wrote = self
1405                .canm()
1406                .back_buffer
1407                .as_mut()
1408                .map(|buf| {
1409                    for row in 0..copy_h {
1410                        let src_base = row * src_width * 3;
1411                        let dst_row = (dst_y + row) * fb_width + dst_x;
1412                        for col in 0..copy_w {
1413                            let i = src_base + col * 3;
1414                            buf[dst_row + col] = fmt.pack_rgb(bytes[i], bytes[i + 1], bytes[i + 2]);
1415                        }
1416                    }
1417                })
1418                .is_some();
1419            if wrote {
1420                self.mark_dirty_rect(dst_x, dst_y, copy_w, copy_h);
1421                return true;
1422            }
1423        }
1424
1425        if fmt.bpp == 32 {
1426            let mut row_buf = alloc::vec![0u32; copy_w];
1427            for row in 0..copy_h {
1428                let src_base = row * src_width * 3;
1429                for col in 0..copy_w {
1430                    let i = src_base + col * 3;
1431                    row_buf[col] =
1432                        self.pack_color(RgbColor::new(bytes[i], bytes[i + 1], bytes[i + 2]));
1433                }
1434                if let Some(off) = self.pixel_offset(dst_x, dst_y + row) {
1435                    unsafe {
1436                        core::ptr::copy_nonoverlapping(
1437                            row_buf.as_ptr(),
1438                            self.can().addr.add(off) as *mut u32,
1439                            copy_w,
1440                        );
1441                    }
1442                }
1443            }
1444        } else {
1445            for row in 0..copy_h {
1446                for col in 0..copy_w {
1447                    let i = (row * src_width + col) * 3;
1448                    let color = RgbColor::new(bytes[i], bytes[i + 1], bytes[i + 2]);
1449                    self.draw_pixel(dst_x + col, dst_y + row, color);
1450                }
1451            }
1452        }
1453        true
1454    }
1455
1456    /// Performs the blit rgba operation.
1457    pub fn blit_rgba(
1458        &mut self,
1459        dst_x: usize,
1460        dst_y: usize,
1461        src_width: usize,
1462        src_height: usize,
1463        bytes: &[u8],
1464        global_alpha: u8,
1465    ) -> bool {
1466        let needed = src_width.saturating_mul(src_height).saturating_mul(4);
1467        if !self.enabled
1468            || src_width == 0
1469            || src_height == 0
1470            || bytes.len() < needed
1471            || global_alpha == 0
1472        {
1473            return false;
1474        }
1475
1476        let Some((sx, sy, sw, sh)) = self.clipped_rect(dst_x, dst_y, src_width, src_height) else {
1477            return true;
1478        };
1479        let src_x0 = sx.saturating_sub(dst_x);
1480        let src_y0 = sy.saturating_sub(dst_y);
1481
1482        for row in 0..sh {
1483            let syi = src_y0 + row;
1484            for col in 0..sw {
1485                let sxi = src_x0 + col;
1486                let i = (syi * src_width + sxi) * 4;
1487                let r = bytes[i];
1488                let g = bytes[i + 1];
1489                let b = bytes[i + 2];
1490                let sa = bytes[i + 3];
1491                if sa == 0 {
1492                    continue;
1493                }
1494                let a = ((sa as u16 * global_alpha as u16 + 127) / 255) as u8;
1495                let dx = sx + col;
1496                let dy = sy + row;
1497                if a == 255 {
1498                    self.put_pixel_raw(dx, dy, self.pack_color(RgbColor::new(r, g, b)));
1499                } else if a != 0 {
1500                    self.draw_pixel_alpha(dx, dy, RgbColor::new(r, g, b), a);
1501                }
1502            }
1503        }
1504        true
1505    }
1506
1507    /// Performs the blit sprite rgba operation.
1508    pub fn blit_sprite_rgba(
1509        &mut self,
1510        dst_x: usize,
1511        dst_y: usize,
1512        sprite: SpriteRgba<'_>,
1513        global_alpha: u8,
1514    ) -> bool {
1515        self.blit_rgba(
1516            dst_x,
1517            dst_y,
1518            sprite.width,
1519            sprite.height,
1520            sprite.pixels,
1521            global_alpha,
1522        )
1523    }
1524
1525    /// Performs the draw text at operation.
1526    pub fn draw_text_at(
1527        &mut self,
1528        pixel_x: usize,
1529        pixel_y: usize,
1530        text: &str,
1531        fg: RgbColor,
1532        bg: RgbColor,
1533    ) {
1534        if !self.enabled {
1535            return;
1536        }
1537        let gw = self.font_info.glyph_w;
1538        let gh = self.font_info.glyph_h;
1539        let fg_packed = self.pack_color(fg);
1540        let bg_packed = self.pack_color(bg);
1541        let mut cx = pixel_x;
1542        let cy = pixel_y;
1543        for ch in text.chars() {
1544            if ch == '\n' {
1545                break;
1546            }
1547            if cx + gw > self.can().width || cy + gh > self.can().height {
1548                break;
1549            }
1550            self.draw_glyph_at_pixel(cx, cy, ch, fg_packed, bg_packed);
1551            cx += gw;
1552        }
1553    }
1554
1555    /// Performs the glyph index for char operation.
1556    pub(crate) fn glyph_index_for_char(&self, ch: char) -> usize {
1557        if ch.is_ascii() {
1558            let idx = ch as usize;
1559            if idx < self.font_info.glyph_count {
1560                return idx;
1561            }
1562        }
1563        let cp = ch as u32;
1564        if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == cp) {
1565            return *glyph;
1566        }
1567        if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == ('?' as u32)) {
1568            return *glyph;
1569        }
1570        if ('?' as usize) < self.font_info.glyph_count {
1571            return '?' as usize;
1572        }
1573        0
1574    }
1575
1576    /// Performs the draw glyph index at pixel operation.
1577    pub(crate) fn draw_glyph_index_at_pixel(
1578        &mut self,
1579        pixel_x: usize,
1580        pixel_y: usize,
1581        glyph_index: usize,
1582        fg: u32,
1583        bg: u32,
1584    ) {
1585        if !self.enabled {
1586            return;
1587        }
1588        let glyph_index = core::cmp::min(glyph_index, self.font_info.glyph_count.saturating_sub(1));
1589        let gw = self.font_info.glyph_w;
1590        let gh = self.font_info.glyph_h;
1591        let glyph_pixels = gw.saturating_mul(gh);
1592        let Some(mask) = self.glyph_mask_slice(glyph_index) else {
1593            return;
1594        };
1595        let mask_ptr = mask.as_ptr();
1596
1597        if self.draw_to_back_buffer()
1598            && pixel_x + gw <= self.can().width
1599            && pixel_y + gh <= self.can().height
1600        {
1601            let fb_width = self.can().width;
1602            if let Some(buf) = self.canm().back_buffer.as_mut() {
1603                for gy in 0..gh {
1604                    let row_start = (pixel_y + gy) * fb_width + pixel_x;
1605                    buf[row_start..row_start + gw].fill(bg);
1606                    for gx in 0..gw {
1607                        let idx = gy * gw + gx;
1608                        if idx >= glyph_pixels {
1609                            continue;
1610                        }
1611                        let bit = unsafe { *mask_ptr.add(idx) };
1612                        if bit != 0 {
1613                            buf[row_start + gx] = fg;
1614                        }
1615                    }
1616                }
1617            }
1618            self.mark_dirty_rect(pixel_x, pixel_y, gw, gh);
1619        } else {
1620            if self.fmt.bpp == 32 {
1621                for gy in 0..gh {
1622                    if let Some(off) = self.pixel_offset(pixel_x, pixel_y + gy) {
1623                        let row_ptr = unsafe { self.can().addr.add(off) } as *mut u32;
1624                        for gx in 0..gw {
1625                            let idx = gy * gw + gx;
1626                            let color = if idx < glyph_pixels && unsafe { *mask_ptr.add(idx) } != 0
1627                            {
1628                                fg
1629                            } else {
1630                                bg
1631                            };
1632                            unsafe {
1633                                core::ptr::write_volatile(row_ptr.add(gx), color);
1634                            }
1635                        }
1636                    }
1637                }
1638            } else {
1639                for gy in 0..gh {
1640                    for gx in 0..gw {
1641                        let idx = gy * gw + gx;
1642                        if idx >= glyph_pixels {
1643                            continue;
1644                        }
1645                        let color = if unsafe { *mask_ptr.add(idx) } != 0 {
1646                            fg
1647                        } else {
1648                            bg
1649                        };
1650                        self.put_pixel_raw(pixel_x + gx, pixel_y + gy, color);
1651                    }
1652                }
1653            }
1654        }
1655    }
1656
1657    /// Performs the draw glyph at pixel operation.
1658    pub(crate) fn draw_glyph_at_pixel(
1659        &mut self,
1660        pixel_x: usize,
1661        pixel_y: usize,
1662        ch: char,
1663        fg: u32,
1664        bg: u32,
1665    ) {
1666        let glyph_index = self.glyph_index_for_char(ch);
1667        self.draw_glyph_index_at_pixel(pixel_x, pixel_y, glyph_index, fg, bg);
1668    }
1669
1670    pub(crate) fn fill_text_span_bg(
1671        &mut self,
1672        pixel_x: usize,
1673        pixel_y: usize,
1674        width: usize,
1675        height: usize,
1676        bg: u32,
1677    ) {
1678        if self.draw_to_back_buffer()
1679            && pixel_x + width <= self.can().width
1680            && pixel_y + height <= self.can().height
1681        {
1682            let fb_width = self.can().width;
1683            if let Some(buf) = self.canm().back_buffer.as_mut() {
1684                for row in 0..height {
1685                    let start = (pixel_y + row) * fb_width + pixel_x;
1686                    let end = start + width;
1687                    buf[start..end].fill(bg);
1688                }
1689            }
1690            self.mark_dirty_rect(pixel_x, pixel_y, width, height);
1691            return;
1692        }
1693
1694        let gh = height;
1695        let color = self.unpack_color(bg);
1696        self.fill_rect(pixel_x, pixel_y, width, gh, color);
1697    }
1698
1699    pub(crate) fn clear_text_line_pixels(&mut self, vis_row: usize) {
1700        let gh = self.font_info.glyph_h;
1701        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
1702        self.fill_text_span_bg(0, vis_row.saturating_mul(gh), text_w, gh, self.bg);
1703    }
1704
1705    pub(crate) fn visible_virtual_bounds(&self) -> (usize, usize, usize, usize, bool) {
1706        let total_complete = self.sb.rows.len();
1707        let has_partial = !self.sb.cur_row.is_empty();
1708        let total_virtual = total_complete + if has_partial { 1 } else { 0 };
1709        let view_end = total_virtual.saturating_sub(self.scroll_offset);
1710        let view_start = view_end.saturating_sub(self.rows);
1711        (
1712            total_complete,
1713            total_virtual,
1714            view_start,
1715            view_end,
1716            has_partial,
1717        )
1718    }
1719
1720    pub(crate) fn sync_live_cursor_from_view(
1721        &mut self,
1722        total_complete: usize,
1723        view_start: usize,
1724        view_end: usize,
1725    ) {
1726        let display_len = view_end.saturating_sub(view_start);
1727        self.row = if display_len > 0 { display_len - 1 } else { 0 };
1728        let last_virt = view_start + display_len.saturating_sub(1);
1729        let last_len = if last_virt < total_complete {
1730            self.sb_row_at(last_virt).map(|row| row.len()).unwrap_or(0)
1731        } else if last_virt == total_complete {
1732            self.sb.cur_row.len()
1733        } else {
1734            0
1735        };
1736        self.col = last_len.min(self.cols);
1737    }
1738
1739    pub(crate) fn selection_colors_for_cell(
1740        &self,
1741        virt_row: usize,
1742        col: usize,
1743        fg: u32,
1744        bg: u32,
1745    ) -> (u32, u32) {
1746        if !self.sel_active {
1747            return (fg, bg);
1748        }
1749        let (sel_sr, sel_sc, sel_er, sel_ec) = self.sel_normalized();
1750        let in_sel = if virt_row < sel_sr || virt_row > sel_er {
1751            false
1752        } else if sel_sr == sel_er {
1753            col >= sel_sc && col < sel_ec
1754        } else if virt_row == sel_sr {
1755            col >= sel_sc
1756        } else if virt_row == sel_er {
1757            col < sel_ec
1758        } else {
1759            true
1760        };
1761        if in_sel {
1762            (
1763                self.pack_color(RgbColor::WHITE),
1764                self.pack_color(RgbColor::new(0x26, 0x5F, 0xCC)),
1765            )
1766        } else {
1767            (fg, bg)
1768        }
1769    }
1770
1771    pub(crate) fn render_virtual_row(
1772        &mut self,
1773        virt_row: usize,
1774        vis_row: usize,
1775        total_complete: usize,
1776        has_partial: bool,
1777    ) {
1778        let glyph_h = self.font_info.glyph_h;
1779        let glyph_w = self.font_info.glyph_w;
1780        let py = vis_row.saturating_mul(glyph_h);
1781
1782        let (row_ptr, row_len) = if virt_row < total_complete {
1783            let row = match self.sb_row_at(virt_row) {
1784                Some(row) => row,
1785                None => return,
1786            };
1787            (row.as_ptr(), row.len())
1788        } else if has_partial && virt_row == total_complete {
1789            (self.sb.cur_row.as_ptr(), self.sb.cur_row.len())
1790        } else {
1791            (core::ptr::null(), 0)
1792        };
1793
1794        let cell_count = row_len.min(self.cols);
1795        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
1796        let used_width = cell_count.saturating_mul(glyph_w).min(text_w);
1797
1798        if self.draw_to_back_buffer() && py + glyph_h <= self.can().height {
1799            let fb_width = self.can().width;
1800            let default_bg = self.bg;
1801            let glyph_pixels = glyph_w.saturating_mul(glyph_h);
1802            let glyph_cache_ptr = self.glyph_mask_cache.as_ptr();
1803            self.prepared_row_cells.clear();
1804            if self.prepared_row_cells.capacity() < cell_count {
1805                self.prepared_row_cells
1806                    .reserve(cell_count - self.prepared_row_cells.capacity());
1807            }
1808            for col in 0..cell_count {
1809                let cell = unsafe { &*row_ptr.add(col) };
1810                let glyph_index = self.glyph_index_for_char(cell.ch);
1811                let (draw_fg, draw_bg) =
1812                    self.selection_colors_for_cell(virt_row, col, cell.fg, cell.bg);
1813                self.prepared_row_cells
1814                    .push((glyph_index, draw_fg, draw_bg));
1815            }
1816            let cells_ptr = self.prepared_row_cells.as_ptr();
1817            let cells_len = self.prepared_row_cells.len();
1818            if let Some(buf) = self.canm().back_buffer.as_mut() {
1819                for gy in 0..glyph_h {
1820                    let row_start = (py + gy) * fb_width;
1821                    buf[row_start..row_start + text_w].fill(default_bg);
1822                }
1823
1824                for col in 0..cells_len {
1825                    let (glyph_index, draw_fg, draw_bg) = unsafe { *cells_ptr.add(col) };
1826                    let px = col.saturating_mul(glyph_w);
1827                    if draw_bg != default_bg {
1828                        for gy in 0..glyph_h {
1829                            let row_start = (py + gy) * fb_width + px;
1830                            buf[row_start..row_start + glyph_w].fill(draw_bg);
1831                        }
1832                    }
1833
1834                    let glyph_base = glyph_index.saturating_mul(glyph_pixels);
1835                    for gy in 0..glyph_h {
1836                        let row_start = (py + gy) * fb_width + px;
1837                        for gx in 0..glyph_w {
1838                            let idx = glyph_base + gy * glyph_w + gx;
1839                            let bit = unsafe { *glyph_cache_ptr.add(idx) };
1840                            if bit != 0 {
1841                                buf[row_start + gx] = draw_fg;
1842                            }
1843                        }
1844                    }
1845                }
1846            }
1847
1848            self.mark_dirty_rect(0, py, text_w, glyph_h);
1849            return;
1850        }
1851
1852        for col in 0..cell_count {
1853            let px = col.saturating_mul(glyph_w);
1854            let cell = unsafe { &*row_ptr.add(col) };
1855            let (draw_fg, draw_bg) =
1856                self.selection_colors_for_cell(virt_row, col, cell.fg, cell.bg);
1857            self.draw_glyph_at_pixel(px, py, cell.ch, draw_fg, draw_bg);
1858        }
1859        if text_w > used_width {
1860            self.fill_text_span_bg(used_width, py, text_w - used_width, glyph_h, self.bg);
1861        }
1862    }
1863
1864    pub(crate) fn redraw_visible_rows(&mut self, start_vis_row: usize, count: usize) {
1865        let (total_complete, _, view_start, view_end, has_partial) = self.visible_virtual_bounds();
1866        let display_len = view_end.saturating_sub(view_start);
1867        let end_vis_row = start_vis_row.saturating_add(count).min(self.rows);
1868        for vis_row in start_vis_row..end_vis_row {
1869            if vis_row < display_len {
1870                self.render_virtual_row(view_start + vis_row, vis_row, total_complete, has_partial);
1871            } else {
1872                self.clear_text_line_pixels(vis_row);
1873            }
1874        }
1875    }
1876
1877    pub(crate) fn ensure_back_buffer_for_viewport(&mut self) {
1878        let canvas = self.can();
1879        if canvas.back_buffer.is_none() {
1880            let total = canvas.width.saturating_mul(canvas.height);
1881            if total > 0 {
1882                let mut buf = alloc::vec![0u32; total];
1883                // Sync back buffer from hardware so content written directly
1884                // (e.g. status bar during boot) survives present().
1885                if self.enabled && !canvas.addr.is_null() {
1886                    if self.fmt.bpp == 32 {
1887                        unsafe {
1888                            core::ptr::copy_nonoverlapping(
1889                                canvas.addr as *const u8,
1890                                buf.as_mut_ptr() as *mut u8,
1891                                total * 4,
1892                            );
1893                        }
1894                    } else {
1895                        for y in 0..canvas.height {
1896                            for x in 0..canvas.width {
1897                                buf[y * canvas.width + x] = self.read_hw_pixel_packed(x, y);
1898                            }
1899                        }
1900                    }
1901                }
1902                drop(canvas);
1903                self.canm().back_buffer = Some(buf);
1904            }
1905        }
1906    }
1907
1908    pub(crate) fn begin_viewport_render(&mut self) -> (bool, bool) {
1909        self.ensure_back_buffer_for_viewport();
1910        let canvas = self.can();
1911        let prev_draw_to_back = canvas.draw_to_back;
1912        let prev_track_dirty = canvas.track_dirty;
1913        let has_back = canvas.back_buffer.is_some();
1914        drop(canvas);
1915        if has_back {
1916            let canvas = self.canm();
1917            canvas.draw_to_back = true;
1918            canvas.track_dirty = true;
1919            canvas.dirty.clear();
1920        }
1921        (prev_draw_to_back, prev_track_dirty)
1922    }
1923
1924    pub(crate) fn end_viewport_render(&mut self, prev_draw_to_back: bool, prev_track_dirty: bool) {
1925        let has_back = self.can().back_buffer.is_some();
1926        if has_back {
1927            self.request_present();
1928            let now = crate::process::scheduler::ticks();
1929            let force_present = self.canm().last_present_tick == 0 || now == 0;
1930            self.present_if_due(force_present);
1931            let canvas = self.canm();
1932            canvas.draw_to_back = prev_draw_to_back;
1933            canvas.track_dirty = prev_track_dirty;
1934            if !prev_track_dirty {
1935                canvas.dirty.clear();
1936            }
1937        }
1938    }
1939
1940    pub(crate) fn finalize_live_view_state(&mut self) {
1941        let (total_complete, _, view_start, view_end, _) = self.visible_virtual_bounds();
1942        if self.scroll_offset == 0 {
1943            self.sync_live_cursor_from_view(total_complete, view_start, view_end);
1944        }
1945    }
1946
1947    pub(crate) fn render_viewport_full(&mut self) {
1948        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
1949        let text_h = self.text_area_height();
1950        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
1951        self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
1952        self.redraw_visible_rows(0, self.rows);
1953        self.finalize_live_view_state();
1954        self.draw_scrollbar_inner();
1955        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
1956    }
1957
1958    pub(crate) fn refresh_viewport_decorations(&mut self) {
1959        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
1960        self.finalize_live_view_state();
1961        self.draw_scrollbar_inner();
1962        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
1963    }
1964
1965    pub(crate) fn set_scroll_offset_and_render(&mut self, new_offset: usize) {
1966        let old_offset = self.scroll_offset;
1967        self.scroll_offset = new_offset;
1968        if !self.redraw_from_scrollback_incremental(old_offset) {
1969            self.redraw_from_scrollback();
1970        }
1971    }
1972
1973    pub(crate) fn move_text_view_pixels_up(&mut self, pixels: usize) {
1974        if pixels == 0 {
1975            return;
1976        }
1977        let text_h = self.text_area_height();
1978        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
1979        if pixels >= text_h {
1980            self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
1981            return;
1982        }
1983        let move_rows = text_h - pixels;
1984        if self.draw_to_back_buffer() {
1985            let row_width = self.can().width;
1986            if let Some(buf) = self.canm().back_buffer.as_mut() {
1987                buf.copy_within(pixels * row_width..text_h * row_width, 0);
1988            }
1989            self.mark_dirty_rect(0, 0, row_width, text_h);
1990        } else {
1991            unsafe {
1992                core::ptr::copy(
1993                    self.can().addr.add(pixels * self.can().pitch),
1994                    self.can().addr,
1995                    move_rows * self.can().pitch,
1996                );
1997            }
1998        }
1999        self.fill_rect(0, move_rows, text_w, pixels, self.unpack_color(self.bg));
2000    }
2001
2002    pub(crate) fn move_text_view_pixels_down(&mut self, pixels: usize) {
2003        if pixels == 0 {
2004            return;
2005        }
2006        let text_h = self.text_area_height();
2007        let text_w = self.can().width.saturating_sub(SCROLLBAR_W);
2008        if pixels >= text_h {
2009            self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2010            return;
2011        }
2012        let move_rows = text_h - pixels;
2013        if self.draw_to_back_buffer() {
2014            let row_width = self.can().width;
2015            if let Some(buf) = self.canm().back_buffer.as_mut() {
2016                buf.copy_within(0..move_rows * row_width, pixels * row_width);
2017            }
2018            self.mark_dirty_rect(0, 0, row_width, text_h);
2019        } else {
2020            unsafe {
2021                core::ptr::copy(
2022                    self.can().addr,
2023                    self.can().addr.add(pixels * self.can().pitch),
2024                    move_rows * self.can().pitch,
2025                );
2026            }
2027        }
2028        self.fill_rect(0, 0, text_w, pixels, self.unpack_color(self.bg));
2029    }
2030
2031    /// Performs the layout text lines operation.
2032    pub(crate) fn layout_text_lines(
2033        &self,
2034        text: &str,
2035        wrap: bool,
2036        max_cols: Option<usize>,
2037    ) -> Vec<Vec<char>> {
2038        let mut lines: Vec<Vec<char>> = Vec::new();
2039        let mut current: Vec<char> = Vec::new();
2040        let wrap_cols = max_cols.filter(|&c| c > 0);
2041
2042        for ch in text.chars() {
2043            if ch == '\n' {
2044                lines.push(current);
2045                current = Vec::new();
2046                continue;
2047            }
2048
2049            if wrap {
2050                if let Some(cols) = wrap_cols {
2051                    if current.len() >= cols {
2052                        lines.push(current);
2053                        current = Vec::new();
2054                    }
2055                }
2056            }
2057
2058            current.push(ch);
2059        }
2060
2061        lines.push(current);
2062        lines
2063    }
2064
2065    /// Performs the measure text operation.
2066    pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
2067        if !self.enabled {
2068            return TextMetrics {
2069                width: 0,
2070                height: 0,
2071                lines: 0,
2072            };
2073        }
2074        let gw = self.font_info.glyph_w;
2075        let gh = self.font_info.glyph_h;
2076        let max_cols = max_width.map(|w| core::cmp::max(1, w / gw));
2077        let lines = self.layout_text_lines(text, wrap, max_cols);
2078
2079        let mut max_line_cols = 0usize;
2080        for line in &lines {
2081            max_line_cols = core::cmp::max(max_line_cols, line.len());
2082        }
2083
2084        TextMetrics {
2085            width: max_line_cols * gw,
2086            height: lines.len() * gh,
2087            lines: lines.len(),
2088        }
2089    }
2090
2091    /// Performs the draw text operation.
2092    pub fn draw_text(
2093        &mut self,
2094        pixel_x: usize,
2095        pixel_y: usize,
2096        text: &str,
2097        opts: TextOptions,
2098    ) -> TextMetrics {
2099        if !self.enabled {
2100            return TextMetrics {
2101                width: 0,
2102                height: 0,
2103                lines: 0,
2104            };
2105        }
2106
2107        let gw = self.font_info.glyph_w;
2108        let gh = self.font_info.glyph_h;
2109        let max_cols = opts.max_width.map(|w| core::cmp::max(1, w / gw));
2110        let lines = self.layout_text_lines(text, opts.wrap, max_cols);
2111        let region_w = opts.max_width.unwrap_or_else(|| {
2112            let mut max_line_cols = 0usize;
2113            for line in &lines {
2114                max_line_cols = core::cmp::max(max_line_cols, line.len());
2115            }
2116            max_line_cols * gw
2117        });
2118
2119        let fg = self.pack_color(opts.fg);
2120        let bg = self.pack_color(opts.bg);
2121        let mut max_line_px = 0usize;
2122
2123        for (line_idx, line) in lines.iter().enumerate() {
2124            let line_px = line.len() * gw;
2125            max_line_px = core::cmp::max(max_line_px, line_px);
2126            let x = match opts.align {
2127                TextAlign::Left => pixel_x,
2128                TextAlign::Center => pixel_x.saturating_add(region_w.saturating_sub(line_px) / 2),
2129                TextAlign::Right => pixel_x.saturating_add(region_w.saturating_sub(line_px)),
2130            };
2131            let y = pixel_y + line_idx * gh;
2132
2133            for (col, ch) in line.iter().enumerate() {
2134                self.draw_glyph_at_pixel(x + col * gw, y, *ch, fg, bg);
2135            }
2136        }
2137
2138        TextMetrics {
2139            width: max_line_px,
2140            height: lines.len() * gh,
2141            lines: lines.len(),
2142        }
2143    }
2144
2145    /// Performs the draw strata stack operation.
2146    pub fn draw_strata_stack(
2147        &mut self,
2148        origin_x: usize,
2149        origin_y: usize,
2150        layer_w: usize,
2151        layer_h: usize,
2152    ) {
2153        if !self.enabled || layer_w == 0 || layer_h == 0 {
2154            return;
2155        }
2156
2157        // Simple "strata" stack: each layer is slightly shifted and tinted.
2158        let palette = [
2159            RgbColor::new(0x24, 0x3B, 0x55),
2160            RgbColor::new(0x2B, 0x54, 0x77),
2161            RgbColor::new(0x2F, 0x74, 0x93),
2162            RgbColor::new(0x3A, 0x93, 0xA8),
2163            RgbColor::new(0x5F, 0xB1, 0xA1),
2164            RgbColor::new(0xA4, 0xCC, 0x94),
2165        ];
2166
2167        let dx = 6usize;
2168        let dy = 5usize;
2169        for (i, color) in palette.iter().enumerate() {
2170            let x = origin_x.saturating_add(i * dx);
2171            let y = origin_y.saturating_add(i * dy);
2172            let w = layer_w.saturating_sub(i * dx);
2173            let h = layer_h.saturating_sub(i * dy);
2174            if w < 8 || h < 8 {
2175                break;
2176            }
2177
2178            self.fill_rect(x, y, w, h, *color);
2179            self.draw_rect(x, y, w, h, RgbColor::new(0x10, 0x16, 0x20));
2180        }
2181    }
2182
2183    /// Performs the draw glyph operation.
2184    pub(crate) fn draw_glyph(&mut self, cx: usize, cy: usize, ch: char) {
2185        let glyph_index = self.glyph_index_for_char(ch);
2186        self.draw_glyph_index_at_pixel(
2187            cx * self.font_info.glyph_w,
2188            cy * self.font_info.glyph_h,
2189            glyph_index,
2190            self.fg,
2191            self.bg,
2192        );
2193    }
2194
2195    /// Performs the clear row operation.
2196    pub(crate) fn clear_row(&mut self, row: usize) {
2197        if !self.enabled {
2198            return;
2199        }
2200        self.clear_text_line_pixels(row);
2201    }
2202
2203    /// Performs the scroll operation.
2204    pub(crate) fn scroll(&mut self) {
2205        if !self.enabled {
2206            return;
2207        }
2208        let dy = self.font_info.glyph_h;
2209        let text_h = self.text_area_height();
2210        if dy >= text_h {
2211            self.clear();
2212            return;
2213        }
2214        self.move_text_view_pixels_up(dy);
2215        self.row = self.rows - 1;
2216    }
2217
2218    /// Writes char.
2219    pub(crate) fn write_char(&mut self, c: char) {
2220        if !self.enabled {
2221            return;
2222        }
2223        let c = normalize_console_char(c);
2224
2225        // Mirror into scrollback (always, even when scrolled back) ========================================
2226        self.sb_mirror_char(c);
2227        // If the user is viewing history, suppress live rendering.
2228        if self.scroll_offset > 0 {
2229            return;
2230        }
2231        // ==============================
2232
2233        match c {
2234            '\n' => {
2235                self.col = 0;
2236                self.row += 1;
2237            }
2238            '\r' => self.col = 0,
2239            '\t' => self.col = (self.col + 4) & !3,
2240            '\u{8}' => {
2241                if self.col > 0 {
2242                    self.col -= 1;
2243                    self.draw_glyph(self.col, self.row, ' ');
2244                }
2245            }
2246            '\0' => {}
2247            ch => {
2248                self.draw_glyph(self.col, self.row, ch);
2249                self.col += 1;
2250            }
2251        }
2252
2253        if self.col >= self.cols {
2254            self.col = 0;
2255            self.row += 1;
2256        }
2257
2258        if self.row >= self.rows {
2259            self.scroll();
2260        }
2261    }
2262
2263    /// Writes bytes.
2264    pub(crate) fn write_bytes(&mut self, s: &str) {
2265        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2266        // Skip basic ANSI escape sequences to avoid rendering control garbage.
2267        let bytes = s.as_bytes();
2268        let mut i = 0;
2269        while i < bytes.len() {
2270            let b = bytes[i];
2271            if b == 0x1B {
2272                // ESC : skip CSI sequences: ESC [ ... final_byte
2273                if i + 1 < bytes.len() && bytes[i + 1] == b'[' {
2274                    i += 2;
2275                    while i < bytes.len() {
2276                        let c = bytes[i];
2277                        i += 1;
2278                        if c >= b'@' && c <= b'~' {
2279                            break;
2280                        }
2281                    }
2282                } else {
2283                    i += 1;
2284                }
2285                continue;
2286            }
2287            // Safe: ASCII bytes 0x20..=0x7E are valid single-char UTF-8.
2288            let ch = b as char;
2289            self.write_char(ch);
2290            i += 1;
2291        }
2292        // Refresh scrollbar after each batch of output.
2293        self.draw_scrollbar_inner();
2294        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2295    }
2296
2297    // =============================================================
2298    // Scrollback buffer + scrollbar
2299    // =============================================================
2300
2301    /// Mirror a normalized character into the scrollback model.
2302    /// Called by `write_char` before any live rendering.
2303    /// Mirror a typed character into the scrollback buffer.
2304    pub(crate) fn sb_mirror_char(&mut self, c: char) {
2305        let cols = self.cols;
2306        let fg = self.fg;
2307        let bg = self.bg;
2308        let rows = self.rows;
2309        self.sb.mirror_char(c, cols, fg, bg, rows);
2310    }
2311
2312    /// Keep the scrollback buffer within MAX_SCROLLBACK + rows.
2313    #[inline]
2314    pub(crate) fn sb_trim(&mut self) {
2315        let rows = self.rows;
2316        self.sb.trim(rows);
2317    }
2318
2319    /// Draw (or refresh) the scrollbar strip on the right edge of the text area.
2320    pub(crate) fn draw_scrollbar_inner(&mut self) {
2321        if !self.enabled || self.can().width == 0 {
2322            return;
2323        }
2324        let text_h = self.text_area_height();
2325        if text_h == 0 {
2326            return;
2327        }
2328        let sb_x = self.can().width.saturating_sub(SCROLLBAR_W);
2329        let total = self.sb.rows.len() + 1; // +1 accounts for current partial row
2330        let track_packed = self.fmt.pack_rgb(0x22, 0x28, 0x38);
2331        let thumb_packed = self.fmt.pack_rgb(0x58, 0x72, 0xA0);
2332        let thumb_hi = self.fmt.pack_rgb(0x80, 0xA0, 0xC8);
2333
2334        if total <= self.rows {
2335            // Not enough content to scroll: full-height thumb.
2336            for y in 0..text_h {
2337                for x in sb_x..self.can().width {
2338                    let c = if x == sb_x || x == self.can().width - 1 || y == 0 || y == text_h - 1 {
2339                        track_packed
2340                    } else {
2341                        thumb_hi
2342                    };
2343                    self.put_pixel_raw(x, y, c);
2344                }
2345            }
2346            return;
2347        }
2348
2349        let max_offset = total.saturating_sub(self.rows);
2350        let thumb_h = ((text_h * self.rows) / total).max(6);
2351        let avail = text_h.saturating_sub(thumb_h);
2352        // offset 0 = thumb at bottom; max_offset = thumb at top
2353        let thumb_y = if self.scroll_offset == 0 || avail == 0 {
2354            avail // = text_h - thumb_h (bottom)
2355        } else {
2356            avail - (avail * self.scroll_offset / max_offset)
2357        };
2358
2359        for y in 0..text_h {
2360            let in_thumb = y >= thumb_y && y < thumb_y + thumb_h;
2361            let packed = if in_thumb { thumb_packed } else { track_packed };
2362            for x in sb_x..self.can().width {
2363                self.put_pixel_raw(x, y, packed);
2364            }
2365        }
2366    }
2367
2368    /// Redraw the entire text area from the scrollback buffer.
2369    /// Called when scroll_offset changes.
2370    pub(crate) fn redraw_from_scrollback(&mut self) {
2371        if !self.enabled {
2372            return;
2373        }
2374        self.render_viewport_full();
2375    }
2376
2377    pub(crate) fn redraw_from_scrollback_incremental(&mut self, old_offset: usize) -> bool {
2378        if !self.enabled || self.rows == 0 {
2379            return false;
2380        }
2381        let diff = old_offset.abs_diff(self.scroll_offset);
2382        if diff == 0 || diff >= self.rows {
2383            return false;
2384        }
2385        let pixel_delta = diff.saturating_mul(self.font_info.glyph_h);
2386        if pixel_delta == 0 {
2387            return false;
2388        }
2389
2390        let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2391
2392        if self.scroll_offset > old_offset {
2393            self.move_text_view_pixels_down(pixel_delta);
2394            self.redraw_visible_rows(0, diff);
2395        } else {
2396            self.move_text_view_pixels_up(pixel_delta);
2397            self.redraw_visible_rows(self.rows.saturating_sub(diff), diff);
2398        }
2399
2400        self.finalize_live_view_state();
2401        self.draw_scrollbar_inner();
2402        self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2403        true
2404    }
2405
2406    /// Scroll the view up (backward in history) by `lines` lines.
2407    pub fn scroll_view_up(&mut self, lines: usize) {
2408        if !self.enabled {
2409            return;
2410        }
2411        let total = self.sb.rows.len() + 1;
2412        let max_off = total.saturating_sub(self.rows);
2413        self.set_scroll_offset_and_render((self.scroll_offset + lines).min(max_off));
2414    }
2415
2416    /// Scroll the view down (forward, toward live) by `lines` lines.
2417    pub fn scroll_view_down(&mut self, lines: usize) {
2418        if !self.enabled {
2419            return;
2420        }
2421        self.set_scroll_offset_and_render(self.scroll_offset.saturating_sub(lines));
2422    }
2423
2424    /// Immediately return to the live (bottom) view.
2425    pub fn scroll_to_live(&mut self) {
2426        if self.scroll_offset == 0 {
2427            return;
2428        }
2429        self.set_scroll_offset_and_render(0);
2430    }
2431
2432    /// Handle a click at pixel `(px_x, px_y)` : if it falls in the scrollbar,
2433    /// jump the view to the corresponding scroll position.
2434    pub fn scrollbar_click(&mut self, px_x: usize, px_y: usize) {
2435        if !self.enabled {
2436            return;
2437        }
2438        let sb_x = self.can().width.saturating_sub(SCROLLBAR_W);
2439        if px_x < sb_x {
2440            return;
2441        }
2442        let text_h = self.text_area_height();
2443        if text_h <= 1 {
2444            return;
2445        }
2446        let total = self.sb.rows.len() + 1;
2447        let max_off = total.saturating_sub(self.rows);
2448        if max_off == 0 {
2449            return;
2450        }
2451        // py = 0 → top = oldest = max_offset; py = text_h - 1 → bottom = 0
2452        let py = px_y.min(text_h - 1);
2453        let offset = max_off * (text_h - 1 - py) / (text_h - 1);
2454        self.set_scroll_offset_and_render(offset.min(max_off));
2455    }
2456
2457    /// Drag the scrollbar thumb to vertical pixel `px_y`.
2458    ///
2459    /// Unlike `scrollbar_click`, this only depends on Y and is intended for
2460    /// click-and-drag interactions where the pointer may slightly leave the
2461    /// scrollbar strip horizontally.
2462    pub fn scrollbar_drag_to(&mut self, px_y: usize) {
2463        if !self.enabled {
2464            return;
2465        }
2466        let text_h = self.text_area_height();
2467        if text_h <= 1 {
2468            return;
2469        }
2470        let total = self.sb.rows.len() + 1;
2471        let max_off = total.saturating_sub(self.rows);
2472        if max_off == 0 {
2473            return;
2474        }
2475        // py = 0 -> top = oldest = max_offset; py = text_h - 1 -> bottom = 0
2476        let py = px_y.min(text_h - 1);
2477        let offset = max_off * (text_h - 1 - py) / (text_h - 1);
2478        self.set_scroll_offset_and_render(offset.min(max_off));
2479    }
2480
2481    /// Returns `true` if the pixel coordinates fall within the scrollbar strip.
2482    pub fn scrollbar_hit_test(&self, px_x: usize, px_y: usize) -> bool {
2483        if !self.enabled {
2484            return false;
2485        }
2486        let sb_x = self.can().width.saturating_sub(SCROLLBAR_W);
2487        px_x >= sb_x && px_y < self.text_area_height()
2488    }
2489}
2490
2491/// Performs the normalize console char operation.
2492fn normalize_console_char(ch: char) -> char {
2493    match ch {
2494        '\n' | '\r' | '\t' | '\u{8}' => ch,
2495        c if c.is_control() => '\0',
2496        // Graceful fallback when font lacks box-drawing coverage.
2497        '\u{2500}' | '\u{2501}' | '\u{2504}' | '\u{2505}' | '\u{2013}' | '\u{2014}' => '-',
2498        '\u{2502}' | '\u{2503}' => '|',
2499        '\u{250c}' | '\u{2510}' | '\u{2514}' | '\u{2518}' | '\u{251c}' | '\u{2524}'
2500        | '\u{252c}' | '\u{2534}' | '\u{253c}' => '+',
2501        '\u{00a0}' => ' ',
2502        _ => ch,
2503    }
2504}
2505
2506impl fmt::Write for VgaWriter {
2507    /// Writes str.
2508    fn write_str(&mut self, s: &str) -> fmt::Result {
2509        if self.enabled {
2510            self.write_bytes(s);
2511        } else {
2512            crate::arch::x86_64::serial::_print(format_args!("{}", s));
2513        }
2514        Ok(())
2515    }
2516}