diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-21 20:13:59 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-21 20:13:59 +0200 |
| commit | 2f4cf666c0c7bd224932fa48973e7334360c1de3 (patch) | |
| tree | 7526314b18728b05c4e9d285b9a41cf6fcd6d6d6 /rsnix/src/wm.rs | |
| parent | image: single dd-able system.img replaces vvfat+data-disk pair (diff) | |
| download | rs-dos-2f4cf666c0c7bd224932fa48973e7334360c1de3.tar.gz rs-dos-2f4cf666c0c7bd224932fa48973e7334360c1de3.zip | |
input: total keyboard routing + per-terminal discipline (ADR 0006 ph1)
wm: FOCUSED is now a plain window id — window 0 (home terminal) is an
ordinary focusable window in the z-order. The CONSOLE sentinel
(usize::MAX) and the focused()==0 special cases are gone; focus_console()
is an alias for focus(0). Desktop clicks focus window 0. The WM input
pump routes every key unconditionally: IRQ ring -> pump -> focused
window's owner; nothing polls the scancode rings directly any more.
tty: read_line() reads from the calling task's message queue (the old
global-ring read_line and the duplicate read_line_from_queue are merged);
raw mode (read_byte/try_read_byte) consumes the same queue;
interrupt_pressed drains the queue instead of the ring.
shell: task 0 owns home terminal window 0 and blocks on its queue like a
windowed shell; cat/hexdump stdin paths pass the home terminal handle.
sched: watchdog comments updated (shell parks on recv_blocking).
Verified in QEMU: F1 spawns terminals, F2 cycles focus home->term1->
term2, keystrokes land only in the focused window, ^C cancels a line,
edit runs raw over the queue and exits to a working prompt; test-disk
regression PASS.
Diffstat (limited to '')
| -rw-r--r-- | rsnix/src/wm.rs | 144 |
1 files changed, 80 insertions, 64 deletions
diff --git a/rsnix/src/wm.rs b/rsnix/src/wm.rs index 39ad1be..9f3806f 100644 --- a/rsnix/src/wm.rs +++ b/rsnix/src/wm.rs | |||
| @@ -16,8 +16,9 @@ use crate::sched::TaskMsg; | |||
| 16 | /// Height of a window's title bar in pixels (one 8×16 glyph row). | 16 | /// Height of a window's title bar in pixels (one 8×16 glyph row). |
| 17 | pub const TITLE_BAR_H: usize = 16; | 17 | pub const TITLE_BAR_H: usize = 16; |
| 18 | 18 | ||
| 19 | /// Sentinel: `FOCUSED == CONSOLE` means the console (vt) has focus. | 19 | /// Window 0 is the home terminal — an ordinary focusable window like any |
| 20 | pub const CONSOLE: usize = usize::MAX; | 20 | /// other (ADR 0006 Phase 1: no CONSOLE sentinel, keyboard routing is |
| 21 | /// total: every key goes to `focused()`'s owner). | ||
| 21 | 22 | ||
| 22 | const MAX_WINDOWS: usize = 16; | 23 | const MAX_WINDOWS: usize = 16; |
| 23 | const MIN_W: usize = 64; | 24 | const MIN_W: usize = 64; |
| @@ -87,7 +88,7 @@ const EMPTY: Window = Window { | |||
| 87 | static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS]; | 88 | static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS]; |
| 88 | static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS]; | 89 | static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS]; |
| 89 | static mut ZLEN: usize = 0; | 90 | static mut ZLEN: usize = 0; |
| 90 | static FOCUSED: AtomicUsize = AtomicUsize::new(CONSOLE); | 91 | static FOCUSED: AtomicUsize = AtomicUsize::new(0); |
| 91 | static mut CASCADE_X: usize = 0; | 92 | static mut CASCADE_X: usize = 0; |
| 92 | static mut CASCADE_Y: usize = 0; | 93 | static mut CASCADE_Y: usize = 0; |
| 93 | 94 | ||
| @@ -111,7 +112,6 @@ pub fn init() { | |||
| 111 | CASCADE_X = 0; | 112 | CASCADE_X = 0; |
| 112 | CASCADE_Y = 0; | 113 | CASCADE_Y = 0; |
| 113 | } | 114 | } |
| 114 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | ||
| 115 | } | 115 | } |
| 116 | 116 | ||
| 117 | /// Create window 0 (the home terminal): full-screen, chrome-less, | 117 | /// Create window 0 (the home terminal): full-screen, chrome-less, |
| @@ -124,7 +124,7 @@ pub fn create_home_window(buf: *mut u32, w: usize, h: usize) -> usize { | |||
| 124 | state: WinState::Visible, | 124 | state: WinState::Visible, |
| 125 | title: [0; TITLE_MAX], | 125 | title: [0; TITLE_MAX], |
| 126 | title_len: 0, | 126 | title_len: 0, |
| 127 | owner: usize::MAX, | 127 | owner: crate::sched::SHELL_TID, |
| 128 | x: 0, | 128 | x: 0, |
| 129 | y: 0, | 129 | y: 0, |
| 130 | w, | 130 | w, |
| @@ -135,12 +135,17 @@ pub fn create_home_window(buf: *mut u32, w: usize, h: usize) -> usize { | |||
| 135 | ZORDER[0] = id; | 135 | ZORDER[0] = id; |
| 136 | ZLEN = 1; | 136 | ZLEN = 1; |
| 137 | } | 137 | } |
| 138 | FOCUSED.store(0, Ordering::Relaxed); | ||
| 138 | id | 139 | id |
| 139 | } | 140 | } |
| 140 | 141 | ||
| 141 | pub fn focused() -> Option<usize> { | 142 | pub fn focused() -> Option<usize> { |
| 142 | let v = FOCUSED.load(Ordering::Relaxed); | 143 | let v = FOCUSED.load(Ordering::Relaxed); |
| 143 | if v == CONSOLE || v == 0 { None } else { Some(v) } | 144 | if v < MAX_WINDOWS && get(v).is_some() { |
| 145 | Some(v) | ||
| 146 | } else { | ||
| 147 | None | ||
| 148 | } | ||
| 144 | } | 149 | } |
| 145 | 150 | ||
| 146 | fn get(id: usize) -> Option<Window> { | 151 | fn get(id: usize) -> Option<Window> { |
| @@ -231,8 +236,10 @@ pub fn flush(id: usize) { | |||
| 231 | let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; | 236 | let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; |
| 232 | let pad = if id != 0 { 1 } else { 0 }; | 237 | let pad = if id != 0 { 1 } else { 0 }; |
| 233 | composite_rect( | 238 | composite_rect( |
| 234 | win.x.saturating_sub(pad), win.y.saturating_sub(pad), | 239 | win.x.saturating_sub(pad), |
| 235 | win.w + 2 * pad, outer_h + 2 * pad, | 240 | win.y.saturating_sub(pad), |
| 241 | win.w + 2 * pad, | ||
| 242 | outer_h + 2 * pad, | ||
| 236 | ); | 243 | ); |
| 237 | } | 244 | } |
| 238 | 245 | ||
| @@ -256,8 +263,8 @@ pub fn destroy(id: usize) { | |||
| 256 | if focused() == Some(id) { | 263 | if focused() == Some(id) { |
| 257 | if unsafe { ZLEN } > 0 { | 264 | if unsafe { ZLEN } > 0 { |
| 258 | focus(unsafe { ZORDER[ZLEN - 1] }); | 265 | focus(unsafe { ZORDER[ZLEN - 1] }); |
| 259 | } else { | 266 | } else if id != 0 { |
| 260 | focus_console(); | 267 | focus(0); |
| 261 | } | 268 | } |
| 262 | } | 269 | } |
| 263 | composite_rect(x, y, w, h); | 270 | composite_rect(x, y, w, h); |
| @@ -277,7 +284,7 @@ pub fn minimize(id: usize) { | |||
| 277 | } | 284 | } |
| 278 | remove_from_zorder(id); | 285 | remove_from_zorder(id); |
| 279 | if focused() == Some(id) { | 286 | if focused() == Some(id) { |
| 280 | focus_console(); | 287 | focus(0); |
| 281 | } | 288 | } |
| 282 | composite_rect(x, y, w, h); | 289 | composite_rect(x, y, w, h); |
| 283 | } | 290 | } |
| @@ -307,20 +314,12 @@ pub fn focus(id: usize) { | |||
| 307 | return; | 314 | return; |
| 308 | } | 315 | } |
| 309 | let prev = focused(); | 316 | let prev = focused(); |
| 310 | // Window 0 (home terminal) is the console — store the CONSOLE | 317 | FOCUSED.store(id, Ordering::Relaxed); |
| 311 | // sentinel so `focused()` returns None and the tty layer reads | 318 | // Every window lives in the z-order now, window 0 included. |
| 312 | // keyboard input directly (same as focus_console). | 319 | remove_from_zorder(id); |
| 313 | if id == 0 { | 320 | unsafe { |
| 314 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | 321 | ZORDER[ZLEN] = id; |
| 315 | } else { | 322 | ZLEN += 1; |
| 316 | FOCUSED.store(id, Ordering::Relaxed); | ||
| 317 | } | ||
| 318 | if id != 0 { | ||
| 319 | remove_from_zorder(id); | ||
| 320 | unsafe { | ||
| 321 | ZORDER[ZLEN] = id; | ||
| 322 | ZLEN += 1; | ||
| 323 | } | ||
| 324 | } | 323 | } |
| 325 | if let Some(p) = prev | 324 | if let Some(p) = prev |
| 326 | && p != id | 325 | && p != id |
| @@ -331,8 +330,10 @@ pub fn focus(id: usize) { | |||
| 331 | let oh = pw.h + if pw.chrome { TITLE_BAR_H } else { 0 }; | 330 | let oh = pw.h + if pw.chrome { TITLE_BAR_H } else { 0 }; |
| 332 | let pad = if p != 0 { 1 } else { 0 }; | 331 | let pad = if p != 0 { 1 } else { 0 }; |
| 333 | composite_rect( | 332 | composite_rect( |
| 334 | pw.x.saturating_sub(pad), pw.y.saturating_sub(pad), | 333 | pw.x.saturating_sub(pad), |
| 335 | pw.w + 2 * pad, oh + 2 * pad, | 334 | pw.y.saturating_sub(pad), |
| 335 | pw.w + 2 * pad, | ||
| 336 | oh + 2 * pad, | ||
| 336 | ); | 337 | ); |
| 337 | } | 338 | } |
| 338 | let _ = crate::sched::send(win.owner, TaskMsg::Focus(true)); | 339 | let _ = crate::sched::send(win.owner, TaskMsg::Focus(true)); |
| @@ -340,35 +341,26 @@ pub fn focus(id: usize) { | |||
| 340 | let oh = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; | 341 | let oh = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; |
| 341 | let pad = if id != 0 { 1 } else { 0 }; | 342 | let pad = if id != 0 { 1 } else { 0 }; |
| 342 | composite_rect( | 343 | composite_rect( |
| 343 | win.x.saturating_sub(pad), win.y.saturating_sub(pad), | 344 | win.x.saturating_sub(pad), |
| 344 | win.w + 2 * pad, oh + 2 * pad, | 345 | win.y.saturating_sub(pad), |
| 346 | win.w + 2 * pad, | ||
| 347 | oh + 2 * pad, | ||
| 345 | ); | 348 | ); |
| 346 | } | 349 | } |
| 347 | 350 | ||
| 351 | /// Legacy name — the home terminal is now just window 0. | ||
| 348 | pub fn focus_console() { | 352 | pub fn focus_console() { |
| 349 | let prev = focused(); | 353 | focus(0); |
| 350 | FOCUSED.store(CONSOLE, Ordering::Relaxed); | ||
| 351 | if let Some(p) = prev | ||
| 352 | && let Some(pw) = get(p) | ||
| 353 | { | ||
| 354 | let _ = crate::sched::send(pw.owner, TaskMsg::Focus(false)); | ||
| 355 | let oh = pw.h + if pw.chrome { TITLE_BAR_H } else { 0 }; | ||
| 356 | let pad = if p != 0 { 1 } else { 0 }; | ||
| 357 | composite_rect( | ||
| 358 | pw.x.saturating_sub(pad), pw.y.saturating_sub(pad), | ||
| 359 | pw.w + 2 * pad, oh + 2 * pad, | ||
| 360 | ); | ||
| 361 | } | ||
| 362 | } | 354 | } |
| 363 | 355 | ||
| 364 | pub fn focus_next() { | 356 | pub fn focus_next() { |
| 365 | unsafe { | 357 | unsafe { |
| 366 | if ZLEN == 0 { | ||
| 367 | focus_console(); | ||
| 368 | return; | ||
| 369 | } | ||
| 370 | match focused() { | 358 | match focused() { |
| 371 | None => focus(ZORDER[ZLEN - 1]), | 359 | None => { |
| 360 | if ZLEN > 0 { | ||
| 361 | focus(ZORDER[ZLEN - 1]); | ||
| 362 | } | ||
| 363 | } | ||
| 372 | Some(cur) => { | 364 | Some(cur) => { |
| 373 | let mut idx = usize::MAX; | 365 | let mut idx = usize::MAX; |
| 374 | for i in 0..ZLEN { | 366 | for i in 0..ZLEN { |
| @@ -378,7 +370,9 @@ pub fn focus_next() { | |||
| 378 | } | 370 | } |
| 379 | } | 371 | } |
| 380 | if idx == usize::MAX || idx == 0 { | 372 | if idx == usize::MAX || idx == 0 { |
| 381 | focus_console(); | 373 | if ZLEN > 0 { |
| 374 | focus(ZORDER[ZLEN - 1]); | ||
| 375 | } | ||
| 382 | } else { | 376 | } else { |
| 383 | focus(ZORDER[idx - 1]); | 377 | focus(ZORDER[idx - 1]); |
| 384 | } | 378 | } |
| @@ -530,6 +524,7 @@ fn mouse_press(x: i32, y: i32, buttons: u8) { | |||
| 530 | Hit::None => {} | 524 | Hit::None => {} |
| 531 | } | 525 | } |
| 532 | } else { | 526 | } else { |
| 527 | // Click on the bare desktop: focus the home terminal (window 0). | ||
| 533 | focus_console(); | 528 | focus_console(); |
| 534 | } | 529 | } |
| 535 | } | 530 | } |
| @@ -606,8 +601,18 @@ fn move_window(id: usize, nx: usize, ny: usize) { | |||
| 606 | } | 601 | } |
| 607 | let pad: usize = 1; | 602 | let pad: usize = 1; |
| 608 | let oh = w.h + TITLE_BAR_H; | 603 | let oh = w.h + TITLE_BAR_H; |
| 609 | composite_rect(ox.saturating_sub(pad), oy.saturating_sub(pad), w.w + 2 * pad, oh + 2 * pad); | 604 | composite_rect( |
| 610 | composite_rect(nx.saturating_sub(pad), ny.saturating_sub(pad), w.w + 2 * pad, oh + 2 * pad); | 605 | ox.saturating_sub(pad), |
| 606 | oy.saturating_sub(pad), | ||
| 607 | w.w + 2 * pad, | ||
| 608 | oh + 2 * pad, | ||
| 609 | ); | ||
| 610 | composite_rect( | ||
| 611 | nx.saturating_sub(pad), | ||
| 612 | ny.saturating_sub(pad), | ||
| 613 | w.w + 2 * pad, | ||
| 614 | oh + 2 * pad, | ||
| 615 | ); | ||
| 611 | } | 616 | } |
| 612 | 617 | ||
| 613 | pub fn owner(id: usize) -> Option<usize> { | 618 | pub fn owner(id: usize) -> Option<usize> { |
| @@ -637,13 +642,14 @@ pub fn spawn_input_pump() -> Option<usize> { | |||
| 637 | } | 642 | } |
| 638 | 643 | ||
| 639 | fn pump_main() { | 644 | fn pump_main() { |
| 645 | // Total routing (ADR 0006 Phase 1): every key from every source is | ||
| 646 | // pumped to the focused window's owner. No task ever polls the | ||
| 647 | // global rings directly; when nothing is focused the keys drop. | ||
| 640 | loop { | 648 | loop { |
| 641 | if let Some(id) = focused() { | 649 | let o = focused().and_then(owner).unwrap_or(crate::sched::SHELL_TID); |
| 642 | let o = owner(id).unwrap_or(0); | 650 | while let Some(ev) = crate::ps2::try_poll_key().or_else(crate::uhci::try_poll_key) { |
| 643 | while let Some(ev) = crate::ps2::try_poll_key().or_else(crate::uhci::try_poll_key) { | 651 | if !handle_key(ev) { |
| 644 | if !handle_key(ev) { | 652 | let _ = crate::sched::send(o, TaskMsg::Key(ev)); |
| 645 | let _ = crate::sched::send(o, TaskMsg::Key(ev)); | ||
| 646 | } | ||
| 647 | } | 653 | } |
| 648 | } | 654 | } |
| 649 | crate::sched::sleep_ms(5); | 655 | crate::sched::sleep_ms(5); |
| @@ -669,11 +675,19 @@ fn composite_rect(x: usize, y: usize, w: usize, h: usize) { | |||
| 669 | /// 1px border drawn around every window except the root (id 0). | 675 | /// 1px border drawn around every window except the root (id 0). |
| 670 | /// Blue when focused, grey when not. | 676 | /// Blue when focused, grey when not. |
| 671 | fn draw_window_border(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { | 677 | fn draw_window_border(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { |
| 672 | if id == 0 { return; } | 678 | if id == 0 { |
| 679 | return; | ||
| 680 | } | ||
| 673 | let win = unsafe { WINDOWS[id] }; | 681 | let win = unsafe { WINDOWS[id] }; |
| 674 | if !win.used || win.state != WinState::Visible { return; } | 682 | if !win.used || win.state != WinState::Visible { |
| 683 | return; | ||
| 684 | } | ||
| 675 | let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; | 685 | let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 }; |
| 676 | let bg = if focused() == Some(id) { TITLE_BG_FOCUS } else { TITLE_BG }; | 686 | let bg = if focused() == Some(id) { |
| 687 | TITLE_BG_FOCUS | ||
| 688 | } else { | ||
| 689 | TITLE_BG | ||
| 690 | }; | ||
| 677 | let bc = gop::rgb(bg.0, bg.1, bg.2); | 691 | let bc = gop::rgb(bg.0, bg.1, bg.2); |
| 678 | // Border sits 1px outside the window's outer rect. | 692 | // Border sits 1px outside the window's outer rect. |
| 679 | let bx = win.x.saturating_sub(1); | 693 | let bx = win.x.saturating_sub(1); |
| @@ -684,12 +698,14 @@ fn draw_window_border(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { | |||
| 684 | let iy = by.max(ry); | 698 | let iy = by.max(ry); |
| 685 | let ix2 = (bx + bw).min(rx + rw); | 699 | let ix2 = (bx + bw).min(rx + rw); |
| 686 | let iy2 = (by + bh).min(ry + rh); | 700 | let iy2 = (by + bh).min(ry + rh); |
| 687 | if ix >= ix2 || iy >= iy2 { return; } | 701 | if ix >= ix2 || iy >= iy2 { |
| 702 | return; | ||
| 703 | } | ||
| 688 | // Four edges. | 704 | // Four edges. |
| 689 | gop::shadow_fill_rect(ix, iy, ix2 - ix, 1, bc); // top | 705 | gop::shadow_fill_rect(ix, iy, ix2 - ix, 1, bc); // top |
| 690 | gop::shadow_fill_rect(ix, iy2 - 1, ix2 - ix, 1, bc); // bottom | 706 | gop::shadow_fill_rect(ix, iy2 - 1, ix2 - ix, 1, bc); // bottom |
| 691 | gop::shadow_fill_rect(ix, iy + 1, 1, iy2 - iy - 2, bc); // left | 707 | gop::shadow_fill_rect(ix, iy + 1, 1, iy2 - iy - 2, bc); // left |
| 692 | gop::shadow_fill_rect(ix2 - 1, iy + 1, 1, iy2 - iy - 2, bc); // right | 708 | gop::shadow_fill_rect(ix2 - 1, iy + 1, 1, iy2 - iy - 2, bc); // right |
| 693 | } | 709 | } |
| 694 | 710 | ||
| 695 | /// Blit the portion of a window that intersects `(rx, ry, rw, rh)`. | 711 | /// Blit the portion of a window that intersects `(rx, ry, rw, rh)`. |
