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 autofill_group(&self) -> bool {
self.inner.autofill_group()
}
});
#[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 autofill_group(&self) -> bool {
self.inner.autofill_group()
}
});
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 autofill_group(&self) -> bool {
self.inner.autofill_group()
}
});
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(scene, bounds, status.opacity);
}
}
fn foreground(&self, _theme: &Theme) -> Option<BoxDecoration> {
(self.position == DecorationPosition::Foreground).then_some(self.decoration)
}
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 autofill_group(&self) -> bool {
self.inner.autofill_group()
}
});
#[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 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) {
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).expect("the mark")
}
#[test]
fn a_fade_is_the_value_it_was_given() {
for (given, expected) in [(0.0, 0.0), (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]);
}
}