diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 0f2caa15..75efb030 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -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(); diff --git a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs index c66d17d0..a7632d55 100644 --- a/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs +++ b/crates/console/prometeu-system/src/programs/prometeu_hub/prometeu_hub.rs @@ -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 { 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 { + 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); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 8584c727..62e01607 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -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":[]} diff --git a/discussion/workflow/plans/PLN-0063-hub-mouse-input-and-click-routing.md b/discussion/workflow/plans/PLN-0063-hub-mouse-input-and-click-routing.md index a0e521bc..6d967108 100644 --- a/discussion/workflow/plans/PLN-0063-hub-mouse-input-and-click-routing.md +++ b/discussion/workflow/plans/PLN-0063-hub-mouse-input-and-click-routing.md @@ -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