feat(console): full gamepad shell — hosts, PIN pairing, settings, OSK, screen transitions
apple / swift (push) Successful in 1m6s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
audit / bun-audit (push) Has been cancelled
audit / cargo-audit (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
decky / build-publish (push) Successful in 25s
flatpak / build-publish (push) Successful in 4m31s
apple / swift (push) Successful in 1m6s
android / android (push) Has been cancelled
apple / screenshots (push) Has been cancelled
arch / build-publish (push) Has been cancelled
audit / bun-audit (push) Has been cancelled
audit / cargo-audit (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / web (push) Has been cancelled
ci / docs-site (push) Has been cancelled
ci / bench (push) Has been cancelled
deb / build-publish (push) Has been cancelled
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Has been cancelled
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Has been cancelled
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Has been cancelled
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Has been cancelled
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled
release / apple (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
decky / build-publish (push) Successful in 25s
flatpak / build-publish (push) Successful in 4m31s
The Skia console UI grows from the single library coverflow into a complete couch shell (Apple gamepad-UI parity), so a Deck/gamescope session is self-sufficient end to end: - Home: host carousel (saved + discovered + Add Host tile) with presence pips, paired locks, wake & connect; A/Y/X/B per the Apple launcher. - In-console SPAKE2 PIN pairing (previously needed the Decky plugin or a desktop), add-host with a controller keyboard, couch settings over the shared Settings store, wake-on-LAN overlay with retry, cancelable dials. - Shell chrome: screen stack with push/pop entrance/exit choreography (slide + fade + recede; aurora↔form backdrop crossfade), per-pad button glyphs (PS shapes vs ABXY, keycaps with no pad), menu haptics, toasts, embedded Geist typography, in-stream start banner. - Steam Deck: our OSK never draws — fields start SDL text input (the new Overlay::text_input_active hook) and Steam's keyboard types; a hint chip points at STEAM + X. Other Linux gets the full gamepad keyboard tray. - punktfunk-session: bare --browse opens Home; --browse host opens that host's library with Home behind it (Decky launches keep working, B now pops instead of quitting). Service threads run discovery, 10 s probes, pairing, wake loops, fetches, and known-hosts persistence. - Presenter contract: Launch actions carry the host, CancelConnect never engages a won race, pad kind/list ride FrameCtx, menu events flow while dialing so B can cancel. Every screen renders to PNG on CPU raster for review (PF_CONSOLE_DUMP=<dir> cargo test -p pf-console-ui -- --ignored dump). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
//! The console's screens and their shared contract. Each screen owns its focus state
|
||||
//! and rendering; the [`crate::shell::Shell`] owns the stack, the transitions, the
|
||||
//! chrome, and the overlays — a screen never draws its own background or hint bar, so
|
||||
//! every screen animates and reads identically.
|
||||
|
||||
pub(crate) mod add_host;
|
||||
pub(crate) mod home;
|
||||
pub(crate) mod library;
|
||||
pub(crate) mod pair;
|
||||
pub(crate) mod settings;
|
||||
|
||||
use crate::glyphs::Hint;
|
||||
use crate::library::LibraryShared;
|
||||
use crate::model::{ConsoleCmd, HostRow};
|
||||
use crate::theme::Fonts;
|
||||
use pf_client_core::gamepad::{MenuEvent, MenuPulse};
|
||||
use pf_client_core::{gamepad::PadInfo, trust};
|
||||
use skia_safe::{Canvas, Rect};
|
||||
|
||||
/// What a screen draws over (the shell crossfades between them on push/pop).
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum Bg {
|
||||
/// The living mesh aurora (home, library).
|
||||
Aurora,
|
||||
/// The quiet indigo form backdrop (settings, add-host, pair).
|
||||
Form,
|
||||
}
|
||||
|
||||
/// Everything a screen may read while handling input or rendering. Settings are
|
||||
/// mutable — the settings screen edits and persists them in place.
|
||||
pub(crate) struct Ctx<'a> {
|
||||
pub hosts: &'a [HostRow],
|
||||
/// The one live library model slot (the screen on top of the stack owns it).
|
||||
pub library: &'a LibraryShared,
|
||||
pub settings: &'a mut trust::Settings,
|
||||
pub pads: &'a [PadInfo],
|
||||
/// Steam Deck: never draw our keyboard — Steam's types via SDL text input.
|
||||
pub deck: bool,
|
||||
/// The name the HOST stores this client under when pairing (the machine's
|
||||
/// hostname, resolved by the binary).
|
||||
pub device_name: &'a str,
|
||||
/// The shell clock, seconds (spinners, pulses).
|
||||
pub t: f64,
|
||||
}
|
||||
|
||||
/// A host a screen wants to start a session on (the shell turns this into an
|
||||
/// `OverlayAction::Launch` + the connecting overlay).
|
||||
pub(crate) struct ConnectIntent {
|
||||
pub addr: String,
|
||||
pub port: u16,
|
||||
pub fp_hex: String,
|
||||
/// Library title id (`None` streams the desktop).
|
||||
pub launch: Option<String>,
|
||||
/// What the connecting card says (host or game title).
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
pub(crate) enum Nav {
|
||||
Push(Box<Screen>),
|
||||
/// Pop this screen; popping the root quits the console.
|
||||
Pop,
|
||||
}
|
||||
|
||||
/// Everything a screen's input handling may ask of the shell, collected per event and
|
||||
/// applied AFTER the dispatch (no re-entrant stack mutation).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Outbox {
|
||||
pub nav: Option<Nav>,
|
||||
pub connect: Option<ConnectIntent>,
|
||||
pub cmds: Vec<ConsoleCmd>,
|
||||
pub toast: Option<String>,
|
||||
}
|
||||
|
||||
impl Outbox {
|
||||
pub(crate) fn push(&mut self, screen: Screen) {
|
||||
self.nav = Some(Nav::Push(Box::new(screen)));
|
||||
}
|
||||
|
||||
pub(crate) fn pop(&mut self) {
|
||||
self.nav = Some(Nav::Pop);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Screen {
|
||||
Home(home::HomeScreen),
|
||||
Library(library::LibraryScreen),
|
||||
Settings(settings::SettingsScreen),
|
||||
AddHost(add_host::AddHostScreen),
|
||||
Pair(pair::PairScreen),
|
||||
}
|
||||
|
||||
impl Screen {
|
||||
pub(crate) fn menu(
|
||||
&mut self,
|
||||
ev: MenuEvent,
|
||||
ctx: &mut Ctx,
|
||||
fx: &mut Outbox,
|
||||
) -> Option<MenuPulse> {
|
||||
match self {
|
||||
Screen::Home(s) => s.menu(ev, ctx, fx),
|
||||
Screen::Library(s) => s.menu(ev, ctx, fx),
|
||||
Screen::Settings(s) => s.menu(ev, ctx, fx),
|
||||
Screen::AddHost(s) => s.menu(ev, ctx, fx),
|
||||
Screen::Pair(s) => s.menu(ev, ctx, fx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Committed text (SDL `TextInput` — hardware keyboards everywhere, Steam's
|
||||
/// keyboard under gamescope). Only the editing screens consume it.
|
||||
pub(crate) fn text_input(&mut self, text: &str) {
|
||||
match self {
|
||||
Screen::AddHost(s) => s.text_input(text),
|
||||
Screen::Pair(s) => s.text_input(text),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Raw key edits while a field is editing (Backspace repeats, Return = done).
|
||||
/// Returns true when consumed.
|
||||
pub(crate) fn edit_key(&mut self, sc: sdl3::keyboard::Scancode) -> bool {
|
||||
match self {
|
||||
Screen::AddHost(s) => s.edit_key(sc),
|
||||
Screen::Pair(s) => s.edit_key(sc),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// A text field is being edited — the run loop keeps SDL text input started.
|
||||
pub(crate) fn editing(&self) -> bool {
|
||||
match self {
|
||||
Screen::AddHost(s) => s.editing(),
|
||||
Screen::Pair(s) => s.editing(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn background(&self) -> Bg {
|
||||
match self {
|
||||
Screen::Home(_) | Screen::Library(_) => Bg::Aurora,
|
||||
_ => Bg::Form,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn title(&self, _ctx: &Ctx) -> String {
|
||||
match self {
|
||||
Screen::Home(_) => "Select a Host".into(),
|
||||
Screen::Library(s) => s.host_name().to_string(),
|
||||
Screen::Settings(_) => "Settings".into(),
|
||||
Screen::AddHost(_) => "Add Host".into(),
|
||||
Screen::Pair(s) => format!("Pair with {}", s.host_name()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hints(&self, ctx: &Ctx) -> Vec<Hint> {
|
||||
match self {
|
||||
Screen::Home(s) => s.hints(ctx),
|
||||
Screen::Library(s) => s.hints(ctx),
|
||||
Screen::Settings(s) => s.hints(ctx),
|
||||
Screen::AddHost(s) => s.hints(ctx),
|
||||
Screen::Pair(s) => s.hints(ctx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render the screen's content into `rect` (between the title bar and hint bar).
|
||||
/// Backgrounds and chrome are the shell's.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn render(
|
||||
&mut self,
|
||||
canvas: &Canvas,
|
||||
rect: Rect,
|
||||
k: f64,
|
||||
dt: f64,
|
||||
fonts: &Fonts,
|
||||
ctx: &mut Ctx,
|
||||
) {
|
||||
match self {
|
||||
Screen::Home(s) => s.render(canvas, rect, k, dt, fonts, ctx),
|
||||
Screen::Library(s) => s.render(canvas, rect, k, dt, fonts, ctx),
|
||||
Screen::Settings(s) => s.render(canvas, rect, k, dt, fonts, ctx),
|
||||
Screen::AddHost(s) => s.render(canvas, rect, k, dt, fonts, ctx),
|
||||
Screen::Pair(s) => s.render(canvas, rect, k, dt, fonts, ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user