App and example changes, with the apps that moved to the Stage repository (apps/vj, apps/asset-ui) removed here. Covers work ebf442a94..2b1a41df4 for apps examples: - wm: the Android super-app compiles tiles on the phone against the host engine - cargo-makepad: the super-app APK is packed by `android dyn-pack`, in Rust - widgets, storybook: the widget catalogue app, some eighty new widgets, a theme store with mixable style sheets, and the rule that a press belongs to - wm, widgets: the style tween starts with all eight weights, and the new style sheet takes the last number instead of macOS's - platform: the Android desk swipes at 120 Hz — offscreen render passes and framebuffers cached, SVG meshes kept, font families that complete - flow: the flow-ui app and the flowgraph canvas return to the workspace, with staged progress in libs/flow - app_main!: only ship the fonts an app declares (#1247) - platform, widgets, wm: every Vulkan pass renders through a negative-height viewport again, so a Wayland or X11 window stands upright, and no capture - calendar: its own colour contrast is named explicitly, now that the theme store exports one under the same name through the widgets glob - platform, wm, director: a hosted app's software frame arrives as a top-left texture and the window manager and the director carry no flip term at al - wm: the desktop style menu lists each style once - cef, video, platform: what Stage's browser needs from the shared layers - platform, wm: work builds again on iOS, tvOS and wasm, and the window manager is warning-free on Windows - ci: tools/ci, a Splash runtime that watches a branch and runs every ci.splash it finds - ci: the terminal gets its pty helper, director is a desktop tool, and a refused input is asked again - apps: what the CI box found - Remove Flow, asset and VJ applications moved into Stage - ci: a quiet header, a build that visibly moves, and scripts that leave the machine alone - tests: the workspace suite, run as a whole for the first time, passes outside the example UI tests - examples: the UI tests pass, for the reasons they failed - tests: the last three failures of the CI box's night - tools: makepad-screen is makepad-agents, and its binary is `agents` - tests: the three binaries over ten seconds in release come under it - wm: the shell menu says what it did, and the CI waits for that - mail: the phone layout lines up - clock: the phone layout fits both orientations - calendar, reminders, calculator, sheets: the phone layouts fit their space - platform, widgets, wm: a touch that is taken away is cancelled, never released - weather, notes, finance, photos, route: the phone layouts tidy up - files: the phone layout tidies up - wm: the phone shell's surfaces match the app and the grid - wm on Android: every app is its own process - clock: a cancelled release neither opens an alarm nor switches tab - platform, wm: hosted apps ask the WM for what they cannot do themselves - wm on Android: apps build on the phone - wm on Android: app transitions and gestures feel like the phone's - android: rotated Vulkan windows stop rebuilding their swapchain every frame, and hosted children get touch - android: hosted children hand frames over with GPU fences and draw only when they have work - wm on Android: gestures and surfaces settle like the phone's - widgets: touch lists scroll with Android's physics - wm on Android: the shell follows the Pixel launcher's motion - wm on Android: Recents shows the apps over a receding home, and the home screen switches looks - wm on Android: the look switcher stays put, the iOS dock shows its icons, background apps follow a look change - task, cargo-makepad, widgets, audio: the task manager chooses its columns, graphs every process' network and disk traffic and installs itself as a D - files: the tile view's three projections are toolbar buttons - mail: the local view takes a theme-derived palette and line icons, search pages share records - director: agent lanes form a tree and Grok joins the usage bar - widgets, draw: skeuomorphic surfaces light each other through an optional relief buffer - widgets, platform: a texture can light the relief buffer, menus take colour chips, and a style reload recompiles changed shader functions - platform, widgets, audio_route, ai: window crossfades and whole-frame presents, caption controls that click, themed menus, a stereo-pair audio tap, - builder: the Builder TUI matches the new terminal design, Windows executables carry app icons, and every app has one - files: the Files app deletes nothing and never overwrites - director: coding agents ask before acting unless you choose otherwise - sheets, score, git, home: saves cannot cut a file short or overwrite another one, and checkouts cannot write outside the repository - widgets, builder: the big widget families are features, all on by default, and the Builder takes none of them - ai: Claude Desktop drives any app with the F10 panel - wm: starts on Windows - wm: children open no console windows on Windows - wm: the whole deck out of a Builder's source, compiled only when the person opens an app - ai: Connect hands the .mcpb to Claude Desktop itself - wm: the clock and calendar know the date on Windows - wm: opening an app rebuilds it when its binary is out of date; warm instances never build - civil-time: one local wall clock for every app, and it knows the zone on Windows - platform + wm: a hosted child redraws at its tile's new size - calendar: the Calendars sheet is an opaque panel, and the wide layout does not float one over its sidebar - platform: an app's package dir, icons and bundle name rebuild only that app - build: the AI stack, csg, Scope and Source Library join the one workspace - ai chat: the chat's backends are the CLI, MCP and cloud providers; the local model is the `localai` feature - apps: every app that hosts the chat has a `localai` feature for the local model, off unless the app's own job runs one - widgets: every widget family is a crate of its own under widgets/families, and makepad-widgets links, re-exports and registers the ones its features - aichat, widgets: the chat reads the design feedback through the tweaker's hook, so linking the chat no longer needs the tweaker - apps, examples, libs: each crate builds only the widget families it uses, and the design overlay is each app's own default feature - ai chat: a build without local AI starts on the first provider that answers here, not on "No model" - repo: no third-party comic archive, generated models or sample asset in the tree - remove big example media files - wm: in a Makepad Builder installation apps build through the Builder, however wm was started Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
371 lines
15 KiB
Rust
371 lines
15 KiB
Rust
//! The Android phone shell's motion, to the Pixel Launcher's numbers
|
|
//! (Launcher3 + Quickstep; the parity spec is
|
|
//! local/agent_state/wm-android-proc/launcher-parity-spec.md). Units are
|
|
//! the shell's points, which are Android's dp (Makepad scales by density);
|
|
//! a spec speed in px/ms or px/s is converted with the device density where
|
|
//! it is given in pixels.
|
|
|
|
use makepad_widgets::*;
|
|
|
|
/// A cubic Bézier easing through (0,0), (x1,y1), (x2,y2), (1,1), evaluated
|
|
/// at `t` along x (Android's `PathInterpolator`).
|
|
pub fn cubic(x1: f64, y1: f64, x2: f64, y2: f64, t: f64) -> f64 {
|
|
bezier_segment((0.0, 0.0), (x1, y1), (x2, y2), (1.0, 1.0), t)
|
|
}
|
|
|
|
/// A cubic segment from `p0` to `p3`, y at x = `x`.
|
|
fn bezier_segment(p0: (f64, f64), p1: (f64, f64), p2: (f64, f64), p3: (f64, f64), x: f64) -> f64 {
|
|
let x = x.clamp(p0.0, p3.0);
|
|
let at = |s: f64, a: f64, b: f64, c: f64, d: f64| {
|
|
let u = 1.0 - s;
|
|
u * u * u * a + 3.0 * u * u * s * b + 3.0 * u * s * s * c + s * s * s * d
|
|
};
|
|
// x is monotonic along these curves: bisect for the parameter.
|
|
let (mut lo, mut hi) = (0.0, 1.0);
|
|
for _ in 0..40 {
|
|
let mid = (lo + hi) * 0.5;
|
|
if at(mid, p0.0, p1.0, p2.0, p3.0) < x { lo = mid } else { hi = mid }
|
|
}
|
|
let s = (lo + hi) * 0.5;
|
|
at(s, p0.1, p1.1, p2.1, p3.1)
|
|
}
|
|
|
|
/// A two-segment path easing (`M0,0 C a b c C d e 1,1`): `mid` is where
|
|
/// the first segment ends.
|
|
fn path2(c1: [(f64, f64); 3], c2: [(f64, f64); 2], t: f64) -> f64 {
|
|
let t = t.clamp(0.0, 1.0);
|
|
let mid = c1[2];
|
|
if t <= mid.0 {
|
|
bezier_segment((0.0, 0.0), c1[0], c1[1], mid, t)
|
|
} else {
|
|
bezier_segment(mid, c2[0], c2[1], (1.0, 1.0), t)
|
|
}
|
|
}
|
|
|
|
/// EMPHASIZED (fast_out_extra_slow_in).
|
|
pub fn emphasized(t: f64) -> f64 {
|
|
path2([(0.05, 0.0), (0.133333, 0.06), (0.166666, 0.4)], [(0.208333, 0.82), (0.25, 1.0)], t)
|
|
}
|
|
/// EMPHASIZED_COMPLEMENT (`app_open_x`): the app-open path's x.
|
|
pub fn emphasized_complement(t: f64) -> f64 {
|
|
path2([(0.1217, 0.0462), (0.15, 0.4686), (0.1667, 0.66)], [(0.1834, 0.8878), (0.1667, 1.0)], t)
|
|
}
|
|
pub fn emphasized_decelerate(t: f64) -> f64 { cubic(0.05, 0.7, 0.1, 1.0, t) }
|
|
pub fn touch_response(t: f64) -> f64 { cubic(0.3, 0.0, 0.1, 1.0, t) }
|
|
/// The home screen's scaling reveal after a swipe home.
|
|
pub fn scaling_reveal(t: f64) -> f64 {
|
|
path2([(0.045, 0.0356), (0.0975, 0.2055), (0.15, 0.3952)], [(0.235, 0.6855), (0.235, 1.0)], t)
|
|
}
|
|
pub fn decelerate(t: f64) -> f64 { let u = 1.0 - t.clamp(0.0, 1.0); 1.0 - u * u }
|
|
/// DECELERATE with factor `n` (1 - (1-t)^(2n)).
|
|
pub fn decelerate_n(t: f64, n: f64) -> f64 { 1.0 - (1.0 - t.clamp(0.0, 1.0)).powf(2.0 * n) }
|
|
/// ACCELERATE_1_5 as the spec gives it.
|
|
pub fn accelerate_1_5(t: f64) -> f64 { t.clamp(0.0, 1.0).powi(3) }
|
|
pub fn scroll(t: f64) -> f64 { 1.0 + (t.clamp(0.0, 1.0) - 1.0).powi(5) }
|
|
pub fn scroll_cubic(t: f64) -> f64 { 1.0 + (t.clamp(0.0, 1.0) - 1.0).powi(3) }
|
|
/// OvershootInterpolator(1.2).
|
|
pub fn overshoot_1_2(t: f64) -> f64 {
|
|
let t = t.clamp(0.0, 1.0) - 1.0;
|
|
let tension = 1.2;
|
|
t * t * ((tension + 1.0) * t + tension) + 1.0
|
|
}
|
|
/// A linear ramp from 0 at `a` to 1 at `b`.
|
|
pub fn ramp(x: f64, a: f64, b: f64) -> f64 { ((x - a) / (b - a)).clamp(0.0, 1.0) }
|
|
|
|
/// A damped spring (androidx SpringForce: stiffness, damping ratio).
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
pub struct Spring {
|
|
pub x: f64,
|
|
pub v: f64,
|
|
pub target: f64,
|
|
pub stiffness: f64,
|
|
pub damping: f64,
|
|
}
|
|
impl Spring {
|
|
pub fn new(x: f64, v: f64, target: f64, stiffness: f64, damping: f64) -> Self {
|
|
Self { x, v, target, stiffness, damping }
|
|
}
|
|
/// Advance by `dt` in small substeps (semi-implicit Euler).
|
|
pub fn step(&mut self, dt: f64) {
|
|
let steps = (dt / 0.002).ceil().max(1.0) as usize;
|
|
let h = dt / steps as f64;
|
|
let c = 2.0 * self.damping * self.stiffness.sqrt();
|
|
for _ in 0..steps {
|
|
let a = -self.stiffness * (self.x - self.target) - c * self.v;
|
|
self.v += a * h;
|
|
self.x += self.v * h;
|
|
}
|
|
}
|
|
/// At rest within `eps` of the target (and barely moving).
|
|
pub fn settled(&self, eps: f64) -> bool {
|
|
(self.x - self.target).abs() < eps && self.v.abs() < eps * 20.0
|
|
}
|
|
}
|
|
|
|
/// Swipe-up release velocity clamp (Quickstep `RectFSpringAnim`, v2):
|
|
/// between `min` and `max`, with any excess over `max` compressed as
|
|
/// excess^(1/1.4). In the units given.
|
|
pub fn clamp_release(v: f64, min: f64, max: f64) -> f64 {
|
|
let a = v.abs();
|
|
let clamped = if a < min { min } else if a <= max { a } else { max + (a - max).powf(1.0 / 1.4) };
|
|
clamped * v.signum()
|
|
}
|
|
|
|
/// App open from an icon: 500 ms (`APP_LAUNCH_DURATION`).
|
|
pub const APP_OPEN_SECS: f64 = 0.5;
|
|
/// The icon over the growing window fades out from 25 ms to 75 ms.
|
|
pub const ICON_FADE: (f64, f64) = (0.025, 0.075);
|
|
/// The home screen behind a launch: 1 -> 0.97 over 350 ms.
|
|
pub const LAUNCH_HOME_SCALE: f64 = 0.97;
|
|
pub const LAUNCH_HOME_SECS: f64 = 0.35;
|
|
|
|
/// Where the opening window is `elapsed` seconds in, growing out of the
|
|
/// icon `icon` (a circle the icon's size) into `app`: size and y on
|
|
/// EMPHASIZED, x on EMPHASIZED_COMPLEMENT (the curved path). Returns the
|
|
/// rect, its corner radius (from a circle to square) and the window's
|
|
/// alpha over the fading icon.
|
|
pub fn open_window(icon: Rect, app: Rect, elapsed: f64) -> (Rect, f64, f64) {
|
|
let t = (elapsed / APP_OPEN_SECS).clamp(0.0, 1.0);
|
|
let side = icon.size.x.min(icon.size.y).max(1.0);
|
|
let c0 = icon.pos + icon.size * 0.5;
|
|
let c1 = app.pos + app.size * 0.5;
|
|
let e = emphasized(t);
|
|
let ex = emphasized_complement(t);
|
|
let size = dvec2(side + (app.size.x - side) * e, side + (app.size.y - side) * e);
|
|
let centre = dvec2(c0.x + (c1.x - c0.x) * ex, c0.y + (c1.y - c0.y) * e);
|
|
let radius = side * 0.5 * (1.0 - e);
|
|
let alpha = ramp(elapsed, ICON_FADE.0, ICON_FADE.1);
|
|
(Rect { pos: centre - size * 0.5, size }, radius, alpha)
|
|
}
|
|
|
|
/// The home screen's scale while an app launches over it.
|
|
pub fn launch_home_scale(elapsed: f64) -> f64 {
|
|
1.0 + (LAUNCH_HOME_SCALE - 1.0) * decelerate_n(elapsed / LAUNCH_HOME_SECS, 1.5)
|
|
}
|
|
|
|
/// A lifted app released toward Home flies into its icon on three springs
|
|
/// (Quickstep v2 `RectFSpringAnim`): centre x (450, 0.965), centre y (400,
|
|
/// 0.95) and the size progress (500, 0.99), started with the finger's
|
|
/// velocity (clamped as Quickstep does).
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct HomeFlight {
|
|
pub from: Rect,
|
|
pub to: Rect,
|
|
/// The icon's corner radius at the end (a circle for the fallback).
|
|
pub to_radius: f64,
|
|
pub from_radius: f64,
|
|
pub cx: Spring,
|
|
pub cy: Spring,
|
|
pub progress: Spring,
|
|
pub elapsed: f64,
|
|
}
|
|
impl HomeFlight {
|
|
/// `velocity` in points/s, `density` pixels per point.
|
|
pub fn new(from: Rect, from_radius: f64, to: Rect, to_radius: f64, velocity: Vec2d, density: f64) -> Self {
|
|
let d = density.max(0.5);
|
|
let vx = clamp_release(velocity.x * d, 300.0, 500.0) / d;
|
|
let vy = clamp_release(velocity.y.min(-1.0) * d, 2000.0, 5000.0) / d;
|
|
let c0 = from.pos + from.size * 0.5;
|
|
let c1 = to.pos + to.size * 0.5;
|
|
Self {
|
|
from, to, to_radius, from_radius,
|
|
cx: Spring::new(c0.x, vx, c1.x, 450.0, 0.965),
|
|
cy: Spring::new(c0.y, vy, c1.y, 400.0, 0.95),
|
|
progress: Spring::new(0.0, 0.0, 1.0, 500.0, 0.99),
|
|
elapsed: 0.0,
|
|
}
|
|
}
|
|
pub fn step(&mut self, dt: f64) -> bool {
|
|
self.elapsed += dt;
|
|
self.cx.step(dt);
|
|
self.cy.step(dt);
|
|
self.progress.step(dt);
|
|
!(self.cx.settled(0.25) && self.cy.settled(0.25) && self.progress.settled(0.002))
|
|
}
|
|
pub fn rect(&self) -> Rect {
|
|
let p = self.progress.x;
|
|
let size = self.from.size + (self.to.size - self.from.size) * p;
|
|
Rect { pos: dvec2(self.cx.x, self.cy.x) - size * 0.5, size }
|
|
}
|
|
pub fn radius(&self) -> f64 {
|
|
let p = self.progress.x.clamp(0.0, 1.0);
|
|
self.from_radius + (self.to_radius - self.from_radius) * p
|
|
}
|
|
/// The window fades out over the first 85 % of the size progress.
|
|
pub fn window_alpha(&self) -> f64 {
|
|
1.0 - accelerate_1_5(self.progress.x / 0.85)
|
|
}
|
|
}
|
|
|
|
/// The home screen revealed after a swipe home: scale 0.85 -> 1 over 1 s on
|
|
/// the scaling-reveal curve, alpha 0 -> 1 over the first 200 ms.
|
|
pub const REVEAL_SECS: f64 = 1.0;
|
|
pub const REVEAL_FADE_SECS: f64 = 0.2;
|
|
pub const REVEAL_FROM: f64 = 0.85;
|
|
pub fn reveal(elapsed: f64) -> (f64, f64) {
|
|
let s = REVEAL_FROM + (1.0 - REVEAL_FROM) * scaling_reveal(elapsed / REVEAL_SECS);
|
|
(s, ramp(elapsed, 0.0, REVEAL_FADE_SECS))
|
|
}
|
|
|
|
/// All Apps on a phone: the panel rises 300 dp while it fades in.
|
|
pub const ALL_APPS_SHIFT: f64 = 300.0;
|
|
/// Release: a fling faster than 1 dp/ms goes its way; else open past 40 %
|
|
/// (from Home) or close under 60 % (from All Apps).
|
|
pub const ALL_APPS_FLING: f64 = 1000.0;
|
|
pub const ALL_APPS_OPEN_AT: f64 = 0.4;
|
|
pub const ALL_APPS_CLOSE_AT: f64 = 0.6;
|
|
/// How the home and the All Apps panel look at progress `p` (0 home, 1 All
|
|
/// Apps): (home scale, home alpha, panel background alpha, panel content
|
|
/// alpha, panel shift in points).
|
|
pub fn all_apps_frame(p: f64) -> (f64, f64, f64, f64, f64) {
|
|
let q = p.clamp(0.0, 1.0);
|
|
let home_scale = 1.0 + (LAUNCH_HOME_SCALE - 1.0) * ramp(q, 0.0, 0.4);
|
|
let home_alpha = if q >= 0.4 { 0.0 } else { 1.0 };
|
|
let background = ramp(q, 0.117, 0.4);
|
|
let content = ramp(q, 0.4, 0.8);
|
|
let shift = (1.0 - p.min(1.0)) * ALL_APPS_SHIFT;
|
|
(home_scale, home_alpha, background, content, shift)
|
|
}
|
|
/// The settle after release (Launcher `BaseSwipeDetector.calculateDuration`):
|
|
/// seconds for `remaining` progress at `v` points/s (converted to px/ms with
|
|
/// `density`), and whether the fast (quintic) curve applies.
|
|
pub fn all_apps_settle(remaining: f64, v: f64, density: f64) -> (f64, bool) {
|
|
let v_px_ms = (v * density / 1000.0).abs();
|
|
let ms = (1200.0 / (0.5 * v_px_ms).max(2.0) * remaining.abs().max(0.2)).max(100.0);
|
|
(ms / 1000.0, v_px_ms > 10.0)
|
|
}
|
|
|
|
/// A timed ease from `from` to `to`.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct Tween {
|
|
pub from: f64,
|
|
pub to: f64,
|
|
pub elapsed: f64,
|
|
pub duration: f64,
|
|
pub curve: Curve,
|
|
}
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum Curve { Scroll, ScrollCubic, Overshoot, Decelerate, TouchResponse, Linear }
|
|
impl Tween {
|
|
pub fn new(from: f64, to: f64, duration: f64, curve: Curve) -> Self {
|
|
Self { from, to, elapsed: 0.0, duration: duration.max(0.001), curve }
|
|
}
|
|
pub fn step(&mut self, dt: f64) -> (f64, bool) {
|
|
self.elapsed += dt;
|
|
let t = (self.elapsed / self.duration).clamp(0.0, 1.0);
|
|
let e = match self.curve {
|
|
Curve::Scroll => scroll(t),
|
|
Curve::ScrollCubic => scroll_cubic(t),
|
|
Curve::Overshoot => overshoot_1_2(t),
|
|
Curve::Decelerate => decelerate(t),
|
|
Curve::TouchResponse => touch_response(t),
|
|
Curve::Linear => t,
|
|
};
|
|
(self.from + (self.to - self.from) * e, t < 1.0)
|
|
}
|
|
}
|
|
|
|
/// The home screen behind the switcher recedes to the launcher's hint
|
|
/// scale (Quickstep HINT state, 0.92) as it fades.
|
|
pub const OVERVIEW_HOME_SCALE: f64 = 0.92;
|
|
/// The wallpaper behind the switcher dims by the launcher's overview scrim
|
|
/// (100/255).
|
|
pub const OVERVIEW_SCRIM: f64 = 100.0 / 255.0;
|
|
|
|
/// Recents: neighbour cards slide in over 300 ms when the lift pauses.
|
|
pub const RECENTS_ATTACH_SECS: f64 = 0.3;
|
|
/// Swipe-up settle into Recents / back into the app (Quickstep
|
|
/// `MAX_SWIPE_DURATION` 350 ms, 3.33 per unit of shift).
|
|
pub fn swipe_settle_secs(delta: f64) -> f64 {
|
|
(delta.abs() * 0.350 * 3.33).min(0.350).max(0.05)
|
|
}
|
|
/// Tapping a Recents card opens it in 336 ms on TOUCH_RESPONSE.
|
|
pub const RECENTS_LAUNCH_SECS: f64 = 0.336;
|
|
|
|
/// Android: the quickstep slop from the gesture bar (8 dp x 1.414).
|
|
pub const QUICKSTEP_SLOP: f64 = 11.3;
|
|
|
|
/// Haptics the shell asks for (Launcher's call sites).
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub enum Haptic {
|
|
/// A lift paused: the switcher (EFFECT_CLICK).
|
|
Click,
|
|
/// All Apps starts opening (VIRTUAL_KEY).
|
|
VirtualKey,
|
|
/// A Recents page crossed (low tick) or a card dismissed (tick).
|
|
Tick,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn curves_start_at_zero_end_at_one_and_rise() {
|
|
for f in [emphasized, emphasized_complement, emphasized_decelerate, touch_response, scaling_reveal, decelerate, scroll, scroll_cubic] {
|
|
assert!(f(0.0).abs() < 1e-6 && (f(1.0) - 1.0).abs() < 1e-6);
|
|
let mut last = 0.0;
|
|
for i in 1..=100 {
|
|
let v = f(i as f64 / 100.0);
|
|
assert!(v >= last - 1e-6, "monotonic");
|
|
last = v;
|
|
}
|
|
}
|
|
// EMPHASIZED is 0.4 a sixth of the way in (its path's joint).
|
|
assert!((emphasized(0.166666) - 0.4).abs() < 1e-3);
|
|
assert!(overshoot_1_2(0.8) > 1.0, "overshoots");
|
|
}
|
|
|
|
#[test]
|
|
fn app_open_grows_from_the_icon_circle_to_the_app() {
|
|
let icon = Rect { pos: dvec2(40.0, 600.0), size: dvec2(60.0, 60.0) };
|
|
let app = Rect { pos: dvec2(0.0, 42.0), size: dvec2(412.0, 794.0) };
|
|
let (r0, rad0, a0) = open_window(icon, app, 0.0);
|
|
assert!((r0.size.x - 60.0).abs() < 1e-6 && (rad0 - 30.0).abs() < 1e-6 && a0 == 0.0);
|
|
let (r1, rad1, a1) = open_window(icon, app, APP_OPEN_SECS);
|
|
assert!((r1.size.x - app.size.x).abs() < 1e-6 && (r1.pos.y - app.pos.y).abs() < 1e-6 && rad1.abs() < 1e-9 && a1 == 1.0);
|
|
let mut last = 0.0;
|
|
for i in 0..=50 {
|
|
let (r, _, _) = open_window(icon, app, APP_OPEN_SECS * i as f64 / 50.0);
|
|
assert!(r.size.x >= last - 1e-6, "only grows");
|
|
last = r.size.x;
|
|
}
|
|
assert!((open_window(icon, app, 0.05).2 - 0.5).abs() < 1e-9, "the icon is half faded at 50 ms");
|
|
}
|
|
|
|
#[test]
|
|
fn a_home_flight_lands_on_the_icon_and_fades_first() {
|
|
let from = Rect { pos: dvec2(60.0, 200.0), size: dvec2(290.0, 600.0) };
|
|
let to = Rect { pos: dvec2(40.0, 600.0), size: dvec2(60.0, 60.0) };
|
|
let mut f = HomeFlight::new(from, 28.0, to, 30.0, dvec2(0.0, -3000.0), 2.4);
|
|
let mut faded_at = None;
|
|
for i in 0..600 {
|
|
if !f.step(1.0 / 120.0) { break; }
|
|
if faded_at.is_none() && f.window_alpha() <= 0.0 { faded_at = Some(i); }
|
|
}
|
|
let r = f.rect();
|
|
assert!((r.pos - to.pos).length() < 1.0 && (r.size - to.size).length() < 1.0);
|
|
assert!(faded_at.is_some(), "the window is gone before the icon lands");
|
|
}
|
|
|
|
#[test]
|
|
fn all_apps_frames_follow_the_spec() {
|
|
let (s, a, bg, c, shift) = all_apps_frame(0.0);
|
|
assert_eq!((s, a, bg, c, shift), (1.0, 1.0, 0.0, 0.0, ALL_APPS_SHIFT));
|
|
let (s, a, _, c, _) = all_apps_frame(0.39);
|
|
assert!(s < 1.0 && s > LAUNCH_HOME_SCALE && a == 1.0 && c == 0.0);
|
|
let (_, a, bg, _, _) = all_apps_frame(0.4);
|
|
assert!(a == 0.0 && bg == 1.0, "home gone and the panel ground full at 40 %");
|
|
let (_, _, _, c, shift) = all_apps_frame(1.0);
|
|
assert!(c == 1.0 && shift == 0.0);
|
|
let (fast, quintic) = all_apps_settle(0.5, 6000.0, 2.4);
|
|
assert!(fast >= 0.1 && quintic);
|
|
}
|
|
|
|
#[test]
|
|
fn release_velocity_is_clamped_like_quickstep() {
|
|
assert_eq!(clamp_release(100.0, 300.0, 500.0), 300.0);
|
|
assert_eq!(clamp_release(-400.0, 300.0, 500.0), -400.0);
|
|
assert!(clamp_release(-9000.0, 2000.0, 5000.0) > -5500.0);
|
|
}
|
|
}
|