freya-components 0.5.0

Components for Freya apps
Documentation
use freya::prelude::*;
use freya_testing::prelude::*;

#[derive(PartialEq)]
struct PanelContent {
    name: &'static str,
    key: DiffKey,
}

impl KeyExt for PanelContent {
    fn key_mut(&mut self) -> &mut DiffKey {
        &mut self.key
    }
}

impl Component for PanelContent {
    fn render(&self) -> impl IntoElement {
        let mut count = use_state(|| 0);
        rect()
            .expanded()
            .on_press(move |_| *count.write() += 1)
            .child(
                label()
                    .expanded()
                    .text(format!("{}:{}", self.name, count())),
            )
    }

    fn render_key(&self) -> DiffKey {
        self.key.clone().or(self.default_key())
    }
}

#[derive(Clone, PartialEq)]
struct Configuration {
    panels: Vec<ResizablePanel>,
    direction: Direction,
}

fn app() -> impl IntoElement {
    let configuration = use_consume::<State<Configuration>>();
    let controller = use_consume::<State<ResizableContext>>();
    let configuration = configuration.read();

    ResizableContainer::new()
        .direction(configuration.direction)
        .controller(controller)
        .panels_iter(configuration.panels.iter().cloned())
}

fn launch(
    panels: Vec<ResizablePanel>,
) -> (TestingRunner, State<Configuration>, State<ResizableContext>) {
    let (test, (configuration, controller)) = TestingRunner::new(
        app,
        (500., 500.).into(),
        move |runner| {
            let configuration = runner.provide_root_context(|| {
                State::create(Configuration {
                    panels,
                    direction: Direction::Horizontal,
                })
            });
            let controller =
                runner.provide_root_context(|| State::create(ResizableContext::default()));
            (configuration, controller)
        },
        1.,
    );
    (test, configuration, controller)
}

fn panel(name: &'static str, initial_size: PanelSize) -> ResizablePanel {
    ResizablePanel::new(initial_size)
        .key(name)
        .child(PanelContent {
            name,
            key: DiffKey::None,
        })
}

fn panels(names: &[&'static str]) -> Vec<ResizablePanel> {
    names
        .iter()
        .map(|&name| panel(name, PanelSize::px(150.)))
        .collect()
}

fn sizes(controller: State<ResizableContext>) -> Vec<f32> {
    controller
        .peek()
        .panels()
        .iter()
        .map(|panel| panel.size)
        .collect()
}

fn snapshot(test: &TestingRunner) -> Vec<(String, f32)> {
    test.find_many(|node, element| Label::try_downcast(element).map(|_| node))
        .iter()
        .map(|node| {
            (
                Label::try_downcast(&*node.element())
                    .unwrap()
                    .text
                    .to_string(),
                node.layout().area.width(),
            )
        })
        .collect()
}

#[test]
fn keyed_moves_preserve_sizes_and_child_state() {
    let (mut test, mut configuration, mut controller) = launch(panels(&["A", "B"]));
    test.sync_and_update();
    test.click_cursor((50., 50.));
    assert!(controller.write().apply_resize(1, 30., 500.));
    test.sync_and_update();
    assert_eq!(
        snapshot(&test),
        vec![("A:1".into(), 180.), ("B:0".into(), 120.)]
    );

    configuration.write().panels.swap(0, 1);
    test.sync_and_update();
    assert_eq!(
        snapshot(&test),
        vec![("B:0".into(), 120.), ("A:1".into(), 180.)]
    );
    assert_eq!(sizes(controller), vec![120., 180.]);

    test.press_cursor((122., 50.));
    test.move_cursor((142., 50.));
    test.release_cursor((142., 50.));
    let panels = snapshot(&test);
    assert!(panels[0].1 > 120.);
    assert!(panels[1].1 < 180.);
    assert_eq!(panels[1].0, "A:1");
}

#[test]
fn hide_show_uses_declaration_order_and_resets_removed_panels() {
    let (mut test, mut configuration, mut controller) = launch(panels(&["A", "B"]));
    test.sync_and_update();
    test.click_cursor((50., 50.));
    controller.write().apply_resize(1, 30., 500.);
    test.sync_and_update();

    configuration.write().panels = panels(&["B"]);
    test.sync_and_update();
    assert_eq!(snapshot(&test), vec![("B:0".into(), 120.)]);

    configuration.write().panels = panels(&["A", "B"]);
    test.sync_and_update();
    assert_eq!(
        snapshot(&test),
        vec![("A:0".into(), 150.), ("B:0".into(), 120.)]
    );

    test.press_cursor((152., 50.));
    test.move_cursor((172., 50.));
    test.release_cursor((172., 50.));
    let panels = snapshot(&test);
    assert!(panels[0].1 > 150.);
    assert!(panels[1].1 < 120.);
}

#[test]
fn middle_insertion_and_removal_preserve_surviving_panels() {
    let (mut test, mut configuration, mut controller) = launch(panels(&["A", "C"]));
    test.sync_and_update();
    test.click_cursor((200., 50.));
    controller.write().apply_resize(1, 20., 500.);
    test.sync_and_update();

    configuration.write().panels = panels(&["A", "B", "C"]);
    test.sync_and_update();
    assert_eq!(
        snapshot(&test),
        vec![
            ("A:0".into(), 170.),
            ("B:0".into(), 150.),
            ("C:1".into(), 130.),
        ]
    );

    configuration.write().panels = panels(&["A", "C"]);
    test.sync_and_update();
    assert_eq!(
        snapshot(&test),
        vec![("A:0".into(), 170.), ("C:1".into(), 130.)]
    );
}

#[test]
fn structural_changes_cancel_an_active_drag() {
    for (names, direction) in [
        (vec!["B", "A"], Direction::Horizontal),
        (vec!["A", "C", "B"], Direction::Horizontal),
        (vec!["B"], Direction::Horizontal),
        (vec!["A", "B"], Direction::Vertical),
    ] {
        let (mut test, mut configuration, controller) = launch(panels(&["A", "B"]));
        test.sync_and_update();
        test.press_cursor((152., 50.));

        configuration.set(Configuration {
            panels: panels(&names),
            direction,
        });
        test.sync_and_update();
        test.move_cursor((200., 200.));
        test.release_cursor((200., 200.));
        assert_eq!(sizes(controller), vec![150.; names.len()]);
    }
}

#[test]
fn only_the_active_divider_resizes_until_pointer_up() {
    let (mut test, _, controller) = launch(panels(&["A", "B", "C"]));
    test.sync_and_update();
    test.press_cursor((152., 50.));
    test.move_cursor((172., 50.));
    let resized = sizes(controller);
    assert!(resized[0] > 150.);
    assert!(resized[1] < 150.);
    assert_eq!(resized[2], 150.);

    test.release_cursor((172., 50.));
    test.move_cursor((200., 50.));
    assert_eq!(sizes(controller), resized);
}

#[test]
fn empty_containers_and_stale_divider_indices_are_safe() {
    let (mut test, mut configuration, mut controller) = launch(panels(&["A", "B"]));
    test.sync_and_update();
    assert!(!controller.write().apply_resize(0, 20., 500.));
    assert!(!controller.write().apply_resize(2, 20., 500.));

    configuration.write().panels.clear();
    test.sync_and_update();
    assert!(snapshot(&test).is_empty());
    assert!(controller.peek().panels().is_empty());
    assert!(!controller.write().apply_resize(1, 20., 500.));

    configuration.write().panels = panels(&["A"]);
    test.sync_and_update();
    assert_eq!(snapshot(&test), vec![("A:0".into(), 150.)]);
}

#[test]
fn percentage_reorders_do_not_redistribute_sizes() {
    let (mut test, mut configuration, mut controller) = launch(vec![
        panel("A", PanelSize::percent(50.)),
        panel("B", PanelSize::percent(50.)),
    ]);
    test.sync_and_update();
    assert!(controller.write().apply_resize(1, 50., 500.));
    test.sync_and_update();
    let previous = sizes(controller);

    configuration.write().panels.swap(0, 1);
    test.sync_and_update();
    assert_eq!(sizes(controller), vec![previous[1], previous[0]]);
}

#[test]
fn percentage_membership_changes_redistribute_centrally() {
    let (mut test, mut configuration, mut controller) = launch(vec![
        panel("A", PanelSize::percent(50.)),
        panel("B", PanelSize::percent(50.)),
    ]);
    test.sync_and_update();
    controller.write().apply_resize(1, 50., 500.);
    test.sync_and_update();

    let left = configuration.write().panels.remove(0);
    test.sync_and_update();
    assert_eq!(sizes(controller), vec![50.]);

    configuration.write().panels.insert(0, left);
    test.sync_and_update();
    assert_eq!(sizes(controller), vec![50., 50.]);
}

#[test]
fn changed_constraints_and_sizing_modes_follow_declarations() {
    let (mut test, mut configuration, mut controller) = launch(panels(&["A", "B"]));
    test.sync_and_update();
    controller.write().apply_resize(1, 30., 500.);
    test.sync_and_update();

    configuration.write().panels[1] = panel("B", PanelSize::px(150.)).min_size(140.);
    test.sync_and_update();
    assert_eq!(sizes(controller), vec![180., 140.]);

    configuration.set(Configuration {
        panels: vec![
            panel("A", PanelSize::px(150.)),
            panel("B", PanelSize::percent(50.)),
        ],
        direction: Direction::Vertical,
    });
    test.sync_and_update();
    assert_eq!(sizes(controller), vec![180., 50.]);
    assert_eq!(controller.peek().direction(), Direction::Vertical);

    controller.write().reset();
    test.sync_and_update();
    assert_eq!(sizes(controller), vec![150., 50.]);
}

#[test]
fn reset_respects_updated_minimum_sizes() {
    let (mut test, mut configuration, mut controller) = launch(panels(&["A", "B"]));
    test.sync_and_update();

    configuration.write().panels[0] = panel("A", PanelSize::px(150.)).min_size(200.);
    test.sync_and_update();
    controller.write().reset();
    test.sync_and_update();

    assert_eq!(sizes(controller), vec![200., 150.]);
    assert_eq!(
        snapshot(&test),
        vec![("A:0".into(), 200.), ("B:0".into(), 150.)]
    );
}

#[test]
fn three_panel_dividers_resize_their_neighbors_in_both_directions() {
    for (divider, distance, expected) in [
        (152., 20., vec![170., 130., 150.]),
        (152., -20., vec![130., 170., 150.]),
        (306., 20., vec![150., 170., 130.]),
        (306., -20., vec![150., 130., 170.]),
    ] {
        let (mut test, _, controller) = launch(panels(&["A", "B", "C"]));
        test.sync_and_update();
        test.press_cursor((divider, 50.));
        test.move_cursor((divider + distance, 50.));
        test.release_cursor((divider + distance, 50.));
        test.sync_and_update();

        for (actual, expected) in sizes(controller).iter().zip(&expected) {
            assert!((actual - expected).abs() < 3., "{actual} != {expected}");
        }
        for ((_, actual), expected) in snapshot(&test).iter().zip(&expected) {
            assert!((actual - expected).abs() < 3., "{actual} != {expected}");
        }
    }
}

#[test]
fn leftward_resize_skips_a_neighbor_at_its_minimum() {
    let (mut test, _, mut controller) = launch(vec![
        panel("A", PanelSize::px(150.)),
        panel("B", PanelSize::px(150.)).min_size(150.),
        panel("C", PanelSize::px(150.)),
    ]);
    test.sync_and_update();

    assert!(controller.write().apply_resize(2, -20., 500.));
    test.sync_and_update();
    assert_eq!(sizes(controller), vec![130., 150., 170.]);
}

#[test]
fn snapshots_cannot_modify_membership_or_sizes() {
    let (mut test, _, controller) = launch(panels(&["A"]));
    test.sync_and_update();

    let mut snapshot = controller.peek().panels();
    snapshot[0].size = 400.;
    snapshot.clear();
    assert_eq!(sizes(controller), vec![150.]);
}

#[test]
fn unkeyed_sizes_follow_positions_when_content_moves() {
    let (mut test, mut configuration, mut controller) = launch(vec![
        ResizablePanel::new(PanelSize::px(150.)).child("Left"),
        ResizablePanel::new(PanelSize::px(150.)).child("Right"),
    ]);
    test.sync_and_update();
    controller.write().apply_resize(1, 30., 500.);
    test.sync_and_update();

    configuration.write().panels.swap(0, 1);
    test.sync_and_update();
    assert_eq!(sizes(controller), vec![180., 120.]);
}