Files
punktfunk/crates/pf-console-ui/src/anim.rs
T
enricobuehler 180ac3aa61
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 / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (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
decky / build-publish (push) Successful in 25s
flatpak / build-publish (push) Successful in 4m31s
feat(console): full gamepad shell — hosts, PIN pairing, settings, OSK, screen transitions
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>
2026-07-09 08:46:49 +02:00

125 lines
3.8 KiB
Rust

//! The console shell's motion vocabulary. Two kinds of movement, deliberately kept
//! apart: **springs** (`Spring`, wrapping `library::spring_advance`) for anything the
//! user pushes around — cursors, trays, recoil — where velocity must carry across
//! retargets; and **timed progressions** (`Progress` + the easing functions) for
//! fire-and-forget choreography — screen entrances/exits, fades — where a deterministic
//! duration matters more than momentum.
use crate::library::spring_advance;
/// Ease-out cubic — fast start, gentle landing. The screen-transition curve (the WinUI
/// shell's entrance tween uses the same shape).
pub(crate) fn ease_out_cubic(t: f64) -> f64 {
let u = 1.0 - t.clamp(0.0, 1.0);
1.0 - u * u * u
}
/// Exponential approach: move `current` toward `target` with time-constant `tau`
/// seconds. Frame-rate independent, never overshoots — the focus-scale smoothing
/// (SwiftUI's `.smooth(0.18)` reads the same).
pub(crate) fn approach(current: f64, target: f64, dt: f64, tau: f64) -> f64 {
current + (target - current) * (1.0 - (-dt / tau).exp())
}
/// A damped spring with persistent velocity. `k`/`c` choose the feel; see the pairs in
/// [`crate::library`] (cursor chase, boundary bump) and [`TRAY_K`]/[`TRAY_C`] below.
#[derive(Clone, Copy)]
pub(crate) struct Spring {
pub pos: f64,
pub vel: f64,
}
impl Spring {
pub(crate) fn rest(pos: f64) -> Spring {
Spring { pos, vel: 0.0 }
}
pub(crate) fn step(&mut self, target: f64, k: f64, c: f64, dt: f64) {
(self.pos, self.vel) = spring_advance(self.pos, self.vel, target, k, c, dt);
}
/// Snap onto `target` once the motion is imperceptible (stops per-frame damage).
pub(crate) fn settle(&mut self, target: f64, eps_pos: f64, eps_vel: f64) {
if (target - self.pos).abs() < eps_pos && self.vel.abs() < eps_vel {
self.pos = target;
self.vel = 0.0;
}
}
}
/// The keyboard tray's slide (SwiftUI `.spring(response: 0.32, dampingFraction: 0.86)`:
/// k = (2π/response)², c = 2·ζ·√k).
pub(crate) const TRAY_K: f64 = 385.0;
pub(crate) const TRAY_C: f64 = 33.7;
/// A clamped 0→1 timer for fire-and-forget choreography. `advance` returns the RAW
/// progress — callers apply their easing so one Progress can drive several curves.
#[derive(Clone, Copy)]
pub(crate) struct Progress {
t: f64,
duration: f64,
}
impl Progress {
pub(crate) fn new(duration: f64) -> Progress {
Progress { t: 0.0, duration }
}
pub(crate) fn advance(&mut self, dt: f64) -> f64 {
self.t = (self.t + dt / self.duration).min(1.0);
self.t
}
pub(crate) fn value(&self) -> f64 {
self.t
}
pub(crate) fn done(&self) -> bool {
self.t >= 1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ease_out_cubic_shape() {
assert_eq!(ease_out_cubic(0.0), 0.0);
assert_eq!(ease_out_cubic(1.0), 1.0);
assert!(ease_out_cubic(0.5) > 0.5, "front-loaded");
assert_eq!(ease_out_cubic(2.0), 1.0, "clamped");
}
#[test]
fn approach_converges_and_never_overshoots() {
let mut v = 0.0;
for _ in 0..120 {
v = approach(v, 1.0, 1.0 / 60.0, 0.06);
assert!(v <= 1.0);
}
assert!((v - 1.0).abs() < 1e-6);
}
#[test]
fn progress_completes_on_time() {
let mut p = Progress::new(0.3);
let mut steps = 0;
while !p.done() {
p.advance(1.0 / 60.0);
steps += 1;
}
assert!((17..=19).contains(&steps), "{steps}"); // 0.3 s at 60 Hz
}
#[test]
fn spring_settles() {
let mut s = Spring::rest(0.0);
for _ in 0..240 {
s.step(1.0, 200.0, 24.0, 1.0 / 60.0);
s.settle(1.0, 0.001, 0.01);
}
assert_eq!((s.pos, s.vel), (1.0, 0.0));
}
}