use std::cell::{Cell, RefCell};
use rxrust::subscription::BoxedSubscription;
use crate::{prelude::*, ticker::FrameMsg, window::WindowFlags, wrap_render::*};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SizeMode {
Visual,
#[default]
Layout,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SizeEffect {
#[default]
Clip,
Scale,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PosAxes {
#[default]
None,
X,
Y,
Pos,
}
impl PosAxes {
fn has_x(self) -> bool { matches!(self, Self::X | Self::Pos) }
fn has_y(self) -> bool { matches!(self, Self::Y | Self::Pos) }
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SizeAxes {
#[default]
None,
Width,
Height,
Size,
}
impl SizeAxes {
fn has_width(self) -> bool { matches!(self, Self::Width | Self::Size) }
fn has_height(self) -> bool { matches!(self, Self::Height | Self::Size) }
}
#[declare(stateless)]
pub struct SmoothLayout {
#[declare(default = PosAxes::Pos)]
pos_axes: PosAxes,
#[declare(default = SizeAxes::Size)]
size_axes: SizeAxes,
#[declare(default)]
size_mode: SizeMode,
#[declare(default)]
size_effect: SizeEffect,
#[declare(custom, default = default_transition())]
transition: Rc<Box<dyn Transition>>,
#[declare(default)]
init_pos: Anchor,
#[declare(custom, default)]
init_width: Option<Measure>,
#[declare(custom, default)]
init_height: Option<Measure>,
#[declare(skip, default = Rc::new(Cell::new(MotionState::default())))]
motion: Rc<Cell<MotionState<Rect>>>,
}
#[derive(Clone, Copy)]
struct ActiveTween<T> {
from: T,
to: T,
started_at: Instant,
}
#[derive(Clone, Copy, Default)]
struct MotionState<T> {
target: T,
initialized: bool,
active: Option<ActiveTween<T>>,
}
#[derive(Clone, Copy, Default)]
struct GlobalRuntime {
motion: MotionState<Point>,
target_size: Size,
presented_global: Option<Point>,
}
fn update_cell<T: Copy, R>(cell: &Cell<T>, f: impl FnOnce(&mut T) -> R) -> R {
let mut value = cell.get();
let result = f(&mut value);
cell.set(value);
result
}
fn boxed_transition(t: impl Transition + 'static) -> Rc<Box<dyn Transition>> {
Rc::new(Box::new(t))
}
fn init_anchor(init_pos: &mut Option<Anchor>) -> &mut Anchor {
init_pos.get_or_insert_with(Anchor::default)
}
fn sample_motion<T: Copy>(
motion: &mut MotionState<T>, transition: &dyn Transition, now: Instant,
lerp: impl Fn(&T, &T, f32) -> T,
) -> T {
let Some(active) = motion.active else { return motion.target };
let progress = transition.rate_of_change(now - active.started_at);
if progress.is_finish() {
motion.active = None;
active.to
} else {
lerp(&active.from, &active.to, progress.value())
}
}
fn start_motion<T: Copy + PartialEq>(
motion: &mut MotionState<T>, from: T, to: T, now: Instant, enabled: bool,
) {
if !enabled || from == to {
motion.active = None;
} else {
motion.active = Some(ActiveTween { from, to, started_at: now });
}
}
fn restart_motion<T: Copy + PartialEq>(
motion: &mut MotionState<T>, current: T, target: T, now: Instant, enabled: bool,
) {
motion.target = target;
start_motion(motion, current, target, now, enabled);
}
fn initialize_motion<T: Copy + PartialEq>(
motion: &mut MotionState<T>, from: Option<T>, now: Instant, enabled: bool,
) {
let target = motion.target;
motion.initialized = true;
start_motion(motion, from.unwrap_or(target), target, now, enabled);
}
fn resolve_init_origin(
target: Point, size: Size, clamp: BoxClamp, axes: PosAxes, init_pos: &Anchor,
) -> Point {
let mut origin = target;
let max = Size::new(clamp.container_width(size.width), clamp.container_height(size.height));
if let Some(anchor) = init_pos.x.as_ref().filter(|_| axes.has_x()) {
origin.x = anchor.calculate(max.width, size.width);
}
if let Some(anchor) = init_pos.y.as_ref().filter(|_| axes.has_y()) {
origin.y = anchor.calculate(max.height, size.height);
}
origin
}
fn resolve_init_size(
target: Size, clamp: BoxClamp, axes: SizeAxes, init_width: Option<Measure>,
init_height: Option<Measure>,
) -> Size {
let mut size = target;
if let Some(width) = init_width.filter(|_| axes.has_width()) {
size.width = width.into_pixel(clamp.max.width);
}
if let Some(height) = init_height.filter(|_| axes.has_height()) {
size.height = height.into_pixel(clamp.max.height);
}
size
}
fn lerp_point_axes(from: &Point, to: &Point, factor: f32, axes: PosAxes) -> Point {
let mut out = *to;
if axes.has_x() {
out.x = from.x.lerp(&to.x, factor);
}
if axes.has_y() {
out.y = from.y.lerp(&to.y, factor);
}
out
}
fn lerp_size_axes(from: &Size, to: &Size, factor: f32, axes: SizeAxes) -> Size {
let mut out = *to;
if axes.has_width() {
out.width = from.width.lerp(&to.width, factor);
}
if axes.has_height() {
out.height = from.height.lerp(&to.height, factor);
}
out
}
fn lerp_rect_axes(from: &Rect, to: &Rect, factor: f32, pos: PosAxes, size: SizeAxes) -> Rect {
Rect::new(
lerp_point_axes(&from.origin, &to.origin, factor, pos),
lerp_size_axes(&from.size, &to.size, factor, size),
)
}
fn constrain_to_animated_axes(clamp: BoxClamp, size: Size, axes: SizeAxes) -> BoxClamp {
let mut constrained = clamp;
if axes.has_width() {
constrained.min.width = size.width;
constrained.max.width = size.width;
}
if axes.has_height() {
constrained.min.height = size.height;
constrained.max.height = size.height;
}
constrained
}
fn animations_on(w: &Window) -> bool { w.flags().contains(WindowFlags::ANIMATIONS) }
fn to_global(w: &Window, p: Option<WidgetId>, pos: Point) -> Point {
p.map_or(pos, |p| w.map_to_global(pos, p))
}
fn to_local(w: &Window, p: Option<WidgetId>, pos: Point) -> Point {
p.map_or(pos, |p| w.map_from_global(pos, p))
}
impl SmoothLayout {
fn has_pos_axes(&self) -> bool { self.pos_axes != PosAxes::None }
fn has_size_axes(&self) -> bool { self.size_axes != SizeAxes::None }
fn has_motion_axes(&self) -> bool { self.has_pos_axes() || self.has_size_axes() }
fn transition(&self) -> &dyn Transition { self.transition.as_ref().as_ref() }
fn motion_enabled(&self, window: &Window) -> bool {
self.has_motion_axes() && animations_on(window)
}
fn with_motion<R>(&self, f: impl FnOnce(&mut MotionState<Rect>) -> R) -> R {
update_cell(self.motion.as_ref(), f)
}
fn sample_rect(&self, motion: &mut MotionState<Rect>, now: Instant) -> Rect {
sample_motion(motion, self.transition(), now, |from, to, factor| {
lerp_rect_axes(from, to, factor, self.pos_axes, self.size_axes)
})
}
fn presented_rect(&self, now: Instant) -> Rect {
self.with_motion(|motion| self.sample_rect(motion, now))
}
fn is_animating(&self, now: Instant) -> bool {
self.with_motion(|motion| {
self.sample_rect(motion, now);
motion.active.is_some()
})
}
fn target_size(&self) -> Size { self.motion.get().target.size }
fn animated_size(&self, now: Instant) -> Size { self.presented_rect(now).size }
fn scale_factor(&self, now: Instant) -> Vector {
let target = self.target_size();
let animated = self.animated_size(now);
let sx = if target.width > 0. { animated.width / target.width } else { 1. };
let sy = if target.height > 0. { animated.height / target.height } else { 1. };
Vector::new(sx, sy)
}
fn required_dirty_phase(&self) -> DirtyPhase {
if !self.has_size_axes() && self.has_pos_axes() {
return DirtyPhase::Position;
}
if self.has_size_axes() && self.size_mode == SizeMode::Visual && !self.has_pos_axes() {
return DirtyPhase::Paint;
}
DirtyPhase::Layout
}
fn is_size_effect_active(&self, animations_on: bool, now: Instant) -> bool {
self.has_size_axes()
&& self.size_mode == SizeMode::Visual
&& animations_on
&& self.is_animating(now)
}
fn resolve_init_rect(&self, target: Rect, clamp: BoxClamp) -> Option<Rect> {
let size =
resolve_init_size(target.size, clamp, self.size_axes, self.init_width, self.init_height);
let origin = resolve_init_origin(target.origin, size, clamp, self.pos_axes, &self.init_pos);
let from = Rect::new(origin, size);
(from != target).then_some(from)
}
}
impl<'c> ComposeChild<'c> for SmoothLayout {
type Child = Widget<'c>;
fn compose_child(this: impl StateWriter<Value = Self>, child: Self::Child) -> Widget<'c> {
WrapRender::combine_child(this, child)
}
}
impl WrapRender for SmoothLayout {
fn measure(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut MeasureCtx) -> Size {
let target = host.measure(clamp, ctx);
let now = Instant::now();
let enabled = self.motion_enabled(&ctx.window());
self.with_motion(|motion| {
if motion.initialized {
if target != motion.target.size {
let current = self.sample_rect(motion, now);
restart_motion(motion, current, Rect::new(motion.target.origin, target), now, enabled);
}
} else {
motion.target.size = target;
if !self.has_pos_axes() {
let from = self.resolve_init_rect(motion.target, clamp);
initialize_motion(motion, from, now, enabled);
}
}
});
let layout_size = if self.has_size_axes() && self.size_mode == SizeMode::Layout {
self.animated_size(now)
} else {
target
};
if self.has_size_axes() && self.size_mode == SizeMode::Layout && layout_size != target {
host.measure(constrain_to_animated_axes(clamp, layout_size, self.size_axes), ctx)
} else {
layout_size
}
}
fn place_children(&self, size: Size, host: &dyn Render, ctx: &mut PlaceCtx) {
let place_size = if self.has_size_axes() && self.size_mode == SizeMode::Visual {
self.target_size()
} else {
size
};
host.place_children(place_size, ctx)
}
fn adjust_position(&self, host: &dyn Render, pos: Point, ctx: &mut PlaceCtx) -> Point {
let target_pos = host.adjust_position(pos, ctx);
let now = Instant::now();
let enabled = self.motion_enabled(&ctx.window());
self.with_motion(|motion| {
if motion.initialized {
if target_pos != motion.target.origin {
let current = self.sample_rect(motion, now);
restart_motion(motion, current, Rect::new(target_pos, motion.target.size), now, enabled);
}
} else {
motion.target.origin = target_pos;
let from = self.resolve_init_rect(motion.target, ctx.clamp());
initialize_motion(motion, from, now, enabled);
}
});
self.presented_rect(now).origin
}
fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) {
let now = Instant::now();
let was_active = self.motion.get().active.is_some();
if self.is_size_effect_active(animations_on(&ctx.window()), now) {
match self.size_effect {
SizeEffect::Clip => {
let rect = Rect::from_size(self.animated_size(now));
ctx.box_painter().clip(Path::rect(&rect).into());
}
SizeEffect::Scale => {
let scale = self.scale_factor(now);
if scale != Vector::one() {
ctx.painter().scale(scale.x, scale.y);
}
}
}
}
host.paint(ctx);
if was_active {
ctx
.window()
.tree()
.dirty_marker()
.mark(ctx.widget_id(), self.required_dirty_phase());
}
}
fn hit_test(&self, host: &dyn Render, ctx: &mut HitTestCtx, pos: Point) -> HitTest {
let now = Instant::now();
if !self.is_size_effect_active(animations_on(&ctx.window()), now) {
return host.hit_test(ctx, pos);
}
let box_pos = ctx.box_pos().unwrap_or(Point::zero());
let local_pos = pos - box_pos.to_vector();
let animated = self.animated_size(now);
match self.size_effect {
SizeEffect::Clip => {
if local_pos.x < 0.
|| local_pos.y < 0.
|| local_pos.x > animated.width
|| local_pos.y > animated.height
{
HitTest { hit: false, can_hit_child: false }
} else {
host.hit_test(ctx, pos)
}
}
SizeEffect::Scale => {
let scale = self.scale_factor(now);
Transform::scale(scale.x, scale.y)
.inverse()
.map_or(HitTest { hit: false, can_hit_child: false }, |inv| {
host.hit_test(ctx, inv.transform_point(local_pos) + box_pos.to_vector())
})
}
}
}
fn get_transform(&self, host: &dyn Render) -> Option<Transform> {
if self.size_effect != SizeEffect::Scale
|| self.size_mode != SizeMode::Visual
|| !self.has_size_axes()
{
return host.get_transform();
}
let now = Instant::now();
if !self.is_animating(now) {
return host.get_transform();
}
let scale = self.scale_factor(now);
if scale == Vector::one() {
return host.get_transform();
}
let transform = Transform::scale(scale.x, scale.y);
host
.get_transform()
.map_or(Some(transform), |host_transform| Some(transform.then(&host_transform)))
}
fn dirty_phase(&self, host: &dyn Render) -> DirtyPhase {
use DirtyPhase::*;
match (self.required_dirty_phase(), host.dirty_phase()) {
(LayoutSubtree, _) | (_, LayoutSubtree) => LayoutSubtree,
(Layout, _) | (_, Layout) => Layout,
(Position, _) | (_, Position) => Position,
(Paint, Paint) => Paint,
}
}
fn wrapper_dirty_phase(&self) -> DirtyPhase { self.required_dirty_phase() }
}
fn default_transition() -> Rc<Box<dyn Transition>> {
boxed_transition(EasingTransition {
easing: easing::LinearEasing,
duration: Duration::from_millis(200),
})
}
impl SmoothLayoutDeclarer {
pub fn with_transition(&mut self, t: impl Transition + 'static) -> &mut Self {
self.transition = Some(boxed_transition(t));
self
}
pub fn with_init_size(&mut self, init: impl Into<Size<Measure>>) -> &mut Self {
let init = init.into();
self.init_width = Some(Some(init.width));
self.init_height = Some(Some(init.height));
self
}
pub fn with_init_x(&mut self, init: impl Into<AnchorX>) -> &mut Self {
init_anchor(&mut self.init_pos).x = Some(init.into());
self
}
pub fn with_init_y(&mut self, init: impl Into<AnchorY>) -> &mut Self {
init_anchor(&mut self.init_pos).y = Some(init.into());
self
}
pub fn with_init_width(&mut self, init: impl Into<Measure>) -> &mut Self {
self.init_width = Some(Some(init.into()));
self
}
pub fn with_init_height(&mut self, init: impl Into<Measure>) -> &mut Self {
self.init_height = Some(Some(init.into()));
self
}
}
#[declare(stateless)]
pub struct SmoothGlobal {
#[declare(default = PosAxes::Pos)]
pos_axes: PosAxes,
#[declare(custom, default = default_transition())]
transition: Rc<Box<dyn Transition>>,
#[declare(default)]
init_pos: Anchor,
#[declare(skip, default = Rc::new(Cell::new(GlobalRuntime::default())))]
runtime: Rc<Cell<GlobalRuntime>>,
#[declare(skip, default = Rc::new(RefCell::new(None)))]
layout_ready_sub: Rc<RefCell<Option<BoxedSubscription>>>,
}
impl SmoothGlobal {
fn transition(&self) -> &dyn Transition { self.transition.as_ref().as_ref() }
fn has_pos_axes(&self) -> bool { self.pos_axes != PosAxes::None }
fn motion_enabled(&self, window: &Window) -> bool { self.has_pos_axes() && animations_on(window) }
fn with_runtime<R>(&self, f: impl FnOnce(&mut GlobalRuntime) -> R) -> R {
update_cell(self.runtime.as_ref(), f)
}
fn sample_point(&self, motion: &mut MotionState<Point>, now: Instant) -> Point {
sample_motion(motion, self.transition(), now, |from, to, factor| {
lerp_point_axes(from, to, factor, self.pos_axes)
})
}
fn refresh_presented_point(&self, now: Instant) -> Point {
self.with_runtime(|runtime| {
let presented = self.sample_point(&mut runtime.motion, now);
runtime.presented_global = Some(presented);
presented
})
}
fn resolve_init_point(&self, target: Point, size: Size, clamp: BoxClamp) -> Option<Point> {
let from = resolve_init_origin(target, size, clamp, self.pos_axes, &self.init_pos);
(from != target).then_some(from)
}
fn ensure_tracking(&self, window: &Rc<Window>, widget_id: WidgetId) {
if self.layout_ready_sub.borrow().is_some() {
return;
}
let runtime = self.runtime.clone();
let tracked_window = window.clone();
let sub = window
.frame_tick_stream()
.filter(|msg| matches!(msg, FrameMsg::LayoutReady(_)))
.subscribe(move |_| {
let Some(local) = tracked_window.widget_pos(widget_id) else { return };
let parent = tracked_window.parent(widget_id);
update_cell(runtime.as_ref(), |state| {
state.presented_global = Some(to_global(&tracked_window, parent, local));
});
});
*self.layout_ready_sub.borrow_mut() = Some(BoxedSubscription::new(sub));
}
}
impl Drop for SmoothGlobal {
fn drop(&mut self) {
if let Some(sub) = self.layout_ready_sub.borrow_mut().take() {
sub.unsubscribe();
}
}
}
impl<'c> ComposeChild<'c> for SmoothGlobal {
type Child = Widget<'c>;
fn compose_child(this: impl StateWriter<Value = Self>, child: Self::Child) -> Widget<'c> {
WrapRender::combine_child(this, child)
}
}
impl WrapRender for SmoothGlobal {
fn adjust_position(&self, host: &dyn Render, pos: Point, ctx: &mut PlaceCtx) -> Point {
let target_pos = host.adjust_position(pos, ctx);
let window = ctx.window();
let parent = ctx.parent();
let target = to_global(&window, parent, target_pos);
let now = Instant::now();
let enabled = self.motion_enabled(&window);
self.ensure_tracking(&window, ctx.widget_id());
self.with_runtime(|runtime| {
if runtime.motion.initialized {
if target != runtime.motion.target {
let current = runtime
.presented_global
.unwrap_or_else(|| self.sample_point(&mut runtime.motion, now));
restart_motion(&mut runtime.motion, current, target, now, enabled);
}
} else {
runtime.motion.target = target;
let local_target = to_local(&window, parent, target);
let from = self
.resolve_init_point(local_target, runtime.target_size, ctx.clamp())
.map(|local_from| to_global(&window, parent, local_from));
initialize_motion(&mut runtime.motion, from, now, enabled);
}
});
to_local(&window, parent, self.refresh_presented_point(now))
}
fn measure(&self, clamp: BoxClamp, host: &dyn Render, ctx: &mut MeasureCtx) -> Size {
let size = host.measure(clamp, ctx);
self.with_runtime(|runtime| runtime.target_size = size);
size
}
fn paint(&self, host: &dyn Render, ctx: &mut PaintingCtx) {
let was_active = self.runtime.get().motion.active.is_some();
host.paint(ctx);
if was_active {
self.refresh_presented_point(Instant::now());
ctx
.window()
.tree()
.dirty_marker()
.mark(ctx.widget_id(), DirtyPhase::Position);
}
}
fn dirty_phase(&self, host: &dyn Render) -> DirtyPhase {
use DirtyPhase::*;
match host.dirty_phase() {
LayoutSubtree => LayoutSubtree,
Layout => Layout,
Paint | Position => Position,
}
}
fn wrapper_dirty_phase(&self) -> DirtyPhase { DirtyPhase::Position }
}
impl SmoothGlobalDeclarer {
pub fn with_transition(&mut self, t: impl Transition + 'static) -> &mut Self {
self.transition = Some(boxed_transition(t));
self
}
pub fn with_init_x(&mut self, init: impl Into<AnchorX>) -> &mut Self {
init_anchor(&mut self.init_pos).x = Some(init.into());
self
}
pub fn with_init_y(&mut self, init: impl Into<AnchorY>) -> &mut Self {
init_anchor(&mut self.init_pos).y = Some(init.into());
self
}
}
#[cfg(test)]
mod tests {
use std::cell::Cell;
use ribir::{
core::{reset_test_env, test_helper::*, window::WindowFlags},
prelude::{easing::LinearEasing, *},
};
use ribir_dev_helper::*;
const TEST_TRANS: EasingTransition<LinearEasing> =
EasingTransition { easing: easing::LinearEasing, duration: Duration::from_millis(200) };
#[derive(Clone)]
struct StepTransition {
progress: Rc<Cell<AnimateProgress>>,
}
impl Transition for StepTransition {
fn rate_of_change(&self, _: Duration) -> AnimateProgress { self.progress.get() }
fn duration(&self) -> Duration { Duration::from_millis(1) }
fn dyn_clone(&self) -> Box<dyn Transition> { Box::new(self.clone()) }
}
fn red_block() -> Widget<'static> {
container! { background: Color::RED, size: Size::new(10., 10.) }.into_widget()
}
fn centered_red_block() -> Widget<'static> {
container! {
background: Color::RED,
size: Size::new(10., 10.),
x: AnchorX::center(),
y: AnchorY::center(),
}
.into_widget()
}
fn reused_smooth_global(
tracker: impl StateWriter<Value = Option<WidgetId>>, progress: Rc<Cell<AnimateProgress>>,
) -> Widget<'static> {
fn_widget! {
let progress = progress.clone();
@Reuse {
reuse: ReuseKey::global("smooth_global_reuse"),
@SmoothGlobal {
on_mounted: move |e| *$write(tracker) = Some(e.current_target()),
pos_axes: PosAxes::X,
transition: StepTransition { progress },
@red_block()
}
}
}
.into_widget()
}
#[test]
fn smooth_pos() {
reset_test_env!();
assert_widget_eq_image!(
WidgetTester::new(stack! {
clamp: BoxClamp::EXPAND_BOTH,
@SmoothLayout {
pos_axes: PosAxes::Pos,
size_axes: SizeAxes::None,
transition: TEST_TRANS,
@centered_red_block()
}
@SmoothLayout {
pos_axes: PosAxes::Pos,
size_axes: SizeAxes::None,
transition: TEST_TRANS,
init_pos: Anchor::left_top(5., 10.percent()),
@centered_red_block()
}
@SmoothLayout {
pos_axes: PosAxes::Pos,
size_axes: SizeAxes::None,
transition: TEST_TRANS,
init_pos: Anchor::right_bottom(10.percent(), 5.),
@centered_red_block()
}
})
.with_wnd_size(Size::new(100., 100.))
.with_flags(WindowFlags::ANIMATIONS),
"smooth_pos"
);
}
#[test]
fn smooth_init_no_panic() {
reset_test_env!();
for (pos_axes, size_axes) in [(PosAxes::Pos, SizeAxes::None), (PosAxes::None, SizeAxes::Size)] {
let wnd = TestWindow::new(
fn_widget! {
@SmoothLayout {
pos_axes,
size_axes,
transition: TEST_TRANS,
init_pos: Anchor::left_top(5., 5.),
init_size: Size::splat(5f32.into()),
@centered_red_block()
}
},
Size::new(100., 100.),
WindowFlags::ANIMATIONS,
);
wnd.draw_frame();
wnd.draw_frame();
}
}
#[test]
fn smooth_layout_defaults_smoothing() {
reset_test_env!();
let progress = Rc::new(Cell::new(AnimateProgress::Finish));
let tracker = Stateful::new(None);
let offset = Stateful::new(0.);
let p_inner = progress.clone();
let wnd = TestWindow::new(
fn_widget! {
let p_inner = p_inner.clone();
@MockBox {
size: Size::new(200., 200.),
@SmoothLayout {
on_mounted: move |e| *$write(tracker) = Some(e.current_target()),
transition: StepTransition { progress: p_inner },
@MockBox {
size: Size::new(100., 100.),
x: pipe!(AnchorX::left().offset(*$read(offset))),
y: pipe!(AnchorY::top().offset(*$read(offset))),
}
}
}
},
Size::new(200., 200.),
WindowFlags::ANIMATIONS,
);
wnd.draw_frame();
wnd.draw_frame();
let id = (*tracker.read()).unwrap();
assert_eq!(wnd.widget_pos(id).unwrap(), Point::zero());
*offset.write() = 100.;
progress.set(AnimateProgress::Between(0.5));
wnd.draw_frame();
let pos = wnd.widget_pos(id).unwrap();
assert!((0. ..100.).contains(&pos.x));
}
#[test]
fn smooth_layout_mode_updates_size() {
reset_test_env!();
let tracker = Stateful::new(None);
let progress = Rc::new(Cell::new(AnimateProgress::Between(0.1)));
let p_inner = progress.clone();
let wnd = TestWindow::new(
fn_widget! {
let p_inner = p_inner.clone();
@SmoothLayout {
on_mounted: move |e| *$write(tracker) = Some(e.current_target()),
pos_axes: PosAxes::None,
size_axes: SizeAxes::Size,
size_mode: SizeMode::Layout,
transition: StepTransition { progress: p_inner },
init_size: Size::splat(5f32.into()),
@red_block()
}
},
Size::new(200., 200.),
WindowFlags::ANIMATIONS,
);
wnd.draw_frame();
let id = (*tracker.read()).unwrap();
let s1 = wnd.widget_size(id).unwrap();
assert!(s1.width < 10. && s1.width > 5.);
progress.set(AnimateProgress::Between(0.8));
wnd.draw_frame();
let s2 = wnd.widget_size(id).unwrap();
assert!(s2.width > s1.width);
}
#[test]
fn smooth_global_reuse_continuity() {
reset_test_env!();
let tracker = Stateful::new(None);
let progress = Rc::new(Cell::new(AnimateProgress::Finish));
let place_on_right = Stateful::new(false);
let tracker_for_widget = tracker.clone_writer();
let progress_for_widget = progress.clone();
let wnd = TestWindow::new(
fn_widget! {
let tracker = tracker_for_widget.clone_writer();
let progress = progress_for_widget.clone();
@MockBox {
size: Size::new(200., 100.),
@ {
if *$read(place_on_right) {
@MockBox {
size: Size::new(10., 10.),
x: 120.,
@reused_smooth_global(tracker.clone_writer(), progress.clone())
}
} else {
@MockBox {
size: Size::new(10., 10.),
@reused_smooth_global(tracker.clone_writer(), progress.clone())
}
}
}
}
},
Size::new(200., 100.),
WindowFlags::ANIMATIONS,
);
wnd.draw_frame();
wnd.draw_frame();
let id = (*tracker.read()).unwrap();
assert_eq!(wnd.map_to_global(Point::zero(), id).x, 0.);
*place_on_right.write() = true;
progress.set(AnimateProgress::Between(0.5));
wnd.draw_frame();
let x = wnd.map_to_global(Point::zero(), id).x;
assert!((0. ..120.).contains(&x));
}
#[test]
fn smooth_global_scroll_motion() {
reset_test_env!();
let tracker = Stateful::new(None);
let scroll = Stateful::new(None::<Box<dyn StateWriter<Value = ScrollableWidget>>>);
let wnd = TestWindow::new(
fn_widget! {
@ScrollableWidget {
scrollable: Scrollable::Y,
on_mounted: move |e| *$write(scroll) = ScrollableWidget::writer_of(e),
@MockBox {
size: Size::new(100., 300.),
@MockBox {
size: Size::new(10., 10.),
y: 120.,
@SmoothGlobal {
on_mounted: move |e| *$write(tracker) = Some(e.current_target()),
pos_axes: PosAxes::Y,
transition: TEST_TRANS,
@red_block()
}
}
}
}
},
Size::new(100., 100.),
WindowFlags::ANIMATIONS,
);
wnd.draw_frame();
wnd.draw_frame();
let id = (*tracker.read()).unwrap();
assert_eq!(wnd.map_to_global(Point::zero(), id).y, 120.);
scroll
.read()
.as_ref()
.unwrap()
.write()
.jump_to(Point::new(0., 60.));
wnd.draw_frame();
let y = wnd.map_to_global(Point::zero(), id).y;
assert!((60. ..120.).contains(&y));
}
}