apple / swift (pull_request) Successful in 1m33s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 1m39s
ci / rust-arm64 (pull_request) Successful in 4m7s
android / android (pull_request) Successful in 5m1s
ci / docs-site (pull_request) Successful in 1m47s
ci / bun-nix (pull_request) Successful in 42s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m19s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m23s
ci / rust (pull_request) Successful in 14m32s
Four changes to the client interface, kept together because two of them touch the same rows
and the last is a bug the first would have made far more visible.
A thirteenth `ui_palette` entry, `oled`. The palette table is hand-mirrored in three languages
(`pf-console-ui`'s `library.rs`, `GamepadPalette.swift`, `GamepadPalette.kt`), so it goes into
all three at index 1, directly after the brand default — which keeps `PALETTES[0]` the unknown-id
fallback and keeps the dark-to-pale cycling order intact. What earns the name is arithmetic, not
a darker shade of violet: the ramp's first two stops are literally (0,0,0) and the ground is pure
black, so the shaded half of the field is pixels switched off rather than "very dark grey", and
the calm mix the form screens sit under lifts toward nothing at all. Mean cell luminance is 0.019
against Violet's 0.254. The bright corner keeps a faint indigo-to-violet ember so the backdrop is
still a field with somewhere to go, and that ember carries enough chroma at that luminance
(60 degrees of hue travel across 13 of the 16 cells) to satisfy the existing multi-tone assertion
without adding `oled` to the near-neutral exemption Graphite and Opal take. Each port gains an
`oled_is_actually_black` test that measures the claim — pure-black corner cells, a mean under half
the darkest other field's — rather than restating the table.
A new device key, `gamepad_ui_mode`. The gamepad-UI switch had been deciding two things at once:
whether to offer the controller-optimized interface at all, and that it appears only while a pad
is attached. A user asked for the second half to stop applying. `"connected"` (the default, and
exactly what the lone Bool meant) and `"always"` separate them, surfaced as a "Show it" row
directly under the switch on all five settings surfaces and built only while that switch is on —
a picker whose every option decides nothing is worse than no picker. `GamepadUIEnvironment.isActive`
takes the mode with NO default argument on purpose: a call site that forgot it would silently
strand everyone who chose Always back on "only with a controller", which is the one bug this
parameter exists to make impossible. An unrecognized value waits for a controller, so a mode a
newer client wrote can never trap an older one in a layout it has no way back out of. It stays a
device preference on both platforms, never part of a profile: which interface this device wears
has nothing to do with how a host streams to it.
The smoothness buffer is hidden under Lowest latency, not dimmed. Everywhere else already hid it
— the GTK and WinUI shells, the Apple touch and tvOS screens, the Android touch screen — because
under that intent it names a quantity that does not exist. Two surfaces disagreed: Apple's gamepad
settings screen left the row live and steppable, and the desktop console dimmed it, having no way
to drop a row from a fixed list. That list is now rebuilt each frame through a `row_applies`
filter. The concern about a vanishing row moving everything under the cursor does not apply here
and the new test says why: the row it drops sits directly BELOW the row that drops it, so the only
cursor that can be present when the list shrinks is the one on the intent row, which does not
move. Two latent hazards went with it — `apply_row` had been indexing the row list on the
assumption the cursor is always in range, and nothing re-clamped that cursor when another writer
changed the intent behind the screen's back.
Pale palettes were unreadable on tvOS, reported from the field. `GamepadInk` was never the
problem: it flips correctly for a pale field, it is not platform-gated, and every tvOS gamepad
entry point already published it. The cause is that this app sets `preferredColorScheme` nowhere
and declares no `UIUserInterfaceStyle`, so every SYSTEM-derived colour landing on those screens —
a `.secondary` placeholder, a `.bordered` button's chrome, a NavigationStack title, a material's
frost — resolved against the DEVICE appearance, which the palette cannot reach. On iPhone, iPad
and Mac a great many users sit in Light mode, so under a pale palette those colours came out dark
and the theme looked correct by accident; an Apple TV is Dark essentially always, so every one of
them rendered white on a light field. The mirror image was broken too and had simply never been
reported: a dark palette on a Light-mode iPhone was already drawing dark on dark. The scheme is
now published beside the ink, once, in `GamepadInkModifier`, because the two are halves of one
decision and publishing only the ink silently loses every colour the frameworks draw on the app's
behalf. Two structural amplifiers went with it: `ConsoleGlass` had been scoping the scheme to the
fill inside its `.background {}` on the tvOS and pre-26 branches while the 26 branch put it on the
content, so no console row's own content ever saw it on tvOS; and `LibraryView`'s navigation
chrome and its loading, error and empty states sit above `LibraryCoverflowView` and so were never
inked at all on tvOS and macOS, where that view is presented directly rather than through the
iOS-only `GamepadLibraryScreen` wrapper.
That last one exposed a second tvOS gap worth closing in the same breath: `ui_palette` had no row
in tvOS's ordinary Settings, and the gamepad settings screen that owns it everywhere else needs an
extended-profile controller to open on tvOS. An Apple TV driven by the Siri Remote alone could not
reach the palettes at all, which would now include the OLED one. `SettingsView.tvBody` carries a
Background row.
Verified: pf-console-ui builds, passes `clippy --all-targets -D warnings` and runs 74 tests clean
under linux/amd64 (a Mac `cargo check` of that crate is vacuous — every module is cfg'd to
linux/windows); `cargo fmt --check` clean for it and pf-client-core. Android `:app` runs 80 tests
with 0 failures, including four new `gamepadUiActive` cases and the palette parity table. The
Apple package builds for macOS AND tvOS and its 9 palette/gamepad-UI tests pass — the tvOS
typecheck is possible because the checked-in xcframework already carries a `tvos-arm64` slice. The
tvOS RENDERING fix is compile-verified only; an on-glass Apple TV check under a pale palette is
still owed, and is the one thing here that a build cannot answer.
450 lines
16 KiB
Rust
450 lines
16 KiB
Rust
use super::*;
|
|
use crate::model::WakeStatus;
|
|
use crate::screens::home::HomeScreen;
|
|
use crate::screens::library::LibraryScreen;
|
|
use punktfunk_core::config::GamepadPref;
|
|
|
|
/// Point the settings/known-hosts stores at a throwaway HOME — the settings screen
|
|
/// SAVES on adjust, and a test must never write the developer's real config.
|
|
fn fake_home() {
|
|
use std::sync::OnceLock;
|
|
static HOME: OnceLock<std::path::PathBuf> = OnceLock::new();
|
|
let dir = HOME.get_or_init(|| {
|
|
let dir = std::env::temp_dir().join(format!("pf-console-test-{}", std::process::id()));
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
std::env::set_var("HOME", &dir);
|
|
dir.clone()
|
|
});
|
|
std::env::set_var("HOME", dir);
|
|
}
|
|
|
|
fn hosts() -> Vec<HostRow> {
|
|
let base = HostRow {
|
|
key: String::new(),
|
|
name: String::new(),
|
|
addr: "10.0.0.20".into(),
|
|
port: 9777,
|
|
fp_hex: String::new(),
|
|
paired: false,
|
|
saved: true,
|
|
online: false,
|
|
mgmt_port: 47990,
|
|
can_wake: false,
|
|
last_used: None,
|
|
os: String::new(),
|
|
pin: None,
|
|
bound_profile: None,
|
|
};
|
|
vec![
|
|
HostRow {
|
|
key: "aa11".into(),
|
|
name: "Living Room PC".into(),
|
|
fp_hex: "aa11".into(),
|
|
paired: true,
|
|
online: true,
|
|
last_used: Some(1),
|
|
..base.clone()
|
|
},
|
|
HostRow {
|
|
key: "bb22".into(),
|
|
name: "Office Tower".into(),
|
|
addr: "10.0.0.21".into(),
|
|
fp_hex: "bb22".into(),
|
|
paired: true,
|
|
can_wake: true,
|
|
..base.clone()
|
|
},
|
|
HostRow {
|
|
key: "10.0.0.30:9777".into(),
|
|
name: "steambox".into(),
|
|
addr: "10.0.0.30".into(),
|
|
saved: false,
|
|
online: true,
|
|
..base
|
|
},
|
|
]
|
|
}
|
|
|
|
fn shell(stack: Vec<Screen>) -> (Shell, ConsoleShared, LibraryShared) {
|
|
fake_home();
|
|
let console = ConsoleShared::default();
|
|
console.set_hosts(hosts());
|
|
let library = LibraryShared::default();
|
|
let bus = ConsoleBus::default();
|
|
let shell = Shell::new(
|
|
console.clone(),
|
|
library.clone(),
|
|
bus,
|
|
ConsoleOptions {
|
|
device_name: "deck".into(),
|
|
deck: false,
|
|
},
|
|
stack,
|
|
)
|
|
.unwrap();
|
|
(shell, console, library)
|
|
}
|
|
|
|
/// The shell survives a full navigation lap (a smoke test over every screen's
|
|
/// input handling — no rendering, no GPU).
|
|
#[test]
|
|
fn navigation_lap() {
|
|
let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]);
|
|
s.sync();
|
|
// Home → Settings (X), adjust something, back out.
|
|
s.handle_menu(MenuEvent::Tertiary);
|
|
assert_eq!(s.stack.len(), 2);
|
|
finish_motion(&mut s);
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Down));
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Right));
|
|
s.handle_menu(MenuEvent::Back);
|
|
finish_motion(&mut s);
|
|
assert_eq!(s.stack.len(), 1);
|
|
// Home → Library on the paired host (Y), then back.
|
|
s.handle_menu(MenuEvent::Secondary);
|
|
assert_eq!(s.stack.len(), 2);
|
|
finish_motion(&mut s);
|
|
s.handle_menu(MenuEvent::Back);
|
|
finish_motion(&mut s);
|
|
assert_eq!(s.stack.len(), 1);
|
|
// B at the root quits.
|
|
s.handle_menu(MenuEvent::Back);
|
|
assert!(matches!(s.take_action(), Some(OverlayAction::Quit)));
|
|
}
|
|
|
|
#[test]
|
|
fn connect_flow_raises_launch_and_cancel() {
|
|
let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]);
|
|
s.sync();
|
|
s.handle_menu(MenuEvent::Confirm); // paired+online host focused first
|
|
assert!(matches!(
|
|
s.take_action(),
|
|
Some(OverlayAction::Launch { launch: None, .. })
|
|
));
|
|
assert!(s.connecting.is_some());
|
|
// While connecting: B cancels exactly once.
|
|
s.handle_menu(MenuEvent::Back);
|
|
assert!(matches!(
|
|
s.take_action(),
|
|
Some(OverlayAction::CancelConnect)
|
|
));
|
|
s.handle_menu(MenuEvent::Back);
|
|
assert!(s.take_action().is_none(), "cancel is idempotent");
|
|
// The canceled dial ends silently.
|
|
s.session_ended(None);
|
|
assert!(s.connecting.is_none());
|
|
}
|
|
|
|
fn finish_motion(s: &mut Shell) {
|
|
// Transitions block input; tests fast-forward them.
|
|
s.motion = Motion::None;
|
|
}
|
|
|
|
#[test]
|
|
fn wake_gates_input_in_the_same_press() {
|
|
let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]);
|
|
s.sync();
|
|
// Focus "Office Tower" (offline + wakeable), then A: the wake starts.
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Right));
|
|
s.handle_menu(MenuEvent::Confirm);
|
|
let w = s
|
|
.wake
|
|
.as_ref()
|
|
.expect("Waking card raised in the SAME call as the A press");
|
|
assert_eq!(w.name, "Office Tower");
|
|
assert!(!w.online);
|
|
// The very next input is modal-gated — the cursor can't drift onto Add Host —
|
|
// and sync (which runs first in handle_menu) must not clear the placeholder
|
|
// before the service thread reports its first real status.
|
|
assert!(s.handle_menu(MenuEvent::Move(MenuDir::Right)).is_none());
|
|
assert!(
|
|
s.wake.is_some(),
|
|
"optimistic card survived a sync with no service status"
|
|
);
|
|
// B cancels: the gate releases and navigation works again.
|
|
s.handle_menu(MenuEvent::Back);
|
|
assert!(s.wake.is_none());
|
|
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.
|
|
/// Tab / Shift+Tab change section. The strip shipped on the shoulder buttons and
|
|
/// PgUp/PgDn only, and the legend names PgUp/PgDn solely when NO pad is attached — so with
|
|
/// a controller plugged in a keyboard user had no way in, and no way to find one.
|
|
#[test]
|
|
fn tab_and_shift_tab_change_section() {
|
|
use sdl3::keyboard::Scancode;
|
|
let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]);
|
|
s.handle_menu(MenuEvent::Tertiary); // X → Settings
|
|
s.motion = Motion::None; // skip the push transition, which drops input
|
|
let tab = |s: &Shell| match s.stack.last() {
|
|
Some(Screen::Settings(st)) => st.tab_for_test(),
|
|
_ => panic!("the settings screen is on top"),
|
|
};
|
|
assert_eq!(tab(&s), 0);
|
|
assert!(s.key(Scancode::Tab, false, false), "Tab is consumed");
|
|
assert_eq!(tab(&s), 1, "Tab goes forward");
|
|
assert!(s.key(Scancode::Tab, true, false));
|
|
assert_eq!(tab(&s), 0, "Shift+Tab goes back");
|
|
// …and it wraps backwards off the first tab, exactly as the shoulders do.
|
|
s.key(Scancode::Tab, true, false);
|
|
assert_eq!(tab(&s), crate::screens::settings::TAB_COUNT - 1);
|
|
// A key repeat must not run through the strip a section per frame held.
|
|
let before = tab(&s);
|
|
s.key(Scancode::Tab, false, true);
|
|
assert_eq!(tab(&s), before, "held Tab doesn't skip sections");
|
|
}
|
|
|
|
/// A right-click is Back on every screen, so a pointer always has a way out.
|
|
#[test]
|
|
fn a_secondary_press_goes_back() {
|
|
let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]);
|
|
s.handle_menu(MenuEvent::Tertiary); // X → Settings
|
|
s.motion = Motion::None;
|
|
assert_eq!(s.stack.len(), 2);
|
|
assert!(s.pointer(crate::pointer::Pointer {
|
|
x: 10.0,
|
|
y: 10.0,
|
|
kind: crate::pointer::PointerKind::Back,
|
|
}));
|
|
// The pop runs through the same transition a B press does.
|
|
assert!(matches!(s.motion, Motion::Pop { .. }));
|
|
}
|
|
|
|
/// Up on a saved tile opens that host's menu; a discovered-but-unsaved one has none.
|
|
#[test]
|
|
fn up_opens_host_options_for_saved_tiles_only() {
|
|
let (mut s, _console, _library) = shell(vec![Screen::Home(HomeScreen::new())]);
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Up));
|
|
assert!(
|
|
matches!(s.stack.last(), Some(Screen::HostOptions(_))),
|
|
"the first tile is a saved host"
|
|
);
|
|
s.motion = Motion::None;
|
|
s.handle_menu(MenuEvent::Back);
|
|
s.motion = Motion::None;
|
|
// The third fixture host is discovered-only (`saved: false`).
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Right));
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Right));
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Up));
|
|
assert!(
|
|
matches!(s.stack.last(), Some(Screen::Home(_))),
|
|
"an unsaved host has nothing to edit or forget"
|
|
);
|
|
}
|
|
|
|
#[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.
|
|
#[test]
|
|
#[ignore]
|
|
fn dump_console_screens() {
|
|
let dir = std::env::var("PF_CONSOLE_DUMP").expect("set PF_CONSOLE_DUMP to an output dir");
|
|
let fonts = crate::theme::build_fonts().unwrap();
|
|
let (w, h) = (1280, 800);
|
|
let pads: Vec<PadInfo> = Vec::new();
|
|
let dump = |shell: &mut Shell, frames: usize, sleep_ms: u64, name: &str, pad: bool| {
|
|
let mut surface = skia_safe::surfaces::raster_n32_premul((w, h)).unwrap();
|
|
for _ in 0..frames {
|
|
shell.render(
|
|
surface.canvas(),
|
|
w as u32,
|
|
h as u32,
|
|
&fonts,
|
|
pad.then_some("Xbox Wireless Controller"),
|
|
pad.then_some(GamepadPref::Xbox360),
|
|
&pads,
|
|
);
|
|
std::thread::sleep(std::time::Duration::from_millis(sleep_ms));
|
|
}
|
|
let png = surface
|
|
.image_snapshot()
|
|
.encode(None, skia_safe::EncodedImageFormat::PNG, 100)
|
|
.unwrap();
|
|
std::fs::write(format!("{dir}/{name}.png"), png.as_bytes()).unwrap();
|
|
};
|
|
|
|
// Home, settled, with a pad (Letters glyphs).
|
|
let (mut s, console, library) = shell(vec![Screen::Home(HomeScreen::new())]);
|
|
dump(&mut s, 40, 8, "01-home", true);
|
|
|
|
// The host menu — Up on the focused saved tile. The home frame above carries the new
|
|
// ▲ Options hint that leads here, so the two are worth eyeballing together.
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Up));
|
|
dump(&mut s, 40, 8, "01b-host-options", true);
|
|
s.handle_menu(MenuEvent::Back);
|
|
dump(&mut s, 20, 8, "_settle0", true);
|
|
|
|
// Mid-push into Settings (the transition still): a couple of fast frames land
|
|
// the capture around p ≈ 0.4 — both layers visible.
|
|
s.handle_menu(MenuEvent::Tertiary);
|
|
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 these
|
|
// frames show the strip mid-list AND the palette picker. Palettes are set directly rather
|
|
// than by counting Confirm presses, so reordering the table can't silently shoot the wrong
|
|
// one. Each is a whole LOOK, not just a backdrop: accent, ink and scrim move together, so
|
|
// the pale ones must be eyeballed with dark text on them.
|
|
for _ in 0..5 {
|
|
s.handle_menu(MenuEvent::JumpForward);
|
|
}
|
|
for id in ["violet", "oled", "ember", "abyss", "holo", "sunset", "mint"] {
|
|
s.settings.ui_palette = id.to_string();
|
|
dump(&mut s, 40, 8, &format!("03-settings-{id}"), true);
|
|
}
|
|
// Back to the first tab so the later scenes look like they always did.
|
|
for _ in 0..5 {
|
|
s.handle_menu(MenuEvent::JumpBack);
|
|
}
|
|
// …and the LAUNCHER at full contrast under a few of them — the backdrop's loudest form,
|
|
// and the one the palettes are really chosen by.
|
|
s.handle_menu(MenuEvent::Back);
|
|
dump(&mut s, 20, 8, "_settle", true);
|
|
for id in ["nebula", "sunset", "holo"] {
|
|
s.settings.ui_palette = id.to_string();
|
|
dump(&mut s, 40, 8, &format!("01-home-{id}"), true);
|
|
}
|
|
s.settings.ui_palette = "violet".to_string();
|
|
dump(&mut s, 20, 8, "_settle2", true);
|
|
s.handle_menu(MenuEvent::Tertiary); // back into Settings for the scenes below
|
|
dump(&mut s, 20, 8, "_settle3", true);
|
|
|
|
// Add Host with the keyboard tray up (keyboard glyph style: no pad).
|
|
s.handle_menu(MenuEvent::Back);
|
|
dump(&mut s, 40, 8, "_back", true);
|
|
for _ in 0..3 {
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Right));
|
|
}
|
|
s.handle_menu(MenuEvent::Confirm); // Add Host screen
|
|
dump(&mut s, 40, 8, "04-addhost", false);
|
|
s.handle_menu(MenuEvent::Confirm); // open the Name keyboard
|
|
for ev in [
|
|
MenuEvent::Move(MenuDir::Down),
|
|
MenuEvent::Confirm,
|
|
MenuEvent::Confirm,
|
|
] {
|
|
s.handle_menu(ev);
|
|
}
|
|
dump(&mut s, 40, 8, "05-addhost-keyboard", false);
|
|
|
|
// Pair (focused on the unpaired discovered host).
|
|
s.handle_menu(MenuEvent::Back); // close keyboard
|
|
s.handle_menu(MenuEvent::Back); // leave add-host
|
|
dump(&mut s, 40, 8, "_back2", true);
|
|
s.handle_menu(MenuEvent::Move(MenuDir::Left)); // onto "steambox"
|
|
s.handle_menu(MenuEvent::Confirm);
|
|
dump(&mut s, 40, 8, "06-pair", true);
|
|
|
|
// Library with placeholder posters.
|
|
library.set_games(
|
|
[
|
|
"Hades II",
|
|
"Elden Ring",
|
|
"Hollow Knight",
|
|
"Baldur's Gate 3",
|
|
"Celeste",
|
|
"Deep Rock Galactic",
|
|
"Portal 2",
|
|
]
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, t)| crate::library::LibraryGame {
|
|
id: format!("steam:{i}"),
|
|
title: (*t).to_string(),
|
|
store: "steam".into(),
|
|
launcher: false,
|
|
})
|
|
.collect(),
|
|
);
|
|
let (mut s2, _c2, _l2) = {
|
|
let console2 = ConsoleShared::default();
|
|
console2.set_hosts(hosts());
|
|
let bus = ConsoleBus::default();
|
|
let sh = Shell::new(
|
|
console2.clone(),
|
|
library.clone(),
|
|
bus,
|
|
ConsoleOptions {
|
|
device_name: "deck".into(),
|
|
deck: false,
|
|
},
|
|
vec![
|
|
Screen::Home(HomeScreen::new()),
|
|
Screen::Library(LibraryScreen::new(&hosts()[0])),
|
|
],
|
|
)
|
|
.unwrap();
|
|
(sh, console2, library.clone())
|
|
};
|
|
s2.handle_menu(MenuEvent::Move(MenuDir::Right));
|
|
s2.handle_menu(MenuEvent::Move(MenuDir::Right));
|
|
dump(&mut s2, 40, 8, "07-library", true);
|
|
|
|
// The wake and connecting overlays + a toast.
|
|
console.set_wake(Some(WakeStatus {
|
|
key: "bb22".into(),
|
|
name: "Office Tower".into(),
|
|
seconds: 12,
|
|
timed_out: false,
|
|
online: false,
|
|
then_connect: true,
|
|
}));
|
|
dump(&mut s, 10, 8, "08-waking", true);
|
|
console.set_wake(Some(WakeStatus {
|
|
key: "bb22".into(),
|
|
name: "Office Tower".into(),
|
|
seconds: 90,
|
|
timed_out: true,
|
|
online: false,
|
|
then_connect: true,
|
|
}));
|
|
dump(&mut s, 10, 8, "08b-wake-timed-out", true);
|
|
console.set_wake(None);
|
|
s.set_connecting(Some("Elden Ring".into()));
|
|
dump(&mut s, 10, 8, "09-connecting", true);
|
|
s.set_connecting(None);
|
|
s.session_failed("Connection timed out");
|
|
dump(&mut s, 10, 8, "10-toast", true);
|
|
}
|