use frus_core::{Alignment, BoxDecoration, Curve, Rect, Scene, TextAlign, TextOverflow, TextStyle};
use frus_layout::Style;
use crate::interaction::Status;
use crate::theme::Theme;
use crate::widget::Widget;
pub struct FadeTransition<Msg = crate::callback::Callback> {
inner: Box<dyn Widget<Msg>>,
}
impl<Msg: Clone + 'static> FadeTransition<Msg> {
pub fn new(opacity: f32, child: impl Widget<Msg> + 'static) -> Self {
Self {
inner: Box::new(crate::Opacity::new(opacity.clamp(0.0, 1.0), child)),
}
}
}
impl<Msg> FadeTransition<Msg> {
fn restyle(&self, base: Style) -> Style {
base
}
}
crate::transparent::forward_transparent!(FadeTransition {
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)
}
});
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SlideFrom {
Left,
Right,
Top,
Bottom,
}
pub struct SlideTransition<Msg = crate::callback::Callback> {
inner: Box<dyn Widget<Msg>>,
}
impl<Msg: Clone + 'static> SlideTransition<Msg> {
pub fn new(fx: f32, fy: f32, child: impl Widget<Msg> + 'static) -> Self {
Self {
inner: Box::new(crate::FractionalTranslation::new(fx, fy).child(child)),
}
}
pub fn from_edge(edge: SlideFrom, progress: f32, child: impl Widget<Msg> + 'static) -> Self {
let away = 1.0 - progress;
let (fx, fy) = match edge {
SlideFrom::Left => (-away, 0.0),
SlideFrom::Right => (away, 0.0),
SlideFrom::Top => (0.0, -away),
SlideFrom::Bottom => (0.0, away),
};
Self::new(fx, fy, child)
}
}
impl<Msg> SlideTransition<Msg> {
fn restyle(&self, base: Style) -> Style {
base
}
}
crate::transparent::forward_transparent!(SlideTransition {
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 ScaleTransition<Msg = crate::callback::Callback> {
inner: Box<dyn Widget<Msg>>,
}
impl<Msg: Clone + 'static> ScaleTransition<Msg> {
pub fn new(scale: f32, child: impl Widget<Msg> + 'static) -> Self {
Self::about(scale, Alignment::CENTER, child)
}
pub fn about(scale: f32, pivot: Alignment, child: impl Widget<Msg> + 'static) -> Self {
Self {
inner: Box::new(
crate::Transform::scale_xy_from(scale, scale, pivot)
.child(child),
),
}
}
}
impl<Msg> ScaleTransition<Msg> {
fn restyle(&self, base: Style) -> Style {
base
}
}
crate::transparent::forward_transparent!(ScaleTransition {
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 fn eased(progress: f32, curve: &Curve) -> f32 {
curve.transform(progress.clamp(0.0, 1.0))
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DecorationPosition {
#[default]
Background,
Foreground,
}
pub struct DecoratedBoxTransition<Msg = crate::callback::Callback> {
children: Vec<Box<dyn Widget<Msg>>>,
decoration: BoxDecoration,
position: DecorationPosition,
}
impl<Msg> DecoratedBoxTransition<Msg> {
pub fn new(decoration: BoxDecoration, child: impl Widget<Msg> + 'static) -> Self {
Self {
children: vec![Box::new(child)],
decoration,
position: DecorationPosition::Background,
}
}
pub fn between(
from: BoxDecoration,
to: BoxDecoration,
progress: f32,
child: impl Widget<Msg> + 'static,
) -> Self {
Self::new(from.lerp(to, progress), child)
}
pub fn position(mut self, position: DecorationPosition) -> Self {
self.position = position;
self
}
}
impl<Msg: Clone> Widget<Msg> for DecoratedBoxTransition<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) {
if self.position == DecorationPosition::Background {
self.decoration
.paint_into_in(scene, bounds, status.opacity, theme.direction);
}
}
fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
(self.position == DecorationPosition::Foreground).then_some(self.decoration.clone())
}
fn on_click(&self) -> Option<Msg> {
None
}
fn debug_name(&self) -> &'static str {
"DecoratedBoxTransition"
}
}
pub struct DefaultTextStyleTransition<Msg = crate::callback::Callback> {
handed: crate::widgettheme::DefaultTextStyle,
inner: Box<dyn Widget<Msg>>,
}
impl<Msg: Clone + 'static> DefaultTextStyleTransition<Msg> {
pub fn new(style: TextStyle, child: impl Widget<Msg> + 'static) -> Self {
Self {
handed: crate::widgettheme::DefaultTextStyle::from_text_style(style),
inner: Box::new(child),
}
}
pub fn between(
from: TextStyle,
to: TextStyle,
progress: f32,
child: impl Widget<Msg> + 'static,
) -> Self {
Self::new(from.lerp(to, progress), child)
}
pub fn align(mut self, align: TextAlign) -> Self {
self.handed.align = Some(align);
self
}
pub fn soft_wrap(mut self, wrap: bool) -> Self {
self.handed.soft_wrap = Some(wrap);
self
}
pub fn overflow(mut self, overflow: TextOverflow) -> Self {
self.handed.overflow = Some(overflow);
self
}
pub fn max_lines(mut self, lines: usize) -> Self {
self.handed.max_lines = Some(lines);
self
}
}
impl<Msg> DefaultTextStyleTransition<Msg> {
fn restyle(&self, base: Style) -> Style {
base
}
}
crate::transparent::forward_transparent!(DefaultTextStyleTransition {
fn key(&self) -> Option<u64> {
self.inner.key()
}
fn positioned(&self) -> Option<crate::positioned::Positioning> {
self.inner.positioned()
}
fn theme_override(&self, inherited: &Theme) -> Option<Box<Theme>> {
let mut mine = inherited.clone();
mine.widgets.text = mine.widgets.text.merge(self.handed);
Some(
self.inner
.theme_override(&mine)
.unwrap_or_else(|| Box::new(mine)),
)
}
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 SizeTransition<Msg = crate::callback::Callback> {
factor: f32,
axis: crate::scroll::Axis,
axis_alignment: f32,
children: Vec<Box<dyn Widget<Msg>>>,
}
impl<Msg> SizeTransition<Msg> {
pub fn new(factor: f32, child: impl Widget<Msg> + 'static) -> Self {
Self {
factor,
axis: crate::scroll::Axis::Vertical,
axis_alignment: 0.0,
children: vec![Box::new(child)],
}
}
pub fn axis(mut self, axis: crate::scroll::Axis) -> Self {
self.axis = axis;
self
}
pub fn axis_alignment(mut self, alignment: f32) -> Self {
self.axis_alignment = alignment;
self
}
fn vertical(&self) -> bool {
!matches!(self.axis, crate::scroll::Axis::Horizontal)
}
}
impl<Msg: Clone> Widget<Msg> for SizeTransition<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 constraints_transform(&self) -> Option<crate::constraints::ConstraintsTransform> {
use crate::constraints::AxisConstraint::{AsGiven, Unbounded};
let (width, height, alignment) = if self.vertical() {
(
AsGiven,
Unbounded,
Alignment {
x: -1.0,
y: self.axis_alignment,
},
)
} else {
(
Unbounded,
AsGiven,
Alignment {
x: self.axis_alignment,
y: -1.0,
},
)
};
Some(crate::constraints::ConstraintsTransform {
width,
height,
alignment,
report: false,
})
}
fn size_factor(&self) -> Option<(f32, f32)> {
let factor = self.factor.max(0.0);
Some(if self.vertical() {
(1.0, factor)
} else {
(factor, 1.0)
})
}
fn clips_to_box(&self) -> bool {
true
}
fn fill_axes(&self, _theme: &Theme) -> crate::widget::FillAxes {
crate::widget::FillAxes {
horizontal: self.vertical(),
vertical: !self.vertical(),
}
}
fn debug_name(&self) -> &'static str {
"SizeTransition"
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{build_ui, Container, Runtime, Theme};
use frus_core::{Color, Primitive, Rect, Size};
const RED: Color = Color {
r: 1.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
const BLUE: Color = Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
fn mark() -> Container<()> {
Container::new().width(40.0).height(20.0).color(RED)
}
fn red(primitives: &[Primitive]) -> Option<(Rect, Rect)> {
primitives.iter().find_map(|p| match p {
Primitive::Rect {
rect, clip, color, ..
} if color.r > 0.5 && color.g < 0.5 => Some((*rect, *clip)),
Primitive::Layer { primitives, .. } => red(primitives),
_ => None,
})
}
fn revealed(factor: f32, alignment: f32) -> crate::Ui<()> {
let root = crate::Flex::<()>::column()
.child(SizeTransition::new(factor, mark()).axis_alignment(alignment))
.child(Container::new().width(40.0).height(10.0).color(BLUE));
build_ui(
&root,
Size::new(100.0, 100.0),
&Runtime::default(),
&Theme::dark(),
)
}
#[test]
fn a_size_transition_is_a_fraction_of_its_child() {
let ui = revealed(0.5, -1.0);
assert_eq!(blue_y(ui.scene().primitives()), Some(10.0));
let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
assert_eq!(rect.height, 20.0, "uncovered, not squeezed");
assert_eq!((rect.y, clip.y + clip.height), (0.0, 10.0), "its top half");
}
#[test]
fn the_axis_alignment_chooses_what_shows() {
let ui = revealed(0.5, 1.0);
let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
assert_eq!(
rect.y, -10.0,
"the mark moved up by the half that is hidden"
);
assert_eq!(clip.y + clip.height, 10.0);
}
#[test]
fn zero_is_nothing_and_one_is_all() {
assert_eq!(blue_y(revealed(0.0, -1.0).scene().primitives()), Some(0.0));
assert_eq!(blue_y(revealed(1.0, -1.0).scene().primitives()), Some(20.0));
}
#[test]
fn a_new_factor_is_a_new_box() {
let rt = Runtime::default();
let frame = |factor: f32| {
let root = crate::Flex::<()>::column()
.child(SizeTransition::new(factor, mark()))
.child(Container::new().width(40.0).height(10.0).color(BLUE));
let ui = build_ui(&root, Size::new(100.0, 100.0), &rt, &Theme::dark());
blue_y(ui.scene().primitives())
};
assert_eq!(frame(0.5), Some(10.0));
assert_eq!(frame(1.0), Some(20.0));
}
#[test]
fn across_the_axis_the_box_is_the_room() {
let root = Container::<()>::new()
.width(100.0)
.child(SizeTransition::new(
1.0,
Container::new().height(20.0).color(RED),
));
let ui = build_ui(
&root,
Size::new(300.0, 100.0),
&Runtime::default(),
&Theme::dark(),
);
let (rect, _) = red(ui.scene().primitives()).expect("the mark");
assert_eq!(rect.width, 100.0);
}
#[test]
fn a_horizontal_size_transition_reveals_across() {
let root = crate::Flex::<()>::row()
.child(
SizeTransition::new(0.25, mark())
.axis(crate::scroll::Axis::Horizontal)
.axis_alignment(-1.0),
)
.child(Container::new().width(40.0).height(10.0).color(BLUE));
let ui = build_ui(
&root,
Size::new(100.0, 100.0),
&Runtime::default(),
&Theme::dark(),
);
fn blue_x(primitives: &[Primitive]) -> Option<f32> {
primitives.iter().find_map(|p| match p {
Primitive::Rect { rect, color, .. } if color.b > 0.5 && color.r < 0.5 => {
Some(rect.x)
}
Primitive::Layer { primitives, .. } => blue_x(primitives),
_ => None,
})
}
assert_eq!(blue_x(ui.scene().primitives()), Some(10.0));
let (rect, clip) = red(ui.scene().primitives()).expect("the mark");
assert_eq!(rect.width, 40.0);
assert_eq!(clip.x + clip.width, 10.0);
}
fn blue_y(primitives: &[Primitive]) -> Option<f32> {
for primitive in primitives {
match primitive {
Primitive::Rect { rect, color, .. } if color.b > 0.5 && color.r < 0.5 => {
return Some(rect.y)
}
Primitive::Layer { primitives, .. } => {
if let Some(found) = blue_y(primitives) {
return Some(found);
}
}
_ => {}
}
}
None
}
fn painted(root: &dyn Widget<()>) -> (Rect, f32) {
painted_if_any(root).expect("the mark")
}
fn painted_if_any(root: &dyn Widget<()>) -> Option<(Rect, f32)> {
let ui = build_ui(
root,
Size::new(100.0, 100.0),
&Runtime::default(),
&Theme::dark(),
);
fn walk(primitives: &[Primitive], opacity: f32) -> Option<(Rect, f32)> {
for primitive in primitives {
match primitive {
Primitive::Rect { rect, color, .. } if color.r > 0.5 && color.g < 0.5 => {
return Some((*rect, opacity))
}
Primitive::Layer {
primitives,
opacity: layer,
..
} => {
if let Some(found) = walk(primitives, opacity * layer) {
return Some(found);
}
}
_ => {}
}
}
None
}
walk(ui.scene().primitives(), 1.0)
}
#[test]
fn a_fade_is_the_value_it_was_given() {
assert_eq!(painted_if_any(&FadeTransition::new(0.0, mark())), None);
for (given, expected) in [(0.5, 0.5), (1.0, 1.0)] {
let root = FadeTransition::new(given, mark());
let (_, opacity) = painted(&root);
assert!(
(opacity - expected).abs() < 1e-3,
"at {given}: painted at {opacity}"
);
}
}
#[test]
fn a_slide_is_a_multiple_of_the_child_s_own_box() {
for (given, expected) in [(0.0, 0.0), (0.5, 20.0), (1.0, 40.0)] {
let root = crate::Flex::<()>::column().child(SlideTransition::new(given, 0.0, mark()));
let (rect, _) = painted(&root);
assert!(
(rect.x - expected).abs() < 0.5,
"at {given}: painted at {}",
rect.x
);
}
}
#[test]
fn coming_in_from_an_edge_arrives_at_one() {
let at = |progress: f32| {
let root = crate::Flex::<()>::column().child(SlideTransition::from_edge(
SlideFrom::Left,
progress,
mark(),
));
painted(&root).0.x
};
assert!((at(0.0) + 40.0).abs() < 0.5, "a whole width outside");
assert!((at(0.5) + 20.0).abs() < 0.5, "half of one, still outside");
assert!(at(1.0).abs() < 0.5, "and home");
let mark_at = |edge: SlideFrom| {
let root =
crate::Flex::<()>::column().child(SlideTransition::from_edge(edge, 0.0, mark()));
let rect = painted(&root).0;
(rect.x, rect.y)
};
assert!(mark_at(SlideFrom::Left).0 < -1.0);
assert!(mark_at(SlideFrom::Right).0 > 1.0);
assert!(mark_at(SlideFrom::Top).1 < -1.0);
assert!(mark_at(SlideFrom::Bottom).1 > 1.0);
}
#[test]
fn a_scale_is_paint_and_not_layout() {
let built = |scale: f32| {
let root = crate::Flex::<()>::column()
.width(100.0)
.child(ScaleTransition::new(scale, mark()))
.child(Container::<()>::new().width(10.0).height(10.0).color(BLUE));
let ui = build_ui(
&root,
Size::new(100.0, 100.0),
&Runtime::default(),
&Theme::dark(),
);
let matrix = ui.scene().primitives().iter().find_map(|p| match p {
Primitive::Layer {
transform: Some(t), ..
} => Some(t.affine.m[0]),
_ => None,
});
let neighbour = blue_y(ui.scene().primitives()).expect("the neighbour");
(matrix, neighbour)
};
for (given, expected) in [(0.5, Some(0.5)), (1.0, None), (2.0, Some(2.0))] {
let (matrix, neighbour) = built(given);
match (matrix, expected) {
(Some(m), Some(e)) => {
assert!((m - e).abs() < 1e-3, "at {given}: painted at {m}")
}
(None, None) => {}
_ => panic!("at {given}: {matrix:?} where {expected:?} was expected"),
}
assert!(
(neighbour - 20.0).abs() < 0.5,
"the neighbour stayed where the layout put it: {neighbour}"
);
}
}
#[test]
fn easing_a_progress_is_the_frameworks_own_curve() {
let curve = Curve::ease_out();
assert!(eased(0.0, &curve).abs() < 1e-3);
assert!((eased(1.0, &curve) - 1.0).abs() < 1e-3);
assert_eq!(eased(0.5, &curve), curve.transform(0.5));
assert!(
eased(0.5, &curve) > 0.5,
"an ease-out is ahead at the middle"
);
assert_eq!(
eased(2.0, &curve),
eased(1.0, &curve),
"and a progress past its end is its end"
);
}
use frus_core::{Border, TextStyle};
fn rects_of(root: &dyn Widget<()>) -> Vec<(Rect, Color)> {
let ui = build_ui(
root,
Size::new(200.0, 200.0),
&Runtime::default(),
&Theme::dark(),
);
fn walk(primitives: &[Primitive], out: &mut Vec<(Rect, Color)>) {
for primitive in primitives {
match primitive {
Primitive::Rect { rect, color, .. } => out.push((*rect, *color)),
Primitive::Layer { primitives, .. } => walk(primitives, out),
_ => {}
}
}
}
let mut out = Vec::new();
walk(ui.scene().primitives(), &mut out);
out
}
fn texts_of(root: &dyn Widget<()>) -> Vec<(f32, Color)> {
let ui = build_ui(
root,
Size::new(200.0, 200.0),
&Runtime::default(),
&Theme::dark(),
);
fn walk(primitives: &[Primitive], out: &mut Vec<(f32, Color)>) {
for primitive in primitives {
match primitive {
Primitive::Text { size, color, .. } => out.push((*size, *color)),
Primitive::Layer { primitives, .. } => walk(primitives, out),
_ => {}
}
}
}
let mut out = Vec::new();
walk(ui.scene().primitives(), &mut out);
out
}
fn close(a: Color, b: Color) -> bool {
(a.r - b.r).abs() < 1e-4
&& (a.g - b.g).abs() < 1e-4
&& (a.b - b.b).abs() < 1e-4
&& (a.a - b.a).abs() < 1e-4
}
#[test]
fn a_decoration_is_painted_as_it_stands() {
for (t, want) in [(0.0, RED), (0.5, RED.lerp(BLUE, 0.5)), (1.0, BLUE)] {
let tree = DecoratedBoxTransition::<()>::between(
BoxDecoration::filled(RED),
BoxDecoration::filled(BLUE),
t,
Container::new().width(40.0).height(20.0),
);
let rects = rects_of(&tree);
assert_eq!(rects.len(), 1, "at {t}: {rects:?}");
assert!(close(rects[0].1, want), "at {t}: {:?}", rects[0].1);
}
}
#[test]
fn a_fading_fill_is_painted_in_its_own_colour() {
let tree = DecoratedBoxTransition::<()>::between(
BoxDecoration::filled(RED),
BoxDecoration::default(),
0.5,
Container::new().width(40.0).height(20.0),
);
let rects = rects_of(&tree);
assert_eq!(rects.len(), 1, "{rects:?}");
assert!(close(rects[0].1, RED.fade(0.5)), "{:?}", rects[0].1);
}
#[test]
fn a_thickening_border_does_not_move_the_child() {
let child_at = |t: f32| {
let tree = DecoratedBoxTransition::<()>::between(
BoxDecoration::default(),
BoxDecoration::default().border(Border::new(8.0, BLUE)),
t,
mark(),
);
rects_of(&tree)
.into_iter()
.find(|(_, c)| close(*c, RED))
.expect("the child")
.0
};
let (start, end) = (child_at(0.0), child_at(1.0));
assert_eq!(
(start.x, start.y),
(end.x, end.y),
"the child stayed put: {start:?} then {end:?}"
);
}
#[test]
fn in_front_the_decoration_covers_the_child() {
let wash = || BoxDecoration::filled(BLUE.fade(0.3));
let order = |tree: &DecoratedBoxTransition<()>| {
let rects = rects_of(tree);
let child = rects.iter().position(|(_, c)| close(*c, RED));
let over = rects.iter().position(|(_, c)| c.b > 0.5);
(child.expect("the child"), over.expect("the wash"))
};
let (child, over) = order(
&DecoratedBoxTransition::new(wash(), mark()).position(DecorationPosition::Foreground),
);
assert!(child < over, "in front: the wash comes after the child");
let (child, under) = order(&DecoratedBoxTransition::new(wash(), mark()));
assert!(under < child, "behind, by default");
}
#[test]
fn a_text_style_is_handed_down_as_it_stands() {
for (t, want) in [(0.0, 12.0), (0.5, 18.0), (1.0, 24.0)] {
let tree = DefaultTextStyleTransition::<()>::between(
TextStyle::NONE.size(12.0),
TextStyle::NONE.size(24.0),
t,
crate::Text::new("Ag"),
);
let sizes: Vec<f32> = texts_of(&tree).into_iter().map(|(s, _)| s).collect();
assert_eq!(sizes, vec![want], "at {t}");
}
}
#[test]
fn two_nested_hand_down_both_halves() {
let tree = DefaultTextStyleTransition::<()>::new(
TextStyle::NONE.color(BLUE),
DefaultTextStyleTransition::new(TextStyle::NONE.size(20.0), crate::Text::new("Ag")),
);
let texts = texts_of(&tree);
assert_eq!(texts.len(), 1, "{texts:?}");
assert_eq!(texts[0].0, 20.0, "the inner one's size");
assert!(close(texts[0].1, BLUE), "and the outer one's colour");
}
#[test]
fn a_text_that_names_its_size_keeps_it() {
let tree = DefaultTextStyleTransition::<()>::new(
TextStyle::NONE.size(30.0),
crate::Text::new("Ag").size(11.0),
);
let sizes: Vec<f32> = texts_of(&tree).into_iter().map(|(s, _)| s).collect();
assert_eq!(sizes, vec![11.0]);
}
}