use crate::{CrashReport, SystemOS}; use prometeu_hal::color::Color; use prometeu_hal::primitives::Rect; use prometeu_hal::{ FrameId, GfxUiCommand, InputPlatform, InputSignals, RenderSubmission, RuntimePlatform, ShellUiFramePacket, }; 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 { home_input_armed: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NativeShellApp { ShellA, 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 { Self::ShellA => 10_001, Self::ShellB => 10_002, } } pub fn title(self) -> &'static str { match self { Self::ShellA => "ShellA", Self::ShellB => "ShellB", } } pub fn color(self) -> Color { match self { Self::ShellA => Color::GREEN, Self::ShellB => Color::BLUE, } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SystemProfileAction { LaunchNativeShell(NativeShellApp), CloseShell, } pub struct SystemProfileUpdate { pub crash: Option, pub action: Option, } impl Default for PrometeuHub { fn default() -> Self { Self::new() } } impl PrometeuHub { pub fn new() -> Self { Self { home_input_armed: false } } pub fn init(&mut self) { self.home_input_armed = false; } pub fn gui_update( &mut self, os: &mut SystemOS, platform: &mut dyn RuntimePlatform, ) -> Option { let in_shell = os.windows().focused_window().is_some(); if !in_shell { if !self.home_input_armed { self.home_input_armed = !activation_input_down(platform.input()); return None; } } else { self.home_input_armed = false; } let input = platform.input(); if let Some(action) = action_for_click( in_shell, input.touch().x(), input.touch().y(), input.touch().f().pressed, ) { return Some(action); } None } pub fn render(&mut self, os: &mut SystemOS, platform: &mut dyn RuntimePlatform) { let pointer_x = platform.input().touch().x(); let pointer_y = platform.input().touch().y(); let packet = if os.windows().window_count() == 0 { render_home_packet(pointer_x, pointer_y) } else { let mut commands = Vec::new(); for window in os.windows().windows() { append_shell_window_commands( &mut commands, window.viewport, window.color, &window.title, pointer_x, pointer_y, ); } ShellUiFramePacket::new(commands) }; platform .render_submission_sink() .submit_render_submission(RenderSubmission::shell_ui(FrameId::ZERO, packet)) .expect("hub render submission should publish"); } pub fn update_shell_profile( &mut self, os: &mut SystemOS, vm: &mut VirtualMachine, signals: &InputSignals, platform: &mut dyn RuntimePlatform, ) -> SystemProfileUpdate { let mut crash = None; let mut action = self.gui_update(os, platform); if os.windows().focused_window().is_some() { if platform.input().pad().start().down { action = Some(SystemProfileAction::CloseShell); } else if action != Some(SystemProfileAction::CloseShell) { crash = os.vm().tick(vm, signals, platform); } } self.render(os, platform); 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); } HOME_BUTTONS .iter() .find(|button| rect_contains(button.rect, x, y)) .map(|button| SystemProfileAction::LaunchNativeShell(button.app)) } 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 } fn activation_input_down(input: &dyn InputPlatform) -> bool { input.touch().f().down || input.pad().a().down || input.pad().b().down || input.pad().start().down } fn render_home_packet(pointer_x: i32, pointer_y: i32) -> ShellUiFramePacket { let mut commands = Vec::new(); commands.push(GfxUiCommand::Clear { color: COLOR_BG }); fill_rect(&mut commands, VIEWPORT, COLOR_BG); draw_panel(&mut commands, TITLE_PANEL, COLOR_PANEL_DARK, COLOR_BORDER); draw_buffer_text( &mut commands, TITLE_PANEL.x + 58, TITLE_PANEL.y + 9, "PROMETEU HUB", COLOR_TEXT, ); draw_panel(&mut commands, HOME_PANEL, COLOR_PANEL, COLOR_BORDER); draw_buffer_text( &mut commands, HOME_PANEL.x + 26, HOME_PANEL.y + 22, "Native Shell Apps", COLOR_TEXT, ); draw_buffer_text( &mut commands, 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(&mut commands, button.rect, button.label, hovered); } ShellUiFramePacket::new(commands) } fn append_shell_window_commands( commands: &mut Vec, viewport: Rect, color: Color, title: &str, pointer_x: i32, pointer_y: i32, ) { fill_rect(commands, VIEWPORT, COLOR_BG); draw_panel(commands, viewport, COLOR_PANEL_DARK, COLOR_BORDER); fill_rect( commands, Rect { x: viewport.x + 4, y: viewport.y + 24, w: viewport.w - 8, h: viewport.h - 28 }, color, ); draw_buffer_text(commands, viewport.x + 10, viewport.y + 8, title, COLOR_TEXT); draw_buffer_text( commands, 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 }; fill_rect(commands, CLOSE_BUTTON, close_fill); draw_rect(commands, CLOSE_BUTTON, COLOR_BORDER); draw_buffer_text(commands, CLOSE_BUTTON.x + 5, CLOSE_BUTTON.y + 4, "x", COLOR_TEXT); } fn fill_rect(commands: &mut Vec, rect: Rect, color: Color) { commands.push(GfxUiCommand::FillRect { rect, color }); } fn draw_rect(commands: &mut Vec, rect: Rect, color: Color) { commands.push(GfxUiCommand::DrawLine { x0: rect.x, y0: rect.y, x1: rect.x + rect.w - 1, y1: rect.y, color, }); commands.push(GfxUiCommand::DrawLine { x0: rect.x, y0: rect.y + rect.h - 1, x1: rect.x + rect.w - 1, y1: rect.y + rect.h - 1, color, }); commands.push(GfxUiCommand::DrawLine { x0: rect.x, y0: rect.y, x1: rect.x, y1: rect.y + rect.h - 1, color, }); commands.push(GfxUiCommand::DrawLine { x0: rect.x + rect.w - 1, y0: rect.y, x1: rect.x + rect.w - 1, y1: rect.y + rect.h - 1, color, }); } fn draw_panel(commands: &mut Vec, rect: Rect, fill: Color, border: Color) { fill_rect(commands, rect, fill); draw_rect(commands, rect, border); draw_rect( commands, Rect { x: rect.x + 2, y: rect.y + 2, w: rect.w - 4, h: rect.h - 4 }, COLOR_MUTED, ); } fn draw_button(commands: &mut Vec, 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 }; fill_rect(commands, rect, fill); draw_rect(commands, rect, border); draw_rect( commands, Rect { x: rect.x + 3, y: rect.y + 3, w: rect.w - 6, h: rect.h - 6 }, COLOR_MUTED, ); fill_rect(commands, Rect { x: rect.x + 12, y: rect.y + 12, w: 16, h: 24 }, border); draw_rect(commands, Rect { x: rect.x + 34, y: rect.y + 12, w: 62, h: 24 }, COLOR_PANEL_DARK); draw_buffer_text(commands, rect.x + 40, rect.y + 21, label, COLOR_TEXT); } fn draw_buffer_text(commands: &mut Vec, x: i32, y: i32, text: &str, color: Color) { commands.push(GfxUiCommand::DrawText { x, y, text: text.to_string(), color }); } #[cfg(test)] mod tests { use super::*; #[test] fn shell_a_button_click_emits_launch_action() { assert_eq!( action_for_click(false, HOME_BUTTONS[0].rect.x, HOME_BUTTONS[0].rect.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 hub_starts_with_home_input_disarmed() { let hub = PrometeuHub::new(); assert!(!hub.home_input_armed); } #[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); } #[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 home_render_produces_shell_ui_packet_commands() { let packet = render_home_packet(SHELL_A_BUTTON.x, SHELL_A_BUTTON.y); assert!(matches!(packet.commands.first(), Some(GfxUiCommand::Clear { .. }))); assert!(packet.commands.iter().any(|command| { matches!(command, GfxUiCommand::DrawText { text, .. } if text == "PROMETEU HUB") })); } #[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 )); } }