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 a7632d55..4580b605 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,5 +1,6 @@ use crate::{CrashReport, SystemOS}; use prometeu_hal::color::Color; +use prometeu_hal::gfx_bridge::GfxBridge; use prometeu_hal::primitives::Rect; use prometeu_hal::{HardwareBridge, InputSignals}; use prometeu_vm::VirtualMachine; @@ -7,6 +8,21 @@ 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 }; +const VIEWPORT: Rect = Rect { x: 0, y: 0, w: 480, h: 270 }; +const HOME_PANEL: Rect = Rect { x: 48, y: 58, w: 384, h: 154 }; +const TITLE_PANEL: Rect = Rect { x: 72, y: 34, w: 336, h: 28 }; +#[cfg(test)] +const SHELL_FRAME: Rect = Rect { x: 40, y: 20, w: 400, h: 220 }; + +const COLOR_BG: Color = Color::rgb(12, 14, 22); +const COLOR_PANEL: Color = Color::rgb(25, 31, 48); +const COLOR_PANEL_DARK: Color = Color::rgb(8, 10, 16); +const COLOR_BORDER: Color = Color::rgb(180, 218, 222); +const COLOR_HILITE: Color = Color::rgb(252, 226, 122); +const COLOR_TEXT: Color = Color::rgb(230, 236, 224); +const COLOR_MUTED: Color = Color::rgb(91, 112, 128); +const COLOR_BUTTON: Color = Color::rgb(45, 59, 82); +const COLOR_BUTTON_ACTIVE: Color = Color::rgb(72, 91, 122); /// PrometeuHub: Launcher and system UI environment. pub struct PrometeuHub; @@ -17,6 +33,18 @@ pub enum NativeShellApp { ShellB, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct HubButtonSpec { + app: NativeShellApp, + rect: Rect, + label: &'static str, +} + +const HOME_BUTTONS: [HubButtonSpec; 2] = [ + HubButtonSpec { app: NativeShellApp::ShellA, rect: SHELL_A_BUTTON, label: "ShellA" }, + HubButtonSpec { app: NativeShellApp::ShellB, rect: SHELL_B_BUTTON, label: "ShellB" }, +]; + impl NativeShellApp { pub fn app_id(self) -> u32 { match self { @@ -90,13 +118,22 @@ impl PrometeuHub { } pub fn render(&mut self, os: &mut SystemOS, hw: &mut dyn HardwareBridge) { + let pointer_x = hw.touch().x(); + let pointer_y = hw.touch().y(); + + if os.windows().window_count() == 0 { + render_home(hw.gfx_mut(), pointer_x, pointer_y); + return; + } + for window in os.windows().windows() { - hw.gfx_mut().fill_rect( - window.viewport.x, - window.viewport.y, - window.viewport.w, - window.viewport.h, + render_shell_window( + hw.gfx_mut(), + window.viewport, window.color, + &window.title, + pointer_x, + pointer_y, ); } } @@ -135,19 +172,67 @@ fn action_for_click(in_shell: bool, x: i32, y: i32, pressed: bool) -> Option bool { x >= rect.x && x < rect.x + rect.w && y >= rect.y && y < rect.y + rect.h } +fn render_home(gfx: &mut dyn GfxBridge, pointer_x: i32, pointer_y: i32) { + gfx.fill_rect(VIEWPORT.x, VIEWPORT.y, VIEWPORT.w, VIEWPORT.h, COLOR_BG); + draw_panel(gfx, TITLE_PANEL, COLOR_PANEL_DARK, COLOR_BORDER); + gfx.draw_text(TITLE_PANEL.x + 58, TITLE_PANEL.y + 9, "PROMETEU HUB", COLOR_TEXT); + + draw_panel(gfx, HOME_PANEL, COLOR_PANEL, COLOR_BORDER); + gfx.draw_text(HOME_PANEL.x + 26, HOME_PANEL.y + 22, "Native Shell Apps", COLOR_TEXT); + gfx.draw_text(HOME_PANEL.x + 26, HOME_PANEL.y + 40, "first retro ui slice", COLOR_MUTED); + + for button in HOME_BUTTONS { + let hovered = rect_contains(button.rect, pointer_x, pointer_y); + draw_button(gfx, button.rect, button.label, hovered); + } +} + +fn render_shell_window( + gfx: &mut dyn GfxBridge, + viewport: Rect, + color: Color, + title: &str, + pointer_x: i32, + pointer_y: i32, +) { + gfx.fill_rect(VIEWPORT.x, VIEWPORT.y, VIEWPORT.w, VIEWPORT.h, COLOR_BG); + draw_panel(gfx, viewport, COLOR_PANEL_DARK, COLOR_BORDER); + gfx.fill_rect(viewport.x + 4, viewport.y + 24, viewport.w - 8, viewport.h - 28, color); + gfx.draw_text(viewport.x + 10, viewport.y + 8, title, COLOR_TEXT); + gfx.draw_text(viewport.x + 18, viewport.y + 44, "native fake shell", COLOR_PANEL_DARK); + + let close_hovered = rect_contains(CLOSE_BUTTON, pointer_x, pointer_y); + let close_fill = if close_hovered { COLOR_HILITE } else { COLOR_BUTTON }; + gfx.fill_rect(CLOSE_BUTTON.x, CLOSE_BUTTON.y, CLOSE_BUTTON.w, CLOSE_BUTTON.h, close_fill); + gfx.draw_rect(CLOSE_BUTTON.x, CLOSE_BUTTON.y, CLOSE_BUTTON.w, CLOSE_BUTTON.h, COLOR_BORDER); + gfx.draw_text(CLOSE_BUTTON.x + 5, CLOSE_BUTTON.y + 4, "x", COLOR_TEXT); +} + +fn draw_panel(gfx: &mut dyn GfxBridge, rect: Rect, fill: Color, border: Color) { + gfx.fill_rect(rect.x, rect.y, rect.w, rect.h, fill); + gfx.draw_rect(rect.x, rect.y, rect.w, rect.h, border); + gfx.draw_rect(rect.x + 2, rect.y + 2, rect.w - 4, rect.h - 4, COLOR_MUTED); +} + +fn draw_button(gfx: &mut dyn GfxBridge, rect: Rect, label: &str, hovered: bool) { + let fill = if hovered { COLOR_BUTTON_ACTIVE } else { COLOR_BUTTON }; + let border = if hovered { COLOR_HILITE } else { COLOR_BORDER }; + gfx.fill_rect(rect.x, rect.y, rect.w, rect.h, fill); + gfx.draw_rect(rect.x, rect.y, rect.w, rect.h, border); + gfx.draw_rect(rect.x + 3, rect.y + 3, rect.w - 6, rect.h - 6, COLOR_MUTED); + gfx.draw_text(rect.x + 28, rect.y + 18, label, COLOR_TEXT); +} + #[cfg(test)] mod tests { use super::*; @@ -155,7 +240,7 @@ mod tests { #[test] fn shell_a_button_click_emits_launch_action() { assert_eq!( - action_for_click(false, SHELL_A_BUTTON.x, SHELL_A_BUTTON.y, true), + action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.y, true), Some(SystemProfileAction::LaunchNativeShell(NativeShellApp::ShellA)) ); } @@ -198,4 +283,23 @@ mod tests { ); assert_eq!(action_for_click(false, CLOSE_BUTTON.x, CLOSE_BUTTON.y, true), None); } + + #[test] + fn home_layout_exposes_exactly_two_manual_shell_buttons() { + assert_eq!(HOME_BUTTONS.len(), 2); + assert_eq!(HOME_BUTTONS[0].label, "ShellA"); + assert_eq!(HOME_BUTTONS[0].app, NativeShellApp::ShellA); + assert_eq!(HOME_BUTTONS[1].label, "ShellB"); + assert_eq!(HOME_BUTTONS[1].app, NativeShellApp::ShellB); + } + + #[test] + fn shell_close_button_sits_inside_shell_frame() { + assert!(rect_contains(SHELL_FRAME, CLOSE_BUTTON.x, CLOSE_BUTTON.y)); + assert!(rect_contains( + SHELL_FRAME, + CLOSE_BUTTON.x + CLOSE_BUTTON.w - 1, + CLOSE_BUTTON.y + CLOSE_BUTTON.h - 1 + )); + } } diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 62e01607..fc70c9f9 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":"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":[]} +{"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":"done","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-0064-retro-hub-home-and-fake-shell-ui.md b/discussion/workflow/plans/PLN-0064-retro-hub-home-and-fake-shell-ui.md index 28da0d40..8ee30c86 100644 --- a/discussion/workflow/plans/PLN-0064-retro-hub-home-and-fake-shell-ui.md +++ b/discussion/workflow/plans/PLN-0064-retro-hub-home-and-fake-shell-ui.md @@ -2,7 +2,7 @@ id: PLN-0064 ticket: prometeu-hub-ui-direction title: Retro Hub Home and Fake Shell UI -status: open +status: done created: 2026-05-15 ref_decisions: [DEC-0028] tags: [hub, ui, shell, system-apps, lifecycle, design-system] @@ -77,14 +77,14 @@ Replace the current debug-style window surface with the first Prometeu OS visual ## Acceptance Criteria -- [ ] Hub/Home renders full-screen in the 270p viewport. -- [ ] Hub/Home shows exactly two manual launch buttons: `ShellA` and `ShellB`. -- [ ] The visual style is retro/minimalist with strong borders and limited palette. -- [ ] `ShellA` renders as a simple green native fake Shell app/window. -- [ ] `ShellB` renders as a simple blue native fake Shell app/window. -- [ ] Each Shell app view exposes a visible close command. -- [ ] Local primitives are not exported as a general toolkit. -- [ ] No Tabs/List/Status bar/Footer/help bar are added in this wave. +- [x] Hub/Home renders full-screen in the 270p viewport. +- [x] Hub/Home shows exactly two manual launch buttons: `ShellA` and `ShellB`. +- [x] The visual style is retro/minimalist with strong borders and limited palette. +- [x] `ShellA` renders as a simple green native fake Shell app/window. +- [x] `ShellB` renders as a simple blue native fake Shell app/window. +- [x] Each Shell app view exposes a visible close command. +- [x] Local primitives are not exported as a general toolkit. +- [x] No Tabs/List/Status bar/Footer/help bar are added in this wave. ## Tests