freya-core 0.5.0

Reactivity runtime, tree management, accessibility integration, rendering pipeline and more, for Freya
Documentation
use freya::prelude::*;
use freya_engine::prelude::{
    SkColor,
    SkImage,
};
use freya_testing::prelude::*;

#[test]
pub fn basic_render() {
    fn app() -> impl IntoElement {
        let mut show_popup = use_state(|| true);

        rect()
            .child(
                Popup::new()
                    .on_close_request(move |_| show_popup.set(false))
                    .maybe(show_popup(), |popup| {
                        popup
                            .child(PopupTitle::new("Title".to_string()))
                            .child(PopupContent::new().child("Hello, World!"))
                    }),
            )
            .child(
                Button::new()
                    .child("Open")
                    .on_press(move |_| show_popup.toggle()),
            )
    }

    let mut test = launch_test(app);
    test.sync_and_update();

    let data = test.render();

    assert!(!data.is_empty());
}

fn render_filled_text(fill: Fill, left_padding: f32) -> SkImage {
    let mut test = launch_test(move || {
        rect()
            .width(Size::px(400.))
            .height(Size::px(200.))
            .background(Color::WHITE)
            .padding((0., 0., 0., left_padding))
            .child(
                label()
                    .width(Size::px(300.))
                    .text_align(TextAlign::Center)
                    .font_size(28.)
                    .color(fill.clone())
                    .text("A somewhat long line that wraps around"),
            )
    });
    test.sync_and_update();

    SkImage::from_encoded(test.render())
        .and_then(|image| image.make_raster_image(None, None))
        .unwrap()
}

#[test]
pub fn gradient_text_is_not_clipped() {
    fn dark_column_bounds(fill: Fill) -> (i32, i32) {
        let image = render_filled_text(fill, 0.);
        let pixels = image.peek_pixels().unwrap();
        let dark: Vec<i32> = (0..pixels.width())
            .filter(|&x| (0..pixels.height()).any(|y| pixels.get_color((x, y)).r() < 128))
            .collect();

        (dark[0], dark[dark.len() - 1])
    }

    let solid = dark_column_bounds(Color::BLACK.into());
    let gradient = dark_column_bounds(
        LinearGradient::new()
            .stop((Color::BLACK, 0.))
            .stop((Color::BLACK, 100.))
            .into(),
    );

    assert_eq!(
        solid, gradient,
        "gradient text does not cover the same columns as solid text"
    );
}

#[test]
pub fn gradient_text_fits_aligned_text() {
    for alignment in [TextAlign::Left, TextAlign::Center, TextAlign::Right] {
        let mut test = launch_test(move || {
            rect()
                .width(Size::px(400.))
                .height(Size::px(100.))
                .background(Color::WHITE)
                .child(
                    label()
                        .width(Size::px(300.))
                        .font_size(28.)
                        .text_align(alignment)
                        .color(
                            LinearGradient::new()
                                .angle(90.)
                                .stop((Color::RED, 0.))
                                .stop((Color::BLACK, 100.)),
                        )
                        .text("Gradient text"),
                )
        });
        test.sync_and_update();

        let image = SkImage::from_encoded(test.render())
            .and_then(|image| image.make_raster_image(None, None))
            .unwrap();
        let pixels = image.peek_pixels().unwrap();
        let text_pixels: Vec<_> = (0..pixels.width())
            .flat_map(|x| (0..pixels.height()).map(move |y| (x, y)))
            .filter_map(|(x, y)| {
                let color = pixels.get_color((x, y));
                (color.g() < 100).then_some((x, color.r()))
            })
            .collect();

        let left = text_pixels.iter().map(|(x, _)| *x).min().unwrap();
        let right = text_pixels.iter().map(|(x, _)| *x).max().unwrap();

        let start_red = text_pixels
            .iter()
            .filter(|(x, _)| *x < left + 10)
            .map(|(_, red)| *red)
            .min()
            .unwrap();
        let end_red = text_pixels
            .iter()
            .filter(|(x, _)| *x > right - 10)
            .map(|(_, red)| *red)
            .max()
            .unwrap();

        assert!(
            start_red < 50,
            "gradient starts too late for {alignment:?}: {start_red} ({left}..{right})"
        );
        assert!(
            end_red > 180,
            "gradient ends too early for {alignment:?}: {end_red} ({left}..{right})"
        );
    }
}

#[test]
pub fn gradient_text_follows_the_paragraph() {
    let offset = 50;
    let fill: Fill = LinearGradient::new()
        .angle(90.)
        .stop((Color::BLACK, 0.))
        .stop((Color::WHITE, 100.))
        .into();

    let origin_image = render_filled_text(fill.clone(), 0.);
    let moved_image = render_filled_text(fill, offset as f32);
    let origin = origin_image.peek_pixels().unwrap();
    let moved = moved_image.peek_pixels().unwrap();

    let biggest_difference = (0..origin.height())
        .flat_map(|y| (0..origin.width() - offset).map(move |x| (x, y)))
        .filter_map(|(x, y)| {
            let color = origin.get_color((x, y));
            if color == SkColor::WHITE {
                return None;
            }

            let shifted = moved.get_color((x + offset, y));
            Some(
                color
                    .r()
                    .abs_diff(shifted.r())
                    .max(color.g().abs_diff(shifted.g()))
                    .max(color.b().abs_diff(shifted.b())),
            )
        })
        .max()
        .expect("no text pixels rendered");

    assert!(
        biggest_difference <= 4,
        "the gradient did not follow the text, off by {biggest_difference} levels"
    );
}