diff options
Diffstat (limited to '')
| -rw-r--r-- | rsnix/src/wm.rs | 592 |
1 files changed, 592 insertions, 0 deletions
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 | |||
| 14 | extern crate alloc; | ||
| 15 | |||
| 16 | use core::sync::atomic::{AtomicUsize, Ordering}; | ||
| 17 | |||
| 18 | use crate::gop; | ||
| 19 | use crate::memory; | ||
| 20 | use crate::ps2::{AppMouseEvent, KeyEvent, MouseKind}; | ||
| 21 | use crate::sched::TaskMsg; | ||
| 22 | |||
| 23 | /// Height of a window's title bar in pixels (one 8×16 glyph row). | ||
| 24 | pub const TITLE_BAR_H: usize = 16; | ||
| 25 | |||
| 26 | /// Sentinel: `FOCUSED == CONSOLE` means the console (vt) has focus. | ||
| 27 | pub const CONSOLE: usize = usize::MAX; | ||
| 28 | |||
| 29 | const MAX_WINDOWS: usize = 16; | ||
| 30 | const MIN_W: usize = 64; | ||
| 31 | const MIN_H: usize = 64; | ||
| 32 | const CASCADE: usize = 24; | ||
| 33 | const TITLE_MAX: usize = 32; | ||
| 34 | |||
| 35 | // Palette (RGB; `gop::rgb` packs for the current pixel format). | ||
| 36 | const TITLE_BG: (u8, u8, u8) = (0x3a, 0x3c, 0x42); | ||
| 37 | const TITLE_BG_FOCUS: (u8, u8, u8) = (0x28, 0x4a, 0x8c); | ||
| 38 | const TITLE_FG: (u8, u8, u8) = (0xff, 0xff, 0xff); | ||
| 39 | const CLOSE_BG: (u8, u8, u8) = (0xb0, 0x30, 0x30); | ||
| 40 | const MIN_BG: (u8, u8, u8) = (0x50, 0x50, 0x50); | ||
| 41 | |||
| 42 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 43 | enum WinState { | ||
| 44 | Visible, | ||
| 45 | Minimized, | ||
| 46 | } | ||
| 47 | |||
| 48 | #[derive(Clone, Copy)] | ||
| 49 | struct 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 | |||
| 63 | const 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 | |||
| 76 | static mut WINDOWS: [Window; MAX_WINDOWS] = [EMPTY; MAX_WINDOWS]; | ||
| 77 | /// Window ids in bottom→top z-order. | ||
| 78 | static mut ZORDER: [usize; MAX_WINDOWS] = [usize::MAX; MAX_WINDOWS]; | ||
| 79 | static mut ZLEN: usize = 0; | ||
| 80 | static FOCUSED: AtomicUsize = AtomicUsize::new(CONSOLE); | ||
| 81 | static mut CASCADE_X: usize = 0; | ||
| 82 | static mut CASCADE_Y: usize = 0; | ||
| 83 | |||
| 84 | /// Initialise window state. The console (vt) remains the visible layer 0; | ||
| 85 | /// no GUI windows exist yet. | ||
| 86 | pub 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. | ||
| 96 | pub fn focused() -> Option<usize> { | ||
| 97 | let v = FOCUSED.load(Ordering::Relaxed); | ||
| 98 | if v == CONSOLE { None } else { Some(v) } | ||
| 99 | } | ||
| 100 | |||
| 101 | fn 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. | ||
| 112 | pub 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. | ||
| 186 | pub 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. | ||
| 194 | pub 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). | ||
| 218 | pub 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). | ||
| 237 | pub 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. | ||
| 255 | pub 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. | ||
| 282 | pub 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. | ||
| 294 | pub 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. | ||
| 322 | pub 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 | |||
| 348 | fn 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 | |||
| 353 | fn 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. | ||
| 368 | fn 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)`. | ||
| 379 | fn 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). | ||
| 417 | fn 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). | ||
| 454 | pub 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. | ||
| 459 | pub 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. | ||
| 464 | pub 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). | ||
| 480 | pub 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)] | ||
| 487 | enum Hit { | ||
| 488 | None, | ||
| 489 | Close, | ||
| 490 | Minimize, | ||
| 491 | Title, | ||
| 492 | Content, | ||
| 493 | } | ||
| 494 | |||
| 495 | /// Topmost visible window whose *outer* bounds contain the point. | ||
| 496 | pub 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 | |||
| 512 | fn 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). | ||
| 530 | pub 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 | |||
| 555 | fn 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). | ||
| 574 | pub fn spawn_input_pump() -> Option<usize> { | ||
| 575 | crate::sched::spawn(pump_main, "wm-input") | ||
| 576 | } | ||
| 577 | |||
| 578 | fn 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 | } | ||
