diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 19:52:05 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 19:52:05 +0200 |
| commit | 819778f292ee4e1294603b98662e083e49e23463 (patch) | |
| tree | 8df31b678432d2a89cac8e0d50f271a75a3882b5 /rsnix/src/cursor.rs | |
| parent | rsnix v0.4.0: GUI input foundation — PS/2 mouse + scheduler message queues (diff) | |
| download | rs-dos-819778f292ee4e1294603b98662e083e49e23463.tar.gz rs-dos-819778f292ee4e1294603b98662e083e49e23463.zip | |
rsnix v0.5.0: graphics layer + cursor compositor
The first visible GUI surface: real rect primitives, a single
shadow→display push path, and an on-screen mouse cursor that can
never race the text surface.
gop:
- fill_rect + push_rect(x,y,w,h): every raster op rasterises into the
RAM shadow, then push_rect copies the rect to the display (write-only,
still never read) and composites the cursor on top. blit_glyph and
scroll shifts now route through it.
- cursor overlay: 12x16 pointer (const-fn built from column lists) drawn
white with a black offset outline; position in one AtomicU64 (x:high
32, y:low 32) — unpack masks the low word (a bare 'as usize' cast is
a no-op on 64-bit). The cursor never enters the shadow, so erasing is
just a shadow push of the old rect.
- cursor_redraw after scroll shifts and once at spawn (the vt diff never
re-blits idle cells).
cursor task (task 2, spawned at boot when a mouse is present):
- owns the PS/2 ring; polls every 10 ms, moves the cursor (clamped),
tracks buttons, and forwards every event to a task's queue when
'mouse <id>' armed it (the window-manager input path).
- /dev/mouse is now a position device (x/y/buttons snapshot); 'mouse'
watches the cursor live (prints on change); 'cursor' prints one-shot
state; 'gfx' draws a status-bar demo; 'gfx check' is an in-kernel
pixel self-test (shadow readback, display readback, cursor overlay
purity) — PASS/FAIL per stage.
Tests (headless QEMU + monitor injection):
- make test-gfx (new): all four compositor checks PASS.
- make test-mouse (rewritten): cursor tracks mouse_move deltas, buttons
ride along, /dev/mouse snapshot, live watch, real-event queue routing.
- make test-msg: task numbering shifted (cursor is task 2; msgrecv is
task 3) — updated, still PASS.
- make test-disk regression re-passes. 0 build warnings.
Diffstat (limited to 'rsnix/src/cursor.rs')
| -rw-r--r-- | rsnix/src/cursor.rs | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/rsnix/src/cursor.rs b/rsnix/src/cursor.rs new file mode 100644 index 0000000..18c9a1d --- /dev/null +++ b/rsnix/src/cursor.rs | |||
| @@ -0,0 +1,71 @@ | |||
| 1 | // --------------------------------------------------------------------------- | ||
| 2 | // cursor — the mouse cursor task. | ||
| 3 | // | ||
| 4 | // One background task owns the PS/2 mouse: it polls the packet ring every | ||
| 5 | // 10 ms, moves the on-screen cursor (gop's display overlay), tracks the | ||
| 6 | // button state, and optionally *forwards* every event to another task's | ||
| 7 | // message queue (`mouse <id>` sets the target) — the input-routing half | ||
| 8 | // of the future window manager. | ||
| 9 | // | ||
| 10 | // The cursor itself never touches the text surface: `gop::push_rect` | ||
| 11 | // composites it on top of every display write, so text scrolling and | ||
| 12 | // cursor movement cannot corrupt each other (see gop.rs). | ||
| 13 | // --------------------------------------------------------------------------- | ||
| 14 | use core::sync::atomic::{AtomicUsize, Ordering}; | ||
| 15 | |||
| 16 | use crate::ps2::MouseEvent; | ||
| 17 | use crate::sched::TaskMsg; | ||
| 18 | |||
| 19 | /// Forward target task id, or `usize::MAX` = none. | ||
| 20 | static FORWARD_TARGET: AtomicUsize = AtomicUsize::new(usize::MAX); | ||
| 21 | |||
| 22 | /// Spawn the cursor task. Call after `sched::init`; only makes sense | ||
| 23 | /// when the PS/2 mouse came up at boot. The cursor starts centred. | ||
| 24 | pub fn spawn() -> Option<usize> { | ||
| 25 | let (w, h) = crate::gop::pixel_dims(); | ||
| 26 | crate::gop::cursor_set(w / 2 - 6, h / 2 - 8); | ||
| 27 | // Draw it immediately: the vt diff only re-blits changed cells, so an | ||
| 28 | // idle centre cell would never push (and thus never composite) it. | ||
| 29 | crate::gop::cursor_redraw(); | ||
| 30 | let id = crate::sched::spawn(task_main, "cursor")?; | ||
| 31 | crate::serial_println!( | ||
| 32 | "cursor: task {} — mouse poll + display overlay (forward target: none)", | ||
| 33 | id | ||
| 34 | ); | ||
| 35 | Some(id) | ||
| 36 | } | ||
| 37 | |||
| 38 | /// Route decoded mouse events to a task's queue (the shell's `mouse | ||
| 39 | /// <id>`). `None` stops forwarding. | ||
| 40 | pub fn set_forward(target: Option<usize>) { | ||
| 41 | FORWARD_TARGET.store(target.unwrap_or(usize::MAX), Ordering::Relaxed); | ||
| 42 | } | ||
| 43 | |||
| 44 | fn forward_target() -> Option<usize> { | ||
| 45 | match FORWARD_TARGET.load(Ordering::Relaxed) { | ||
| 46 | usize::MAX => None, | ||
| 47 | id => Some(id), | ||
| 48 | } | ||
| 49 | } | ||
| 50 | |||
| 51 | fn task_main() { | ||
| 52 | loop { | ||
| 53 | while let Some(ev) = crate::ps2::try_poll_mouse() { | ||
| 54 | crate::gop::cursor_move(ev.dx, ev.dy); | ||
| 55 | crate::gop::cursor_set_buttons(btn_bits(&ev)); | ||
| 56 | if let Some(id) = forward_target() | ||
| 57 | && crate::sched::send(id, TaskMsg::Mouse(ev)).is_err() | ||
| 58 | { | ||
| 59 | // The listener died mid-stream: stop routing (the | ||
| 60 | // cursor keeps tracking) rather than spam errors. | ||
| 61 | set_forward(None); | ||
| 62 | crate::serial_println!("cursor: forward target task {} gone — stopped routing", id); | ||
| 63 | } | ||
| 64 | } | ||
| 65 | crate::sched::sleep_ms(10); | ||
| 66 | } | ||
| 67 | } | ||
| 68 | |||
| 69 | fn btn_bits(ev: &MouseEvent) -> u8 { | ||
| 70 | (ev.left as u8) | ((ev.right as u8) << 1) | ((ev.middle as u8) << 2) | ||
| 71 | } | ||
