use std::time::Duration;
use frust_core::{
AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, Curve, DiscardScene, EditingState,
EventCtx, EventResult, ImeState, InputEvent, LayoutCtx, PaintCtx, PaintScene, RetainedKey,
SemanticsCtx, View, Widget, any,
};
use frust_theme::Theme;
use kurbo::{Affine, Point, Size, Vec2};
use crate::ChildKey;
use crate::Timing;
use crate::motion::patterns::{FadeThrough, PatternLayer, TransitionPattern};
use crate::nav::transition::{TransitionDriver, make_driver};
use crate::repaint_boundary::{RetainedBoundary, within};
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,
boundary: &mut RetainedBoundary,
ctx: &mut PaintCtx,
scene: &mut dyn PaintScene,
layer: PatternLayer,
) {
pod.set_origin(Point::new(layer.dx, layer.dy));
let alpha = layer.alpha.clamp(0.0, 1.0);
if alpha <= 0.0 {
boundary.paint_plain(pod, ctx, &mut DiscardScene);
return;
}
let origin = ctx.origin();
let size = ctx.size();
let child_origin = origin + Vec2::new(layer.dx, layer.dy);
if (layer.scale - 1.0).abs() <= f64::EPSILON {
let may_record = within(child_origin, size, origin, size);
boundary.paint(pod, ctx, scene, child_origin, size, alpha, may_record);
return;
}
let center = Point::new(
child_origin.x + size.width / 2.0,
child_origin.y + size.height / 2.0,
);
let layered = alpha < 1.0;
scene.push_transform(Affine::scale_about(layer.scale, center));
if layered {
scene.push_layer(child_origin, size, alpha);
}
boundary.paint_plain(pod, ctx, scene);
if layered {
scene.pop_layer();
}
scene.pop_transform();
}
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,
child_boundary: RetainedBoundary,
exiting_boundary: RetainedBoundary,
}
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> {
let keys = RetainedKey::mint_range(2);
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,
child_boundary: RetainedBoundary::with_key(keys),
exiting_boundary: RetainedBoundary::with_key(RetainedKey(keys.get() + 1)),
}
}
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;
}
}
crate::authoring::teardown_child(&prev.child, &mut old, ctx);
element.exiting = Some(old);
std::mem::swap(&mut element.child_boundary, &mut element.exiting_boundary);
element.child_boundary = RetainedBoundary::with_key(element.child_boundary.key());
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,
&mut self.exiting_boundary,
ctx,
scene,
exiting_layer,
);
paint_staged_child(
&mut self.child,
&mut self.child_boundary,
ctx,
scene,
incoming_layer,
);
if adv.animating {
ctx.request_frame();
}
if adv.done {
self.settled = true;
ctx.request_frame();
}
} else {
self.child_boundary.paint_plain(&mut self.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::{FlexView, column};
use frust_core::{
FrameTime, PointerButton, PointerEvent, PointerPhase, RenderRoot, RetainedLedger,
};
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 RetainedRecordingScene {
rects: Vec<(Point, Size)>,
layers: Vec<(Point, Size, f32)>,
transform_pushes: u32,
retained_pushes: Vec<(u64, Point, Size, f32, f64)>,
retained_pops: u32,
draws: Vec<(u64, Point, Size, f32, f64)>,
}
impl PaintScene for RetainedRecordingScene {
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_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
self.retained_pushes.push((key, origin, size, alpha, scale));
}
fn pop_retained(&mut self) {
self.retained_pops += 1;
}
fn draw_retained(&mut self, key: u64, origin: Point, size: Size, alpha: f32, scale: f64) {
self.draws.push((key, origin, size, alpha, scale));
}
}
struct Recorder {
cancelled: Rc<Cell<bool>>,
torn: Rc<Cell<u32>>,
teardowns: 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
}
fn teardown(&self, _element: &mut RecorderWidget, _ctx: &mut BuildCtx<'_>) {
self.teardowns.set(self.teardowns.get() + 1);
}
}
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(),
teardowns: Rc::new(Cell::new(0)),
}
}
fn counting_recorder(torn: &Rc<Cell<u32>>, teardowns: &Rc<Cell<u32>>) -> Recorder {
Recorder {
cancelled: Rc::new(Cell::new(false)),
torn: torn.clone(),
teardowns: teardowns.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, 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");
}
#[test]
fn freeze_teardown_of_a_focused_child_raises_no_second_focus_release() {
let cancelled = Rc::new(Cell::new(false));
let torn = Rc::new(Cell::new(0u32));
let teardowns = Rc::new(Cell::new(0u32));
let v1: PatternSwitcherView<(), _> = pattern_switcher(
1u32,
FadeThrough,
Recorder {
cancelled: cancelled.clone(),
torn: torn.clone(),
teardowns: teardowns.clone(),
},
);
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() && w.child.is_focused());
let _ = frust_core::take_focus_orphaned();
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_eq!(
teardowns.get(),
1,
"the focused child was torn down at the switch"
);
assert!(cancelled.get(), "cancelled exactly as before");
assert!(w.needs_ime_clear, "the IME clear is the session release");
assert!(
!frust_core::take_focus_orphaned(),
"the teardown must not raise a second (orphan) release"
);
}
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()
.child(PersistentField {
size: Size::new(100.0, 40.0),
})
.child(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_torn_down_at_the_switch_and_dropped_after_settle() {
let torn = Rc::new(Cell::new(0u32));
let teardowns = Rc::new(Cell::new(0u32));
let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, counting_recorder(&torn, &teardowns)).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!(teardowns.get(), 1, "torn down at the switch, exactly once");
assert_eq!(
torn.get(),
0,
"its pod is not dropped 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 dropped exactly once, after progress 1.0"
);
assert_eq!(
teardowns.get(),
1,
"the settle drop is not a second teardown"
);
assert!(!w.settled);
assert!(w.driver.is_none());
v3.teardown(&mut w, &mut BuildCtx::new(&mut counter));
assert_eq!((teardowns.get(), torn.get()), (1, 1));
}
#[test]
fn a_navigator_in_the_outgoing_child_unmounts_on_the_switch_but_still_paints() {
use crate::test_support::leaf;
use crate::{NavigatorController, navigator};
let ctrl: NavigatorController<()> = NavigatorController::new();
let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
let section = |key: u32| -> PatternSwitcherView<(), FadeThrough> {
let child = if key == 1 {
any(navigator(&ctrl, || leaf(10.0, 10.0)))
} else {
leaf_any(10.0, 10.0)
};
pattern_switcher(key, FadeThrough, child).timing(short)
};
let mut counter = 0u64;
let v1 = section(1);
let mut w = build(&v1);
assert!(ctrl.is_mounted());
ctrl.push(|| leaf(40.0, 40.0));
let v2 = section(1);
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
assert_eq!(ctrl.depth(), 2);
assert!(
ctrl.back_interest(),
"a mounted depth-2 navigator claims back"
);
let v3 = section(2);
v3.rebuild(&v2, &mut w, &mut BuildCtx::new(&mut counter));
assert!(!ctrl.is_mounted(), "torn down at the switch, not at settle");
assert!(
!ctrl.back_interest(),
"a torn-down navigator claims nothing, with no re-wire in between"
);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let mut scene = RecordingScene::default();
w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
assert!(
scene.rects.iter().any(|(_, s)| *s == Size::new(40.0, 40.0)),
"the torn-down navigator's top page still paints: {:?}",
scene.rects
);
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);
let v4 = section(2);
v4.rebuild(&v3, &mut w, &mut BuildCtx::new(&mut counter));
assert!(w.exiting.is_none());
assert!(!ctrl.is_mounted());
assert!(!ctrl.back_interest());
}
struct ProbeComp {
teardowns: Rc<Cell<u32>>,
state_drops: Rc<Cell<u32>>,
}
struct ProbeState {
drops: Rc<Cell<u32>>,
}
impl Drop for ProbeState {
fn drop(&mut self) {
self.drops.set(self.drops.get() + 1);
}
}
impl frust_core::Component for ProbeComp {
type State = ProbeState;
fn init(&self) -> ProbeState {
ProbeState {
drops: self.state_drops.clone(),
}
}
fn build(&self, _state: &mut ProbeState) -> impl View<ProbeState> {
TeardownLeaf {
teardowns: self.teardowns.clone(),
}
}
}
struct TeardownLeaf {
teardowns: Rc<Cell<u32>>,
}
impl<S: 'static> View<S> for TeardownLeaf {
type Element = crate::test_support::LeafWidget;
fn build(&self, ctx: &mut BuildCtx<'_>) -> Self::Element {
View::<()>::build(&crate::test_support::leaf(30.0, 30.0), ctx)
}
fn rebuild(
&self,
_prev: &Self,
_element: &mut Self::Element,
_ctx: &mut BuildCtx<'_>,
) -> ChangeFlags {
ChangeFlags::NONE
}
fn teardown(&self, _element: &mut Self::Element, _ctx: &mut BuildCtx<'_>) {
self.teardowns.set(self.teardowns.get() + 1);
}
}
#[test]
fn a_component_in_the_outgoing_child_is_torn_down_exactly_once() {
let teardowns = Rc::new(Cell::new(0u32));
let state_drops = Rc::new(Cell::new(0u32));
let short = Timing::Duration(Duration::from_millis(100), Curve::Linear);
let comp = || {
any(frust_core::component(ProbeComp {
teardowns: teardowns.clone(),
state_drops: state_drops.clone(),
}))
};
let mut counter = 0u64;
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, comp()).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);
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
assert_eq!(
teardowns.get(),
1,
"the component is torn down at the freeze"
);
assert_eq!(
state_drops.get(),
0,
"its widget (and state) is still painting"
);
let mut scene = RecordingScene::default();
w.layout(&mut LayoutCtx::new(), &loose());
w.paint(&mut PaintCtx::new(Point::ZERO, size), &mut scene);
assert!(
scene.rects.iter().any(|(_, s)| *s == Size::new(30.0, 30.0)),
"the disposed component's subtree still paints: {:?}",
scene.rects
);
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(),
);
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());
assert_eq!(state_drops.get(), 1, "dropped once at settle");
assert_eq!(
teardowns.get(),
1,
"the settle drop is not a second teardown"
);
v3.teardown(&mut w, &mut BuildCtx::new(&mut counter));
assert_eq!((teardowns.get(), state_drops.get()), (1, 1));
teardowns.set(0);
state_drops.set(0);
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, comp()).timing(short);
let mut w = build(&v1);
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, FadeThrough, leaf_any(10.0, 10.0)).timing(short);
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
assert_eq!((teardowns.get(), state_drops.get()), (1, 0));
v2.teardown(&mut w, &mut BuildCtx::new(&mut counter));
assert!(
w.exiting.is_none(),
"the switcher's teardown drops the exiting pod"
);
assert_eq!(
(teardowns.get(), state_drops.get()),
(1, 1),
"torn down once at the freeze, dropped once by the switcher's teardown"
);
}
#[test]
fn superseding_switches_tear_each_outgoing_child_down_exactly_once() {
let counters = || (Rc::new(Cell::new(0u32)), Rc::new(Cell::new(0u32)));
let (a_drop, a_td) = counters();
let (b_drop, b_td) = counters();
let (c_drop, c_td) = counters();
let mut counter = 0u64;
let va: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, counting_recorder(&a_drop, &a_td));
let mut w = build(&va);
let vb: PatternSwitcherView<(), _> =
pattern_switcher(2u32, FadeThrough, counting_recorder(&b_drop, &b_td));
vb.rebuild(&va, &mut w, &mut BuildCtx::new(&mut counter));
assert_eq!(
(a_td.get(), a_drop.get()),
(1, 0),
"A frozen: torn down, kept"
);
assert_eq!((b_td.get(), b_drop.get()), (0, 0), "B is live");
let vc: PatternSwitcherView<(), _> =
pattern_switcher(3u32, FadeThrough, counting_recorder(&c_drop, &c_td));
vc.rebuild(&vb, &mut w, &mut BuildCtx::new(&mut counter));
assert_eq!(
(a_td.get(), a_drop.get()),
(1, 1),
"A superseded: dropped only"
);
assert_eq!(
(b_td.get(), b_drop.get()),
(1, 0),
"B frozen: torn down, kept"
);
assert_eq!((c_td.get(), c_drop.get()), (0, 0), "C is live");
vc.teardown(&mut w, &mut BuildCtx::new(&mut counter));
assert_eq!((a_td.get(), a_drop.get()), (1, 1));
assert_eq!((b_td.get(), b_drop.get()), (1, 1), "B dropped, not re-torn");
assert_eq!(
c_td.get(),
1,
"the live child is torn down by the switcher's teardown"
);
drop(w);
assert_eq!(
(a_td.get(), b_td.get(), c_td.get()),
(1, 1, 1),
"each child torn down exactly once"
);
assert_eq!((a_drop.get(), b_drop.get(), c_drop.get()), (1, 1, 1));
}
#[test]
fn same_key_rebuilds_reconcile_in_place_with_no_teardown() {
let torn = Rc::new(Cell::new(0u32));
let teardowns = Rc::new(Cell::new(0u32));
let mut counter = 0u64;
let make = || -> PatternSwitcherView<(), FadeThrough> {
pattern_switcher(7u32, FadeThrough, counting_recorder(&torn, &teardowns))
};
let mut prev = make();
let mut w = build(&prev);
for _ in 0..3 {
let next = make();
next.rebuild(&prev, &mut w, &mut BuildCtx::new(&mut counter));
prev = next;
}
assert!(w.exiting.is_none(), "no transition staged");
assert_eq!(
(teardowns.get(), torn.get()),
(0, 0),
"the live child was reconciled in place: never torn down, never dropped"
);
}
#[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 RetainedBoundary::new(),
&mut PaintCtx::new(Point::ZERO, size),
&mut scene,
layer,
);
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"
);
}
fn resident(keys: &[RetainedKey]) -> RetainedLedger {
RetainedLedger::from_entries(1, keys.iter().map(|k| (k.get(), (10, 10), 1.0)))
}
fn switched<P: TransitionPattern + Clone + 'static>(
pattern: P,
) -> (PatternSwitcherWidget<P>, Size, RetainedKey) {
let timing = Timing::Duration(Duration::from_millis(100), Curve::Linear);
let v1: PatternSwitcherView<(), _> =
pattern_switcher(1u32, pattern.clone(), leaf_any(10.0, 10.0)).timing(timing);
let mut w = build(&v1);
let size = w.layout(&mut LayoutCtx::new(), &loose());
let live_key = w.child_boundary.key();
let v2: PatternSwitcherView<(), _> =
pattern_switcher(2u32, pattern, leaf_any(10.0, 10.0)).timing(timing);
let mut counter = 0u64;
v2.rebuild(&v1, &mut w, &mut BuildCtx::new(&mut counter));
w.layout(&mut LayoutCtx::new(), &loose());
(w, size, live_key)
}
#[test]
fn the_seed_frame_records_the_exiting_child_under_the_key_it_held_while_live() {
let (mut w, size, live_key) = switched(SharedAxis::X);
assert_eq!(
w.exiting_boundary.key(),
live_key,
"the frozen pod keeps the boundary it painted through while live"
);
assert_ne!(
w.child_boundary.key(),
live_key,
"the fresh child takes the other key"
);
let ledger = RetainedLedger::empty();
let mut scene = RetainedRecordingScene::default();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&ledger),
&mut scene,
);
assert_eq!(
scene.retained_pushes,
vec![(live_key.get(), Point::ZERO, size, 1.0, 1.0)],
"one recording bracket, keyed by the exiting child's own key, at scale 1"
);
assert_eq!(scene.retained_pops, 1, "the bracket is balanced");
assert!(scene.draws.is_empty(), "nothing is resident yet");
}
#[test]
fn a_clean_resident_exiting_child_composites_its_page_as_it_fades() {
let (mut w, size, live_key) = switched(FadeScale);
let empty = RetainedLedger::empty();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&empty),
&mut RetainedRecordingScene::default(),
);
let ledger = resident(&[live_key]);
let mut scene = RetainedRecordingScene::default();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)).with_retained_ledger(&ledger),
&mut scene,
);
assert_eq!(
scene.draws.len(),
1,
"the exiting page is drawn: {:?}",
scene.draws
);
let (key, origin, draw_size, alpha, scale) = scene.draws[0];
assert_eq!(
(key, origin, draw_size, scale),
(live_key.get(), Point::ZERO, size, 1.0)
);
assert!(
alpha > 0.0 && alpha < 1.0,
"at the fade's own alpha (was {alpha})"
);
assert_eq!(
scene.rects.len(),
1,
"only the incoming child paints a body; the exiting one is composited"
);
assert!(
scene.retained_pushes.is_empty(),
"the incoming child's scale step is never bracketed: {:?}",
scene.retained_pushes
);
assert_eq!(
scene.transform_pushes, 1,
"the incoming child's scale step paints inline under a push_transform"
);
}
#[test]
fn without_a_ledger_staged_children_paint_with_no_bracket() {
let (mut w, size, _) = switched(FadeScale);
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)),
&mut RetainedRecordingScene::default(),
);
let mut scene = RetainedRecordingScene::default();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(15.0)),
&mut scene,
);
assert!(scene.retained_pushes.is_empty() && scene.draws.is_empty());
assert_eq!(scene.rects.len(), 2, "both children paint their bodies");
assert_eq!(scene.layers.len(), 2, "each under its own fade layer");
}
#[test]
fn a_slid_child_is_never_recorded_while_it_overhangs_the_switcher() {
let (mut w, size, _) = switched(SharedAxis::X);
let ledger = RetainedLedger::empty();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(0.0)).with_retained_ledger(&ledger),
&mut RetainedRecordingScene::default(),
);
let mut scene = RetainedRecordingScene::default();
w.paint(
&mut PaintCtx::for_test(Point::ZERO, size, ft_ms(50.0)).with_retained_ledger(&ledger),
&mut scene,
);
assert!(
scene.retained_pushes.is_empty(),
"no bracket: {:?}",
scene.retained_pushes
);
assert_eq!(
scene.rects,
vec![(Point::new(15.0, 0.0), size)],
"the incoming child paints at its slid origin"
);
}
fn key_pair<P: TransitionPattern + Clone + 'static>(w: &PatternSwitcherWidget<P>) -> [u64; 2] {
let mut keys = [w.child_boundary.key().get(), w.exiting_boundary.key().get()];
keys.sort_unstable();
keys
}
#[test]
fn the_key_pair_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 pair = key_pair(&w);
assert_eq!(pair[1], pair[0] + 1, "a reserved consecutive pair");
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!(
key_pair(&w),
pair,
"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!(
key_pair(&w),
pair,
"a second switch on the same widget still keeps the same key pair"
);
}
#[test]
fn two_switcher_instances_get_disjoint_key_pairs() {
let v: PatternSwitcherView<(), _> =
pattern_switcher(1u32, FadeThrough, leaf_any(10.0, 10.0));
let w1 = build(&v);
let w2 = build(&v);
let (a, b) = (key_pair(&w1), key_pair(&w2));
assert!(
a.iter().all(|k| !b.contains(k)),
"two switcher instances never share a key: {a:?} vs {b:?}"
);
}
}