implements PLN-0063 hub mouse click routing

This commit is contained in:
bQUARKz 2026-05-15 17:54:25 +01:00
parent edfac5a99b
commit 414bdd0333
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
4 changed files with 130 additions and 12 deletions

View File

@ -437,6 +437,22 @@ mod tests {
);
}
#[test]
fn hub_home_click_launches_shell_a_as_native_shell_task_window() {
let (mut firmware, mut hardware) = hub_home_firmware();
let signals = InputSignals { f_signal: true, x_pos: 112, y_pos: 108, ..Default::default() };
firmware.tick(&signals, &mut hardware);
let task_id = match &firmware.state {
FirmwareState::ShellRunning(step) => step.task_id,
other => panic!("expected ShellRunning state, got {:?}", other),
};
assert!(firmware.os.windows().focused_window_belongs_to_task(task_id));
assert_eq!(firmware.os.windows().windows()[0].title, "ShellA");
}
#[test]
fn hub_home_launches_shell_b_as_native_shell_task_window() {
let (mut firmware, mut hardware) = hub_home_firmware();
@ -474,6 +490,22 @@ mod tests {
);
}
#[test]
fn shell_running_close_click_uses_lifecycle_and_returns_hub_home() {
let (mut firmware, mut hardware, _signals, task_id) = load_shell_running_firmware();
let close_signals =
InputSignals { f_signal: true, x_pos: 420, y_pos: 24, ..Default::default() };
firmware.tick(&close_signals, &mut hardware);
assert!(matches!(firmware.state, FirmwareState::HubHome(_)));
assert_eq!(firmware.os.lifecycle().task_state(task_id), Some(TaskState::Closed));
assert_eq!(
firmware.os.lifecycle().process_state_for_task(task_id),
Ok(ProcessState::Stopped)
);
}
#[test]
fn shell_running_closes_task_and_returns_hub_home_without_focused_window() {
let (mut firmware, mut hardware, signals, task_id) = load_shell_running_firmware();

View File

@ -1,8 +1,13 @@
use crate::{CrashReport, SystemOS};
use prometeu_hal::color::Color;
use prometeu_hal::primitives::Rect;
use prometeu_hal::{HardwareBridge, InputSignals};
use prometeu_vm::VirtualMachine;
const SHELL_A_BUTTON: Rect = Rect { x: 112, y: 108, w: 112, h: 48 };
const SHELL_B_BUTTON: Rect = Rect { x: 256, y: 108, w: 112, h: 48 };
const CLOSE_BUTTON: Rect = Rect { x: 420, y: 24, w: 16, h: 16 };
/// PrometeuHub: Launcher and system UI environment.
pub struct PrometeuHub;
@ -63,14 +68,21 @@ impl PrometeuHub {
pub fn gui_update(
&mut self,
_os: &mut SystemOS,
os: &mut SystemOS,
hw: &mut dyn HardwareBridge,
) -> Option<SystemProfileAction> {
hw.gfx_mut().clear(Color::BLACK);
if hw.pad().a().pressed {
let in_shell = os.windows().focused_window().is_some();
if let Some(action) =
action_for_click(in_shell, hw.touch().x(), hw.touch().y(), hw.touch().f().pressed)
{
return Some(action);
}
if !in_shell && hw.pad().a().pressed {
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
} else if hw.pad().b().pressed {
} else if !in_shell && hw.pad().b().pressed {
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
} else {
None
@ -102,7 +114,7 @@ impl PrometeuHub {
if os.windows().focused_window().is_some() {
if hw.pad().start().down {
action = Some(SystemProfileAction::CloseShell);
} else {
} else if action != Some(SystemProfileAction::CloseShell) {
crash = os.vm().tick(vm, signals, hw);
}
}
@ -113,3 +125,77 @@ impl PrometeuHub {
SystemProfileUpdate { crash, action }
}
}
fn action_for_click(in_shell: bool, x: i32, y: i32, pressed: bool) -> Option<SystemProfileAction> {
if !pressed {
return None;
}
if in_shell {
return rect_contains(CLOSE_BUTTON, x, y).then_some(SystemProfileAction::CloseShell);
}
if rect_contains(SHELL_A_BUTTON, x, y) {
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
} else if rect_contains(SHELL_B_BUTTON, x, y) {
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
} else {
None
}
}
fn rect_contains(rect: Rect, x: i32, y: i32) -> bool {
x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn shell_a_button_click_emits_launch_action() {
assert_eq!(
action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, true),
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA))
);
}
#[test]
fn shell_b_button_click_emits_launch_action() {
assert_eq!(
action_for_click(
false,
SHELL_B_BUTTON.x + SHELL_B_BUTTON.w - 1,
SHELL_B_BUTTON.y,
true
),
Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellB))
);
}
#[test]
fn button_hit_test_excludes_right_and_bottom_edges() {
assert_eq!(
action_for_click(false, SHELL_A_BUTTON.x + SHELL_A_BUTTON.w, SHELL_A_BUTTON.y, true),
None
);
assert_eq!(
action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y + SHELL_A_BUTTON.h, true),
None
);
}
#[test]
fn held_pointer_does_not_emit_click_action() {
assert_eq!(action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, false), None);
}
#[test]
fn close_button_click_emits_close_only_in_shell() {
assert_eq!(
action_for_click(true, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true),
Some(SystemProfileAction::CloseShell)
);
assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true), None);
}
}

View File

@ -34,4 +34,4 @@
{"type":"discussion","id":"DSC-0031","status":"done","ticket":"runtime-mode-separation-game-system","title":"Agenda - Runtime Mode Separation: Game and System","created_at":"2026-05-11","updated_at":"2026-05-14","tags":["runtime","firmware","hub","system-apps","game-mode","scheduler"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0040","file":"discussion/lessons/DSC-0031-runtime-mode-separation-game-system/LSN-0040-system-pipeline-separation.md","status":"done","created_at":"2026-05-14","updated_at":"2026-05-14"}]}
{"type":"discussion","id":"DSC-0032","status":"done","ticket":"system-os-lifecycle-process-task-contract","title":"Agenda - SystemOS Lifecycle, Process and Task Contract","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","lifecycle","process","task","shell","firmware"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0041","file":"discussion/lessons/DSC-0032-system-os-lifecycle-process-task-contract/LSN-0041-systemos-lifecycle-authority.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0033","status":"done","ticket":"system-os-service-ownership-and-module-layout","title":"Agenda - SystemOS Service Ownership and Module Layout","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","services","module-layout","vm","window-manager","logging"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0042","file":"discussion/lessons/DSC-0033-system-os-service-ownership-and-module-layout/LSN-0042-systemos-service-ownership-boundary.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]}
{"type":"discussion","id":"DSC-0036","status":"in_progress","ticket":"prometeu-hub-ui-direction","title":"Agenda - Prometeu Hub UI Direction","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["hub","ui","shell","system-apps","lifecycle","design-system"],"agendas":[{"id":"AGD-0036","file":"AGD-0036-prometeu-hub-ui-direction.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0028","file":"DEC-0028-prometeu-hub-initial-retro-shell-ui-slice.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0036"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-native-shell-launch-and-lifecycle-wiring.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0063","file":"PLN-0063-hub-mouse-input-and-click-routing.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0064","file":"PLN-0064-retro-hub-home-and-fake-shell-ui.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0065","file":"PLN-0065-pixel-operator-font-integration.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0066","file":"PLN-0066-hub-ui-slice-validation-and-lesson.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0036","status":"in_progress","ticket":"prometeu-hub-ui-direction","title":"Agenda - Prometeu Hub UI Direction","created_at":"2026-05-15","updated_at":"2026-05-15","tags":["hub","ui","shell","system-apps","lifecycle","design-system"],"agendas":[{"id":"AGD-0036","file":"AGD-0036-prometeu-hub-ui-direction.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15"}],"decisions":[{"id":"DEC-0028","file":"DEC-0028-prometeu-hub-initial-retro-shell-ui-slice.md","status":"accepted","created_at":"2026-05-15","updated_at":"2026-05-15","ref_agenda":"AGD-0036"}],"plans":[{"id":"PLN-0062","file":"PLN-0062-native-shell-launch-and-lifecycle-wiring.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0063","file":"PLN-0063-hub-mouse-input-and-click-routing.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0064","file":"PLN-0064-retro-hub-home-and-fake-shell-ui.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0065","file":"PLN-0065-pixel-operator-font-integration.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]},{"id":"PLN-0066","file":"PLN-0066-hub-ui-slice-validation-and-lesson.md","status":"open","created_at":"2026-05-15","updated_at":"2026-05-15","ref_decisions":["DEC-0028"]}],"lessons":[]}

View File

@ -2,7 +2,7 @@
id: PLN-0063
ticket: prometeu-hub-ui-direction
title: Hub Mouse Input and Click Routing
status: open
status: done
created: 2026-05-15
ref_decisions: [DEC-0028]
tags: [hub, ui, shell, system-apps, lifecycle, design-system]
@ -67,12 +67,12 @@ Provide reliable 270p mouse hit testing and click routing for the initial Hub UI
## Acceptance Criteria
- [ ] Pointer coordinates are interpreted in the internal 270p coordinate space.
- [ ] Clicking the `ShellA` button emits the `ShellA` launch action exactly once per click.
- [ ] Clicking the `ShellB` button emits the `ShellB` launch action exactly once per click.
- [ ] Clicking the close command emits close intent for the active Shell app.
- [ ] Holding the mouse button does not repeatedly launch apps.
- [ ] Existing controller/game input paths are not regressed.
- [x] Pointer coordinates are interpreted in the internal 270p coordinate space.
- [x] Clicking the `ShellA` button emits the `ShellA` launch action exactly once per click.
- [x] Clicking the `ShellB` button emits the `ShellB` launch action exactly once per click.
- [x] Clicking the close command emits close intent for the active Shell app.
- [x] Holding the mouse button does not repeatedly launch apps.
- [x] Existing controller/game input paths are not regressed.
## Tests