ferroui-controls 0.3.0

The controls of FerroUI: the application model, top levels, the control set and automation peers
Documentation
use crate::test_support::TestRoot;
use crate::{Border, Control, ControlImpl, Panel, StackPanel};
use ferroui_base::input::InputElementImpl;
use ferroui_base::interactivity::InteractiveImpl;
use ferroui_base::layout::{HorizontalAlignment, LayoutableImpl, Orientation, VerticalAlignment};
use ferroui_base::styling::{Selectors, Setter, Style};
use ferroui_base::{
    ferro_class, ferro_impl_classes, instantiate, FerroObjectImpl, Rect, Ref, Size, StyledElementImpl, Visual,
    VisualImpl,
};
use std::cell::Cell;

fn border(width: Option<f64>, height: Option<f64>) -> Ref<Border> {
    let border = Border::new();
    if let Some(width) = width {
        border.set_width(width);
    }
    if let Some(height) = height {
        border.set_height(height);
    }
    border
}

fn stack_panel(width: f64, height: f64, is_visible: bool) -> Ref<StackPanel> {
    let panel = StackPanel::new();
    panel.set_width(width);
    panel.set_height(height);
    if !is_visible {
        panel.set_is_visible(false);
    }
    panel
}

#[repr(C)]
struct TestControl {
    base: Control,
    measure_constraint: Cell<Size>,
    measure_size: Cell<Size>,
}

ferro_class!(TestControl: Control);
ferro_impl_classes!(
    TestControl: FerroObjectImpl,
    StyledElementImpl,
    VisualImpl,
    InteractiveImpl,
    InputElementImpl,
    ControlImpl
);

impl LayoutableImpl for TestControl {
    fn measure_override(this: &Self, available_size: Size) -> Size {
        this.measure_constraint.set(available_size);
        this.measure_size.get()
    }
}

impl TestControl {
    fn new(measure_size: Size) -> Ref<Self> {
        instantiate(Self {
            base: Control::construct(),
            measure_constraint: Cell::new(Size::default()),
            measure_size: Cell::new(measure_size),
        })
    }

    fn horizontal(alignment: HorizontalAlignment, measure_size: Size) -> Ref<Self> {
        let control = Self::new(measure_size);
        control.set_horizontal_alignment(alignment);
        control
    }

    fn vertical(alignment: VerticalAlignment, measure_size: Size) -> Ref<Self> {
        let control = Self::new(measure_size);
        control.set_vertical_alignment(alignment);
        control
    }
}

#[test]
fn lays_out_children_vertically() {
    let target = StackPanel::new();
    target.children().add(border(Some(120.0), Some(20.0)));
    target.children().add(border(None, Some(30.0)));
    target.children().add(border(None, Some(50.0)));

    target.measure(Size::INFINITY);
    target.arrange(Rect::from_size(target.desired_size()));

    assert_eq!(target.bounds().size(), Size::new(120.0, 100.0));
    assert_eq!(target.children().get(0).bounds(), Rect::new(0.0, 0.0, 120.0, 20.0));
    assert_eq!(target.children().get(1).bounds(), Rect::new(0.0, 20.0, 120.0, 30.0));
    assert_eq!(target.children().get(2).bounds(), Rect::new(0.0, 50.0, 120.0, 50.0));
}

#[test]
fn lays_out_children_horizontally() {
    let target = StackPanel::new();
    target.set_orientation(Orientation::Horizontal);
    target.children().add(border(Some(20.0), Some(120.0)));
    target.children().add(border(Some(30.0), None));
    target.children().add(border(Some(50.0), None));

    target.measure(Size::INFINITY);
    target.arrange(Rect::from_size(target.desired_size()));

    assert_eq!(target.bounds().size(), Size::new(100.0, 120.0));
    assert_eq!(target.children().get(0).bounds(), Rect::new(0.0, 0.0, 20.0, 120.0));
    assert_eq!(target.children().get(1).bounds(), Rect::new(20.0, 0.0, 30.0, 120.0));
    assert_eq!(target.children().get(2).bounds(), Rect::new(50.0, 0.0, 50.0, 120.0));
}

#[test]
fn lays_out_children_vertically_with_spacing() {
    let target = StackPanel::new();
    target.set_spacing(10.0);
    target.children().add(border(Some(120.0), Some(20.0)));
    target.children().add(border(None, Some(30.0)));
    target.children().add(border(None, Some(50.0)));

    target.measure(Size::INFINITY);
    target.arrange(Rect::from_size(target.desired_size()));

    assert_eq!(target.bounds().size(), Size::new(120.0, 120.0));
    assert_eq!(target.children().get(0).bounds(), Rect::new(0.0, 0.0, 120.0, 20.0));
    assert_eq!(target.children().get(1).bounds(), Rect::new(0.0, 30.0, 120.0, 30.0));
    assert_eq!(target.children().get(2).bounds(), Rect::new(0.0, 70.0, 120.0, 50.0));
}

#[test]
fn lays_out_children_horizontally_with_spacing() {
    let target = StackPanel::new();
    target.set_spacing(10.0);
    target.set_orientation(Orientation::Horizontal);
    target.children().add(border(Some(20.0), Some(120.0)));
    target.children().add(border(Some(30.0), None));
    target.children().add(border(Some(50.0), None));

    target.measure(Size::INFINITY);
    target.arrange(Rect::from_size(target.desired_size()));

    assert_eq!(target.bounds().size(), Size::new(120.0, 120.0));
    assert_eq!(target.children().get(0).bounds(), Rect::new(0.0, 0.0, 20.0, 120.0));
    assert_eq!(target.children().get(1).bounds(), Rect::new(30.0, 0.0, 30.0, 120.0));
    assert_eq!(target.children().get(2).bounds(), Rect::new(70.0, 0.0, 50.0, 120.0));
}

#[test]
fn lays_out_children_vertically_even_if_larger_than_panel() {
    let target = StackPanel::new();
    target.set_height(60.0);
    target.children().add(border(Some(120.0), Some(20.0)));
    target.children().add(border(None, Some(30.0)));
    target.children().add(border(None, Some(50.0)));

    target.measure(Size::INFINITY);
    target.arrange(Rect::from_size(target.desired_size()));

    assert_eq!(target.bounds().size(), Size::new(120.0, 60.0));
    assert_eq!(target.children().get(0).bounds(), Rect::new(0.0, 0.0, 120.0, 20.0));
    assert_eq!(target.children().get(1).bounds(), Rect::new(0.0, 20.0, 120.0, 30.0));
    assert_eq!(target.children().get(2).bounds(), Rect::new(0.0, 50.0, 120.0, 50.0));
}

#[test]
fn lays_out_children_horizontally_even_if_larger_than_panel() {
    let target = StackPanel::new();
    target.set_width(60.0);
    target.set_orientation(Orientation::Horizontal);
    target.children().add(border(Some(20.0), Some(120.0)));
    target.children().add(border(Some(30.0), None));
    target.children().add(border(Some(50.0), None));

    target.measure(Size::INFINITY);
    target.arrange(Rect::from_size(target.desired_size()));

    assert_eq!(target.bounds().size(), Size::new(60.0, 120.0));
    assert_eq!(target.children().get(0).bounds(), Rect::new(0.0, 0.0, 20.0, 120.0));
    assert_eq!(target.children().get(1).bounds(), Rect::new(20.0, 0.0, 30.0, 120.0));
    assert_eq!(target.children().get(2).bounds(), Rect::new(50.0, 0.0, 50.0, 120.0));
}

#[test]
fn arranges_vertical_children_with_correct_bounds() {
    let target = StackPanel::new();
    target.set_orientation(Orientation::Vertical);
    for (alignment, width) in [
        (HorizontalAlignment::Left, 50.0),
        (HorizontalAlignment::Left, 150.0),
        (HorizontalAlignment::Center, 50.0),
        (HorizontalAlignment::Center, 150.0),
        (HorizontalAlignment::Right, 50.0),
        (HorizontalAlignment::Right, 150.0),
        (HorizontalAlignment::Stretch, 50.0),
        (HorizontalAlignment::Stretch, 150.0),
    ] {
        target.children().add(TestControl::horizontal(alignment, Size::new(width, 10.0)));
    }

    target.measure(Size::new(100.0, 150.0));
    assert_eq!(target.desired_size(), Size::new(100.0, 80.0));

    target.arrange(Rect::from_size(target.desired_size()));

    let bounds: Vec<Rect> = target.children().to_vec().iter().map(|x| x.bounds()).collect();

    assert_eq!(
        bounds,
        vec![
            Rect::new(0.0, 0.0, 50.0, 10.0),
            Rect::new(0.0, 10.0, 100.0, 10.0),
            Rect::new(25.0, 20.0, 50.0, 10.0),
            Rect::new(0.0, 30.0, 100.0, 10.0),
            Rect::new(50.0, 40.0, 50.0, 10.0),
            Rect::new(0.0, 50.0, 100.0, 10.0),
            Rect::new(0.0, 60.0, 100.0, 10.0),
            Rect::new(0.0, 70.0, 100.0, 10.0),
        ]
    );
}

#[test]
fn arranges_horizontal_children_with_correct_bounds() {
    let target = StackPanel::new();
    target.set_orientation(Orientation::Horizontal);
    for (alignment, height) in [
        (VerticalAlignment::Top, 50.0),
        (VerticalAlignment::Top, 150.0),
        (VerticalAlignment::Center, 50.0),
        (VerticalAlignment::Center, 150.0),
        (VerticalAlignment::Bottom, 50.0),
        (VerticalAlignment::Bottom, 150.0),
        (VerticalAlignment::Stretch, 50.0),
        (VerticalAlignment::Stretch, 150.0),
    ] {
        target.children().add(TestControl::vertical(alignment, Size::new(10.0, height)));
    }

    target.measure(Size::new(150.0, 100.0));
    assert_eq!(target.desired_size(), Size::new(80.0, 100.0));

    target.arrange(Rect::from_size(target.desired_size()));

    let bounds: Vec<Rect> = target.children().to_vec().iter().map(|x| x.bounds()).collect();

    assert_eq!(
        bounds,
        vec![
            Rect::new(0.0, 0.0, 10.0, 50.0),
            Rect::new(10.0, 0.0, 10.0, 100.0),
            Rect::new(20.0, 25.0, 10.0, 50.0),
            Rect::new(30.0, 0.0, 10.0, 100.0),
            Rect::new(40.0, 50.0, 10.0, 50.0),
            Rect::new(50.0, 0.0, 10.0, 100.0),
            Rect::new(60.0, 0.0, 10.0, 100.0),
            Rect::new(70.0, 0.0, 10.0, 100.0),
        ]
    );
}

#[test]
fn spacing_not_added_for_invisible_children() {
    for orientation in [Orientation::Horizontal, Orientation::Vertical] {
        let target_three_children_one_invisible = StackPanel::new();
        target_three_children_one_invisible.set_spacing(40.0);
        target_three_children_one_invisible.set_orientation(orientation);
        target_three_children_one_invisible.children().add(stack_panel(10.0, 10.0, false));
        target_three_children_one_invisible.children().add(stack_panel(10.0, 10.0, true));
        target_three_children_one_invisible.children().add(stack_panel(10.0, 10.0, true));

        let target_two_children_none_invisible = StackPanel::new();
        target_two_children_none_invisible.set_spacing(40.0);
        target_two_children_none_invisible.set_orientation(orientation);
        target_two_children_none_invisible.children().add(stack_panel(10.0, 10.0, true));
        target_two_children_none_invisible.children().add(stack_panel(10.0, 10.0, true));

        target_three_children_one_invisible.measure(Size::INFINITY);
        target_three_children_one_invisible
            .arrange(Rect::from_size(target_three_children_one_invisible.desired_size()));

        target_two_children_none_invisible.measure(Size::INFINITY);
        target_two_children_none_invisible.arrange(Rect::from_size(target_two_children_none_invisible.desired_size()));

        let size_with_two_children = target_two_children_none_invisible.bounds().size();
        let size_with_three_children = target_three_children_one_invisible.bounds().size();

        assert_eq!(size_with_two_children, size_with_three_children);
    }
}

#[test]
fn spacing_not_added_for_children_hidden_by_style_applied_during_measure() {
    for orientation in [Orientation::Horizontal, Orientation::Vertical] {
        let target = StackPanel::new();
        target.set_spacing(40.0);
        target.set_orientation(orientation);
        let hidden = stack_panel(10.0, 10.0, true);
        hidden.classes().add("hidden");
        target.children().add(hidden);
        target.children().add(stack_panel(10.0, 10.0, true));
        target.children().add(stack_panel(10.0, 10.0, true));

        let root = TestRoot::with_child(&target);

        root.styles().add(Style::with_setters(
            Selectors::of_type::<StackPanel>().class("hidden"),
            [Setter::new(Visual::is_visible_property(), false)],
        ));

        target.measure(Size::INFINITY);

        let expected =
            if orientation == Orientation::Horizontal { Size::new(60.0, 10.0) } else { Size::new(10.0, 60.0) };

        assert_eq!(target.desired_size(), expected);
    }
}

#[test]
fn only_arrange_visible_children() {
    for orientation in [Orientation::Horizontal, Orientation::Vertical] {
        let hidden_panel = Panel::new();
        hidden_panel.set_width(10.0);
        hidden_panel.set_height(10.0);
        hidden_panel.set_is_visible(false);
        let panel = Panel::new();
        panel.set_width(10.0);
        panel.set_height(10.0);

        let target = StackPanel::new();
        target.set_spacing(40.0);
        target.set_orientation(orientation);
        target.children().add(&hidden_panel);
        target.children().add(&panel);

        target.measure(Size::INFINITY);
        target.arrange(Rect::from_size(target.desired_size()));
        assert_eq!(panel.bounds(), Rect::new(0.0, 0.0, 10.0, 10.0));
    }
}

#[test]
fn arrange_raises_snap_points_changed_for_the_orientation() {
    use std::rc::Rc;

    for orientation in [Orientation::Horizontal, Orientation::Vertical] {
        let target = StackPanel::new();
        target.set_orientation(orientation);
        target.children().add(border(Some(10.0), Some(10.0)));
        let horizontal = Rc::new(Cell::new(0));
        let vertical = Rc::new(Cell::new(0));
        let raised = horizontal.clone();
        target.horizontal_snap_points_changed(move |_, _| raised.set(raised.get() + 1));
        let raised = vertical.clone();
        target.vertical_snap_points_changed(move |_, _| raised.set(raised.get() + 1));

        target.measure(Size::INFINITY);
        target.arrange(Rect::from_size(target.desired_size()));

        let expected = if orientation == Orientation::Horizontal { (1, 0) } else { (0, 1) };
        assert_eq!((horizontal.get(), vertical.get()), expected);
    }
}

#[test]
fn snap_points_are_computed_from_child_bounds() {
    use crate::primitives::SnapPointsAlignment;

    let target = StackPanel::new();
    target.set_spacing(5.0);
    target.children().add(border(Some(120.0), Some(20.0)));
    target.children().add(border(None, Some(30.0)));

    target.measure(Size::INFINITY);
    target.arrange(Rect::from_size(target.desired_size()));

    assert_eq!(target.get_irregular_snap_points(Orientation::Vertical, SnapPointsAlignment::Near), vec![0.0, 25.0]);
    assert_eq!(target.get_irregular_snap_points(Orientation::Vertical, SnapPointsAlignment::Center), vec![10.0, 40.0]);
    assert_eq!(target.get_irregular_snap_points(Orientation::Vertical, SnapPointsAlignment::Far), vec![20.0, 55.0]);
    assert!(target.get_irregular_snap_points(Orientation::Horizontal, SnapPointsAlignment::Near).is_empty());

    target.set_are_vertical_snap_points_regular(true);

    assert_eq!(target.get_regular_snap_points(Orientation::Vertical, SnapPointsAlignment::Far), (25.0, 20.0));
}