The widget half of the widget catalogue's later work, ported onto makepad-widgets-core and the family crates. - libs/tween: a dependency-free tween and timeline engine -- nested timelines with positions and labels, stagger, repeat and yoyo, seek and reverse, time scale, a ticker with lag smoothing, a global pause and reduced motion, colour-space interpolation, keyframes and motion paths. The core binds it to the widget tree (tween, a widget's TweenHost), gives script a mod.tween (tween_script) and a live inspector (tween_inspect, tween_inspector). The Animator's Ease now answers from the engine's easing, so the curves have one source; the Animator keeps its states, its keyframe timelines and its own clock. - libs/gizmo and a Gizmo3d / ViewCube widget. - core: the ease editor, the sequencer, diagonal headings, hotkeys, the hotkey editor and keyboard map, the overlay layer host, a conceding row and the width a widget would take without drawing; a theme mix and a person's own theme edits that survive a style reload (theme_mix, set_theme_edit), both taken off by set_base_theme. - the surface material: buttons, check boxes, toggles, sliders and knobs carry the moulded material behind one uniform, a press idiom per sign of material_press, and four sheets that raise it: neumorphic, molded, glossy and milled. - the sheets: a row of fields is one height under every sheet. A sheet line that names a widget the app did not register (a family left out) is skipped when the sheet installs, so the date fields a sheet sizes need the dates family only where it is linked. - extras: the gradient editor, registered with the colour picker it builds on. - tweaker: the designer (a widget's literal located and edited in its source as hunks), the theme lab and the theme builder. The window reads the docked panel's band through the tweaker's hooks. - the panel no longer wears the app's theme (the design surface keeps its own palette), so the worn-theme tests go with it. Five theme builder tests fail here as they do on the branch this comes from (every_built_theme_reads and four more on text contrast); radial_menu's tab_under_an_open_menu_finds_nowhere_to_go fails here as it does on dev.
299 lines
9.2 KiB
Rust
299 lines
9.2 KiB
Rust
//! QuickTo: bit parity with pill_nav's UnitTween and the retargeting rules.
|
|
|
|
mod common;
|
|
|
|
use common::{to_easing, Ease, Rng, THEME_EASES};
|
|
use makepad_tween::{Easing, Lerp, QuickTo, Retime};
|
|
|
|
// ---- oracle: a verbatim copy of widgets/src/pill_nav.rs UnitTween ----
|
|
|
|
/// A number on its way to one end of 0..1 along one of the theme's easings:
|
|
/// the panel's grow and the pill's fade here, the line menu's reveal. The
|
|
/// clock runs straight and the ease shapes what is read off it, so a spring
|
|
/// or a bounce can carry the value past its end and back while the clock
|
|
/// still says how much of the run is left.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct UnitTween {
|
|
from: f64,
|
|
to: f64,
|
|
/// 0 when the run starts, 1 once it is over.
|
|
t: f64,
|
|
secs: f64,
|
|
ease: Ease,
|
|
/// The furthest the value has come toward `to` this run, held to 0..1.
|
|
peak: f64,
|
|
}
|
|
|
|
impl Default for UnitTween {
|
|
fn default() -> Self {
|
|
Self::at(0.0)
|
|
}
|
|
}
|
|
|
|
impl UnitTween {
|
|
/// At rest on `value`.
|
|
pub fn at(value: f64) -> Self {
|
|
Self {
|
|
from: value,
|
|
to: value,
|
|
t: 1.0,
|
|
secs: 0.0,
|
|
ease: Ease::Linear,
|
|
peak: value.clamp(0.0, 1.0),
|
|
}
|
|
}
|
|
|
|
/// Head for `to` along `ease`. A run from one end to the other takes
|
|
/// `secs` and a run from part way takes that share of it, so turning back
|
|
/// mid-run starts from where the value is and takes only as long as the
|
|
/// way back is. Aiming where it already heads changes nothing, so a
|
|
/// caller can aim on every frame.
|
|
pub fn aim(&mut self, to: f64, secs: f64, ease: Ease) {
|
|
if to == self.to {
|
|
return;
|
|
}
|
|
let now = self.value();
|
|
self.secs = secs.max(0.0) * (to - now).abs().min(1.0);
|
|
self.from = now;
|
|
self.to = to;
|
|
self.ease = ease;
|
|
self.peak = now.clamp(0.0, 1.0);
|
|
self.t = 0.0;
|
|
if self.secs <= 0.0 {
|
|
self.settle();
|
|
}
|
|
}
|
|
|
|
/// Advance the clock by `dt` seconds. Answers whether the run goes on.
|
|
pub fn step(&mut self, dt: f64) -> bool {
|
|
if self.t >= 1.0 {
|
|
return false;
|
|
}
|
|
if self.secs <= 0.0 {
|
|
self.settle();
|
|
return false;
|
|
}
|
|
self.t = (self.t + dt / self.secs).min(1.0);
|
|
if self.t >= 1.0 {
|
|
self.settle();
|
|
return false;
|
|
}
|
|
let now = self.value().clamp(0.0, 1.0);
|
|
self.peak = if self.to >= self.from {
|
|
self.peak.max(now)
|
|
} else {
|
|
self.peak.min(now)
|
|
};
|
|
true
|
|
}
|
|
|
|
/// End the run on its target now. For motion that has been switched off.
|
|
pub fn settle(&mut self) {
|
|
*self = Self::at(self.to);
|
|
}
|
|
|
|
/// Where the value is now, past either end while an overshooting ease
|
|
/// carries it. Exactly the target once the run is over: a curve read at
|
|
/// its last sample can land a hair off.
|
|
pub fn value(&self) -> f64 {
|
|
if self.t >= 1.0 {
|
|
return self.to;
|
|
}
|
|
self.from + (self.to - self.from) * self.ease.map(self.t)
|
|
}
|
|
|
|
/// The furthest the value has come this run, held to 0..1 and never going
|
|
/// back: what a fade reads, so it does not flicker while a bounce dips.
|
|
pub fn reached(&self) -> f64 {
|
|
self.peak
|
|
}
|
|
|
|
pub fn target(&self) -> f64 {
|
|
self.to
|
|
}
|
|
|
|
/// Resting on `value`, with nothing of the run left.
|
|
pub fn is_at(&self, value: f64) -> bool {
|
|
self.t >= 1.0 && self.to == value
|
|
}
|
|
}
|
|
|
|
// ---- end of oracle ----
|
|
|
|
/// The retiming pill_nav's UnitTween uses: a full run is one unit.
|
|
const UNIT: Retime = Retime::ByDistance { per_unit: 1.0 };
|
|
|
|
fn assert_same(oracle: &UnitTween, quick: &QuickTo<f64>, what: &str) {
|
|
assert_eq!(
|
|
quick.value().to_bits(),
|
|
oracle.value().to_bits(),
|
|
"{what}: value"
|
|
);
|
|
assert_eq!(quick.target(), oracle.target(), "{what}: target");
|
|
assert_eq!(
|
|
quick.is_settled() && quick.target() == 0.0,
|
|
oracle.is_at(0.0),
|
|
"{what}: at 0"
|
|
);
|
|
assert_eq!(
|
|
quick.is_settled() && quick.target() == 1.0,
|
|
oracle.is_at(1.0),
|
|
"{what}: at 1"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quick_to_reproduces_unit_tween() {
|
|
for (k, ease) in THEME_EASES.iter().enumerate() {
|
|
for seed in 0..8u64 {
|
|
let mut rng = Rng(seed * 1000 + k as u64);
|
|
let mut oracle = UnitTween::at(0.0);
|
|
let mut quick = QuickTo::at(0.0);
|
|
let secs = [0.25, 0.4, 0.0, 1.0][(seed % 4) as usize];
|
|
for frame in 0..1500 {
|
|
let what = format!("{ease:?} seed {seed} frame {frame}");
|
|
let roll = rng.next_f64();
|
|
if roll < 0.04 {
|
|
let to = if rng.next_f64() < 0.5 { 0.0 } else { 1.0 };
|
|
oracle.aim(to, secs, *ease);
|
|
quick.aim(to, secs, to_easing(ease), UNIT);
|
|
} else if roll < 0.06 {
|
|
let to = (rng.next_f64() * 8.0).floor() / 8.0;
|
|
oracle.aim(to, secs, *ease);
|
|
quick.aim(to, secs, to_easing(ease), UNIT);
|
|
} else if roll < 0.065 {
|
|
oracle.settle();
|
|
quick.settle();
|
|
}
|
|
assert_same(&oracle, &quick, &what);
|
|
let dt = if rng.next_f64() < 0.02 {
|
|
0.3
|
|
} else {
|
|
rng.next_f64() * 0.034
|
|
};
|
|
assert_eq!(quick.step(dt), oracle.step(dt), "{what}: step");
|
|
assert_same(&oracle, &quick, &what);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn same_target_is_a_noop() {
|
|
let mut q = QuickTo::at(0.0);
|
|
assert!(q.aim(1.0, 0.5, Easing::OutQuad, Retime::Full));
|
|
q.step(0.1);
|
|
let before = q;
|
|
assert!(!q.aim(1.0, 2.0, Easing::Linear, Retime::Full));
|
|
assert_eq!(q, before);
|
|
}
|
|
|
|
#[test]
|
|
fn by_distance_retime() {
|
|
let mut q = QuickTo::at(0.0);
|
|
assert!(q.aim(0.5, 0.4, Easing::Linear, UNIT));
|
|
assert_eq!(q.duration(), 0.2);
|
|
let mut full = QuickTo::at(0.0);
|
|
full.aim(0.5, 0.4, Easing::Linear, Retime::Full);
|
|
assert_eq!(full.duration(), 0.4);
|
|
// Distances beyond one unit take the full duration.
|
|
let mut far = QuickTo::at(0.0);
|
|
far.aim(3.0, 0.4, Easing::Linear, UNIT);
|
|
assert_eq!(far.duration(), 0.4);
|
|
// Arrays use the largest lane distance.
|
|
let mut rect = QuickTo::at([0.0, 0.0, 10.0, 10.0]);
|
|
rect.aim(
|
|
[5.0, 0.0, 10.0, 30.0],
|
|
1.0,
|
|
Easing::Linear,
|
|
Retime::ByDistance { per_unit: 40.0 },
|
|
);
|
|
assert_eq!(rect.duration(), 0.5);
|
|
}
|
|
|
|
#[test]
|
|
fn landing_step_answers_false_and_is_exact() {
|
|
for ease in [
|
|
Easing::OutBack,
|
|
Easing::OutElastic,
|
|
Easing::css(0.3, 0.0, 0.8, 0.15),
|
|
Easing::InOutBounce,
|
|
] {
|
|
let mut q = QuickTo::at(3.0);
|
|
q.aim(7.1, 0.25, ease, Retime::Full);
|
|
let mut frames = 0;
|
|
// 1/64 s frames divide 0.25 s exactly: 15 frames run on, the 16th lands.
|
|
while q.step(1.0 / 64.0) {
|
|
frames += 1;
|
|
assert!(!q.is_settled());
|
|
}
|
|
assert_eq!(frames, 15, "{ease:?}");
|
|
assert!(q.is_settled());
|
|
assert_eq!(q.value().to_bits(), 7.1f64.to_bits());
|
|
assert_eq!(q.progress(), 1.0);
|
|
assert!(!q.step(1.0 / 60.0), "a settled run stays settled");
|
|
}
|
|
// Zero seconds lands at once.
|
|
let mut q = QuickTo::at(0.0);
|
|
assert!(q.aim(1.0, 0.0, Easing::OutQuad, Retime::Full));
|
|
assert!(q.is_settled());
|
|
assert_eq!(q.value(), 1.0);
|
|
assert!(!q.step(0.016));
|
|
}
|
|
|
|
#[test]
|
|
fn restart_always_runs_from_the_visible_value() {
|
|
// wm's StyleTween: smoothstep over 0.65 s, restarted on every selection.
|
|
let mut w = QuickTo::at([1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]);
|
|
let mut target = [0.0; 8];
|
|
target[3] = 1.0;
|
|
w.restart(target, 0.65, Easing::SmoothStep);
|
|
w.step(0.2);
|
|
let seen = w.value();
|
|
target = [0.0; 8];
|
|
target[5] = 1.0;
|
|
w.restart(target, 0.65, Easing::SmoothStep);
|
|
w.step(0.0);
|
|
assert_eq!(w.value().map(f64::to_bits), seen.map(f64::to_bits));
|
|
assert_eq!(w.from_value(), seen);
|
|
// Restarting towards the same target still runs again.
|
|
w.restart(target, 0.65, Easing::SmoothStep);
|
|
assert_eq!(w.progress(), 0.0);
|
|
assert_eq!(w.ease(), Easing::SmoothStep);
|
|
}
|
|
|
|
#[test]
|
|
fn retarget_from() {
|
|
let mut q = QuickTo::at(5.0);
|
|
q.retarget_from(10.0, 20.0, 1.0, Easing::Linear);
|
|
assert_eq!(q.from_value(), 10.0);
|
|
assert_eq!(q.value(), 10.0);
|
|
q.step(0.25);
|
|
assert_eq!(q.value(), 12.5);
|
|
q.settle();
|
|
assert_eq!(q.value(), 20.0);
|
|
assert_eq!(q.from_value(), 10.0, "settle keeps the run's ends");
|
|
}
|
|
|
|
#[test]
|
|
fn value_does_not_force_the_start() {
|
|
// Like UnitTween, the ease is read at t = 0 too (an ease that does not
|
|
// start at 0 shows it at once).
|
|
let mut q = QuickTo::at(0.0);
|
|
q.aim(10.0, 1.0, Easing::Constant(0.5), Retime::Full);
|
|
assert_eq!(q.value(), 5.0);
|
|
}
|
|
|
|
#[test]
|
|
fn lerp_arrays_per_lane() {
|
|
assert_eq!(
|
|
<[f64; 3]>::lerp([0.0, 10.0, -4.0], [10.0, 20.0, 4.0], 0.5),
|
|
[5.0, 15.0, 0.0]
|
|
);
|
|
assert_eq!(
|
|
<[f64; 3]>::distance([0.0, 10.0, -4.0], [1.0, 20.0, 4.0]),
|
|
10.0
|
|
);
|
|
assert_eq!(f64::lerp(2.0, 4.0, 0.25), 2.5);
|
|
assert_eq!(f64::distance(4.0, 2.0), 2.0);
|
|
}
|