Skip to main content

strat9_kernel/arch/x86_64/
vga.rs

1//! Framebuffer text console (Limine framebuffer + PSF font).
2//! https://en.wikipedia.org/wiki/PC_Screen_Font
3//!
4//! Keeps the existing `vga_print!` / `vga_println!` API but renders text into
5//! the graphical framebuffer when available. Falls back to serial otherwise.
6
7use alloc::{collections::VecDeque, format, string::String, vec::Vec};
8use core::{
9    fmt,
10    sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering},
11};
12use spin::Mutex;
13
14/// Whether framebuffer console is available.
15static VGA_AVAILABLE: AtomicBool = AtomicBool::new(false);
16static STATUS_LAST_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
17const STATUS_REFRESH_PERIOD_TICKS: u64 = 100; // 100Hz timer => 1s
18static PRESENTED_FRAMES: AtomicU64 = AtomicU64::new(0);
19static FPS_LAST_TICK: AtomicU64 = AtomicU64::new(0);
20static FPS_LAST_FRAME_COUNT: AtomicU64 = AtomicU64::new(0);
21static FPS_ESTIMATE: AtomicU64 = AtomicU64::new(0);
22static DOUBLE_BUFFER_MODE: AtomicBool = AtomicBool::new(false);
23static UI_SCALE: AtomicU8 = AtomicU8::new(1);
24
25const FONT_PSF: &[u8] = include_bytes!("fonts/zap-ext-light20.psf");
26
27/// VGA colors mapped to RGB for text rendering.
28#[allow(dead_code)]
29#[derive(Debug, Clone, Copy, PartialEq)]
30#[repr(u8)]
31pub enum Color {
32    Black = 0x0,
33    Blue = 0x1,
34    Green = 0x2,
35    Cyan = 0x3,
36    Red = 0x4,
37    Magenta = 0x5,
38    Brown = 0x6,
39    LightGrey = 0x7,
40    DarkGrey = 0x8,
41    LightBlue = 0x9,
42    LightGreen = 0xA,
43    LightCyan = 0xB,
44    LightRed = 0xC,
45    LightMagenta = 0xD,
46    Yellow = 0xE,
47    White = 0xF,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct RgbColor {
52    pub r: u8,
53    pub g: u8,
54    pub b: u8,
55}
56
57impl RgbColor {
58    /// Creates a new instance.
59    pub const fn new(r: u8, g: u8, b: u8) -> Self {
60        Self { r, g, b }
61    }
62
63    pub const BLACK: Self = Self::new(0x00, 0x00, 0x00);
64    pub const WHITE: Self = Self::new(0xFF, 0xFF, 0xFF);
65    pub const RED: Self = Self::new(0xFF, 0x00, 0x00);
66    pub const GREEN: Self = Self::new(0x00, 0xFF, 0x00);
67    pub const BLUE: Self = Self::new(0x00, 0x00, 0xFF);
68    pub const CYAN: Self = Self::new(0x00, 0xFF, 0xFF);
69    pub const MAGENTA: Self = Self::new(0xFF, 0x00, 0xFF);
70    pub const YELLOW: Self = Self::new(0xFF, 0xFF, 0x00);
71    pub const LIGHT_GREY: Self = Self::new(0xAA, 0xAA, 0xAA);
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub enum TextAlign {
76    Left,
77    Center,
78    Right,
79}
80
81#[derive(Debug, Clone, Copy)]
82pub struct TextOptions {
83    pub fg: RgbColor,
84    pub bg: RgbColor,
85    pub align: TextAlign,
86    pub wrap: bool,
87    pub max_width: Option<usize>,
88}
89
90impl TextOptions {
91    /// Creates a new instance.
92    pub const fn new(fg: RgbColor, bg: RgbColor) -> Self {
93        Self {
94            fg,
95            bg,
96            align: TextAlign::Left,
97            wrap: false,
98            max_width: None,
99        }
100    }
101}
102
103#[derive(Debug, Clone, Copy)]
104pub struct TextMetrics {
105    pub width: usize,
106    pub height: usize,
107    pub lines: usize,
108}
109
110#[derive(Debug, Clone, Copy)]
111pub struct SpriteRgba<'a> {
112    pub width: usize,
113    pub height: usize,
114    pub pixels: &'a [u8],
115}
116
117#[derive(Debug, Clone, Copy)]
118pub struct UiTheme {
119    pub background: RgbColor,
120    pub panel_bg: RgbColor,
121    pub panel_border: RgbColor,
122    pub text: RgbColor,
123    pub accent: RgbColor,
124    pub status_bg: RgbColor,
125    pub status_text: RgbColor,
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum UiScale {
130    Compact = 1,
131    Normal = 2,
132    Large = 3,
133}
134
135impl UiScale {
136    /// Performs the factor operation.
137    pub const fn factor(self) -> usize {
138        self as usize
139    }
140}
141
142impl UiTheme {
143    pub const SLATE: Self = Self {
144        background: RgbColor::new(0x12, 0x16, 0x1E),
145        panel_bg: RgbColor::new(0x1A, 0x22, 0x2C),
146        panel_border: RgbColor::new(0x3D, 0x52, 0x66),
147        text: RgbColor::new(0xE2, 0xE8, 0xF0),
148        accent: RgbColor::new(0x4F, 0xB3, 0xB3),
149        status_bg: RgbColor::new(0x0E, 0x13, 0x1A),
150        status_text: RgbColor::new(0xD3, 0xDE, 0xEA),
151    };
152
153    pub const SAND: Self = Self {
154        background: RgbColor::new(0xFA, 0xF6, 0xEF),
155        panel_bg: RgbColor::new(0xF1, 0xE8, 0xD8),
156        panel_border: RgbColor::new(0xA6, 0x8F, 0x6A),
157        text: RgbColor::new(0x2B, 0x2B, 0x2B),
158        accent: RgbColor::new(0x1F, 0x7A, 0x8C),
159        status_bg: RgbColor::new(0xE6, 0xD7, 0xBF),
160        status_text: RgbColor::new(0x2B, 0x2B, 0x2B),
161    };
162
163    pub const OCEAN_STATUS: Self = Self {
164        background: RgbColor::new(0x12, 0x16, 0x1E),
165        panel_bg: RgbColor::new(0x1A, 0x22, 0x2C),
166        panel_border: RgbColor::new(0x3D, 0x52, 0x66),
167        text: RgbColor::new(0xE2, 0xE8, 0xF0),
168        accent: RgbColor::new(0x4F, 0xB3, 0xB3),
169        status_bg: RgbColor::new(0x1B, 0x4D, 0x8A),
170        status_text: RgbColor::new(0xF5, 0xFA, 0xFF),
171    };
172}
173
174#[derive(Debug, Clone)]
175struct StatusLineInfo {
176    hostname: String,
177    ip: String,
178}
179
180static STATUS_LINE_INFO: Mutex<Option<StatusLineInfo>> = Mutex::new(None);
181
182#[derive(Debug, Clone, Copy, Default)]
183pub struct UiRect {
184    pub x: usize,
185    pub y: usize,
186    pub w: usize,
187    pub h: usize,
188}
189
190impl UiRect {
191    /// Creates a new instance.
192    pub const fn new(x: usize, y: usize, w: usize, h: usize) -> Self {
193        Self { x, y, w, h }
194    }
195}
196
197#[derive(Debug, Clone, Copy)]
198pub enum DockEdge {
199    Top,
200    Bottom,
201    Left,
202    Right,
203}
204
205#[derive(Debug, Clone, Copy)]
206pub struct UiDockLayout {
207    remaining: UiRect,
208}
209
210impl UiDockLayout {
211    /// Builds this from screen.
212    pub fn from_screen() -> Self {
213        Self {
214            remaining: UiRect::new(0, 0, width(), height()),
215        }
216    }
217
218    /// Builds this from rect.
219    pub const fn from_rect(rect: UiRect) -> Self {
220        Self { remaining: rect }
221    }
222
223    /// Performs the remaining operation.
224    pub const fn remaining(&self) -> UiRect {
225        self.remaining
226    }
227
228    /// Performs the dock operation.
229    pub fn dock(&mut self, edge: DockEdge, size: usize) -> UiRect {
230        match edge {
231            DockEdge::Top => {
232                let h = core::cmp::min(size, self.remaining.h);
233                let out = UiRect::new(self.remaining.x, self.remaining.y, self.remaining.w, h);
234                self.remaining.y = self.remaining.y.saturating_add(h);
235                self.remaining.h = self.remaining.h.saturating_sub(h);
236                out
237            }
238            DockEdge::Bottom => {
239                let h = core::cmp::min(size, self.remaining.h);
240                let y = self
241                    .remaining
242                    .y
243                    .saturating_add(self.remaining.h.saturating_sub(h));
244                let out = UiRect::new(self.remaining.x, y, self.remaining.w, h);
245                self.remaining.h = self.remaining.h.saturating_sub(h);
246                out
247            }
248            DockEdge::Left => {
249                let w = core::cmp::min(size, self.remaining.w);
250                let out = UiRect::new(self.remaining.x, self.remaining.y, w, self.remaining.h);
251                self.remaining.x = self.remaining.x.saturating_add(w);
252                self.remaining.w = self.remaining.w.saturating_sub(w);
253                out
254            }
255            DockEdge::Right => {
256                let w = core::cmp::min(size, self.remaining.w);
257                let x = self
258                    .remaining
259                    .x
260                    .saturating_add(self.remaining.w.saturating_sub(w));
261                let out = UiRect::new(x, self.remaining.y, w, self.remaining.h);
262                self.remaining.w = self.remaining.w.saturating_sub(w);
263                out
264            }
265        }
266    }
267}
268
269#[derive(Debug, Clone)]
270pub struct UiLabel<'a> {
271    pub rect: UiRect,
272    pub text: &'a str,
273    pub fg: RgbColor,
274    pub bg: RgbColor,
275    pub align: TextAlign,
276}
277
278#[derive(Debug, Clone)]
279pub struct UiPanel<'a> {
280    pub rect: UiRect,
281    pub title: &'a str,
282    pub body: &'a str,
283    pub theme: UiTheme,
284}
285
286#[derive(Debug, Clone, Copy)]
287pub struct UiProgressBar {
288    pub rect: UiRect,
289    pub value: u8, // 0..=100
290    pub fg: RgbColor,
291    pub bg: RgbColor,
292    pub border: RgbColor,
293}
294
295#[derive(Debug, Clone)]
296pub struct UiTable {
297    pub rect: UiRect,
298    pub headers: Vec<String>,
299    pub rows: Vec<Vec<String>>,
300    pub theme: UiTheme,
301}
302
303#[derive(Debug, Clone)]
304struct TerminalLine {
305    text: String,
306    fg: RgbColor,
307}
308
309#[derive(Debug, Clone)]
310pub struct TerminalWidget {
311    pub rect: UiRect,
312    pub title: String,
313    pub fg: RgbColor,
314    pub bg: RgbColor,
315    pub border: RgbColor,
316    pub max_lines: usize,
317    lines: VecDeque<TerminalLine>,
318}
319
320impl TerminalWidget {
321    /// Creates a new instance.
322    pub fn new(rect: UiRect, max_lines: usize) -> Self {
323        Self {
324            rect,
325            title: String::from("Terminal"),
326            fg: RgbColor::LIGHT_GREY,
327            bg: RgbColor::new(0x0F, 0x14, 0x1B),
328            border: RgbColor::new(0x3D, 0x52, 0x66),
329            max_lines: core::cmp::max(1, max_lines),
330            lines: VecDeque::new(),
331        }
332    }
333
334    /// Performs the push line operation.
335    pub fn push_line(&mut self, text: &str) {
336        self.push_colored_line(text, self.fg);
337    }
338
339    /// Performs the push ansi line operation.
340    pub fn push_ansi_line(&mut self, text: &str) {
341        let (fg, stripped) = parse_ansi_color_prefix(text, self.fg);
342        self.push_colored_line(&stripped, fg);
343    }
344
345    /// Performs the push colored line operation.
346    fn push_colored_line(&mut self, text: &str, fg: RgbColor) {
347        if self.lines.len() >= self.max_lines {
348            self.lines.pop_front();
349        }
350        self.lines.push_back(TerminalLine {
351            text: String::from(text),
352            fg,
353        });
354    }
355
356    /// Performs the clear operation.
357    pub fn clear(&mut self) {
358        self.lines.clear();
359    }
360
361    /// Performs the draw operation.
362    pub fn draw(&self) {
363        let _ = with_writer(|w| {
364            if self.rect.w < 8 || self.rect.h < 8 {
365                return;
366            }
367            let (gw, gh) = w.glyph_size();
368            if gw == 0 || gh == 0 {
369                return;
370            }
371
372            w.fill_rect(self.rect.x, self.rect.y, self.rect.w, self.rect.h, self.bg);
373            w.draw_rect(
374                self.rect.x,
375                self.rect.y,
376                self.rect.w,
377                self.rect.h,
378                self.border,
379            );
380
381            let title_h = gh + 2;
382            w.fill_rect(
383                self.rect.x + 1,
384                self.rect.y + 1,
385                self.rect.w.saturating_sub(2),
386                title_h,
387                self.border,
388            );
389            w.draw_text(
390                self.rect.x + 4,
391                self.rect.y + 1,
392                &self.title,
393                TextOptions {
394                    fg: RgbColor::WHITE,
395                    bg: self.border,
396                    align: TextAlign::Left,
397                    wrap: false,
398                    max_width: Some(self.rect.w.saturating_sub(8)),
399                },
400            );
401
402            let content_y = self.rect.y + title_h + 2;
403            let content_h = self.rect.h.saturating_sub(title_h + 3);
404            let rows = core::cmp::max(1, content_h / gh);
405            let start = self.lines.len().saturating_sub(rows);
406
407            for (idx, line) in self.lines.iter().skip(start).enumerate() {
408                let y = content_y + idx * gh;
409                w.draw_text(
410                    self.rect.x + 4,
411                    y,
412                    &line.text,
413                    TextOptions {
414                        fg: line.fg,
415                        bg: self.bg,
416                        align: TextAlign::Left,
417                        wrap: false,
418                        max_width: Some(self.rect.w.saturating_sub(8)),
419                    },
420                );
421            }
422        });
423    }
424}
425
426/// Parses ansi color prefix.
427fn parse_ansi_color_prefix(input: &str, default_fg: RgbColor) -> (RgbColor, String) {
428    let bytes = input.as_bytes();
429    if !bytes.starts_with(b"\x1b[") {
430        return (default_fg, String::from(input));
431    }
432    let Some(mpos) = bytes.iter().position(|b| *b == b'm') else {
433        return (default_fg, String::from(input));
434    };
435    let code = &input[2..mpos];
436    let rest = &input[mpos + 1..];
437    let fg = match code {
438        "30" => RgbColor::BLACK,
439        "31" => RgbColor::new(0xFF, 0x55, 0x55),
440        "32" => RgbColor::new(0x66, 0xFF, 0x66),
441        "33" => RgbColor::new(0xFF, 0xDD, 0x66),
442        "34" => RgbColor::new(0x77, 0xAA, 0xFF),
443        "35" => RgbColor::new(0xFF, 0x77, 0xFF),
444        "36" => RgbColor::new(0x77, 0xFF, 0xFF),
445        "37" | "0" => RgbColor::LIGHT_GREY,
446        _ => default_fg,
447    };
448    (fg, String::from(rest))
449}
450
451/// Performs the color to rgb operation.
452#[inline]
453fn color_to_rgb(c: Color) -> (u8, u8, u8) {
454    match c {
455        Color::Black => (0x00, 0x00, 0x00),
456        Color::Blue => (0x00, 0x00, 0xAA),
457        Color::Green => (0x00, 0xAA, 0x00),
458        Color::Cyan => (0x00, 0xAA, 0xAA),
459        Color::Red => (0xAA, 0x00, 0x00),
460        Color::Magenta => (0xAA, 0x00, 0xAA),
461        Color::Brown => (0xAA, 0x55, 0x00),
462        Color::LightGrey => (0xAA, 0xAA, 0xAA),
463        Color::DarkGrey => (0x55, 0x55, 0x55),
464        Color::LightBlue => (0x55, 0x55, 0xFF),
465        Color::LightGreen => (0x55, 0xFF, 0x55),
466        Color::LightCyan => (0x55, 0xFF, 0xFF),
467        Color::LightRed => (0xFF, 0x55, 0x55),
468        Color::LightMagenta => (0xFF, 0x55, 0xFF),
469        Color::Yellow => (0xFF, 0xFF, 0x55),
470        Color::White => (0xFF, 0xFF, 0xFF),
471    }
472}
473
474impl From<Color> for RgbColor {
475    /// Performs the from operation.
476    fn from(value: Color) -> Self {
477        let (r, g, b) = color_to_rgb(value);
478        Self::new(r, g, b)
479    }
480}
481
482#[derive(Clone, Copy)]
483struct PixelFormat {
484    bpp: u16,
485    red_size: u8,
486    red_shift: u8,
487    green_size: u8,
488    green_shift: u8,
489    blue_size: u8,
490    blue_shift: u8,
491}
492
493#[derive(Debug, Clone, Copy)]
494pub struct FramebufferInfo {
495    pub available: bool,
496    pub width: usize,
497    pub height: usize,
498    pub pitch: usize,
499    pub bpp: u16,
500    pub red_size: u8,
501    pub red_shift: u8,
502    pub green_size: u8,
503    pub green_shift: u8,
504    pub blue_size: u8,
505    pub blue_shift: u8,
506    pub text_cols: usize,
507    pub text_rows: usize,
508    pub glyph_w: usize,
509    pub glyph_h: usize,
510    pub double_buffer_mode: bool,
511    pub double_buffer_enabled: bool,
512    pub ui_scale: UiScale,
513}
514
515impl PixelFormat {
516    /// Performs the pack rgb operation.
517    fn pack_rgb(&self, r: u8, g: u8, b: u8) -> u32 {
518        /// Performs the scale operation.
519        fn scale(v: u8, bits: u8) -> u32 {
520            if bits == 0 {
521                0
522            } else if bits >= 8 {
523                (v as u32) << (bits - 8)
524            } else {
525                (v as u32) >> (8 - bits)
526            }
527        }
528
529        (scale(r, self.red_size) << self.red_shift)
530            | (scale(g, self.green_size) << self.green_shift)
531            | (scale(b, self.blue_size) << self.blue_shift)
532    }
533}
534
535struct FontInfo {
536    glyph_count: usize,
537    bytes_per_glyph: usize,
538    glyph_w: usize,
539    glyph_h: usize,
540    data_offset: usize,
541    unicode_table_offset: Option<usize>,
542}
543
544#[derive(Clone, Copy)]
545struct ClipRect {
546    x: usize,
547    y: usize,
548    w: usize,
549    h: usize,
550}
551
552/// Parses psf.
553fn parse_psf(font: &[u8]) -> Option<FontInfo> {
554    // PSF1
555    if font.len() >= 4 && font[0] == 0x36 && font[1] == 0x04 {
556        let mode = font[2];
557        let glyph_count = if (mode & 0x01) != 0 { 512 } else { 256 };
558        let glyph_h = font[3] as usize;
559        let bytes_per_glyph = glyph_h;
560        return Some(FontInfo {
561            glyph_count,
562            bytes_per_glyph,
563            glyph_w: 8,
564            glyph_h,
565            data_offset: 4,
566            unicode_table_offset: None,
567        });
568    }
569
570    // PSF2
571    if font.len() >= 32 && font[0] == 0x72 && font[1] == 0xB5 && font[2] == 0x4A && font[3] == 0x86
572    {
573        let rd_u32 = |off: usize| -> u32 {
574            u32::from_le_bytes([font[off], font[off + 1], font[off + 2], font[off + 3]])
575        };
576        let headersize = rd_u32(8) as usize;
577        let flags = rd_u32(12);
578        let glyph_count = rd_u32(16) as usize;
579        let bytes_per_glyph = rd_u32(20) as usize;
580        let glyph_h = rd_u32(24) as usize;
581        let glyph_w = rd_u32(28) as usize;
582        let glyph_bytes = glyph_count.saturating_mul(bytes_per_glyph);
583        let unicode_table_offset = if (flags & 1) != 0 {
584            Some(headersize.saturating_add(glyph_bytes))
585        } else {
586            None
587        };
588        return Some(FontInfo {
589            glyph_count,
590            bytes_per_glyph,
591            glyph_w,
592            glyph_h,
593            data_offset: headersize,
594            unicode_table_offset,
595        });
596    }
597
598    None
599}
600
601/// Performs the decode utf8 at operation.
602fn decode_utf8_at(bytes: &[u8], pos: usize) -> Option<(u32, usize)> {
603    let b0 = *bytes.get(pos)?;
604    if b0 < 0x80 {
605        return Some((b0 as u32, 1));
606    }
607    if (b0 & 0xE0) == 0xC0 {
608        let b1 = *bytes.get(pos + 1)?;
609        if (b1 & 0xC0) != 0x80 {
610            return None;
611        }
612        let cp = (((b0 & 0x1F) as u32) << 6) | ((b1 & 0x3F) as u32);
613        return Some((cp, 2));
614    }
615    if (b0 & 0xF0) == 0xE0 {
616        let b1 = *bytes.get(pos + 1)?;
617        let b2 = *bytes.get(pos + 2)?;
618        if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 {
619            return None;
620        }
621        let cp = (((b0 & 0x0F) as u32) << 12) | (((b1 & 0x3F) as u32) << 6) | ((b2 & 0x3F) as u32);
622        return Some((cp, 3));
623    }
624    if (b0 & 0xF8) == 0xF0 {
625        let b1 = *bytes.get(pos + 1)?;
626        let b2 = *bytes.get(pos + 2)?;
627        let b3 = *bytes.get(pos + 3)?;
628        if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80 {
629            return None;
630        }
631        let cp = (((b0 & 0x07) as u32) << 18)
632            | (((b1 & 0x3F) as u32) << 12)
633            | (((b2 & 0x3F) as u32) << 6)
634            | ((b3 & 0x3F) as u32);
635        return Some((cp, 4));
636    }
637    None
638}
639
640/// Parses psf2 unicode map.
641fn parse_psf2_unicode_map(font: &[u8], info: &FontInfo) -> Vec<(u32, usize)> {
642    let Some(mut i) = info.unicode_table_offset else {
643        return Vec::new();
644    };
645    if i >= font.len() {
646        return Vec::new();
647    }
648
649    let mut map = Vec::new();
650    for glyph in 0..info.glyph_count {
651        while i < font.len() {
652            let b = font[i];
653            if b == 0xFF {
654                i += 1;
655                break;
656            }
657            if b == 0xFE {
658                // PSF2 sequence marker; skip marker and continue parsing bytes until glyph separator.
659                i += 1;
660                continue;
661            }
662            if let Some((cp, adv)) = decode_utf8_at(font, i) {
663                if !map.iter().any(|(u, _)| *u == cp) {
664                    map.push((cp, glyph));
665                }
666                i += adv;
667            } else {
668                i += 1;
669            }
670        }
671    }
672
673    map
674}
675
676/// Width of the scrollbar strip on the right edge of the text console.
677const SCROLLBAR_W: usize = 12;
678/// Maximum number of completed lines kept in the scrollback history.
679const MAX_SCROLLBACK: usize = 500;
680
681// ── Mouse cursor (X arrow, 12×16) ─────────────────────────────────────────
682const CURSOR_W: usize = 12;
683const CURSOR_H: usize = 16;
684// 0=transparent, 1=black outline, 2=white fill
685#[rustfmt::skip]
686const CURSOR_PIXELS: [u8; CURSOR_W * CURSOR_H] = [
687    1,0,0,0,0,0,0,0,0,0,0,0,
688    1,1,0,0,0,0,0,0,0,0,0,0,
689    1,2,1,0,0,0,0,0,0,0,0,0,
690    1,2,2,1,0,0,0,0,0,0,0,0,
691    1,2,2,2,1,0,0,0,0,0,0,0,
692    1,2,2,2,2,1,0,0,0,0,0,0,
693    1,2,2,2,2,2,1,0,0,0,0,0,
694    1,2,2,2,2,2,2,1,0,0,0,0,
695    1,2,2,2,2,2,2,2,1,0,0,0,
696    1,2,2,2,2,2,1,1,0,0,0,0,
697    1,2,2,2,1,0,0,0,0,0,0,0,
698    1,2,1,1,2,2,1,0,0,0,0,0,
699    1,1,0,0,1,2,2,1,0,0,0,0,
700    0,0,0,0,0,1,2,1,0,0,0,0,
701    0,0,0,0,0,0,1,1,0,0,0,0,
702    0,0,0,0,0,0,0,0,0,0,0,0,
703];
704
705// ── Selection clipboard ────────────────────────────────────────────────────
706const CLIPBOARD_CAP: usize = 8192;
707static CLIPBOARD: spin::Mutex<([u8; CLIPBOARD_CAP], usize)> =
708    spin::Mutex::new(([0u8; CLIPBOARD_CAP], 0));
709
710/// A single character cell stored in the scrollback buffer.
711#[derive(Clone, Copy)]
712struct SbCell {
713    ch: char,
714    fg: u32, // packed pixel color
715    bg: u32, // packed pixel color
716}
717
718pub struct VgaWriter {
719    enabled: bool,
720    fb_addr: *mut u8,
721    fb_width: usize,
722    fb_height: usize,
723    pitch: usize,
724    fmt: PixelFormat,
725
726    cols: usize,
727    rows: usize,
728    col: usize,
729    row: usize,
730
731    fg: u32,
732    bg: u32,
733
734    font: &'static [u8],
735    font_info: FontInfo,
736    unicode_map: Vec<(u32, usize)>,
737    status_bar_height: usize,
738    clip: ClipRect,
739    back_buffer: Option<Vec<u32>>,
740    draw_to_back: bool,
741    dirty_rect: Option<ClipRect>,
742    track_dirty: bool,
743
744    // ── Scrollback buffer ────────────────────────────────────────────────────
745    /// Completed screen rows kept for history scrolling.
746    sb_rows: VecDeque<Vec<SbCell>>,
747    /// Current (incomplete) row being assembled by write_char.
748    sb_cur_row: Vec<SbCell>,
749    /// Lines scrolled back from the bottom (0 = live view).
750    scroll_offset: usize,
751
752    // ── Mouse cursor ──────────────────────────────────────────────────────────
753    mc_x: i32,
754    mc_y: i32,
755    mc_visible: bool,
756    mc_save: [u32; CURSOR_W * CURSOR_H],
757
758    // ── Text selection ────────────────────────────────────────────────────────
759    sel_active: bool,
760    sel_start_row: usize,
761    sel_start_col: usize,
762    sel_end_row: usize,
763    sel_end_col: usize,
764}
765
766unsafe impl Send for VgaWriter {}
767
768impl VgaWriter {
769    /// Creates a new instance.
770    pub const fn new() -> Self {
771        Self {
772            enabled: false,
773            fb_addr: core::ptr::null_mut(),
774            fb_width: 0,
775            fb_height: 0,
776            pitch: 0,
777            fmt: PixelFormat {
778                bpp: 0,
779                red_size: 0,
780                red_shift: 0,
781                green_size: 0,
782                green_shift: 0,
783                blue_size: 0,
784                blue_shift: 0,
785            },
786            cols: 0,
787            rows: 0,
788            col: 0,
789            row: 0,
790            fg: 0,
791            bg: 0,
792            font: &[],
793            font_info: FontInfo {
794                glyph_count: 0,
795                bytes_per_glyph: 0,
796                glyph_w: 8,
797                glyph_h: 16,
798                data_offset: 0,
799                unicode_table_offset: None,
800            },
801            unicode_map: Vec::new(),
802            status_bar_height: 0,
803            clip: ClipRect {
804                x: 0,
805                y: 0,
806                w: 0,
807                h: 0,
808            },
809            back_buffer: None,
810            draw_to_back: false,
811            dirty_rect: None,
812            track_dirty: false,
813            sb_rows: VecDeque::new(),
814            sb_cur_row: Vec::new(),
815            scroll_offset: 0,
816            mc_x: 0,
817            mc_y: 0,
818            mc_visible: false,
819            mc_save: [0u32; CURSOR_W * CURSOR_H],
820            sel_active: false,
821            sel_start_row: 0,
822            sel_start_col: 0,
823            sel_end_row: 0,
824            sel_end_col: 0,
825        }
826    }
827
828    /// Performs the configure operation.
829    fn configure(
830        &mut self,
831        fb_addr: *mut u8,
832        fb_width: usize,
833        fb_height: usize,
834        pitch: usize,
835        fmt: PixelFormat,
836    ) -> bool {
837        let Some(font_info) = parse_psf(FONT_PSF) else {
838            return false;
839        };
840        let status_bar_height = font_info.glyph_h;
841        let text_height = fb_height.saturating_sub(status_bar_height);
842        let cols = fb_width / font_info.glyph_w;
843        let rows = text_height / font_info.glyph_h;
844        if cols == 0 || rows == 0 {
845            return false;
846        }
847        let (fr, fg, fb) = color_to_rgb(Color::LightGrey);
848        let (br, bg, bb) = color_to_rgb(Color::Black);
849
850        self.enabled = true;
851        self.fb_addr = fb_addr;
852        self.fb_width = fb_width;
853        self.fb_height = fb_height;
854        self.pitch = pitch;
855        self.fmt = fmt;
856        self.cols = cols;
857        self.rows = rows;
858        self.col = 0;
859        self.row = 0;
860        self.fg = fmt.pack_rgb(fr, fg, fb);
861        self.bg = fmt.pack_rgb(br, bg, bb);
862        self.font = FONT_PSF;
863        self.font_info = font_info;
864        self.unicode_map = parse_psf2_unicode_map(FONT_PSF, &self.font_info);
865        self.status_bar_height = status_bar_height;
866        self.clip = ClipRect {
867            x: 0,
868            y: 0,
869            w: fb_width,
870            h: fb_height,
871        };
872        self.back_buffer = None;
873        self.draw_to_back = false;
874        self.dirty_rect = None;
875        self.track_dirty = false;
876        self.sb_rows = VecDeque::new();
877        self.sb_cur_row = Vec::new();
878        self.scroll_offset = 0;
879        self.mc_visible = false;
880        self.sel_active = false;
881        true
882    }
883
884    /// Performs the pack color operation.
885    #[inline]
886    fn pack_color(&self, color: RgbColor) -> u32 {
887        self.fmt.pack_rgb(color.r, color.g, color.b)
888    }
889
890    /// Performs the unpack color operation.
891    fn unpack_color(&self, value: u32) -> RgbColor {
892        /// Performs the unscale operation.
893        fn unscale(v: u32, bits: u8) -> u8 {
894            if bits == 0 {
895                return 0;
896            }
897            let max = (1u32 << bits) - 1;
898            ((v * 255) / max) as u8
899        }
900
901        let r = unscale(
902            (value >> self.fmt.red_shift) & ((1u32 << self.fmt.red_size) - 1),
903            self.fmt.red_size,
904        );
905        let g = unscale(
906            (value >> self.fmt.green_shift) & ((1u32 << self.fmt.green_size) - 1),
907            self.fmt.green_size,
908        );
909        let b = unscale(
910            (value >> self.fmt.blue_shift) & ((1u32 << self.fmt.blue_size) - 1),
911            self.fmt.blue_size,
912        );
913        RgbColor::new(r, g, b)
914    }
915
916    /// Sets color.
917    pub fn set_color(&mut self, fg: Color, bg: Color) {
918        self.set_rgb_color(fg.into(), bg.into());
919    }
920
921    /// Sets rgb color.
922    pub fn set_rgb_color(&mut self, fg: RgbColor, bg: RgbColor) {
923        self.fg = self.pack_color(fg);
924        self.bg = self.pack_color(bg);
925    }
926
927    /// Performs the text colors operation.
928    pub fn text_colors(&self) -> (RgbColor, RgbColor) {
929        (self.unpack_color(self.fg), self.unpack_color(self.bg))
930    }
931
932    /// Performs the width operation.
933    pub fn width(&self) -> usize {
934        self.fb_width
935    }
936
937    /// Performs the height operation.
938    pub fn height(&self) -> usize {
939        self.fb_height
940    }
941
942    /// Performs the cols operation.
943    pub fn cols(&self) -> usize {
944        self.cols
945    }
946
947    /// Performs the rows operation.
948    pub fn rows(&self) -> usize {
949        self.rows
950    }
951
952    /// Performs the glyph size operation.
953    pub fn glyph_size(&self) -> (usize, usize) {
954        (self.font_info.glyph_w, self.font_info.glyph_h)
955    }
956
957    /// Sets cursor cell.
958    pub fn set_cursor_cell(&mut self, col: usize, row: usize) {
959        if !self.enabled || self.cols == 0 || self.rows == 0 {
960            return;
961        }
962        self.col = core::cmp::min(col, self.cols - 1);
963        self.row = core::cmp::min(row, self.rows - 1);
964    }
965
966    /// Performs the text area height operation.
967    fn text_area_height(&self) -> usize {
968        self.fb_height.saturating_sub(self.status_bar_height)
969    }
970
971    /// Performs the enabled operation.
972    pub fn enabled(&self) -> bool {
973        self.enabled
974    }
975
976    /// Performs the framebuffer info operation.
977    pub fn framebuffer_info(&self) -> FramebufferInfo {
978        FramebufferInfo {
979            available: self.enabled,
980            width: self.fb_width,
981            height: self.fb_height,
982            pitch: self.pitch,
983            bpp: self.fmt.bpp,
984            red_size: self.fmt.red_size,
985            red_shift: self.fmt.red_shift,
986            green_size: self.fmt.green_size,
987            green_shift: self.fmt.green_shift,
988            blue_size: self.fmt.blue_size,
989            blue_shift: self.fmt.blue_shift,
990            text_cols: self.cols,
991            text_rows: self.rows,
992            glyph_w: self.font_info.glyph_w,
993            glyph_h: self.font_info.glyph_h,
994            double_buffer_mode: DOUBLE_BUFFER_MODE.load(Ordering::Relaxed),
995            double_buffer_enabled: self.draw_to_back && self.back_buffer.is_some(),
996            ui_scale: current_ui_scale(),
997        }
998    }
999
1000    /// Performs the in clip operation.
1001    #[inline]
1002    fn in_clip(&self, x: usize, y: usize) -> bool {
1003        x >= self.clip.x
1004            && y >= self.clip.y
1005            && x < self.clip.x.saturating_add(self.clip.w)
1006            && y < self.clip.y.saturating_add(self.clip.h)
1007    }
1008
1009    /// Performs the clipped rect operation.
1010    fn clipped_rect(
1011        &self,
1012        x: usize,
1013        y: usize,
1014        width: usize,
1015        height: usize,
1016    ) -> Option<(usize, usize, usize, usize)> {
1017        if width == 0 || height == 0 || !self.enabled {
1018            return None;
1019        }
1020        let src_x2 = core::cmp::min(x.saturating_add(width), self.fb_width);
1021        let src_y2 = core::cmp::min(y.saturating_add(height), self.fb_height);
1022        let clip_x2 = self.clip.x.saturating_add(self.clip.w);
1023        let clip_y2 = self.clip.y.saturating_add(self.clip.h);
1024
1025        let sx = core::cmp::max(x, self.clip.x);
1026        let sy = core::cmp::max(y, self.clip.y);
1027        let ex = core::cmp::min(src_x2, clip_x2);
1028        let ey = core::cmp::min(src_y2, clip_y2);
1029        if ex <= sx || ey <= sy {
1030            return None;
1031        }
1032        Some((sx, sy, ex - sx, ey - sy))
1033    }
1034
1035    /// Performs the clear dirty operation.
1036    fn clear_dirty(&mut self) {
1037        self.dirty_rect = None;
1038    }
1039
1040    /// Performs the mark dirty rect operation.
1041    fn mark_dirty_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
1042        if !self.track_dirty {
1043            return;
1044        }
1045        let Some((sx, sy, sw, sh)) = self.clipped_rect(x, y, width, height) else {
1046            return;
1047        };
1048        let next = ClipRect {
1049            x: sx,
1050            y: sy,
1051            w: sw,
1052            h: sh,
1053        };
1054        self.dirty_rect = Some(match self.dirty_rect {
1055            None => next,
1056            Some(cur) => {
1057                let x0 = core::cmp::min(cur.x, next.x);
1058                let y0 = core::cmp::min(cur.y, next.y);
1059                let x1 = core::cmp::max(cur.x.saturating_add(cur.w), next.x.saturating_add(next.w));
1060                let y1 = core::cmp::max(cur.y.saturating_add(cur.h), next.y.saturating_add(next.h));
1061                ClipRect {
1062                    x: x0,
1063                    y: y0,
1064                    w: x1.saturating_sub(x0),
1065                    h: y1.saturating_sub(y0),
1066                }
1067            }
1068        });
1069    }
1070
1071    /// Sets clip rect.
1072    pub fn set_clip_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
1073        let x_end = core::cmp::min(x.saturating_add(width), self.fb_width);
1074        let y_end = core::cmp::min(y.saturating_add(height), self.fb_height);
1075        self.clip = ClipRect {
1076            x,
1077            y,
1078            w: x_end.saturating_sub(x),
1079            h: y_end.saturating_sub(y),
1080        };
1081    }
1082
1083    /// Performs the reset clip rect operation.
1084    pub fn reset_clip_rect(&mut self) {
1085        self.clip = ClipRect {
1086            x: 0,
1087            y: 0,
1088            w: self.fb_width,
1089            h: self.fb_height,
1090        };
1091    }
1092
1093    /// Performs the draw to back buffer operation.
1094    fn draw_to_back_buffer(&self) -> bool {
1095        self.draw_to_back && self.back_buffer.is_some()
1096    }
1097
1098    /// Enables double buffer.
1099    pub fn enable_double_buffer(&mut self) -> bool {
1100        if !self.enabled {
1101            return false;
1102        }
1103        if self.back_buffer.is_none() {
1104            let mut buf = Vec::with_capacity(self.fb_width.saturating_mul(self.fb_height));
1105            for y in 0..self.fb_height {
1106                for x in 0..self.fb_width {
1107                    buf.push(self.read_hw_pixel_packed(x, y));
1108                }
1109            }
1110            self.back_buffer = Some(buf);
1111        }
1112        self.draw_to_back = true;
1113        self.track_dirty = true;
1114        self.clear_dirty();
1115        true
1116    }
1117
1118    /// Disables double buffer.
1119    pub fn disable_double_buffer(&mut self, present: bool) {
1120        if present {
1121            self.present();
1122        }
1123        self.draw_to_back = false;
1124        self.track_dirty = false;
1125        self.clear_dirty();
1126    }
1127
1128    /// Performs the present operation.
1129    pub fn present(&mut self) {
1130        if !self.enabled {
1131            return;
1132        }
1133        let Some(buf) = self.back_buffer.as_ref() else {
1134            return;
1135        };
1136        let (sx, sy, sw, sh) = if self.track_dirty {
1137            let Some(dirty) = self.dirty_rect else {
1138                return;
1139            };
1140            (dirty.x, dirty.y, dirty.w, dirty.h)
1141        } else {
1142            (0, 0, self.fb_width, self.fb_height)
1143        };
1144        if sw == 0 || sh == 0 {
1145            return;
1146        }
1147
1148        let buf_ptr = buf.as_ptr();
1149        let bpp = self.fmt.bpp;
1150        let fb_addr = self.fb_addr;
1151        let pitch = self.pitch;
1152        let fb_width = self.fb_width;
1153
1154        if bpp == 32 {
1155            let row_bytes = sw * 4;
1156            for y in sy..(sy + sh) {
1157                // SAFETY: back buffer has fb_width * fb_height elements; y < fb_height, sx + sw <= fb_width.
1158                let src = unsafe { buf_ptr.add(y * fb_width + sx) as *const u8 };
1159                let dst_off = y * pitch + sx * 4;
1160                // SAFETY: fb_addr points to the mapped framebuffer; dst_off is within bounds for same reason.
1161                unsafe {
1162                    core::ptr::copy_nonoverlapping(src, fb_addr.add(dst_off), row_bytes);
1163                }
1164            }
1165        } else {
1166            for y in sy..(sy + sh) {
1167                for x in sx..(sx + sw) {
1168                    let packed = unsafe { *buf_ptr.add(y * fb_width + x) };
1169                    self.write_hw_pixel_packed(x, y, packed);
1170                }
1171            }
1172        }
1173
1174        PRESENTED_FRAMES.fetch_add(1, Ordering::Relaxed);
1175        self.clear_dirty();
1176
1177        if crate::hardware::virtio::gpu::is_available() {
1178            if let Some(gpu) = crate::hardware::virtio::gpu::get_gpu() {
1179                gpu.flush_now();
1180            }
1181        }
1182
1183        if self.mc_visible {
1184            self.mc_save_hw();
1185            self.mc_draw_hw();
1186        }
1187    }
1188
1189    // ── Mouse cursor ──────────────────────────────────────────────────────────
1190
1191    /// Performs the mc save hw operation.
1192    fn mc_save_hw(&mut self) {
1193        let x = self.mc_x;
1194        let y = self.mc_y;
1195        let fw = self.fb_width;
1196        let fh = self.fb_height;
1197        for cy in 0..CURSOR_H {
1198            for cx in 0..CURSOR_W {
1199                let px = x + cx as i32;
1200                let py = y + cy as i32;
1201                if px < 0 || py < 0 || px as usize >= fw || py as usize >= fh {
1202                    self.mc_save[cy * CURSOR_W + cx] = 0;
1203                    continue;
1204                }
1205                self.mc_save[cy * CURSOR_W + cx] =
1206                    self.read_hw_pixel_packed(px as usize, py as usize);
1207            }
1208        }
1209    }
1210
1211    /// Performs the mc draw hw operation.
1212    fn mc_draw_hw(&mut self) {
1213        let x = self.mc_x;
1214        let y = self.mc_y;
1215        let black = self.pack_color(RgbColor::BLACK);
1216        let white = self.pack_color(RgbColor::WHITE);
1217        for cy in 0..CURSOR_H {
1218            for cx in 0..CURSOR_W {
1219                let p = CURSOR_PIXELS[cy * CURSOR_W + cx];
1220                if p == 0 {
1221                    continue;
1222                }
1223                let px = x + cx as i32;
1224                let py = y + cy as i32;
1225                if px < 0 || py < 0 || px as usize >= self.fb_width || py as usize >= self.fb_height
1226                {
1227                    continue;
1228                }
1229                let color = if p == 1 { black } else { white };
1230                self.write_hw_pixel_packed(px as usize, py as usize, color);
1231            }
1232        }
1233    }
1234
1235    /// Performs the mc erase hw operation.
1236    fn mc_erase_hw(&mut self) {
1237        let x = self.mc_x;
1238        let y = self.mc_y;
1239        for cy in 0..CURSOR_H {
1240            for cx in 0..CURSOR_W {
1241                if CURSOR_PIXELS[cy * CURSOR_W + cx] == 0 {
1242                    continue;
1243                }
1244                let px = x + cx as i32;
1245                let py = y + cy as i32;
1246                if px < 0 || py < 0 || px as usize >= self.fb_width || py as usize >= self.fb_height
1247                {
1248                    continue;
1249                }
1250                self.write_hw_pixel_packed(
1251                    px as usize,
1252                    py as usize,
1253                    self.mc_save[cy * CURSOR_W + cx],
1254                );
1255            }
1256        }
1257    }
1258
1259    /// Updates mouse cursor.
1260    pub fn update_mouse_cursor(&mut self, x: i32, y: i32) {
1261        if !self.enabled {
1262            return;
1263        }
1264        if self.mc_visible && self.mc_x == x && self.mc_y == y {
1265            return;
1266        }
1267        if self.mc_visible {
1268            self.mc_erase_hw();
1269        }
1270        self.mc_x = x;
1271        self.mc_y = y;
1272        self.mc_save_hw();
1273        self.mc_draw_hw();
1274        self.mc_visible = true;
1275    }
1276
1277    /// Performs the hide mouse cursor operation.
1278    pub fn hide_mouse_cursor(&mut self) {
1279        if self.mc_visible {
1280            self.mc_erase_hw();
1281            self.mc_visible = false;
1282        }
1283    }
1284
1285    // ── Text selection ────────────────────────────────────────────────────────
1286
1287    /// Performs the sel normalized operation.
1288    fn sel_normalized(&self) -> (usize, usize, usize, usize) {
1289        let (sr, sc, er, ec) = (
1290            self.sel_start_row,
1291            self.sel_start_col,
1292            self.sel_end_row,
1293            self.sel_end_col,
1294        );
1295        if sr < er || (sr == er && sc <= ec) {
1296            (sr, sc, er, ec)
1297        } else {
1298            (er, ec, sr, sc)
1299        }
1300    }
1301
1302    /// Performs the pixel to sb pos operation.
1303    pub fn pixel_to_sb_pos(&self, px: usize, py: usize) -> Option<(usize, usize)> {
1304        if !self.enabled {
1305            return None;
1306        }
1307        let gw = self.font_info.glyph_w;
1308        let gh = self.font_info.glyph_h;
1309        if gw == 0 || gh == 0 {
1310            return None;
1311        }
1312        let text_h = self.text_area_height();
1313        let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
1314        if px >= text_w || py >= text_h {
1315            return None;
1316        }
1317        let vis_row = py / gh;
1318        let vis_col = px / gw;
1319        if vis_col >= self.cols {
1320            return None;
1321        }
1322        let total_complete = self.sb_rows.len();
1323        let has_partial = !self.sb_cur_row.is_empty();
1324        let total_virtual = total_complete + if has_partial { 1 } else { 0 };
1325        if total_virtual == 0 {
1326            return None;
1327        }
1328        let view_end = total_virtual.saturating_sub(self.scroll_offset);
1329        let view_start = view_end.saturating_sub(self.rows);
1330        let display_len = view_end.saturating_sub(view_start);
1331        if vis_row >= display_len {
1332            return None;
1333        }
1334        Some((view_start + vis_row, vis_col))
1335    }
1336
1337    /// Starts selection.
1338    pub fn start_selection(&mut self, px: usize, py: usize) {
1339        if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
1340            self.sel_start_row = row;
1341            self.sel_start_col = col;
1342            self.sel_end_row = row;
1343            self.sel_end_col = col;
1344            self.sel_active = true;
1345            self.redraw_from_scrollback();
1346        }
1347    }
1348
1349    /// Updates selection.
1350    pub fn update_selection(&mut self, px: usize, py: usize) {
1351        if !self.sel_active {
1352            return;
1353        }
1354        if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
1355            if row == self.sel_end_row && col == self.sel_end_col {
1356                return;
1357            }
1358            self.sel_end_row = row;
1359            self.sel_end_col = col;
1360            self.redraw_from_scrollback();
1361        }
1362    }
1363
1364    /// Performs the end selection operation.
1365    pub fn end_selection(&mut self) {
1366        if !self.sel_active {
1367            return;
1368        }
1369        let (start_row, start_col, end_row, end_col) = self.sel_normalized();
1370        let mut bytes: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
1371        for row in start_row..=end_row {
1372            let len = if row < self.sb_rows.len() {
1373                self.sb_rows[row].len()
1374            } else if row == self.sb_rows.len() {
1375                self.sb_cur_row.len()
1376            } else {
1377                break;
1378            };
1379            let c0 = if row == start_row {
1380                start_col.min(len)
1381            } else {
1382                0
1383            };
1384            let c1 = if row == end_row {
1385                end_col.min(len)
1386            } else {
1387                len
1388            };
1389            for col in c0..c1 {
1390                let ch = if row < self.sb_rows.len() {
1391                    self.sb_rows[row][col].ch
1392                } else {
1393                    self.sb_cur_row[col].ch
1394                };
1395                let mut buf = [0u8; 4];
1396                bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
1397            }
1398            if row < end_row {
1399                bytes.push(b'\n');
1400            }
1401        }
1402        if let Some(mut clip) = CLIPBOARD.try_lock() {
1403            let n = bytes.len().min(CLIPBOARD_CAP);
1404            clip.0[..n].copy_from_slice(&bytes[..n]);
1405            clip.1 = n;
1406        }
1407    }
1408
1409    /// Performs the clear selection operation.
1410    pub fn clear_selection(&mut self) {
1411        if self.sel_active {
1412            self.sel_active = false;
1413            self.redraw_from_scrollback();
1414        }
1415    }
1416
1417    /// Performs the clear with operation.
1418    pub fn clear_with(&mut self, color: RgbColor) {
1419        if !self.enabled {
1420            return;
1421        }
1422        let packed = self.pack_color(color);
1423        for y in 0..self.fb_height {
1424            for x in 0..self.fb_width {
1425                self.put_pixel_raw(x, y, packed);
1426            }
1427        }
1428        self.col = 0;
1429        self.row = 0;
1430    }
1431
1432    /// Performs the clear operation.
1433    pub fn clear(&mut self) {
1434        self.clear_with(self.unpack_color(self.bg));
1435    }
1436
1437    /// Performs the pixel offset operation.
1438    #[inline]
1439    fn pixel_offset(&self, x: usize, y: usize) -> Option<usize> {
1440        if x >= self.fb_width || y >= self.fb_height {
1441            return None;
1442        }
1443        let bytes_pp = self.fmt.bpp as usize / 8;
1444        let row = y.checked_mul(self.pitch)?;
1445        let col = x.checked_mul(bytes_pp)?;
1446        row.checked_add(col)
1447    }
1448
1449    /// Writes hw pixel packed.
1450    fn write_hw_pixel_packed(&mut self, x: usize, y: usize, color: u32) {
1451        let Some(off) = self.pixel_offset(x, y) else {
1452            return;
1453        };
1454        unsafe {
1455            match self.fmt.bpp {
1456                32 => {
1457                    core::ptr::write_volatile(self.fb_addr.add(off) as *mut u32, color);
1458                }
1459                24 => {
1460                    core::ptr::write_volatile(self.fb_addr.add(off), (color & 0xFF) as u8);
1461                    core::ptr::write_volatile(
1462                        self.fb_addr.add(off + 1),
1463                        ((color >> 8) & 0xFF) as u8,
1464                    );
1465                    core::ptr::write_volatile(
1466                        self.fb_addr.add(off + 2),
1467                        ((color >> 16) & 0xFF) as u8,
1468                    );
1469                }
1470                _ => {}
1471            }
1472        }
1473    }
1474
1475    /// Reads hw pixel packed.
1476    fn read_hw_pixel_packed(&self, x: usize, y: usize) -> u32 {
1477        let Some(off) = self.pixel_offset(x, y) else {
1478            return 0;
1479        };
1480        unsafe {
1481            match self.fmt.bpp {
1482                32 => core::ptr::read_volatile(self.fb_addr.add(off) as *const u32),
1483                24 => {
1484                    let b0 = core::ptr::read_volatile(self.fb_addr.add(off)) as u32;
1485                    let b1 = core::ptr::read_volatile(self.fb_addr.add(off + 1)) as u32;
1486                    let b2 = core::ptr::read_volatile(self.fb_addr.add(off + 2)) as u32;
1487                    b0 | (b1 << 8) | (b2 << 16)
1488                }
1489                _ => 0,
1490            }
1491        }
1492    }
1493
1494    /// Reads pixel packed.
1495    fn read_pixel_packed(&self, x: usize, y: usize) -> u32 {
1496        if self.draw_to_back_buffer() {
1497            if let Some(buf) = self.back_buffer.as_ref() {
1498                return buf[y * self.fb_width + x];
1499            }
1500        }
1501        self.read_hw_pixel_packed(x, y)
1502    }
1503
1504    /// Performs the put pixel raw operation.
1505    fn put_pixel_raw(&mut self, x: usize, y: usize, color: u32) {
1506        if !self.enabled || x >= self.fb_width || y >= self.fb_height || !self.in_clip(x, y) {
1507            return;
1508        }
1509        if self.draw_to_back_buffer() {
1510            if let Some(buf) = self.back_buffer.as_mut() {
1511                buf[y * self.fb_width + x] = color;
1512                self.mark_dirty_rect(x, y, 1, 1);
1513                return;
1514            }
1515        }
1516        self.write_hw_pixel_packed(x, y, color);
1517    }
1518
1519    /// Performs the draw pixel operation.
1520    pub fn draw_pixel(&mut self, x: usize, y: usize, color: RgbColor) {
1521        self.put_pixel_raw(x, y, self.pack_color(color));
1522    }
1523
1524    /// Performs the draw pixel alpha operation.
1525    pub fn draw_pixel_alpha(&mut self, x: usize, y: usize, color: RgbColor, alpha: u8) {
1526        if !self.enabled
1527            || alpha == 0
1528            || x >= self.fb_width
1529            || y >= self.fb_height
1530            || !self.in_clip(x, y)
1531        {
1532            return;
1533        }
1534        if alpha == 255 {
1535            self.put_pixel_raw(x, y, self.pack_color(color));
1536            return;
1537        }
1538        let dst = self.unpack_color(self.read_pixel_packed(x, y));
1539        let inv = (255u16).saturating_sub(alpha as u16);
1540        let a = alpha as u16;
1541        let blended = RgbColor::new(
1542            ((color.r as u16 * a + dst.r as u16 * inv + 127) / 255) as u8,
1543            ((color.g as u16 * a + dst.g as u16 * inv + 127) / 255) as u8,
1544            ((color.b as u16 * a + dst.b as u16 * inv + 127) / 255) as u8,
1545        );
1546        self.put_pixel_raw(x, y, self.pack_color(blended));
1547    }
1548
1549    /// Performs the draw line operation.
1550    pub fn draw_line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
1551        let mut x = x0;
1552        let mut y = y0;
1553        let dx = (x1 - x0).abs();
1554        let sx = if x0 < x1 { 1 } else { -1 };
1555        let dy = -(y1 - y0).abs();
1556        let sy = if y0 < y1 { 1 } else { -1 };
1557        let mut err = dx + dy;
1558        let packed = self.pack_color(color);
1559
1560        loop {
1561            if x >= 0 && y >= 0 {
1562                self.put_pixel_raw(x as usize, y as usize, packed);
1563            }
1564            if x == x1 && y == y1 {
1565                break;
1566            }
1567            let e2 = 2 * err;
1568            if e2 >= dy {
1569                err += dy;
1570                x += sx;
1571            }
1572            if e2 <= dx {
1573                err += dx;
1574                y += sy;
1575            }
1576        }
1577    }
1578
1579    /// Performs the draw rect operation.
1580    pub fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1581        if width == 0 || height == 0 {
1582            return;
1583        }
1584        let x2 = x.saturating_add(width - 1);
1585        let y2 = y.saturating_add(height - 1);
1586        self.draw_line(x as isize, y as isize, x2 as isize, y as isize, color);
1587        self.draw_line(x as isize, y as isize, x as isize, y2 as isize, color);
1588        self.draw_line(x2 as isize, y as isize, x2 as isize, y2 as isize, color);
1589        self.draw_line(x as isize, y2 as isize, x2 as isize, y2 as isize, color);
1590    }
1591
1592    /// Performs the fill rect operation.
1593    pub fn fill_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1594        let Some((sx, sy, sw, sh)) = self.clipped_rect(x, y, width, height) else {
1595            return;
1596        };
1597        let packed = self.pack_color(color);
1598
1599        if self.draw_to_back_buffer() {
1600            if let Some(buf) = self.back_buffer.as_mut() {
1601                for py in sy..(sy + sh) {
1602                    let row = py * self.fb_width;
1603                    let start = row + sx;
1604                    let end = start + sw;
1605                    buf[start..end].fill(packed);
1606                }
1607                self.mark_dirty_rect(sx, sy, sw, sh);
1608                return;
1609            }
1610        }
1611
1612        if self.fmt.bpp == 32 {
1613            for py in sy..(sy + sh) {
1614                let Some(row_off) = py
1615                    .checked_mul(self.pitch)
1616                    .and_then(|v| v.checked_add(sx * 4))
1617                else {
1618                    continue;
1619                };
1620                let count = sw;
1621                unsafe {
1622                    let ptr = self.fb_addr.add(row_off) as *mut u32;
1623                    for i in 0..count {
1624                        core::ptr::write_volatile(ptr.add(i), packed);
1625                    }
1626                }
1627            }
1628            return;
1629        }
1630
1631        for py in sy..(sy + sh) {
1632            for px in sx..(sx + sw) {
1633                self.write_hw_pixel_packed(px, py, packed);
1634            }
1635        }
1636    }
1637
1638    /// Performs the fill rect alpha operation.
1639    pub fn fill_rect_alpha(
1640        &mut self,
1641        x: usize,
1642        y: usize,
1643        width: usize,
1644        height: usize,
1645        color: RgbColor,
1646        alpha: u8,
1647    ) {
1648        if !self.enabled || width == 0 || height == 0 || alpha == 0 {
1649            return;
1650        }
1651        if alpha == 255 {
1652            self.fill_rect(x, y, width, height, color);
1653            return;
1654        }
1655        let x_end = core::cmp::min(x.saturating_add(width), self.fb_width);
1656        let y_end = core::cmp::min(y.saturating_add(height), self.fb_height);
1657        for py in y..y_end {
1658            for px in x..x_end {
1659                self.draw_pixel_alpha(px, py, color, alpha);
1660            }
1661        }
1662    }
1663
1664    /// Performs the blit rgb operation.
1665    pub fn blit_rgb(
1666        &mut self,
1667        dst_x: usize,
1668        dst_y: usize,
1669        src_width: usize,
1670        src_height: usize,
1671        pixels: &[RgbColor],
1672    ) -> bool {
1673        let len = src_width.saturating_mul(src_height);
1674        if !self.enabled || src_width == 0 || src_height == 0 || pixels.len() < len {
1675            return false;
1676        }
1677        let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.fb_width);
1678        let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.fb_height);
1679        if x_end <= dst_x || y_end <= dst_y {
1680            return true;
1681        }
1682        let copy_w = x_end - dst_x;
1683        let copy_h = y_end - dst_y;
1684        for row in 0..copy_h {
1685            let src_row = row * src_width;
1686            for col in 0..copy_w {
1687                self.draw_pixel(dst_x + col, dst_y + row, pixels[src_row + col]);
1688            }
1689        }
1690        true
1691    }
1692
1693    /// Performs the blit rgb24 operation.
1694    pub fn blit_rgb24(
1695        &mut self,
1696        dst_x: usize,
1697        dst_y: usize,
1698        src_width: usize,
1699        src_height: usize,
1700        bytes: &[u8],
1701    ) -> bool {
1702        let needed = src_width.saturating_mul(src_height).saturating_mul(3);
1703        if !self.enabled || src_width == 0 || src_height == 0 || bytes.len() < needed {
1704            return false;
1705        }
1706        let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.fb_width);
1707        let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.fb_height);
1708        if x_end <= dst_x || y_end <= dst_y {
1709            return true;
1710        }
1711        let copy_w = x_end - dst_x;
1712        let copy_h = y_end - dst_y;
1713        for row in 0..copy_h {
1714            for col in 0..copy_w {
1715                let i = (row * src_width + col) * 3;
1716                let color = RgbColor::new(bytes[i], bytes[i + 1], bytes[i + 2]);
1717                self.draw_pixel(dst_x + col, dst_y + row, color);
1718            }
1719        }
1720        true
1721    }
1722
1723    /// Performs the blit rgba operation.
1724    pub fn blit_rgba(
1725        &mut self,
1726        dst_x: usize,
1727        dst_y: usize,
1728        src_width: usize,
1729        src_height: usize,
1730        bytes: &[u8],
1731        global_alpha: u8,
1732    ) -> bool {
1733        let needed = src_width.saturating_mul(src_height).saturating_mul(4);
1734        if !self.enabled
1735            || src_width == 0
1736            || src_height == 0
1737            || bytes.len() < needed
1738            || global_alpha == 0
1739        {
1740            return false;
1741        }
1742
1743        let Some((sx, sy, sw, sh)) = self.clipped_rect(dst_x, dst_y, src_width, src_height) else {
1744            return true;
1745        };
1746        let src_x0 = sx.saturating_sub(dst_x);
1747        let src_y0 = sy.saturating_sub(dst_y);
1748
1749        for row in 0..sh {
1750            let syi = src_y0 + row;
1751            for col in 0..sw {
1752                let sxi = src_x0 + col;
1753                let i = (syi * src_width + sxi) * 4;
1754                let r = bytes[i];
1755                let g = bytes[i + 1];
1756                let b = bytes[i + 2];
1757                let sa = bytes[i + 3];
1758                if sa == 0 {
1759                    continue;
1760                }
1761                let a = ((sa as u16 * global_alpha as u16 + 127) / 255) as u8;
1762                let dx = sx + col;
1763                let dy = sy + row;
1764                if a == 255 {
1765                    self.put_pixel_raw(dx, dy, self.pack_color(RgbColor::new(r, g, b)));
1766                } else if a != 0 {
1767                    self.draw_pixel_alpha(dx, dy, RgbColor::new(r, g, b), a);
1768                }
1769            }
1770        }
1771        true
1772    }
1773
1774    /// Performs the blit sprite rgba operation.
1775    pub fn blit_sprite_rgba(
1776        &mut self,
1777        dst_x: usize,
1778        dst_y: usize,
1779        sprite: SpriteRgba<'_>,
1780        global_alpha: u8,
1781    ) -> bool {
1782        self.blit_rgba(
1783            dst_x,
1784            dst_y,
1785            sprite.width,
1786            sprite.height,
1787            sprite.pixels,
1788            global_alpha,
1789        )
1790    }
1791
1792    /// Performs the draw text at operation.
1793    pub fn draw_text_at(
1794        &mut self,
1795        pixel_x: usize,
1796        pixel_y: usize,
1797        text: &str,
1798        fg: RgbColor,
1799        bg: RgbColor,
1800    ) {
1801        if !self.enabled {
1802            return;
1803        }
1804        let gw = self.font_info.glyph_w;
1805        let gh = self.font_info.glyph_h;
1806        let fg_packed = self.pack_color(fg);
1807        let bg_packed = self.pack_color(bg);
1808        let mut cx = pixel_x;
1809        let cy = pixel_y;
1810        for ch in text.chars() {
1811            if ch == '\n' {
1812                break;
1813            }
1814            if cx + gw > self.fb_width || cy + gh > self.fb_height {
1815                break;
1816            }
1817            self.draw_glyph_at_pixel(cx, cy, ch, fg_packed, bg_packed);
1818            cx += gw;
1819        }
1820    }
1821
1822    /// Performs the glyph index for char operation.
1823    fn glyph_index_for_char(&self, ch: char) -> usize {
1824        if ch.is_ascii() {
1825            let idx = ch as usize;
1826            if idx < self.font_info.glyph_count {
1827                return idx;
1828            }
1829        }
1830        let cp = ch as u32;
1831        if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == cp) {
1832            return *glyph;
1833        }
1834        if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == ('?' as u32)) {
1835            return *glyph;
1836        }
1837        if ('?' as usize) < self.font_info.glyph_count {
1838            return '?' as usize;
1839        }
1840        0
1841    }
1842
1843    /// Performs the draw glyph index at pixel operation.
1844    fn draw_glyph_index_at_pixel(
1845        &mut self,
1846        pixel_x: usize,
1847        pixel_y: usize,
1848        glyph_index: usize,
1849        fg: u32,
1850        bg: u32,
1851    ) {
1852        if !self.enabled {
1853            return;
1854        }
1855        let glyph_index = core::cmp::min(glyph_index, self.font_info.glyph_count.saturating_sub(1));
1856        let start = self.font_info.data_offset + glyph_index * self.font_info.bytes_per_glyph;
1857        if start
1858            .checked_add(self.font_info.bytes_per_glyph)
1859            .map_or(true, |end| end > self.font.len())
1860        {
1861            return;
1862        }
1863        // Extract raw pointer so self can be mutably borrowed below.
1864        // SAFETY: self.font is &'static [u8]; pointer is valid for the program lifetime.
1865        let glyph_ptr = self.font[start..start + self.font_info.bytes_per_glyph].as_ptr();
1866        let row_bytes = self.font_info.glyph_w.div_ceil(8);
1867        let gw = self.font_info.glyph_w;
1868        let gh = self.font_info.glyph_h;
1869
1870        if self.draw_to_back_buffer()
1871            && pixel_x + gw <= self.fb_width
1872            && pixel_y + gh <= self.fb_height
1873        {
1874            let fb_width = self.fb_width;
1875            if let Some(buf) = self.back_buffer.as_mut() {
1876                for gy in 0..gh {
1877                    let row_start = (pixel_y + gy) * fb_width + pixel_x;
1878                    for gx in 0..gw {
1879                        // SAFETY: glyph_ptr valid (static font data), index within bytes_per_glyph.
1880                        let byte = unsafe { *glyph_ptr.add(gy * row_bytes + gx / 8) };
1881                        let mask = 0x80u8 >> (gx % 8);
1882                        buf[row_start + gx] = if (byte & mask) != 0 { fg } else { bg };
1883                    }
1884                }
1885            }
1886            self.mark_dirty_rect(pixel_x, pixel_y, gw, gh);
1887        } else {
1888            for gy in 0..gh {
1889                for gx in 0..gw {
1890                    let byte = unsafe { *glyph_ptr.add(gy * row_bytes + gx / 8) };
1891                    let mask = 0x80u8 >> (gx % 8);
1892                    let color = if (byte & mask) != 0 { fg } else { bg };
1893                    self.put_pixel_raw(pixel_x + gx, pixel_y + gy, color);
1894                }
1895            }
1896        }
1897    }
1898
1899    /// Performs the draw glyph at pixel operation.
1900    fn draw_glyph_at_pixel(&mut self, pixel_x: usize, pixel_y: usize, ch: char, fg: u32, bg: u32) {
1901        let glyph_index = self.glyph_index_for_char(ch);
1902        self.draw_glyph_index_at_pixel(pixel_x, pixel_y, glyph_index, fg, bg);
1903    }
1904
1905    /// Performs the layout text lines operation.
1906    fn layout_text_lines(&self, text: &str, wrap: bool, max_cols: Option<usize>) -> Vec<Vec<char>> {
1907        let mut lines: Vec<Vec<char>> = Vec::new();
1908        let mut current: Vec<char> = Vec::new();
1909        let wrap_cols = max_cols.filter(|&c| c > 0);
1910
1911        for ch in text.chars() {
1912            if ch == '\n' {
1913                lines.push(current);
1914                current = Vec::new();
1915                continue;
1916            }
1917
1918            if wrap {
1919                if let Some(cols) = wrap_cols {
1920                    if current.len() >= cols {
1921                        lines.push(current);
1922                        current = Vec::new();
1923                    }
1924                }
1925            }
1926
1927            current.push(ch);
1928        }
1929
1930        lines.push(current);
1931        lines
1932    }
1933
1934    /// Performs the measure text operation.
1935    pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
1936        if !self.enabled {
1937            return TextMetrics {
1938                width: 0,
1939                height: 0,
1940                lines: 0,
1941            };
1942        }
1943        let gw = self.font_info.glyph_w;
1944        let gh = self.font_info.glyph_h;
1945        let max_cols = max_width.map(|w| core::cmp::max(1, w / gw));
1946        let lines = self.layout_text_lines(text, wrap, max_cols);
1947
1948        let mut max_line_cols = 0usize;
1949        for line in &lines {
1950            max_line_cols = core::cmp::max(max_line_cols, line.len());
1951        }
1952
1953        TextMetrics {
1954            width: max_line_cols * gw,
1955            height: lines.len() * gh,
1956            lines: lines.len(),
1957        }
1958    }
1959
1960    /// Performs the draw text operation.
1961    pub fn draw_text(
1962        &mut self,
1963        pixel_x: usize,
1964        pixel_y: usize,
1965        text: &str,
1966        opts: TextOptions,
1967    ) -> TextMetrics {
1968        if !self.enabled {
1969            return TextMetrics {
1970                width: 0,
1971                height: 0,
1972                lines: 0,
1973            };
1974        }
1975
1976        let gw = self.font_info.glyph_w;
1977        let gh = self.font_info.glyph_h;
1978        let max_cols = opts.max_width.map(|w| core::cmp::max(1, w / gw));
1979        let lines = self.layout_text_lines(text, opts.wrap, max_cols);
1980        let region_w = opts.max_width.unwrap_or_else(|| {
1981            let mut max_line_cols = 0usize;
1982            for line in &lines {
1983                max_line_cols = core::cmp::max(max_line_cols, line.len());
1984            }
1985            max_line_cols * gw
1986        });
1987
1988        let fg = self.pack_color(opts.fg);
1989        let bg = self.pack_color(opts.bg);
1990        let mut max_line_px = 0usize;
1991
1992        for (line_idx, line) in lines.iter().enumerate() {
1993            let line_px = line.len() * gw;
1994            max_line_px = core::cmp::max(max_line_px, line_px);
1995            let x = match opts.align {
1996                TextAlign::Left => pixel_x,
1997                TextAlign::Center => pixel_x.saturating_add(region_w.saturating_sub(line_px) / 2),
1998                TextAlign::Right => pixel_x.saturating_add(region_w.saturating_sub(line_px)),
1999            };
2000            let y = pixel_y + line_idx * gh;
2001
2002            for (col, ch) in line.iter().enumerate() {
2003                self.draw_glyph_at_pixel(x + col * gw, y, *ch, fg, bg);
2004            }
2005        }
2006
2007        TextMetrics {
2008            width: max_line_px,
2009            height: lines.len() * gh,
2010            lines: lines.len(),
2011        }
2012    }
2013
2014    /// Performs the draw strata stack operation.
2015    pub fn draw_strata_stack(
2016        &mut self,
2017        origin_x: usize,
2018        origin_y: usize,
2019        layer_w: usize,
2020        layer_h: usize,
2021    ) {
2022        if !self.enabled || layer_w == 0 || layer_h == 0 {
2023            return;
2024        }
2025
2026        // Simple "strata" stack: each layer is slightly shifted and tinted.
2027        let palette = [
2028            RgbColor::new(0x24, 0x3B, 0x55),
2029            RgbColor::new(0x2B, 0x54, 0x77),
2030            RgbColor::new(0x2F, 0x74, 0x93),
2031            RgbColor::new(0x3A, 0x93, 0xA8),
2032            RgbColor::new(0x5F, 0xB1, 0xA1),
2033            RgbColor::new(0xA4, 0xCC, 0x94),
2034        ];
2035
2036        let dx = 6usize;
2037        let dy = 5usize;
2038        for (i, color) in palette.iter().enumerate() {
2039            let x = origin_x.saturating_add(i * dx);
2040            let y = origin_y.saturating_add(i * dy);
2041            let w = layer_w.saturating_sub(i * dx);
2042            let h = layer_h.saturating_sub(i * dy);
2043            if w < 8 || h < 8 {
2044                break;
2045            }
2046
2047            self.fill_rect(x, y, w, h, *color);
2048            self.draw_rect(x, y, w, h, RgbColor::new(0x10, 0x16, 0x20));
2049        }
2050    }
2051
2052    /// Performs the draw glyph operation.
2053    fn draw_glyph(&mut self, cx: usize, cy: usize, ch: char) {
2054        let glyph_index = self.glyph_index_for_char(ch);
2055        self.draw_glyph_index_at_pixel(
2056            cx * self.font_info.glyph_w,
2057            cy * self.font_info.glyph_h,
2058            glyph_index,
2059            self.fg,
2060            self.bg,
2061        );
2062    }
2063
2064    /// Performs the clear row operation.
2065    fn clear_row(&mut self, row: usize) {
2066        if !self.enabled {
2067            return;
2068        }
2069        let y_start = row * self.font_info.glyph_h;
2070        let y_end = y_start + self.font_info.glyph_h;
2071        for y in y_start..y_end {
2072            for x in 0..self.fb_width {
2073                self.put_pixel_raw(x, y, self.bg);
2074            }
2075        }
2076    }
2077
2078    /// Performs the scroll operation.
2079    fn scroll(&mut self) {
2080        if !self.enabled {
2081            return;
2082        }
2083        let dy = self.font_info.glyph_h;
2084        let text_h = self.text_area_height();
2085        if dy >= text_h {
2086            self.clear();
2087            return;
2088        }
2089
2090        let move_rows = text_h - dy;
2091        if self.draw_to_back_buffer() {
2092            if let Some(buf) = self.back_buffer.as_mut() {
2093                let src_start = dy * self.fb_width;
2094                let src_end = text_h * self.fb_width;
2095                buf.copy_within(src_start..src_end, 0);
2096                self.mark_dirty_rect(0, 0, self.fb_width, text_h);
2097            }
2098        } else {
2099            let bytes_per_row = self.pitch;
2100            unsafe {
2101                core::ptr::copy(
2102                    self.fb_addr.add(dy * bytes_per_row),
2103                    self.fb_addr,
2104                    move_rows * bytes_per_row,
2105                );
2106            }
2107        }
2108
2109        self.fill_rect(0, move_rows, self.fb_width, dy, self.unpack_color(self.bg));
2110        self.row = self.rows - 1;
2111    }
2112
2113    /// Writes char.
2114    fn write_char(&mut self, c: char) {
2115        if !self.enabled {
2116            return;
2117        }
2118        let c = normalize_console_char(c);
2119
2120        // ── Mirror into scrollback (always, even when scrolled back) ──────────────
2121        self.sb_mirror_char(c);
2122        // If the user is viewing history, suppress live rendering.
2123        if self.scroll_offset > 0 {
2124            return;
2125        }
2126        // ──────────────────────────────────────────────────────────────
2127
2128        match c {
2129            '\n' => {
2130                self.col = 0;
2131                self.row += 1;
2132            }
2133            '\r' => self.col = 0,
2134            '\t' => self.col = (self.col + 4) & !3,
2135            '\u{8}' => {
2136                if self.col > 0 {
2137                    self.col -= 1;
2138                    self.draw_glyph(self.col, self.row, ' ');
2139                }
2140            }
2141            '\0' => {}
2142            ch => {
2143                self.draw_glyph(self.col, self.row, ch);
2144                self.col += 1;
2145            }
2146        }
2147
2148        if self.col >= self.cols {
2149            self.col = 0;
2150            self.row += 1;
2151        }
2152
2153        if self.row >= self.rows {
2154            self.scroll();
2155            self.clear_row(self.row);
2156        }
2157    }
2158
2159    /// Writes bytes.
2160    fn write_bytes(&mut self, s: &str) {
2161        // Skip basic ANSI escape sequences to avoid rendering control garbage.
2162        let mut chars = s.chars();
2163        while let Some(ch) = chars.next() {
2164            if ch == '\u{1b}' {
2165                if matches!(chars.clone().next(), Some('[')) {
2166                    let _ = chars.next();
2167                    for c in chars.by_ref() {
2168                        if ('@'..='~').contains(&c) {
2169                            break;
2170                        }
2171                    }
2172                }
2173                continue;
2174            }
2175            self.write_char(ch);
2176        }
2177        // Refresh scrollbar after each batch of output.
2178        self.draw_scrollbar_inner();
2179    }
2180
2181    // ═══════════════════════════════════════════════════════════════════
2182    // Scrollback buffer + scrollbar
2183    // ═══════════════════════════════════════════════════════════════════
2184
2185    /// Mirror a normalized character into the scrollback model.
2186    /// Called by `write_char` before any live rendering.
2187    fn sb_mirror_char(&mut self, c: char) {
2188        let cols = self.cols;
2189        let fg = self.fg;
2190        let bg = self.bg;
2191        match c {
2192            '\n' => {
2193                let row = core::mem::take(&mut self.sb_cur_row);
2194                self.sb_rows.push_back(row);
2195                self.sb_trim();
2196            }
2197            '\r' => {
2198                self.sb_cur_row.clear();
2199            }
2200            '\t' => {
2201                let stop = (self.sb_cur_row.len() + 4) & !3;
2202                let end = stop.min(cols);
2203                while self.sb_cur_row.len() < end {
2204                    self.sb_cur_row.push(SbCell { ch: ' ', fg, bg });
2205                }
2206                if self.sb_cur_row.len() >= cols {
2207                    let row = core::mem::take(&mut self.sb_cur_row);
2208                    self.sb_rows.push_back(row);
2209                    self.sb_trim();
2210                }
2211            }
2212            '\u{8}' => {
2213                self.sb_cur_row.pop();
2214            }
2215            '\0' => {}
2216            ch => {
2217                self.sb_cur_row.push(SbCell { ch, fg, bg });
2218                if self.sb_cur_row.len() >= cols {
2219                    let row = core::mem::take(&mut self.sb_cur_row);
2220                    self.sb_rows.push_back(row);
2221                    self.sb_trim();
2222                }
2223            }
2224        }
2225    }
2226
2227    /// Keep the scrollback buffer within MAX_SCROLLBACK + rows.
2228    #[inline]
2229    fn sb_trim(&mut self) {
2230        let cap = MAX_SCROLLBACK + self.rows + 1;
2231        while self.sb_rows.len() > cap {
2232            self.sb_rows.pop_front();
2233        }
2234    }
2235
2236    /// Draw (or refresh) the scrollbar strip on the right edge of the text area.
2237    fn draw_scrollbar_inner(&mut self) {
2238        if !self.enabled || self.fb_width == 0 {
2239            return;
2240        }
2241        let text_h = self.text_area_height();
2242        if text_h == 0 {
2243            return;
2244        }
2245        let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
2246        let total = self.sb_rows.len() + 1; // +1 accounts for current partial row
2247        let track_packed = self.fmt.pack_rgb(0x22, 0x28, 0x38);
2248        let thumb_packed = self.fmt.pack_rgb(0x58, 0x72, 0xA0);
2249        let thumb_hi = self.fmt.pack_rgb(0x80, 0xA0, 0xC8);
2250
2251        if total <= self.rows {
2252            // Not enough content to scroll: full-height thumb.
2253            for y in 0..text_h {
2254                for x in sb_x..self.fb_width {
2255                    let c = if x == sb_x || x == self.fb_width - 1 || y == 0 || y == text_h - 1 {
2256                        track_packed
2257                    } else {
2258                        thumb_hi
2259                    };
2260                    self.put_pixel_raw(x, y, c);
2261                }
2262            }
2263            return;
2264        }
2265
2266        let max_offset = total.saturating_sub(self.rows);
2267        let thumb_h = ((text_h * self.rows) / total).max(6);
2268        let avail = text_h.saturating_sub(thumb_h);
2269        // offset 0 = thumb at bottom; max_offset = thumb at top
2270        let thumb_y = if self.scroll_offset == 0 || avail == 0 {
2271            avail // = text_h - thumb_h (bottom)
2272        } else {
2273            avail - (avail * self.scroll_offset / max_offset)
2274        };
2275
2276        for y in 0..text_h {
2277            let in_thumb = y >= thumb_y && y < thumb_y + thumb_h;
2278            let packed = if in_thumb { thumb_packed } else { track_packed };
2279            for x in sb_x..self.fb_width {
2280                self.put_pixel_raw(x, y, packed);
2281            }
2282        }
2283    }
2284
2285    /// Redraw the entire text area from the scrollback buffer.
2286    /// Called when scroll_offset changes.
2287    fn redraw_from_scrollback(&mut self) {
2288        if !self.enabled {
2289            return;
2290        }
2291        let text_h = self.text_area_height();
2292        let total_complete = self.sb_rows.len();
2293        let has_partial = !self.sb_cur_row.is_empty();
2294        let total_virtual = total_complete + if has_partial { 1 } else { 0 };
2295        let view_end = total_virtual.saturating_sub(self.scroll_offset);
2296        let view_start = view_end.saturating_sub(self.rows);
2297
2298        if self.back_buffer.is_none() {
2299            let total = self.fb_width.saturating_mul(self.fb_height);
2300            if total > 0 {
2301                self.back_buffer = Some(alloc::vec![0u32; total]);
2302            }
2303        }
2304
2305        let prev_draw_to_back = self.draw_to_back;
2306        let prev_track_dirty = self.track_dirty;
2307        let using_back = self.back_buffer.is_some();
2308        if using_back {
2309            self.draw_to_back = true;
2310            self.track_dirty = true;
2311            self.clear_dirty();
2312        }
2313
2314        let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2315        let bg = self.bg;
2316        if self.draw_to_back_buffer() {
2317            let fb_width = self.fb_width;
2318            if let Some(buf) = self.back_buffer.as_mut() {
2319                for y in 0..text_h {
2320                    let row = y * fb_width;
2321                    buf[row..row + text_w].fill(bg);
2322                }
2323            }
2324            self.mark_dirty_rect(0, 0, text_w, text_h);
2325        } else {
2326            for y in 0..text_h {
2327                for x in 0..text_w {
2328                    self.put_pixel_raw(x, y, bg);
2329                }
2330            }
2331        }
2332
2333        let glyph_h = self.font_info.glyph_h;
2334        let glyph_w = self.font_info.glyph_w;
2335        let max_col = self.cols;
2336        let (sel_active, sel_sr, sel_sc, sel_er, sel_ec) = if self.sel_active {
2337            let (sr, sc, er, ec) = self.sel_normalized();
2338            (true, sr, sc, er, ec)
2339        } else {
2340            (false, 0, 0, 0, 0)
2341        };
2342        let sel_bg = self.pack_color(RgbColor::new(0x26, 0x5F, 0xCC));
2343        let sel_fg = self.pack_color(RgbColor::WHITE);
2344        let display_len = view_end - view_start;
2345        for vis_row in 0..display_len {
2346            let virt_row = view_start + vis_row;
2347            // Get a raw pointer to the row slice to avoid borrow conflicts with draw calls.
2348            // SAFETY: sb_rows and sb_cur_row are not modified during this loop; pointer
2349            // remains valid for the duration of the iteration.
2350            let (row_ptr, row_len) = if virt_row < total_complete {
2351                let row = &self.sb_rows[virt_row];
2352                (row.as_ptr(), row.len())
2353            } else if has_partial && virt_row == total_complete {
2354                (self.sb_cur_row.as_ptr(), self.sb_cur_row.len())
2355            } else {
2356                (core::ptr::null(), 0)
2357            };
2358            let py = vis_row * glyph_h;
2359            let cell_count = row_len.min(max_col);
2360            for col in 0..cell_count {
2361                let px = col * glyph_w;
2362                // SAFETY: col < cell_count <= row_len, row_ptr is valid for row_len elements.
2363                let cell = unsafe { &*row_ptr.add(col) };
2364                let (draw_fg, draw_bg) = if sel_active {
2365                    let in_sel = if virt_row < sel_sr || virt_row > sel_er {
2366                        false
2367                    } else if sel_sr == sel_er {
2368                        col >= sel_sc && col < sel_ec
2369                    } else if virt_row == sel_sr {
2370                        col >= sel_sc
2371                    } else if virt_row == sel_er {
2372                        col < sel_ec
2373                    } else {
2374                        true
2375                    };
2376                    if in_sel {
2377                        (sel_fg, sel_bg)
2378                    } else {
2379                        (cell.fg, cell.bg)
2380                    }
2381                } else {
2382                    (cell.fg, cell.bg)
2383                };
2384                self.draw_glyph_at_pixel(px, py, cell.ch, draw_fg, draw_bg);
2385            }
2386        }
2387
2388        if self.scroll_offset == 0 {
2389            self.row = if display_len > 0 { display_len - 1 } else { 0 };
2390            let last_virt = view_start + display_len.saturating_sub(1);
2391            let last_len = if last_virt < total_complete {
2392                self.sb_rows[last_virt].len()
2393            } else if has_partial && last_virt == total_complete {
2394                self.sb_cur_row.len()
2395            } else {
2396                0
2397            };
2398            self.col = last_len.min(self.cols);
2399        }
2400
2401        self.draw_scrollbar_inner();
2402
2403        if using_back {
2404            self.present();
2405            self.draw_to_back = prev_draw_to_back;
2406            self.track_dirty = prev_track_dirty;
2407            if !prev_track_dirty {
2408                self.clear_dirty();
2409            }
2410        }
2411    }
2412
2413    /// Scroll the view up (backward in history) by `lines` lines.
2414    pub fn scroll_view_up(&mut self, lines: usize) {
2415        if !self.enabled {
2416            return;
2417        }
2418        let total = self.sb_rows.len() + 1;
2419        let max_off = total.saturating_sub(self.rows);
2420        self.scroll_offset = (self.scroll_offset + lines).min(max_off);
2421        self.redraw_from_scrollback();
2422    }
2423
2424    /// Scroll the view down (forward, toward live) by `lines` lines.
2425    pub fn scroll_view_down(&mut self, lines: usize) {
2426        if !self.enabled {
2427            return;
2428        }
2429        self.scroll_offset = self.scroll_offset.saturating_sub(lines);
2430        self.redraw_from_scrollback();
2431    }
2432
2433    /// Immediately return to the live (bottom) view.
2434    pub fn scroll_to_live(&mut self) {
2435        if self.scroll_offset == 0 {
2436            return;
2437        }
2438        self.scroll_offset = 0;
2439        self.redraw_from_scrollback();
2440    }
2441
2442    /// Handle a click at pixel `(px_x, px_y)` — if it falls in the scrollbar,
2443    /// jump the view to the corresponding scroll position.
2444    pub fn scrollbar_click(&mut self, px_x: usize, px_y: usize) {
2445        if !self.enabled {
2446            return;
2447        }
2448        let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
2449        if px_x < sb_x {
2450            return;
2451        }
2452        let text_h = self.text_area_height();
2453        if text_h <= 1 {
2454            return;
2455        }
2456        let total = self.sb_rows.len() + 1;
2457        let max_off = total.saturating_sub(self.rows);
2458        if max_off == 0 {
2459            return;
2460        }
2461        // py = 0 → top = oldest = max_offset; py = text_h - 1 → bottom = 0
2462        let py = px_y.min(text_h - 1);
2463        let offset = max_off * (text_h - 1 - py) / (text_h - 1);
2464        self.scroll_offset = offset.min(max_off);
2465        self.redraw_from_scrollback();
2466    }
2467
2468    /// Drag the scrollbar thumb to vertical pixel `px_y`.
2469    ///
2470    /// Unlike `scrollbar_click`, this only depends on Y and is intended for
2471    /// click-and-drag interactions where the pointer may slightly leave the
2472    /// scrollbar strip horizontally.
2473    pub fn scrollbar_drag_to(&mut self, px_y: usize) {
2474        if !self.enabled {
2475            return;
2476        }
2477        let text_h = self.text_area_height();
2478        if text_h <= 1 {
2479            return;
2480        }
2481        let total = self.sb_rows.len() + 1;
2482        let max_off = total.saturating_sub(self.rows);
2483        if max_off == 0 {
2484            return;
2485        }
2486        // py = 0 -> top = oldest = max_offset; py = text_h - 1 -> bottom = 0
2487        let py = px_y.min(text_h - 1);
2488        let offset = max_off * (text_h - 1 - py) / (text_h - 1);
2489        self.scroll_offset = offset.min(max_off);
2490        self.redraw_from_scrollback();
2491    }
2492
2493    /// Returns `true` if the pixel coordinates fall within the scrollbar strip.
2494    pub fn scrollbar_hit_test(&self, px_x: usize, px_y: usize) -> bool {
2495        if !self.enabled {
2496            return false;
2497        }
2498        let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
2499        px_x >= sb_x && px_y < self.text_area_height()
2500    }
2501}
2502
2503/// Performs the normalize console char operation.
2504fn normalize_console_char(ch: char) -> char {
2505    match ch {
2506        '\n' | '\r' | '\t' | '\u{8}' => ch,
2507        c if c.is_control() => '\0',
2508        // Graceful fallback when font lacks box-drawing coverage.
2509        '\u{2500}' | '\u{2501}' | '\u{2504}' | '\u{2505}' | '\u{2013}' | '\u{2014}' => '-',
2510        '\u{2502}' | '\u{2503}' => '|',
2511        '\u{250c}' | '\u{2510}' | '\u{2514}' | '\u{2518}' | '\u{251c}' | '\u{2524}'
2512        | '\u{252c}' | '\u{2534}' | '\u{253c}' => '+',
2513        '\u{00a0}' => ' ',
2514        _ => ch,
2515    }
2516}
2517
2518impl fmt::Write for VgaWriter {
2519    /// Writes str.
2520    fn write_str(&mut self, s: &str) -> fmt::Result {
2521        if self.enabled {
2522            self.write_bytes(s);
2523        } else {
2524            crate::arch::x86_64::serial::_print(format_args!("{}", s));
2525        }
2526        Ok(())
2527    }
2528}
2529
2530pub static VGA_WRITER: Mutex<VgaWriter> = Mutex::new(VgaWriter::new());
2531
2532/// Returns whether available.
2533#[inline]
2534pub fn is_available() -> bool {
2535    VGA_AVAILABLE.load(Ordering::Relaxed)
2536}
2537
2538/// Performs the with writer operation.
2539pub fn with_writer<R>(f: impl FnOnce(&mut VgaWriter) -> R) -> Option<R> {
2540    if !is_available() {
2541        return None;
2542    }
2543    let mut writer = VGA_WRITER.lock();
2544    Some(f(&mut writer))
2545}
2546
2547/// Performs the status line info operation.
2548fn status_line_info() -> StatusLineInfo {
2549    let mut guard = STATUS_LINE_INFO.lock();
2550    if guard.is_none() {
2551        *guard = Some(StatusLineInfo {
2552            hostname: String::from("strat9"),
2553            ip: String::from("n/a"),
2554        });
2555    }
2556    guard.as_ref().cloned().unwrap_or(StatusLineInfo {
2557        hostname: String::from("strat9"),
2558        ip: String::from("n/a"),
2559    })
2560}
2561
2562/// Performs the format uptime from ticks operation.
2563fn format_uptime_from_ticks(ticks: u64) -> String {
2564    let total_secs = ticks / 100;
2565    let h = total_secs / 3600;
2566    let m = (total_secs % 3600) / 60;
2567    let s = total_secs % 60;
2568    format!("{:02}:{:02}:{:02}", h, m, s)
2569}
2570
2571/// Performs the current fps operation.
2572fn current_fps(tick: u64) -> u64 {
2573    let last_tick = FPS_LAST_TICK.load(Ordering::Relaxed);
2574    let frames = PRESENTED_FRAMES.load(Ordering::Relaxed);
2575
2576    if last_tick == 0 {
2577        let _ = FPS_LAST_TICK.compare_exchange(0, tick, Ordering::Relaxed, Ordering::Relaxed);
2578        let _ =
2579            FPS_LAST_FRAME_COUNT.compare_exchange(0, frames, Ordering::Relaxed, Ordering::Relaxed);
2580        return FPS_ESTIMATE.load(Ordering::Relaxed);
2581    }
2582
2583    let dt = tick.saturating_sub(last_tick);
2584    if dt >= STATUS_REFRESH_PERIOD_TICKS
2585        && FPS_LAST_TICK
2586            .compare_exchange(last_tick, tick, Ordering::Relaxed, Ordering::Relaxed)
2587            .is_ok()
2588    {
2589        let last_frames = FPS_LAST_FRAME_COUNT.swap(frames, Ordering::Relaxed);
2590        let df = frames.saturating_sub(last_frames);
2591        let fps = if dt == 0 {
2592            0
2593        } else {
2594            df.saturating_mul(100) / dt
2595        };
2596        FPS_ESTIMATE.store(fps, Ordering::Relaxed);
2597    }
2598
2599    FPS_ESTIMATE.load(Ordering::Relaxed)
2600}
2601
2602/// Performs the current ui scale operation.
2603fn current_ui_scale() -> UiScale {
2604    match UI_SCALE.load(Ordering::Relaxed) {
2605        1 => UiScale::Compact,
2606        3 => UiScale::Large,
2607        _ => UiScale::Normal,
2608    }
2609}
2610
2611/// Performs the ui scale operation.
2612pub fn ui_scale() -> UiScale {
2613    current_ui_scale()
2614}
2615
2616/// Sets ui scale.
2617pub fn set_ui_scale(scale: UiScale) {
2618    UI_SCALE.store(scale as u8, Ordering::Relaxed);
2619}
2620
2621/// Performs the ui scale px operation.
2622pub fn ui_scale_px(base: usize) -> usize {
2623    let factor = current_ui_scale().factor();
2624    let denom = UiScale::Normal.factor();
2625    base.saturating_mul(factor) / denom
2626}
2627
2628/// Performs the format mem usage operation.
2629fn format_mem_usage() -> String {
2630    let lock = crate::memory::buddy::get_allocator();
2631    let Some(guard) = lock.try_lock() else {
2632        // Never block the status-line task on allocator contention.
2633        return String::from("n/a");
2634    };
2635    let Some(alloc) = guard.as_ref() else {
2636        return String::from("n/a");
2637    };
2638    let (total_pages, allocated_pages) = alloc.page_totals();
2639    let page_size = 4096usize;
2640    let total = total_pages.saturating_mul(page_size);
2641    let used = allocated_pages.saturating_mul(page_size);
2642    let free = total.saturating_sub(used);
2643    format!("{}/{}", format_size(free), format_size(total))
2644}
2645
2646/// Performs the format size operation.
2647fn format_size(bytes: usize) -> String {
2648    const KB: usize = 1024;
2649    const MB: usize = 1024 * KB;
2650    const GB: usize = 1024 * MB;
2651    if bytes >= GB {
2652        format!("{}G", bytes / GB)
2653    } else if bytes >= MB {
2654        format!("{}M", bytes / MB)
2655    } else if bytes >= KB {
2656        format!("{}K", bytes / KB)
2657    } else {
2658        format!("{}B", bytes)
2659    }
2660}
2661
2662/// Performs the draw status bar inner operation.
2663fn draw_status_bar_inner(w: &mut VgaWriter, left: &str, right: &str, theme: UiTheme) {
2664    let saved_clip = w.clip;
2665    w.reset_clip_rect();
2666
2667    let (gw, gh) = w.glyph_size();
2668    if gh == 0 || gw == 0 {
2669        w.clip = saved_clip;
2670        return;
2671    }
2672    let bar_h = gh;
2673    let y = w.height().saturating_sub(bar_h);
2674    w.fill_rect(0, y, w.width(), bar_h, theme.status_bg);
2675
2676    let left_opts = TextOptions {
2677        fg: theme.status_text,
2678        bg: theme.status_bg,
2679        align: TextAlign::Left,
2680        wrap: false,
2681        max_width: Some(w.width().saturating_sub(8)),
2682    };
2683    w.draw_text(0, y, left, left_opts);
2684
2685    let right_opts = TextOptions {
2686        fg: theme.status_text,
2687        bg: theme.status_bg,
2688        align: TextAlign::Right,
2689        wrap: false,
2690        max_width: Some(w.width()),
2691    };
2692    w.draw_text(0, y, right, right_opts);
2693    w.clip = saved_clip;
2694}
2695
2696/// Performs the init operation.
2697#[allow(clippy::too_many_arguments)]
2698pub fn init(
2699    fb_addr: u64,
2700    fb_width: u32,
2701    fb_height: u32,
2702    pitch: u32,
2703    bpp: u16,
2704    red_size: u8,
2705    red_shift: u8,
2706    green_size: u8,
2707    green_shift: u8,
2708    blue_size: u8,
2709    blue_shift: u8,
2710) {
2711    if fb_addr == 0 || fb_width == 0 || fb_height == 0 || pitch == 0 {
2712        VGA_AVAILABLE.store(false, Ordering::Relaxed);
2713        log::info!("Framebuffer console unavailable (no framebuffer)");
2714        return;
2715    }
2716
2717    if bpp != 24 && bpp != 32 {
2718        VGA_AVAILABLE.store(false, Ordering::Relaxed);
2719        log::info!("Framebuffer console unavailable (unsupported bpp={})", bpp);
2720        return;
2721    }
2722
2723    let fmt = PixelFormat {
2724        bpp,
2725        red_size,
2726        red_shift,
2727        green_size,
2728        green_shift,
2729        blue_size,
2730        blue_shift,
2731    };
2732
2733    // Ensure fb_addr is a virtual address in the HHDM.
2734    // If it's already higher-half (>= HHDM), use it as-is.
2735    // Otherwise, convert it via phys_to_virt.
2736    //
2737    // This fix works on VMWare Workstation
2738    //
2739    let hhdm = crate::memory::hhdm_offset();
2740    let fb_virt = if hhdm != 0 && fb_addr < hhdm {
2741        crate::memory::phys_to_virt(fb_addr)
2742    } else {
2743        fb_addr
2744    };
2745
2746    let mut writer = VGA_WRITER.lock();
2747    if writer.configure(
2748        fb_virt as *mut u8,
2749        fb_width as usize,
2750        fb_height as usize,
2751        pitch as usize,
2752        fmt,
2753    ) {
2754        writer.set_color(Color::LightCyan, Color::Black);
2755        writer.clear_with(RgbColor::new(0x12, 0x16, 0x1E));
2756        // Decorative background mark for Strat9 identity.
2757        let deco_w = (writer.width() / 3).clamp(120, 300);
2758        let deco_h = (writer.height() / 4).clamp(90, 220);
2759        let deco_x = writer.width().saturating_sub(deco_w + 24);
2760        let deco_y = 24;
2761        writer.draw_strata_stack(deco_x, deco_y, deco_w, deco_h);
2762        writer.set_rgb_color(
2763            RgbColor::new(0xA7, 0xD8, 0xD8),
2764            RgbColor::new(0x12, 0x16, 0x1E),
2765        );
2766        writer.write_bytes("Strat9-OS v0.1.0\n");
2767        writer.set_rgb_color(
2768            RgbColor::new(0xE2, 0xE8, 0xF0),
2769            RgbColor::new(0x12, 0x16, 0x1E),
2770        );
2771        VGA_AVAILABLE.store(true, Ordering::Relaxed);
2772        log::info!(
2773            "Framebuffer console enabled: {}x{} {}bpp pitch={}",
2774            fb_width,
2775            fb_height,
2776            bpp,
2777            pitch
2778        );
2779        drop(writer);
2780        draw_boot_status_line(UiTheme::OCEAN_STATUS);
2781    } else {
2782        writer.enabled = false;
2783        VGA_AVAILABLE.store(false, Ordering::Relaxed);
2784        log::info!("Framebuffer console unavailable (font parse/init failed)");
2785    }
2786}
2787
2788/// Print to framebuffer console (falls back to serial when unavailable).
2789#[macro_export]
2790macro_rules! vga_print {
2791    ($($arg:tt)*) => {
2792        $crate::arch::x86_64::vga::_print(format_args!($($arg)*));
2793    };
2794}
2795
2796/// Print line to framebuffer console (falls back to serial when unavailable).
2797#[macro_export]
2798macro_rules! vga_println {
2799    () => ($crate::vga_print!("\n"));
2800    ($($arg:tt)*) => ($crate::vga_print!("{}\n", format_args!($($arg)*)));
2801}
2802
2803/// Performs the print operation.
2804#[doc(hidden)]
2805pub fn _print(args: fmt::Arguments) {
2806    use core::fmt::Write;
2807    if is_available() {
2808        VGA_WRITER.lock().write_fmt(args).ok();
2809        return;
2810    }
2811    crate::arch::x86_64::serial::_print(args);
2812}
2813
2814#[derive(Debug, Clone, Copy)]
2815pub struct Canvas {
2816    fg: RgbColor,
2817    bg: RgbColor,
2818}
2819
2820impl Default for Canvas {
2821    /// Builds a default instance.
2822    fn default() -> Self {
2823        Self {
2824            fg: RgbColor::LIGHT_GREY,
2825            bg: RgbColor::BLACK,
2826        }
2827    }
2828}
2829
2830impl Canvas {
2831    /// Creates a new instance.
2832    pub const fn new(fg: RgbColor, bg: RgbColor) -> Self {
2833        Self { fg, bg }
2834    }
2835
2836    /// Sets fg.
2837    pub fn set_fg(&mut self, fg: RgbColor) {
2838        self.fg = fg;
2839    }
2840
2841    /// Sets bg.
2842    pub fn set_bg(&mut self, bg: RgbColor) {
2843        self.bg = bg;
2844    }
2845
2846    /// Sets colors.
2847    pub fn set_colors(&mut self, fg: RgbColor, bg: RgbColor) {
2848        self.fg = fg;
2849        self.bg = bg;
2850    }
2851
2852    /// Sets clip rect.
2853    pub fn set_clip_rect(&self, x: usize, y: usize, w: usize, h: usize) {
2854        set_clip_rect(x, y, w, h);
2855    }
2856
2857    /// Performs the reset clip rect operation.
2858    pub fn reset_clip_rect(&self) {
2859        reset_clip_rect();
2860    }
2861
2862    /// Performs the clear operation.
2863    pub fn clear(&self) {
2864        fill_rect(0, 0, width(), height(), self.bg);
2865    }
2866
2867    /// Performs the pixel operation.
2868    pub fn pixel(&self, x: usize, y: usize) {
2869        draw_pixel(x, y, self.fg);
2870    }
2871
2872    /// Performs the line operation.
2873    pub fn line(&self, x0: isize, y0: isize, x1: isize, y1: isize) {
2874        draw_line(x0, y0, x1, y1, self.fg);
2875    }
2876
2877    /// Performs the rect operation.
2878    pub fn rect(&self, x: usize, y: usize, w: usize, h: usize) {
2879        draw_rect(x, y, w, h, self.fg);
2880    }
2881
2882    /// Performs the fill rect operation.
2883    pub fn fill_rect(&self, x: usize, y: usize, w: usize, h: usize) {
2884        fill_rect(x, y, w, h, self.fg);
2885    }
2886
2887    /// Performs the fill rect alpha operation.
2888    pub fn fill_rect_alpha(&self, x: usize, y: usize, w: usize, h: usize, alpha: u8) {
2889        fill_rect_alpha(x, y, w, h, self.fg, alpha);
2890    }
2891
2892    /// Performs the text operation.
2893    pub fn text(&self, x: usize, y: usize, text: &str) {
2894        draw_text_at(x, y, text, self.fg, self.bg);
2895    }
2896
2897    /// Performs the text opts operation.
2898    pub fn text_opts(
2899        &self,
2900        x: usize,
2901        y: usize,
2902        text: &str,
2903        align: TextAlign,
2904        wrap: bool,
2905        max_width: Option<usize>,
2906    ) -> TextMetrics {
2907        draw_text(
2908            x,
2909            y,
2910            text,
2911            TextOptions {
2912                fg: self.fg,
2913                bg: self.bg,
2914                align,
2915                wrap,
2916                max_width,
2917            },
2918        )
2919    }
2920
2921    /// Performs the measure text operation.
2922    pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
2923        measure_text(text, max_width, wrap)
2924    }
2925
2926    /// Performs the blit rgb operation.
2927    pub fn blit_rgb(&self, x: usize, y: usize, w: usize, h: usize, pixels: &[RgbColor]) -> bool {
2928        blit_rgb(x, y, w, h, pixels)
2929    }
2930
2931    /// Performs the blit rgb24 operation.
2932    pub fn blit_rgb24(&self, x: usize, y: usize, w: usize, h: usize, bytes: &[u8]) -> bool {
2933        blit_rgb24(x, y, w, h, bytes)
2934    }
2935
2936    /// Performs the blit rgba operation.
2937    pub fn blit_rgba(
2938        &self,
2939        x: usize,
2940        y: usize,
2941        w: usize,
2942        h: usize,
2943        bytes: &[u8],
2944        global_alpha: u8,
2945    ) -> bool {
2946        blit_rgba(x, y, w, h, bytes, global_alpha)
2947    }
2948
2949    /// Performs the blit sprite rgba operation.
2950    pub fn blit_sprite_rgba(
2951        &self,
2952        x: usize,
2953        y: usize,
2954        sprite: SpriteRgba<'_>,
2955        global_alpha: u8,
2956    ) -> bool {
2957        blit_sprite_rgba(x, y, sprite, global_alpha)
2958    }
2959
2960    /// Performs the begin frame operation.
2961    pub fn begin_frame(&self) -> bool {
2962        begin_frame()
2963    }
2964
2965    /// Performs the end frame operation.
2966    pub fn end_frame(&self) {
2967        end_frame();
2968    }
2969
2970    /// Performs the ui clear operation.
2971    pub fn ui_clear(&self, theme: UiTheme) {
2972        ui_clear(theme);
2973    }
2974
2975    /// Performs the ui panel operation.
2976    pub fn ui_panel(
2977        &self,
2978        x: usize,
2979        y: usize,
2980        w: usize,
2981        h: usize,
2982        title: &str,
2983        body: &str,
2984        theme: UiTheme,
2985    ) {
2986        ui_draw_panel(x, y, w, h, title, body, theme);
2987    }
2988
2989    /// Performs the ui status bar operation.
2990    pub fn ui_status_bar(&self, left: &str, right: &str, theme: UiTheme) {
2991        ui_draw_status_bar(left, right, theme);
2992    }
2993
2994    /// Performs the system status line operation.
2995    pub fn system_status_line(&self, theme: UiTheme) {
2996        draw_system_status_line(theme);
2997    }
2998
2999    /// Performs the layout screen operation.
3000    pub fn layout_screen(&self) -> UiDockLayout {
3001        UiDockLayout::from_screen()
3002    }
3003
3004    /// Performs the ui label operation.
3005    pub fn ui_label(&self, label: &UiLabel<'_>) {
3006        ui_draw_label(label);
3007    }
3008
3009    /// Performs the ui progress bar operation.
3010    pub fn ui_progress_bar(&self, bar: UiProgressBar) {
3011        ui_draw_progress_bar(bar);
3012    }
3013
3014    /// Performs the ui table operation.
3015    pub fn ui_table(&self, table: &UiTable) {
3016        ui_draw_table(table);
3017    }
3018}
3019
3020/// Performs the width operation.
3021pub fn width() -> usize {
3022    if !is_available() {
3023        return 0;
3024    }
3025    VGA_WRITER.lock().width()
3026}
3027
3028/// Performs the height operation.
3029pub fn height() -> usize {
3030    if !is_available() {
3031        return 0;
3032    }
3033    VGA_WRITER.lock().height()
3034}
3035
3036/// Performs the screen size operation.
3037pub fn screen_size() -> (usize, usize) {
3038    (width(), height())
3039}
3040
3041/// Performs the ui layout screen operation.
3042pub fn ui_layout_screen() -> UiDockLayout {
3043    UiDockLayout::from_screen()
3044}
3045
3046/// Performs the glyph size operation.
3047pub fn glyph_size() -> (usize, usize) {
3048    if !is_available() {
3049        return (0, 0);
3050    }
3051    VGA_WRITER.lock().glyph_size()
3052}
3053
3054/// Performs the text cols operation.
3055pub fn text_cols() -> usize {
3056    if !is_available() {
3057        return 0;
3058    }
3059    VGA_WRITER.lock().cols()
3060}
3061
3062/// Performs the text rows operation.
3063pub fn text_rows() -> usize {
3064    if !is_available() {
3065        return 0;
3066    }
3067    VGA_WRITER.lock().rows()
3068}
3069
3070/// Returns text cursor.
3071pub fn get_text_cursor() -> (usize, usize) {
3072    if !is_available() {
3073        return (0, 0);
3074    }
3075    let writer = VGA_WRITER.lock();
3076    (writer.col, writer.row)
3077}
3078
3079/// Sets text cursor.
3080pub fn set_text_cursor(col: usize, row: usize) {
3081    if !is_available() {
3082        return;
3083    }
3084    VGA_WRITER.lock().set_cursor_cell(col, row);
3085}
3086
3087/// Performs the double buffer mode operation.
3088pub fn double_buffer_mode() -> bool {
3089    DOUBLE_BUFFER_MODE.load(Ordering::Relaxed)
3090}
3091
3092/// Sets double buffer mode.
3093pub fn set_double_buffer_mode(enabled: bool) {
3094    DOUBLE_BUFFER_MODE.store(enabled, Ordering::Relaxed);
3095}
3096
3097/// Performs the draw text cursor operation.
3098pub fn draw_text_cursor(color: RgbColor) {
3099    if !is_available() {
3100        return;
3101    }
3102    let mut writer = VGA_WRITER.lock();
3103    let (gw, gh) = writer.glyph_size();
3104    let x = writer.col * gw;
3105    let y = writer.row * gh;
3106    let packed = writer.pack_color(color);
3107
3108    // Draw a block cursor (full glyph width, 2px height at bottom or full height)
3109    // Let's go with a full block for better visibility
3110    for py in y..(y + gh) {
3111        for px in x..(x + gw) {
3112            writer.put_pixel_raw(px, py, packed);
3113        }
3114    }
3115}
3116
3117/// Performs the framebuffer info operation.
3118pub fn framebuffer_info() -> FramebufferInfo {
3119    if !is_available() {
3120        return FramebufferInfo {
3121            available: false,
3122            width: 0,
3123            height: 0,
3124            pitch: 0,
3125            bpp: 0,
3126            red_size: 0,
3127            red_shift: 0,
3128            green_size: 0,
3129            green_shift: 0,
3130            blue_size: 0,
3131            blue_shift: 0,
3132            text_cols: 0,
3133            text_rows: 0,
3134            glyph_w: 0,
3135            glyph_h: 0,
3136            double_buffer_mode: false,
3137            double_buffer_enabled: false,
3138            ui_scale: UiScale::Normal,
3139        };
3140    }
3141    VGA_WRITER.lock().framebuffer_info()
3142}
3143
3144/// Sets text color.
3145pub fn set_text_color(fg: RgbColor, bg: RgbColor) {
3146    if !is_available() {
3147        return;
3148    }
3149    VGA_WRITER.lock().set_rgb_color(fg, bg);
3150}
3151
3152/// Sets clip rect.
3153pub fn set_clip_rect(x: usize, y: usize, width: usize, height: usize) {
3154    if !is_available() {
3155        return;
3156    }
3157    VGA_WRITER.lock().set_clip_rect(x, y, width, height);
3158}
3159
3160/// Performs the reset clip rect operation.
3161pub fn reset_clip_rect() {
3162    if !is_available() {
3163        return;
3164    }
3165    VGA_WRITER.lock().reset_clip_rect();
3166}
3167
3168/// Performs the begin frame operation.
3169pub fn begin_frame() -> bool {
3170    if !is_available() {
3171        return false;
3172    }
3173    if !double_buffer_mode() {
3174        return false;
3175    }
3176    VGA_WRITER.lock().enable_double_buffer()
3177}
3178
3179/// Performs the end frame operation.
3180pub fn end_frame() {
3181    if !is_available() {
3182        return;
3183    }
3184    let mut writer = VGA_WRITER.lock();
3185    writer.present();
3186    writer.disable_double_buffer(false);
3187}
3188
3189/// Performs the present operation.
3190pub fn present() {
3191    if !is_available() {
3192        return;
3193    }
3194    VGA_WRITER.lock().present();
3195}
3196
3197/// Performs the draw pixel operation.
3198pub fn draw_pixel(x: usize, y: usize, color: RgbColor) {
3199    if !is_available() {
3200        return;
3201    }
3202    VGA_WRITER.lock().draw_pixel(x, y, color);
3203}
3204
3205/// Performs the draw pixel alpha operation.
3206pub fn draw_pixel_alpha(x: usize, y: usize, color: RgbColor, alpha: u8) {
3207    if !is_available() {
3208        return;
3209    }
3210    VGA_WRITER.lock().draw_pixel_alpha(x, y, color, alpha);
3211}
3212
3213/// Performs the draw line operation.
3214pub fn draw_line(x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
3215    if !is_available() {
3216        return;
3217    }
3218    VGA_WRITER.lock().draw_line(x0, y0, x1, y1, color);
3219}
3220
3221/// Performs the draw rect operation.
3222pub fn draw_rect(x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
3223    if !is_available() {
3224        return;
3225    }
3226    VGA_WRITER.lock().draw_rect(x, y, width, height, color);
3227}
3228
3229/// Performs the fill rect operation.
3230pub fn fill_rect(x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
3231    if !is_available() {
3232        return;
3233    }
3234    VGA_WRITER.lock().fill_rect(x, y, width, height, color);
3235}
3236
3237/// Performs the fill rect alpha operation.
3238pub fn fill_rect_alpha(
3239    x: usize,
3240    y: usize,
3241    width: usize,
3242    height: usize,
3243    color: RgbColor,
3244    alpha: u8,
3245) {
3246    if !is_available() {
3247        return;
3248    }
3249    VGA_WRITER
3250        .lock()
3251        .fill_rect_alpha(x, y, width, height, color, alpha);
3252}
3253
3254/// Performs the blit rgb operation.
3255pub fn blit_rgb(
3256    dst_x: usize,
3257    dst_y: usize,
3258    src_width: usize,
3259    src_height: usize,
3260    pixels: &[RgbColor],
3261) -> bool {
3262    if !is_available() {
3263        return false;
3264    }
3265    VGA_WRITER
3266        .lock()
3267        .blit_rgb(dst_x, dst_y, src_width, src_height, pixels)
3268}
3269
3270/// Performs the blit rgb24 operation.
3271pub fn blit_rgb24(
3272    dst_x: usize,
3273    dst_y: usize,
3274    src_width: usize,
3275    src_height: usize,
3276    bytes: &[u8],
3277) -> bool {
3278    if !is_available() {
3279        return false;
3280    }
3281    VGA_WRITER
3282        .lock()
3283        .blit_rgb24(dst_x, dst_y, src_width, src_height, bytes)
3284}
3285
3286/// Performs the blit rgba operation.
3287pub fn blit_rgba(
3288    dst_x: usize,
3289    dst_y: usize,
3290    src_width: usize,
3291    src_height: usize,
3292    bytes: &[u8],
3293    global_alpha: u8,
3294) -> bool {
3295    if !is_available() {
3296        return false;
3297    }
3298    VGA_WRITER
3299        .lock()
3300        .blit_rgba(dst_x, dst_y, src_width, src_height, bytes, global_alpha)
3301}
3302
3303/// Performs the blit sprite rgba operation.
3304pub fn blit_sprite_rgba(
3305    dst_x: usize,
3306    dst_y: usize,
3307    sprite: SpriteRgba<'_>,
3308    global_alpha: u8,
3309) -> bool {
3310    if !is_available() {
3311        return false;
3312    }
3313    VGA_WRITER
3314        .lock()
3315        .blit_sprite_rgba(dst_x, dst_y, sprite, global_alpha)
3316}
3317
3318/// Performs the draw text at operation.
3319pub fn draw_text_at(pixel_x: usize, pixel_y: usize, text: &str, fg: RgbColor, bg: RgbColor) {
3320    if !is_available() {
3321        return;
3322    }
3323    VGA_WRITER
3324        .lock()
3325        .draw_text_at(pixel_x, pixel_y, text, fg, bg);
3326}
3327
3328/// Performs the draw text operation.
3329pub fn draw_text(pixel_x: usize, pixel_y: usize, text: &str, opts: TextOptions) -> TextMetrics {
3330    if !is_available() {
3331        return TextMetrics {
3332            width: 0,
3333            height: 0,
3334            lines: 0,
3335        };
3336    }
3337    VGA_WRITER.lock().draw_text(pixel_x, pixel_y, text, opts)
3338}
3339
3340/// Performs the measure text operation.
3341pub fn measure_text(text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
3342    if !is_available() {
3343        return TextMetrics {
3344            width: 0,
3345            height: 0,
3346            lines: 0,
3347        };
3348    }
3349    VGA_WRITER.lock().measure_text(text, max_width, wrap)
3350}
3351
3352/// Performs the ui clear operation.
3353pub fn ui_clear(theme: UiTheme) {
3354    let _ = with_writer(|w| w.clear_with(theme.background));
3355}
3356
3357/// Performs the ui draw panel operation.
3358pub fn ui_draw_panel(
3359    x: usize,
3360    y: usize,
3361    width: usize,
3362    height: usize,
3363    title: &str,
3364    body: &str,
3365    theme: UiTheme,
3366) {
3367    let _ = with_writer(|w| {
3368        if width < 8 || height < 8 {
3369            return;
3370        }
3371        let (gw, gh) = w.glyph_size();
3372        w.fill_rect(x, y, width, height, theme.panel_bg);
3373        w.draw_rect(x, y, width, height, theme.panel_border);
3374
3375        let title_h = gh + 6;
3376        w.fill_rect(
3377            x.saturating_add(1),
3378            y.saturating_add(1),
3379            width.saturating_sub(2),
3380            title_h,
3381            theme.accent,
3382        );
3383        let title_opts = TextOptions {
3384            fg: theme.text,
3385            bg: theme.accent,
3386            align: TextAlign::Left,
3387            wrap: false,
3388            max_width: Some(width.saturating_sub(10)),
3389        };
3390        w.draw_text(x.saturating_add(6), y.saturating_add(3), title, title_opts);
3391
3392        let body_opts = TextOptions {
3393            fg: theme.text,
3394            bg: theme.panel_bg,
3395            align: TextAlign::Left,
3396            wrap: true,
3397            max_width: Some(width.saturating_sub(10)),
3398        };
3399        w.draw_text(
3400            x.saturating_add(6),
3401            y.saturating_add(title_h + 4),
3402            body,
3403            body_opts,
3404        );
3405
3406        // Visual separator.
3407        w.fill_rect(
3408            x.saturating_add(1),
3409            y.saturating_add(title_h + 1),
3410            width.saturating_sub(2),
3411            1,
3412            theme.panel_border,
3413        );
3414        // Keep an implicit reference to glyph width to avoid dead code warning for gw in tiny fonts.
3415        let _ = gw;
3416    });
3417}
3418
3419/// Performs the ui draw panel widget operation.
3420pub fn ui_draw_panel_widget(panel: &UiPanel<'_>) {
3421    ui_draw_panel(
3422        panel.rect.x,
3423        panel.rect.y,
3424        panel.rect.w,
3425        panel.rect.h,
3426        panel.title,
3427        panel.body,
3428        panel.theme,
3429    );
3430}
3431
3432/// Performs the ui draw label operation.
3433pub fn ui_draw_label(label: &UiLabel<'_>) {
3434    let _ = with_writer(|w| {
3435        w.draw_text(
3436            label.rect.x,
3437            label.rect.y,
3438            label.text,
3439            TextOptions {
3440                fg: label.fg,
3441                bg: label.bg,
3442                align: label.align,
3443                wrap: false,
3444                max_width: Some(label.rect.w),
3445            },
3446        );
3447    });
3448}
3449
3450/// Performs the ui draw progress bar operation.
3451pub fn ui_draw_progress_bar(bar: UiProgressBar) {
3452    let _ = with_writer(|w| {
3453        if bar.rect.w < 3 || bar.rect.h < 3 {
3454            return;
3455        }
3456        let value = core::cmp::min(bar.value, 100) as usize;
3457        w.fill_rect(bar.rect.x, bar.rect.y, bar.rect.w, bar.rect.h, bar.bg);
3458        w.draw_rect(bar.rect.x, bar.rect.y, bar.rect.w, bar.rect.h, bar.border);
3459        let inner_w = bar.rect.w.saturating_sub(2);
3460        let fill_w = inner_w.saturating_mul(value) / 100;
3461        if fill_w > 0 {
3462            w.fill_rect(
3463                bar.rect.x + 1,
3464                bar.rect.y + 1,
3465                fill_w,
3466                bar.rect.h.saturating_sub(2),
3467                bar.fg,
3468            );
3469        }
3470    });
3471}
3472
3473/// Performs the ui draw table operation.
3474pub fn ui_draw_table(table: &UiTable) {
3475    let _ = with_writer(|w| {
3476        if table.rect.w < 8 || table.rect.h < 8 {
3477            return;
3478        }
3479        let (_gw, gh) = w.glyph_size();
3480        if gh == 0 {
3481            return;
3482        }
3483
3484        w.fill_rect(
3485            table.rect.x,
3486            table.rect.y,
3487            table.rect.w,
3488            table.rect.h,
3489            table.theme.panel_bg,
3490        );
3491        w.draw_rect(
3492            table.rect.x,
3493            table.rect.y,
3494            table.rect.w,
3495            table.rect.h,
3496            table.theme.panel_border,
3497        );
3498
3499        let cols = core::cmp::max(1, table.headers.len());
3500        let col_w = table.rect.w / cols;
3501        let header_h = gh + 2;
3502        w.fill_rect(
3503            table.rect.x + 1,
3504            table.rect.y + 1,
3505            table.rect.w.saturating_sub(2),
3506            header_h,
3507            table.theme.accent,
3508        );
3509
3510        for (i, h) in table.headers.iter().enumerate() {
3511            let x = table.rect.x + i * col_w + 2;
3512            w.draw_text(
3513                x,
3514                table.rect.y + 1,
3515                h,
3516                TextOptions {
3517                    fg: table.theme.text,
3518                    bg: table.theme.accent,
3519                    align: TextAlign::Left,
3520                    wrap: false,
3521                    max_width: Some(col_w.saturating_sub(4)),
3522                },
3523            );
3524        }
3525
3526        let mut y = table.rect.y + header_h + 2;
3527        for row in &table.rows {
3528            if y + gh > table.rect.y + table.rect.h {
3529                break;
3530            }
3531            for c in 0..cols {
3532                if c >= row.len() {
3533                    continue;
3534                }
3535                let x = table.rect.x + c * col_w + 2;
3536                w.draw_text(
3537                    x,
3538                    y,
3539                    &row[c],
3540                    TextOptions {
3541                        fg: table.theme.text,
3542                        bg: table.theme.panel_bg,
3543                        align: TextAlign::Left,
3544                        wrap: false,
3545                        max_width: Some(col_w.saturating_sub(4)),
3546                    },
3547                );
3548            }
3549            y += gh;
3550        }
3551    });
3552}
3553
3554/// Performs the ui draw status bar operation.
3555pub fn ui_draw_status_bar(left: &str, right: &str, theme: UiTheme) {
3556    let _ = with_writer(|w| {
3557        draw_status_bar_inner(w, left, right, theme);
3558    });
3559}
3560
3561/// Sets status hostname.
3562pub fn set_status_hostname(hostname: &str) {
3563    let mut guard = STATUS_LINE_INFO.lock();
3564    if guard.is_none() {
3565        *guard = Some(StatusLineInfo {
3566            hostname: String::new(),
3567            ip: String::from("n/a"),
3568        });
3569    }
3570    if let Some(info) = guard.as_mut() {
3571        info.hostname.clear();
3572        info.hostname.push_str(hostname);
3573    }
3574}
3575
3576/// Sets status ip.
3577pub fn set_status_ip(ip: &str) {
3578    let mut guard = STATUS_LINE_INFO.lock();
3579    if guard.is_none() {
3580        *guard = Some(StatusLineInfo {
3581            hostname: String::from("strat9"),
3582            ip: String::new(),
3583        });
3584    }
3585    if let Some(info) = guard.as_mut() {
3586        info.ip.clear();
3587        info.ip.push_str(ip);
3588    }
3589}
3590
3591/// Performs the draw system status line operation.
3592pub fn draw_system_status_line(theme: UiTheme) {
3593    let info = status_line_info();
3594    let version = env!("CARGO_PKG_VERSION");
3595    let tick = crate::process::scheduler::ticks();
3596    let uptime = format_uptime_from_ticks(tick);
3597    let mem = format_mem_usage();
3598    let fps = current_fps(tick);
3599    let left = format!(" {} ", info.hostname);
3600    let right = format!(
3601        "ip:{}  ver:{}  uptime:{}  ticks:{}  FPS:{}  load:n/a  memfree:{} ",
3602        info.ip, version, uptime, tick, fps, mem
3603    );
3604    ui_draw_status_bar(&left, &right, theme);
3605}
3606
3607/// Performs the draw boot status line operation.
3608fn draw_boot_status_line(theme: UiTheme) {
3609    let _ = with_writer(|w| {
3610        draw_status_bar_inner(
3611            w,
3612            " strat9 ",
3613            "ip:n/a  ver:boot  up:00:00:00  ticks:0  load:n/a  mem:n/a ",
3614            theme,
3615        );
3616    });
3617}
3618
3619/// Performs the refresh status ip from net scheme operation.
3620fn refresh_status_ip_from_net_scheme() {
3621    let paths = ["/net/address", "/net/ip"];
3622    for path in paths {
3623        let fd = match crate::vfs::open(path, crate::vfs::OpenFlags::READ) {
3624            Ok(fd) => fd,
3625            Err(_) => continue,
3626        };
3627        let mut buf = [0u8; 64];
3628        let read_res = crate::vfs::read(fd, &mut buf);
3629        let _ = crate::vfs::close(fd);
3630        let n = match read_res {
3631            Ok(n) => n,
3632            Err(_) => continue,
3633        };
3634        if n == 0 {
3635            continue;
3636        }
3637        let Ok(text) = core::str::from_utf8(&buf[..n]) else {
3638            continue;
3639        };
3640        let mut ip = text.trim();
3641        if let Some(slash) = ip.find('/') {
3642            ip = &ip[..slash];
3643        }
3644        if ip.is_empty() || ip == "0.0.0.0" || ip == "169.254.0.0" {
3645            continue;
3646        }
3647        set_status_ip(ip);
3648        return;
3649    }
3650
3651    set_status_ip("n/a");
3652}
3653
3654/// Stack-only string buffer to avoid heap allocation in the status-line path.
3655struct StackStr<const N: usize> {
3656    buf: [u8; N],
3657    len: usize,
3658}
3659
3660impl<const N: usize> StackStr<N> {
3661    /// Creates a new instance.
3662    const fn new() -> Self {
3663        Self {
3664            buf: [0; N],
3665            len: 0,
3666        }
3667    }
3668    /// Returns this as str.
3669    fn as_str(&self) -> &str {
3670        unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
3671    }
3672}
3673
3674impl<const N: usize> core::fmt::Write for StackStr<N> {
3675    /// Writes str.
3676    fn write_str(&mut self, s: &str) -> core::fmt::Result {
3677        let bytes = s.as_bytes();
3678        let avail = N - self.len;
3679        let n = bytes.len().min(avail);
3680        self.buf[self.len..self.len + n].copy_from_slice(&bytes[..n]);
3681        self.len += n;
3682        Ok(())
3683    }
3684}
3685
3686/// Performs the maybe refresh system status line operation.
3687pub fn maybe_refresh_system_status_line(theme: UiTheme) {
3688    if !is_available() {
3689        return;
3690    }
3691
3692    let tick = crate::process::scheduler::ticks();
3693    let last = STATUS_LAST_REFRESH_TICK.load(Ordering::Relaxed);
3694    if tick.saturating_sub(last) < STATUS_REFRESH_PERIOD_TICKS {
3695        return;
3696    }
3697    if STATUS_LAST_REFRESH_TICK
3698        .compare_exchange(last, tick, Ordering::Relaxed, Ordering::Relaxed)
3699        .is_err()
3700    {
3701        return;
3702    }
3703    refresh_status_ip_from_net_scheme();
3704
3705    let (hostname, ip) = if let Some(guard) = STATUS_LINE_INFO.try_lock() {
3706        if let Some(info) = guard.as_ref() {
3707            let mut h = StackStr::<64>::new();
3708            let mut i = StackStr::<48>::new();
3709            let _ = core::fmt::Write::write_str(&mut h, &info.hostname);
3710            let _ = core::fmt::Write::write_str(&mut i, &info.ip);
3711            (h, i)
3712        } else {
3713            let mut h = StackStr::<64>::new();
3714            let mut i = StackStr::<48>::new();
3715            let _ = core::fmt::Write::write_str(&mut h, "strat9");
3716            let _ = core::fmt::Write::write_str(&mut i, "n/a");
3717            (h, i)
3718        }
3719    } else {
3720        return;
3721    };
3722
3723    let version = env!("CARGO_PKG_VERSION");
3724
3725    let total_secs = tick / 100;
3726    let h = total_secs / 3600;
3727    let m = (total_secs % 3600) / 60;
3728    let s = total_secs % 60;
3729
3730    let mem_str = {
3731        use core::fmt::Write;
3732        let lock = crate::memory::buddy::get_allocator();
3733        if let Some(guard) = lock.try_lock() {
3734            if let Some(alloc) = guard.as_ref() {
3735                let (tp, ap) = alloc.page_totals();
3736                let total = tp.saturating_mul(4096);
3737                let used = ap.saturating_mul(4096);
3738                let free = total.saturating_sub(used);
3739                let mut buf = StackStr::<32>::new();
3740                let _ = write!(
3741                    buf,
3742                    "{}/{}",
3743                    format_size_stack(free),
3744                    format_size_stack(total)
3745                );
3746                buf
3747            } else {
3748                let mut buf = StackStr::<32>::new();
3749                let _ = core::fmt::Write::write_str(&mut buf, "n/a");
3750                buf
3751            }
3752        } else {
3753            let mut buf = StackStr::<32>::new();
3754            let _ = core::fmt::Write::write_str(&mut buf, "n/a");
3755            buf
3756        }
3757    };
3758
3759    let fps = current_fps(tick);
3760
3761    use core::fmt::Write;
3762    let mut left = StackStr::<80>::new();
3763    let _ = write!(left, " {} ", hostname.as_str());
3764
3765    let mut right = StackStr::<256>::new();
3766    let _ = write!(
3767        right,
3768        "ip:{}  ver:{}  up:{:02}:{:02}:{:02}  ticks:{}  fps:{}  load:n/a  mem:{} ",
3769        ip.as_str(),
3770        version,
3771        h,
3772        m,
3773        s,
3774        tick,
3775        fps,
3776        mem_str.as_str()
3777    );
3778
3779    if let Some(mut writer) = VGA_WRITER.try_lock() {
3780        draw_status_bar_inner(&mut writer, left.as_str(), right.as_str(), theme);
3781    }
3782}
3783
3784/// Performs the format size stack operation.
3785fn format_size_stack(bytes: usize) -> StackStr<16> {
3786    use core::fmt::Write;
3787    const KB: usize = 1024;
3788    const MB: usize = 1024 * KB;
3789    const GB: usize = 1024 * MB;
3790    let mut buf = StackStr::<16>::new();
3791    if bytes >= GB {
3792        let _ = write!(buf, "{}G", bytes / GB);
3793    } else if bytes >= MB {
3794        let _ = write!(buf, "{}M", bytes / MB);
3795    } else if bytes >= KB {
3796        let _ = write!(buf, "{}K", bytes / KB);
3797    } else {
3798        let _ = write!(buf, "{}B", bytes);
3799    }
3800    buf
3801}
3802
3803impl<const N: usize> core::fmt::Display for StackStr<N> {
3804    /// Performs the fmt operation.
3805    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3806        f.write_str(self.as_str())
3807    }
3808}
3809
3810/// Performs the status line task main operation.
3811pub extern "C" fn status_line_task_main() -> ! {
3812    let mut last_tick = 0u64;
3813    let mut diag_counter = 0u64;
3814    loop {
3815        let tick = crate::process::scheduler::ticks();
3816        if tick != last_tick {
3817            last_tick = tick;
3818            maybe_refresh_system_status_line(UiTheme::OCEAN_STATUS);
3819        }
3820        diag_counter += 1;
3821        if diag_counter % 5000 == 0 {
3822            crate::serial_println!(
3823                "[status-line] heartbeat tick={} vga={}",
3824                tick,
3825                is_available()
3826            );
3827        }
3828        crate::process::yield_task();
3829    }
3830}
3831
3832/// Performs the draw strata stack operation.
3833pub fn draw_strata_stack(origin_x: usize, origin_y: usize, layer_w: usize, layer_h: usize) {
3834    if !is_available() {
3835        return;
3836    }
3837    VGA_WRITER
3838        .lock()
3839        .draw_strata_stack(origin_x, origin_y, layer_w, layer_h);
3840}
3841// ── Scrollback / scrollbar public API ────────────────────────────────────────
3842
3843/// Scroll the console view up (backward in history) by `lines` lines.
3844pub fn scroll_view_up(lines: usize) {
3845    if !is_available() {
3846        return;
3847    }
3848    VGA_WRITER.lock().scroll_view_up(lines);
3849}
3850
3851/// Scroll the console view down (forward, toward live output) by `lines` lines.
3852pub fn scroll_view_down(lines: usize) {
3853    if !is_available() {
3854        return;
3855    }
3856    VGA_WRITER.lock().scroll_view_down(lines);
3857}
3858
3859/// Return immediately to the live (bottom) view.
3860pub fn scroll_to_live() {
3861    if !is_available() {
3862        return;
3863    }
3864    if let Some(mut w) = VGA_WRITER.try_lock() {
3865        w.scroll_to_live();
3866    }
3867}
3868
3869/// Handle a click at framebuffer pixel `(px_x, px_y)`.
3870/// If the click lands on the scrollbar, jump the view accordingly.
3871pub fn scrollbar_click(px_x: usize, px_y: usize) {
3872    if !is_available() {
3873        return;
3874    }
3875    if let Some(mut w) = VGA_WRITER.try_lock() {
3876        w.scrollbar_click(px_x, px_y);
3877    }
3878}
3879
3880/// Drag the scrollbar to a given Y pixel coordinate.
3881pub fn scrollbar_drag_to(px_y: usize) {
3882    if !is_available() {
3883        return;
3884    }
3885    if let Some(mut w) = VGA_WRITER.try_lock() {
3886        w.scrollbar_drag_to(px_y);
3887    }
3888}
3889
3890/// Returns `true` if `(px_x, px_y)` falls within the scrollbar strip.
3891pub fn scrollbar_hit_test(px_x: usize, px_y: usize) -> bool {
3892    if !is_available() {
3893        return false;
3894    }
3895    if let Some(w) = VGA_WRITER.try_lock() {
3896        w.scrollbar_hit_test(px_x, px_y)
3897    } else {
3898        false
3899    }
3900}
3901
3902/// Updates mouse cursor.
3903pub fn update_mouse_cursor(x: i32, y: i32) {
3904    if !is_available() {
3905        return;
3906    }
3907    if let Some(mut w) = VGA_WRITER.try_lock() {
3908        w.update_mouse_cursor(x, y);
3909    }
3910}
3911
3912/// Performs the hide mouse cursor operation.
3913pub fn hide_mouse_cursor() {
3914    if !is_available() {
3915        return;
3916    }
3917    if let Some(mut w) = VGA_WRITER.try_lock() {
3918        w.hide_mouse_cursor();
3919    }
3920}
3921
3922/// Starts selection.
3923pub fn start_selection(px: usize, py: usize) {
3924    if !is_available() {
3925        return;
3926    }
3927    if let Some(mut w) = VGA_WRITER.try_lock() {
3928        w.start_selection(px, py);
3929    }
3930}
3931
3932/// Updates selection.
3933pub fn update_selection(px: usize, py: usize) {
3934    if !is_available() {
3935        return;
3936    }
3937    if let Some(mut w) = VGA_WRITER.try_lock() {
3938        w.update_selection(px, py);
3939    }
3940}
3941
3942/// Performs the end selection operation.
3943pub fn end_selection() {
3944    if !is_available() {
3945        return;
3946    }
3947    if let Some(mut w) = VGA_WRITER.try_lock() {
3948        w.end_selection();
3949    }
3950}
3951
3952/// Performs the clear selection operation.
3953pub fn clear_selection() {
3954    if !is_available() {
3955        return;
3956    }
3957    if let Some(mut w) = VGA_WRITER.try_lock() {
3958        w.clear_selection();
3959    }
3960}
3961
3962/// Returns clipboard text.
3963pub fn get_clipboard_text(buf: &mut [u8]) -> usize {
3964    if let Some(clip) = CLIPBOARD.try_lock() {
3965        let n = clip.1.min(buf.len());
3966        buf[..n].copy_from_slice(&clip.0[..n]);
3967        n
3968    } else {
3969        0
3970    }
3971}