Skip to main content

strat9_kernel/arch/x86_64/vga/
api.rs

1//! Public VGA API surface : free functions, the global `VGA_WRITER`, and the
2//! `vga_print!` / `vga_println!` macros that the rest of the kernel uses to
3//! talk to the framebuffer console.
4
5use super::{
6    debug_overlay::vga_debug_init,
7    font::{parse_psf, FONT_PSF},
8    panic_screen::init_panic_fb_globals,
9    status_line::draw_boot_status_line,
10    types::*,
11    writer::{
12        VgaWriter, DOUBLE_BUFFER_MODE, PRESENTED_FRAMES, VGA_PRESENT_PIXEL_COUNT,
13        VGA_PRESENT_REGION_COUNT,
14    },
15};
16use alloc::{format, vec};
17use core::{
18    fmt,
19    sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering},
20};
21use spin::Mutex;
22
23pub(crate) static VGA_AVAILABLE: AtomicBool = AtomicBool::new(false);
24pub(crate) static FPS_LAST_TICK: AtomicU64 = AtomicU64::new(0);
25pub(crate) static FPS_LAST_FRAME_COUNT: AtomicU64 = AtomicU64::new(0);
26static FPS_ESTIMATE: AtomicU64 = AtomicU64::new(0);
27const FPS_REFRESH_PERIOD_TICKS: u64 = 100; // 100Hz timer => 1s
28pub(crate) static UI_SCALE: AtomicU8 = AtomicU8::new(1);
29const CLIPBOARD_CAP: usize = 8192;
30static CLIPBOARD: Mutex<([u8; CLIPBOARD_CAP], usize)> = Mutex::new(([0u8; CLIPBOARD_CAP], 0));
31
32pub static VGA_WRITER: Mutex<VgaWriter> = Mutex::new(VgaWriter::new());
33
34/// Returns whether available.
35#[inline]
36pub fn is_available() -> bool {
37    VGA_AVAILABLE.load(Ordering::Relaxed)
38}
39
40/// Performs the with writer operation.
41pub fn with_writer<R>(f: impl FnOnce(&mut VgaWriter) -> R) -> Option<R> {
42    if !is_available() {
43        return None;
44    }
45    let mut writer = VGA_WRITER.lock();
46    let mc_was_visible = writer.cursor.mc_visible;
47    let tc_was_visible = writer.cursor.tc_visible;
48    // Track mouse cursor position to detect if update_mouse_cursor was called inside f
49    let mc_coords_before = (writer.cursor.mc_x, writer.cursor.mc_y);
50
51    if mc_was_visible {
52        writer.mc_erase_hw();
53    }
54    if tc_was_visible {
55        writer.text_cursor_erase_hw();
56    }
57
58    let res = f(&mut writer);
59
60    // Only redraw mouse cursor if coordinates didn't change (update_mouse_cursor wasn't called)
61    // If update_mouse_cursor was called, it already redrew the cursor
62    let mc_coords_after = (writer.cursor.mc_x, writer.cursor.mc_y);
63    if writer.cursor.mc_visible && mc_coords_before == mc_coords_after {
64        writer.mc_save_hw();
65        writer.mc_draw_hw();
66    }
67    if writer.cursor.tc_visible {
68        writer.text_cursor_save_hw();
69        writer.text_cursor_draw_hw();
70    }
71
72    Some(res)
73}
74
75/// Performs the try with writer operation.
76pub fn try_with_writer<R>(f: impl FnOnce(&mut VgaWriter) -> R) -> Option<R> {
77    if !is_available() {
78        return None;
79    }
80    let mut writer = VGA_WRITER.try_lock()?;
81    let mc_was_visible = writer.cursor.mc_visible;
82    let tc_was_visible = writer.cursor.tc_visible;
83    // Track mouse cursor position to detect if update_mouse_cursor was called inside f
84    let mc_coords_before = (writer.cursor.mc_x, writer.cursor.mc_y);
85
86    if mc_was_visible {
87        writer.mc_erase_hw();
88    }
89    if tc_was_visible {
90        writer.text_cursor_erase_hw();
91    }
92
93    let res = f(&mut writer);
94
95    // Only redraw mouse cursor if coordinates didn't change (update_mouse_cursor wasn't called)
96    // If update_mouse_cursor was called, it already redrew the cursor
97    let mc_coords_after = (writer.cursor.mc_x, writer.cursor.mc_y);
98    if writer.cursor.mc_visible && mc_coords_before == mc_coords_after {
99        writer.mc_save_hw();
100        writer.mc_draw_hw();
101    }
102    if writer.cursor.tc_visible {
103        writer.text_cursor_save_hw();
104        writer.text_cursor_draw_hw();
105    }
106
107    Some(res)
108}
109
110/// Writes raw console text to the framebuffer console in a single writer batch.
111pub fn write_text(text: &str) {
112    if !is_available() {
113        crate::arch::x86_64::serial::_print(format_args!("{}", text));
114        return;
115    }
116    let _ = with_writer(|w| {
117        w.write_bytes(text);
118    });
119}
120
121/// Writes one console character to the framebuffer console.
122pub fn write_char(ch: char) {
123    if !is_available() {
124        crate::arch::x86_64::serial::_print(format_args!("{}", ch));
125        return;
126    }
127    let mut buf = [0u8; 4];
128    write_text(ch.encode_utf8(&mut buf));
129}
130
131/// Performs the current fps operation.
132pub(crate) fn current_fps(tick: u64) -> u64 {
133    let last_tick = FPS_LAST_TICK.load(Ordering::Relaxed);
134    let frames = PRESENTED_FRAMES.load(Ordering::Relaxed);
135
136    if last_tick == 0 {
137        let _ = FPS_LAST_TICK.compare_exchange(0, tick, Ordering::Relaxed, Ordering::Relaxed);
138        let _ =
139            FPS_LAST_FRAME_COUNT.compare_exchange(0, frames, Ordering::Relaxed, Ordering::Relaxed);
140        return FPS_ESTIMATE.load(Ordering::Relaxed);
141    }
142
143    let dt = tick.saturating_sub(last_tick);
144    if dt >= FPS_REFRESH_PERIOD_TICKS
145        && FPS_LAST_TICK
146            .compare_exchange(last_tick, tick, Ordering::Relaxed, Ordering::Relaxed)
147            .is_ok()
148    {
149        let last_frames = FPS_LAST_FRAME_COUNT.swap(frames, Ordering::Relaxed);
150        let df = frames.saturating_sub(last_frames);
151        let fps = if dt == 0 {
152            0
153        } else {
154            df.saturating_mul(100) / dt
155        };
156        FPS_ESTIMATE.store(fps, Ordering::Relaxed);
157    }
158
159    FPS_ESTIMATE.load(Ordering::Relaxed)
160}
161
162/// Performs the current ui scale operation.
163pub(crate) fn current_ui_scale() -> UiScale {
164    match UI_SCALE.load(Ordering::Relaxed) {
165        1 => UiScale::Compact,
166        3 => UiScale::Large,
167        _ => UiScale::Normal,
168    }
169}
170
171/// Performs the ui scale operation.
172pub fn ui_scale() -> UiScale {
173    current_ui_scale()
174}
175
176/// Sets ui scale.
177pub fn set_ui_scale(scale: UiScale) {
178    UI_SCALE.store(scale as u8, Ordering::Relaxed);
179}
180
181/// Performs the ui scale px operation.
182pub fn ui_scale_px(base: usize) -> usize {
183    let factor = current_ui_scale().factor();
184    let denom = UiScale::Normal.factor();
185    base.saturating_mul(factor) / denom
186}
187
188/// Performs the init operation.
189#[allow(clippy::too_many_arguments)]
190pub fn init(
191    fb_addr: u64,
192    fb_width: u32,
193    fb_height: u32,
194    pitch: u32,
195    bpp: u16,
196    red_size: u8,
197    red_shift: u8,
198    green_size: u8,
199    green_shift: u8,
200    blue_size: u8,
201    blue_shift: u8,
202) {
203    if fb_addr == 0 || fb_width == 0 || fb_height == 0 || pitch == 0 {
204        VGA_AVAILABLE.store(false, Ordering::Relaxed);
205        log::info!("Framebuffer console unavailable (no framebuffer)");
206        return;
207    }
208
209    if bpp != 24 && bpp != 32 {
210        VGA_AVAILABLE.store(false, Ordering::Relaxed);
211        log::info!("Framebuffer console unavailable (unsupported bpp={})", bpp);
212        return;
213    }
214
215    let fmt = PixelFormat {
216        bpp,
217        red_size,
218        red_shift,
219        green_size,
220        green_shift,
221        blue_size,
222        blue_shift,
223    };
224
225    // Ensure fb_addr is a virtual address in the HHDM.
226    // If it's already higher-half (>= HHDM), use it as-is.
227    // Otherwise, convert it via phys_to_virt.
228    //
229    // This fix works on VMWare Workstation
230    //
231    let hhdm = crate::memory::hhdm_offset();
232    let fb_virt = if hhdm != 0 && fb_addr < hhdm {
233        crate::memory::phys_to_virt(fb_addr)
234    } else {
235        fb_addr
236    };
237
238    let mut writer = VGA_WRITER.lock();
239    if writer.configure(
240        fb_virt as *mut u8,
241        fb_width as usize,
242        fb_height as usize,
243        pitch as usize,
244        fmt,
245    ) {
246        writer.set_color(Color::LightCyan, Color::Black);
247        writer.clear_with(RgbColor::new(0x12, 0x16, 0x1E));
248        // Decorative background mark for Strat9 identity.
249        let deco_w = (writer.width() / 3).clamp(120, 300);
250        let deco_h = (writer.height() / 4).clamp(90, 220);
251        let deco_x = writer.width().saturating_sub(deco_w + 24);
252        let deco_y = 24;
253        writer.draw_strata_stack(deco_x, deco_y, deco_w, deco_h);
254        writer.set_rgb_color(
255            RgbColor::new(0xA7, 0xD8, 0xD8),
256            RgbColor::new(0x12, 0x16, 0x1E),
257        );
258        writer.write_bytes("Strat9-OS v0.1.0\n");
259        writer.set_rgb_color(
260            RgbColor::new(0xE2, 0xE8, 0xF0),
261            RgbColor::new(0x12, 0x16, 0x1E),
262        );
263        VGA_AVAILABLE.store(true, Ordering::Relaxed);
264        // Initialise panic-screen raw framebuffer globals so the panic
265        // handler can draw directly without locking VGA_WRITER.
266        init_panic_fb_globals(
267            fb_virt,
268            fb_width as usize,
269            fb_height as usize,
270            pitch as usize,
271            bpp,
272            red_shift,
273            green_shift,
274            blue_shift,
275        );
276        // Initialise the live VGA debug writer geometry.
277        vga_debug_init(fb_width as usize, fb_height as usize);
278        log::info!(
279            "Framebuffer console enabled: {}x{} {}bpp pitch={}",
280            fb_width,
281            fb_height,
282            bpp,
283            pitch
284        );
285        drop(writer);
286        draw_boot_status_line(UiTheme::OCEAN_STATUS);
287    } else {
288        writer.enabled = false;
289        VGA_AVAILABLE.store(false, Ordering::Relaxed);
290        log::info!("Framebuffer console unavailable (font parse/init failed)");
291    }
292}
293
294// vga_print! / vga_println! macros are defined in mod.rs
295
296/// Performs the print operation.
297#[doc(hidden)]
298pub fn _print(args: fmt::Arguments) {
299    use core::fmt::Write;
300    if is_available() {
301        let _ = with_writer(|w| {
302            w.write_fmt(args).ok();
303        });
304        return;
305    }
306    crate::arch::x86_64::serial::_print(args);
307}
308
309/// Performs the width operation.
310pub fn width() -> usize {
311    if !is_available() {
312        return 0;
313    }
314    VGA_WRITER.lock().width()
315}
316
317/// Performs the height operation.
318pub fn height() -> usize {
319    if !is_available() {
320        return 0;
321    }
322    VGA_WRITER.lock().height()
323}
324
325/// Performs the screen size operation.
326pub fn screen_size() -> (usize, usize) {
327    (width(), height())
328}
329
330/// Performs the ui layout screen operation.
331pub fn ui_layout_screen() -> UiDockLayout {
332    UiDockLayout::from_screen()
333}
334
335/// Performs the glyph size operation.
336pub fn glyph_size() -> (usize, usize) {
337    if !is_available() {
338        return (0, 0);
339    }
340    VGA_WRITER.lock().glyph_size()
341}
342
343/// Performs the text cols operation.
344pub fn text_cols() -> usize {
345    if !is_available() {
346        return 0;
347    }
348    VGA_WRITER.lock().cols()
349}
350
351/// Performs the text rows operation.
352pub fn text_rows() -> usize {
353    if !is_available() {
354        return 0;
355    }
356    VGA_WRITER.lock().rows()
357}
358
359/// Returns text cursor.
360pub fn get_text_cursor() -> (usize, usize) {
361    if !is_available() {
362        return (0, 0);
363    }
364    let writer = VGA_WRITER.lock();
365    (writer.col, writer.row)
366}
367
368/// Sets text cursor.
369pub fn set_text_cursor(col: usize, row: usize) {
370    if !is_available() {
371        return;
372    }
373    VGA_WRITER.lock().set_cursor_cell(col, row);
374}
375
376/// Performs the double buffer mode operation.
377pub fn double_buffer_mode() -> bool {
378    DOUBLE_BUFFER_MODE.load(Ordering::Relaxed)
379}
380
381/// Sets double buffer mode.
382pub fn set_double_buffer_mode(enabled: bool) {
383    DOUBLE_BUFFER_MODE.store(enabled, Ordering::Relaxed);
384}
385
386/// Performs the draw text cursor operation.
387pub fn draw_text_cursor(color: RgbColor) {
388    if !is_available() {
389        return;
390    }
391    let mut writer = VGA_WRITER.lock();
392    writer.draw_text_cursor_overlay(color);
393}
394
395/// Performs the hide text cursor operation.
396pub fn hide_text_cursor() {
397    if !is_available() {
398        return;
399    }
400    let mut writer = VGA_WRITER.lock();
401    writer.hide_text_cursor();
402}
403
404/// Performs the framebuffer info operation.
405pub fn framebuffer_info() -> FramebufferInfo {
406    if !is_available() {
407        return FramebufferInfo {
408            available: false,
409            width: 0,
410            height: 0,
411            pitch: 0,
412            bpp: 0,
413            red_size: 0,
414            red_shift: 0,
415            green_size: 0,
416            green_shift: 0,
417            blue_size: 0,
418            blue_shift: 0,
419            text_cols: 0,
420            text_rows: 0,
421            glyph_w: 0,
422            glyph_h: 0,
423            double_buffer_mode: false,
424            double_buffer_enabled: false,
425            ui_scale: UiScale::Normal,
426        };
427    }
428    VGA_WRITER.lock().framebuffer_info()
429}
430
431/// Returns lightweight render metrics for profiling.
432pub fn render_stats() -> RenderStats {
433    RenderStats {
434        presented_frames: PRESENTED_FRAMES.load(Ordering::Relaxed),
435        estimated_fps: FPS_ESTIMATE.load(Ordering::Relaxed),
436        present_region_count: VGA_PRESENT_REGION_COUNT.load(Ordering::Relaxed),
437        present_pixel_count: VGA_PRESENT_PIXEL_COUNT.load(Ordering::Relaxed),
438    }
439}
440
441/// Sets text color.
442pub fn set_text_color(fg: RgbColor, bg: RgbColor) {
443    if !is_available() {
444        return;
445    }
446    VGA_WRITER.lock().set_rgb_color(fg, bg);
447}
448
449/// Sets clip rect.
450pub fn set_clip_rect(x: usize, y: usize, width: usize, height: usize) {
451    if !is_available() {
452        return;
453    }
454    VGA_WRITER.lock().set_clip_rect(x, y, width, height);
455}
456
457/// Performs the reset clip rect operation.
458pub fn reset_clip_rect() {
459    if !is_available() {
460        return;
461    }
462    VGA_WRITER.lock().reset_clip_rect();
463}
464
465/// Performs the begin frame operation.
466pub fn begin_frame() -> bool {
467    if !is_available() {
468        return false;
469    }
470    if !double_buffer_mode() {
471        return false;
472    }
473    VGA_WRITER.lock().enable_double_buffer()
474}
475
476/// Performs the end frame operation.
477pub fn end_frame() {
478    if !is_available() {
479        return;
480    }
481    let mut writer = VGA_WRITER.lock();
482    writer.present();
483    writer.disable_double_buffer(false);
484}
485
486/// Performs the present operation.
487pub fn present() {
488    if !is_available() {
489        return;
490    }
491    VGA_WRITER.lock().present();
492}
493
494/// Performs the draw pixel operation.
495pub fn draw_pixel(x: usize, y: usize, color: RgbColor) {
496    if !is_available() {
497        return;
498    }
499    VGA_WRITER.lock().draw_pixel(x, y, color);
500}
501
502/// Performs the draw pixel alpha operation.
503pub fn draw_pixel_alpha(x: usize, y: usize, color: RgbColor, alpha: u8) {
504    if !is_available() {
505        return;
506    }
507    VGA_WRITER.lock().draw_pixel_alpha(x, y, color, alpha);
508}
509
510/// Performs the draw line operation.
511pub fn draw_line(x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
512    if !is_available() {
513        return;
514    }
515    VGA_WRITER.lock().draw_line(x0, y0, x1, y1, color);
516}
517
518/// Performs the draw rect operation.
519pub fn draw_rect(x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
520    if !is_available() {
521        return;
522    }
523    VGA_WRITER.lock().draw_rect(x, y, width, height, color);
524}
525
526/// Performs the fill rect operation.
527pub fn fill_rect(x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
528    if !is_available() {
529        return;
530    }
531    VGA_WRITER.lock().fill_rect(x, y, width, height, color);
532}
533
534/// Performs the fill rect alpha operation.
535pub fn fill_rect_alpha(
536    x: usize,
537    y: usize,
538    width: usize,
539    height: usize,
540    color: RgbColor,
541    alpha: u8,
542) {
543    if !is_available() {
544        return;
545    }
546    VGA_WRITER
547        .lock()
548        .fill_rect_alpha(x, y, width, height, color, alpha);
549}
550
551/// Performs the blit rgb operation.
552pub fn blit_rgb(
553    dst_x: usize,
554    dst_y: usize,
555    src_width: usize,
556    src_height: usize,
557    pixels: &[RgbColor],
558) -> bool {
559    if !is_available() {
560        return false;
561    }
562    VGA_WRITER
563        .lock()
564        .blit_rgb(dst_x, dst_y, src_width, src_height, pixels)
565}
566
567/// Performs the blit rgb24 operation.
568pub fn blit_rgb24(
569    dst_x: usize,
570    dst_y: usize,
571    src_width: usize,
572    src_height: usize,
573    bytes: &[u8],
574) -> bool {
575    if !is_available() {
576        return false;
577    }
578    VGA_WRITER
579        .lock()
580        .blit_rgb24(dst_x, dst_y, src_width, src_height, bytes)
581}
582
583/// Performs the blit rgba operation.
584pub fn blit_rgba(
585    dst_x: usize,
586    dst_y: usize,
587    src_width: usize,
588    src_height: usize,
589    bytes: &[u8],
590    global_alpha: u8,
591) -> bool {
592    if !is_available() {
593        return false;
594    }
595    VGA_WRITER
596        .lock()
597        .blit_rgba(dst_x, dst_y, src_width, src_height, bytes, global_alpha)
598}
599
600/// Performs the blit sprite rgba operation.
601pub fn blit_sprite_rgba(
602    dst_x: usize,
603    dst_y: usize,
604    sprite: SpriteRgba<'_>,
605    global_alpha: u8,
606) -> bool {
607    if !is_available() {
608        return false;
609    }
610    VGA_WRITER
611        .lock()
612        .blit_sprite_rgba(dst_x, dst_y, sprite, global_alpha)
613}
614
615/// Performs the draw text at operation.
616pub fn draw_text_at(pixel_x: usize, pixel_y: usize, text: &str, fg: RgbColor, bg: RgbColor) {
617    if !is_available() {
618        return;
619    }
620    VGA_WRITER
621        .lock()
622        .draw_text_at(pixel_x, pixel_y, text, fg, bg);
623}
624
625/// Performs the draw text operation.
626pub fn draw_text(pixel_x: usize, pixel_y: usize, text: &str, opts: TextOptions) -> TextMetrics {
627    if !is_available() {
628        return TextMetrics {
629            width: 0,
630            height: 0,
631            lines: 0,
632        };
633    }
634    VGA_WRITER.lock().draw_text(pixel_x, pixel_y, text, opts)
635}
636
637/// Performs the measure text operation.
638pub fn measure_text(text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
639    if !is_available() {
640        return TextMetrics {
641            width: 0,
642            height: 0,
643            lines: 0,
644        };
645    }
646    VGA_WRITER.lock().measure_text(text, max_width, wrap)
647}
648
649/// Performs the ui clear operation.
650pub fn ui_clear(theme: UiTheme) {
651    let _ = with_writer(|w| w.clear_with(theme.background));
652}
653
654/// Performs the ui draw panel operation.
655pub fn ui_draw_panel(
656    x: usize,
657    y: usize,
658    width: usize,
659    height: usize,
660    title: &str,
661    body: &str,
662    theme: UiTheme,
663) {
664    let _ = with_writer(|w| {
665        if width < 8 || height < 8 {
666            return;
667        }
668        let (gw, gh) = w.glyph_size();
669        w.fill_rect(x, y, width, height, theme.panel_bg);
670        w.draw_rect(x, y, width, height, theme.panel_border);
671
672        let title_h = gh + 6;
673        w.fill_rect(
674            x.saturating_add(1),
675            y.saturating_add(1),
676            width.saturating_sub(2),
677            title_h,
678            theme.accent,
679        );
680        let title_opts = TextOptions {
681            fg: theme.text,
682            bg: theme.accent,
683            align: TextAlign::Left,
684            wrap: false,
685            max_width: Some(width.saturating_sub(10)),
686        };
687        w.draw_text(x.saturating_add(6), y.saturating_add(3), title, title_opts);
688
689        let body_opts = TextOptions {
690            fg: theme.text,
691            bg: theme.panel_bg,
692            align: TextAlign::Left,
693            wrap: true,
694            max_width: Some(width.saturating_sub(10)),
695        };
696        w.draw_text(
697            x.saturating_add(6),
698            y.saturating_add(title_h + 4),
699            body,
700            body_opts,
701        );
702
703        // Visual separator.
704        w.fill_rect(
705            x.saturating_add(1),
706            y.saturating_add(title_h + 1),
707            width.saturating_sub(2),
708            1,
709            theme.panel_border,
710        );
711        // Keep an implicit reference to glyph width to avoid dead code warning for gw in tiny fonts.
712        let _ = gw;
713    });
714}
715
716/// Performs the ui draw panel widget operation.
717pub fn ui_draw_panel_widget(panel: &UiPanel<'_>) {
718    ui_draw_panel(
719        panel.rect.x,
720        panel.rect.y,
721        panel.rect.w,
722        panel.rect.h,
723        panel.title,
724        panel.body,
725        panel.theme,
726    );
727}
728
729/// Performs the ui draw label operation.
730pub fn ui_draw_label(label: &UiLabel<'_>) {
731    let _ = with_writer(|w| {
732        w.draw_text(
733            label.rect.x,
734            label.rect.y,
735            label.text,
736            TextOptions {
737                fg: label.fg,
738                bg: label.bg,
739                align: label.align,
740                wrap: false,
741                max_width: Some(label.rect.w),
742            },
743        );
744    });
745}
746
747/// Performs the ui draw progress bar operation.
748pub fn ui_draw_progress_bar(bar: UiProgressBar) {
749    let _ = with_writer(|w| {
750        if bar.rect.w < 3 || bar.rect.h < 3 {
751            return;
752        }
753        let value = core::cmp::min(bar.value, 100) as usize;
754        w.fill_rect(bar.rect.x, bar.rect.y, bar.rect.w, bar.rect.h, bar.bg);
755        w.draw_rect(bar.rect.x, bar.rect.y, bar.rect.w, bar.rect.h, bar.border);
756        let inner_w = bar.rect.w.saturating_sub(2);
757        let fill_w = inner_w.saturating_mul(value) / 100;
758        if fill_w > 0 {
759            w.fill_rect(
760                bar.rect.x + 1,
761                bar.rect.y + 1,
762                fill_w,
763                bar.rect.h.saturating_sub(2),
764                bar.fg,
765            );
766        }
767    });
768}
769
770/// Performs the ui draw table operation.
771pub fn ui_draw_table(table: &UiTable) {
772    let _ = with_writer(|w| {
773        if table.rect.w < 8 || table.rect.h < 8 {
774            return;
775        }
776        let (_gw, gh) = w.glyph_size();
777        if gh == 0 {
778            return;
779        }
780
781        w.fill_rect(
782            table.rect.x,
783            table.rect.y,
784            table.rect.w,
785            table.rect.h,
786            table.theme.panel_bg,
787        );
788        w.draw_rect(
789            table.rect.x,
790            table.rect.y,
791            table.rect.w,
792            table.rect.h,
793            table.theme.panel_border,
794        );
795
796        let cols = core::cmp::max(1, table.headers.len());
797        let col_w = table.rect.w / cols;
798        let header_h = gh + 2;
799        w.fill_rect(
800            table.rect.x + 1,
801            table.rect.y + 1,
802            table.rect.w.saturating_sub(2),
803            header_h,
804            table.theme.accent,
805        );
806
807        for (i, h) in table.headers.iter().enumerate() {
808            let x = table.rect.x + i * col_w + 2;
809            w.draw_text(
810                x,
811                table.rect.y + 1,
812                h,
813                TextOptions {
814                    fg: table.theme.text,
815                    bg: table.theme.accent,
816                    align: TextAlign::Left,
817                    wrap: false,
818                    max_width: Some(col_w.saturating_sub(4)),
819                },
820            );
821        }
822
823        let mut y = table.rect.y + header_h + 2;
824        for row in &table.rows {
825            if y + gh > table.rect.y + table.rect.h {
826                break;
827            }
828            for c in 0..cols {
829                if c >= row.len() {
830                    continue;
831                }
832                let x = table.rect.x + c * col_w + 2;
833                w.draw_text(
834                    x,
835                    y,
836                    &row[c],
837                    TextOptions {
838                        fg: table.theme.text,
839                        bg: table.theme.panel_bg,
840                        align: TextAlign::Left,
841                        wrap: false,
842                        max_width: Some(col_w.saturating_sub(4)),
843                    },
844                );
845            }
846            y += gh;
847        }
848    });
849}
850
851/// Performs the draw strata stack operation.
852pub fn draw_strata_stack(origin_x: usize, origin_y: usize, layer_w: usize, layer_h: usize) {
853    if !is_available() {
854        return;
855    }
856    VGA_WRITER
857        .lock()
858        .draw_strata_stack(origin_x, origin_y, layer_w, layer_h);
859}
860//  Scrollback / scrollbar public API ==================================================================================================================================
861
862/// Scroll the console view up (backward in history) by `lines` lines.
863pub fn scroll_view_up(lines: usize) {
864    if !is_available() {
865        return;
866    }
867    VGA_WRITER.lock().scroll_view_up(lines);
868}
869
870/// Scroll the console view down (forward, toward live output) by `lines` lines.
871pub fn scroll_view_down(lines: usize) {
872    if !is_available() {
873        return;
874    }
875    VGA_WRITER.lock().scroll_view_down(lines);
876}
877
878/// Return immediately to the live (bottom) view.
879pub fn scroll_to_live() {
880    if !is_available() {
881        return;
882    }
883    let _ = try_with_writer(|w| {
884        w.scroll_to_live();
885    });
886}
887
888/// Handle a click at framebuffer pixel `(px_x, px_y)`.
889/// If the click lands on the scrollbar, jump the view accordingly.
890pub fn scrollbar_click(px_x: usize, px_y: usize) {
891    if !is_available() {
892        return;
893    }
894    let _ = try_with_writer(|w| {
895        w.scrollbar_click(px_x, px_y);
896    });
897}
898
899/// Drag the scrollbar to a given Y pixel coordinate.
900pub fn scrollbar_drag_to(px_y: usize) {
901    if !is_available() {
902        return;
903    }
904    let _ = try_with_writer(|w| {
905        w.scrollbar_drag_to(px_y);
906    });
907}
908
909/// Returns `true` if `(px_x, px_y)` falls within the scrollbar strip.
910pub fn scrollbar_hit_test(px_x: usize, px_y: usize) -> bool {
911    if !is_available() {
912        return false;
913    }
914    try_with_writer(|w| w.scrollbar_hit_test(px_x, px_y)).unwrap_or(false)
915}
916
917/// Updates mouse cursor.
918pub fn update_mouse_cursor(x: i32, y: i32) {
919    if !is_available() {
920        return;
921    }
922    let _ = try_with_writer(|w| {
923        w.update_mouse_cursor(x, y);
924    });
925}
926
927/// Performs the hide mouse cursor operation.
928pub fn hide_mouse_cursor() {
929    if !is_available() {
930        return;
931    }
932    let _ = try_with_writer(|w| {
933        w.hide_mouse_cursor();
934    });
935}
936
937/// Starts selection.
938pub fn start_selection(px: usize, py: usize) {
939    if !is_available() {
940        return;
941    }
942    let _ = try_with_writer(|w| {
943        w.start_selection(px, py);
944    });
945}
946
947/// Updates selection.
948pub fn update_selection(px: usize, py: usize) {
949    if !is_available() {
950        return;
951    }
952    let _ = try_with_writer(|w| {
953        w.update_selection(px, py);
954    });
955}
956
957/// Performs the end selection operation.
958pub fn end_selection() {
959    if !is_available() {
960        return;
961    }
962    let _ = try_with_writer(|w| {
963        w.end_selection();
964    });
965}
966
967/// Performs the clear selection operation.
968pub fn clear_selection() {
969    if !is_available() {
970        return;
971    }
972    let _ = try_with_writer(|w| {
973        w.clear_selection();
974    });
975}
976
977/// Returns clipboard text.
978pub fn get_clipboard_text(buf: &mut [u8]) -> usize {
979    if let Some(clip) = CLIPBOARD.try_lock() {
980        let n = clip.1.min(buf.len());
981        buf[..n].copy_from_slice(&clip.0[..n]);
982        n
983    } else {
984        0
985    }
986}