use std::sync::atomic::Ordering;
use std::time::Duration;
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, Curve, DiscardScene, EditingState,
EventCtx, EventResult, ImeState, InputEvent, LayoutCtx, PaintCtx, PaintScene, SemanticsCtx,
View, Widget, any,
};
use frust_theme::Theme;
use kurbo::{Point, Size, Vec2};
use crate::ChildKey;
use crate::Timing;
use crate::motion::patterns::{FadeThrough, PatternLayer, TransitionPattern};
use crate::nav::navigator::NEXT_SNAPSHOT_KEY;
use crate::nav::transition::{TransitionDriver, make_driver};
pub const FALLBACK_DURATION: Duration = Duration::from_millis(220);
pub const REDUCE_MOTION_DURATION: Duration = Duration::from_millis(120);
fn resolve_switch_timing(theme: Option<&Theme>, explicit: Option<Timing>) -> Timing {
explicit.unwrap_or_else(|| match theme {
Some(theme) => Timing::Duration(
Duration::from_secs_f64(theme.motion.durations.base / 1000.0),
theme.motion.easing.effects,
),
None => Timing::Duration(FALLBACK_DURATION, Curve::Emphasized),
})
}
fn paint_staged_child(
pod: &mut ChildPod,
ctx: &mut PaintCtx,
scene: &mut dyn PaintScene,
layer: PatternLayer,
snapshot: u64,
) {
pod.set_origin(Point::new(layer.dx, layer.dy));
let alpha = layer.alpha.clamp(0.0, 1.0);
if alpha <= 0.0 {
let mut sink = DiscardScene;
pod.paint_child(ctx, &mut sink);
return;
}
let bracket_origin = ctx.origin() + Vec2::new(layer.dx, layer.dy);
scene.push_snapshot(snapshot, bracket_origin, ctx.size(), alpha, layer.scale);
pod.paint_child(ctx, scene);
scene.pop_snapshot();
}
fn cleared_ime_state() -> ImeState {
ImeState {
active: false,
editing: EditingState {
text: String::new(),
selection_base: -1,
selection_extent: -1,
composing_base: -1,
composing_extent: -1,
},
caret: None,
content_type: Default::default(),
suppress_soft_keyboard: false,
}
}
pub struct PatternSwitcherView<State: 'static, P: TransitionPattern + Clone + 'static> {
key: ChildKey,
pattern: P,
timing: Option<Timing>,
reverse: bool,
child: AnyView<State>,
}
pub fn pattern_switcher<State, P, V>(
key: impl Into<ChildKey>,
pattern: P,
child: V,
) -> PatternSwitcherView<State, P>
where
State: 'static,
P: TransitionPattern + Clone + 'static,
V: View<State>,
{
PatternSwitcherView {
key: key.into(),
pattern,
timing: None,
reverse: false,
child: any(child),
}
}
#[allow(non_snake_case)]
pub fn PatternSwitcher<State, P, V>(
key: impl Into<ChildKey>,
pattern: P,
child: V,
) -> PatternSwitcherView<State, P>
where
State: 'static,
P: TransitionPattern + Clone + 'static,
V: View<State>,
{
pattern_switcher(key, pattern, child)
}
impl<State: 'static, P: TransitionPattern + Clone + 'static> PatternSwitcherView<State, P> {
pub fn timing(mut self, timing: Timing) -> Self {
self.timing = Some(timing);
self
}
pub fn reverse(mut self, reverse: bool) -> Self {
self.reverse = reverse;
self
}
}
pub struct PatternSwitcherWidget<P: TransitionPattern + Clone + 'static> {
key: ChildKey,
child: ChildPod,
exiting: Option<ChildPod>,
pattern: P,
explicit_timing: Option<Timing>,
reverse: bool,
driver: Option<TransitionDriver>,
pending_start: bool,
settled: bool,
reduce_motion: bool,
needs_ime_clear: bool,
snapshot_base: u64,
}
impl<State: 'static, P: TransitionPattern + Clone + 'static> View<State>
for PatternSwitcherView<State, P>
{
type Element = PatternSwitcherWidget<P>;
fn build(&self, ctx: &mut BuildCtx<'_>) -> PatternSwitcherWidget<P> {
PatternSwitcherWidget {
key: self.key,
child: crate::authoring::build_child(&self.child, ctx),
exiting: None,
pattern: self.pattern.clone(),
explicit_timing: self.timing,
reverse: self.reverse,
driver: None,
pending_start: false,
settled: false,
reduce_motion: false,
needs_ime_clear: false,
snapshot_base: NEXT_SNAPSHOT_KEY.fetch_add(2, Ordering::Relaxed),
}
}
fn rebuild(
&self,
prev: &Self,
element: &mut PatternSwitcherWidget<P>,
ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
let mut flags = ChangeFlags::NONE;
element.pattern = self.pattern.clone();
if element.explicit_timing != self.timing {
element.explicit_timing = self.timing;
flags |= ChangeFlags::PAINT;
}
if element.reverse != self.reverse {
element.reverse = self.reverse;
flags |= ChangeFlags::PAINT;
}
if element.settled {
element.exiting = None;
element.driver = None;
element.settled = false;
}
if self.key != element.key {
element.exiting = None;
element.driver = None;
element.reduce_motion = false;
let mut old = std::mem::replace(
&mut element.child,
crate::authoring::build_child(&self.child, ctx),
);
if old.is_active() {
crate::authoring::cancel_pod(&mut old);
old.set_active(false);
}
if old.is_focused() {
old.set_focused(false);
if ctx.has_focus() {
element.needs_ime_clear = true;
}
}
element.exiting = Some(old);
element.key = self.key;
element.pending_start = true;
flags |= ChangeFlags::LAYOUT | ChangeFlags::PAINT;
} else {
flags |=
crate::authoring::rebuild_child(&prev.child, &self.child, &mut element.child, ctx);
}
flags
}
fn teardown(&self, element: &mut PatternSwitcherWidget<P>, ctx: &mut BuildCtx<'_>) {
crate::authoring::teardown_child(&self.child, &mut element.child, ctx);
element.exiting = None;
}
}
impl<P: TransitionPattern + Clone + 'static> Widget for PatternSwitcherWidget<P> {
fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
let size = self.child.layout_child(ctx, bc);
self.child.set_origin(Point::ZERO);
if let Some(exiting) = &mut self.exiting {
exiting.layout_child(ctx, bc);
exiting.set_origin(Point::ZERO);
}
size
}
fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
if self.pending_start {
let theme = Theme::from_paint_ctx(ctx);
let reduce = theme.map(|t| t.motion.reduce_motion).unwrap_or(false);
let timing = if reduce {
Timing::Duration(REDUCE_MOTION_DURATION, Curve::Linear)
} else {
resolve_switch_timing(theme, self.explicit_timing)
};
self.driver = Some(make_driver(timing).0);
self.reduce_motion = reduce;
self.pending_start = false;
}
if let (Some(exiting), Some(driver)) = (self.exiting.as_mut(), self.driver.as_mut()) {
let adv = driver.advance(ctx.frame_time());
let size = ctx.size();
let (incoming_layer, exiting_layer) = if self.reduce_motion {
FadeThrough.resolve(adv.value, self.reverse, size)
} else {
self.pattern.resolve(adv.value, self.reverse, size)
};
paint_staged_child(exiting, ctx, scene, exiting_layer, self.snapshot_base + 1);
paint_staged_child(
&mut self.child,
ctx,
scene,
incoming_layer,
self.snapshot_base,
);
if adv.animating {
ctx.request_frame();
}
if adv.done {
self.settled = true;
ctx.request_frame();
}
} else {
self.child.paint_child(ctx, scene);
}
if self.needs_ime_clear {
ctx.publish_ime_state(cleared_ime_state());
self.needs_ime_clear = false;
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
crate::authoring::route_event_single(&mut self.child, ctx, event)
}
fn semantics(&self, ctx: &mut SemanticsCtx) {
self.child.semantics_child(ctx);
}
crate::authoring::visit_children!(exiting, child);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::motion::patterns::{FadeScale, SharedAxis};
use crate::test_support::{RecordingScene, leaf_any};
use crate::{Column, FlexView};
use frust_core::{FrameTime, PointerButton, PointerEvent, PointerPhase, RenderRoot};
use kurbo::Rect;
use std::cell::Cell;
use std::rc::Rc;
fn ft_ms(ms: f64) -> FrameTime {
FrameTime::from_nanos((ms * 1_000_000.0) as u64)
}
fn build<S: 'static, V: View<S>>(view: &V) -> V::Element {
let mut counter = 0u64;
view.build(&mut BuildCtx::new(&mut counter))
}
fn down(x: f64, y: f64) -> InputEvent {
InputEvent::Pointer(PointerEvent {
phase: PointerPhase::Down,
position: Point::new(x, y),
button: PointerButton::Primary,
})
}
fn loose() -> BoxConstraints {
BoxConstraints::loose(Size::new(100.0, 100.0))
}
#[derive(Default)]
struct SnapshotRecordingScene {
rects: Vec<(Point, Size)>,
layers: Vec<(Point, Size, f32)>,
transform_pushes: u32,
snapshots: Vec<(u64, Point, Size, f32, f64)>,
snapshot_pops: u32,
}
impl PaintScene for SnapshotRecordingScene {
fn fill_rect(&mut self, origin: Point, size: Size, _color: peniko::Color) {
self.rects.push((origin, size));
}
fn draw_text(&mut self, _origin: Point, _text: &str) {}
fn push_layer(&mut self, origin: Point, size: Size, alpha: f32) {
self.layers.push((origin, size, alpha));
}
fn push_transform(&mut self, _transform: kurbo::Affine) {
self.transform_pushes += 1;
}
fn push_snapshot(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
self.snapshots.push((key, origin, size, alpha, scale));
}
fn pop_snapshot(&mut self) {
self.snapshot_pops += 1;
}
}
struct Recorder {
cancelled: Rc<Cell<bool>>,
torn: Rc<Cell<u32>>,
}
struct RecorderWidget {
cancelled: Rc<Cell<bool>>,
torn: Rc<Cell<u32>>,
}
impl View<()> for Recorder {
type Element = RecorderWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
RecorderWidget {
cancelled: self.cancelled.clone(),
torn: self.torn.clone(),
}
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut RecorderWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
impl Widget for RecorderWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.max()
}
fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let InputEvent::Pointer(p) = event {
match p.phase {
PointerPhase::Down => {
ctx.capture_pointer();
ctx.request_focus();
return EventResult::Handled;
}
PointerPhase::Cancel => {
self.cancelled.set(true);
return EventResult::Handled;
}
_ => {}
}
}
EventResult::Ignored
}
}
impl Drop for RecorderWidget {
fn drop(&mut self) {
self.torn.set(self.torn.get() + 1);
}
}
fn recorder(cancelled: &Rc<Cell<bool>>, torn: &Rc<Cell<u32>>) -> Recorder {
Recorder {
cancelled: cancelled.clone(),
torn: torn.clone(),
}
}
fn reduced_motion_theme() -> Theme {
let mut theme = Theme::neutral();
theme.motion.reduce_motion = true;
theme
}
#[test]
fn same_identity_rebuild_does_not_transition_but_changed_identity_does() {
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
let mut w = build(&v1);
assert!(w.exiting.is_none());
let v2: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, leaf_any(20.0, 20.0));
let mut counter = 0u64;
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
assert!(
w.exiting.is_none(),
"same identity must not stage a transition"
);
assert!(!w.pending_start);
let v3: PatternSwitcherView<(), _> =
pattern_switcher(2u32, FadeThrough, leaf_any(30.0, 30.0));
v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
assert!(
w.exiting.is_some(),
"an identity change must retain the exiting child"
);
assert!(w.pending_start, "the next paint starts the driver");
assert_eq!(w.key, 2u32.into());
}
#[test]
fn identity_change_cancels_focus_and_clears_ime_on_the_exiting_child() {
let cancelled = Rc::new(Cell::new(false));
let torn = Rc::new(Cell::new(0u32));
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, any(recorder(&cancelled, &torn)));
let mut w = build(&v1);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let mut st = ();
let mut ectx = EventCtx::new(&mut st, Point::ZERO, size);
w.event(&mut ectx, &down(5.0, 5.0));
assert!(w.child.is_active(), "the child captured the pointer");
assert!(w.child.is_focused(), "the child took focus");
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
let mut counter = 0u64;
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
assert!(
cancelled.get(),
"the exiting child received a synthetic Cancel"
);
let exiting = w.exiting.as_ref().expect("exiting child retained");
assert!(
!exiting.is_focused(),
"the exiting child's focus was cleared"
);
assert!(!exiting.is_active(), "its capture path was released");
assert!(
w.needs_ime_clear,
"an IME clear is queued for the next paint"
);
let mut pctx = PaintCtx::new(Point::ZERO, size);
let mut scene = RecordingScene::default();
w.paint(&mut pctx, &mut scene);
let ime = pctx
.take_ime_state()
.expect("a cleared IME surface was published on the post-switch paint");
assert!(!ime.active);
assert!(ime.editing.text.is_empty());
assert!(!w.needs_ime_clear, "the queued clear was consumed");
}
fn field_surface() -> ImeState {
ImeState {
active: true,
editing: EditingState {
text: "query".to_string(),
selection_base: 5,
selection_extent: 5,
composing_base: -1,
composing_extent: -1,
},
caret: Some(Rect::new(0.0, 0.0, 1.0, 12.0)),
content_type: Default::default(),
suppress_soft_keyboard: false,
}
}
struct PersistentField {
size: Size,
}
struct PersistentFieldWidget {
size: Size,
}
impl<S: 'static> View<S> for PersistentField {
type Element = PersistentFieldWidget;
fn build(&self, _ctx: &mut BuildCtx<'_>) -> PersistentFieldWidget {
PersistentFieldWidget { size: self.size }
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut PersistentFieldWidget,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
}
impl Widget for PersistentFieldWidget {
fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
bc.constrain(self.size)
}
fn paint(&mut self, ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {
if ctx.has_focus() {
ctx.publish_ime_state(field_surface());
}
}
fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
if let InputEvent::Pointer(p) = event
&& p.phase == PointerPhase::Down
{
ctx.request_focus();
ctx.publish_ime_state(field_surface());
return EventResult::Handled;
}
EventResult::Ignored
}
}
#[test]
fn identity_change_in_an_unfocused_switcher_leaves_a_sibling_fields_session_alive() {
let key = Rc::new(Cell::new(1u32));
let mut app = {
let key = key.clone();
move |_: &mut ()| {
Column(vec![
any(PersistentField {
size: Size::new(100.0, 40.0),
}),
any(pattern_switcher(
key.get(),
FadeThrough,
leaf_any(100.0, 40.0),
)),
])
}
};
let mut root: RenderRoot<(), FlexView<()>> = RenderRoot::new();
let mut state = ();
root.rebuild(&mut app, &mut state);
root.layout(Size::new(100.0, 100.0));
root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
root.event(&mut state, &down(5.0, 5.0));
assert!(
root.is_focus_active(),
"the sibling field owns the focus session"
);
assert_eq!(
root.ime_state().map(|s| s.editing.text),
Some("query".to_string()),
"…and the shell-facing surface is the field's"
);
let generation = root.focus_ime_generation();
key.set(2);
root.rebuild(&mut app, &mut state);
root.layout(Size::new(100.0, 100.0));
root.paint(&mut RecordingScene::default(), FrameTime::ZERO);
assert!(
root.is_focus_active(),
"an identity change in an unfocused switcher must not release the \
sibling field's session"
);
let ime = root
.ime_state()
.expect("the identity change dropped the sibling field's IME surface entirely");
assert!(ime.active, "the field's surface was left inactive");
assert_eq!(
ime.editing.text, "query",
"the field's surface was replaced with the switcher's cleared one"
);
assert_eq!(
root.focus_ime_generation(),
generation,
"an identity change in an unfocused switcher is not a focus/IME edge"
);
}
#[test]
fn exiting_child_is_retained_until_settle_then_torn_down() {
let cancelled = Rc::new(Cell::new(false));
let torn = Rc::new(Cell::new(0u32));
let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, any(recorder(&cancelled, &torn))).timing(short);
let mut w = build(&v1);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
let mut counter = 0u64;
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
assert!(w.exiting.is_some());
assert_eq!(
torn.get(),
0,
"exiting not torn down while the transition runs"
);
w.paint(
&mut PaintCtx::new(Point::ZERO, size),
&mut RecordingScene::default(),
);
assert!(w.exiting.is_some(), "retained during the transition");
assert_eq!(torn.get(), 0);
assert!(!w.settled);
w.driver
.as_mut()
.expect("driver created on first paint")
.advance(ft_ms(10_000.0));
w.paint(
&mut PaintCtx::new(Point::ZERO, size),
&mut RecordingScene::default(),
);
assert!(w.settled, "paint flags settle once the driver reaches rest");
assert!(
w.exiting.is_some(),
"still retained until the finalize rebuild"
);
assert_eq!(torn.get(), 0);
let v3: PatternSwitcherView<(), _> =
pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
assert!(w.exiting.is_none(), "exiting dropped after settle");
assert_eq!(
torn.get(),
1,
"exiting torn down exactly once, after progress 1.0"
);
assert!(!w.settled);
assert!(w.driver.is_none());
}
#[test]
fn reduce_motion_collapses_to_fast_crossfade() {
let theme = reduced_motion_theme();
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
let mut w = build(&v1);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
let mut counter = 0u64;
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
let mut pctx = PaintCtx::new(Point::ZERO, size).with_theme(&theme);
w.paint(&mut pctx, &mut RecordingScene::default());
assert!(
w.reduce_motion,
"reduce_motion must be resolved from the threaded theme"
);
assert_eq!(
w.child.origin(),
Point::ZERO,
"reduce_motion collapses to a non-directional crossfade (no slide)"
);
}
#[test]
fn without_reduce_motion_the_configured_directional_pattern_slides() {
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0));
let mut w = build(&v1);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0));
let mut counter = 0u64;
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
w.paint(
&mut PaintCtx::new(Point::ZERO, size),
&mut RecordingScene::default(),
);
assert!(!w.reduce_motion);
assert_eq!(w.child.origin().x, 30.0);
}
#[test]
fn steady_state_paints_the_single_child_with_no_layer_or_transform() {
let v: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
let mut w = build(&v);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let mut scene = RecordingScene::default();
w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
assert_eq!(scene.rects.len(), 1, "the single child paints its fill");
assert!(
scene.layers.is_empty(),
"no crossfade layer at steady state"
);
assert!(
scene.transforms.is_empty(),
"no scale transform at steady state"
);
}
#[test]
fn alpha_zero_staged_child_records_no_ops_and_no_zero_alpha_layer() {
let leaf = leaf_any(10.0, 10.0);
let mut counter = 0u64;
let mut pod = crate::authoring::build_child(&leaf, &mut BuildCtx::new(&mut counter));
let size = pod.layout_child(&mut LayoutCtx::new(), &loose());
pod.set_origin(Point::ZERO);
let mut scene = RecordingScene::default();
let layer = PatternLayer {
dx: 0.0,
dy: 0.0,
alpha: 0.0,
scale: 1.0,
};
paint_staged_child(
&mut pod,
&mut PaintCtx::new(Point::ZERO, size),
&mut scene,
layer,
1,
);
assert!(scene.rects.is_empty(), "an alpha-0 child records no fill");
assert!(
scene.layers.is_empty(),
"no zero-alpha push_layer is recorded either"
);
}
#[test]
fn staged_children_paint_through_a_snapshot_bracket_keyed_by_snapshot_base() {
let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeScale, leaf_any(10.0, 10.0)).timing(timing);
let mut w = build(&v1);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, FadeScale, leaf_any(10.0, 10.0)).timing(timing);
let mut counter = 0u64;
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
&mut SnapshotRecordingScene::default(),
);
let base = w.snapshot_base;
let mut scene = SnapshotRecordingScene::default();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)),
&mut scene,
);
assert_eq!(
scene.snapshots.len(),
2,
"both the incoming and frozen exiting child paint through a \
snapshot bracket: {:?}",
scene.snapshots
);
assert_eq!(scene.snapshot_pops, 2, "each bracket pops exactly once");
assert!(
scene.layers.is_empty(),
"no push_layer bracket of its own: {:?}",
scene.layers
);
assert_eq!(
scene.transform_pushes, 0,
"no push_transform bracket of its own"
);
let keys: Vec<u64> = scene.snapshots.iter().map(|(k, ..)| *k).collect();
assert!(
keys.contains(&base),
"the incoming child is keyed by snapshot_base: {keys:?}"
);
assert!(
keys.contains(&(base + 1)),
"the frozen exiting child is keyed by snapshot_base + 1: {keys:?}"
);
for (_, _, _, alpha, _) in &scene.snapshots {
assert!(*alpha > 0.0, "an alpha-0 child never reaches the bracket");
}
}
#[test]
fn bracket_origin_follows_the_pod_dx_in_slide_patterns() {
let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, SharedAxis::X, leaf_any(10.0, 10.0)).timing(timing);
let mut w = build(&v1);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, SharedAxis::X, leaf_any(10.0, 10.0)).timing(timing);
let mut counter = 0u64;
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
&mut SnapshotRecordingScene::default(),
);
let mut scene = SnapshotRecordingScene::default();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
&mut scene,
);
assert_eq!(
scene.snapshots.len(),
1,
"only the exiting child is visible at p=0"
);
let (_, origin, _, _, _) = scene.snapshots[0];
assert_eq!(
origin,
Point::new(0.0, 0.0),
"exiting child bracket origin at x=0.0 when dx=0 (was {:?})",
origin
);
let mut scene_mid = SnapshotRecordingScene::default();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(50.0)),
&mut scene_mid,
);
assert_eq!(
scene_mid.snapshots.len(),
1,
"at mid-frame (p=0.5), only one child is visible through a snapshot"
);
let (_, origin_mid, _, _, _) = scene_mid.snapshots[0];
assert_eq!(
origin_mid,
Point::new(15.0, 0.0),
"incoming child bracket origin at x=15.0 when dx=15.0 (was {:?})",
origin_mid
);
}
#[test]
fn snapshot_base_is_stable_across_multiple_switches_on_the_same_widget() {
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
let mut w = build(&v1);
let base = w.snapshot_base;
let mut counter = 0u64;
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0));
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
assert_eq!(
w.snapshot_base, base,
"a switch does not reassign the key pair"
);
let v3: PatternSwitcherView<(), _> =
pattern_switcher(3u32, FadeThrough, leaf_any(10.0, 10.0));
v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
assert_eq!(
w.snapshot_base, base,
"a second switch on the same widget still keeps the same key pair"
);
}
#[test]
fn two_switcher_instances_get_disjoint_snapshot_key_pairs() {
let v: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
let w1 = build(&v);
let w2 = build(&v);
assert_ne!(
w1.snapshot_base, w2.snapshot_base,
"two switcher instances never share a snapshot key pair"
);
}
}