feat(clients/gamepad-ui): section tabs, background palettes, and a backdrop that moves everywhere
ci / web (pull_request) Successful in 1m17s
ci / docs-site (pull_request) Successful in 1m42s
ci / rust-arm64 (pull_request) Successful in 2m36s
android / android (pull_request) Successful in 3m33s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 8m37s
ci / rust (pull_request) Successful in 8m58s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 3m47s
apple / swift (pull_request) Successful in 1m29s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m17s
ci / docs-site (pull_request) Successful in 1m42s
ci / rust-arm64 (pull_request) Successful in 2m36s
android / android (pull_request) Successful in 3m33s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 8m37s
ci / rust (pull_request) Successful in 8m58s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 3m47s
apple / swift (pull_request) Successful in 1m29s
apple / screenshots (pull_request) Skipped
The console settings were one 30-row scroll, which on a Deck meant thumbing past Video and Audio to reach the pad settings. They are now split across sections — Stream · Video · Audio · Controller · Interface · Profiles, plus Input on the desktop console, which alone carries the touch/mouse rows. L1/R1 walks them, each section remembers where its cursor was, and the names are the same word on every client so a setting is where you looked for it last. Shoulders are not the only route, because a D-pad remote hasn't got any: on Android, Up from the first row moves onto the strip (left/right walks sections there, A drops back in), and on tvOS the pills are focusable, so the focus engine handles it — a Siri Remote has no extended gamepad profile and never reaches the input poll at all. The desktop console needs neither; PageUp and PageDown already map to the same events. New "Background" row, six palettes: Violet (the brand default), Tide, Forest, Ember, Rose, Graphite. A palette is a hue rotation plus a saturation scale over the ONE colour field each client already draws, so every palette inherits its structure and Violet is the identity transform — existing installs see exactly what they see today. The maths is ported three times (Rust/Swift/Kotlin) under one shared `ui_palette` key, with the same assertions pinned in each language. It is presentation only, so it is a device preference and never part of a profile. The form screens no longer have a backdrop of their own. Settings, add-host and pair used to sit on a still gradient; they now wear the same living field at a calm mix — pools dimmed onto the palette's own corner colour, vignette halved so rows that run to the edges don't get crushed. On the desktop console that collapsed the old aurora-over-static crossfade into one shader pass with a chased uniform. Motion speed is identical in both modes on purpose: changing it would make the field jump mid-transition. Nothing in the gamepad UI is backed by a static image now, and Reduce Motion (Apple) / "remove animations" (Android) still freeze it. Also: the settings screen had no raster coverage at all — the eyeball dump is `#[ignore]`d — so a new test draws every tab, and the Android screenshot set gains a console-settings scene. Both earned their keep immediately: the renders showed the extra hint pushing "Done" off a 360 dp phone (the legend scrolls now, and the Section cell only appears where shoulders exist) and the form backdrop crushing its own edges.
This commit is contained in:
@@ -166,7 +166,7 @@ impl Shell {
|
||||
canvas.save_layer_alpha_f(None, appear as f32);
|
||||
// Opaque aurora — the same living backdrop the home wears, so the takeover reads as the
|
||||
// console taking over rather than a card popping up.
|
||||
self.draw_aurora(canvas, w, h, t);
|
||||
self.draw_aurora(canvas, w, h, t, 0.0);
|
||||
// A soft pool of shade under the centre seats the white text against a bright aurora.
|
||||
let mut vignette = Paint::default();
|
||||
vignette.set_shader(gradient_shader::radial(
|
||||
|
||||
@@ -8,7 +8,7 @@ use crate::screens::{Bg, Ctx, Screen};
|
||||
use crate::theme::{white, Fonts, PanelStroke, W, WHITE};
|
||||
use pf_client_core::gamepad::PadInfo;
|
||||
use pf_client_core::trust;
|
||||
use skia_safe::{Canvas, Color4f, Rect};
|
||||
use skia_safe::{Canvas, Rect};
|
||||
use std::time::Instant;
|
||||
|
||||
use super::{Motion, Shell, BOTTOM_BAND, TOP_BAND};
|
||||
@@ -67,7 +67,9 @@ impl Shell {
|
||||
}
|
||||
};
|
||||
|
||||
// Backdrop crossfade follows the top screen.
|
||||
// The backdrop settles into (or out of) calm with the screen transition. It is the
|
||||
// SAME living field either way — a form screen quiets it, it doesn't replace it —
|
||||
// so this is one shader pass with a chased uniform, not two stacked backdrops.
|
||||
let bg_target = match self.stack.last().expect("non-empty").background() {
|
||||
Bg::Aurora => 0.0,
|
||||
Bg::Form => 1.0,
|
||||
@@ -76,16 +78,7 @@ impl Shell {
|
||||
if (self.bg_mix - bg_target).abs() < 0.005 {
|
||||
self.bg_mix = bg_target;
|
||||
}
|
||||
if self.bg_mix < 1.0 {
|
||||
self.draw_aurora(canvas, w, h, t);
|
||||
} else {
|
||||
canvas.clear(Color4f::new(0.0, 0.0, 0.0, 1.0));
|
||||
}
|
||||
if self.bg_mix > 0.0 {
|
||||
canvas.save_layer_alpha_f(None, self.bg_mix as f32);
|
||||
crate::theme::draw_form_background(canvas, w, h);
|
||||
canvas.restore();
|
||||
}
|
||||
self.draw_aurora(canvas, w, h, t, self.bg_mix);
|
||||
|
||||
// The screens, through the transition choreography.
|
||||
let content = Rect::from_ltrb(
|
||||
|
||||
@@ -167,6 +167,47 @@ fn wake_gates_input_in_the_same_press() {
|
||||
assert!(s.handle_menu(MenuEvent::Move(MenuDir::Left)).is_some());
|
||||
}
|
||||
|
||||
/// Every settings tab actually RASTERS. The eyeball dump below is `#[ignore]`d, so without
|
||||
/// this nothing in the normal gate ever ran the tab strip's layout arithmetic or a settings
|
||||
/// screen's rows — a bad index there would only surface on a Deck. CPU raster: the SkSL
|
||||
/// backdrop, the layers and the text all run without a GPU.
|
||||
#[test]
|
||||
fn every_settings_tab_rasters() {
|
||||
let fonts = crate::theme::build_fonts().unwrap();
|
||||
let (w, h) = (1280u32, 800u32);
|
||||
let pads: Vec<PadInfo> = Vec::new();
|
||||
let mut surface = skia_safe::surfaces::raster_n32_premul((w as i32, h as i32)).unwrap();
|
||||
let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]);
|
||||
s.handle_menu(MenuEvent::Tertiary); // X → Settings
|
||||
|
||||
let mut frame = |s: &mut Shell| {
|
||||
s.render(
|
||||
surface.canvas(),
|
||||
w,
|
||||
h,
|
||||
&fonts,
|
||||
Some("Xbox Wireless Controller"),
|
||||
Some(GamepadPref::Xbox360),
|
||||
&pads,
|
||||
);
|
||||
};
|
||||
// One lap of the strip — R1 wraps back to where it started. Every tab's rows fit on an
|
||||
// 800-tall window at once, so ONE frame per tab draws all of them; the cursor is walked to
|
||||
// the end first (input only, no render) so the focused and unfocused row paths both run.
|
||||
// Deliberately frugal: a full-screen SkSL field on the CPU costs the better part of a second
|
||||
// per frame in a debug build, and this test's job is to catch a panic, not to look pretty.
|
||||
for _ in 0..crate::screens::settings::TAB_COUNT {
|
||||
for _ in 0..12 {
|
||||
s.handle_menu(MenuEvent::Move(MenuDir::Down));
|
||||
}
|
||||
frame(&mut s);
|
||||
s.handle_menu(MenuEvent::JumpForward);
|
||||
}
|
||||
// A narrow window is the case the strip has to shrink for (the pills are laid out from
|
||||
// measured text, so a too-small width must clamp rather than lay out off-screen).
|
||||
s.render(surface.canvas(), 640, 400, &fonts, None, None, &pads);
|
||||
}
|
||||
|
||||
/// Render every console scene to PNGs for the eyeball pass (ignored; run with
|
||||
/// `PF_CONSOLE_DUMP=<dir> cargo test -p pf-console-ui --release -- --ignored dump`).
|
||||
/// CPU raster — the SkSL aurora, layers and text all run without a GPU.
|
||||
@@ -208,6 +249,26 @@ fn dump_console_screens() {
|
||||
dump(&mut s, 3, 25, "02-transition", true);
|
||||
dump(&mut s, 40, 8, "03-settings", true);
|
||||
|
||||
// The Interface tab (5 shoulder presses along) leads with the Background row, so this frame
|
||||
// shows both the strip mid-list and the palette picker…
|
||||
for _ in 0..5 {
|
||||
s.handle_menu(MenuEvent::JumpForward);
|
||||
}
|
||||
dump(&mut s, 40, 8, "03b-settings-interface", true);
|
||||
// …and cycling it three times lands on Ember, which is the whole point: the CALM backdrop
|
||||
// behind these rows recolours live.
|
||||
for _ in 0..3 {
|
||||
s.handle_menu(MenuEvent::Confirm);
|
||||
}
|
||||
dump(&mut s, 40, 8, "03c-settings-ember", true);
|
||||
// Back to the brand default and the first tab so the later scenes look like they always did.
|
||||
for _ in 0..3 {
|
||||
s.handle_menu(MenuEvent::Confirm);
|
||||
}
|
||||
for _ in 0..5 {
|
||||
s.handle_menu(MenuEvent::JumpBack);
|
||||
}
|
||||
|
||||
// Add Host with the keyboard tray up (keyboard glyph style: no pad).
|
||||
s.handle_menu(MenuEvent::Back);
|
||||
dump(&mut s, 40, 8, "_back", true);
|
||||
|
||||
Reference in New Issue
Block a user