diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-17 18:58:06 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-17 18:58:06 +0200 |
| commit | 0e405aad6bc9f631b9b7a766fb1147fef943f492 (patch) | |
| tree | d9c10ed2e3fae589aabff3375d53bbf66139add2 | |
| parent | Windowed GUI: wm module, input routing, and the paint app (diff) | |
| download | rs-dos-0e405aad6bc9f631b9b7a766fb1147fef943f492.tar.gz rs-dos-0e405aad6bc9f631b9b7a766fb1147fef943f492.zip | |
wm: title-bar drag-to-move; stop delivering title-bar gestures to apps
- Track the active mouse gesture (none / window-move / app-stroke) so a
press on the title bar becomes a move drag instead of leaking Move
events into the app's content
- Add move_window (clamped to the canvas, repaint old + new regions)
- Always deliver MouseKind::Release so a stroke that ends outside the
window still terminates
| -rw-r--r-- | docs/adr/0004-windowed-gui-and-compositor.md | 4 | ||||
| -rw-r--r-- | rsnix/src/wm.rs | 126 |
2 files changed, 108 insertions, 22 deletions
diff --git a/docs/adr/0004-windowed-gui-and-compositor.md b/docs/adr/0004-windowed-gui-and-compositor.md index 86e8b30..3476fab 100644 --- a/docs/adr/0004-windowed-gui-and-compositor.md +++ b/docs/adr/0004-windowed-gui-and-compositor.md | |||
| @@ -62,8 +62,8 @@ Introduce an overlapping-window GUI: | |||
| 62 | console (app 0) never closes. | 62 | console (app 0) never closes. |
| 63 | - **Placement** — new windows cascade (+24px each, wrapping) and are | 63 | - **Placement** — new windows cascade (+24px each, wrapping) and are |
| 64 | focused and raised on creation; the console is full-canvas at (0,0). | 64 | focused and raised on creation; the console is full-canvas at (0,0). |
| 65 | Windows are fixed-position and fixed-size in v1: move is the first | 65 | Windows are movable by dragging the title bar (clamped to the canvas); |
| 66 | post-v1 feature and resize is deferred. | 66 | resize is deferred. |
| 67 | 67 | ||
| 68 | ## Consequences | 68 | ## Consequences |
| 69 | 69 | ||
diff --git a/rsnix/src/wm.rs b/rsnix/src/wm.rs index a39702d..be74f9a 100644 --- a/rsnix/src/wm.rs +++ b/rsnix/src/wm.rs | |||
| @@ -526,29 +526,93 @@ fn hit_test(id: usize, x: usize, y: usize) -> Hit { | |||
| 526 | Hit::Title | 526 | Hit::Title |
| 527 | } | 527 | } |
| 528 | 528 | ||
| 529 | /// What an in-progress mouse gesture is doing (started on a press). | ||
| 530 | #[derive(Clone, Copy, PartialEq, Eq)] | ||
| 531 | enum Drag { | ||
| 532 | None, | ||
| 533 | Move, // dragging a window by its title bar | ||
| 534 | App, // a stroke/drag inside a window's content | ||
| 535 | } | ||
| 536 | |||
| 537 | static mut DRAG: Drag = Drag::None; | ||
| 538 | static mut DRAG_ID: usize = usize::MAX; | ||
| 539 | static mut DRAG_DX: i32 = 0; // cursor - window.x at drag start | ||
| 540 | static mut DRAG_DY: i32 = 0; // cursor - window.y at drag start | ||
| 541 | |||
| 529 | /// Route one mouse event (absolute canvas coords, button bitmask, kind). | 542 | /// Route one mouse event (absolute canvas coords, button bitmask, kind). |
| 530 | pub fn route_mouse(x: i32, y: i32, buttons: u8, kind: MouseKind) { | 543 | pub fn route_mouse(x: i32, y: i32, buttons: u8, kind: MouseKind) { |
| 531 | if kind == MouseKind::Press { | 544 | match kind { |
| 532 | if x < 0 || y < 0 { | 545 | MouseKind::Press => mouse_press(x, y, buttons), |
| 533 | return; | 546 | MouseKind::Move => mouse_move(x, y, buttons), |
| 534 | } | 547 | MouseKind::Release => mouse_release(x, y, buttons), |
| 535 | let (cx, cy) = (x as usize, y as usize); | 548 | } |
| 536 | if let Some(id) = window_at(cx, cy) { | 549 | } |
| 537 | match hit_test(id, cx, cy) { | 550 | |
| 538 | Hit::Close => request_close(id), | 551 | fn mouse_press(x: i32, y: i32, buttons: u8) { |
| 539 | Hit::Minimize => minimize(id), | 552 | if x < 0 || y < 0 { |
| 540 | Hit::Title => focus(id), | 553 | return; |
| 541 | Hit::Content => { | 554 | } |
| 542 | focus(id); | 555 | let (cx, cy) = (x as usize, y as usize); |
| 543 | deliver_mouse(id, cx, cy, buttons, kind); | 556 | if let Some(id) = window_at(cx, cy) { |
| 557 | match hit_test(id, cx, cy) { | ||
| 558 | Hit::Close => request_close(id), | ||
| 559 | Hit::Minimize => minimize(id), | ||
| 560 | Hit::Title => { | ||
| 561 | focus(id); | ||
| 562 | if let Some(w) = get(id) { | ||
| 563 | unsafe { | ||
| 564 | DRAG = Drag::Move; | ||
| 565 | DRAG_ID = id; | ||
| 566 | DRAG_DX = cx as i32 - w.x as i32; | ||
| 567 | DRAG_DY = cy as i32 - w.y as i32; | ||
| 568 | } | ||
| 544 | } | 569 | } |
| 545 | Hit::None => {} | ||
| 546 | } | 570 | } |
| 547 | } else { | 571 | Hit::Content => { |
| 548 | focus_console(); | 572 | focus(id); |
| 573 | unsafe { | ||
| 574 | DRAG = Drag::App; | ||
| 575 | DRAG_ID = id; | ||
| 576 | } | ||
| 577 | deliver_mouse(id, cx, cy, buttons, MouseKind::Press); | ||
| 578 | } | ||
| 579 | Hit::None => {} | ||
| 549 | } | 580 | } |
| 550 | } else if let Some(id) = focused() { | 581 | } else { |
| 551 | deliver_mouse(id, x as usize, y as usize, buttons, kind); | 582 | focus_console(); |
| 583 | } | ||
| 584 | } | ||
| 585 | |||
| 586 | fn mouse_move(x: i32, y: i32, buttons: u8) { | ||
| 587 | match unsafe { DRAG } { | ||
| 588 | Drag::Move => { | ||
| 589 | let id = unsafe { DRAG_ID }; | ||
| 590 | let Some(w) = get(id) else { | ||
| 591 | unsafe { DRAG = Drag::None } | ||
| 592 | return; | ||
| 593 | }; | ||
| 594 | let nx = (x - unsafe { DRAG_DX }).max(0) as usize; | ||
| 595 | let ny = (y - unsafe { DRAG_DY }).max(0) as usize; | ||
| 596 | if nx != w.x || ny != w.y { | ||
| 597 | move_window(id, nx, ny); | ||
| 598 | } | ||
| 599 | } | ||
| 600 | Drag::App => { | ||
| 601 | let id = unsafe { DRAG_ID }; | ||
| 602 | deliver_mouse(id, x as usize, y as usize, buttons, MouseKind::Move); | ||
| 603 | } | ||
| 604 | Drag::None => {} | ||
| 605 | } | ||
| 606 | } | ||
| 607 | |||
| 608 | fn mouse_release(x: i32, y: i32, buttons: u8) { | ||
| 609 | if unsafe { DRAG } == Drag::App { | ||
| 610 | let id = unsafe { DRAG_ID }; | ||
| 611 | deliver_mouse(id, x as usize, y as usize, buttons, MouseKind::Release); | ||
| 612 | } | ||
| 613 | unsafe { | ||
| 614 | DRAG = Drag::None; | ||
| 615 | DRAG_ID = usize::MAX; | ||
| 552 | } | 616 | } |
| 553 | } | 617 | } |
| 554 | 618 | ||
| @@ -558,8 +622,10 @@ fn deliver_mouse(id: usize, cx: usize, cy: usize, buttons: u8, kind: MouseKind) | |||
| 558 | }; | 622 | }; |
| 559 | let rx = cx as i32 - w.x as i32; | 623 | let rx = cx as i32 - w.x as i32; |
| 560 | let ry = cy as i32 - (w.y + TITLE_BAR_H) as i32; | 624 | 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 { | 625 | // Suppress out-of-bounds Press/Move, but always deliver Release so a |
| 562 | return; // out of bounds — suppressed | 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) { | ||
| 628 | return; | ||
| 563 | } | 629 | } |
| 564 | let ev = AppMouseEvent { | 630 | let ev = AppMouseEvent { |
| 565 | x: rx, | 631 | x: rx, |
| @@ -570,6 +636,26 @@ fn deliver_mouse(id: usize, cx: usize, cy: usize, buttons: u8, kind: MouseKind) | |||
| 570 | let _ = crate::sched::send(w.owner, TaskMsg::AppMouse(ev)); | 636 | let _ = crate::sched::send(w.owner, TaskMsg::AppMouse(ev)); |
| 571 | } | 637 | } |
| 572 | 638 | ||
| 639 | /// Move a window to (nx, ny), clamping to the canvas, and repaint old+new. | ||
| 640 | fn move_window(id: usize, nx: usize, ny: usize) { | ||
| 641 | let Some(w) = get(id) else { | ||
| 642 | return; | ||
| 643 | }; | ||
| 644 | let (cw, ch) = gop::pixel_dims(); | ||
| 645 | let nx = nx.min(cw.saturating_sub(w.w)); | ||
| 646 | let ny = ny.min(ch.saturating_sub(w.h + TITLE_BAR_H)); | ||
| 647 | if nx == w.x && ny == w.y { | ||
| 648 | return; | ||
| 649 | } | ||
| 650 | let (ox, oy) = (w.x, w.y); | ||
| 651 | unsafe { | ||
| 652 | WINDOWS[id].x = nx; | ||
| 653 | WINDOWS[id].y = ny; | ||
| 654 | } | ||
| 655 | composite_rect(ox, oy, w.w, w.h + TITLE_BAR_H); | ||
| 656 | composite_rect(nx, ny, w.w, w.h + TITLE_BAR_H); | ||
| 657 | } | ||
| 658 | |||
| 573 | /// Spawn the keyboard pump task (routes keys to the focused GUI window). | 659 | /// Spawn the keyboard pump task (routes keys to the focused GUI window). |
| 574 | pub fn spawn_input_pump() -> Option<usize> { | 660 | pub fn spawn_input_pump() -> Option<usize> { |
| 575 | crate::sched::spawn(pump_main, "wm-input") | 661 | crate::sched::spawn(pump_main, "wm-input") |
