1use alloc::{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 STATUS_LAST_IP_REFRESH_TICK: AtomicU64 = AtomicU64::new(0);
19const STATUS_IP_REFRESH_PERIOD_TICKS: u64 = 3_000; static PRESENTED_FRAMES: AtomicU64 = AtomicU64::new(0);
21static FPS_LAST_TICK: AtomicU64 = AtomicU64::new(0);
22static FPS_LAST_FRAME_COUNT: AtomicU64 = AtomicU64::new(0);
23static FPS_ESTIMATE: AtomicU64 = AtomicU64::new(0);
24static VGA_PRESENT_REGION_COUNT: AtomicU64 = AtomicU64::new(0);
25static VGA_PRESENT_PIXEL_COUNT: AtomicU64 = AtomicU64::new(0);
26static DOUBLE_BUFFER_MODE: AtomicBool = AtomicBool::new(false);
27static UI_SCALE: AtomicU8 = AtomicU8::new(1);
28
29const FONT_PSF: &[u8] = include_bytes!("fonts/zap-ext-light20.psf");
30
31#[allow(dead_code)]
33#[derive(Debug, Clone, Copy, PartialEq)]
34#[repr(u8)]
35pub enum Color {
36 Black = 0x0,
37 Blue = 0x1,
38 Green = 0x2,
39 Cyan = 0x3,
40 Red = 0x4,
41 Magenta = 0x5,
42 Brown = 0x6,
43 LightGrey = 0x7,
44 DarkGrey = 0x8,
45 LightBlue = 0x9,
46 LightGreen = 0xA,
47 LightCyan = 0xB,
48 LightRed = 0xC,
49 LightMagenta = 0xD,
50 Yellow = 0xE,
51 White = 0xF,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub struct RgbColor {
56 pub r: u8,
57 pub g: u8,
58 pub b: u8,
59}
60
61impl RgbColor {
62 pub const fn new(r: u8, g: u8, b: u8) -> Self {
64 Self { r, g, b }
65 }
66
67 pub const BLACK: Self = Self::new(0x00, 0x00, 0x00);
68 pub const WHITE: Self = Self::new(0xFF, 0xFF, 0xFF);
69 pub const RED: Self = Self::new(0xFF, 0x00, 0x00);
70 pub const GREEN: Self = Self::new(0x00, 0xFF, 0x00);
71 pub const BLUE: Self = Self::new(0x00, 0x00, 0xFF);
72 pub const CYAN: Self = Self::new(0x00, 0xFF, 0xFF);
73 pub const MAGENTA: Self = Self::new(0xFF, 0x00, 0xFF);
74 pub const YELLOW: Self = Self::new(0xFF, 0xFF, 0x00);
75 pub const LIGHT_GREY: Self = Self::new(0xAA, 0xAA, 0xAA);
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum TextAlign {
80 Left,
81 Center,
82 Right,
83}
84
85#[derive(Debug, Clone, Copy)]
86pub struct TextOptions {
87 pub fg: RgbColor,
88 pub bg: RgbColor,
89 pub align: TextAlign,
90 pub wrap: bool,
91 pub max_width: Option<usize>,
92}
93
94impl TextOptions {
95 pub const fn new(fg: RgbColor, bg: RgbColor) -> Self {
97 Self {
98 fg,
99 bg,
100 align: TextAlign::Left,
101 wrap: false,
102 max_width: None,
103 }
104 }
105}
106
107#[derive(Debug, Clone, Copy)]
108pub struct TextMetrics {
109 pub width: usize,
110 pub height: usize,
111 pub lines: usize,
112}
113
114#[derive(Debug, Clone, Copy)]
115pub struct SpriteRgba<'a> {
116 pub width: usize,
117 pub height: usize,
118 pub pixels: &'a [u8],
119}
120
121#[derive(Debug, Clone, Copy)]
122pub struct UiTheme {
123 pub background: RgbColor,
124 pub panel_bg: RgbColor,
125 pub panel_border: RgbColor,
126 pub text: RgbColor,
127 pub accent: RgbColor,
128 pub status_bg: RgbColor,
129 pub status_text: RgbColor,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum UiScale {
134 Compact = 1,
135 Normal = 2,
136 Large = 3,
137}
138
139impl UiScale {
140 pub const fn factor(self) -> usize {
142 self as usize
143 }
144}
145
146impl UiTheme {
147 pub const SLATE: Self = Self {
148 background: RgbColor::new(0x12, 0x16, 0x1E),
149 panel_bg: RgbColor::new(0x1A, 0x22, 0x2C),
150 panel_border: RgbColor::new(0x3D, 0x52, 0x66),
151 text: RgbColor::new(0xE2, 0xE8, 0xF0),
152 accent: RgbColor::new(0x4F, 0xB3, 0xB3),
153 status_bg: RgbColor::new(0x0E, 0x13, 0x1A),
154 status_text: RgbColor::new(0xD3, 0xDE, 0xEA),
155 };
156
157 pub const SAND: Self = Self {
158 background: RgbColor::new(0xFA, 0xF6, 0xEF),
159 panel_bg: RgbColor::new(0xF1, 0xE8, 0xD8),
160 panel_border: RgbColor::new(0xA6, 0x8F, 0x6A),
161 text: RgbColor::new(0x2B, 0x2B, 0x2B),
162 accent: RgbColor::new(0x1F, 0x7A, 0x8C),
163 status_bg: RgbColor::new(0xE6, 0xD7, 0xBF),
164 status_text: RgbColor::new(0x2B, 0x2B, 0x2B),
165 };
166
167 pub const OCEAN_STATUS: Self = Self {
168 background: RgbColor::new(0x12, 0x16, 0x1E),
169 panel_bg: RgbColor::new(0x1A, 0x22, 0x2C),
170 panel_border: RgbColor::new(0x3D, 0x52, 0x66),
171 text: RgbColor::new(0xE2, 0xE8, 0xF0),
172 accent: RgbColor::new(0x4F, 0xB3, 0xB3),
173 status_bg: RgbColor::new(0x1B, 0x4D, 0x8A),
174 status_text: RgbColor::new(0xF5, 0xFA, 0xFF),
175 };
176}
177
178#[derive(Debug, Clone)]
179struct StatusLineInfo {
180 hostname: String,
181 ip: String,
182}
183
184static STATUS_LINE_INFO: Mutex<Option<StatusLineInfo>> = Mutex::new(None);
185
186#[derive(Debug, Clone, Copy, Default)]
187pub struct UiRect {
188 pub x: usize,
189 pub y: usize,
190 pub w: usize,
191 pub h: usize,
192}
193
194impl UiRect {
195 pub const fn new(x: usize, y: usize, w: usize, h: usize) -> Self {
197 Self { x, y, w, h }
198 }
199}
200
201#[derive(Debug, Clone, Copy)]
202pub enum DockEdge {
203 Top,
204 Bottom,
205 Left,
206 Right,
207}
208
209#[derive(Debug, Clone, Copy)]
210pub struct UiDockLayout {
211 remaining: UiRect,
212}
213
214impl UiDockLayout {
215 pub fn from_screen() -> Self {
217 Self {
218 remaining: UiRect::new(0, 0, width(), height()),
219 }
220 }
221
222 pub const fn from_rect(rect: UiRect) -> Self {
224 Self { remaining: rect }
225 }
226
227 pub const fn remaining(&self) -> UiRect {
229 self.remaining
230 }
231
232 pub fn dock(&mut self, edge: DockEdge, size: usize) -> UiRect {
234 match edge {
235 DockEdge::Top => {
236 let h = core::cmp::min(size, self.remaining.h);
237 let out = UiRect::new(self.remaining.x, self.remaining.y, self.remaining.w, h);
238 self.remaining.y = self.remaining.y.saturating_add(h);
239 self.remaining.h = self.remaining.h.saturating_sub(h);
240 out
241 }
242 DockEdge::Bottom => {
243 let h = core::cmp::min(size, self.remaining.h);
244 let y = self
245 .remaining
246 .y
247 .saturating_add(self.remaining.h.saturating_sub(h));
248 let out = UiRect::new(self.remaining.x, y, self.remaining.w, h);
249 self.remaining.h = self.remaining.h.saturating_sub(h);
250 out
251 }
252 DockEdge::Left => {
253 let w = core::cmp::min(size, self.remaining.w);
254 let out = UiRect::new(self.remaining.x, self.remaining.y, w, self.remaining.h);
255 self.remaining.x = self.remaining.x.saturating_add(w);
256 self.remaining.w = self.remaining.w.saturating_sub(w);
257 out
258 }
259 DockEdge::Right => {
260 let w = core::cmp::min(size, self.remaining.w);
261 let x = self
262 .remaining
263 .x
264 .saturating_add(self.remaining.w.saturating_sub(w));
265 let out = UiRect::new(x, self.remaining.y, w, self.remaining.h);
266 self.remaining.w = self.remaining.w.saturating_sub(w);
267 out
268 }
269 }
270 }
271}
272
273#[derive(Debug, Clone)]
274pub struct UiLabel<'a> {
275 pub rect: UiRect,
276 pub text: &'a str,
277 pub fg: RgbColor,
278 pub bg: RgbColor,
279 pub align: TextAlign,
280}
281
282#[derive(Debug, Clone)]
283pub struct UiPanel<'a> {
284 pub rect: UiRect,
285 pub title: &'a str,
286 pub body: &'a str,
287 pub theme: UiTheme,
288}
289
290#[derive(Debug, Clone, Copy)]
291pub struct UiProgressBar {
292 pub rect: UiRect,
293 pub value: u8, pub fg: RgbColor,
295 pub bg: RgbColor,
296 pub border: RgbColor,
297}
298
299#[derive(Debug, Clone)]
300pub struct UiTable {
301 pub rect: UiRect,
302 pub headers: Vec<String>,
303 pub rows: Vec<Vec<String>>,
304 pub theme: UiTheme,
305}
306
307#[derive(Debug, Clone)]
308struct TerminalLine {
309 text: String,
310 fg: RgbColor,
311}
312
313#[derive(Debug, Clone)]
314pub struct TerminalWidget {
315 pub rect: UiRect,
316 pub title: String,
317 pub fg: RgbColor,
318 pub bg: RgbColor,
319 pub border: RgbColor,
320 pub max_lines: usize,
321 lines: Vec<TerminalLine>,
322}
323
324impl TerminalWidget {
325 pub fn new(rect: UiRect, max_lines: usize) -> Self {
327 Self {
328 rect,
329 title: String::from("Terminal"),
330 fg: RgbColor::LIGHT_GREY,
331 bg: RgbColor::new(0x0F, 0x14, 0x1B),
332 border: RgbColor::new(0x3D, 0x52, 0x66),
333 max_lines: core::cmp::max(1, max_lines),
334 lines: Vec::new(),
335 }
336 }
337
338 pub fn push_line(&mut self, text: &str) {
340 self.push_colored_line(text, self.fg);
341 }
342
343 pub fn push_ansi_line(&mut self, text: &str) {
345 let (fg, stripped) = parse_ansi_color_prefix(text, self.fg);
346 self.push_colored_line(&stripped, fg);
347 }
348
349 fn push_colored_line(&mut self, text: &str, fg: RgbColor) {
351 if self.lines.len() >= self.max_lines {
352 self.lines.remove(0);
353 }
354 self.lines.push(TerminalLine {
355 text: String::from(text),
356 fg,
357 });
358 }
359
360 pub fn clear(&mut self) {
362 self.lines.clear();
363 }
364
365 pub fn draw(&self) {
367 let _ = with_writer(|w| {
368 if self.rect.w < 8 || self.rect.h < 8 {
369 return;
370 }
371 let (gw, gh) = w.glyph_size();
372 if gw == 0 || gh == 0 {
373 return;
374 }
375
376 w.fill_rect(self.rect.x, self.rect.y, self.rect.w, self.rect.h, self.bg);
377 w.draw_rect(
378 self.rect.x,
379 self.rect.y,
380 self.rect.w,
381 self.rect.h,
382 self.border,
383 );
384
385 let title_h = gh + 2;
386 w.fill_rect(
387 self.rect.x + 1,
388 self.rect.y + 1,
389 self.rect.w.saturating_sub(2),
390 title_h,
391 self.border,
392 );
393 w.draw_text(
394 self.rect.x + 4,
395 self.rect.y + 1,
396 &self.title,
397 TextOptions {
398 fg: RgbColor::WHITE,
399 bg: self.border,
400 align: TextAlign::Left,
401 wrap: false,
402 max_width: Some(self.rect.w.saturating_sub(8)),
403 },
404 );
405
406 let content_y = self.rect.y + title_h + 2;
407 let content_h = self.rect.h.saturating_sub(title_h + 3);
408 let rows = core::cmp::max(1, content_h / gh);
409 let start = self.lines.len().saturating_sub(rows);
410
411 for (idx, line) in self.lines.iter().skip(start).enumerate() {
412 let y = content_y + idx * gh;
413 w.draw_text(
414 self.rect.x + 4,
415 y,
416 &line.text,
417 TextOptions {
418 fg: line.fg,
419 bg: self.bg,
420 align: TextAlign::Left,
421 wrap: false,
422 max_width: Some(self.rect.w.saturating_sub(8)),
423 },
424 );
425 }
426 });
427 }
428}
429
430fn parse_ansi_color_prefix(input: &str, default_fg: RgbColor) -> (RgbColor, String) {
432 let bytes = input.as_bytes();
433 if !bytes.starts_with(b"\x1b[") {
434 return (default_fg, String::from(input));
435 }
436 let Some(mpos) = bytes.iter().position(|b| *b == b'm') else {
437 return (default_fg, String::from(input));
438 };
439 let code = &input[2..mpos];
440 let rest = &input[mpos + 1..];
441 let fg = match code {
442 "30" => RgbColor::BLACK,
443 "31" => RgbColor::new(0xFF, 0x55, 0x55),
444 "32" => RgbColor::new(0x66, 0xFF, 0x66),
445 "33" => RgbColor::new(0xFF, 0xDD, 0x66),
446 "34" => RgbColor::new(0x77, 0xAA, 0xFF),
447 "35" => RgbColor::new(0xFF, 0x77, 0xFF),
448 "36" => RgbColor::new(0x77, 0xFF, 0xFF),
449 "37" | "0" => RgbColor::LIGHT_GREY,
450 _ => default_fg,
451 };
452 (fg, String::from(rest))
453}
454
455#[inline]
457fn color_to_rgb(c: Color) -> (u8, u8, u8) {
458 match c {
459 Color::Black => (0x00, 0x00, 0x00),
460 Color::Blue => (0x00, 0x00, 0xAA),
461 Color::Green => (0x00, 0xAA, 0x00),
462 Color::Cyan => (0x00, 0xAA, 0xAA),
463 Color::Red => (0xAA, 0x00, 0x00),
464 Color::Magenta => (0xAA, 0x00, 0xAA),
465 Color::Brown => (0xAA, 0x55, 0x00),
466 Color::LightGrey => (0xAA, 0xAA, 0xAA),
467 Color::DarkGrey => (0x55, 0x55, 0x55),
468 Color::LightBlue => (0x55, 0x55, 0xFF),
469 Color::LightGreen => (0x55, 0xFF, 0x55),
470 Color::LightCyan => (0x55, 0xFF, 0xFF),
471 Color::LightRed => (0xFF, 0x55, 0x55),
472 Color::LightMagenta => (0xFF, 0x55, 0xFF),
473 Color::Yellow => (0xFF, 0xFF, 0x55),
474 Color::White => (0xFF, 0xFF, 0xFF),
475 }
476}
477
478impl From<Color> for RgbColor {
479 fn from(value: Color) -> Self {
481 let (r, g, b) = color_to_rgb(value);
482 Self::new(r, g, b)
483 }
484}
485
486#[derive(Clone, Copy)]
487struct PixelFormat {
488 bpp: u16,
489 red_size: u8,
490 red_shift: u8,
491 green_size: u8,
492 green_shift: u8,
493 blue_size: u8,
494 blue_shift: u8,
495}
496
497#[derive(Debug, Clone, Copy)]
498pub struct FramebufferInfo {
499 pub available: bool,
500 pub width: usize,
501 pub height: usize,
502 pub pitch: usize,
503 pub bpp: u16,
504 pub red_size: u8,
505 pub red_shift: u8,
506 pub green_size: u8,
507 pub green_shift: u8,
508 pub blue_size: u8,
509 pub blue_shift: u8,
510 pub text_cols: usize,
511 pub text_rows: usize,
512 pub glyph_w: usize,
513 pub glyph_h: usize,
514 pub double_buffer_mode: bool,
515 pub double_buffer_enabled: bool,
516 pub ui_scale: UiScale,
517}
518
519#[derive(Debug, Clone, Copy)]
520pub struct RenderStats {
521 pub presented_frames: u64,
522 pub estimated_fps: u64,
523 pub present_region_count: u64,
524 pub present_pixel_count: u64,
525}
526
527impl PixelFormat {
528 fn pack_rgb(&self, r: u8, g: u8, b: u8) -> u32 {
530 fn scale(v: u8, bits: u8) -> u32 {
532 if bits == 0 {
533 0
534 } else if bits >= 8 {
535 (v as u32) << (bits - 8)
536 } else {
537 (v as u32) >> (8 - bits)
538 }
539 }
540
541 (scale(r, self.red_size) << self.red_shift)
542 | (scale(g, self.green_size) << self.green_shift)
543 | (scale(b, self.blue_size) << self.blue_shift)
544 }
545}
546
547struct FontInfo {
548 glyph_count: usize,
549 bytes_per_glyph: usize,
550 glyph_w: usize,
551 glyph_h: usize,
552 data_offset: usize,
553 unicode_table_offset: Option<usize>,
554}
555
556#[derive(Clone, Copy)]
557struct ClipRect {
558 x: usize,
559 y: usize,
560 w: usize,
561 h: usize,
562}
563
564#[derive(Clone, Copy)]
565struct DirtyRegionSet {
566 rects: [ClipRect; MAX_DIRTY_RECTS],
567 len: usize,
568}
569
570impl DirtyRegionSet {
571 const fn empty() -> Self {
572 Self {
573 rects: [ClipRect {
574 x: 0,
575 y: 0,
576 w: 0,
577 h: 0,
578 }; MAX_DIRTY_RECTS],
579 len: 0,
580 }
581 }
582}
583
584fn parse_psf(font: &[u8]) -> Option<FontInfo> {
586 if font.len() >= 4 && font[0] == 0x36 && font[1] == 0x04 {
588 let mode = font[2];
589 let glyph_count = if (mode & 0x01) != 0 { 512 } else { 256 };
590 let glyph_h = font[3] as usize;
591 let bytes_per_glyph = glyph_h;
592 return Some(FontInfo {
593 glyph_count,
594 bytes_per_glyph,
595 glyph_w: 8,
596 glyph_h,
597 data_offset: 4,
598 unicode_table_offset: None,
599 });
600 }
601
602 if font.len() >= 32 && font[0] == 0x72 && font[1] == 0xB5 && font[2] == 0x4A && font[3] == 0x86
604 {
605 let rd_u32 = |off: usize| -> u32 {
606 u32::from_le_bytes([font[off], font[off + 1], font[off + 2], font[off + 3]])
607 };
608 let headersize = rd_u32(8) as usize;
609 let flags = rd_u32(12);
610 let glyph_count = rd_u32(16) as usize;
611 let bytes_per_glyph = rd_u32(20) as usize;
612 let glyph_h = rd_u32(24) as usize;
613 let glyph_w = rd_u32(28) as usize;
614 let glyph_bytes = glyph_count.saturating_mul(bytes_per_glyph);
615 let unicode_table_offset = if (flags & 1) != 0 {
616 Some(headersize.saturating_add(glyph_bytes))
617 } else {
618 None
619 };
620 return Some(FontInfo {
621 glyph_count,
622 bytes_per_glyph,
623 glyph_w,
624 glyph_h,
625 data_offset: headersize,
626 unicode_table_offset,
627 });
628 }
629
630 None
631}
632
633fn decode_utf8_at(bytes: &[u8], pos: usize) -> Option<(u32, usize)> {
635 let b0 = *bytes.get(pos)?;
636 if b0 < 0x80 {
637 return Some((b0 as u32, 1));
638 }
639 if (b0 & 0xE0) == 0xC0 {
640 let b1 = *bytes.get(pos + 1)?;
641 if (b1 & 0xC0) != 0x80 {
642 return None;
643 }
644 let cp = (((b0 & 0x1F) as u32) << 6) | ((b1 & 0x3F) as u32);
645 return Some((cp, 2));
646 }
647 if (b0 & 0xF0) == 0xE0 {
648 let b1 = *bytes.get(pos + 1)?;
649 let b2 = *bytes.get(pos + 2)?;
650 if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 {
651 return None;
652 }
653 let cp = (((b0 & 0x0F) as u32) << 12) | (((b1 & 0x3F) as u32) << 6) | ((b2 & 0x3F) as u32);
654 return Some((cp, 3));
655 }
656 if (b0 & 0xF8) == 0xF0 {
657 let b1 = *bytes.get(pos + 1)?;
658 let b2 = *bytes.get(pos + 2)?;
659 let b3 = *bytes.get(pos + 3)?;
660 if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 || (b3 & 0xC0) != 0x80 {
661 return None;
662 }
663 let cp = (((b0 & 0x07) as u32) << 18)
664 | (((b1 & 0x3F) as u32) << 12)
665 | (((b2 & 0x3F) as u32) << 6)
666 | ((b3 & 0x3F) as u32);
667 return Some((cp, 4));
668 }
669 None
670}
671
672fn parse_psf2_unicode_map(font: &[u8], info: &FontInfo) -> Vec<(u32, usize)> {
674 let Some(mut i) = info.unicode_table_offset else {
675 return Vec::new();
676 };
677 if i >= font.len() {
678 return Vec::new();
679 }
680
681 let mut map = Vec::new();
682 for glyph in 0..info.glyph_count {
683 while i < font.len() {
684 let b = font[i];
685 if b == 0xFF {
686 i += 1;
687 break;
688 }
689 if b == 0xFE {
690 i += 1;
692 continue;
693 }
694 if let Some((cp, adv)) = decode_utf8_at(font, i) {
695 if !map.iter().any(|(u, _)| *u == cp) {
696 map.push((cp, glyph));
697 }
698 i += adv;
699 } else {
700 i += 1;
701 }
702 }
703 }
704
705 map
706}
707
708const SCROLLBAR_W: usize = 12;
710const MAX_SCROLLBACK: usize = 500;
712const MAX_DIRTY_RECTS: usize = 8;
713
714const CURSOR_W: usize = 12;
716const CURSOR_H: usize = 16;
717const TEXT_CURSOR_MAX_DIM: usize = 32;
718const PRESENT_MIN_TICKS: u64 = 1;
719#[rustfmt::skip]
721const CURSOR_PIXELS: [u8; CURSOR_W * CURSOR_H] = [
722 1,0,0,0,0,0,0,0,0,0,0,0,
723 1,1,0,0,0,0,0,0,0,0,0,0,
724 1,2,1,0,0,0,0,0,0,0,0,0,
725 1,2,2,1,0,0,0,0,0,0,0,0,
726 1,2,2,2,1,0,0,0,0,0,0,0,
727 1,2,2,2,2,1,0,0,0,0,0,0,
728 1,2,2,2,2,2,1,0,0,0,0,0,
729 1,2,2,2,2,2,2,1,0,0,0,0,
730 1,2,2,2,2,2,2,2,1,0,0,0,
731 1,2,2,2,2,2,1,1,0,0,0,0,
732 1,2,2,2,1,0,0,0,0,0,0,0,
733 1,2,1,1,2,2,1,0,0,0,0,0,
734 1,1,0,0,1,2,2,1,0,0,0,0,
735 0,0,0,0,0,1,2,1,0,0,0,0,
736 0,0,0,0,0,0,1,1,0,0,0,0,
737 0,0,0,0,0,0,0,0,0,0,0,0,
738];
739
740const CLIPBOARD_CAP: usize = 8192;
742static CLIPBOARD: spin::Mutex<([u8; CLIPBOARD_CAP], usize)> =
743 spin::Mutex::new(([0u8; CLIPBOARD_CAP], 0));
744
745#[derive(Clone, Copy)]
747struct SbCell {
748 ch: char,
749 fg: u32, bg: u32, }
752
753pub struct VgaWriter {
754 enabled: bool,
755 fb_addr: *mut u8,
756 fb_width: usize,
757 fb_height: usize,
758 pitch: usize,
759 fmt: PixelFormat,
760
761 cols: usize,
762 rows: usize,
763 col: usize,
764 row: usize,
765
766 fg: u32,
767 bg: u32,
768
769 font: &'static [u8],
770 font_info: FontInfo,
771 glyph_mask_cache: Vec<u8>,
772 unicode_map: Vec<(u32, usize)>,
773 status_bar_height: usize,
774 clip: ClipRect,
775 back_buffer: Option<Vec<u32>>,
776 draw_to_back: bool,
777 dirty_regions: DirtyRegionSet,
778 track_dirty: bool,
779
780 sb_rows: Vec<Vec<SbCell>>,
783 sb_cur_row: Vec<SbCell>,
785 sb_row_head: usize,
786 scroll_offset: usize,
788
789 mc_x: i32,
791 mc_y: i32,
792 mc_visible: bool,
793 mc_save: [u32; CURSOR_W * CURSOR_H],
794 tc_col: usize,
795 tc_row: usize,
796 tc_w: usize,
797 tc_h: usize,
798 tc_visible: bool,
799 tc_color: u32,
800 tc_save: [u32; TEXT_CURSOR_MAX_DIM * TEXT_CURSOR_MAX_DIM],
801
802 sel_active: bool,
804 sel_start_row: usize,
805 sel_start_col: usize,
806 sel_end_row: usize,
807 sel_end_col: usize,
808 present_pending: bool,
809 last_present_tick: u64,
810 prepared_row_cells: Vec<(usize, u32, u32)>,
811}
812
813unsafe impl Send for VgaWriter {}
814
815impl VgaWriter {
816 fn sb_capacity(&self) -> usize {
817 MAX_SCROLLBACK + self.rows + 1
818 }
819
820 fn sb_row_at(&self, logical_idx: usize) -> Option<&Vec<SbCell>> {
821 if logical_idx >= self.sb_rows.len() {
822 return None;
823 }
824 let phys = (self.sb_row_head + logical_idx) % self.sb_rows.len();
825 self.sb_rows.get(phys)
826 }
827
828 fn sb_push_row(&mut self, row: Vec<SbCell>) {
829 let cap = self.sb_capacity();
830 if cap == 0 {
831 return;
832 }
833 if self.sb_rows.len() < cap {
834 self.sb_rows.push(row);
835 return;
836 }
837 if self.sb_rows.is_empty() {
838 self.sb_rows.push(row);
839 self.sb_row_head = 0;
840 return;
841 }
842 self.sb_rows[self.sb_row_head] = row;
843 self.sb_row_head = (self.sb_row_head + 1) % self.sb_rows.len();
844 }
845
846 fn build_glyph_mask_cache(font: &'static [u8], info: &FontInfo) -> Vec<u8> {
847 let glyph_pixels = info.glyph_w.saturating_mul(info.glyph_h);
848 let total_pixels = info.glyph_count.saturating_mul(glyph_pixels);
849 let mut cache = Vec::with_capacity(total_pixels);
850 if glyph_pixels == 0 || info.glyph_count == 0 {
851 return cache;
852 }
853
854 let row_bytes = info.glyph_w.div_ceil(8);
855 for glyph_index in 0..info.glyph_count {
856 let start = info.data_offset + glyph_index * info.bytes_per_glyph;
857 let end = start.saturating_add(info.bytes_per_glyph);
858 if end > font.len() {
859 cache.resize(cache.len().saturating_add(glyph_pixels), 0);
860 continue;
861 }
862 let glyph_ptr = font[start..end].as_ptr();
863 for gy in 0..info.glyph_h {
864 for gx in 0..info.glyph_w {
865 let byte = unsafe { *glyph_ptr.add(gy * row_bytes + gx / 8) };
866 let mask = 0x80u8 >> (gx % 8);
867 cache.push(if (byte & mask) != 0 { 1 } else { 0 });
868 }
869 }
870 }
871 cache
872 }
873
874 fn glyph_mask_slice(&self, glyph_index: usize) -> Option<&[u8]> {
875 let glyph_pixels = self
876 .font_info
877 .glyph_w
878 .saturating_mul(self.font_info.glyph_h);
879 if glyph_pixels == 0 {
880 return None;
881 }
882 let start = glyph_index.checked_mul(glyph_pixels)?;
883 let end = start.checked_add(glyph_pixels)?;
884 self.glyph_mask_cache.get(start..end)
885 }
886
887 pub const fn new() -> Self {
889 Self {
890 enabled: false,
891 fb_addr: core::ptr::null_mut(),
892 fb_width: 0,
893 fb_height: 0,
894 pitch: 0,
895 fmt: PixelFormat {
896 bpp: 0,
897 red_size: 0,
898 red_shift: 0,
899 green_size: 0,
900 green_shift: 0,
901 blue_size: 0,
902 blue_shift: 0,
903 },
904 cols: 0,
905 rows: 0,
906 col: 0,
907 row: 0,
908 fg: 0,
909 bg: 0,
910 font: &[],
911 font_info: FontInfo {
912 glyph_count: 0,
913 bytes_per_glyph: 0,
914 glyph_w: 8,
915 glyph_h: 16,
916 data_offset: 0,
917 unicode_table_offset: None,
918 },
919 glyph_mask_cache: Vec::new(),
920 unicode_map: Vec::new(),
921 status_bar_height: 0,
922 clip: ClipRect {
923 x: 0,
924 y: 0,
925 w: 0,
926 h: 0,
927 },
928 back_buffer: None,
929 draw_to_back: false,
930 dirty_regions: DirtyRegionSet::empty(),
931 track_dirty: false,
932 sb_rows: Vec::new(),
933 sb_cur_row: Vec::new(),
934 sb_row_head: 0,
935 scroll_offset: 0,
936 mc_x: 0,
937 mc_y: 0,
938 mc_visible: false,
939 mc_save: [0u32; CURSOR_W * CURSOR_H],
940 tc_col: 0,
941 tc_row: 0,
942 tc_w: 0,
943 tc_h: 0,
944 tc_visible: false,
945 tc_color: 0,
946 tc_save: [0u32; TEXT_CURSOR_MAX_DIM * TEXT_CURSOR_MAX_DIM],
947 sel_active: false,
948 sel_start_row: 0,
949 sel_start_col: 0,
950 sel_end_row: 0,
951 sel_end_col: 0,
952 present_pending: false,
953 last_present_tick: 0,
954 prepared_row_cells: Vec::new(),
955 }
956 }
957
958 fn configure(
960 &mut self,
961 fb_addr: *mut u8,
962 fb_width: usize,
963 fb_height: usize,
964 pitch: usize,
965 fmt: PixelFormat,
966 ) -> bool {
967 let Some(font_info) = parse_psf(FONT_PSF) else {
968 return false;
969 };
970 let status_bar_height = font_info.glyph_h;
971 let text_height = fb_height.saturating_sub(status_bar_height);
972 let cols = fb_width / font_info.glyph_w;
973 let rows = text_height / font_info.glyph_h;
974 if cols == 0 || rows == 0 {
975 return false;
976 }
977 let (fr, fg, fb) = color_to_rgb(Color::LightGrey);
978 let (br, bg, bb) = color_to_rgb(Color::Black);
979
980 self.enabled = true;
981 self.fb_addr = fb_addr;
982 self.fb_width = fb_width;
983 self.fb_height = fb_height;
984 self.pitch = pitch;
985 self.fmt = fmt;
986 self.cols = cols;
987 self.rows = rows;
988 self.col = 0;
989 self.row = 0;
990 self.fg = fmt.pack_rgb(fr, fg, fb);
991 self.bg = fmt.pack_rgb(br, bg, bb);
992 self.font = FONT_PSF;
993 self.font_info = font_info;
994 self.glyph_mask_cache = Self::build_glyph_mask_cache(FONT_PSF, &self.font_info);
995 self.unicode_map = parse_psf2_unicode_map(FONT_PSF, &self.font_info);
996 self.status_bar_height = status_bar_height;
997 self.clip = ClipRect {
998 x: 0,
999 y: 0,
1000 w: fb_width,
1001 h: fb_height,
1002 };
1003 self.back_buffer = None;
1004 self.draw_to_back = false;
1005 self.dirty_regions = DirtyRegionSet::empty();
1006 self.track_dirty = false;
1007 self.sb_rows = Vec::new();
1008 self.sb_cur_row = Vec::new();
1009 self.sb_row_head = 0;
1010 self.scroll_offset = 0;
1011 self.mc_visible = false;
1012 self.tc_visible = false;
1013 self.tc_col = 0;
1014 self.tc_row = 0;
1015 self.tc_w = 0;
1016 self.tc_h = 0;
1017 self.tc_color = 0;
1018 self.sel_active = false;
1019 self.present_pending = false;
1020 self.last_present_tick = 0;
1021 self.prepared_row_cells = Vec::new();
1022 true
1023 }
1024
1025 #[inline]
1027 fn pack_color(&self, color: RgbColor) -> u32 {
1028 self.fmt.pack_rgb(color.r, color.g, color.b)
1029 }
1030
1031 fn unpack_color(&self, value: u32) -> RgbColor {
1033 fn unscale(v: u32, bits: u8) -> u8 {
1035 if bits == 0 {
1036 return 0;
1037 }
1038 let max = (1u32 << bits) - 1;
1039 ((v * 255) / max) as u8
1040 }
1041
1042 let r = unscale(
1043 (value >> self.fmt.red_shift) & ((1u32 << self.fmt.red_size) - 1),
1044 self.fmt.red_size,
1045 );
1046 let g = unscale(
1047 (value >> self.fmt.green_shift) & ((1u32 << self.fmt.green_size) - 1),
1048 self.fmt.green_size,
1049 );
1050 let b = unscale(
1051 (value >> self.fmt.blue_shift) & ((1u32 << self.fmt.blue_size) - 1),
1052 self.fmt.blue_size,
1053 );
1054 RgbColor::new(r, g, b)
1055 }
1056
1057 pub fn set_color(&mut self, fg: Color, bg: Color) {
1059 self.set_rgb_color(fg.into(), bg.into());
1060 }
1061
1062 pub fn set_rgb_color(&mut self, fg: RgbColor, bg: RgbColor) {
1064 self.fg = self.pack_color(fg);
1065 self.bg = self.pack_color(bg);
1066 }
1067
1068 pub fn text_colors(&self) -> (RgbColor, RgbColor) {
1070 (self.unpack_color(self.fg), self.unpack_color(self.bg))
1071 }
1072
1073 pub fn width(&self) -> usize {
1075 self.fb_width
1076 }
1077
1078 pub fn height(&self) -> usize {
1080 self.fb_height
1081 }
1082
1083 pub fn cols(&self) -> usize {
1085 self.cols
1086 }
1087
1088 pub fn rows(&self) -> usize {
1090 self.rows
1091 }
1092
1093 pub fn glyph_size(&self) -> (usize, usize) {
1095 (self.font_info.glyph_w, self.font_info.glyph_h)
1096 }
1097
1098 pub fn set_cursor_cell(&mut self, col: usize, row: usize) {
1100 if !self.enabled || self.cols == 0 || self.rows == 0 {
1101 return;
1102 }
1103 if self.tc_visible {
1104 self.text_cursor_erase_hw();
1105 self.tc_visible = false;
1106 }
1107 self.col = core::cmp::min(col, self.cols - 1);
1108 self.row = core::cmp::min(row, self.rows - 1);
1109 }
1110
1111 fn text_area_height(&self) -> usize {
1113 self.fb_height.saturating_sub(self.status_bar_height)
1114 }
1115
1116 pub fn enabled(&self) -> bool {
1118 self.enabled
1119 }
1120
1121 pub fn framebuffer_info(&self) -> FramebufferInfo {
1123 FramebufferInfo {
1124 available: self.enabled,
1125 width: self.fb_width,
1126 height: self.fb_height,
1127 pitch: self.pitch,
1128 bpp: self.fmt.bpp,
1129 red_size: self.fmt.red_size,
1130 red_shift: self.fmt.red_shift,
1131 green_size: self.fmt.green_size,
1132 green_shift: self.fmt.green_shift,
1133 blue_size: self.fmt.blue_size,
1134 blue_shift: self.fmt.blue_shift,
1135 text_cols: self.cols,
1136 text_rows: self.rows,
1137 glyph_w: self.font_info.glyph_w,
1138 glyph_h: self.font_info.glyph_h,
1139 double_buffer_mode: DOUBLE_BUFFER_MODE.load(Ordering::Relaxed),
1140 double_buffer_enabled: self.draw_to_back && self.back_buffer.is_some(),
1141 ui_scale: current_ui_scale(),
1142 }
1143 }
1144
1145 #[inline]
1147 fn in_clip(&self, x: usize, y: usize) -> bool {
1148 x >= self.clip.x
1149 && y >= self.clip.y
1150 && x < self.clip.x.saturating_add(self.clip.w)
1151 && y < self.clip.y.saturating_add(self.clip.h)
1152 }
1153
1154 fn clipped_rect(
1156 &self,
1157 x: usize,
1158 y: usize,
1159 width: usize,
1160 height: usize,
1161 ) -> Option<(usize, usize, usize, usize)> {
1162 if width == 0 || height == 0 || !self.enabled {
1163 return None;
1164 }
1165 let src_x2 = core::cmp::min(x.saturating_add(width), self.fb_width);
1166 let src_y2 = core::cmp::min(y.saturating_add(height), self.fb_height);
1167 let clip_x2 = self.clip.x.saturating_add(self.clip.w);
1168 let clip_y2 = self.clip.y.saturating_add(self.clip.h);
1169
1170 let sx = core::cmp::max(x, self.clip.x);
1171 let sy = core::cmp::max(y, self.clip.y);
1172 let ex = core::cmp::min(src_x2, clip_x2);
1173 let ey = core::cmp::min(src_y2, clip_y2);
1174 if ex <= sx || ey <= sy {
1175 return None;
1176 }
1177 Some((sx, sy, ex - sx, ey - sy))
1178 }
1179
1180 fn clear_dirty(&mut self) {
1182 self.dirty_regions = DirtyRegionSet::empty();
1183 }
1184
1185 fn mark_dirty_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
1187 if !self.track_dirty {
1188 return;
1189 }
1190 let Some((sx, sy, sw, sh)) = self.clipped_rect(x, y, width, height) else {
1191 return;
1192 };
1193 let next = ClipRect {
1194 x: sx,
1195 y: sy,
1196 w: sw,
1197 h: sh,
1198 };
1199 let mut merged = next;
1200 let mut idx = 0usize;
1201 while idx < self.dirty_regions.len {
1202 let cur = self.dirty_regions.rects[idx];
1203 let overlaps = merged.x <= cur.x.saturating_add(cur.w)
1204 && merged.x.saturating_add(merged.w) >= cur.x
1205 && merged.y <= cur.y.saturating_add(cur.h)
1206 && merged.y.saturating_add(merged.h) >= cur.y;
1207 if overlaps {
1208 let x0 = core::cmp::min(cur.x, merged.x);
1209 let y0 = core::cmp::min(cur.y, merged.y);
1210 let x1 = core::cmp::max(
1211 cur.x.saturating_add(cur.w),
1212 merged.x.saturating_add(merged.w),
1213 );
1214 let y1 = core::cmp::max(
1215 cur.y.saturating_add(cur.h),
1216 merged.y.saturating_add(merged.h),
1217 );
1218 merged = ClipRect {
1219 x: x0,
1220 y: y0,
1221 w: x1.saturating_sub(x0),
1222 h: y1.saturating_sub(y0),
1223 };
1224 self.dirty_regions.rects[idx] =
1225 self.dirty_regions.rects[self.dirty_regions.len - 1];
1226 self.dirty_regions.rects[self.dirty_regions.len - 1] = ClipRect {
1227 x: 0,
1228 y: 0,
1229 w: 0,
1230 h: 0,
1231 };
1232 self.dirty_regions.len -= 1;
1233 idx = 0;
1234 continue;
1235 }
1236 idx += 1;
1237 }
1238
1239 if self.dirty_regions.len < MAX_DIRTY_RECTS {
1240 self.dirty_regions.rects[self.dirty_regions.len] = merged;
1241 self.dirty_regions.len += 1;
1242 } else {
1243 let cur = self.dirty_regions.rects[0];
1244 let x0 = core::cmp::min(cur.x, merged.x);
1245 let y0 = core::cmp::min(cur.y, merged.y);
1246 let x1 = core::cmp::max(
1247 cur.x.saturating_add(cur.w),
1248 merged.x.saturating_add(merged.w),
1249 );
1250 let y1 = core::cmp::max(
1251 cur.y.saturating_add(cur.h),
1252 merged.y.saturating_add(merged.h),
1253 );
1254 self.dirty_regions.rects[0] = ClipRect {
1255 x: x0,
1256 y: y0,
1257 w: x1.saturating_sub(x0),
1258 h: y1.saturating_sub(y0),
1259 };
1260 }
1261 }
1262
1263 pub fn set_clip_rect(&mut self, x: usize, y: usize, width: usize, height: usize) {
1265 let x_end = core::cmp::min(x.saturating_add(width), self.fb_width);
1266 let y_end = core::cmp::min(y.saturating_add(height), self.fb_height);
1267 self.clip = ClipRect {
1268 x,
1269 y,
1270 w: x_end.saturating_sub(x),
1271 h: y_end.saturating_sub(y),
1272 };
1273 }
1274
1275 pub fn reset_clip_rect(&mut self) {
1277 self.clip = ClipRect {
1278 x: 0,
1279 y: 0,
1280 w: self.fb_width,
1281 h: self.fb_height,
1282 };
1283 }
1284
1285 fn draw_to_back_buffer(&self) -> bool {
1287 self.draw_to_back && self.back_buffer.is_some()
1288 }
1289
1290 pub fn enable_double_buffer(&mut self) -> bool {
1292 if !self.enabled {
1293 return false;
1294 }
1295 if self.back_buffer.is_none() {
1296 let mut buf = Vec::with_capacity(self.fb_width.saturating_mul(self.fb_height));
1297 for y in 0..self.fb_height {
1298 for x in 0..self.fb_width {
1299 buf.push(self.read_hw_pixel_packed(x, y));
1300 }
1301 }
1302 self.back_buffer = Some(buf);
1303 }
1304 self.draw_to_back = true;
1305 self.track_dirty = true;
1306 self.clear_dirty();
1307 true
1308 }
1309
1310 pub fn disable_double_buffer(&mut self, present: bool) {
1312 if present {
1313 self.present();
1314 }
1315 self.draw_to_back = false;
1316 self.track_dirty = false;
1317 self.clear_dirty();
1318 }
1319
1320 pub fn present(&mut self) {
1322 if !self.enabled {
1323 return;
1324 }
1325 let Some(buf) = self.back_buffer.as_ref() else {
1326 return;
1327 };
1328 if self.track_dirty && self.dirty_regions.len == 0 {
1329 return;
1330 }
1331
1332 let buf_ptr = buf.as_ptr();
1333 let bpp = self.fmt.bpp;
1334 let fb_addr = self.fb_addr;
1335 let pitch = self.pitch;
1336 let fb_width = self.fb_width;
1337 let region_count = if self.track_dirty {
1338 self.dirty_regions.len
1339 } else {
1340 1
1341 };
1342
1343 let mut regions = [ClipRect {
1344 x: 0,
1345 y: 0,
1346 w: 0,
1347 h: 0,
1348 }; MAX_DIRTY_RECTS];
1349 if self.track_dirty {
1350 let mut idx = 0;
1351 while idx < self.dirty_regions.len {
1352 regions[idx] = self.dirty_regions.rects[idx];
1353 idx += 1;
1354 }
1355 } else {
1356 regions[0] = ClipRect {
1357 x: 0,
1358 y: 0,
1359 w: self.fb_width,
1360 h: self.fb_height,
1361 };
1362 }
1363
1364 let mut region_idx = 0;
1365 while region_idx < region_count {
1366 let region = regions[region_idx];
1367 if region.w == 0 || region.h == 0 {
1368 region_idx += 1;
1369 continue;
1370 }
1371
1372 if bpp == 32 {
1373 let row_bytes = region.w * 4;
1374 for y in region.y..(region.y + region.h) {
1375 let src = unsafe { buf_ptr.add(y * fb_width + region.x) as *const u8 };
1376 let dst_off = y * pitch + region.x * 4;
1377 unsafe {
1378 core::ptr::copy_nonoverlapping(src, fb_addr.add(dst_off), row_bytes);
1379 }
1380 }
1381 } else {
1382 for y in region.y..(region.y + region.h) {
1383 for x in region.x..(region.x + region.w) {
1384 let packed = unsafe { *buf_ptr.add(y * fb_width + x) };
1385 self.write_hw_pixel_packed(x, y, packed);
1386 }
1387 }
1388 }
1389
1390 VGA_PRESENT_REGION_COUNT.fetch_add(1, Ordering::Relaxed);
1391 VGA_PRESENT_PIXEL_COUNT
1392 .fetch_add(region.w.saturating_mul(region.h) as u64, Ordering::Relaxed);
1393 region_idx += 1;
1394 }
1395
1396 PRESENTED_FRAMES.fetch_add(1, Ordering::Relaxed);
1397 self.clear_dirty();
1398 self.present_pending = false;
1399 self.last_present_tick = crate::process::scheduler::ticks();
1400
1401 if crate::hardware::virtio::gpu::is_available() {
1402 if let Some(gpu) = crate::hardware::virtio::gpu::get_gpu() {
1403 gpu.flush_now();
1404 }
1405 }
1406
1407 if self.mc_visible {
1408 self.mc_save_hw();
1409 self.mc_draw_hw();
1410 }
1411 if self.tc_visible {
1412 self.text_cursor_save_hw();
1413 self.text_cursor_draw_hw();
1414 }
1415 }
1416
1417 fn request_present(&mut self) {
1418 if !self.draw_to_back_buffer() {
1419 return;
1420 }
1421 self.present_pending = true;
1422 self.present_if_due(false);
1423 }
1424
1425 fn present_if_due(&mut self, force: bool) {
1426 if !self.present_pending || !self.draw_to_back_buffer() {
1427 return;
1428 }
1429 let now = crate::process::scheduler::ticks();
1430 if force || now.saturating_sub(self.last_present_tick) >= PRESENT_MIN_TICKS {
1431 self.present();
1432 }
1433 }
1434
1435 fn mc_save_hw(&mut self) {
1439 let x = self.mc_x;
1440 let y = self.mc_y;
1441 let fw = self.fb_width;
1442 let fh = self.fb_height;
1443 for cy in 0..CURSOR_H {
1444 for cx in 0..CURSOR_W {
1445 let px = x + cx as i32;
1446 let py = y + cy as i32;
1447 if px < 0 || py < 0 || px as usize >= fw || py as usize >= fh {
1448 self.mc_save[cy * CURSOR_W + cx] = 0;
1449 continue;
1450 }
1451 self.mc_save[cy * CURSOR_W + cx] =
1452 self.read_hw_pixel_packed(px as usize, py as usize);
1453 }
1454 }
1455 }
1456
1457 fn mc_draw_hw(&mut self) {
1459 let x = self.mc_x;
1460 let y = self.mc_y;
1461 let black = self.pack_color(RgbColor::BLACK);
1462 let white = self.pack_color(RgbColor::WHITE);
1463 for cy in 0..CURSOR_H {
1464 for cx in 0..CURSOR_W {
1465 let p = CURSOR_PIXELS[cy * CURSOR_W + cx];
1466 if p == 0 {
1467 continue;
1468 }
1469 let px = x + cx as i32;
1470 let py = y + cy as i32;
1471 if px < 0 || py < 0 || px as usize >= self.fb_width || py as usize >= self.fb_height
1472 {
1473 continue;
1474 }
1475 let color = if p == 1 { black } else { white };
1476 self.write_hw_pixel_packed(px as usize, py as usize, color);
1477 }
1478 }
1479 }
1480
1481 fn mc_erase_hw(&mut self) {
1483 let x = self.mc_x;
1484 let y = self.mc_y;
1485 for cy in 0..CURSOR_H {
1486 for cx in 0..CURSOR_W {
1487 if CURSOR_PIXELS[cy * CURSOR_W + cx] == 0 {
1488 continue;
1489 }
1490 let px = x + cx as i32;
1491 let py = y + cy as i32;
1492 if px < 0 || py < 0 || px as usize >= self.fb_width || py as usize >= self.fb_height
1493 {
1494 continue;
1495 }
1496 self.write_hw_pixel_packed(
1497 px as usize,
1498 py as usize,
1499 self.mc_save[cy * CURSOR_W + cx],
1500 );
1501 }
1502 }
1503 }
1504
1505 pub fn update_mouse_cursor(&mut self, x: i32, y: i32) {
1507 if !self.enabled {
1508 return;
1509 }
1510 if self.mc_visible && self.mc_x == x && self.mc_y == y {
1511 self.present_if_due(false);
1512 return;
1513 }
1514 if self.mc_visible {
1515 self.mc_erase_hw();
1516 }
1517 self.mc_x = x;
1518 self.mc_y = y;
1519 self.mc_save_hw();
1520 self.mc_draw_hw();
1521 self.mc_visible = true;
1522 self.present_if_due(false);
1523 }
1524
1525 pub fn hide_mouse_cursor(&mut self) {
1527 if self.mc_visible {
1528 self.mc_erase_hw();
1529 self.mc_visible = false;
1530 }
1531 self.present_if_due(false);
1532 }
1533
1534 fn text_cursor_rect(&self) -> Option<(usize, usize, usize, usize)> {
1535 if !self.enabled {
1536 return None;
1537 }
1538 let (gw, gh) = self.glyph_size();
1539 if gw == 0 || gh == 0 {
1540 return None;
1541 }
1542 let x = self.tc_col.saturating_mul(gw);
1543 let y = self.tc_row.saturating_mul(gh);
1544 if x >= self.fb_width || y >= self.fb_height {
1545 return None;
1546 }
1547 Some((
1548 x,
1549 y,
1550 gw.min(TEXT_CURSOR_MAX_DIM).min(self.fb_width - x),
1551 gh.min(TEXT_CURSOR_MAX_DIM).min(self.fb_height - y),
1552 ))
1553 }
1554
1555 fn text_cursor_save_hw(&mut self) {
1556 let Some((x, y, w, h)) = self.text_cursor_rect() else {
1557 self.tc_w = 0;
1558 self.tc_h = 0;
1559 return;
1560 };
1561 self.tc_w = w;
1562 self.tc_h = h;
1563 for cy in 0..h {
1564 for cx in 0..w {
1565 self.tc_save[cy * TEXT_CURSOR_MAX_DIM + cx] =
1566 self.read_hw_pixel_packed(x + cx, y + cy);
1567 }
1568 }
1569 }
1570
1571 fn text_cursor_draw_hw(&mut self) {
1572 let Some((x, y, w, h)) = self.text_cursor_rect() else {
1573 return;
1574 };
1575 for cy in 0..h {
1576 for cx in 0..w {
1577 self.write_hw_pixel_packed(x + cx, y + cy, self.tc_color);
1578 }
1579 }
1580 }
1581
1582 fn text_cursor_erase_hw(&mut self) {
1583 let Some((x, y, w, h)) = self.text_cursor_rect() else {
1584 return;
1585 };
1586 let restore_w = w.min(self.tc_w);
1587 let restore_h = h.min(self.tc_h);
1588 for cy in 0..restore_h {
1589 for cx in 0..restore_w {
1590 self.write_hw_pixel_packed(
1591 x + cx,
1592 y + cy,
1593 self.tc_save[cy * TEXT_CURSOR_MAX_DIM + cx],
1594 );
1595 }
1596 }
1597 }
1598
1599 fn draw_text_cursor_overlay(&mut self, color: RgbColor) {
1600 if !self.enabled {
1601 return;
1602 }
1603 let packed = self.pack_color(color);
1604 if self.tc_visible {
1605 self.text_cursor_erase_hw();
1606 self.tc_visible = false;
1607 }
1608 if packed == self.bg {
1609 self.present_if_due(false);
1610 return;
1611 }
1612 self.tc_col = self.col;
1613 self.tc_row = self.row;
1614 self.tc_color = packed;
1615 self.text_cursor_save_hw();
1616 self.text_cursor_draw_hw();
1617 self.tc_visible = true;
1618 self.present_if_due(false);
1619 }
1620
1621 fn sel_normalized(&self) -> (usize, usize, usize, usize) {
1625 let (sr, sc, er, ec) = (
1626 self.sel_start_row,
1627 self.sel_start_col,
1628 self.sel_end_row,
1629 self.sel_end_col,
1630 );
1631 if sr < er || (sr == er && sc <= ec) {
1632 (sr, sc, er, ec)
1633 } else {
1634 (er, ec, sr, sc)
1635 }
1636 }
1637
1638 pub fn pixel_to_sb_pos(&self, px: usize, py: usize) -> Option<(usize, usize)> {
1640 if !self.enabled {
1641 return None;
1642 }
1643 let gw = self.font_info.glyph_w;
1644 let gh = self.font_info.glyph_h;
1645 if gw == 0 || gh == 0 {
1646 return None;
1647 }
1648 let text_h = self.text_area_height();
1649 let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
1650 if px >= text_w || py >= text_h {
1651 return None;
1652 }
1653 let vis_row = py / gh;
1654 let vis_col = px / gw;
1655 if vis_col >= self.cols {
1656 return None;
1657 }
1658 let total_complete = self.sb_rows.len();
1659 let has_partial = !self.sb_cur_row.is_empty();
1660 let total_virtual = total_complete + if has_partial { 1 } else { 0 };
1661 if total_virtual == 0 {
1662 return None;
1663 }
1664 let view_end = total_virtual.saturating_sub(self.scroll_offset);
1665 let view_start = view_end.saturating_sub(self.rows);
1666 let display_len = view_end.saturating_sub(view_start);
1667 if vis_row >= display_len {
1668 return None;
1669 }
1670 Some((view_start + vis_row, vis_col))
1671 }
1672
1673 pub fn start_selection(&mut self, px: usize, py: usize) {
1675 if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
1676 self.sel_start_row = row;
1677 self.sel_start_col = col;
1678 self.sel_end_row = row;
1679 self.sel_end_col = col;
1680 self.sel_active = true;
1681 self.render_viewport_full();
1682 }
1683 }
1684
1685 pub fn update_selection(&mut self, px: usize, py: usize) {
1687 if !self.sel_active {
1688 return;
1689 }
1690 if let Some((row, col)) = self.pixel_to_sb_pos(px, py) {
1691 if row == self.sel_end_row && col == self.sel_end_col {
1692 return;
1693 }
1694 self.sel_end_row = row;
1695 self.sel_end_col = col;
1696 self.render_viewport_full();
1697 }
1698 }
1699
1700 pub fn end_selection(&mut self) {
1702 if !self.sel_active {
1703 return;
1704 }
1705 let (start_row, start_col, end_row, end_col) = self.sel_normalized();
1706 let mut bytes: alloc::vec::Vec<u8> = alloc::vec::Vec::new();
1707 for row in start_row..=end_row {
1708 let len = if row < self.sb_rows.len() {
1709 self.sb_row_at(row).map(|r| r.len()).unwrap_or(0)
1710 } else if row == self.sb_rows.len() {
1711 self.sb_cur_row.len()
1712 } else {
1713 break;
1714 };
1715 let c0 = if row == start_row {
1716 start_col.min(len)
1717 } else {
1718 0
1719 };
1720 let c1 = if row == end_row {
1721 end_col.min(len)
1722 } else {
1723 len
1724 };
1725 for col in c0..c1 {
1726 let ch = if row < self.sb_rows.len() {
1727 self.sb_row_at(row)
1728 .and_then(|r| r.get(col))
1729 .map(|cell| cell.ch)
1730 .unwrap_or(' ')
1731 } else {
1732 self.sb_cur_row[col].ch
1733 };
1734 let mut buf = [0u8; 4];
1735 bytes.extend_from_slice(ch.encode_utf8(&mut buf).as_bytes());
1736 }
1737 if row < end_row {
1738 bytes.push(b'\n');
1739 }
1740 }
1741 if let Some(mut clip) = CLIPBOARD.try_lock() {
1742 let n = bytes.len().min(CLIPBOARD_CAP);
1743 clip.0[..n].copy_from_slice(&bytes[..n]);
1744 clip.1 = n;
1745 }
1746 }
1747
1748 pub fn clear_selection(&mut self) {
1750 if self.sel_active {
1751 self.sel_active = false;
1752 self.render_viewport_full();
1753 }
1754 }
1755
1756 pub fn clear_with(&mut self, color: RgbColor) {
1758 if !self.enabled {
1759 return;
1760 }
1761 let packed = self.pack_color(color);
1762 for y in 0..self.fb_height {
1763 for x in 0..self.fb_width {
1764 self.put_pixel_raw(x, y, packed);
1765 }
1766 }
1767 self.col = 0;
1768 self.row = 0;
1769 }
1770
1771 pub fn clear(&mut self) {
1773 self.clear_with(self.unpack_color(self.bg));
1774 }
1775
1776 #[inline]
1778 fn pixel_offset(&self, x: usize, y: usize) -> Option<usize> {
1779 if x >= self.fb_width || y >= self.fb_height {
1780 return None;
1781 }
1782 let bytes_pp = self.fmt.bpp as usize / 8;
1783 let row = y.checked_mul(self.pitch)?;
1784 let col = x.checked_mul(bytes_pp)?;
1785 row.checked_add(col)
1786 }
1787
1788 fn write_hw_pixel_packed(&mut self, x: usize, y: usize, color: u32) {
1790 let Some(off) = self.pixel_offset(x, y) else {
1791 return;
1792 };
1793 unsafe {
1794 match self.fmt.bpp {
1795 32 => {
1796 core::ptr::write_volatile(self.fb_addr.add(off) as *mut u32, color);
1797 }
1798 24 => {
1799 core::ptr::write_volatile(self.fb_addr.add(off), (color & 0xFF) as u8);
1800 core::ptr::write_volatile(
1801 self.fb_addr.add(off + 1),
1802 ((color >> 8) & 0xFF) as u8,
1803 );
1804 core::ptr::write_volatile(
1805 self.fb_addr.add(off + 2),
1806 ((color >> 16) & 0xFF) as u8,
1807 );
1808 }
1809 _ => {}
1810 }
1811 }
1812 }
1813
1814 fn read_hw_pixel_packed(&self, x: usize, y: usize) -> u32 {
1816 let Some(off) = self.pixel_offset(x, y) else {
1817 return 0;
1818 };
1819 unsafe {
1820 match self.fmt.bpp {
1821 32 => core::ptr::read_volatile(self.fb_addr.add(off) as *const u32),
1822 24 => {
1823 let b0 = core::ptr::read_volatile(self.fb_addr.add(off)) as u32;
1824 let b1 = core::ptr::read_volatile(self.fb_addr.add(off + 1)) as u32;
1825 let b2 = core::ptr::read_volatile(self.fb_addr.add(off + 2)) as u32;
1826 b0 | (b1 << 8) | (b2 << 16)
1827 }
1828 _ => 0,
1829 }
1830 }
1831 }
1832
1833 fn read_pixel_packed(&self, x: usize, y: usize) -> u32 {
1835 if self.draw_to_back_buffer() {
1836 if let Some(buf) = self.back_buffer.as_ref() {
1837 return buf[y * self.fb_width + x];
1838 }
1839 }
1840 self.read_hw_pixel_packed(x, y)
1841 }
1842
1843 fn put_pixel_raw(&mut self, x: usize, y: usize, color: u32) {
1845 if !self.enabled || x >= self.fb_width || y >= self.fb_height || !self.in_clip(x, y) {
1846 return;
1847 }
1848 if self.draw_to_back_buffer() {
1849 if let Some(buf) = self.back_buffer.as_mut() {
1850 buf[y * self.fb_width + x] = color;
1851 self.mark_dirty_rect(x, y, 1, 1);
1852 return;
1853 }
1854 }
1855 self.write_hw_pixel_packed(x, y, color);
1856 }
1857
1858 pub fn draw_pixel(&mut self, x: usize, y: usize, color: RgbColor) {
1860 self.put_pixel_raw(x, y, self.pack_color(color));
1861 }
1862
1863 pub fn draw_pixel_alpha(&mut self, x: usize, y: usize, color: RgbColor, alpha: u8) {
1865 if !self.enabled
1866 || alpha == 0
1867 || x >= self.fb_width
1868 || y >= self.fb_height
1869 || !self.in_clip(x, y)
1870 {
1871 return;
1872 }
1873 if alpha == 255 {
1874 self.put_pixel_raw(x, y, self.pack_color(color));
1875 return;
1876 }
1877 let dst = self.unpack_color(self.read_pixel_packed(x, y));
1878 let inv = (255u16).saturating_sub(alpha as u16);
1879 let a = alpha as u16;
1880 let blended = RgbColor::new(
1881 ((color.r as u16 * a + dst.r as u16 * inv + 127) / 255) as u8,
1882 ((color.g as u16 * a + dst.g as u16 * inv + 127) / 255) as u8,
1883 ((color.b as u16 * a + dst.b as u16 * inv + 127) / 255) as u8,
1884 );
1885 self.put_pixel_raw(x, y, self.pack_color(blended));
1886 }
1887
1888 pub fn draw_line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
1890 let mut x = x0;
1891 let mut y = y0;
1892 let dx = (x1 - x0).abs();
1893 let sx = if x0 < x1 { 1 } else { -1 };
1894 let dy = -(y1 - y0).abs();
1895 let sy = if y0 < y1 { 1 } else { -1 };
1896 let mut err = dx + dy;
1897 let packed = self.pack_color(color);
1898
1899 loop {
1900 if x >= 0 && y >= 0 {
1901 self.put_pixel_raw(x as usize, y as usize, packed);
1902 }
1903 if x == x1 && y == y1 {
1904 break;
1905 }
1906 let e2 = 2 * err;
1907 if e2 >= dy {
1908 err += dy;
1909 x += sx;
1910 }
1911 if e2 <= dx {
1912 err += dx;
1913 y += sy;
1914 }
1915 }
1916 }
1917
1918 pub fn draw_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1920 if width == 0 || height == 0 {
1921 return;
1922 }
1923 let x2 = x.saturating_add(width - 1);
1924 let y2 = y.saturating_add(height - 1);
1925 self.draw_line(x as isize, y as isize, x2 as isize, y as isize, color);
1926 self.draw_line(x as isize, y as isize, x as isize, y2 as isize, color);
1927 self.draw_line(x2 as isize, y as isize, x2 as isize, y2 as isize, color);
1928 self.draw_line(x as isize, y2 as isize, x2 as isize, y2 as isize, color);
1929 }
1930
1931 pub fn fill_rect(&mut self, x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
1933 let Some((sx, sy, sw, sh)) = self.clipped_rect(x, y, width, height) else {
1934 return;
1935 };
1936 let packed = self.pack_color(color);
1937
1938 if self.draw_to_back_buffer() {
1939 if let Some(buf) = self.back_buffer.as_mut() {
1940 for py in sy..(sy + sh) {
1941 let row = py * self.fb_width;
1942 let start = row + sx;
1943 let end = start + sw;
1944 buf[start..end].fill(packed);
1945 }
1946 self.mark_dirty_rect(sx, sy, sw, sh);
1947 return;
1948 }
1949 }
1950
1951 if self.fmt.bpp == 32 {
1952 for py in sy..(sy + sh) {
1953 let Some(row_off) = py
1954 .checked_mul(self.pitch)
1955 .and_then(|v| v.checked_add(sx * 4))
1956 else {
1957 continue;
1958 };
1959 let count = sw;
1960 unsafe {
1961 let ptr = self.fb_addr.add(row_off) as *mut u32;
1962 for i in 0..count {
1963 core::ptr::write_volatile(ptr.add(i), packed);
1964 }
1965 }
1966 }
1967 return;
1968 }
1969
1970 for py in sy..(sy + sh) {
1971 for px in sx..(sx + sw) {
1972 self.write_hw_pixel_packed(px, py, packed);
1973 }
1974 }
1975 }
1976
1977 pub fn fill_rect_alpha(
1979 &mut self,
1980 x: usize,
1981 y: usize,
1982 width: usize,
1983 height: usize,
1984 color: RgbColor,
1985 alpha: u8,
1986 ) {
1987 if !self.enabled || width == 0 || height == 0 || alpha == 0 {
1988 return;
1989 }
1990 if alpha == 255 {
1991 self.fill_rect(x, y, width, height, color);
1992 return;
1993 }
1994 let x_end = core::cmp::min(x.saturating_add(width), self.fb_width);
1995 let y_end = core::cmp::min(y.saturating_add(height), self.fb_height);
1996 for py in y..y_end {
1997 for px in x..x_end {
1998 self.draw_pixel_alpha(px, py, color, alpha);
1999 }
2000 }
2001 }
2002
2003 pub fn blit_rgb(
2005 &mut self,
2006 dst_x: usize,
2007 dst_y: usize,
2008 src_width: usize,
2009 src_height: usize,
2010 pixels: &[RgbColor],
2011 ) -> bool {
2012 let len = src_width.saturating_mul(src_height);
2013 if !self.enabled || src_width == 0 || src_height == 0 || pixels.len() < len {
2014 return false;
2015 }
2016 let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.fb_width);
2017 let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.fb_height);
2018 if x_end <= dst_x || y_end <= dst_y {
2019 return true;
2020 }
2021 let copy_w = x_end - dst_x;
2022 let copy_h = y_end - dst_y;
2023 for row in 0..copy_h {
2024 let src_row = row * src_width;
2025 for col in 0..copy_w {
2026 self.draw_pixel(dst_x + col, dst_y + row, pixels[src_row + col]);
2027 }
2028 }
2029 true
2030 }
2031
2032 pub fn blit_rgb24(
2034 &mut self,
2035 dst_x: usize,
2036 dst_y: usize,
2037 src_width: usize,
2038 src_height: usize,
2039 bytes: &[u8],
2040 ) -> bool {
2041 let needed = src_width.saturating_mul(src_height).saturating_mul(3);
2042 if !self.enabled || src_width == 0 || src_height == 0 || bytes.len() < needed {
2043 return false;
2044 }
2045 let x_end = core::cmp::min(dst_x.saturating_add(src_width), self.fb_width);
2046 let y_end = core::cmp::min(dst_y.saturating_add(src_height), self.fb_height);
2047 if x_end <= dst_x || y_end <= dst_y {
2048 return true;
2049 }
2050 let copy_w = x_end - dst_x;
2051 let copy_h = y_end - dst_y;
2052 for row in 0..copy_h {
2053 for col in 0..copy_w {
2054 let i = (row * src_width + col) * 3;
2055 let color = RgbColor::new(bytes[i], bytes[i + 1], bytes[i + 2]);
2056 self.draw_pixel(dst_x + col, dst_y + row, color);
2057 }
2058 }
2059 true
2060 }
2061
2062 pub fn blit_rgba(
2064 &mut self,
2065 dst_x: usize,
2066 dst_y: usize,
2067 src_width: usize,
2068 src_height: usize,
2069 bytes: &[u8],
2070 global_alpha: u8,
2071 ) -> bool {
2072 let needed = src_width.saturating_mul(src_height).saturating_mul(4);
2073 if !self.enabled
2074 || src_width == 0
2075 || src_height == 0
2076 || bytes.len() < needed
2077 || global_alpha == 0
2078 {
2079 return false;
2080 }
2081
2082 let Some((sx, sy, sw, sh)) = self.clipped_rect(dst_x, dst_y, src_width, src_height) else {
2083 return true;
2084 };
2085 let src_x0 = sx.saturating_sub(dst_x);
2086 let src_y0 = sy.saturating_sub(dst_y);
2087
2088 for row in 0..sh {
2089 let syi = src_y0 + row;
2090 for col in 0..sw {
2091 let sxi = src_x0 + col;
2092 let i = (syi * src_width + sxi) * 4;
2093 let r = bytes[i];
2094 let g = bytes[i + 1];
2095 let b = bytes[i + 2];
2096 let sa = bytes[i + 3];
2097 if sa == 0 {
2098 continue;
2099 }
2100 let a = ((sa as u16 * global_alpha as u16 + 127) / 255) as u8;
2101 let dx = sx + col;
2102 let dy = sy + row;
2103 if a == 255 {
2104 self.put_pixel_raw(dx, dy, self.pack_color(RgbColor::new(r, g, b)));
2105 } else if a != 0 {
2106 self.draw_pixel_alpha(dx, dy, RgbColor::new(r, g, b), a);
2107 }
2108 }
2109 }
2110 true
2111 }
2112
2113 pub fn blit_sprite_rgba(
2115 &mut self,
2116 dst_x: usize,
2117 dst_y: usize,
2118 sprite: SpriteRgba<'_>,
2119 global_alpha: u8,
2120 ) -> bool {
2121 self.blit_rgba(
2122 dst_x,
2123 dst_y,
2124 sprite.width,
2125 sprite.height,
2126 sprite.pixels,
2127 global_alpha,
2128 )
2129 }
2130
2131 pub fn draw_text_at(
2133 &mut self,
2134 pixel_x: usize,
2135 pixel_y: usize,
2136 text: &str,
2137 fg: RgbColor,
2138 bg: RgbColor,
2139 ) {
2140 if !self.enabled {
2141 return;
2142 }
2143 let gw = self.font_info.glyph_w;
2144 let gh = self.font_info.glyph_h;
2145 let fg_packed = self.pack_color(fg);
2146 let bg_packed = self.pack_color(bg);
2147 let mut cx = pixel_x;
2148 let cy = pixel_y;
2149 for ch in text.chars() {
2150 if ch == '\n' {
2151 break;
2152 }
2153 if cx + gw > self.fb_width || cy + gh > self.fb_height {
2154 break;
2155 }
2156 self.draw_glyph_at_pixel(cx, cy, ch, fg_packed, bg_packed);
2157 cx += gw;
2158 }
2159 }
2160
2161 fn glyph_index_for_char(&self, ch: char) -> usize {
2163 if ch.is_ascii() {
2164 let idx = ch as usize;
2165 if idx < self.font_info.glyph_count {
2166 return idx;
2167 }
2168 }
2169 let cp = ch as u32;
2170 if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == cp) {
2171 return *glyph;
2172 }
2173 if let Some((_, glyph)) = self.unicode_map.iter().find(|(u, _)| *u == ('?' as u32)) {
2174 return *glyph;
2175 }
2176 if ('?' as usize) < self.font_info.glyph_count {
2177 return '?' as usize;
2178 }
2179 0
2180 }
2181
2182 fn draw_glyph_index_at_pixel(
2184 &mut self,
2185 pixel_x: usize,
2186 pixel_y: usize,
2187 glyph_index: usize,
2188 fg: u32,
2189 bg: u32,
2190 ) {
2191 if !self.enabled {
2192 return;
2193 }
2194 let glyph_index = core::cmp::min(glyph_index, self.font_info.glyph_count.saturating_sub(1));
2195 let gw = self.font_info.glyph_w;
2196 let gh = self.font_info.glyph_h;
2197 let glyph_pixels = gw.saturating_mul(gh);
2198 let Some(mask) = self.glyph_mask_slice(glyph_index) else {
2199 return;
2200 };
2201 let mask_ptr = mask.as_ptr();
2202
2203 if self.draw_to_back_buffer()
2204 && pixel_x + gw <= self.fb_width
2205 && pixel_y + gh <= self.fb_height
2206 {
2207 let fb_width = self.fb_width;
2208 if let Some(buf) = self.back_buffer.as_mut() {
2209 for gy in 0..gh {
2210 let row_start = (pixel_y + gy) * fb_width + pixel_x;
2211 buf[row_start..row_start + gw].fill(bg);
2212 for gx in 0..gw {
2213 let idx = gy * gw + gx;
2214 if idx >= glyph_pixels {
2215 continue;
2216 }
2217 let bit = unsafe { *mask_ptr.add(idx) };
2218 if bit != 0 {
2219 buf[row_start + gx] = fg;
2220 }
2221 }
2222 }
2223 }
2224 self.mark_dirty_rect(pixel_x, pixel_y, gw, gh);
2225 } else {
2226 for gy in 0..gh {
2227 for gx in 0..gw {
2228 let idx = gy * gw + gx;
2229 if idx >= glyph_pixels {
2230 continue;
2231 }
2232 let color = if unsafe { *mask_ptr.add(idx) } != 0 {
2233 fg
2234 } else {
2235 bg
2236 };
2237 self.put_pixel_raw(pixel_x + gx, pixel_y + gy, color);
2238 }
2239 }
2240 }
2241 }
2242
2243 fn draw_glyph_at_pixel(&mut self, pixel_x: usize, pixel_y: usize, ch: char, fg: u32, bg: u32) {
2245 let glyph_index = self.glyph_index_for_char(ch);
2246 self.draw_glyph_index_at_pixel(pixel_x, pixel_y, glyph_index, fg, bg);
2247 }
2248
2249 fn fill_text_span_bg(
2250 &mut self,
2251 pixel_x: usize,
2252 pixel_y: usize,
2253 width: usize,
2254 height: usize,
2255 bg: u32,
2256 ) {
2257 if self.draw_to_back_buffer()
2258 && pixel_x + width <= self.fb_width
2259 && pixel_y + height <= self.fb_height
2260 {
2261 let fb_width = self.fb_width;
2262 if let Some(buf) = self.back_buffer.as_mut() {
2263 for row in 0..height {
2264 let start = (pixel_y + row) * fb_width + pixel_x;
2265 let end = start + width;
2266 buf[start..end].fill(bg);
2267 }
2268 self.mark_dirty_rect(pixel_x, pixel_y, width, height);
2269 return;
2270 }
2271 }
2272
2273 let gh = height;
2274 let color = self.unpack_color(bg);
2275 self.fill_rect(pixel_x, pixel_y, width, gh, color);
2276 }
2277
2278 fn clear_text_line_pixels(&mut self, vis_row: usize) {
2279 let gh = self.font_info.glyph_h;
2280 let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2281 self.fill_text_span_bg(0, vis_row.saturating_mul(gh), text_w, gh, self.bg);
2282 }
2283
2284 fn visible_virtual_bounds(&self) -> (usize, usize, usize, usize, bool) {
2285 let total_complete = self.sb_rows.len();
2286 let has_partial = !self.sb_cur_row.is_empty();
2287 let total_virtual = total_complete + if has_partial { 1 } else { 0 };
2288 let view_end = total_virtual.saturating_sub(self.scroll_offset);
2289 let view_start = view_end.saturating_sub(self.rows);
2290 (
2291 total_complete,
2292 total_virtual,
2293 view_start,
2294 view_end,
2295 has_partial,
2296 )
2297 }
2298
2299 fn sync_live_cursor_from_view(
2300 &mut self,
2301 total_complete: usize,
2302 view_start: usize,
2303 view_end: usize,
2304 ) {
2305 let display_len = view_end.saturating_sub(view_start);
2306 self.row = if display_len > 0 { display_len - 1 } else { 0 };
2307 let last_virt = view_start + display_len.saturating_sub(1);
2308 let last_len = if last_virt < total_complete {
2309 self.sb_row_at(last_virt).map(|row| row.len()).unwrap_or(0)
2310 } else if last_virt == total_complete {
2311 self.sb_cur_row.len()
2312 } else {
2313 0
2314 };
2315 self.col = last_len.min(self.cols);
2316 }
2317
2318 fn selection_colors_for_cell(
2319 &self,
2320 virt_row: usize,
2321 col: usize,
2322 fg: u32,
2323 bg: u32,
2324 ) -> (u32, u32) {
2325 if !self.sel_active {
2326 return (fg, bg);
2327 }
2328 let (sel_sr, sel_sc, sel_er, sel_ec) = self.sel_normalized();
2329 let in_sel = if virt_row < sel_sr || virt_row > sel_er {
2330 false
2331 } else if sel_sr == sel_er {
2332 col >= sel_sc && col < sel_ec
2333 } else if virt_row == sel_sr {
2334 col >= sel_sc
2335 } else if virt_row == sel_er {
2336 col < sel_ec
2337 } else {
2338 true
2339 };
2340 if in_sel {
2341 (
2342 self.pack_color(RgbColor::WHITE),
2343 self.pack_color(RgbColor::new(0x26, 0x5F, 0xCC)),
2344 )
2345 } else {
2346 (fg, bg)
2347 }
2348 }
2349
2350 fn render_virtual_row(
2351 &mut self,
2352 virt_row: usize,
2353 vis_row: usize,
2354 total_complete: usize,
2355 has_partial: bool,
2356 ) {
2357 let glyph_h = self.font_info.glyph_h;
2358 let glyph_w = self.font_info.glyph_w;
2359 let py = vis_row.saturating_mul(glyph_h);
2360
2361 let (row_ptr, row_len) = if virt_row < total_complete {
2362 let row = match self.sb_row_at(virt_row) {
2363 Some(row) => row,
2364 None => return,
2365 };
2366 (row.as_ptr(), row.len())
2367 } else if has_partial && virt_row == total_complete {
2368 (self.sb_cur_row.as_ptr(), self.sb_cur_row.len())
2369 } else {
2370 (core::ptr::null(), 0)
2371 };
2372
2373 let cell_count = row_len.min(self.cols);
2374 let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2375 let used_width = cell_count.saturating_mul(glyph_w).min(text_w);
2376
2377 if self.draw_to_back_buffer() && py + glyph_h <= self.fb_height {
2378 let fb_width = self.fb_width;
2379 let default_bg = self.bg;
2380 let glyph_pixels = glyph_w.saturating_mul(glyph_h);
2381 let glyph_cache_ptr = self.glyph_mask_cache.as_ptr();
2382 self.prepared_row_cells.clear();
2383 if self.prepared_row_cells.capacity() < cell_count {
2384 self.prepared_row_cells
2385 .reserve(cell_count - self.prepared_row_cells.capacity());
2386 }
2387 for col in 0..cell_count {
2388 let cell = unsafe { &*row_ptr.add(col) };
2389 let glyph_index = self.glyph_index_for_char(cell.ch);
2390 let (draw_fg, draw_bg) =
2391 self.selection_colors_for_cell(virt_row, col, cell.fg, cell.bg);
2392 self.prepared_row_cells
2393 .push((glyph_index, draw_fg, draw_bg));
2394 }
2395 if let Some(buf) = self.back_buffer.as_mut() {
2396 for gy in 0..glyph_h {
2397 let row_start = (py + gy) * fb_width;
2398 buf[row_start..row_start + text_w].fill(default_bg);
2399 }
2400
2401 for (col, (glyph_index, draw_fg, draw_bg)) in
2402 self.prepared_row_cells.iter().copied().enumerate()
2403 {
2404 let px = col.saturating_mul(glyph_w);
2405 if draw_bg != default_bg {
2406 for gy in 0..glyph_h {
2407 let row_start = (py + gy) * fb_width + px;
2408 buf[row_start..row_start + glyph_w].fill(draw_bg);
2409 }
2410 }
2411
2412 let glyph_base = glyph_index.saturating_mul(glyph_pixels);
2413 for gy in 0..glyph_h {
2414 let row_start = (py + gy) * fb_width + px;
2415 for gx in 0..glyph_w {
2416 let idx = glyph_base + gy * glyph_w + gx;
2417 let bit = unsafe { *glyph_cache_ptr.add(idx) };
2418 if bit != 0 {
2419 buf[row_start + gx] = draw_fg;
2420 }
2421 }
2422 }
2423 }
2424 }
2425
2426 self.mark_dirty_rect(0, py, text_w, glyph_h);
2427 return;
2428 }
2429
2430 for col in 0..cell_count {
2431 let px = col.saturating_mul(glyph_w);
2432 let cell = unsafe { &*row_ptr.add(col) };
2433 let (draw_fg, draw_bg) =
2434 self.selection_colors_for_cell(virt_row, col, cell.fg, cell.bg);
2435 self.draw_glyph_at_pixel(px, py, cell.ch, draw_fg, draw_bg);
2436 }
2437 if text_w > used_width {
2438 self.fill_text_span_bg(used_width, py, text_w - used_width, glyph_h, self.bg);
2439 }
2440 }
2441
2442 fn redraw_visible_rows(&mut self, start_vis_row: usize, count: usize) {
2443 let (total_complete, _, view_start, view_end, has_partial) = self.visible_virtual_bounds();
2444 let display_len = view_end.saturating_sub(view_start);
2445 let end_vis_row = start_vis_row.saturating_add(count).min(self.rows);
2446 for vis_row in start_vis_row..end_vis_row {
2447 if vis_row < display_len {
2448 self.render_virtual_row(view_start + vis_row, vis_row, total_complete, has_partial);
2449 } else {
2450 self.clear_text_line_pixels(vis_row);
2451 }
2452 }
2453 }
2454
2455 fn ensure_back_buffer_for_viewport(&mut self) {
2456 if self.back_buffer.is_none() {
2457 let total = self.fb_width.saturating_mul(self.fb_height);
2458 if total > 0 {
2459 self.back_buffer = Some(alloc::vec![0u32; total]);
2460 }
2461 }
2462 }
2463
2464 fn begin_viewport_render(&mut self) -> (bool, bool) {
2465 self.ensure_back_buffer_for_viewport();
2466 let prev_draw_to_back = self.draw_to_back;
2467 let prev_track_dirty = self.track_dirty;
2468 if self.back_buffer.is_some() {
2469 self.draw_to_back = true;
2470 self.track_dirty = true;
2471 self.clear_dirty();
2472 }
2473 (prev_draw_to_back, prev_track_dirty)
2474 }
2475
2476 fn end_viewport_render(&mut self, prev_draw_to_back: bool, prev_track_dirty: bool) {
2477 if self.back_buffer.is_some() {
2478 self.request_present();
2479 let now = crate::process::scheduler::ticks();
2480 let force_present = self.last_present_tick == 0 || now == 0;
2481 self.present_if_due(force_present);
2482 self.draw_to_back = prev_draw_to_back;
2483 self.track_dirty = prev_track_dirty;
2484 if !prev_track_dirty {
2485 self.clear_dirty();
2486 }
2487 }
2488 }
2489
2490 fn finalize_live_view_state(&mut self) {
2491 let (total_complete, _, view_start, view_end, _) = self.visible_virtual_bounds();
2492 if self.scroll_offset == 0 {
2493 self.sync_live_cursor_from_view(total_complete, view_start, view_end);
2494 }
2495 }
2496
2497 fn render_viewport_full(&mut self) {
2498 let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2499 let text_h = self.text_area_height();
2500 let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2501 self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2502 self.redraw_visible_rows(0, self.rows);
2503 self.finalize_live_view_state();
2504 self.draw_scrollbar_inner();
2505 self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2506 }
2507
2508 fn refresh_viewport_decorations(&mut self) {
2509 let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2510 self.finalize_live_view_state();
2511 self.draw_scrollbar_inner();
2512 self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2513 }
2514
2515 fn set_scroll_offset_and_render(&mut self, new_offset: usize) {
2516 let old_offset = self.scroll_offset;
2517 self.scroll_offset = new_offset;
2518 if !self.redraw_from_scrollback_incremental(old_offset) {
2519 self.redraw_from_scrollback();
2520 }
2521 }
2522
2523 fn move_text_view_pixels_up(&mut self, pixels: usize) {
2524 if pixels == 0 {
2525 return;
2526 }
2527 let text_h = self.text_area_height();
2528 let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2529 if pixels >= text_h {
2530 self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2531 return;
2532 }
2533 let move_rows = text_h - pixels;
2534 if self.draw_to_back_buffer() {
2535 if let Some(buf) = self.back_buffer.as_mut() {
2536 let row_width = self.fb_width;
2537 buf.copy_within(pixels * row_width..text_h * row_width, 0);
2538 self.mark_dirty_rect(0, 0, self.fb_width, text_h);
2539 }
2540 } else {
2541 unsafe {
2542 core::ptr::copy(
2543 self.fb_addr.add(pixels * self.pitch),
2544 self.fb_addr,
2545 move_rows * self.pitch,
2546 );
2547 }
2548 }
2549 self.fill_rect(0, move_rows, text_w, pixels, self.unpack_color(self.bg));
2550 }
2551
2552 fn move_text_view_pixels_down(&mut self, pixels: usize) {
2553 if pixels == 0 {
2554 return;
2555 }
2556 let text_h = self.text_area_height();
2557 let text_w = self.fb_width.saturating_sub(SCROLLBAR_W);
2558 if pixels >= text_h {
2559 self.fill_rect(0, 0, text_w, text_h, self.unpack_color(self.bg));
2560 return;
2561 }
2562 let move_rows = text_h - pixels;
2563 if self.draw_to_back_buffer() {
2564 if let Some(buf) = self.back_buffer.as_mut() {
2565 let row_width = self.fb_width;
2566 buf.copy_within(0..move_rows * row_width, pixels * row_width);
2567 self.mark_dirty_rect(0, 0, self.fb_width, text_h);
2568 }
2569 } else {
2570 unsafe {
2571 core::ptr::copy(
2572 self.fb_addr,
2573 self.fb_addr.add(pixels * self.pitch),
2574 move_rows * self.pitch,
2575 );
2576 }
2577 }
2578 self.fill_rect(0, 0, text_w, pixels, self.unpack_color(self.bg));
2579 }
2580
2581 fn layout_text_lines(&self, text: &str, wrap: bool, max_cols: Option<usize>) -> Vec<Vec<char>> {
2583 let mut lines: Vec<Vec<char>> = Vec::new();
2584 let mut current: Vec<char> = Vec::new();
2585 let wrap_cols = max_cols.filter(|&c| c > 0);
2586
2587 for ch in text.chars() {
2588 if ch == '\n' {
2589 lines.push(current);
2590 current = Vec::new();
2591 continue;
2592 }
2593
2594 if wrap {
2595 if let Some(cols) = wrap_cols {
2596 if current.len() >= cols {
2597 lines.push(current);
2598 current = Vec::new();
2599 }
2600 }
2601 }
2602
2603 current.push(ch);
2604 }
2605
2606 lines.push(current);
2607 lines
2608 }
2609
2610 pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
2612 if !self.enabled {
2613 return TextMetrics {
2614 width: 0,
2615 height: 0,
2616 lines: 0,
2617 };
2618 }
2619 let gw = self.font_info.glyph_w;
2620 let gh = self.font_info.glyph_h;
2621 let max_cols = max_width.map(|w| core::cmp::max(1, w / gw));
2622 let lines = self.layout_text_lines(text, wrap, max_cols);
2623
2624 let mut max_line_cols = 0usize;
2625 for line in &lines {
2626 max_line_cols = core::cmp::max(max_line_cols, line.len());
2627 }
2628
2629 TextMetrics {
2630 width: max_line_cols * gw,
2631 height: lines.len() * gh,
2632 lines: lines.len(),
2633 }
2634 }
2635
2636 pub fn draw_text(
2638 &mut self,
2639 pixel_x: usize,
2640 pixel_y: usize,
2641 text: &str,
2642 opts: TextOptions,
2643 ) -> TextMetrics {
2644 if !self.enabled {
2645 return TextMetrics {
2646 width: 0,
2647 height: 0,
2648 lines: 0,
2649 };
2650 }
2651
2652 let gw = self.font_info.glyph_w;
2653 let gh = self.font_info.glyph_h;
2654 let max_cols = opts.max_width.map(|w| core::cmp::max(1, w / gw));
2655 let lines = self.layout_text_lines(text, opts.wrap, max_cols);
2656 let region_w = opts.max_width.unwrap_or_else(|| {
2657 let mut max_line_cols = 0usize;
2658 for line in &lines {
2659 max_line_cols = core::cmp::max(max_line_cols, line.len());
2660 }
2661 max_line_cols * gw
2662 });
2663
2664 let fg = self.pack_color(opts.fg);
2665 let bg = self.pack_color(opts.bg);
2666 let mut max_line_px = 0usize;
2667
2668 for (line_idx, line) in lines.iter().enumerate() {
2669 let line_px = line.len() * gw;
2670 max_line_px = core::cmp::max(max_line_px, line_px);
2671 let x = match opts.align {
2672 TextAlign::Left => pixel_x,
2673 TextAlign::Center => pixel_x.saturating_add(region_w.saturating_sub(line_px) / 2),
2674 TextAlign::Right => pixel_x.saturating_add(region_w.saturating_sub(line_px)),
2675 };
2676 let y = pixel_y + line_idx * gh;
2677
2678 for (col, ch) in line.iter().enumerate() {
2679 self.draw_glyph_at_pixel(x + col * gw, y, *ch, fg, bg);
2680 }
2681 }
2682
2683 TextMetrics {
2684 width: max_line_px,
2685 height: lines.len() * gh,
2686 lines: lines.len(),
2687 }
2688 }
2689
2690 pub fn draw_strata_stack(
2692 &mut self,
2693 origin_x: usize,
2694 origin_y: usize,
2695 layer_w: usize,
2696 layer_h: usize,
2697 ) {
2698 if !self.enabled || layer_w == 0 || layer_h == 0 {
2699 return;
2700 }
2701
2702 let palette = [
2704 RgbColor::new(0x24, 0x3B, 0x55),
2705 RgbColor::new(0x2B, 0x54, 0x77),
2706 RgbColor::new(0x2F, 0x74, 0x93),
2707 RgbColor::new(0x3A, 0x93, 0xA8),
2708 RgbColor::new(0x5F, 0xB1, 0xA1),
2709 RgbColor::new(0xA4, 0xCC, 0x94),
2710 ];
2711
2712 let dx = 6usize;
2713 let dy = 5usize;
2714 for (i, color) in palette.iter().enumerate() {
2715 let x = origin_x.saturating_add(i * dx);
2716 let y = origin_y.saturating_add(i * dy);
2717 let w = layer_w.saturating_sub(i * dx);
2718 let h = layer_h.saturating_sub(i * dy);
2719 if w < 8 || h < 8 {
2720 break;
2721 }
2722
2723 self.fill_rect(x, y, w, h, *color);
2724 self.draw_rect(x, y, w, h, RgbColor::new(0x10, 0x16, 0x20));
2725 }
2726 }
2727
2728 fn draw_glyph(&mut self, cx: usize, cy: usize, ch: char) {
2730 let glyph_index = self.glyph_index_for_char(ch);
2731 self.draw_glyph_index_at_pixel(
2732 cx * self.font_info.glyph_w,
2733 cy * self.font_info.glyph_h,
2734 glyph_index,
2735 self.fg,
2736 self.bg,
2737 );
2738 }
2739
2740 fn clear_row(&mut self, row: usize) {
2742 if !self.enabled {
2743 return;
2744 }
2745 self.clear_text_line_pixels(row);
2746 }
2747
2748 fn scroll(&mut self) {
2750 if !self.enabled {
2751 return;
2752 }
2753 let dy = self.font_info.glyph_h;
2754 let text_h = self.text_area_height();
2755 if dy >= text_h {
2756 self.clear();
2757 return;
2758 }
2759 self.move_text_view_pixels_up(dy);
2760 self.row = self.rows - 1;
2761 }
2762
2763 fn write_char(&mut self, c: char) {
2765 if !self.enabled {
2766 return;
2767 }
2768 let c = normalize_console_char(c);
2769
2770 self.sb_mirror_char(c);
2772 if self.scroll_offset > 0 {
2774 return;
2775 }
2776 match c {
2779 '\n' => {
2780 self.col = 0;
2781 self.row += 1;
2782 }
2783 '\r' => self.col = 0,
2784 '\t' => self.col = (self.col + 4) & !3,
2785 '\u{8}' => {
2786 if self.col > 0 {
2787 self.col -= 1;
2788 self.draw_glyph(self.col, self.row, ' ');
2789 }
2790 }
2791 '\0' => {}
2792 ch => {
2793 self.draw_glyph(self.col, self.row, ch);
2794 self.col += 1;
2795 }
2796 }
2797
2798 if self.col >= self.cols {
2799 self.col = 0;
2800 self.row += 1;
2801 }
2802
2803 if self.row >= self.rows {
2804 self.scroll();
2805 }
2806 }
2807
2808 fn write_bytes(&mut self, s: &str) {
2810 let (prev_draw_to_back, prev_track_dirty) = self.begin_viewport_render();
2811 let mut chars = s.chars();
2813 while let Some(ch) = chars.next() {
2814 if ch == '\u{1b}' {
2815 if matches!(chars.clone().next(), Some('[')) {
2816 let _ = chars.next();
2817 for c in chars.by_ref() {
2818 if ('@'..='~').contains(&c) {
2819 break;
2820 }
2821 }
2822 }
2823 continue;
2824 }
2825 self.write_char(ch);
2826 }
2827 self.draw_scrollbar_inner();
2829 self.end_viewport_render(prev_draw_to_back, prev_track_dirty);
2830 }
2831
2832 fn sb_mirror_char(&mut self, c: char) {
2839 let cols = self.cols;
2840 let fg = self.fg;
2841 let bg = self.bg;
2842 match c {
2843 '\n' => {
2844 let row = core::mem::take(&mut self.sb_cur_row);
2845 self.sb_push_row(row);
2846 self.sb_trim();
2847 }
2848 '\r' => {
2849 self.sb_cur_row.clear();
2850 }
2851 '\t' => {
2852 let stop = (self.sb_cur_row.len() + 4) & !3;
2853 let end = stop.min(cols);
2854 while self.sb_cur_row.len() < end {
2855 self.sb_cur_row.push(SbCell { ch: ' ', fg, bg });
2856 }
2857 if self.sb_cur_row.len() >= cols {
2858 let row = core::mem::take(&mut self.sb_cur_row);
2859 self.sb_push_row(row);
2860 self.sb_trim();
2861 }
2862 }
2863 '\u{8}' => {
2864 self.sb_cur_row.pop();
2865 }
2866 '\0' => {}
2867 ch => {
2868 self.sb_cur_row.push(SbCell { ch, fg, bg });
2869 if self.sb_cur_row.len() >= cols {
2870 let row = core::mem::take(&mut self.sb_cur_row);
2871 self.sb_push_row(row);
2872 self.sb_trim();
2873 }
2874 }
2875 }
2876 }
2877
2878 #[inline]
2880 fn sb_trim(&mut self) {
2881 let cap = self.sb_capacity();
2882 if self.sb_rows.len() <= cap {
2883 return;
2884 }
2885 let keep = cap.min(self.sb_rows.len());
2886 let start = self.sb_rows.len().saturating_sub(keep);
2887 let mut compacted = Vec::with_capacity(keep);
2888 for idx in start..self.sb_rows.len() {
2889 if let Some(row) = self.sb_row_at(idx).cloned() {
2890 compacted.push(row);
2891 }
2892 }
2893 self.sb_rows = compacted;
2894 self.sb_row_head = 0;
2895 }
2896
2897 fn draw_scrollbar_inner(&mut self) {
2899 if !self.enabled || self.fb_width == 0 {
2900 return;
2901 }
2902 let text_h = self.text_area_height();
2903 if text_h == 0 {
2904 return;
2905 }
2906 let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
2907 let total = self.sb_rows.len() + 1; let track_packed = self.fmt.pack_rgb(0x22, 0x28, 0x38);
2909 let thumb_packed = self.fmt.pack_rgb(0x58, 0x72, 0xA0);
2910 let thumb_hi = self.fmt.pack_rgb(0x80, 0xA0, 0xC8);
2911
2912 if total <= self.rows {
2913 for y in 0..text_h {
2915 for x in sb_x..self.fb_width {
2916 let c = if x == sb_x || x == self.fb_width - 1 || y == 0 || y == text_h - 1 {
2917 track_packed
2918 } else {
2919 thumb_hi
2920 };
2921 self.put_pixel_raw(x, y, c);
2922 }
2923 }
2924 return;
2925 }
2926
2927 let max_offset = total.saturating_sub(self.rows);
2928 let thumb_h = ((text_h * self.rows) / total).max(6);
2929 let avail = text_h.saturating_sub(thumb_h);
2930 let thumb_y = if self.scroll_offset == 0 || avail == 0 {
2932 avail } else {
2934 avail - (avail * self.scroll_offset / max_offset)
2935 };
2936
2937 for y in 0..text_h {
2938 let in_thumb = y >= thumb_y && y < thumb_y + thumb_h;
2939 let packed = if in_thumb { thumb_packed } else { track_packed };
2940 for x in sb_x..self.fb_width {
2941 self.put_pixel_raw(x, y, packed);
2942 }
2943 }
2944 }
2945
2946 fn redraw_from_scrollback(&mut self) {
2949 if !self.enabled {
2950 return;
2951 }
2952 self.render_viewport_full();
2953 }
2954
2955 fn redraw_from_scrollback_incremental(&mut self, old_offset: usize) -> bool {
2956 if !self.enabled || self.rows == 0 {
2957 return false;
2958 }
2959 let diff = old_offset.abs_diff(self.scroll_offset);
2960 if diff == 0 || diff >= self.rows {
2961 return false;
2962 }
2963 let pixel_delta = diff.saturating_mul(self.font_info.glyph_h);
2964 if pixel_delta == 0 {
2965 return false;
2966 }
2967
2968 if self.scroll_offset > old_offset {
2969 self.move_text_view_pixels_down(pixel_delta);
2970 self.redraw_visible_rows(0, diff);
2971 } else {
2972 self.move_text_view_pixels_up(pixel_delta);
2973 self.redraw_visible_rows(self.rows.saturating_sub(diff), diff);
2974 }
2975
2976 self.finalize_live_view_state();
2977 self.draw_scrollbar_inner();
2978 self.request_present();
2979 true
2980 }
2981
2982 pub fn scroll_view_up(&mut self, lines: usize) {
2984 if !self.enabled {
2985 return;
2986 }
2987 let total = self.sb_rows.len() + 1;
2988 let max_off = total.saturating_sub(self.rows);
2989 self.set_scroll_offset_and_render((self.scroll_offset + lines).min(max_off));
2990 }
2991
2992 pub fn scroll_view_down(&mut self, lines: usize) {
2994 if !self.enabled {
2995 return;
2996 }
2997 self.set_scroll_offset_and_render(self.scroll_offset.saturating_sub(lines));
2998 }
2999
3000 pub fn scroll_to_live(&mut self) {
3002 if self.scroll_offset == 0 {
3003 return;
3004 }
3005 self.set_scroll_offset_and_render(0);
3006 }
3007
3008 pub fn scrollbar_click(&mut self, px_x: usize, px_y: usize) {
3011 if !self.enabled {
3012 return;
3013 }
3014 let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
3015 if px_x < sb_x {
3016 return;
3017 }
3018 let text_h = self.text_area_height();
3019 if text_h <= 1 {
3020 return;
3021 }
3022 let total = self.sb_rows.len() + 1;
3023 let max_off = total.saturating_sub(self.rows);
3024 if max_off == 0 {
3025 return;
3026 }
3027 let py = px_y.min(text_h - 1);
3029 let offset = max_off * (text_h - 1 - py) / (text_h - 1);
3030 self.set_scroll_offset_and_render(offset.min(max_off));
3031 }
3032
3033 pub fn scrollbar_drag_to(&mut self, px_y: usize) {
3039 if !self.enabled {
3040 return;
3041 }
3042 let text_h = self.text_area_height();
3043 if text_h <= 1 {
3044 return;
3045 }
3046 let total = self.sb_rows.len() + 1;
3047 let max_off = total.saturating_sub(self.rows);
3048 if max_off == 0 {
3049 return;
3050 }
3051 let py = px_y.min(text_h - 1);
3053 let offset = max_off * (text_h - 1 - py) / (text_h - 1);
3054 self.set_scroll_offset_and_render(offset.min(max_off));
3055 }
3056
3057 pub fn scrollbar_hit_test(&self, px_x: usize, px_y: usize) -> bool {
3059 if !self.enabled {
3060 return false;
3061 }
3062 let sb_x = self.fb_width.saturating_sub(SCROLLBAR_W);
3063 px_x >= sb_x && px_y < self.text_area_height()
3064 }
3065}
3066
3067fn normalize_console_char(ch: char) -> char {
3069 match ch {
3070 '\n' | '\r' | '\t' | '\u{8}' => ch,
3071 c if c.is_control() => '\0',
3072 '\u{2500}' | '\u{2501}' | '\u{2504}' | '\u{2505}' | '\u{2013}' | '\u{2014}' => '-',
3074 '\u{2502}' | '\u{2503}' => '|',
3075 '\u{250c}' | '\u{2510}' | '\u{2514}' | '\u{2518}' | '\u{251c}' | '\u{2524}'
3076 | '\u{252c}' | '\u{2534}' | '\u{253c}' => '+',
3077 '\u{00a0}' => ' ',
3078 _ => ch,
3079 }
3080}
3081
3082impl fmt::Write for VgaWriter {
3083 fn write_str(&mut self, s: &str) -> fmt::Result {
3085 if self.enabled {
3086 self.write_bytes(s);
3087 } else {
3088 crate::arch::x86_64::serial::_print(format_args!("{}", s));
3089 }
3090 Ok(())
3091 }
3092}
3093
3094pub static VGA_WRITER: Mutex<VgaWriter> = Mutex::new(VgaWriter::new());
3095
3096#[inline]
3098pub fn is_available() -> bool {
3099 VGA_AVAILABLE.load(Ordering::Relaxed)
3100}
3101
3102pub fn with_writer<R>(f: impl FnOnce(&mut VgaWriter) -> R) -> Option<R> {
3104 if !is_available() {
3105 return None;
3106 }
3107 let mut writer = VGA_WRITER.lock();
3108 Some(f(&mut writer))
3109}
3110
3111pub fn write_text(text: &str) {
3113 if !is_available() {
3114 crate::arch::x86_64::serial::_print(format_args!("{}", text));
3115 return;
3116 }
3117 let mut writer = VGA_WRITER.lock();
3118 writer.write_bytes(text);
3119}
3120
3121pub fn write_char(ch: char) {
3123 if !is_available() {
3124 crate::arch::x86_64::serial::_print(format_args!("{}", ch));
3125 return;
3126 }
3127 let mut buf = [0u8; 4];
3128 write_text(ch.encode_utf8(&mut buf));
3129}
3130
3131fn status_line_info() -> StatusLineInfo {
3133 let mut guard = STATUS_LINE_INFO.lock();
3134 if guard.is_none() {
3135 *guard = Some(StatusLineInfo {
3136 hostname: String::from("strat9"),
3137 ip: String::from("n/a"),
3138 });
3139 }
3140 guard.as_ref().cloned().unwrap_or(StatusLineInfo {
3141 hostname: String::from("strat9"),
3142 ip: String::from("n/a"),
3143 })
3144}
3145
3146fn format_uptime_from_ticks(ticks: u64) -> String {
3148 let total_secs = ticks / 100;
3149 let h = total_secs / 3600;
3150 let m = (total_secs % 3600) / 60;
3151 let s = total_secs % 60;
3152 format!("{:02}:{:02}:{:02}", h, m, s)
3153}
3154
3155fn current_fps(tick: u64) -> u64 {
3157 let last_tick = FPS_LAST_TICK.load(Ordering::Relaxed);
3158 let frames = PRESENTED_FRAMES.load(Ordering::Relaxed);
3159
3160 if last_tick == 0 {
3161 let _ = FPS_LAST_TICK.compare_exchange(0, tick, Ordering::Relaxed, Ordering::Relaxed);
3162 let _ =
3163 FPS_LAST_FRAME_COUNT.compare_exchange(0, frames, Ordering::Relaxed, Ordering::Relaxed);
3164 return FPS_ESTIMATE.load(Ordering::Relaxed);
3165 }
3166
3167 let dt = tick.saturating_sub(last_tick);
3168 if dt >= STATUS_REFRESH_PERIOD_TICKS
3169 && FPS_LAST_TICK
3170 .compare_exchange(last_tick, tick, Ordering::Relaxed, Ordering::Relaxed)
3171 .is_ok()
3172 {
3173 let last_frames = FPS_LAST_FRAME_COUNT.swap(frames, Ordering::Relaxed);
3174 let df = frames.saturating_sub(last_frames);
3175 let fps = if dt == 0 {
3176 0
3177 } else {
3178 df.saturating_mul(100) / dt
3179 };
3180 FPS_ESTIMATE.store(fps, Ordering::Relaxed);
3181 }
3182
3183 FPS_ESTIMATE.load(Ordering::Relaxed)
3184}
3185
3186fn current_ui_scale() -> UiScale {
3188 match UI_SCALE.load(Ordering::Relaxed) {
3189 1 => UiScale::Compact,
3190 3 => UiScale::Large,
3191 _ => UiScale::Normal,
3192 }
3193}
3194
3195pub fn ui_scale() -> UiScale {
3197 current_ui_scale()
3198}
3199
3200pub fn set_ui_scale(scale: UiScale) {
3202 UI_SCALE.store(scale as u8, Ordering::Relaxed);
3203}
3204
3205pub fn ui_scale_px(base: usize) -> usize {
3207 let factor = current_ui_scale().factor();
3208 let denom = UiScale::Normal.factor();
3209 base.saturating_mul(factor) / denom
3210}
3211
3212fn format_mem_usage() -> String {
3214 let lock = crate::memory::buddy::get_allocator();
3215 let Some(guard) = lock.try_lock() else {
3216 return String::from("n/a");
3218 };
3219 let Some(alloc) = guard.as_ref() else {
3220 return String::from("n/a");
3221 };
3222 let (total_pages, allocated_pages) = alloc.page_totals();
3223 let page_size = 4096usize;
3224 let total = total_pages.saturating_mul(page_size);
3225 let used = allocated_pages.saturating_mul(page_size);
3226 let free = total.saturating_sub(used);
3227 format!("{}/{}", format_size(free), format_size(total))
3228}
3229
3230fn format_size(bytes: usize) -> String {
3232 const KB: usize = 1024;
3233 const MB: usize = 1024 * KB;
3234 const GB: usize = 1024 * MB;
3235 if bytes >= GB {
3236 format!("{}G", bytes / GB)
3237 } else if bytes >= MB {
3238 format!("{}M", bytes / MB)
3239 } else if bytes >= KB {
3240 format!("{}K", bytes / KB)
3241 } else {
3242 format!("{}B", bytes)
3243 }
3244}
3245
3246fn draw_status_bar_inner(w: &mut VgaWriter, left: &str, right: &str, theme: UiTheme) {
3248 let saved_clip = w.clip;
3249 w.reset_clip_rect();
3250
3251 let (gw, gh) = w.glyph_size();
3252 if gh == 0 || gw == 0 {
3253 w.clip = saved_clip;
3254 return;
3255 }
3256 let bar_h = gh;
3257 let y = w.height().saturating_sub(bar_h);
3258 w.fill_rect(0, y, w.width(), bar_h, theme.status_bg);
3259
3260 let left_opts = TextOptions {
3261 fg: theme.status_text,
3262 bg: theme.status_bg,
3263 align: TextAlign::Left,
3264 wrap: false,
3265 max_width: Some(w.width().saturating_sub(8)),
3266 };
3267 w.draw_text(0, y, left, left_opts);
3268
3269 let right_opts = TextOptions {
3270 fg: theme.status_text,
3271 bg: theme.status_bg,
3272 align: TextAlign::Right,
3273 wrap: false,
3274 max_width: Some(w.width()),
3275 };
3276 w.draw_text(0, y, right, right_opts);
3277 w.clip = saved_clip;
3278}
3279
3280#[allow(clippy::too_many_arguments)]
3282pub fn init(
3283 fb_addr: u64,
3284 fb_width: u32,
3285 fb_height: u32,
3286 pitch: u32,
3287 bpp: u16,
3288 red_size: u8,
3289 red_shift: u8,
3290 green_size: u8,
3291 green_shift: u8,
3292 blue_size: u8,
3293 blue_shift: u8,
3294) {
3295 if fb_addr == 0 || fb_width == 0 || fb_height == 0 || pitch == 0 {
3296 VGA_AVAILABLE.store(false, Ordering::Relaxed);
3297 log::info!("Framebuffer console unavailable (no framebuffer)");
3298 return;
3299 }
3300
3301 if bpp != 24 && bpp != 32 {
3302 VGA_AVAILABLE.store(false, Ordering::Relaxed);
3303 log::info!("Framebuffer console unavailable (unsupported bpp={})", bpp);
3304 return;
3305 }
3306
3307 let fmt = PixelFormat {
3308 bpp,
3309 red_size,
3310 red_shift,
3311 green_size,
3312 green_shift,
3313 blue_size,
3314 blue_shift,
3315 };
3316
3317 let hhdm = crate::memory::hhdm_offset();
3324 let fb_virt = if hhdm != 0 && fb_addr < hhdm {
3325 crate::memory::phys_to_virt(fb_addr)
3326 } else {
3327 fb_addr
3328 };
3329
3330 let mut writer = VGA_WRITER.lock();
3331 if writer.configure(
3332 fb_virt as *mut u8,
3333 fb_width as usize,
3334 fb_height as usize,
3335 pitch as usize,
3336 fmt,
3337 ) {
3338 writer.set_color(Color::LightCyan, Color::Black);
3339 writer.clear_with(RgbColor::new(0x12, 0x16, 0x1E));
3340 let deco_w = (writer.width() / 3).clamp(120, 300);
3342 let deco_h = (writer.height() / 4).clamp(90, 220);
3343 let deco_x = writer.width().saturating_sub(deco_w + 24);
3344 let deco_y = 24;
3345 writer.draw_strata_stack(deco_x, deco_y, deco_w, deco_h);
3346 writer.set_rgb_color(
3347 RgbColor::new(0xA7, 0xD8, 0xD8),
3348 RgbColor::new(0x12, 0x16, 0x1E),
3349 );
3350 writer.write_bytes("Strat9-OS v0.1.0\n");
3351 writer.set_rgb_color(
3352 RgbColor::new(0xE2, 0xE8, 0xF0),
3353 RgbColor::new(0x12, 0x16, 0x1E),
3354 );
3355 VGA_AVAILABLE.store(true, Ordering::Relaxed);
3356 log::info!(
3357 "Framebuffer console enabled: {}x{} {}bpp pitch={}",
3358 fb_width,
3359 fb_height,
3360 bpp,
3361 pitch
3362 );
3363 drop(writer);
3364 draw_boot_status_line(UiTheme::OCEAN_STATUS);
3365 } else {
3366 writer.enabled = false;
3367 VGA_AVAILABLE.store(false, Ordering::Relaxed);
3368 log::info!("Framebuffer console unavailable (font parse/init failed)");
3369 }
3370}
3371
3372#[macro_export]
3374macro_rules! vga_print {
3375 ($($arg:tt)*) => {
3376 $crate::arch::x86_64::vga::_print(format_args!($($arg)*));
3377 };
3378}
3379
3380#[macro_export]
3382macro_rules! vga_println {
3383 () => ($crate::vga_print!("\n"));
3384 ($($arg:tt)*) => ($crate::vga_print!("{}\n", format_args!($($arg)*)));
3385}
3386
3387#[doc(hidden)]
3389pub fn _print(args: fmt::Arguments) {
3390 use core::fmt::Write;
3391 if is_available() {
3392 VGA_WRITER.lock().write_fmt(args).ok();
3393 return;
3394 }
3395 crate::arch::x86_64::serial::_print(args);
3396}
3397
3398#[derive(Debug, Clone, Copy)]
3399pub struct Canvas {
3400 fg: RgbColor,
3401 bg: RgbColor,
3402}
3403
3404impl Default for Canvas {
3405 fn default() -> Self {
3407 Self {
3408 fg: RgbColor::LIGHT_GREY,
3409 bg: RgbColor::BLACK,
3410 }
3411 }
3412}
3413
3414impl Canvas {
3415 pub const fn new(fg: RgbColor, bg: RgbColor) -> Self {
3417 Self { fg, bg }
3418 }
3419
3420 pub fn set_fg(&mut self, fg: RgbColor) {
3422 self.fg = fg;
3423 }
3424
3425 pub fn set_bg(&mut self, bg: RgbColor) {
3427 self.bg = bg;
3428 }
3429
3430 pub fn set_colors(&mut self, fg: RgbColor, bg: RgbColor) {
3432 self.fg = fg;
3433 self.bg = bg;
3434 }
3435
3436 pub fn set_clip_rect(&self, x: usize, y: usize, w: usize, h: usize) {
3438 set_clip_rect(x, y, w, h);
3439 }
3440
3441 pub fn reset_clip_rect(&self) {
3443 reset_clip_rect();
3444 }
3445
3446 pub fn clear(&self) {
3448 fill_rect(0, 0, width(), height(), self.bg);
3449 }
3450
3451 pub fn pixel(&self, x: usize, y: usize) {
3453 draw_pixel(x, y, self.fg);
3454 }
3455
3456 pub fn line(&self, x0: isize, y0: isize, x1: isize, y1: isize) {
3458 draw_line(x0, y0, x1, y1, self.fg);
3459 }
3460
3461 pub fn rect(&self, x: usize, y: usize, w: usize, h: usize) {
3463 draw_rect(x, y, w, h, self.fg);
3464 }
3465
3466 pub fn fill_rect(&self, x: usize, y: usize, w: usize, h: usize) {
3468 fill_rect(x, y, w, h, self.fg);
3469 }
3470
3471 pub fn fill_rect_alpha(&self, x: usize, y: usize, w: usize, h: usize, alpha: u8) {
3473 fill_rect_alpha(x, y, w, h, self.fg, alpha);
3474 }
3475
3476 pub fn text(&self, x: usize, y: usize, text: &str) {
3478 draw_text_at(x, y, text, self.fg, self.bg);
3479 }
3480
3481 pub fn text_opts(
3483 &self,
3484 x: usize,
3485 y: usize,
3486 text: &str,
3487 align: TextAlign,
3488 wrap: bool,
3489 max_width: Option<usize>,
3490 ) -> TextMetrics {
3491 draw_text(
3492 x,
3493 y,
3494 text,
3495 TextOptions {
3496 fg: self.fg,
3497 bg: self.bg,
3498 align,
3499 wrap,
3500 max_width,
3501 },
3502 )
3503 }
3504
3505 pub fn measure_text(&self, text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
3507 measure_text(text, max_width, wrap)
3508 }
3509
3510 pub fn blit_rgb(&self, x: usize, y: usize, w: usize, h: usize, pixels: &[RgbColor]) -> bool {
3512 blit_rgb(x, y, w, h, pixels)
3513 }
3514
3515 pub fn blit_rgb24(&self, x: usize, y: usize, w: usize, h: usize, bytes: &[u8]) -> bool {
3517 blit_rgb24(x, y, w, h, bytes)
3518 }
3519
3520 pub fn blit_rgba(
3522 &self,
3523 x: usize,
3524 y: usize,
3525 w: usize,
3526 h: usize,
3527 bytes: &[u8],
3528 global_alpha: u8,
3529 ) -> bool {
3530 blit_rgba(x, y, w, h, bytes, global_alpha)
3531 }
3532
3533 pub fn blit_sprite_rgba(
3535 &self,
3536 x: usize,
3537 y: usize,
3538 sprite: SpriteRgba<'_>,
3539 global_alpha: u8,
3540 ) -> bool {
3541 blit_sprite_rgba(x, y, sprite, global_alpha)
3542 }
3543
3544 pub fn begin_frame(&self) -> bool {
3546 begin_frame()
3547 }
3548
3549 pub fn end_frame(&self) {
3551 end_frame();
3552 }
3553
3554 pub fn ui_clear(&self, theme: UiTheme) {
3556 ui_clear(theme);
3557 }
3558
3559 pub fn ui_panel(
3561 &self,
3562 x: usize,
3563 y: usize,
3564 w: usize,
3565 h: usize,
3566 title: &str,
3567 body: &str,
3568 theme: UiTheme,
3569 ) {
3570 ui_draw_panel(x, y, w, h, title, body, theme);
3571 }
3572
3573 pub fn ui_status_bar(&self, left: &str, right: &str, theme: UiTheme) {
3575 ui_draw_status_bar(left, right, theme);
3576 }
3577
3578 pub fn system_status_line(&self, theme: UiTheme) {
3580 draw_system_status_line(theme);
3581 }
3582
3583 pub fn layout_screen(&self) -> UiDockLayout {
3585 UiDockLayout::from_screen()
3586 }
3587
3588 pub fn ui_label(&self, label: &UiLabel<'_>) {
3590 ui_draw_label(label);
3591 }
3592
3593 pub fn ui_progress_bar(&self, bar: UiProgressBar) {
3595 ui_draw_progress_bar(bar);
3596 }
3597
3598 pub fn ui_table(&self, table: &UiTable) {
3600 ui_draw_table(table);
3601 }
3602}
3603
3604pub fn width() -> usize {
3606 if !is_available() {
3607 return 0;
3608 }
3609 VGA_WRITER.lock().width()
3610}
3611
3612pub fn height() -> usize {
3614 if !is_available() {
3615 return 0;
3616 }
3617 VGA_WRITER.lock().height()
3618}
3619
3620pub fn screen_size() -> (usize, usize) {
3622 (width(), height())
3623}
3624
3625pub fn ui_layout_screen() -> UiDockLayout {
3627 UiDockLayout::from_screen()
3628}
3629
3630pub fn glyph_size() -> (usize, usize) {
3632 if !is_available() {
3633 return (0, 0);
3634 }
3635 VGA_WRITER.lock().glyph_size()
3636}
3637
3638pub fn text_cols() -> usize {
3640 if !is_available() {
3641 return 0;
3642 }
3643 VGA_WRITER.lock().cols()
3644}
3645
3646pub fn text_rows() -> usize {
3648 if !is_available() {
3649 return 0;
3650 }
3651 VGA_WRITER.lock().rows()
3652}
3653
3654pub fn get_text_cursor() -> (usize, usize) {
3656 if !is_available() {
3657 return (0, 0);
3658 }
3659 let writer = VGA_WRITER.lock();
3660 (writer.col, writer.row)
3661}
3662
3663pub fn set_text_cursor(col: usize, row: usize) {
3665 if !is_available() {
3666 return;
3667 }
3668 VGA_WRITER.lock().set_cursor_cell(col, row);
3669}
3670
3671pub fn double_buffer_mode() -> bool {
3673 DOUBLE_BUFFER_MODE.load(Ordering::Relaxed)
3674}
3675
3676pub fn set_double_buffer_mode(enabled: bool) {
3678 DOUBLE_BUFFER_MODE.store(enabled, Ordering::Relaxed);
3679}
3680
3681pub fn draw_text_cursor(color: RgbColor) {
3683 if !is_available() {
3684 return;
3685 }
3686 let mut writer = VGA_WRITER.lock();
3687 writer.draw_text_cursor_overlay(color);
3688}
3689
3690pub fn framebuffer_info() -> FramebufferInfo {
3692 if !is_available() {
3693 return FramebufferInfo {
3694 available: false,
3695 width: 0,
3696 height: 0,
3697 pitch: 0,
3698 bpp: 0,
3699 red_size: 0,
3700 red_shift: 0,
3701 green_size: 0,
3702 green_shift: 0,
3703 blue_size: 0,
3704 blue_shift: 0,
3705 text_cols: 0,
3706 text_rows: 0,
3707 glyph_w: 0,
3708 glyph_h: 0,
3709 double_buffer_mode: false,
3710 double_buffer_enabled: false,
3711 ui_scale: UiScale::Normal,
3712 };
3713 }
3714 VGA_WRITER.lock().framebuffer_info()
3715}
3716
3717pub fn render_stats() -> RenderStats {
3719 RenderStats {
3720 presented_frames: PRESENTED_FRAMES.load(Ordering::Relaxed),
3721 estimated_fps: FPS_ESTIMATE.load(Ordering::Relaxed),
3722 present_region_count: VGA_PRESENT_REGION_COUNT.load(Ordering::Relaxed),
3723 present_pixel_count: VGA_PRESENT_PIXEL_COUNT.load(Ordering::Relaxed),
3724 }
3725}
3726
3727pub fn set_text_color(fg: RgbColor, bg: RgbColor) {
3729 if !is_available() {
3730 return;
3731 }
3732 VGA_WRITER.lock().set_rgb_color(fg, bg);
3733}
3734
3735pub fn set_clip_rect(x: usize, y: usize, width: usize, height: usize) {
3737 if !is_available() {
3738 return;
3739 }
3740 VGA_WRITER.lock().set_clip_rect(x, y, width, height);
3741}
3742
3743pub fn reset_clip_rect() {
3745 if !is_available() {
3746 return;
3747 }
3748 VGA_WRITER.lock().reset_clip_rect();
3749}
3750
3751pub fn begin_frame() -> bool {
3753 if !is_available() {
3754 return false;
3755 }
3756 if !double_buffer_mode() {
3757 return false;
3758 }
3759 VGA_WRITER.lock().enable_double_buffer()
3760}
3761
3762pub fn end_frame() {
3764 if !is_available() {
3765 return;
3766 }
3767 let mut writer = VGA_WRITER.lock();
3768 writer.present();
3769 writer.disable_double_buffer(false);
3770}
3771
3772pub fn present() {
3774 if !is_available() {
3775 return;
3776 }
3777 VGA_WRITER.lock().present();
3778}
3779
3780pub fn draw_pixel(x: usize, y: usize, color: RgbColor) {
3782 if !is_available() {
3783 return;
3784 }
3785 VGA_WRITER.lock().draw_pixel(x, y, color);
3786}
3787
3788pub fn draw_pixel_alpha(x: usize, y: usize, color: RgbColor, alpha: u8) {
3790 if !is_available() {
3791 return;
3792 }
3793 VGA_WRITER.lock().draw_pixel_alpha(x, y, color, alpha);
3794}
3795
3796pub fn draw_line(x0: isize, y0: isize, x1: isize, y1: isize, color: RgbColor) {
3798 if !is_available() {
3799 return;
3800 }
3801 VGA_WRITER.lock().draw_line(x0, y0, x1, y1, color);
3802}
3803
3804pub fn draw_rect(x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
3806 if !is_available() {
3807 return;
3808 }
3809 VGA_WRITER.lock().draw_rect(x, y, width, height, color);
3810}
3811
3812pub fn fill_rect(x: usize, y: usize, width: usize, height: usize, color: RgbColor) {
3814 if !is_available() {
3815 return;
3816 }
3817 VGA_WRITER.lock().fill_rect(x, y, width, height, color);
3818}
3819
3820pub fn fill_rect_alpha(
3822 x: usize,
3823 y: usize,
3824 width: usize,
3825 height: usize,
3826 color: RgbColor,
3827 alpha: u8,
3828) {
3829 if !is_available() {
3830 return;
3831 }
3832 VGA_WRITER
3833 .lock()
3834 .fill_rect_alpha(x, y, width, height, color, alpha);
3835}
3836
3837pub fn blit_rgb(
3839 dst_x: usize,
3840 dst_y: usize,
3841 src_width: usize,
3842 src_height: usize,
3843 pixels: &[RgbColor],
3844) -> bool {
3845 if !is_available() {
3846 return false;
3847 }
3848 VGA_WRITER
3849 .lock()
3850 .blit_rgb(dst_x, dst_y, src_width, src_height, pixels)
3851}
3852
3853pub fn blit_rgb24(
3855 dst_x: usize,
3856 dst_y: usize,
3857 src_width: usize,
3858 src_height: usize,
3859 bytes: &[u8],
3860) -> bool {
3861 if !is_available() {
3862 return false;
3863 }
3864 VGA_WRITER
3865 .lock()
3866 .blit_rgb24(dst_x, dst_y, src_width, src_height, bytes)
3867}
3868
3869pub fn blit_rgba(
3871 dst_x: usize,
3872 dst_y: usize,
3873 src_width: usize,
3874 src_height: usize,
3875 bytes: &[u8],
3876 global_alpha: u8,
3877) -> bool {
3878 if !is_available() {
3879 return false;
3880 }
3881 VGA_WRITER
3882 .lock()
3883 .blit_rgba(dst_x, dst_y, src_width, src_height, bytes, global_alpha)
3884}
3885
3886pub fn blit_sprite_rgba(
3888 dst_x: usize,
3889 dst_y: usize,
3890 sprite: SpriteRgba<'_>,
3891 global_alpha: u8,
3892) -> bool {
3893 if !is_available() {
3894 return false;
3895 }
3896 VGA_WRITER
3897 .lock()
3898 .blit_sprite_rgba(dst_x, dst_y, sprite, global_alpha)
3899}
3900
3901pub fn draw_text_at(pixel_x: usize, pixel_y: usize, text: &str, fg: RgbColor, bg: RgbColor) {
3903 if !is_available() {
3904 return;
3905 }
3906 VGA_WRITER
3907 .lock()
3908 .draw_text_at(pixel_x, pixel_y, text, fg, bg);
3909}
3910
3911pub fn draw_text(pixel_x: usize, pixel_y: usize, text: &str, opts: TextOptions) -> TextMetrics {
3913 if !is_available() {
3914 return TextMetrics {
3915 width: 0,
3916 height: 0,
3917 lines: 0,
3918 };
3919 }
3920 VGA_WRITER.lock().draw_text(pixel_x, pixel_y, text, opts)
3921}
3922
3923pub fn measure_text(text: &str, max_width: Option<usize>, wrap: bool) -> TextMetrics {
3925 if !is_available() {
3926 return TextMetrics {
3927 width: 0,
3928 height: 0,
3929 lines: 0,
3930 };
3931 }
3932 VGA_WRITER.lock().measure_text(text, max_width, wrap)
3933}
3934
3935pub fn ui_clear(theme: UiTheme) {
3937 let _ = with_writer(|w| w.clear_with(theme.background));
3938}
3939
3940pub fn ui_draw_panel(
3942 x: usize,
3943 y: usize,
3944 width: usize,
3945 height: usize,
3946 title: &str,
3947 body: &str,
3948 theme: UiTheme,
3949) {
3950 let _ = with_writer(|w| {
3951 if width < 8 || height < 8 {
3952 return;
3953 }
3954 let (gw, gh) = w.glyph_size();
3955 w.fill_rect(x, y, width, height, theme.panel_bg);
3956 w.draw_rect(x, y, width, height, theme.panel_border);
3957
3958 let title_h = gh + 6;
3959 w.fill_rect(
3960 x.saturating_add(1),
3961 y.saturating_add(1),
3962 width.saturating_sub(2),
3963 title_h,
3964 theme.accent,
3965 );
3966 let title_opts = TextOptions {
3967 fg: theme.text,
3968 bg: theme.accent,
3969 align: TextAlign::Left,
3970 wrap: false,
3971 max_width: Some(width.saturating_sub(10)),
3972 };
3973 w.draw_text(x.saturating_add(6), y.saturating_add(3), title, title_opts);
3974
3975 let body_opts = TextOptions {
3976 fg: theme.text,
3977 bg: theme.panel_bg,
3978 align: TextAlign::Left,
3979 wrap: true,
3980 max_width: Some(width.saturating_sub(10)),
3981 };
3982 w.draw_text(
3983 x.saturating_add(6),
3984 y.saturating_add(title_h + 4),
3985 body,
3986 body_opts,
3987 );
3988
3989 w.fill_rect(
3991 x.saturating_add(1),
3992 y.saturating_add(title_h + 1),
3993 width.saturating_sub(2),
3994 1,
3995 theme.panel_border,
3996 );
3997 let _ = gw;
3999 });
4000}
4001
4002pub fn ui_draw_panel_widget(panel: &UiPanel<'_>) {
4004 ui_draw_panel(
4005 panel.rect.x,
4006 panel.rect.y,
4007 panel.rect.w,
4008 panel.rect.h,
4009 panel.title,
4010 panel.body,
4011 panel.theme,
4012 );
4013}
4014
4015pub fn ui_draw_label(label: &UiLabel<'_>) {
4017 let _ = with_writer(|w| {
4018 w.draw_text(
4019 label.rect.x,
4020 label.rect.y,
4021 label.text,
4022 TextOptions {
4023 fg: label.fg,
4024 bg: label.bg,
4025 align: label.align,
4026 wrap: false,
4027 max_width: Some(label.rect.w),
4028 },
4029 );
4030 });
4031}
4032
4033pub fn ui_draw_progress_bar(bar: UiProgressBar) {
4035 let _ = with_writer(|w| {
4036 if bar.rect.w < 3 || bar.rect.h < 3 {
4037 return;
4038 }
4039 let value = core::cmp::min(bar.value, 100) as usize;
4040 w.fill_rect(bar.rect.x, bar.rect.y, bar.rect.w, bar.rect.h, bar.bg);
4041 w.draw_rect(bar.rect.x, bar.rect.y, bar.rect.w, bar.rect.h, bar.border);
4042 let inner_w = bar.rect.w.saturating_sub(2);
4043 let fill_w = inner_w.saturating_mul(value) / 100;
4044 if fill_w > 0 {
4045 w.fill_rect(
4046 bar.rect.x + 1,
4047 bar.rect.y + 1,
4048 fill_w,
4049 bar.rect.h.saturating_sub(2),
4050 bar.fg,
4051 );
4052 }
4053 });
4054}
4055
4056pub fn ui_draw_table(table: &UiTable) {
4058 let _ = with_writer(|w| {
4059 if table.rect.w < 8 || table.rect.h < 8 {
4060 return;
4061 }
4062 let (_gw, gh) = w.glyph_size();
4063 if gh == 0 {
4064 return;
4065 }
4066
4067 w.fill_rect(
4068 table.rect.x,
4069 table.rect.y,
4070 table.rect.w,
4071 table.rect.h,
4072 table.theme.panel_bg,
4073 );
4074 w.draw_rect(
4075 table.rect.x,
4076 table.rect.y,
4077 table.rect.w,
4078 table.rect.h,
4079 table.theme.panel_border,
4080 );
4081
4082 let cols = core::cmp::max(1, table.headers.len());
4083 let col_w = table.rect.w / cols;
4084 let header_h = gh + 2;
4085 w.fill_rect(
4086 table.rect.x + 1,
4087 table.rect.y + 1,
4088 table.rect.w.saturating_sub(2),
4089 header_h,
4090 table.theme.accent,
4091 );
4092
4093 for (i, h) in table.headers.iter().enumerate() {
4094 let x = table.rect.x + i * col_w + 2;
4095 w.draw_text(
4096 x,
4097 table.rect.y + 1,
4098 h,
4099 TextOptions {
4100 fg: table.theme.text,
4101 bg: table.theme.accent,
4102 align: TextAlign::Left,
4103 wrap: false,
4104 max_width: Some(col_w.saturating_sub(4)),
4105 },
4106 );
4107 }
4108
4109 let mut y = table.rect.y + header_h + 2;
4110 for row in &table.rows {
4111 if y + gh > table.rect.y + table.rect.h {
4112 break;
4113 }
4114 for c in 0..cols {
4115 if c >= row.len() {
4116 continue;
4117 }
4118 let x = table.rect.x + c * col_w + 2;
4119 w.draw_text(
4120 x,
4121 y,
4122 &row[c],
4123 TextOptions {
4124 fg: table.theme.text,
4125 bg: table.theme.panel_bg,
4126 align: TextAlign::Left,
4127 wrap: false,
4128 max_width: Some(col_w.saturating_sub(4)),
4129 },
4130 );
4131 }
4132 y += gh;
4133 }
4134 });
4135}
4136
4137pub fn ui_draw_status_bar(left: &str, right: &str, theme: UiTheme) {
4139 let _ = with_writer(|w| {
4140 draw_status_bar_inner(w, left, right, theme);
4141 });
4142}
4143
4144pub fn set_status_hostname(hostname: &str) {
4146 let mut guard = STATUS_LINE_INFO.lock();
4147 if guard.is_none() {
4148 *guard = Some(StatusLineInfo {
4149 hostname: String::new(),
4150 ip: String::from("n/a"),
4151 });
4152 }
4153 if let Some(info) = guard.as_mut() {
4154 info.hostname.clear();
4155 info.hostname.push_str(hostname);
4156 }
4157}
4158
4159pub fn set_status_ip(ip: &str) {
4161 let mut guard = STATUS_LINE_INFO.lock();
4162 if guard.is_none() {
4163 *guard = Some(StatusLineInfo {
4164 hostname: String::from("strat9"),
4165 ip: String::new(),
4166 });
4167 }
4168 if let Some(info) = guard.as_mut() {
4169 info.ip.clear();
4170 info.ip.push_str(ip);
4171 }
4172}
4173
4174pub fn draw_system_status_line(theme: UiTheme) {
4176 let info = status_line_info();
4177 let version = env!("CARGO_PKG_VERSION");
4178 let tick = crate::process::scheduler::ticks();
4179 let uptime = format_uptime_from_ticks(tick);
4180 let mem = format_mem_usage();
4181 let fps = current_fps(tick);
4182 let left = format!(" {} ", info.hostname);
4183 let right = format!(
4184 "ip:{} ver:{} uptime:{} ticks:{} FPS:{} load:n/a memfree:{} ",
4185 info.ip, version, uptime, tick, fps, mem
4186 );
4187 ui_draw_status_bar(&left, &right, theme);
4188}
4189
4190fn draw_boot_status_line(theme: UiTheme) {
4192 let _ = with_writer(|w| {
4193 draw_status_bar_inner(
4194 w,
4195 " strat9 ",
4196 "ip:n/a ver:boot up:00:00:00 ticks:0 load:n/a mem:n/a ",
4197 theme,
4198 );
4199 });
4200}
4201
4202fn refresh_status_ip_from_net_scheme() {
4204 let tick = crate::process::scheduler::ticks();
4205 let last = STATUS_LAST_IP_REFRESH_TICK.load(Ordering::Relaxed);
4206 if tick.saturating_sub(last) < STATUS_IP_REFRESH_PERIOD_TICKS {
4207 return;
4208 }
4209 if STATUS_LAST_IP_REFRESH_TICK
4210 .compare_exchange(last, tick, Ordering::Relaxed, Ordering::Relaxed)
4211 .is_err()
4212 {
4213 return;
4214 }
4215
4216 let paths = ["/net/address", "/net/ip"];
4217 for path in paths {
4218 let fd = match crate::vfs::open(path, crate::vfs::OpenFlags::READ) {
4219 Ok(fd) => fd,
4220 Err(_) => continue,
4221 };
4222 let mut buf = [0u8; 64];
4223 let read_res = crate::vfs::read(fd, &mut buf);
4224 let _ = crate::vfs::close(fd);
4225 let n = match read_res {
4226 Ok(n) => n,
4227 Err(_) => continue,
4228 };
4229 if n == 0 {
4230 continue;
4231 }
4232 let Ok(text) = core::str::from_utf8(&buf[..n]) else {
4233 continue;
4234 };
4235 let mut ip = text.trim();
4236 if let Some(slash) = ip.find('/') {
4237 ip = &ip[..slash];
4238 }
4239 if ip.is_empty() || ip == "0.0.0.0" || ip == "169.254.0.0" {
4240 continue;
4241 }
4242 set_status_ip(ip);
4243 return;
4244 }
4245
4246 set_status_ip("n/a");
4247}
4248
4249struct StackStr<const N: usize> {
4251 buf: [u8; N],
4252 len: usize,
4253}
4254
4255impl<const N: usize> StackStr<N> {
4256 const fn new() -> Self {
4258 Self {
4259 buf: [0; N],
4260 len: 0,
4261 }
4262 }
4263 fn as_str(&self) -> &str {
4265 unsafe { core::str::from_utf8_unchecked(&self.buf[..self.len]) }
4266 }
4267}
4268
4269impl<const N: usize> core::fmt::Write for StackStr<N> {
4270 fn write_str(&mut self, s: &str) -> core::fmt::Result {
4272 let bytes = s.as_bytes();
4273 let avail = N - self.len;
4274 let n = bytes.len().min(avail);
4275 self.buf[self.len..self.len + n].copy_from_slice(&bytes[..n]);
4276 self.len += n;
4277 Ok(())
4278 }
4279}
4280
4281pub fn maybe_refresh_system_status_line(theme: UiTheme) {
4283 if !is_available() {
4284 return;
4285 }
4286
4287 let tick = crate::process::scheduler::ticks();
4288 let last = STATUS_LAST_REFRESH_TICK.load(Ordering::Relaxed);
4289 if tick.saturating_sub(last) < STATUS_REFRESH_PERIOD_TICKS {
4290 return;
4291 }
4292 if STATUS_LAST_REFRESH_TICK
4293 .compare_exchange(last, tick, Ordering::Relaxed, Ordering::Relaxed)
4294 .is_err()
4295 {
4296 return;
4297 }
4298 refresh_status_ip_from_net_scheme();
4299
4300 let (hostname, ip) = if let Some(guard) = STATUS_LINE_INFO.try_lock() {
4301 if let Some(info) = guard.as_ref() {
4302 let mut h = StackStr::<64>::new();
4303 let mut i = StackStr::<48>::new();
4304 let _ = core::fmt::Write::write_str(&mut h, &info.hostname);
4305 let _ = core::fmt::Write::write_str(&mut i, &info.ip);
4306 (h, i)
4307 } else {
4308 let mut h = StackStr::<64>::new();
4309 let mut i = StackStr::<48>::new();
4310 let _ = core::fmt::Write::write_str(&mut h, "strat9");
4311 let _ = core::fmt::Write::write_str(&mut i, "n/a");
4312 (h, i)
4313 }
4314 } else {
4315 return;
4316 };
4317
4318 let version = env!("CARGO_PKG_VERSION");
4319
4320 let total_secs = tick / 100;
4321 let h = total_secs / 3600;
4322 let m = (total_secs % 3600) / 60;
4323 let s = total_secs % 60;
4324
4325 let mem_str = {
4326 use core::fmt::Write;
4327 let lock = crate::memory::buddy::get_allocator();
4328 if let Some(guard) = lock.try_lock() {
4329 if let Some(alloc) = guard.as_ref() {
4330 let (tp, ap) = alloc.page_totals();
4331 let total = tp.saturating_mul(4096);
4332 let used = ap.saturating_mul(4096);
4333 let free = total.saturating_sub(used);
4334 let mut buf = StackStr::<32>::new();
4335 let _ = write!(
4336 buf,
4337 "{}/{}",
4338 format_size_stack(free),
4339 format_size_stack(total)
4340 );
4341 buf
4342 } else {
4343 let mut buf = StackStr::<32>::new();
4344 let _ = core::fmt::Write::write_str(&mut buf, "n/a");
4345 buf
4346 }
4347 } else {
4348 let mut buf = StackStr::<32>::new();
4349 let _ = core::fmt::Write::write_str(&mut buf, "n/a");
4350 buf
4351 }
4352 };
4353
4354 let fps = current_fps(tick);
4355
4356 use core::fmt::Write;
4357 let mut left = StackStr::<80>::new();
4358 let _ = write!(left, " {} ", hostname.as_str());
4359
4360 let mut right = StackStr::<256>::new();
4361 let _ = write!(
4362 right,
4363 "ip:{} ver:{} up:{:02}:{:02}:{:02} ticks:{} fps:{} load:n/a mem:{} ",
4364 ip.as_str(),
4365 version,
4366 h,
4367 m,
4368 s,
4369 tick,
4370 fps,
4371 mem_str.as_str()
4372 );
4373
4374 if let Some(mut writer) = VGA_WRITER.try_lock() {
4375 draw_status_bar_inner(&mut writer, left.as_str(), right.as_str(), theme);
4376 }
4377}
4378
4379fn format_size_stack(bytes: usize) -> StackStr<16> {
4381 use core::fmt::Write;
4382 const KB: usize = 1024;
4383 const MB: usize = 1024 * KB;
4384 const GB: usize = 1024 * MB;
4385 let mut buf = StackStr::<16>::new();
4386 if bytes >= GB {
4387 let _ = write!(buf, "{}G", bytes / GB);
4388 } else if bytes >= MB {
4389 let _ = write!(buf, "{}M", bytes / MB);
4390 } else if bytes >= KB {
4391 let _ = write!(buf, "{}K", bytes / KB);
4392 } else {
4393 let _ = write!(buf, "{}B", bytes);
4394 }
4395 buf
4396}
4397
4398impl<const N: usize> core::fmt::Display for StackStr<N> {
4399 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
4401 f.write_str(self.as_str())
4402 }
4403}
4404
4405pub extern "C" fn status_line_task_main() -> ! {
4407 let mut last_tick = 0u64;
4408 let mut diag_counter = 0u64;
4409 loop {
4410 let tick = crate::process::scheduler::ticks();
4411 if tick != last_tick {
4412 last_tick = tick;
4413 maybe_refresh_system_status_line(UiTheme::OCEAN_STATUS);
4414 }
4415 diag_counter += 1;
4416 if diag_counter % 5000 == 0 {
4417 crate::serial_println!(
4418 "[status-line] heartbeat tick={} vga={}",
4419 tick,
4420 is_available()
4421 );
4422 }
4423 crate::process::yield_task();
4424 }
4425}
4426
4427pub fn draw_strata_stack(origin_x: usize, origin_y: usize, layer_w: usize, layer_h: usize) {
4429 if !is_available() {
4430 return;
4431 }
4432 VGA_WRITER
4433 .lock()
4434 .draw_strata_stack(origin_x, origin_y, layer_w, layer_h);
4435}
4436pub fn scroll_view_up(lines: usize) {
4440 if !is_available() {
4441 return;
4442 }
4443 VGA_WRITER.lock().scroll_view_up(lines);
4444}
4445
4446pub fn scroll_view_down(lines: usize) {
4448 if !is_available() {
4449 return;
4450 }
4451 VGA_WRITER.lock().scroll_view_down(lines);
4452}
4453
4454pub fn scroll_to_live() {
4456 if !is_available() {
4457 return;
4458 }
4459 if let Some(mut w) = VGA_WRITER.try_lock() {
4460 w.scroll_to_live();
4461 }
4462}
4463
4464pub fn scrollbar_click(px_x: usize, px_y: usize) {
4467 if !is_available() {
4468 return;
4469 }
4470 if let Some(mut w) = VGA_WRITER.try_lock() {
4471 w.scrollbar_click(px_x, px_y);
4472 }
4473}
4474
4475pub fn scrollbar_drag_to(px_y: usize) {
4477 if !is_available() {
4478 return;
4479 }
4480 if let Some(mut w) = VGA_WRITER.try_lock() {
4481 w.scrollbar_drag_to(px_y);
4482 }
4483}
4484
4485pub fn scrollbar_hit_test(px_x: usize, px_y: usize) -> bool {
4487 if !is_available() {
4488 return false;
4489 }
4490 if let Some(w) = VGA_WRITER.try_lock() {
4491 w.scrollbar_hit_test(px_x, px_y)
4492 } else {
4493 false
4494 }
4495}
4496
4497pub fn update_mouse_cursor(x: i32, y: i32) {
4499 if !is_available() {
4500 return;
4501 }
4502 if let Some(mut w) = VGA_WRITER.try_lock() {
4503 w.update_mouse_cursor(x, y);
4504 }
4505}
4506
4507pub fn hide_mouse_cursor() {
4509 if !is_available() {
4510 return;
4511 }
4512 if let Some(mut w) = VGA_WRITER.try_lock() {
4513 w.hide_mouse_cursor();
4514 }
4515}
4516
4517pub fn start_selection(px: usize, py: usize) {
4519 if !is_available() {
4520 return;
4521 }
4522 if let Some(mut w) = VGA_WRITER.try_lock() {
4523 w.start_selection(px, py);
4524 }
4525}
4526
4527pub fn update_selection(px: usize, py: usize) {
4529 if !is_available() {
4530 return;
4531 }
4532 if let Some(mut w) = VGA_WRITER.try_lock() {
4533 w.update_selection(px, py);
4534 }
4535}
4536
4537pub fn end_selection() {
4539 if !is_available() {
4540 return;
4541 }
4542 if let Some(mut w) = VGA_WRITER.try_lock() {
4543 w.end_selection();
4544 }
4545}
4546
4547pub fn clear_selection() {
4549 if !is_available() {
4550 return;
4551 }
4552 if let Some(mut w) = VGA_WRITER.try_lock() {
4553 w.clear_selection();
4554 }
4555}
4556
4557pub fn get_clipboard_text(buf: &mut [u8]) -> usize {
4559 if let Some(clip) = CLIPBOARD.try_lock() {
4560 let n = clip.1.min(buf.len());
4561 buf[..n].copy_from_slice(&clip.0[..n]);
4562 n
4563 } else {
4564 0
4565 }
4566}