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