summaryrefslogtreecommitdiffstats
path: root/rsnix/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--rsnix/src/cursor.rs19
-rw-r--r--rsnix/src/gop.rs103
-rw-r--r--rsnix/src/lib.rs9
-rw-r--r--rsnix/src/ps2.rs20
-rw-r--r--rsnix/src/sched.rs59
-rw-r--r--rsnix/src/tty.rs25
-rw-r--r--rsnix/src/vt.rs25
-rw-r--r--rsnix/src/wm.rs592
8 files changed, 818 insertions, 34 deletions
diff --git a/rsnix/src/cursor.rs b/rsnix/src/cursor.rs
index 31b919e..b986b23 100644
--- a/rsnix/src/cursor.rs
+++ b/rsnix/src/cursor.rs
@@ -49,14 +49,29 @@ fn forward_target() -> Option<usize> {
49} 49}
50 50
51fn task_main() { 51fn task_main() {
52 let mut prev_buttons: u8 = 0;
52 loop { 53 loop {
53 while let Some(ev) = crate::usb::poll_mouse() 54 while let Some(ev) = crate::usb::poll_mouse()
54 .or_else(crate::ehci::poll_mouse) 55 .or_else(crate::ehci::poll_mouse)
55 .or_else(crate::uhci::poll_mouse) 56 .or_else(crate::uhci::poll_mouse)
56 .or_else(crate::ps2::try_poll_mouse) 57 .or_else(crate::ps2::try_poll_mouse)
57 { 58 {
58 crate::gop::cursor_move(ev.dx, ev.dy); 59 let (cx, cy) = crate::gop::cursor_move(ev.dx, ev.dy);
59 crate::gop::cursor_set_buttons(btn_bits(&ev)); 60 let buttons = btn_bits(&ev);
61 crate::gop::cursor_set_buttons(buttons);
62 // Left-button edge detection → press/release.
63 let kind = if buttons & 1 != 0 && prev_buttons & 1 == 0 {
64 crate::ps2::MouseKind::Press
65 } else if buttons & 1 == 0 && prev_buttons & 1 != 0 {
66 crate::ps2::MouseKind::Release
67 } else {
68 crate::ps2::MouseKind::Move
69 };
70 prev_buttons = buttons;
71 // Route to the window manager (focus + deliver to the focused
72 // window with window-relative coordinates).
73 crate::wm::route_mouse(cx as i32, cy as i32, buttons, kind);
74 // Legacy single-target debug routing (`mouse <id>`).
60 if let Some(id) = forward_target() 75 if let Some(id) = forward_target()
61 && crate::sched::send(id, TaskMsg::Mouse(ev)).is_err() 76 && crate::sched::send(id, TaskMsg::Mouse(ev)).is_err()
62 { 77 {
diff --git a/rsnix/src/gop.rs b/rsnix/src/gop.rs
index 676814e..deff226 100644
--- a/rsnix/src/gop.rs
+++ b/rsnix/src/gop.rs
@@ -4,10 +4,10 @@
4/// module is the low-level rasteriser: glyph blits and pixel shifts on the 4/// module is the low-level rasteriser: glyph blits and pixel shifts on the
5/// linear framebuffer. 5/// linear framebuffer.
6use crate::font; 6use crate::font;
7use crate::uefi::{GopModeInfo, GraphicsOutputProtocol, EFI_SUCCESS, PIXEL_BGR, PIXEL_RGB}; 7use crate::uefi::{EFI_SUCCESS, GopModeInfo, GraphicsOutputProtocol, PIXEL_BGR, PIXEL_RGB};
8use core::fmt; 8use core::fmt;
9use core::ptr; 9use core::ptr;
10use core::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicU8, Ordering}; 10use core::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
11 11
12/// Framebuffer state captured before ExitBootServices. 12/// Framebuffer state captured before ExitBootServices.
13pub struct Framebuffer { 13pub struct Framebuffer {
@@ -512,6 +512,105 @@ pub fn fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) {
512 push_rect(x, y, w, h); 512 push_rect(x, y, w, h);
513} 513}
514 514
515/// Fill a shadow rectangle WITHOUT pushing to the display. The window
516/// manager composites several surfaces into the shadow and pushes once at
517/// the end.
518pub(crate) fn shadow_fill_rect(x: usize, y: usize, w: usize, h: usize, color: u32) {
519 let Some(fb) = fb().as_ref() else {
520 return;
521 };
522 if unsafe { SHADOW.is_null() } || x >= fb.width || y >= fb.height {
523 return;
524 }
525 let w = w.min(fb.width - x);
526 let h = h.min(fb.height - y);
527 let row_words = fb.pitch / 4;
528 unsafe {
529 for dy in 0..h {
530 let off = (y + dy) * row_words + x;
531 for dx in 0..w {
532 *SHADOW.add(off + dx) = color;
533 }
534 }
535 }
536}
537
538/// Copy a row-major `u32` source buffer (stride in pixels) into the shadow
539/// WITHOUT pushing. Clipped to the framebuffer.
540pub(crate) fn shadow_blit(
541 x: usize,
542 y: usize,
543 w: usize,
544 h: usize,
545 src: *const u32,
546 src_stride: usize,
547) {
548 let Some(fb) = fb().as_ref() else {
549 return;
550 };
551 if unsafe { SHADOW.is_null() } || src.is_null() || x >= fb.width || y >= fb.height {
552 return;
553 }
554 let w = w.min(fb.width - x);
555 let h = h.min(fb.height - y);
556 let row_words = fb.pitch / 4;
557 unsafe {
558 for dy in 0..h {
559 let doff = (y + dy) * row_words + x;
560 let soff = dy * src_stride;
561 for dx in 0..w {
562 *SHADOW.add(doff + dx) = *src.add(soff + dx);
563 }
564 }
565 }
566}
567
568/// Render one 8×16 glyph into the shadow at a pixel origin WITHOUT pushing
569/// (for window chrome and console repaint). Same glyph lookup as
570/// `blit_glyph`, but caller-controlled push.
571pub(crate) fn shadow_glyph_px(x: usize, y: usize, ch: u16, fg: (u8, u8, u8), bg: (u8, u8, u8)) {
572 let Some(fb) = fb().as_ref() else {
573 return;
574 };
575 if unsafe { SHADOW.is_null() } {
576 return;
577 }
578 let glyph = font::GLYPH_8X16;
579 let data = if ch < 256 {
580 match glyph.get(ch as usize) {
581 Some(d) => *d,
582 None => return,
583 }
584 } else if let Some(bits) = unicode_glyph_bits(ch) {
585 bits
586 } else {
587 match glyph.get(32) {
588 Some(d) => *d,
589 None => return,
590 }
591 };
592 let fgc = rgb(fg.0, fg.1, fg.2);
593 let bgc = rgb(bg.0, bg.1, bg.2);
594 let row_words = fb.pitch / 4;
595 unsafe {
596 for cy in 0..16 {
597 let py = y + cy;
598 if py >= fb.height {
599 break;
600 }
601 let row_bits = data[cy];
602 for cx in 0..8 {
603 let px = x + cx;
604 if px >= fb.width {
605 break;
606 }
607 let on = row_bits & (0x80 >> cx) != 0;
608 *SHADOW.add(py * row_words + px) = if on { fgc } else { bgc };
609 }
610 }
611 }
612}
613
515/// Print crash/fault lines directly to the framebuffer, bypassing the vt 614/// Print crash/fault lines directly to the framebuffer, bypassing the vt
516/// entirely (the vt state may be corrupt when an exception fires). 615/// entirely (the vt state may be corrupt when an exception fires).
517/// 616///
diff --git a/rsnix/src/lib.rs b/rsnix/src/lib.rs
index f5c3419..a422057 100644
--- a/rsnix/src/lib.rs
+++ b/rsnix/src/lib.rs
@@ -4,6 +4,7 @@ extern crate alloc;
4 4
5pub mod ahci; 5pub mod ahci;
6pub mod cursor; 6pub mod cursor;
7pub mod ehci;
7pub mod fat; 8pub mod fat;
8pub mod font; 9pub mod font;
9pub mod fs; 10pub mod fs;
@@ -14,16 +15,16 @@ pub mod klog;
14pub mod memory; 15pub mod memory;
15pub mod paging; 16pub mod paging;
16pub mod pci; 17pub mod pci;
17pub mod qr;
18pub mod power; 18pub mod power;
19pub mod ps2; 19pub mod ps2;
20pub mod uhci; 20pub mod qr;
21pub mod ehci;
22pub mod usb;
23pub mod rtc; 21pub mod rtc;
24pub mod sched; 22pub mod sched;
25pub mod serial; 23pub mod serial;
26pub mod strbuf; 24pub mod strbuf;
27pub mod tty; 25pub mod tty;
28pub mod uefi; 26pub mod uefi;
27pub mod uhci;
28pub mod usb;
29pub mod vt; 29pub mod vt;
30pub mod wm;
diff --git a/rsnix/src/ps2.rs b/rsnix/src/ps2.rs
index 965e4a0..cac476f 100644
--- a/rsnix/src/ps2.rs
+++ b/rsnix/src/ps2.rs
@@ -463,6 +463,26 @@ pub struct MouseEvent {
463 pub middle: bool, 463 pub middle: bool,
464} 464}
465 465
466/// What happened in a window-relative (GUI) mouse event.
467#[derive(Clone, Copy, Debug, PartialEq, Eq)]
468pub enum MouseKind {
469 Move,
470 Press,
471 Release,
472}
473
474/// A mouse event delivered to a focused GUI window: absolute coordinates
475/// relative to the window's content top-left, the current button bitmask,
476/// and the action (move / press / release). Unlike `MouseEvent` (relative
477/// deltas), this is what an app's drawing loop consumes.
478#[derive(Clone, Copy, Debug)]
479pub struct AppMouseEvent {
480 pub x: i32,
481 pub y: i32,
482 pub buttons: u8,
483 pub kind: MouseKind,
484}
485
466/// Packet assembly state — lives in main context (consumer side), fed by 486/// Packet assembly state — lives in main context (consumer side), fed by
467/// the raw byte ring that the IRQ12 handler fills. 487/// the raw byte ring that the IRQ12 handler fills.
468struct MouseState { 488struct MouseState {
diff --git a/rsnix/src/sched.rs b/rsnix/src/sched.rs
index 259ddf6..39ecb65 100644
--- a/rsnix/src/sched.rs
+++ b/rsnix/src/sched.rs
@@ -59,6 +59,9 @@ const MSG_CAP: usize = 16;
59pub enum TaskMsg { 59pub enum TaskMsg {
60 Key(crate::ps2::KeyEvent), 60 Key(crate::ps2::KeyEvent),
61 Mouse(crate::ps2::MouseEvent), 61 Mouse(crate::ps2::MouseEvent),
62 AppMouse(crate::ps2::AppMouseEvent),
63 Focus(bool),
64 Close,
62 Timer, 65 Timer,
63 User(u64), 66 User(u64),
64} 67}
@@ -94,6 +97,11 @@ pub fn msg_describe(msg: &TaskMsg) -> alloc::string::String {
94 if m.left { "L" } else { "-" }, 97 if m.left { "L" } else { "-" },
95 if m.right { "R" } else { "-" } 98 if m.right { "R" } else { "-" }
96 ), 99 ),
100 TaskMsg::AppMouse(m) => {
101 alloc::format!("AppMouse({},{} {:?} b={:#x})", m.x, m.y, m.kind, m.buttons)
102 }
103 TaskMsg::Focus(f) => alloc::format!("Focus({})", f),
104 TaskMsg::Close => "Close".into(),
97 TaskMsg::Timer => "Timer".into(), 105 TaskMsg::Timer => "Timer".into(),
98 TaskMsg::User(n) => alloc::format!("User({})", n), 106 TaskMsg::User(n) => alloc::format!("User({})", n),
99 } 107 }
@@ -179,7 +187,10 @@ struct SpinWatch {
179 last_rsp: usize, 187 last_rsp: usize,
180 same: u32, 188 same: u32,
181} 189}
182const SPIN_WATCH_INIT: SpinWatch = SpinWatch { last_rsp: 0, same: 0 }; 190const SPIN_WATCH_INIT: SpinWatch = SpinWatch {
191 last_rsp: 0,
192 same: 0,
193};
183static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS]; 194static mut SPIN_WATCH: [SpinWatch; MAX_TASKS] = [SPIN_WATCH_INIT; MAX_TASKS];
184/// A spinning background task is interrupted at roughly half the tick rate 195/// A spinning background task is interrupted at roughly half the tick rate
185/// (it alternates with idle), so 1500 hits is ~30 s of wall time. 196/// (it alternates with idle), so 1500 hits is ~30 s of wall time.
@@ -381,11 +392,7 @@ pub fn spawn(entry: fn(), name: &str) -> Option<usize> {
381 }; 392 };
382 let ok = spawn_at(id, entry, name); 393 let ok = spawn_at(id, entry, name);
383 unlock(); 394 unlock();
384 if ok { 395 if ok { Some(id) } else { None }
385 Some(id)
386 } else {
387 None
388 }
389} 396}
390 397
391unsafe fn reap(id: usize) { 398unsafe fn reap(id: usize) {
@@ -698,10 +705,12 @@ pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFra
698 } 705 }
699 let ra = *(fp as *const u64).add(1); // [rbp+8] = return address 706 let ra = *(fp as *const u64).add(1); // [rbp+8] = return address
700 if let Some((start, end)) = filter 707 if let Some((start, end)) = filter
701 && ra >= start as u64 && ra < end as u64 { 708 && ra >= start as u64
702 bt[bn] = ra; 709 && ra < end as u64
703 bn += 1; 710 {
704 } 711 bt[bn] = ra;
712 bn += 1;
713 }
705 let nfp = *(fp as *const u64); 714 let nfp = *(fp as *const u64);
706 if nfp <= fp { 715 if nfp <= fp {
707 break; // chain must grow upward 716 break; // chain must grow upward
@@ -715,19 +724,21 @@ pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFra
715 while p + 8 <= limit && bn < bt.len() { 724 while p + 8 <= limit && bn < bt.len() {
716 let v = *(p as *const u64); 725 let v = *(p as *const u64);
717 if let Some((start, end)) = filter 726 if let Some((start, end)) = filter
718 && v >= start as u64 && v < end as u64 { 727 && v >= start as u64
719 let mut dup = false; 728 && v < end as u64
720 for i in 0..bn { 729 {
721 if bt[i] == v { 730 let mut dup = false;
722 dup = true; 731 for i in 0..bn {
723 break; 732 if bt[i] == v {
724 } 733 dup = true;
725 } 734 break;
726 if !dup {
727 bt[bn] = v;
728 bn += 1;
729 } 735 }
730 } 736 }
737 if !dup {
738 bt[bn] = v;
739 bn += 1;
740 }
741 }
731 p += 8; 742 p += 8;
732 } 743 }
733 } 744 }
@@ -842,7 +853,11 @@ pub(crate) fn fault_dump(kind: &str, cr2: u64, frame: &crate::interrupts::IntFra
842 &mut w, 853 &mut w,
843 core::format_args!( 854 core::format_args!(
844 "rax={:#x} rcx={:#x} rdx={:#x} rsi={:#x} rdi={:#x}", 855 "rax={:#x} rcx={:#x} rdx={:#x} rsi={:#x} rdi={:#x}",
845 frame.rax, frame.rcx, frame.rdx, frame.rsi, frame.rdi 856 frame.rax,
857 frame.rcx,
858 frame.rdx,
859 frame.rsi,
860 frame.rdi
846 ), 861 ),
847 ); 862 );
848 w.len() 863 w.len()
diff --git a/rsnix/src/tty.rs b/rsnix/src/tty.rs
index 0783521..6102fb5 100644
--- a/rsnix/src/tty.rs
+++ b/rsnix/src/tty.rs
@@ -41,14 +41,31 @@ static mut RAW_LEN: usize = 0;
41/// Combined blocking key poll: USB HID keyboard first (polled UHCI), then 41/// Combined blocking key poll: USB HID keyboard first (polled UHCI), then
42/// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands). 42/// the PS/2 path (which parks via `hlt` until an IRQ-fed scancode lands).
43fn poll_key() -> KeyEvent { 43fn poll_key() -> KeyEvent {
44 if let Some(ev) = crate::uhci::try_poll_key() { 44 loop {
45 return ev; 45 // A GUI window has focus: the keyboard pump owns the keys. Park so
46 // the shell/editor don't steal input from the focused app.
47 if crate::wm::focused().is_some() {
48 crate::sched::sleep_ms(5);
49 continue;
50 }
51 if let Some(ev) = crate::uhci::try_poll_key() {
52 return ev;
53 }
54 if let Some(ev) = ps2::try_poll_key() {
55 return ev;
56 }
57 // No key ready: park briefly, then re-check focus (so a focus change
58 // to a GUI window is noticed instead of blocking on a stolen key).
59 crate::sched::sleep_ms(5);
46 } 60 }
47 ps2::poll_key()
48} 61}
49 62
50/// Combined non-blocking key poll (USB first, then PS/2). 63/// Combined non-blocking key poll (USB first, then PS/2). Returns `None`
64/// while a GUI window is focused (the keyboard pump owns the keys).
51fn try_poll_key() -> Option<KeyEvent> { 65fn try_poll_key() -> Option<KeyEvent> {
66 if crate::wm::focused().is_some() {
67 return None;
68 }
52 crate::uhci::try_poll_key().or_else(ps2::try_poll_key) 69 crate::uhci::try_poll_key().or_else(ps2::try_poll_key)
53} 70}
54 71
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs
index 9444063..c5fc5fd 100644
--- a/rsnix/src/vt.rs
+++ b/rsnix/src/vt.rs
@@ -147,6 +147,31 @@ pub fn size() -> (usize, usize) {
147 unsafe { (ROWS, COLS) } 147 unsafe { (ROWS, COLS) }
148} 148}
149 149
150/// Re-render the console's cells into the shadow for a pixel region, so the
151/// window manager can restore the console (layer 0) beneath a window that
152/// moved, closed, or was minimized. Does not push — the caller pushes once.
153pub fn repaint_region(x: usize, y: usize, w: usize, h: usize) {
154 unsafe {
155 if CELLS.is_null() {
156 return;
157 }
158 let (cols, rows) = (COLS, ROWS);
159 let c0 = x / 8;
160 let c1 = (x + w).div_ceil(8).min(cols);
161 let r0 = y / 16;
162 let r1 = (y + h).div_ceil(16).min(rows);
163 let cells = &*CELLS;
164 for r in r0..r1 {
165 for c in c0..c1 {
166 let cell = cells[r * cols + c];
167 let fg = PALETTE[cell.fg as usize];
168 let bg = PALETTE[cell.bg as usize];
169 crate::gop::shadow_glyph_px(c * 8, r * 16, cell.ch, fg, bg);
170 }
171 }
172 }
173}
174
150/// DEBUG (crash-family): read one cell's raw word (ch | fg<<16 | bg<<24) 175/// DEBUG (crash-family): read one cell's raw word (ch | fg<<16 | bg<<24)
151/// from the live cells grid and the rendered grid, for the given text 176/// from the live cells grid and the rendered grid, for the given text
152/// row/col. Returns (raw_cell, raw_rendered). 177/// row/col. Returns (raw_cell, raw_rendered).
diff --git a/rsnix/src/wm.rs b/rsnix/src/wm.rs
new file mode 100644
index 0000000..a39702d
--- /dev/null
+++ b/rsnix/src/wm.rs
@@ -0,0 +1,592 @@
1//! Window manager — a shared facility (no dedicated task).
2//!
3//! Implements ADR 0004: overlapping windows backed by per-window pixel
4//! buffers, composited onto the canvas with window-granularity damage
5//! rectangles. The console (`vt`) is layer 0 and is re-rendered (never
6//! re-allocated) when a region is exposed; GUI windows are composited
7//! above it. Chrome (title bar) is WM-drawn; each window's buffer is its
8//! content only.
9//!
10//! Apps call in directly (`create_window`, `flush`, …). Input routing and
11//! keybindings are handled here so `handle_key` is the single entry point
12//! for the GUI keyboard path.
13
14extern crate alloc;
15
16use core::sync::atomic::{AtomicUsize, Ordering};
17
18use crate::gop;
19use crate::memory;
20use crate::ps2::{AppMouseEvent, KeyEvent, MouseKind};
21use crate::sched::TaskMsg;
22
23/// Height of a window's title bar in pixels (one 8×16 glyph row).
24pub const TITLE_BAR_H: usize = 16;
25
26/// Sentinel: `FOCUSED == CONSOLE` means the console (vt) has focus.
27pub const CONSOLE: usize = usize::MAX;
28
29const MAX_WINDOWS: usize = 16;
30const MIN_W: usize = 64;
31const MIN_H: usize = 64;
32const CASCADE: usize = 24;
33const TITLE_MAX: usize = 32;
34
35// Palette (RGB; `gop::rgb` packs for the current pixel format).
36const TITLE_BG: (u8, u8, u8) = (0x3a, 0x3c, 0x42);
37const TITLE_BG_FOCUS: (u8, u8, u8) = (0x28, 0x4a, 0x8c);
38const TITLE_FG: (u8, u8, u8) = (0xff, 0xff, 0xff);
39const CLOSE_BG: (u8, u8, u8) = (0xb0, 0x30, 0x30);
40const MIN_BG: (u8, u8, u8) = (0x50, 0x50, 0x50);
41
42#[derive(Clone, Copy, PartialEq, Eq)]
43enum WinState {
44 Visible,
45 Minimized,
46}
47
48#[derive(Clone, Copy)]
49struct Window {
50 used: bool,
51 state: WinState,
52 title: [u8; TITLE_MAX],
53 title_len: usize,
54 #[allow(dead_code)] // read once input routing delivers to the owner's queue
55 owner: usize,
56 x: usize,
57 y: usize,
58 w: usize,
59 h: usize,
60 buf: *mut u32,
61}
62
63const EMPTY: Window = Window {
64 used: false,
65 state: WinState::Visible,
66 title: [0; TITLE_MAX],
67 title_len: 0,
68 owner: usize::MAX,
69 x: 0,
70 y: 0,
71 w: 0,
72 h: 0,
73 buf: core::ptr::null_mut(),
74};
75
76static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS];
77/// Window ids in bottom→top z-order.
78static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS];
79static mut ZLEN: usize = 0;
80static FOCUSED: AtomicUsize = AtomicUsize::new(CONSOLE);
81static mut CASCADE_X: usize = 0;
82static mut CASCADE_Y: usize = 0;
83
84/// Initialise window state. The console (vt) remains the visible layer 0;
85/// no GUI windows exist yet.
86pub fn init() {
87 unsafe {
88 ZLEN = 0;
89 CASCADE_X = 0;
90 CASCADE_Y = 0;
91 }
92 FOCUSED.store(CONSOLE, Ordering::Relaxed);
93}
94
95/// The focused window, or `None` if the console has focus.
96pub fn focused() -> Option<usize> {
97 let v = FOCUSED.load(Ordering::Relaxed);
98 if v == CONSOLE { None } else { Some(v) }
99}
100
101fn get(id: usize) -> Option<Window> {
102 if id >= MAX_WINDOWS {
103 return None;
104 }
105 let w = unsafe { WINDOWS[id] };
106 if w.used { Some(w) } else { None }
107}
108
109/// Create a window owned by the current task. `w`×`h` is the *content*
110/// size (title bar excluded). Returns the window id, or `None` on a full
111/// table or an allocation failure.
112pub fn create_window(title: &str, w: usize, h: usize) -> Option<usize> {
113 let (cw, ch) = gop::pixel_dims();
114 let w = w.max(MIN_W).min(cw.saturating_sub(8));
115 let h = h.max(MIN_H).min(ch.saturating_sub(TITLE_BAR_H + 8));
116 if w < MIN_W || h < MIN_H || cw == 0 || ch == 0 {
117 return None;
118 }
119
120 let pages = (w * h * 4).div_ceil(4096);
121 let buf = memory::alloc_pages(pages).map(|a| a as *mut u32)?;
122 // Zero-fill so a fresh canvas reads black, not garbage.
123 unsafe {
124 for i in 0..(w * h) {
125 *buf.add(i) = 0;
126 }
127 }
128
129 let id = unsafe {
130 let mut slot = None;
131 for i in 0..MAX_WINDOWS {
132 if !WINDOWS[i].used {
133 slot = Some(i);
134 break;
135 }
136 }
137 match slot {
138 Some(i) => i,
139 None => {
140 memory::free_pages(buf as usize, pages);
141 return None;
142 }
143 }
144 };
145
146 let (x, y) = unsafe {
147 let mut px = CASCADE_X;
148 let mut py = CASCADE_Y;
149 if px + w > cw {
150 px = 0;
151 }
152 if py + TITLE_BAR_H + h > ch {
153 py = 0;
154 }
155 CASCADE_X = px + CASCADE;
156 CASCADE_Y = py + CASCADE;
157 (px, py)
158 };
159
160 let mut tl = [0u8; TITLE_MAX];
161 let n = title.len().min(TITLE_MAX);
162 tl[..n].copy_from_slice(&title.as_bytes()[..n]);
163
164 unsafe {
165 WINDOWS[id] = Window {
166 used: true,
167 state: WinState::Visible,
168 title: tl,
169 title_len: n,
170 owner: crate::sched::current(),
171 x,
172 y,
173 w,
174 h,
175 buf,
176 };
177 ZORDER[ZLEN] = id;
178 ZLEN += 1;
179 }
180 focus(id);
181 Some(id)
182}
183
184/// Composite a window's full outer bounds (bottom→top), restoring the
185/// console beneath it. Called by an app after it writes into its buffer.
186pub fn flush(id: usize) {
187 let Some(win) = get(id) else {
188 return;
189 };
190 composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H);
191}
192
193/// Destroy a window: free its buffer, remove it, restore the exposed region.
194pub fn destroy(id: usize) {
195 let Some(win) = get(id) else {
196 return;
197 };
198 let (x, y, w, h) = (win.x, win.y, win.w, win.h + TITLE_BAR_H);
199 if !win.buf.is_null() {
200 let pages = (win.w * win.h * 4).div_ceil(4096);
201 memory::free_pages(win.buf as usize, pages);
202 }
203 remove_from_zorder(id);
204 unsafe {
205 WINDOWS[id] = EMPTY;
206 }
207 if focused() == Some(id) {
208 if unsafe { ZLEN } > 0 {
209 focus(unsafe { ZORDER[ZLEN - 1] });
210 } else {
211 focus_console();
212 }
213 }
214 composite_rect(x, y, w, h);
215}
216
217/// Minimize a window: it is no longer rendered (the app keeps running).
218pub fn minimize(id: usize) {
219 let Some(win) = get(id) else {
220 return;
221 };
222 if win.state == WinState::Minimized {
223 return;
224 }
225 let (x, y, w, h) = (win.x, win.y, win.w, win.h + TITLE_BAR_H);
226 unsafe {
227 WINDOWS[id].state = WinState::Minimized;
228 }
229 remove_from_zorder(id);
230 if focused() == Some(id) {
231 focus_console();
232 }
233 composite_rect(x, y, w, h);
234}
235
236/// Restore a minimized window (back to the top, focused).
237pub fn restore(id: usize) {
238 let Some(win) = get(id) else {
239 return;
240 };
241 if win.state == WinState::Visible {
242 return;
243 }
244 unsafe {
245 WINDOWS[id].state = WinState::Visible;
246 ZORDER[ZLEN] = id;
247 ZLEN += 1;
248 }
249 focus(id);
250 composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H);
251}
252
253/// Focus a window (raising it). Repaints the newly-focused window (so it
254/// appears above former occluders) and un-focuses the previous title bar.
255pub fn focus(id: usize) {
256 let Some(win) = get(id) else {
257 return;
258 };
259 if win.state != WinState::Visible {
260 return;
261 }
262 // Raise to the top of the z-order.
263 remove_from_zorder(id);
264 unsafe {
265 ZORDER[ZLEN] = id;
266 ZLEN += 1;
267 }
268 let prev = focused();
269 FOCUSED.store(id, Ordering::Relaxed);
270 if let Some(p) = prev
271 && p != id
272 && let Some(pw) = get(p)
273 {
274 let _ = crate::sched::send(pw.owner, TaskMsg::Focus(false));
275 composite_rect(pw.x, pw.y, pw.w, TITLE_BAR_H);
276 }
277 let _ = crate::sched::send(win.owner, TaskMsg::Focus(true));
278 composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H);
279}
280
281/// Move focus to the console.
282pub fn focus_console() {
283 let prev = focused();
284 FOCUSED.store(CONSOLE, Ordering::Relaxed);
285 if let Some(p) = prev
286 && let Some(pw) = get(p)
287 {
288 let _ = crate::sched::send(pw.owner, TaskMsg::Focus(false));
289 composite_rect(pw.x, pw.y, pw.w, TITLE_BAR_H);
290 }
291}
292
293/// Cycle focus: console → top window → … → bottom window → console.
294pub fn focus_next() {
295 unsafe {
296 if ZLEN == 0 {
297 focus_console();
298 return;
299 }
300 match focused() {
301 None => focus(ZORDER[ZLEN - 1]),
302 Some(cur) => {
303 let mut idx = usize::MAX;
304 for i in 0..ZLEN {
305 if ZORDER[i] == cur {
306 idx = i;
307 break;
308 }
309 }
310 if idx == usize::MAX || idx == 0 {
311 focus_console();
312 } else {
313 focus(ZORDER[idx - 1]);
314 }
315 }
316 }
317 }
318}
319
320/// Handle a global WM keybinding. Returns `true` if the key was consumed
321/// (F1–F4), `false` if it should be delivered to the focused app.
322pub fn handle_key(ev: KeyEvent) -> bool {
323 match ev {
324 KeyEvent::F(1) => {
325 spawn_terminal();
326 true
327 }
328 KeyEvent::F(2) => {
329 focus_next();
330 true
331 }
332 KeyEvent::F(3) => {
333 if let Some(id) = focused() {
334 minimize(id);
335 }
336 true
337 }
338 KeyEvent::F(4) => {
339 if let Some(id) = focused() {
340 request_close(id);
341 }
342 true
343 }
344 _ => false,
345 }
346}
347
348fn spawn_terminal() {
349 // Terminal app deferred (needs per-instance vt + per-instance shell).
350 crate::serial_println!("wm: F1 (spawn terminal) not implemented yet");
351}
352
353fn remove_from_zorder(id: usize) {
354 unsafe {
355 let mut j = 0;
356 for i in 0..ZLEN {
357 if ZORDER[i] != id {
358 ZORDER[j] = ZORDER[i];
359 j += 1;
360 }
361 }
362 ZLEN = j;
363 }
364}
365
366/// Repaint a canvas region bottom→top: restore the console (layer 0), then
367/// blit every visible window that intersects, then push once.
368fn composite_rect(x: usize, y: usize, w: usize, h: usize) {
369 crate::vt::repaint_region(x, y, w, h);
370 unsafe {
371 for i in 0..ZLEN {
372 draw_window_intersect(ZORDER[i], x, y, w, h);
373 }
374 }
375 gop::push_rect(x, y, w, h);
376}
377
378/// Blit the portion of a window that intersects `(rx, ry, rw, rh)`.
379fn draw_window_intersect(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) {
380 let win = unsafe { WINDOWS[id] };
381 if !win.used || win.state != WinState::Visible {
382 return;
383 }
384 let wx = win.x;
385 let wy = win.y;
386 let ww = win.w;
387 let whh = win.h + TITLE_BAR_H;
388 let ix = wx.max(rx);
389 let iy = wy.max(ry);
390 let ix2 = (wx + ww).min(rx + rw);
391 let iy2 = (wy + whh).min(ry + rh);
392 if ix >= ix2 || iy >= iy2 {
393 return;
394 }
395 // Title bar strip.
396 let tb0 = wy;
397 let tb1 = wy + TITLE_BAR_H;
398 if iy < tb1 && iy2 > tb0 {
399 draw_title_bar(id);
400 }
401 // Content strip.
402 let c0 = wy + TITLE_BAR_H;
403 let c1 = wy + TITLE_BAR_H + win.h;
404 if iy2 > c0 && iy < c1 {
405 let cy0 = iy.max(c0);
406 let cy1 = iy2.min(c1);
407 let sx = ix - wx;
408 let sy = cy0 - c0;
409 let cw = ix2 - ix;
410 let chh = cy1 - cy0;
411 let src = unsafe { win.buf.add(sy * win.w + sx) };
412 gop::shadow_blit(ix, cy0, cw, chh, src, win.w);
413 }
414}
415
416/// Draw a window's full title bar (background, title text, min/close).
417fn draw_title_bar(id: usize) {
418 let win = unsafe { WINDOWS[id] };
419 let bg = if focused() == Some(id) {
420 TITLE_BG_FOCUS
421 } else {
422 TITLE_BG
423 };
424 let bgc = gop::rgb(bg.0, bg.1, bg.2);
425 gop::shadow_fill_rect(win.x, win.y, win.w, TITLE_BAR_H, bgc);
426
427 // Title text, clipped short of the two buttons.
428 let btn_w = 16;
429 let cols = win.w.saturating_sub(btn_w + 4) / 8;
430 for (i, &b) in win.title[..win.title_len].iter().enumerate().take(cols) {
431 gop::shadow_glyph_px(win.x + 4 + i * 8, win.y, b as u16, TITLE_FG, bg);
432 }
433
434 // Close button (right edge, red) and minimize button (grey).
435 let close_x = win.x + win.w - 8;
436 let min_x = win.x + win.w - 16;
437 gop::shadow_fill_rect(
438 min_x,
439 win.y,
440 8,
441 TITLE_BAR_H,
442 gop::rgb(MIN_BG.0, MIN_BG.1, MIN_BG.2),
443 );
444 gop::shadow_fill_rect(
445 close_x,
446 win.y,
447 8,
448 TITLE_BAR_H,
449 gop::rgb(CLOSE_BG.0, CLOSE_BG.1, CLOSE_BG.2),
450 );
451}
452
453/// The task that owns a window (its input destination).
454pub fn owner(id: usize) -> Option<usize> {
455 get(id).map(|w| w.owner)
456}
457
458/// A window's content buffer (pointer, width, height) for direct drawing.
459pub fn buffer(id: usize) -> Option<(*mut u32, usize, usize)> {
460 get(id).map(|w| (w.buf, w.w, w.h))
461}
462
463/// List windows as `(id, title, minimized)` for the shell `windows` command.
464pub fn list() -> alloc::vec::Vec<(usize, alloc::string::String, bool)> {
465 let mut out = alloc::vec::Vec::new();
466 for i in 0..MAX_WINDOWS {
467 let w = unsafe { WINDOWS[i] };
468 if w.used {
469 let title = alloc::string::String::from(
470 core::str::from_utf8(&w.title[..w.title_len]).unwrap_or("?"),
471 );
472 out.push((i, title, w.state == WinState::Minimized));
473 }
474 }
475 out
476}
477
478/// Request a cooperative close: deliver `Close` to the owning app (which is
479/// expected to call `destroy` and exit).
480pub fn request_close(id: usize) {
481 if let Some(w) = get(id) {
482 let _ = crate::sched::send(w.owner, TaskMsg::Close);
483 }
484}
485
486#[derive(Clone, Copy, PartialEq, Eq)]
487enum Hit {
488 None,
489 Close,
490 Minimize,
491 Title,
492 Content,
493}
494
495/// Topmost visible window whose *outer* bounds contain the point.
496pub fn window_at(x: usize, y: usize) -> Option<usize> {
497 unsafe {
498 for i in (0..ZLEN).rev() {
499 let id = ZORDER[i];
500 let w = WINDOWS[id];
501 if !w.used || w.state != WinState::Visible {
502 continue;
503 }
504 if x >= w.x && x < w.x + w.w && y >= w.y && y < w.y + w.h + TITLE_BAR_H {
505 return Some(id);
506 }
507 }
508 }
509 None
510}
511
512fn hit_test(id: usize, x: usize, y: usize) -> Hit {
513 let Some(w) = get(id) else {
514 return Hit::None;
515 };
516 if y >= w.y + TITLE_BAR_H {
517 return Hit::Content;
518 }
519 let right = w.x + w.w;
520 if x >= right.saturating_sub(8) {
521 return Hit::Close;
522 }
523 if x >= right.saturating_sub(16) {
524 return Hit::Minimize;
525 }
526 Hit::Title
527}
528
529/// Route one mouse event (absolute canvas coords, button bitmask, kind).
530pub fn route_mouse(x: i32, y: i32, buttons: u8, kind: MouseKind) {
531 if kind == MouseKind::Press {
532 if x < 0 || y < 0 {
533 return;
534 }
535 let (cx, cy) = (x as usize, y as usize);
536 if let Some(id) = window_at(cx, cy) {
537 match hit_test(id, cx, cy) {
538 Hit::Close => request_close(id),
539 Hit::Minimize => minimize(id),
540 Hit::Title => focus(id),
541 Hit::Content => {
542 focus(id);
543 deliver_mouse(id, cx, cy, buttons, kind);
544 }
545 Hit::None => {}
546 }
547 } else {
548 focus_console();
549 }
550 } else if let Some(id) = focused() {
551 deliver_mouse(id, x as usize, y as usize, buttons, kind);
552 }
553}
554
555fn deliver_mouse(id: usize, cx: usize, cy: usize, buttons: u8, kind: MouseKind) {
556 let Some(w) = get(id) else {
557 return;
558 };
559 let rx = cx as i32 - w.x as i32;
560 let ry = cy as i32 - (w.y + TITLE_BAR_H) as i32;
561 if rx < 0 || ry < 0 || rx >= w.w as i32 || ry >= w.h as i32 {
562 return; // out of bounds — suppressed
563 }
564 let ev = AppMouseEvent {
565 x: rx,
566 y: ry,
567 buttons,
568 kind,
569 };
570 let _ = crate::sched::send(w.owner, TaskMsg::AppMouse(ev));
571}
572
573/// Spawn the keyboard pump task (routes keys to the focused GUI window).
574pub fn spawn_input_pump() -> Option<usize> {
575 crate::sched::spawn(pump_main, "wm-input")
576}
577
578fn pump_main() {
579 loop {
580 if focused().is_some() {
581 while let Some(ev) = crate::ps2::try_poll_key().or_else(crate::uhci::try_poll_key) {
582 if !handle_key(ev)
583 && let Some(id) = focused()
584 && let Some(o) = owner(id)
585 {
586 let _ = crate::sched::send(o, TaskMsg::Key(ev));
587 }
588 }
589 }
590 crate::sched::sleep_ms(5);
591 }
592}