use frus_core::{
BorderRadius, Color, Curve, Rect, Scene, Size, TextAlign, TextOverflow, TextStyle,
};
use frus_layout::Style;
use crate::container::Container;
use crate::interaction::Status;
use crate::theme::Theme;
use crate::widget::Widget;
macro_rules! forward_to_container {
($ty:ident) => {
impl<Msg: Clone + 'static> Widget<Msg> for $ty<Msg> {
fn style(&self) -> Style {
Widget::style(&self.inner)
}
fn style_themed(&self, theme: &Theme) -> Style {
Widget::style_themed(&self.inner, theme)
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
Widget::children(&self.inner)
}
fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
Widget::paint(&self.inner, bounds, status, theme, scene)
}
fn on_click(&self) -> Option<Msg> {
Widget::on_click(&self.inner)
}
fn opaque(&self) -> bool {
Widget::opaque(&self.inner)
}
fn on_long_press(&self) -> Option<Msg> {
Widget::on_long_press(&self.inner)
}
fn repaint_boundary(&self) -> bool {
Widget::repaint_boundary(&self.inner)
}
fn opacity_group(&self) -> Option<f32> {
Widget::opacity_group(&self.inner)
}
fn anim_target(&self) -> Option<f32> {
Widget::anim_target(&self.inner)
}
fn anim_transform(&self) -> Option<$crate::runtime::TransformValues> {
Widget::anim_transform(&self.inner)
}
fn anim_color(&self) -> Option<Color> {
Widget::anim_color(&self.inner)
}
fn anim_size(&self) -> Option<Size> {
Widget::anim_size(&self.inner)
}
fn anim_radius(&self) -> Option<BorderRadius> {
Widget::anim_radius(&self.inner)
}
fn anim_padding(&self) -> Option<frus_core::Insets> {
Widget::anim_padding(&self.inner)
}
fn anim_offset(&self) -> Option<(f32, f32)> {
Widget::anim_offset(&self.inner)
}
fn anim_duration(&self) -> f32 {
Widget::anim_duration(&self.inner)
}
fn anim_curve(&self) -> Curve {
Widget::anim_curve(&self.inner)
}
fn alignment_geometry(&self) -> Option<frus_core::AlignmentGeometry> {
Widget::alignment_geometry(&self.inner)
}
fn transform_translate(&self) -> Option<(f32, f32)> {
Widget::transform_translate(&self.inner)
}
fn transform_scale(&self) -> Option<(f32, f32, frus_core::Alignment)> {
Widget::transform_scale(&self.inner)
}
fn transform_rotate(&self) -> Option<(f32, frus_core::Alignment)> {
Widget::transform_rotate(&self.inner)
}
fn clip_shape(&self) -> Option<frus_core::ClipShape> {
Widget::clip_shape(&self.inner)
}
fn clip_path(&self) -> Option<&frus_core::Path> {
Widget::clip_path(&self.inner)
}
fn barrier(&self) -> Option<$crate::barrier::ModalBarrier> {
Widget::barrier(&self.inner)
}
fn interactive(&self) -> Option<(f32, f32)> {
Widget::interactive(&self.inner)
}
fn fitted(&self) -> Option<frus_core::BoxFit> {
Widget::fitted(&self.inner)
}
fn rotated_quarter_turns(&self) -> Option<i32> {
Widget::rotated_quarter_turns(&self.inner)
}
fn foreground(&self, theme: &Theme) -> Option<frus_core::BoxDecoration> {
Widget::foreground(&self.inner, theme)
}
}
};
}
pub(crate) use forward_to_container;
pub struct Opacity<Msg = crate::callback::Callback> {
inner: Container<Msg>,
}
impl<Msg: Clone + 'static> Opacity<Msg> {
pub fn new(opacity: f32, child: impl Widget<Msg> + 'static) -> Self {
Self {
inner: Container::new().opacity(opacity).child(child),
}
}
}
forward_to_container!(Opacity);
pub struct AnimatedOpacity<Msg = crate::callback::Callback> {
inner: Container<Msg>,
}
impl<Msg: Clone + 'static> AnimatedOpacity<Msg> {
pub fn new(
opacity: f32,
duration: f32,
curve: Curve,
child: impl Widget<Msg> + 'static,
) -> Self {
Self {
inner: Container::new()
.animated_opacity(opacity, duration, curve)
.child(child),
}
}
}
forward_to_container!(AnimatedOpacity);
pub struct AnimatedContainer<Msg = crate::callback::Callback> {
inner: Container<Msg>,
duration: f32,
curve: Curve,
}
impl<Msg: Clone + 'static> AnimatedContainer<Msg> {
pub fn new(duration: f32, curve: Curve) -> Self {
Self {
inner: Container::new(),
duration,
curve,
}
}
pub fn color(mut self, color: Color) -> Self {
self.inner = self
.inner
.animated_color(color, self.duration, self.curve.clone());
self
}
pub fn size(mut self, width: f32, height: f32) -> Self {
self.inner = self
.inner
.animated_size(width, height, self.duration, self.curve.clone());
self
}
pub fn radius(mut self, radius: impl Into<BorderRadius>) -> Self {
self.inner = self
.inner
.animated_radius(radius, self.duration, self.curve.clone());
self
}
pub fn opacity(mut self, opacity: f32) -> Self {
self.inner = self
.inner
.animated_opacity(opacity, self.duration, self.curve.clone());
self
}
pub fn padding(mut self, padding: f32) -> Self {
self.inner = self.inner.padding(padding);
self
}
pub fn child(mut self, child: impl Widget<Msg> + 'static) -> Self {
self.inner = self.inner.child(child);
self
}
}
forward_to_container!(AnimatedContainer);
pub struct AnimatedAlign<Msg = crate::callback::Callback> {
inner: crate::Aligned<Msg>,
}
impl<Msg: Clone + 'static> AnimatedAlign<Msg> {
pub fn new(
alignment: impl Into<frus_core::AlignmentGeometry>,
duration: f32,
curve: Curve,
child: impl Widget<Msg> + 'static,
) -> Self {
Self {
inner: crate::Aligned::animated(alignment, duration, curve, child),
}
}
pub fn width_factor(mut self, factor: f32) -> Self {
self.inner = self.inner.width_factor(factor);
self
}
pub fn height_factor(mut self, factor: f32) -> Self {
self.inner = self.inner.height_factor(factor);
self
}
}
impl<Msg: Clone> Widget<Msg> for AnimatedAlign<Msg> {
fn style(&self) -> Style {
self.inner.style()
}
fn style_themed(&self, theme: &Theme) -> Style {
self.inner.style_themed(theme)
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
self.inner.children()
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {}
fn on_click(&self) -> Option<Msg> {
None
}
fn alignment_geometry(&self) -> Option<frus_core::AlignmentGeometry> {
self.inner.alignment_geometry()
}
fn anim_offset(&self) -> Option<(f32, f32)> {
self.inner.anim_offset()
}
fn anim_duration(&self) -> f32 {
self.inner.anim_duration()
}
fn anim_curve(&self) -> Curve {
self.inner.anim_curve()
}
fn constraints_transform(&self) -> Option<crate::constraints::ConstraintsTransform> {
self.inner.constraints_transform()
}
fn size_factor(&self) -> Option<(f32, f32)> {
self.inner.size_factor()
}
fn fill_axes(&self, theme: &Theme) -> crate::widget::FillAxes {
self.inner.fill_axes(theme)
}
fn debug_name(&self) -> &'static str {
"AnimatedAlign"
}
}
pub struct AnimatedSlide<Msg = crate::callback::Callback> {
inner: Box<dyn Widget<Msg>>,
}
impl<Msg: Clone + 'static> AnimatedSlide<Msg> {
pub fn new(
fx: f32,
fy: f32,
duration: f32,
curve: Curve,
child: impl Widget<Msg> + 'static,
) -> Self {
Self {
inner: Box::new(
crate::FractionalTranslation::new(fx, fy)
.animated(duration, curve)
.child(child),
),
}
}
}
impl<Msg> AnimatedSlide<Msg> {
fn restyle(&self, base: Style) -> Style {
base
}
}
crate::transparent::forward_transparent!(AnimatedSlide {
fn key(&self) -> Option<u64> {
self.inner.key()
}
fn positioned(&self) -> Option<crate::positioned::Positioning> {
self.inner.positioned()
}
fn theme_override(
&self,
inherited: &crate::theme::Theme,
) -> Option<Box<crate::theme::Theme>> {
self.inner.theme_override(inherited)
}
fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
self.inner.media_override(inherited)
}
fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
self.inner.scaffold_override()
}
fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
self.inner.scroll_behavior_override()
}
fn autofill_group(&self) -> bool {
self.inner.autofill_group()
}
fn area_toolbar(
&self,
context: crate::ToolbarContext,
) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
self.inner.area_toolbar(context)
}
});
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn animated_container_declares_all_targets() {
let blue = Color::rgb(0.0, 0.0, 1.0);
let w: AnimatedContainer<()> = AnimatedContainer::new(0.25, Curve::ease_out())
.color(blue)
.size(200.0, 100.0)
.radius(12.0)
.opacity(0.5);
assert_eq!(Widget::<()>::anim_color(&w), Some(blue));
assert_eq!(Widget::<()>::anim_size(&w), Some(Size::new(200.0, 100.0)));
assert_eq!(
Widget::<()>::anim_radius(&w),
Some(BorderRadius::from(12.0))
);
assert_eq!(Widget::<()>::anim_target(&w), Some(0.5)); assert_eq!(Widget::<()>::opacity_group(&w), Some(0.5));
assert_eq!(Widget::<()>::anim_duration(&w), 0.25);
assert_eq!(Widget::<()>::anim_curve(&w), Curve::ease_out());
}
#[test]
fn opacity_wraps_child_as_a_group() {
let w: Opacity<()> = Opacity::new(0.4, crate::Container::new().width(10.0).height(10.0));
assert_eq!(Widget::<()>::opacity_group(&w), Some(0.4));
assert_eq!(Widget::<()>::anim_target(&w), None);
assert_eq!(
Widget::<()>::children(&w).len(),
1,
"the child is a separate node"
);
}
#[test]
fn animated_opacity_declares_a_group_target() {
let w: AnimatedOpacity<()> =
AnimatedOpacity::new(0.0, 0.2, Curve::ease_in(), crate::Container::new());
assert_eq!(Widget::<()>::opacity_group(&w), Some(0.0));
assert_eq!(Widget::<()>::anim_target(&w), Some(0.0));
assert_eq!(Widget::<()>::anim_duration(&w), 0.2);
assert_eq!(Widget::<()>::debug_name(&w), "AnimatedOpacity");
}
}
pub struct AnimatedScale<Msg = crate::callback::Callback> {
inner: Box<dyn Widget<Msg>>,
}
impl<Msg: Clone + 'static> AnimatedScale<Msg> {
pub fn new(scale: f32, duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
Self::about(scale, frus_core::Alignment::CENTER, duration, curve, child)
}
pub fn about(
scale: f32,
pivot: frus_core::Alignment,
duration: f32,
curve: Curve,
child: impl Widget<Msg> + 'static,
) -> Self {
Self {
inner: Box::new(
crate::Transform::scale_xy_from(scale, scale, pivot)
.animated(duration, curve)
.child(child),
),
}
}
}
impl<Msg> AnimatedScale<Msg> {
fn restyle(&self, base: Style) -> Style {
base
}
}
crate::transparent::forward_transparent!(AnimatedScale {
fn key(&self) -> Option<u64> {
self.inner.key()
}
fn positioned(&self) -> Option<crate::positioned::Positioning> {
self.inner.positioned()
}
fn theme_override(
&self,
inherited: &crate::theme::Theme,
) -> Option<Box<crate::theme::Theme>> {
self.inner.theme_override(inherited)
}
fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
self.inner.media_override(inherited)
}
fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
self.inner.scaffold_override()
}
fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
self.inner.scroll_behavior_override()
}
fn autofill_group(&self) -> bool {
self.inner.autofill_group()
}
fn area_toolbar(
&self,
context: crate::ToolbarContext,
) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
self.inner.area_toolbar(context)
}
});
pub struct AnimatedRotation<Msg = crate::callback::Callback> {
inner: Box<dyn Widget<Msg>>,
}
impl<Msg: Clone + 'static> AnimatedRotation<Msg> {
pub fn new(
radians: f32,
duration: f32,
curve: Curve,
child: impl Widget<Msg> + 'static,
) -> Self {
Self::about(
radians,
frus_core::Alignment::CENTER,
duration,
curve,
child,
)
}
pub fn about(
radians: f32,
pivot: frus_core::Alignment,
duration: f32,
curve: Curve,
child: impl Widget<Msg> + 'static,
) -> Self {
Self {
inner: Box::new(
crate::Transform::rotate_from(radians, pivot)
.animated(duration, curve)
.child(child),
),
}
}
}
impl<Msg> AnimatedRotation<Msg> {
fn restyle(&self, base: Style) -> Style {
base
}
}
crate::transparent::forward_transparent!(AnimatedRotation {
fn key(&self) -> Option<u64> {
self.inner.key()
}
fn positioned(&self) -> Option<crate::positioned::Positioning> {
self.inner.positioned()
}
fn theme_override(
&self,
inherited: &crate::theme::Theme,
) -> Option<Box<crate::theme::Theme>> {
self.inner.theme_override(inherited)
}
fn media_override(&self, inherited: crate::MediaQuery) -> Option<crate::MediaQuery> {
self.inner.media_override(inherited)
}
fn scaffold_override(&self) -> Option<crate::ScaffoldInfo> {
self.inner.scaffold_override()
}
fn scroll_behavior_override(&self) -> Option<crate::ScrollBehavior> {
self.inner.scroll_behavior_override()
}
fn autofill_group(&self) -> bool {
self.inner.autofill_group()
}
fn area_toolbar(
&self,
context: crate::ToolbarContext,
) -> Option<Option<&dyn crate::widget::Widget<Msg>>> {
self.inner.area_toolbar(context)
}
});
pub struct AnimatedPadding<Msg = crate::callback::Callback> {
inner: Container<Msg>,
}
impl<Msg: Clone + 'static> AnimatedPadding<Msg> {
pub fn new(
padding: f32,
duration: f32,
curve: Curve,
child: impl Widget<Msg> + 'static,
) -> Self {
Self {
inner: Container::new()
.animated_padding(padding, duration, curve)
.child(child),
}
}
}
forward_to_container!(AnimatedPadding);
pub struct AnimatedPositioned<Msg = crate::callback::Callback> {
children: Vec<Box<dyn Widget<Msg>>>,
spec: crate::positioned::Positioning,
duration: f32,
curve: Curve,
}
impl<Msg> AnimatedPositioned<Msg> {
pub fn new(duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
Self {
children: vec![Box::new(child)],
spec: crate::positioned::Positioning::default(),
duration,
curve,
}
}
pub fn left(mut self, px: f32) -> Self {
self.spec.left = Some(px);
self
}
pub fn top(mut self, px: f32) -> Self {
self.spec.top = Some(px);
self
}
pub fn right(mut self, px: f32) -> Self {
self.spec.right = Some(px);
self
}
pub fn bottom(mut self, px: f32) -> Self {
self.spec.bottom = Some(px);
self
}
pub fn start(mut self, px: f32) -> Self {
self.spec.start = Some(px);
self
}
pub fn end(mut self, px: f32) -> Self {
self.spec.end = Some(px);
self
}
pub fn width(mut self, px: f32) -> Self {
self.spec.width = Some(px);
self
}
pub fn height(mut self, px: f32) -> Self {
self.spec.height = Some(px);
self
}
}
impl<Msg: Clone> Widget<Msg> for AnimatedPositioned<Msg> {
fn style(&self) -> Style {
Style::default()
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
}
fn on_click(&self) -> Option<Msg> {
None
}
fn positioned(&self) -> Option<crate::positioned::Positioning> {
Some(self.spec)
}
fn anim_pins(&self) -> Option<crate::positioned::Positioning> {
Some(self.spec)
}
fn anim_duration(&self) -> f32 {
self.duration
}
fn anim_curve(&self) -> Curve {
self.curve.clone()
}
fn debug_name(&self) -> &'static str {
"AnimatedPositioned"
}
}
pub struct AnimatedDefaultTextStyle<Msg = crate::callback::Callback> {
children: Vec<Box<dyn Widget<Msg>>>,
style: TextStyle,
align: Option<TextAlign>,
soft_wrap: Option<bool>,
overflow: Option<TextOverflow>,
max_lines: Option<usize>,
duration: f32,
curve: Curve,
}
impl<Msg> AnimatedDefaultTextStyle<Msg> {
pub fn new(
duration: f32,
curve: Curve,
style: TextStyle,
child: impl Widget<Msg> + 'static,
) -> Self {
Self {
children: vec![Box::new(child)],
style,
align: None,
soft_wrap: None,
overflow: None,
max_lines: None,
duration,
curve,
}
}
pub fn align(mut self, align: TextAlign) -> Self {
self.align = Some(align);
self
}
pub fn soft_wrap(mut self, wrap: bool) -> Self {
self.soft_wrap = Some(wrap);
self
}
pub fn overflow(mut self, overflow: TextOverflow) -> Self {
self.overflow = Some(overflow);
self
}
pub fn max_lines(mut self, lines: usize) -> Self {
self.max_lines = Some(lines);
self
}
fn handed_down(&self) -> crate::widgettheme::DefaultTextStyle {
crate::widgettheme::DefaultTextStyle {
style: self.style,
align: self.align,
soft_wrap: self.soft_wrap,
overflow: self.overflow,
max_lines: self.max_lines,
..crate::widgettheme::DefaultTextStyle::NONE
}
}
}
impl<Msg: Clone> Widget<Msg> for AnimatedDefaultTextStyle<Msg> {
fn style(&self) -> Style {
Style::default()
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
&self.children
}
fn paint(&self, _bounds: Rect, _status: Status, _theme: &Theme, _scene: &mut Scene) {
}
fn on_click(&self) -> Option<Msg> {
None
}
fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
let mut theme = inherited.clone();
theme.widgets.text = theme.widgets.text.merge(self.handed_down());
Some(Box::new(theme))
}
fn anim_text_style(&self) -> Option<TextStyle> {
Some(self.style)
}
fn anim_duration(&self) -> f32 {
self.duration
}
fn anim_curve(&self) -> Curve {
self.curve.clone()
}
fn debug_name(&self) -> &'static str {
"AnimatedDefaultTextStyle"
}
}
pub struct AnimatedFractionallySizedBox<Msg = crate::callback::Callback> {
inner: crate::fractional::FractionallySizedBox<Msg>,
}
impl<Msg: Clone + 'static> AnimatedFractionallySizedBox<Msg> {
pub fn new(duration: f32, curve: Curve, child: impl Widget<Msg> + 'static) -> Self {
Self {
inner: crate::fractional::FractionallySizedBox::new()
.animated(duration, curve)
.child(child),
}
}
pub fn width_factor(mut self, factor: f32) -> Self {
self.inner = self.inner.width_factor(factor);
self
}
pub fn height_factor(mut self, factor: f32) -> Self {
self.inner = self.inner.height_factor(factor);
self
}
}
impl<Msg: Clone + 'static> Widget<Msg> for AnimatedFractionallySizedBox<Msg> {
fn style(&self) -> Style {
Widget::style(&self.inner)
}
fn style_themed(&self, theme: &Theme) -> Style {
Widget::style_themed(&self.inner, theme)
}
fn children(&self) -> &[Box<dyn Widget<Msg>>] {
Widget::children(&self.inner)
}
fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
Widget::paint(&self.inner, bounds, status, theme, scene)
}
fn on_click(&self) -> Option<Msg> {
Widget::on_click(&self.inner)
}
fn anim_fractions(&self) -> Option<(Option<f32>, Option<f32>)> {
Widget::anim_fractions(&self.inner)
}
fn anim_duration(&self) -> f32 {
Widget::anim_duration(&self.inner)
}
fn anim_curve(&self) -> Curve {
Widget::anim_curve(&self.inner)
}
}
#[cfg(test)]
mod implicit_tests {
use super::*;
use crate::interaction::WidgetId;
use crate::{build_ui, Runtime, Text};
use frus_core::{Insets, Primitive};
fn scaled(to: f32) -> AnimatedScale<()> {
AnimatedScale::new(to, 0.10, Curve::Linear, Text::new("x"))
}
#[test]
fn a_scale_mounts_settled_moves_and_arrives() {
let mut rt = Runtime::default();
assert!(
!rt.advance_transforms(&scaled(2.0), 1.0),
"a mount is not a transition"
);
let id = WidgetId::ROOT;
assert_eq!(
rt.anim_transform(id).map(|t| t.scale_x),
Some(2.0),
"and it adopts the target whole"
);
assert!(rt.advance_transforms(&scaled(1.0), 0.05));
let mid = rt.anim_transform(id).expect("in flight");
assert!(
(mid.scale_x - 1.5).abs() < 1e-3 && (mid.scale_y - 1.5).abs() < 1e-3,
"halfway between the two: {mid:?}"
);
assert_eq!(mid.rotation, 0.0, "and it turned nothing on the way");
rt.advance_transforms(&scaled(1.0), 1.0);
assert_eq!(rt.anim_transform(id).map(|t| t.scale_x), Some(1.0));
assert!(
!rt.advance_transforms(&scaled(1.0), 0.05),
"and stops asking for frames once it is there"
);
}
#[test]
fn a_rotation_mounts_settled_moves_and_arrives() {
let turned = |to: f32| -> AnimatedRotation<()> {
AnimatedRotation::new(to, 0.10, Curve::Linear, Text::new("x"))
};
let mut rt = Runtime::default();
assert!(!rt.advance_transforms(&turned(0.0), 1.0));
let id = WidgetId::ROOT;
let mounted = rt.anim_transform(id).expect("mounted");
assert_eq!((mounted.scale_x, mounted.scale_y), (1.0, 1.0), "identity");
assert!(rt.advance_transforms(&turned(1.0), 0.05));
let mid = rt.anim_transform(id).expect("in flight");
assert!((mid.rotation - 0.5).abs() < 1e-3, "halfway: {mid:?}");
assert_eq!(
(mid.scale_x, mid.scale_y),
(1.0, 1.0),
"and the scale stayed the identity, not nought"
);
rt.advance_transforms(&turned(1.0), 1.0);
assert_eq!(rt.anim_transform(id).map(|t| t.rotation), Some(1.0));
}
#[test]
fn a_padding_mounts_settled_moves_and_arrives() {
let padded = |to: f32| -> AnimatedPadding<()> {
AnimatedPadding::new(to, 0.10, Curve::Linear, Text::new("x"))
};
let mut rt = Runtime::default();
assert!(!rt.advance_paddings(&padded(0.0), 1.0));
let id = WidgetId::ROOT;
assert_eq!(rt.anim_padding(id), Some(Insets::uniform(0.0)));
assert!(rt.advance_paddings(&padded(20.0), 0.05));
let mid = rt.anim_padding(id).expect("in flight");
assert!((mid.left - 10.0).abs() < 1e-3, "halfway: {mid:?}");
rt.advance_paddings(&padded(20.0), 1.0);
assert_eq!(rt.anim_padding(id), Some(Insets::uniform(20.0)));
}
#[test]
fn the_paint_uses_the_tweened_transform_and_not_the_target() {
let matrix_of = |rt: &Runtime, widget: &AnimatedScale<()>| {
let ui = build_ui(widget, Size::new(100.0, 100.0), rt, &Theme::default());
ui.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Layer {
transform: Some(t), ..
} => Some(t.affine),
_ => None,
})
.map(|m| m.m[0])
};
let mut rt = Runtime::default();
rt.advance_transforms(&scaled(3.0), 1.0);
rt.advance_transforms(&scaled(1.0), 0.05);
let painted = matrix_of(&rt, &scaled(1.0)).expect("a transformed layer");
assert!(
(painted - 2.0).abs() < 1e-2,
"the paint took the tweened scale (2), not the target (1): {painted}"
);
let fresh = Runtime::default();
let painted = matrix_of(&fresh, &scaled(3.0)).expect("a transformed layer");
assert!((painted - 3.0).abs() < 1e-2, "the target: {painted}");
}
fn anchored(to: frus_core::Alignment) -> AnimatedAlign<()> {
AnimatedAlign::new(to, 0.10, Curve::Linear, Text::new("x"))
}
#[test]
fn an_anchor_mounts_settled_moves_and_arrives() {
let mut rt = Runtime::default();
assert!(
!rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_LEFT), 1.0),
"a mount is not a transition"
);
let id = WidgetId::ROOT;
assert_eq!(
rt.anim_offset(id),
Some((-1.0, 0.0)),
"and it adopts the anchor whole"
);
assert!(rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 0.05));
let mid = rt.anim_offset(id).expect("in flight");
assert!(mid.0.abs() < 1e-3, "at the centre on the way past: {mid:?}");
assert_eq!(mid.1, 0.0, "and it did not drift vertically");
rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 1.0);
assert_eq!(rt.anim_offset(id), Some((1.0, 0.0)));
assert!(
!rt.advance_offsets(&anchored(frus_core::Alignment::CENTER_RIGHT), 0.05),
"and stops asking for frames once it is there"
);
}
#[test]
fn a_slide_mounts_settled_moves_and_arrives() {
let slid = |fx: f32| -> AnimatedSlide<()> {
AnimatedSlide::new(fx, 0.0, 0.10, Curve::Linear, Text::new("x"))
};
let mut rt = Runtime::default();
assert!(!rt.advance_offsets(&slid(-1.0), 1.0));
let id = WidgetId::ROOT;
assert_eq!(rt.anim_offset(id), Some((-1.0, 0.0)));
assert!(rt.advance_offsets(&slid(0.0), 0.05));
let mid = rt.anim_offset(id).expect("in flight");
assert!((mid.0 + 0.5).abs() < 1e-3, "halfway back: {mid:?}");
rt.advance_offsets(&slid(0.0), 1.0);
assert_eq!(rt.anim_offset(id), Some((0.0, 0.0)));
}
#[test]
fn the_paint_uses_the_tweened_offset_and_not_the_target() {
let mark = |root: &dyn Widget<()>, rt: &Runtime| {
let ui = build_ui(root, Size::new(100.0, 40.0), rt, &Theme::default());
ui.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
_ => None,
})
.expect("the mark")
};
let red = frus_core::Color::rgb(1.0, 0.0, 0.0);
let mark_widget = || {
crate::Container::<()>::new()
.width(20.0)
.height(20.0)
.color(red)
};
let aligned = |to: frus_core::Alignment| {
crate::Container::<()>::new()
.width(100.0)
.height(40.0)
.animated_alignment(to, 0.10, Curve::Linear)
.child(mark_widget())
};
let mut rt = Runtime::default();
rt.advance_offsets(&aligned(frus_core::Alignment::CENTER_LEFT), 1.0);
rt.advance_offsets(&aligned(frus_core::Alignment::CENTER_RIGHT), 0.05);
let x = mark(&aligned(frus_core::Alignment::CENTER_RIGHT), &rt);
assert!(
(x - 40.0).abs() < 0.5,
"the anchor's tween reached the paint: {x}"
);
let slid = |fx: f32| AnimatedSlide::new(fx, 0.0, 0.10, Curve::Linear, mark_widget());
let mut rt = Runtime::default();
rt.advance_offsets(&slid(0.0), 1.0);
rt.advance_offsets(&slid(1.0), 0.05);
let x = mark(&slid(1.0), &rt);
assert!(
(x - 10.0).abs() < 0.5,
"the slide's tween reached the paint: {x}"
);
}
#[test]
fn a_directional_anchor_mirrors_as_a_whole() {
let start = frus_core::AlignmentDirectional::CENTER_START;
let end = frus_core::AlignmentDirectional::CENTER_END;
let aligned = |to: frus_core::AlignmentDirectional| {
crate::Container::<()>::new()
.width(100.0)
.height(40.0)
.animated_alignment(to, 0.10, Curve::Linear)
.child(
crate::Container::<()>::new()
.width(20.0)
.height(20.0)
.color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
)
};
let mut rt = Runtime::default();
rt.advance_offsets(&aligned(start), 1.0);
rt.advance_offsets(&aligned(end), 0.025);
let x_in = |rtl: bool| {
let theme = match rtl {
true => Theme::default().rtl(),
false => Theme::default(),
};
let ui = build_ui(&aligned(end), Size::new(100.0, 40.0), &rt, &theme);
ui.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
_ => None,
})
.expect("the mark")
};
let ltr = x_in(false);
let rtl = x_in(true);
assert!(
(ltr - 20.0).abs() < 0.5,
"a quarter across, from the left: {ltr}"
);
assert!(
(rtl - 60.0).abs() < 0.5,
"and the same quarter from the right: {rtl}"
);
}
fn pinned_at(left: f32) -> crate::Stack<()> {
crate::Stack::new().layer(
AnimatedPositioned::new(
0.10,
Curve::Linear,
crate::Container::<()>::new()
.color(frus_core::Color::rgb(1.0, 0.0, 0.0))
.child(Text::new("x")),
)
.left(left)
.top(0.0)
.width(20.0)
.height(20.0),
)
}
#[test]
fn a_layer_mounts_settled_moves_and_arrives() {
let mut rt = Runtime::default();
assert!(
!rt.advance_pins(&pinned_at(0.0), 1.0),
"a mount is not a transition"
);
let id = WidgetId::ROOT.child(0);
assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(0.0));
assert!(rt.advance_pins(&pinned_at(80.0), 0.05));
let mid = rt.anim_pins(id).expect("in flight");
assert!(
mid.left.is_some_and(|l| (l - 40.0).abs() < 1e-3),
"halfway between the two: {mid:?}"
);
assert_eq!(
mid.width,
Some(20.0),
"and the extents it never asked to move stayed"
);
rt.advance_pins(&pinned_at(80.0), 1.0);
assert_eq!(rt.anim_pins(id).and_then(|p| p.left), Some(80.0));
assert!(
!rt.advance_pins(&pinned_at(80.0), 0.05),
"and stops asking for frames once it is there"
);
}
#[test]
fn a_pin_that_is_unset_takes_hold_and_lets_go_at_once() {
let layer = |left: Option<f32>, right: Option<f32>| {
let mut l: AnimatedPositioned<()> =
AnimatedPositioned::new(0.10, Curve::Linear, Text::new("x")).top(0.0);
if let Some(px) = left {
l = l.left(px);
}
if let Some(px) = right {
l = l.right(px);
}
crate::Stack::<()>::new().layer(l)
};
let id = WidgetId::ROOT.child(0);
let mut rt = Runtime::default();
rt.advance_pins(&layer(Some(0.0), None), 1.0);
rt.advance_pins(&layer(None, Some(10.0)), 0.02);
let mid = rt.anim_pins(id).expect("present");
assert_eq!(
mid.left, None,
"the edge that let go let go at once: {mid:?}"
);
assert_eq!(
mid.right,
Some(10.0),
"and the edge that took hold took hold at once: {mid:?}"
);
}
#[test]
fn the_stack_places_the_layer_by_the_tween_and_not_the_target() {
let mut rt = Runtime::default();
rt.advance_pins(&pinned_at(0.0), 1.0);
rt.advance_pins(&pinned_at(80.0), 0.05);
let ui = build_ui(
&pinned_at(80.0),
Size::new(100.0, 40.0),
&rt,
&Theme::default(),
);
let x = ui
.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.x),
_ => None,
})
.expect("the layer");
assert!(
(x - 40.0).abs() < 0.5,
"halfway across, not at either end: {x}"
);
}
fn share(width_factor: f32) -> crate::Flex<()> {
crate::Flex::column().width(100.0).height(40.0).child(
AnimatedFractionallySizedBox::new(
0.10,
Curve::Linear,
crate::Container::<()>::new()
.flex(1.0)
.height(20.0)
.color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
)
.width_factor(width_factor),
)
}
#[test]
fn a_share_of_the_parent_mounts_settled_moves_and_arrives() {
let mut rt = Runtime::default();
assert!(
!rt.advance_fractions(&share(0.25), 1.0),
"a mount is not a transition"
);
let id = WidgetId::ROOT.child(0);
assert_eq!(rt.anim_fractions(id), Some((Some(0.25), None)));
assert!(rt.advance_fractions(&share(0.75), 0.05));
let (width, height) = rt.anim_fractions(id).expect("in flight");
assert!(
width.is_some_and(|w| (w - 0.5).abs() < 1e-3),
"halfway between a quarter and three quarters: {width:?}"
);
assert_eq!(
height, None,
"and the axis nobody set still follows its content"
);
rt.advance_fractions(&share(0.75), 1.0);
assert_eq!(rt.anim_fractions(id), Some((Some(0.75), None)));
assert!(
!rt.advance_fractions(&share(0.75), 0.05),
"and stops asking for frames once it is there"
);
}
#[test]
fn the_layout_uses_the_tweened_fraction_and_not_the_target() {
let mut rt = Runtime::default();
rt.advance_fractions(&share(0.25), 1.0);
rt.advance_fractions(&share(0.75), 0.05);
let ui = build_ui(&share(0.75), Size::new(100.0, 40.0), &rt, &Theme::default());
let width = ui
.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Rect { rect, color, .. } if color.r > 0.5 => Some(rect.width),
_ => None,
})
.expect("the child's background");
assert!(
(width - 50.0).abs() < 0.5,
"half the parent, on the way from a quarter to three quarters: {width}"
);
}
fn styled(size: f32) -> AnimatedDefaultTextStyle<()> {
AnimatedDefaultTextStyle::new(
0.10,
Curve::Linear,
TextStyle::NONE.size(size),
Text::new("Ag"),
)
}
#[test]
fn a_default_text_style_mounts_settled_moves_and_arrives() {
let mut rt = Runtime::default();
assert!(
!rt.advance_text_styles(&styled(24.0), 1.0),
"a mount is not a transition"
);
let id = WidgetId::ROOT;
assert_eq!(
rt.anim_text_style(id).and_then(|s| s.size),
Some(24.0),
"and it adopts the target whole"
);
assert!(rt.advance_text_styles(&styled(12.0), 0.05));
let mid = rt.anim_text_style(id).expect("in flight").size.unwrap();
assert!((mid - 18.0).abs() < 1e-3, "halfway between the two: {mid}");
rt.advance_text_styles(&styled(12.0), 1.0);
assert_eq!(rt.anim_text_style(id).and_then(|s| s.size), Some(12.0));
assert!(
!rt.advance_text_styles(&styled(12.0), 0.05),
"and stops asking for frames once it is there"
);
}
#[test]
fn the_moving_style_reaches_the_words() {
fn painted_size(rt: &Runtime, tree: &AnimatedDefaultTextStyle<()>) -> f32 {
fn walk(primitives: &[Primitive], out: &mut Vec<f32>) {
for p in primitives {
match p {
Primitive::Text { size, .. } => out.push(*size),
Primitive::Layer { primitives, .. } => walk(primitives, out),
_ => {}
}
}
}
let ui = build_ui(tree, Size::new(400.0, 200.0), rt, &Theme::default());
let mut sizes = Vec::new();
walk(ui.scene().primitives(), &mut sizes);
assert_eq!(sizes.len(), 1, "one run of words: {sizes:?}");
sizes[0]
}
let mut rt = Runtime::default();
rt.advance_text_styles(&styled(24.0), 1.0);
assert_eq!(
painted_size(&rt, &styled(24.0)),
24.0,
"mounted at its target"
);
rt.advance_text_styles(&styled(12.0), 0.05);
let mid = painted_size(&rt, &styled(12.0));
assert!(
(mid - 18.0).abs() < 1e-3,
"the words are drawn at the style in flight, not at the one it is heading \
for: {mid}"
);
}
#[test]
fn the_answers_that_are_not_quantities_take_effect_at_once() {
let tree = AnimatedDefaultTextStyle::<()>::new(
0.10,
Curve::Linear,
TextStyle::NONE.size(16.0),
Text::new("Ag"),
)
.align(TextAlign::Center)
.max_lines(2);
let handed = Widget::<()>::theme_override(&tree, &Theme::default()).expect("a theme");
assert_eq!(handed.widgets.text.align, Some(TextAlign::Center));
assert_eq!(handed.widgets.text.max_lines, Some(2));
}
#[test]
fn two_nested_styles_are_two_timelines() {
let nested = AnimatedDefaultTextStyle::<()>::new(
0.10,
Curve::Linear,
TextStyle::NONE.color(frus_core::Color::rgb(1.0, 0.0, 0.0)),
AnimatedDefaultTextStyle::new(
0.10,
Curve::Linear,
TextStyle::NONE.size(20.0),
Text::new("Ag"),
),
);
let mut rt = Runtime::default();
rt.advance_text_styles(&nested, 1.0);
let outer = rt.anim_text_style(WidgetId::ROOT).expect("the outer one");
let inner_id = crate::ui::child_id(WidgetId::ROOT, 0, nested.children()[0].as_ref());
let inner = rt.anim_text_style(inner_id).expect("the inner one");
assert_ne!(inner_id, WidgetId::ROOT, "two nodes, not one");
assert_eq!(outer.size, None, "the outer one says nothing about size");
assert_eq!(inner.size, Some(20.0), "and the inner one does");
assert!(outer.color.is_some() && inner.color.is_none());
}
fn square_at(rt: &Runtime, root: &dyn Widget<()>, size: frus_core::Size) -> frus_core::Rect {
let red = frus_core::Color::rgb(1.0, 0.0, 0.0);
build_ui(root, size, rt, &Theme::dark())
.scene()
.primitives()
.iter()
.find_map(|p| match p {
Primitive::Rect { rect, color, .. } if *color == red => Some(*rect),
_ => None,
})
.expect("the square")
}
fn square() -> Container<()> {
Container::new()
.width(20.0)
.height(20.0)
.color(frus_core::Color::rgb(1.0, 0.0, 0.0))
}
#[test]
fn an_anchor_slides_across_a_box_with_a_factor() {
let size = frus_core::Size::new(300.0, 100.0);
let tree = |to| {
crate::Flex::<()>::row()
.height(100.0)
.child(AnimatedAlign::new(to, 0.10, Curve::Linear, square()).width_factor(4.0))
};
let (left, right) = (
frus_core::Alignment::CENTER_LEFT,
frus_core::Alignment::CENTER_RIGHT,
);
let mut rt = Runtime::default();
rt.advance_offsets(&tree(left), 1.0);
assert!(square_at(&rt, &tree(left), size).x.abs() < 0.5);
rt.advance_offsets(&tree(right), 0.05);
let mid = square_at(&rt, &tree(right), size);
assert!(
(mid.x - 30.0).abs() < 0.5,
"half way across the 80-px box: {mid:?}"
);
rt.advance_offsets(&tree(right), 1.0);
let end = square_at(&rt, &tree(right), size);
assert!((end.x - 60.0).abs() < 0.5, "{end:?}");
}
#[test]
fn an_anchor_slides_across_the_room_beside_a_factor() {
let size = frus_core::Size::new(200.0, 300.0);
let tree = |to| {
crate::Flex::<()>::column()
.child(AnimatedAlign::new(to, 0.10, Curve::Linear, square()).height_factor(2.0))
};
let (left, right) = (
frus_core::Alignment::TOP_LEFT,
frus_core::Alignment::TOP_RIGHT,
);
let mut rt = Runtime::default();
rt.advance_offsets(&tree(left), 1.0);
rt.advance_offsets(&tree(right), 0.05);
let mid = square_at(&rt, &tree(right), size);
assert!(
(mid.x - 90.0).abs() < 0.5,
"half way across the page: {mid:?}"
);
assert!(mid.y.abs() < 0.5, "at the top of its 40-px box: {mid:?}");
}
#[test]
fn an_animated_anchor_takes_the_room_as_aligned_does() {
let size = frus_core::Size::new(300.0, 300.0);
let root = Container::<()>::new()
.width(100.0)
.height(60.0)
.child(AnimatedAlign::new(
frus_core::Alignment::BOTTOM_RIGHT,
0.10,
Curve::Linear,
square(),
));
let at = square_at(&Runtime::default(), &root, size);
assert!(
(at.x - 80.0).abs() < 0.5 && (at.y - 40.0).abs() < 0.5,
"{at:?}"
);
}
}