1use alloc::{collections::VecDeque, format, string::String, vec::Vec};
8use core::{
9 fmt,
10 sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering},
11};
12use spin::Mutex;
13
14static VGA_AVAILABLE: AtomicBool = AtomicBool::new(false);
16static STATUS_LAST_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
17const STATUS_REFRESH_PERIOD_TICKS: u64 = 100; static 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#[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 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 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 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 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 pub fn from_screen() -> Self {
213 Self {
214 remaining: UiRect::new(0, 0, width(), height()),
215 }
216 }
217
218 pub const fn from_rect(rect: UiRect) -> Self {
220 Self { remaining: rect }
221 }
222
223 pub const fn remaining(&self) -> UiRect {
225 self.remaining
226 }
227
228 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, 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 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 pub fn push_line(&mut self, text: &str) {
336 self.push_colored_line(text, self.fg);
337 }
338
339 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 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 pub fn clear(&mut self) {
358 self.lines.clear();
359 }
360
361 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
426fn 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#[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 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 fn pack_rgb(&self, r: u8, g: u8, b: u8) -> u32 {
518 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
552fn parse_psf(font: &[u8]) -> Option<FontInfo> {
554 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 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
601fn 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
640fn 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 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
676const SCROLLBAR_W: usize = 12;
678const MAX_SCROLLBACK: usize = 500;
680
681const CURSOR_W: usize = 12;
683const CURSOR_H: usize = 16;
684#[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
705const CLIPBOARD_CAP: usize = 8192;
707static CLIPBOARD: spin::Mutex<([u8; CLIPBOARD_CAP], usize)> =
708 spin::Mutex::new(([0u8; CLIPBOARD_CAP], 0));
709
710#[derive(Clone, Copy)]
712struct SbCell {
713 ch: char,
714 fg: u32, bg: u32, }
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 sb_rows: VecDeque<Vec<SbCell>>,
747 sb_cur_row: Vec<SbCell>,
749 scroll_offset: usize,
751
752 mc_x: i32,
754 mc_y: i32,
755 mc_visible: bool,
756 mc_save: [u32; CURSOR_W * CURSOR_H],
757
758 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 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 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 #[inline]
886 fn pack_color(&self, color: RgbColor) -> u32 {
887 self.fmt.pack_rgb(color.r, color.g, color.b)
888 }
889
890 fn unpack_color(&self, value: u32) -> RgbColor {
892 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 pub fn set_color(&mut self, fg: Color, bg: Color) {
918 self.set_rgb_color(fg.into(), bg.into());
919 }
920
921 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 pub fn text_colors(&self) -> (RgbColor, RgbColor) {
929 (self.unpack_color(self.fg), self.unpack_color(self.bg))
930 }
931
932 pub fn width(&self) -> usize {
934 self.fb_width
935 }
936
937 pub fn height(&self) -> usize {
939 self.fb_height
940 }
941
942 pub fn cols(&self) -> usize {
944 self.cols
945 }
946
947 pub fn rows(&self) -> usize {
949 self.rows
950 }
951
952 pub fn glyph_size(&self) -> (usize, usize) {
954 (self.font_info.glyph_w, self.font_info.glyph_h)
955 }
956
957 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 fn text_area_height(&self) -> usize {
968 self.fb_height.saturating_sub(self.status_bar_height)
969 }
970
971 pub fn enabled(&self) -> bool {
973 self.enabled
974 }
975
976 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 #[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 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 fn clear_dirty(&mut self) {
1037 self.dirty_rect = None;
1038 }
1039
1040 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 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 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 fn draw_to_back_buffer(&self) -> bool {
1095 self.draw_to_back && self.back_buffer.is_some()
1096 }
1097
1098 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 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 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 let src = unsafe { buf_ptr.add(y * fb_width + sx) as *const u8 };
1159 let dst_off = y * pitch + sx * 4;
1160 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 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 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 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 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 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 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 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 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 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 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 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 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 pub fn clear(&mut self) {
1434 self.clear_with(self.unpack_color(self.bg));
1435 }
1436
1437 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn write_char(&mut self, c: char) {
2115 if !self.enabled {
2116 return;
2117 }
2118 let c = normalize_console_char(c);
2119
2120 self.sb_mirror_char(c);
2122 if self.scroll_offset > 0 {
2124 return;
2125 }
2126 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 fn write_bytes(&mut self, s: &str) {
2161 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 self.draw_scrollbar_inner();
2179 }
2180
2181 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 #[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 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; 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 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 let thumb_y = if self.scroll_offset == 0 || avail == 0 {
2271 avail } 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 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 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 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 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 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 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 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 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 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 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 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
2503fn normalize_console_char(ch: char) -> char {
2505 match ch {
2506 '\n' | '\r' | '\t' | '\u{8}' => ch,
2507 c if c.is_control() => '\0',
2508 '\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 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#[inline]
2534pub fn is_available() -> bool {
2535 VGA_AVAILABLE.load(Ordering::Relaxed)
2536}
2537
2538pub 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
2547fn 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
2562fn 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
2571fn 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
2602fn 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
2611pub fn ui_scale() -> UiScale {
2613 current_ui_scale()
2614}
2615
2616pub fn set_ui_scale(scale: UiScale) {
2618 UI_SCALE.store(scale as u8, Ordering::Relaxed);
2619}
2620
2621pub 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
2628fn format_mem_usage() -> String {
2630 let lock = crate::memory::buddy::get_allocator();
2631 let Some(guard) = lock.try_lock() else {
2632 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
2646fn 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
2662fn 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#[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 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 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#[macro_export]
2790macro_rules! vga_print {
2791 ($($arg:tt)*) => {
2792 $crate::arch::x86_64::vga::_print(format_args!($($arg)*));
2793 };
2794}
2795
2796#[macro_export]
2798macro_rules! vga_println {
2799 () => ($crate::vga_print!("\n"));
2800 ($($arg:tt)*) => ($crate::vga_print!("{}\n", format_args!($($arg)*)));
2801}
2802
2803#[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 fn default() -> Self {
2823 Self {
2824 fg: RgbColor::LIGHT_GREY,
2825 bg: RgbColor::BLACK,
2826 }
2827 }
2828}
2829
2830impl Canvas {
2831 pub const fn new(fg: RgbColor, bg: RgbColor) -> Self {
2833 Self { fg, bg }
2834 }
2835
2836 pub fn set_fg(&mut self, fg: RgbColor) {
2838 self.fg = fg;
2839 }
2840
2841 pub fn set_bg(&mut self, bg: RgbColor) {
2843 self.bg = bg;
2844 }
2845
2846 pub fn set_colors(&mut self, fg: RgbColor, bg: RgbColor) {
2848 self.fg = fg;
2849 self.bg = bg;
2850 }
2851
2852 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 pub fn reset_clip_rect(&self) {
2859 reset_clip_rect();
2860 }
2861
2862 pub fn clear(&self) {
2864 fill_rect(0, 0, width(), height(), self.bg);
2865 }
2866
2867 pub fn pixel(&self, x: usize, y: usize) {
2869 draw_pixel(x, y, self.fg);
2870 }
2871
2872 pub fn line(&self, x0: isize, y0: isize, x1: isize, y1: isize) {
2874 draw_line(x0, y0, x1, y1, self.fg);
2875 }
2876
2877 pub fn rect(&self, x: usize, y: usize, w: usize, h: usize) {
2879 draw_rect(x, y, w, h, self.fg);
2880 }
2881
2882 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 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 pub fn text(&self, x: usize, y: usize, text: &str) {
2894 draw_text_at(x, y, text, self.fg, self.bg);
2895 }
2896
2897 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 pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
2923 measure_text(text, max_width, wrap)
2924 }
2925
2926 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 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 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 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 pub fn begin_frame(&self) -> bool {
2962 begin_frame()
2963 }
2964
2965 pub fn end_frame(&self) {
2967 end_frame();
2968 }
2969
2970 pub fn ui_clear(&self, theme: UiTheme) {
2972 ui_clear(theme);
2973 }
2974
2975 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 pub fn ui_status_bar(&self, left: &str, right: &str, theme: UiTheme) {
2991 ui_draw_status_bar(left, right, theme);
2992 }
2993
2994 pub fn system_status_line(&self, theme: UiTheme) {
2996 draw_system_status_line(theme);
2997 }
2998
2999 pub fn layout_screen(&self) -> UiDockLayout {
3001 UiDockLayout::from_screen()
3002 }
3003
3004 pub fn ui_label(&self, label: &UiLabel<'_>) {
3006 ui_draw_label(label);
3007 }
3008
3009 pub fn ui_progress_bar(&self, bar: UiProgressBar) {
3011 ui_draw_progress_bar(bar);
3012 }
3013
3014 pub fn ui_table(&self, table: &UiTable) {
3016 ui_draw_table(table);
3017 }
3018}
3019
3020pub fn width() -> usize {
3022 if !is_available() {
3023 return 0;
3024 }
3025 VGA_WRITER.lock().width()
3026}
3027
3028pub fn height() -> usize {
3030 if !is_available() {
3031 return 0;
3032 }
3033 VGA_WRITER.lock().height()
3034}
3035
3036pub fn screen_size() -> (usize, usize) {
3038 (width(), height())
3039}
3040
3041pub fn ui_layout_screen() -> UiDockLayout {
3043 UiDockLayout::from_screen()
3044}
3045
3046pub fn glyph_size() -> (usize, usize) {
3048 if !is_available() {
3049 return (0, 0);
3050 }
3051 VGA_WRITER.lock().glyph_size()
3052}
3053
3054pub fn text_cols() -> usize {
3056 if !is_available() {
3057 return 0;
3058 }
3059 VGA_WRITER.lock().cols()
3060}
3061
3062pub fn text_rows() -> usize {
3064 if !is_available() {
3065 return 0;
3066 }
3067 VGA_WRITER.lock().rows()
3068}
3069
3070pub 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
3079pub 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
3087pub fn double_buffer_mode() -> bool {
3089 DOUBLE_BUFFER_MODE.load(Ordering::Relaxed)
3090}
3091
3092pub fn set_double_buffer_mode(enabled: bool) {
3094 DOUBLE_BUFFER_MODE.store(enabled, Ordering::Relaxed);
3095}
3096
3097pub 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 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
3117pub 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
3144pub 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
3152pub 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
3160pub fn reset_clip_rect() {
3162 if !is_available() {
3163 return;
3164 }
3165 VGA_WRITER.lock().reset_clip_rect();
3166}
3167
3168pub 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
3179pub 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
3189pub fn present() {
3191 if !is_available() {
3192 return;
3193 }
3194 VGA_WRITER.lock().present();
3195}
3196
3197pub 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
3205pub 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
3213pub 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
3221pub 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
3229pub 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
3237pub 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
3254pub 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
3270pub 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
3286pub 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
3303pub 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
3318pub 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
3328pub 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
3340pub 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
3352pub fn ui_clear(theme: UiTheme) {
3354 let _ = with_writer(|w| w.clear_with(theme.background));
3355}
3356
3357pub 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 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 let _ = gw;
3416 });
3417}
3418
3419pub 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
3432pub 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
3450pub 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
3473pub 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
3554pub 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
3561pub 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
3576pub 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
3591pub 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
3607fn 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
3619fn 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
3654struct StackStr<const N: usize> {
3656 buf: [u8; N],
3657 len: usize,
3658}
3659
3660impl<const N: usize> StackStr<N> {
3661 const fn new() -> Self {
3663 Self {
3664 buf: [0; N],
3665 len: 0,
3666 }
3667 }
3668 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 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
3686pub 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
3784fn 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 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
3806 f.write_str(self.as_str())
3807 }
3808}
3809
3810pub 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
3832pub 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}
3841pub fn scroll_view_up(lines: usize) {
3845 if !is_available() {
3846 return;
3847 }
3848 VGA_WRITER.lock().scroll_view_up(lines);
3849}
3850
3851pub fn scroll_view_down(lines: usize) {
3853 if !is_available() {
3854 return;
3855 }
3856 VGA_WRITER.lock().scroll_view_down(lines);
3857}
3858
3859pub 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
3869pub 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
3880pub 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
3890pub 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
3902pub 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
3912pub 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
3922pub 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
3932pub 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
3942pub 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
3952pub 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
3962pub 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}