ferroui-view 0.4.0-preview.1

A declarative Rust API for FerroUI user interfaces without XAML: element builders generated for every control, an rsx! macro, signals that properties follow, keyed dynamic children
Documentation
use crate::generated::builders::*;
use crate::reactive::{signal, Owner};
use crate::{Element, GridDefinitions, IntoElement};
use ferroui_base::interactivity::RoutedEventArgs;
use ferroui_base::layout::HorizontalAlignment;
use ferroui_base::{BoxedValue, Ref, Thickness};
use ferroui_controls::{Button, Control, Grid, StackPanel, TextBlock, TextBox};
use std::cell::Cell;
use std::rc::Rc;

fn mount(element: impl IntoElement) -> (Ref<Control>, Owner) {
    element.into_element().mount_root()
}

fn content_text(button: &Button) -> Option<String> {
    button.content().and_then(|content: BoxedValue| content.downcast_ref::<String>().cloned())
}

#[test]
fn setters_give_the_properties_their_values_with_the_conversions() {
    let (control, owner) = mount(
        text_block()
            .text("Hello")
            .width(120)
            .margin(4.0)
            .padding((2.0, 6.0))
            .horizontal_alignment(HorizontalAlignment::Center)
            .name("greeting")
            .class("title large"),
    );
    let text = control.cast::<TextBlock>().expect("a text block");
    assert_eq!(text.text().as_deref(), Some("Hello"));
    assert_eq!(text.width(), 120.0);
    assert_eq!(text.margin(), Thickness::uniform(4.0));
    assert_eq!(text.padding(), Thickness::symmetric(2.0, 6.0));
    assert_eq!(text.horizontal_alignment(), HorizontalAlignment::Center);
    assert_eq!(text.name().as_deref(), Some("greeting"));
    assert!(text.classes().contains("title") && text.classes().contains("large"));
    owner.dispose();
}

#[test]
fn set_takes_any_property_like_the_indexer_upstream() {
    let (control, owner) = mount(button().set(Grid::row_property(), 2).grid_column(1));
    assert_eq!(Grid::get_row(&control), 2);
    assert_eq!(Grid::get_column(&control), 1);
    owner.dispose();
}

#[test]
fn text_children_go_into_the_content_and_controls_into_a_panel() {
    let (control, owner) = mount(
        stack_panel()
            .spacing(8.0)
            .child(button().child("First"))
            .children((text_block().text("Second"), button().content("Third"))),
    );
    let panel = control.cast::<StackPanel>().expect("a panel");
    let children: Vec<Ref<Control>> = panel.children().to_vec().into_iter().collect();
    assert_eq!(children.len(), 3);
    assert_eq!(content_text(&children[0].cast::<Button>().unwrap()).as_deref(), Some("First"));
    assert_eq!(children[1].cast::<TextBlock>().unwrap().text().as_deref(), Some("Second"));
    assert_eq!(content_text(&children[2].cast::<Button>().unwrap()).as_deref(), Some("Third"));
    owner.dispose();
}

#[test]
fn an_element_given_to_an_object_property_becomes_its_control() {
    let (control, owner) = mount(button().content(text_block().text("Inside")));
    let button = control.cast::<Button>().unwrap();
    let content = button.content().and_then(|content| Control::from_boxed(&content)).expect("a control");
    assert_eq!(content.cast::<TextBlock>().unwrap().text().as_deref(), Some("Inside"));
    owner.dispose();
}

#[test]
fn a_property_follows_a_signal_and_a_closure() {
    let owner = Owner::new_root();
    let (count, control) = owner.with(|| {
        let count = signal(1);
        let control = stack_panel()
            .child(text_block().text(count))
            .child(text_block().text(move || format!("Count: {}", count.get())))
            .into_element()
            .mount();
        (count, control)
    });
    let texts = || -> Vec<String> {
        control.cast::<StackPanel>().unwrap().children().to_vec().into_iter().map(|c| c.cast::<TextBlock>().unwrap().text().unwrap_or_default()).collect()
    };
    assert_eq!(texts(), vec!["1", "Count: 1"]);
    count.set(7);
    assert_eq!(texts(), vec!["7", "Count: 7"]);
    owner.dispose();
}

#[test]
fn a_two_way_signal_follows_the_property_and_the_property_the_signal() {
    let owner = Owner::new_root();
    let (draft, control) = owner.with(|| {
        let draft = signal(String::from("start"));
        let control = text_box().text(draft.two_way()).into_element().mount();
        (draft, control)
    });
    let text_box = control.cast::<TextBox>().unwrap();
    assert_eq!(text_box.text().as_deref(), Some("start"));
    text_box.set_text(Some("typed"));
    assert_eq!(draft.get_untracked(), "typed");
    draft.set(String::from("from code"));
    assert_eq!(text_box.text().as_deref(), Some("from code"));
    owner.dispose();
}

#[test]
fn a_handler_of_a_routed_event_is_called() {
    let clicks = Rc::new(Cell::new(0));
    let counted = clicks.clone();
    let (control, owner) = mount(button().on_click(move |_| counted.set(counted.get() + 1)));
    control.raise_event(&RoutedEventArgs::with_event(Button::click_event()));
    control.raise_event(&RoutedEventArgs::with_event(Button::click_event()));
    assert_eq!(clicks.get(), 2);
    owner.dispose();
}

#[test]
fn on_changed_reports_the_new_value_of_a_property() {
    let seen = Rc::new(std::cell::RefCell::new(Vec::new()));
    let log = seen.clone();
    let (control, owner) = mount(slider().minimum(0.0).maximum(10.0).on_changed(
        ferroui_controls::primitives::RangeBase::value_property(),
        move |value| log.borrow_mut().push(*value),
    ));
    control.set_value(ferroui_controls::primitives::RangeBase::value_property(), 4.0);
    assert_eq!(*seen.borrow(), vec![4.0]);
    owner.dispose();
}

#[test]
fn grid_definitions_and_attached_properties() {
    let (control, owner) = mount(
        grid()
            .row_definitions("Auto,*")
            .column_definitions("*,Auto")
            .child(text_box().grid_row(0).grid_column(0))
            .child(button().grid_row(0).grid_column(1).child("Add")),
    );
    let grid = control.cast::<Grid>().unwrap();
    assert_eq!(grid.row_definitions().count(), 2);
    assert_eq!(grid.column_definitions().count(), 2);
    let add = grid.children().to_vec().into_iter().nth(1).unwrap();
    assert_eq!(Grid::get_column(&add), 1);
    owner.dispose();
}

#[test]
fn an_element_is_data_that_compares_by_value() {
    let a: Element = button().width(10.0).child("x").into_element();
    let b: Element = button().width(10.0).child("x").into_element();
    let (Element::Control(a), Element::Control(b)) = (a, b) else { panic!("control elements") };
    assert_eq!(a.class, b.class);
    assert_eq!(a.props, b.props);
    let c = button().width(11.0).into_element();
    let Element::Control(c) = c else { panic!() };
    assert_ne!(a.props, c.props);
}