2026-06-15 05:59:06 +01:00

424 lines
13 KiB
Rust

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<CrashReport>,
pub action: Option<SystemProfileAction>,
}
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<SystemProfileAction> {
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<SystemProfileAction> {
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<GfxUiCommand>,
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<GfxUiCommand>, rect: Rect, color: Color) {
commands.push(GfxUiCommand::FillRect { rect, color });
}
fn draw_rect(commands: &mut Vec<GfxUiCommand>, 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<GfxUiCommand>, 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<GfxUiCommand>, 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<GfxUiCommand>, 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
));
}
}