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.
330 lines
11 KiB
Rust
330 lines
11 KiB
Rust
//! Overwrite (design 12.9), replayed against the GSAP 3.15 overwrite cases
|
|
//! (tests/golden/overwrite.rs).
|
|
|
|
mod util;
|
|
|
|
#[path = "golden"]
|
|
mod golden {
|
|
pub mod common;
|
|
pub mod overwrite;
|
|
}
|
|
|
|
use golden::common::{list, num, val as gval, Val};
|
|
use golden::overwrite::{OverwriteCase, CASES};
|
|
use makepad_tween::*;
|
|
use util::*;
|
|
|
|
fn case(id: &str) -> &'static OverwriteCase {
|
|
CASES
|
|
.iter()
|
|
.find(|c| c.id == id)
|
|
.unwrap_or_else(|| panic!("no overwrite case {id}"))
|
|
}
|
|
|
|
/// Checks one snapshot: property values on target 0 (and 1 for `t2`) and
|
|
/// the Interrupt callbacks since the last snapshot.
|
|
#[track_caller]
|
|
fn snap(e: &mut TweenEngine, names: &mut Names, c: &OverwriteCase, step: usize) {
|
|
let s = &c.steps[step];
|
|
let ctx = format!("{} [{}]", c.id, s.at);
|
|
for (key, t, p) in [
|
|
("obj.x", 0, X),
|
|
("obj.y", 0, Y),
|
|
("obj.a", 0, A),
|
|
("t1.a", 0, A),
|
|
("t2.a", 1, A),
|
|
] {
|
|
if let Some(Val::F(want)) = gval(s.vals, key) {
|
|
close(val(e, t, p), want, VAL_TOL, &format!("{ctx}: {key}"));
|
|
}
|
|
}
|
|
let got = names.drain(e).1;
|
|
let want = list(s.vals, "callbacks");
|
|
assert_eq!(got, want, "{ctx}: callbacks");
|
|
}
|
|
|
|
fn t1_opts(variant: &str) -> TweenOpts {
|
|
let o = lin(1.0).events(EventMask::INTERRUPT);
|
|
match variant {
|
|
"paused" => o.paused(true),
|
|
"not_started" => o.delay(1.0),
|
|
_ => o,
|
|
}
|
|
}
|
|
|
|
/// The standard sequence: t1 x,y -> 100 (playing at 0.5, paused at 0.5 or
|
|
/// not started); t2 x -> 200 with the given overwrite, paused.
|
|
fn standard(id: &str, mode: Overwrite, variant: &str) {
|
|
let c = case(id);
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
e.seed(tg(0), Y, TweenValue::F64(0.0));
|
|
let t1 = e.to(one(0), &[to(X, 100.0), to(Y, 100.0)], t1_opts(variant));
|
|
names.name(t1, "t1");
|
|
if variant != "not_started" {
|
|
e.anim(t1).set_total_time(0.5, Emit::Fire);
|
|
}
|
|
snap(&mut e, &mut names, c, 0);
|
|
let t2 = e.to(
|
|
one(0),
|
|
&[to(X, 200.0)],
|
|
lin(1.0).overwrite(mode).paused(true),
|
|
);
|
|
snap(&mut e, &mut names, c, 1);
|
|
e.anim(t2).set_total_time(0.0, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 2);
|
|
e.anim(t1).set_total_time(1.0, Emit::Fire);
|
|
if mode == Overwrite::All && variant == "not_started" {
|
|
// GSAP re-initialises the killed, never-started t1 when driven and
|
|
// it writes 100/100; the engine frees a killed tween, so its handle
|
|
// is stale and nothing moves (deviation).
|
|
assert!(!e.anim_ref(t1).is_alive());
|
|
assert_eq!((val(&e, 0, X), val(&e, 0, Y)), (0.0, 0.0));
|
|
names.drain(&mut e);
|
|
e.anim(t2).set_total_time(1.0, Emit::Fire);
|
|
assert_eq!((val(&e, 0, X), val(&e, 0, Y)), (200.0, 0.0));
|
|
return;
|
|
}
|
|
snap(&mut e, &mut names, c, 3);
|
|
e.anim(t2).set_total_time(1.0, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_auto_kills_the_active_rival_at_first_render() {
|
|
standard("auto_t1_playing", Overwrite::Auto, "playing");
|
|
// T12a [golden: design_auto_single_prop].
|
|
let c = case("design_auto_single_prop");
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), A, TweenValue::F64(0.0));
|
|
let a = e.to(
|
|
one(0),
|
|
&[to(A, 100.0)],
|
|
lin(1.0).events(EventMask::INTERRUPT),
|
|
);
|
|
names.name(a, "A");
|
|
e.anim(a).set_total_time(0.5, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 0);
|
|
let b = e.to(
|
|
one(0),
|
|
&[to(A, 0.0)],
|
|
lin(1.0).overwrite(Overwrite::Auto).paused(true),
|
|
);
|
|
snap(&mut e, &mut names, c, 1);
|
|
e.anim(b).set_total_time(0.0, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 2);
|
|
assert!(!e.anim_ref(a).is_alive(), "A has no track left: killed");
|
|
e.anim(b).set_total_time(0.5, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 3);
|
|
e.anim(a).set_total_time(0.8, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 4);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_auto_kills_every_active_rival() {
|
|
// T12b: Interrupts in reverse creation order [golden: design_auto_two_rivals].
|
|
let c = case("design_auto_two_rivals");
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), A, TweenValue::F64(0.0));
|
|
let a = e.to(
|
|
one(0),
|
|
&[to(A, 100.0)],
|
|
lin(1.0).events(EventMask::INTERRUPT),
|
|
);
|
|
e.anim(a).set_total_time(0.5, Emit::Fire);
|
|
let a2 = e.to(
|
|
one(0),
|
|
&[to(A, 50.0)],
|
|
lin(2.0).events(EventMask::INTERRUPT),
|
|
);
|
|
e.anim(a2).set_total_time(0.5, Emit::Fire);
|
|
names.name(a, "A");
|
|
names.name(a2, "A2");
|
|
snap(&mut e, &mut names, c, 0);
|
|
let cc = e.to(
|
|
one(0),
|
|
&[to(A, -100.0)],
|
|
lin(1.0).overwrite(Overwrite::Auto).paused(true),
|
|
);
|
|
e.anim(cc).set_total_time(0.0, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 1);
|
|
assert!(!e.anim_ref(a).is_alive() && !e.anim_ref(a2).is_alive());
|
|
}
|
|
|
|
#[test]
|
|
fn golden_all_is_per_target() {
|
|
standard("true_t1_playing", Overwrite::All, "playing");
|
|
standard("true_t1_paused", Overwrite::All, "paused");
|
|
standard("true_t1_not_started", Overwrite::All, "not_started");
|
|
// T12c [golden: design_true_multi_target].
|
|
let c = case("design_true_multi_target");
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
seed_all(&mut e, 2, A, 0.0);
|
|
let a = e.to(
|
|
Targets::Range { first: 0, count: 2 },
|
|
&[to(A, 100.0)],
|
|
lin(1.0).events(EventMask::INTERRUPT),
|
|
);
|
|
names.name(a, "A");
|
|
e.anim(a).set_total_time(0.5, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 0);
|
|
e.to(
|
|
one(0),
|
|
&[to(A, 0.0)],
|
|
lin(1.0).overwrite(Overwrite::All).paused(true),
|
|
);
|
|
snap(&mut e, &mut names, c, 1);
|
|
assert!(e.anim_ref(a).is_alive(), "A keeps its t2 track");
|
|
e.anim(a).set_total_time(1.0, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_auto_ignores_unstarted_paused_and_finished() {
|
|
standard("auto_t1_paused", Overwrite::Auto, "paused");
|
|
standard("auto_t1_not_started", Overwrite::Auto, "not_started");
|
|
// A finished tween is not a rival [golden: design_auto_ignores_finished].
|
|
// GSAP keeps the completed A object and re-links it on totalTime(); the
|
|
// engine frees completed bare tweens, so A is built with keep(true).
|
|
let c = case("design_auto_ignores_finished");
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), A, TweenValue::F64(0.0));
|
|
let a = e.to(
|
|
one(0),
|
|
&[to(A, 100.0)],
|
|
lin(1.0).keep(true).events(EventMask::INTERRUPT),
|
|
);
|
|
names.name(a, "A");
|
|
e.anim(a).set_total_time(1.0, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 0);
|
|
let b = e.to(
|
|
one(0),
|
|
&[to(A, 0.0)],
|
|
lin(1.0).overwrite(Overwrite::Auto).paused(true),
|
|
);
|
|
e.anim(b).set_total_time(0.0, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 1);
|
|
e.anim(a).set_total_time(0.5, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_none_lets_the_later_in_walk_win() {
|
|
standard("false_t1_playing", Overwrite::None, "playing");
|
|
}
|
|
|
|
#[test]
|
|
fn golden_kill_tweens_of_is_property_level() {
|
|
let c = case("design_kill_tweens_of_props");
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
e.seed(tg(0), Y, TweenValue::F64(0.0));
|
|
let a = e.to(
|
|
one(0),
|
|
&[to(X, 100.0), to(Y, 100.0)],
|
|
lin(1.0).events(EventMask::INTERRUPT),
|
|
);
|
|
names.name(a, "A");
|
|
e.anim(a).set_total_time(0.5, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 0);
|
|
e.kill_tweens_of(one(0), Some(&[X]));
|
|
snap(&mut e, &mut names, c, 1);
|
|
e.anim(a).set_total_time(1.0, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 2);
|
|
e.kill_tweens_of(one(0), Some(&[Y]));
|
|
snap(&mut e, &mut names, c, 3);
|
|
}
|
|
|
|
#[test]
|
|
fn golden_auto_waits_for_the_start_of_an_immediate_render() {
|
|
for (id, kind) in [
|
|
("design_auto_deferred_from", 0),
|
|
("design_auto_deferred_fromto", 1),
|
|
("design_auto_deferred_to_immediaterender", 2),
|
|
] {
|
|
let c = case(id);
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let a = e.to(
|
|
one(0),
|
|
&[to(X, 100.0)],
|
|
lin(1.0).events(EventMask::INTERRUPT),
|
|
);
|
|
names.name(a, "A");
|
|
e.anim(a).set_total_time(0.5, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 0);
|
|
let o = lin(1.0).overwrite(Overwrite::Auto).paused(true);
|
|
let b = match kind {
|
|
0 => e.from(one(0), &[PropTo::from(X, (-50.0).into())], o),
|
|
1 => e.from_to(one(0), &[PropTo::from_to(X, (-50.0).into(), 0.0.into())], o),
|
|
_ => e.to(one(0), &[to(X, 0.0)], o.immediate_render(true)),
|
|
};
|
|
snap(&mut e, &mut names, c, 1);
|
|
e.anim(b).set_total_time(0.0, Emit::Fire);
|
|
if kind == 0 {
|
|
// GSAP's from() kills already at this render at exactly 0; the
|
|
// engine waits for a render past 0 for from() and fromTo()
|
|
// alike (deviation 17).
|
|
assert_eq!(list(c.steps[2].vals, "callbacks"), ["A:onInterrupt"]);
|
|
assert!(names.drain(&mut e).1.is_empty());
|
|
close(val(&e, 0, X), num(c.steps[2].vals, "obj.x"), VAL_TOL, id);
|
|
e.anim(b).set_total_time(0.1, Emit::Fire);
|
|
assert_eq!(names.drain(&mut e).1, ["A:onInterrupt"]);
|
|
close(val(&e, 0, X), num(c.steps[3].vals, "obj.x"), VAL_TOL, id);
|
|
continue;
|
|
}
|
|
snap(&mut e, &mut names, c, 2);
|
|
e.anim(b).set_total_time(0.1, Emit::Fire);
|
|
snap(&mut e, &mut names, c, 3);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn kill_survives_rewind() {
|
|
let mut e = TweenEngine::new();
|
|
e.seed(tg(0), X, TweenValue::F64(0.0));
|
|
let tl = e.timeline(TimelineOpts::new().paused(true));
|
|
e.tl(tl).to(one(0), &[to(X, 100.0)], lin(1.0), 0.0).to(
|
|
one(0),
|
|
&[to(X, 0.0)],
|
|
lin(1.0).overwrite(Overwrite::Auto),
|
|
0.5,
|
|
);
|
|
e.anim(tl).set_total_time(0.75, Emit::Fire);
|
|
// The first rendered to 75, then the second captured 75 and killed it.
|
|
close(val(&e, 0, X), 56.25, 1e-9, "the second took over from 75");
|
|
e.anim(tl).set_total_time(0.25, Emit::Fire);
|
|
// Rewound before the second's start: the first stays dead, the second
|
|
// renders its start value.
|
|
close(val(&e, 0, X), 75.0, 1e-9, "no resurrection");
|
|
}
|
|
|
|
#[test]
|
|
fn overwrite_all_in_a_stagger_kills_the_group() {
|
|
let mut e = TweenEngine::new();
|
|
let mut names = Names::new();
|
|
seed_all(&mut e, 3, X, 0.0);
|
|
let g = e.to(
|
|
Targets::Range { first: 0, count: 3 },
|
|
&[to(X, 100.0)],
|
|
lin(1.0)
|
|
.stagger(Stagger::each(0.1))
|
|
.events(EventMask::INTERRUPT),
|
|
);
|
|
names.name(g, "g");
|
|
e.advance(0.2);
|
|
e.to(
|
|
Targets::Range { first: 0, count: 3 },
|
|
&[to(X, 0.0)],
|
|
lin(1.0).overwrite(Overwrite::All),
|
|
);
|
|
assert_eq!(names.drain(&mut e).1, ["g:onInterrupt"]);
|
|
assert!(!e.anim_ref(g).is_alive());
|
|
}
|