summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-19 18:46:35 +0200
committersalmonik <salmonik@tuta.io>2026-08-19 18:46:35 +0200
commit09a44f15678f27dd3d07f90e9ed0e71cf0e771ff (patch)
treee9942dd03df3eb16b5b261cb6b0e129390593ef6
parentvt → per-instance Terminal rendering into a pixel buffer (diff)
downloadrs-dos-09a44f15678f27dd3d07f90e9ed0e71cf0e771ff.tar.gz
rs-dos-09a44f15678f27dd3d07f90e9ed0e71cf0e771ff.zip
wm: integrate home terminal as window 0 + fix console focus after close
- wm rewrite: chrome flag per window, create_home_window for the console (window 0, chrome-less, bottom-pinned, never closed), composite_rect fills desktop background then blits windows bottom-to-top, draw_window_intersect handles chrome-less windows, hit_test returns Content for chrome-less, destroy/move_window no-op for id 0. - vt: add window_params() so main.rs can wire the Terminal to wm window 0. - main.rs: create home terminal window 0 and attach Terminal after wm::init(). - Focus fix: focus(0) now stores CONSOLE sentinel (not raw 0) so focused() returns None when the home terminal has focus. Without this, closing a GUI window left FOCUSED=0, which tty::poll_key() interpreted as 'a GUI app has focus', spinning forever and swallowing all keyboard input.
-rw-r--r--rsdos/src/main.rs6
-rw-r--r--rsnix/src/vt.rs15
-rw-r--r--rsnix/src/wm.rs442
3 files changed, 261 insertions, 202 deletions
diff --git a/rsdos/src/main.rs b/rsdos/src/main.rs
index b147e63..6d2fa4a 100644
--- a/rsdos/src/main.rs
+++ b/rsdos/src/main.rs
@@ -413,6 +413,12 @@ pub unsafe extern "efiapi" fn efi_main(image_handle: Handle, system_table: *mut
413 // Windowed GUI: initialise the window manager and its input pump. 413 // Windowed GUI: initialise the window manager and its input pump.
414 // The console (vt) stays layer 0; GUI apps are windows composited on top. 414 // The console (vt) stays layer 0; GUI apps are windows composited on top.
415 rsnix::wm::init(); 415 rsnix::wm::init();
416 {
417 let t = rsnix::vt::home_terminal();
418 let (buf, w, h) = t.window_params();
419 let id = rsnix::wm::create_home_window(buf as *mut u32, w, h);
420 t.set_window(id);
421 }
416 rsnix::wm::spawn_input_pump(); 422 rsnix::wm::spawn_input_pump();
417 423
418 shell::run(); 424 shell::run();
diff --git a/rsnix/src/vt.rs b/rsnix/src/vt.rs
index 90264f6..12e2cdc 100644
--- a/rsnix/src/vt.rs
+++ b/rsnix/src/vt.rs
@@ -80,6 +80,7 @@ pub struct Terminal {
80 cur: usize, 80 cur: usize,
81 pending_scrolls: usize, 81 pending_scrolls: usize,
82 last_sync_tick: u64, 82 last_sync_tick: u64,
83 dirty_all: bool,
83 buffer: Vec<u32>, 84 buffer: Vec<u32>,
84 w: usize, 85 w: usize,
85 h: usize, 86 h: usize,
@@ -128,6 +129,7 @@ impl Terminal {
128 cur: 0, 129 cur: 0,
129 pending_scrolls: 0, 130 pending_scrolls: 0,
130 last_sync_tick: 0, 131 last_sync_tick: 0,
132 dirty_all: true,
131 buffer: vec![0; w * h], 133 buffer: vec![0; w * h],
132 w, 134 w,
133 h, 135 h,
@@ -140,6 +142,11 @@ impl Terminal {
140 self.window_id = Some(id); 142 self.window_id = Some(id);
141 } 143 }
142 144
145 /// Buffer pointer and pixel dimensions (for the WM to composite).
146 pub fn window_params(&self) -> (*const u32, usize, usize) {
147 (self.buffer.as_ptr(), self.w, self.h)
148 }
149
143 pub fn ready(&self) -> bool { 150 pub fn ready(&self) -> bool {
144 self.rows != 0 151 self.rows != 0
145 } 152 }
@@ -193,6 +200,7 @@ impl Terminal {
193 } 200 }
194 self.pending_scrolls = 0; 201 self.pending_scrolls = 0;
195 self.last_sync_tick = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed); 202 self.last_sync_tick = crate::interrupts::TICKS.load(core::sync::atomic::Ordering::Relaxed);
203 self.dirty_all = true;
196 self.flush(); 204 self.flush();
197 } 205 }
198 206
@@ -509,7 +517,12 @@ impl Terminal {
509 let n = rows * cols; 517 let n = rows * cols;
510 let cursor_visible = self.cursor_visible; 518 let cursor_visible = self.cursor_visible;
511 let (cr, cc) = (self.cursor_r, self.cursor_c); 519 let (cr, cc) = (self.cursor_r, self.cursor_c);
512 let mut dirty: Option<(usize, usize, usize, usize)> = None; 520 let mut dirty = if self.dirty_all {
521 self.dirty_all = false;
522 Some((0, 0, self.w, self.h))
523 } else {
524 None
525 };
513 526
514 // Diff cells vs rendered, re-blitting changed cells into the buffer. 527 // Diff cells vs rendered, re-blitting changed cells into the buffer.
515 for i in 0..n { 528 for i in 0..n {
diff --git a/rsnix/src/wm.rs b/rsnix/src/wm.rs
index be74f9a..2641b69 100644
--- a/rsnix/src/wm.rs
+++ b/rsnix/src/wm.rs
@@ -2,16 +2,9 @@
2//! 2//!
3//! Implements ADR 0004: overlapping windows backed by per-window pixel 3//! Implements ADR 0004: overlapping windows backed by per-window pixel
4//! buffers, composited onto the canvas with window-granularity damage 4//! buffers, composited onto the canvas with window-granularity damage
5//! rectangles. The console (`vt`) is layer 0 and is re-rendered (never 5//! rectangles. The home terminal is window 0; GUI windows are composited
6//! re-allocated) when a region is exposed; GUI windows are composited 6//! above it. Chrome (title bars) is WM-drawn; each window's buffer is its
7//! above it. Chrome (title bar) is WM-drawn; each window's buffer is its
8//! content only. 7//! 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 8
16use core::sync::atomic::{AtomicUsize, Ordering}; 9use core::sync::atomic::{AtomicUsize, Ordering};
17 10
@@ -38,6 +31,7 @@ const TITLE_BG_FOCUS: (u8, u8, u8) = (0x28, 0x4a, 0x8c);
38const TITLE_FG: (u8, u8, u8) = (0xff, 0xff, 0xff); 31const TITLE_FG: (u8, u8, u8) = (0xff, 0xff, 0xff);
39const CLOSE_BG: (u8, u8, u8) = (0xb0, 0x30, 0x30); 32const CLOSE_BG: (u8, u8, u8) = (0xb0, 0x30, 0x30);
40const MIN_BG: (u8, u8, u8) = (0x50, 0x50, 0x50); 33const MIN_BG: (u8, u8, u8) = (0x50, 0x50, 0x50);
34const DESKTOP: (u8, u8, u8) = (0x18, 0x1a, 0x1e);
41 35
42#[derive(Clone, Copy, PartialEq, Eq)] 36#[derive(Clone, Copy, PartialEq, Eq)]
43enum WinState { 37enum WinState {
@@ -45,19 +39,35 @@ enum WinState {
45 Minimized, 39 Minimized,
46} 40}
47 41
42#[derive(Clone, Copy, PartialEq, Eq)]
43enum Hit {
44 None,
45 Close,
46 Minimize,
47 Title,
48 Content,
49}
50
51#[derive(Clone, Copy, PartialEq, Eq)]
52enum Drag {
53 None,
54 Move,
55 App,
56}
57
48#[derive(Clone, Copy)] 58#[derive(Clone, Copy)]
49struct Window { 59struct Window {
50 used: bool, 60 used: bool,
51 state: WinState, 61 state: WinState,
52 title: [u8; TITLE_MAX], 62 title: [u8; TITLE_MAX],
53 title_len: usize, 63 title_len: usize,
54 #[allow(dead_code)] // read once input routing delivers to the owner's queue
55 owner: usize, 64 owner: usize,
56 x: usize, 65 x: usize,
57 y: usize, 66 y: usize,
58 w: usize, 67 w: usize,
59 h: usize, 68 h: usize,
60 buf: *mut u32, 69 buf: *mut u32,
70 chrome: bool,
61} 71}
62 72
63const EMPTY: Window = Window { 73const EMPTY: Window = Window {
@@ -71,18 +81,30 @@ const EMPTY: Window = Window {
71 w: 0, 81 w: 0,
72 h: 0, 82 h: 0,
73 buf: core::ptr::null_mut(), 83 buf: core::ptr::null_mut(),
84 chrome: true,
74}; 85};
75 86
76static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS]; 87static 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]; 88static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS];
79static mut ZLEN: usize = 0; 89static mut ZLEN: usize = 0;
80static FOCUSED: AtomicUsize = AtomicUsize::new(CONSOLE); 90static FOCUSED: AtomicUsize = AtomicUsize::new(CONSOLE);
81static mut CASCADE_X: usize = 0; 91static mut CASCADE_X: usize = 0;
82static mut CASCADE_Y: usize = 0; 92static mut CASCADE_Y: usize = 0;
83 93
84/// Initialise window state. The console (vt) remains the visible layer 0; 94// ---------------------------------------------------------------------------
85/// no GUI windows exist yet. 95// Mouse gesture state
96// ---------------------------------------------------------------------------
97
98static mut DRAG: Drag = Drag::None;
99static mut DRAG_ID: usize = usize::MAX;
100static mut DRAG_DX: i32 = 0;
101static mut DRAG_DY: i32 = 0;
102
103// ---------------------------------------------------------------------------
104// Public API
105// ---------------------------------------------------------------------------
106
107/// Initialise window state.
86pub fn init() { 108pub fn init() {
87 unsafe { 109 unsafe {
88 ZLEN = 0; 110 ZLEN = 0;
@@ -92,10 +114,33 @@ pub fn init() {
92 FOCUSED.store(CONSOLE, Ordering::Relaxed); 114 FOCUSED.store(CONSOLE, Ordering::Relaxed);
93} 115}
94 116
95/// The focused window, or `None` if the console has focus. 117/// Create window 0 (the home terminal): full-screen, chrome-less,
118/// bottom-pinned, never closed.
119pub fn create_home_window(buf: *mut u32, w: usize, h: usize) -> usize {
120 let id = 0;
121 unsafe {
122 WINDOWS[id] = Window {
123 used: true,
124 state: WinState::Visible,
125 title: [0; TITLE_MAX],
126 title_len: 0,
127 owner: usize::MAX,
128 x: 0,
129 y: 0,
130 w,
131 h,
132 buf,
133 chrome: false,
134 };
135 ZORDER[0] = id;
136 ZLEN = 1;
137 }
138 id
139}
140
96pub fn focused() -> Option<usize> { 141pub fn focused() -> Option<usize> {
97 let v = FOCUSED.load(Ordering::Relaxed); 142 let v = FOCUSED.load(Ordering::Relaxed);
98 if v == CONSOLE { None } else { Some(v) } 143 if v == CONSOLE || v == 0 { None } else { Some(v) }
99} 144}
100 145
101fn get(id: usize) -> Option<Window> { 146fn get(id: usize) -> Option<Window> {
@@ -106,9 +151,7 @@ fn get(id: usize) -> Option<Window> {
106 if w.used { Some(w) } else { None } 151 if w.used { Some(w) } else { None }
107} 152}
108 153
109/// Create a window owned by the current task. `w`×`h` is the *content* 154/// Create a normal (chrome) window.
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> { 155pub fn create_window(title: &str, w: usize, h: usize) -> Option<usize> {
113 let (cw, ch) = gop::pixel_dims(); 156 let (cw, ch) = gop::pixel_dims();
114 let w = w.max(MIN_W).min(cw.saturating_sub(8)); 157 let w = w.max(MIN_W).min(cw.saturating_sub(8));
@@ -119,7 +162,6 @@ pub fn create_window(title: &str, w: usize, h: usize) -> Option<usize> {
119 162
120 let pages = (w * h * 4).div_ceil(4096); 163 let pages = (w * h * 4).div_ceil(4096);
121 let buf = memory::alloc_pages(pages).map(|a| a as *mut u32)?; 164 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 { 165 unsafe {
124 for i in 0..(w * h) { 166 for i in 0..(w * h) {
125 *buf.add(i) = 0; 167 *buf.add(i) = 0;
@@ -173,6 +215,7 @@ pub fn create_window(title: &str, w: usize, h: usize) -> Option<usize> {
173 w, 215 w,
174 h, 216 h,
175 buf, 217 buf,
218 chrome: true,
176 }; 219 };
177 ZORDER[ZLEN] = id; 220 ZORDER[ZLEN] = id;
178 ZLEN += 1; 221 ZLEN += 1;
@@ -181,21 +224,23 @@ pub fn create_window(title: &str, w: usize, h: usize) -> Option<usize> {
181 Some(id) 224 Some(id)
182} 225}
183 226
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) { 227pub fn flush(id: usize) {
187 let Some(win) = get(id) else { 228 let Some(win) = get(id) else {
188 return; 229 return;
189 }; 230 };
190 composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H); 231 let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 };
232 composite_rect(win.x, win.y, win.w, outer_h);
191} 233}
192 234
193/// Destroy a window: free its buffer, remove it, restore the exposed region.
194pub fn destroy(id: usize) { 235pub fn destroy(id: usize) {
236 if id == 0 {
237 return;
238 }
195 let Some(win) = get(id) else { 239 let Some(win) = get(id) else {
196 return; 240 return;
197 }; 241 };
198 let (x, y, w, h) = (win.x, win.y, win.w, win.h + TITLE_BAR_H); 242 let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 };
243 let (x, y, w, h) = (win.x, win.y, win.w, outer_h);
199 if !win.buf.is_null() { 244 if !win.buf.is_null() {
200 let pages = (win.w * win.h * 4).div_ceil(4096); 245 let pages = (win.w * win.h * 4).div_ceil(4096);
201 memory::free_pages(win.buf as usize, pages); 246 memory::free_pages(win.buf as usize, pages);
@@ -214,15 +259,15 @@ pub fn destroy(id: usize) {
214 composite_rect(x, y, w, h); 259 composite_rect(x, y, w, h);
215} 260}
216 261
217/// Minimize a window: it is no longer rendered (the app keeps running).
218pub fn minimize(id: usize) { 262pub fn minimize(id: usize) {
219 let Some(win) = get(id) else { 263 let Some(win) = get(id) else {
220 return; 264 return;
221 }; 265 };
222 if win.state == WinState::Minimized { 266 if win.state == WinState::Minimized || id == 0 {
223 return; 267 return;
224 } 268 }
225 let (x, y, w, h) = (win.x, win.y, win.w, win.h + TITLE_BAR_H); 269 let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 };
270 let (x, y, w, h) = (win.x, win.y, win.w, outer_h);
226 unsafe { 271 unsafe {
227 WINDOWS[id].state = WinState::Minimized; 272 WINDOWS[id].state = WinState::Minimized;
228 } 273 }
@@ -233,7 +278,6 @@ pub fn minimize(id: usize) {
233 composite_rect(x, y, w, h); 278 composite_rect(x, y, w, h);
234} 279}
235 280
236/// Restore a minimized window (back to the top, focused).
237pub fn restore(id: usize) { 281pub fn restore(id: usize) {
238 let Some(win) = get(id) else { 282 let Some(win) = get(id) else {
239 return; 283 return;
@@ -247,11 +291,10 @@ pub fn restore(id: usize) {
247 ZLEN += 1; 291 ZLEN += 1;
248 } 292 }
249 focus(id); 293 focus(id);
250 composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H); 294 let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 };
295 composite_rect(win.x, win.y, win.w, outer_h);
251} 296}
252 297
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) { 298pub fn focus(id: usize) {
256 let Some(win) = get(id) else { 299 let Some(win) = get(id) else {
257 return; 300 return;
@@ -259,14 +302,22 @@ pub fn focus(id: usize) {
259 if win.state != WinState::Visible { 302 if win.state != WinState::Visible {
260 return; 303 return;
261 } 304 }
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(); 305 let prev = focused();
269 FOCUSED.store(id, Ordering::Relaxed); 306 // Window 0 (home terminal) is the console — store the CONSOLE
307 // sentinel so `focused()` returns None and the tty layer reads
308 // keyboard input directly (same as focus_console).
309 if id == 0 {
310 FOCUSED.store(CONSOLE, Ordering::Relaxed);
311 } else {
312 FOCUSED.store(id, Ordering::Relaxed);
313 }
314 if id != 0 {
315 remove_from_zorder(id);
316 unsafe {
317 ZORDER[ZLEN] = id;
318 ZLEN += 1;
319 }
320 }
270 if let Some(p) = prev 321 if let Some(p) = prev
271 && p != id 322 && p != id
272 && let Some(pw) = get(p) 323 && let Some(pw) = get(p)
@@ -275,10 +326,11 @@ pub fn focus(id: usize) {
275 composite_rect(pw.x, pw.y, pw.w, TITLE_BAR_H); 326 composite_rect(pw.x, pw.y, pw.w, TITLE_BAR_H);
276 } 327 }
277 let _ = crate::sched::send(win.owner, TaskMsg::Focus(true)); 328 let _ = crate::sched::send(win.owner, TaskMsg::Focus(true));
278 composite_rect(win.x, win.y, win.w, win.h + TITLE_BAR_H); 329 if win.chrome {
330 composite_rect(win.x, win.y, win.w, TITLE_BAR_H);
331 }
279} 332}
280 333
281/// Move focus to the console.
282pub fn focus_console() { 334pub fn focus_console() {
283 let prev = focused(); 335 let prev = focused();
284 FOCUSED.store(CONSOLE, Ordering::Relaxed); 336 FOCUSED.store(CONSOLE, Ordering::Relaxed);
@@ -290,7 +342,6 @@ pub fn focus_console() {
290 } 342 }
291} 343}
292 344
293/// Cycle focus: console → top window → … → bottom window → console.
294pub fn focus_next() { 345pub fn focus_next() {
295 unsafe { 346 unsafe {
296 if ZLEN == 0 { 347 if ZLEN == 0 {
@@ -317,8 +368,6 @@ pub fn focus_next() {
317 } 368 }
318} 369}
319 370
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 { 371pub fn handle_key(ev: KeyEvent) -> bool {
323 match ev { 372 match ev {
324 KeyEvent::F(1) => { 373 KeyEvent::F(1) => {
@@ -346,7 +395,6 @@ pub fn handle_key(ev: KeyEvent) -> bool {
346} 395}
347 396
348fn spawn_terminal() { 397fn spawn_terminal() {
349 // Terminal app deferred (needs per-instance vt + per-instance shell).
350 crate::serial_println!("wm: F1 (spawn terminal) not implemented yet"); 398 crate::serial_println!("wm: F1 (spawn terminal) not implemented yet");
351} 399}
352 400
@@ -363,136 +411,30 @@ fn remove_from_zorder(id: usize) {
363 } 411 }
364} 412}
365 413
366/// Repaint a canvas region bottom→top: restore the console (layer 0), then 414// ---------------------------------------------------------------------------
367/// blit every visible window that intersects, then push once. 415// Hit-testing, mouse routing, and drag state
368fn composite_rect(x: usize, y: usize, w: usize, h: usize) { 416// ---------------------------------------------------------------------------
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 417
378/// Blit the portion of a window that intersects `(rx, ry, rw, rh)`. 418fn hit_test(id: usize, x: usize, y: usize) -> Hit {
379fn draw_window_intersect(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) { 419 let Some(w) = get(id) else {
380 let win = unsafe { WINDOWS[id] }; 420 return Hit::None;
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 }; 421 };
424 let bgc = gop::rgb(bg.0, bg.1, bg.2); 422 if !w.chrome {
425 gop::shadow_fill_rect(win.x, win.y, win.w, TITLE_BAR_H, bgc); 423 return Hit::Content;
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 } 424 }
433 425 if y >= w.y + TITLE_BAR_H {
434 // Close button (right edge, red) and minimize button (grey). 426 return Hit::Content;
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 } 427 }
475 out 428 let right = w.x + w.w;
476} 429 if x >= right.saturating_sub(8) {
477 430 return Hit::Close;
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 } 431 }
432 if x >= right.saturating_sub(16) {
433 return Hit::Minimize;
434 }
435 Hit::Title
484} 436}
485 437
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> { 438pub fn window_at(x: usize, y: usize) -> Option<usize> {
497 unsafe { 439 unsafe {
498 for i in (0..ZLEN).rev() { 440 for i in (0..ZLEN).rev() {
@@ -501,7 +443,8 @@ pub fn window_at(x: usize, y: usize) -> Option<usize> {
501 if !w.used || w.state != WinState::Visible { 443 if !w.used || w.state != WinState::Visible {
502 continue; 444 continue;
503 } 445 }
504 if x >= w.x && x < w.x + w.w && y >= w.y && y < w.y + w.h + TITLE_BAR_H { 446 let outer_h = w.h + if w.chrome { TITLE_BAR_H } else { 0 };
447 if x >= w.x && x < w.x + w.w && y >= w.y && y < w.y + outer_h {
505 return Some(id); 448 return Some(id);
506 } 449 }
507 } 450 }
@@ -509,37 +452,12 @@ pub fn window_at(x: usize, y: usize) -> Option<usize> {
509 None 452 None
510} 453}
511 454
512fn hit_test(id: usize, x: usize, y: usize) -> Hit { 455pub fn request_close(id: usize) {
513 let Some(w) = get(id) else { 456 if let Some(w) = get(id) {
514 return Hit::None; 457 let _ = crate::sched::send(w.owner, TaskMsg::Close);
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 } 458 }
526 Hit::Title
527} 459}
528 460
529/// What an in-progress mouse gesture is doing (started on a press).
530#[derive(Clone, Copy, PartialEq, Eq)]
531enum Drag {
532 None,
533 Move, // dragging a window by its title bar
534 App, // a stroke/drag inside a window's content
535}
536
537static mut DRAG: Drag = Drag::None;
538static mut DRAG_ID: usize = usize::MAX;
539static mut DRAG_DX: i32 = 0; // cursor - window.x at drag start
540static mut DRAG_DY: i32 = 0; // cursor - window.y at drag start
541
542/// Route one mouse event (absolute canvas coords, button bitmask, kind).
543pub fn route_mouse(x: i32, y: i32, buttons: u8, kind: MouseKind) { 461pub fn route_mouse(x: i32, y: i32, buttons: u8, kind: MouseKind) {
544 match kind { 462 match kind {
545 MouseKind::Press => mouse_press(x, y, buttons), 463 MouseKind::Press => mouse_press(x, y, buttons),
@@ -620,10 +538,9 @@ fn deliver_mouse(id: usize, cx: usize, cy: usize, buttons: u8, kind: MouseKind)
620 let Some(w) = get(id) else { 538 let Some(w) = get(id) else {
621 return; 539 return;
622 }; 540 };
541 let content_y = w.y + if w.chrome { TITLE_BAR_H } else { 0 };
623 let rx = cx as i32 - w.x as i32; 542 let rx = cx as i32 - w.x as i32;
624 let ry = cy as i32 - (w.y + TITLE_BAR_H) as i32; 543 let ry = cy as i32 - content_y as i32;
625 // Suppress out-of-bounds Press/Move, but always deliver Release so a
626 // stroke that ends outside the window still terminates.
627 if kind != MouseKind::Release && (rx < 0 || ry < 0 || rx >= w.w as i32 || ry >= w.h as i32) { 544 if kind != MouseKind::Release && (rx < 0 || ry < 0 || rx >= w.w as i32 || ry >= w.h as i32) {
628 return; 545 return;
629 } 546 }
@@ -636,8 +553,10 @@ fn deliver_mouse(id: usize, cx: usize, cy: usize, buttons: u8, kind: MouseKind)
636 let _ = crate::sched::send(w.owner, TaskMsg::AppMouse(ev)); 553 let _ = crate::sched::send(w.owner, TaskMsg::AppMouse(ev));
637} 554}
638 555
639/// Move a window to (nx, ny), clamping to the canvas, and repaint old+new.
640fn move_window(id: usize, nx: usize, ny: usize) { 556fn move_window(id: usize, nx: usize, ny: usize) {
557 if id == 0 {
558 return;
559 }
641 let Some(w) = get(id) else { 560 let Some(w) = get(id) else {
642 return; 561 return;
643 }; 562 };
@@ -656,7 +575,28 @@ fn move_window(id: usize, nx: usize, ny: usize) {
656 composite_rect(nx, ny, w.w, w.h + TITLE_BAR_H); 575 composite_rect(nx, ny, w.w, w.h + TITLE_BAR_H);
657} 576}
658 577
659/// Spawn the keyboard pump task (routes keys to the focused GUI window). 578pub fn owner(id: usize) -> Option<usize> {
579 get(id).map(|w| w.owner)
580}
581
582pub fn buffer(id: usize) -> Option<(*mut u32, usize, usize)> {
583 get(id).map(|w| (w.buf, w.w, w.h))
584}
585
586pub fn list() -> alloc::vec::Vec<(usize, alloc::string::String, bool)> {
587 let mut out = alloc::vec::Vec::new();
588 for i in 0..MAX_WINDOWS {
589 let w = unsafe { WINDOWS[i] };
590 if w.used && i != 0 {
591 let title = alloc::string::String::from(
592 core::str::from_utf8(&w.title[..w.title_len]).unwrap_or("?"),
593 );
594 out.push((i, title, w.state == WinState::Minimized));
595 }
596 }
597 out
598}
599
660pub fn spawn_input_pump() -> Option<usize> { 600pub fn spawn_input_pump() -> Option<usize> {
661 crate::sched::spawn(pump_main, "wm-input") 601 crate::sched::spawn(pump_main, "wm-input")
662} 602}
@@ -676,3 +616,103 @@ fn pump_main() {
676 crate::sched::sleep_ms(5); 616 crate::sched::sleep_ms(5);
677 } 617 }
678} 618}
619
620// ---------------------------------------------------------------------------
621// Compositing
622// ---------------------------------------------------------------------------
623
624/// Composite a region: fill background, blit windows bottom-to-top, push.
625fn composite_rect(x: usize, y: usize, w: usize, h: usize) {
626 gop::shadow_fill_rect(x, y, w, h, gop::rgb(DESKTOP.0, DESKTOP.1, DESKTOP.2));
627 unsafe {
628 for i in 0..ZLEN {
629 draw_window_intersect(ZORDER[i], x, y, w, h);
630 }
631 }
632 gop::push_rect(x, y, w, h);
633}
634
635/// Blit the portion of a window that intersects `(rx, ry, rw, rh)`.
636fn draw_window_intersect(id: usize, rx: usize, ry: usize, rw: usize, rh: usize) {
637 let win = unsafe { WINDOWS[id] };
638 if !win.used || win.state != WinState::Visible || win.buf.is_null() {
639 return;
640 }
641 let outer_h = win.h + if win.chrome { TITLE_BAR_H } else { 0 };
642 let wx = win.x;
643 let wy = win.y;
644 let ww = win.w;
645 let ix = wx.max(rx);
646 let iy = wy.max(ry);
647 let ix2 = (wx + ww).min(rx + rw);
648 let iy2 = (wy + outer_h).min(ry + rh);
649 if ix >= ix2 || iy >= iy2 {
650 return;
651 }
652 if win.chrome {
653 // Title bar strip
654 let tb0 = wy;
655 let tb1 = wy + TITLE_BAR_H;
656 if iy < tb1 && iy2 > tb0 {
657 draw_title_bar(id);
658 }
659 // Content strip
660 let c0 = wy + TITLE_BAR_H;
661 let c1 = wy + TITLE_BAR_H + win.h;
662 if iy2 > c0 && iy < c1 {
663 let cy0 = iy.max(c0);
664 let cy1 = iy2.min(c1);
665 let sx = ix - wx;
666 let sy = cy0 - c0;
667 let cw = ix2 - ix;
668 let chh = cy1 - cy0;
669 let src = unsafe { win.buf.add(sy * win.w + sx) };
670 gop::shadow_blit(ix, cy0, cw, chh, src, win.w);
671 }
672 } else {
673 // Chrome-less: no title bar, buffer is full-window
674 let sx = ix - wx;
675 let sy = iy - wy;
676 let cw = ix2 - ix;
677 let chh = iy2 - iy;
678 let src = unsafe { win.buf.add(sy * win.w + sx) };
679 gop::shadow_blit(ix, iy, cw, chh, src, win.w);
680 }
681}
682
683/// Draw a window's full title bar (background, title text, min/close).
684fn draw_title_bar(id: usize) {
685 let win = unsafe { WINDOWS[id] };
686 let bg = if focused() == Some(id) {
687 TITLE_BG_FOCUS
688 } else {
689 TITLE_BG
690 };
691 let bgc = gop::rgb(bg.0, bg.1, bg.2);
692 gop::shadow_fill_rect(win.x, win.y, win.w, TITLE_BAR_H, bgc);
693
694 // Title text, clipped short of the two buttons.
695 let btn_w = 16;
696 let cols = win.w.saturating_sub(btn_w + 4) / 8;
697 for (i, &b) in win.title[..win.title_len].iter().enumerate().take(cols) {
698 gop::shadow_glyph_px(win.x + 4 + i * 8, win.y, b as u16, TITLE_FG, bg);
699 }
700
701 // Close button (right edge, red) and minimize button (grey).
702 let close_x = win.x + win.w - 8;
703 let min_x = win.x + win.w - 16;
704 gop::shadow_fill_rect(
705 min_x,
706 win.y,
707 8,
708 TITLE_BAR_H,
709 gop::rgb(MIN_BG.0, MIN_BG.1, MIN_BG.2),
710 );
711 gop::shadow_fill_rect(
712 close_x,
713 win.y,
714 8,
715 TITLE_BAR_H,
716 gop::rgb(CLOSE_BG.0, CLOSE_BG.1, CLOSE_BG.2),
717 );
718}