Files
qbz/crates/qbz-ui/ui/shell/KioskAlbumGrid.slint
T
vicrodh f1eeaf9126 feat(steam-deck): game mode support (plan PR1-PR5)
- PR1: gamescope detection at boot — X11 backend, scale 1, kiosk
  fullscreen; tray + miniplayer suppressed (click-offset fix).
- PR2: full keyboard navigation for the kiosk (3 zones, focus ring,
  scroll-into-view; typing never navigates).
- PR3: native gamepad via gilrs — D-pad/stick/A/B to synthetic Slint
  keys with hysteresis, repeat and debounce; B suppresses Escape while
  a text field is focused (closes the Steam OSK).
- PR4: Steam OSK via the steam://open/keyboard deeplink on text focus.
- PR5: scripts/qbz-add-to-steam (stdlib-only, 19/19 unit tests) with
  artwork assets + pure-gamepad controller layout.

Spec: qbz-nix-docs/steam-deck/STEAM_DECK_GAME_MODE_PLAN.md

km
2026-07-25 12:04:58 -06:00

84 lines
3.9 KiB
Plaintext

// KioskAlbumGrid — lightweight wrapping grid of KioskCards for the kiosk
// Search/Library album tabs (2.0.2 frente #3). Absolute-positioned cells inside
// a vertical Flickable; card width derives from the grid width so it reflows.
//
// WINDOWED (in-window pattern, mirrors the desktop Carousel): the `for` creates
// only cheap empty positioned slots for every album, but the heavy KioskCard
// (Image + text) mounts ONLY for slots whose band intersects the viewport. A
// plain full grid mounted every card at once and spiked CPU on entry — a Pi
// can't afford that. Binds `[AlbumCardItem]` so artwork pushes land on the cards.
import { Theme } from "../foundation/semantic-colors.slint";
import { AlbumCardItem, KioskNavState } from "../state.slint";
import { KioskCard } from "KioskCard.slint";
export component KioskAlbumGrid inherits Flickable {
in property <[AlbumCardItem]> albums;
in property <int> columns: 6;
in property <length> gap: 16px;
in property <length> pad: 16px;
callback open(string);
property <length> card-w: (self.width - 2 * root.pad - (root.columns - 1) * root.gap) / root.columns;
property <length> card-h: root.card-w + 46px;
property <float> rows: Math.ceil(root.albums.length / root.columns);
viewport-width: self.width;
viewport-height: root.rows * (root.card-h + root.gap) + root.pad;
// ---- Keyboard/gamepad nav (PR 2). The HOSTING VIEW publishes the grid
// geometry (tabs / columns / count) into KioskNavState; the grid only
// mirrors the focus index (ring + scroll-into-view) and answers the
// Enter pulse. The content index space skips `tabs` leading tab entries.
property <int> focused-item: KioskNavState.index - KioskNavState.tabs;
property <bool> item-focused: KioskNavState.nav-active
&& KioskNavState.zone == "content"
&& root.focused-item >= 0
&& root.focused-item < root.albums.length;
// Scroll the focused card's row into view. viewport-y is WRITTEN from
// this callback (plain imperative assignment on a settled layout), never
// read by a binding that feeds layout — the AlbumCollectionView
// "Recursion detected" panic class.
property <length> focus-top: root.pad + Math.floor(root.focused-item / root.columns) * (root.card-h + root.gap);
property <int> nav-index-probe: KioskNavState.index;
changed nav-index-probe => {
if (root.item-focused) {
if (root.focus-top < -self.viewport-y) {
self.viewport-y = -(root.focus-top - 8px);
} else if (root.focus-top + root.card-h > -self.viewport-y + self.height) {
self.viewport-y = -(root.focus-top + root.card-h - self.height + 8px);
}
}
}
// Enter pulse → open the focused album.
property <int> act-probe: KioskNavState.activate-seq;
changed act-probe => {
if (root.item-focused) {
root.open(root.albums[root.focused-item].id);
}
}
Rectangle {
for album[i] in root.albums: slot := Rectangle {
x: root.pad + Math.mod(i, root.columns) * (root.card-w + root.gap);
y: root.pad + Math.floor(i / root.columns) * (root.card-h + root.gap);
width: root.card-w;
height: root.card-h;
// Visible band (content coords) is [-viewport-y, -viewport-y+height];
// 240px margin keeps a card mounted just off-screen for smooth scroll.
property <bool> in-window: (self.y + self.height >= -root.viewport-y - 240px)
&& (self.y <= -root.viewport-y + root.height + 240px);
if slot.in-window: KioskCard {
width: root.card-w;
art-size: root.card-w;
album: album;
nav-focused: root.item-focused && root.focused-item == i;
clicked(id) => {
root.open(id);
}
}
}
}
}