cranpose-ui 0.9.21

Composable widgets, text, layouts and input for Cranpose apps
Documentation
use cranpose_foundation::lazy::{LazyItems, LazyListScope, rememberLazyListState};
use cranpose_ui::{
    Box, BoxSpec, Column, ColumnSpec, LayoutBox, LayoutDirection, Modifier, ProvideLayoutDirection,
    Row, RowSpec, Size,
};

use crate::text_measured_layout_integration::layout_composition;

fn tagged<'a>(node: &'a LayoutBox, tag: &str) -> &'a LayoutBox {
    fn find<'a>(node: &'a LayoutBox, tag: &str) -> Option<&'a LayoutBox> {
        if node
            .node_data
            .semantics()
            .and_then(|config| config.content_description.as_deref())
            == Some(tag)
        {
            return Some(node);
        }
        node.children.iter().find_map(|node| find(node, tag))
    }
    find(node, tag).expect("tagged box")
}

#[test]
fn rtl_rows_columns_boxes_and_flow_rows_resolve_start_from_the_right() {
    let mut composition = cranpose_ui::run_test_composition(|| {
        ProvideLayoutDirection(LayoutDirection::Rtl, || {
            Column(
                Modifier::empty().width(200.0),
                ColumnSpec::default(),
                || {
                    Row(
                        Modifier::empty().fill_max_width(),
                        RowSpec::default(),
                        || {
                            Box(
                                Modifier::empty()
                                    .size_points(20.0, 10.0)
                                    .content_description("first"),
                                BoxSpec::default(),
                                || {},
                            );
                            Box(
                                Modifier::empty()
                                    .size_points(30.0, 10.0)
                                    .content_description("second"),
                                BoxSpec::default(),
                                || {},
                            );
                        },
                    );
                    Box(
                        Modifier::empty()
                            .size_points(40.0, 10.0)
                            .content_description("column"),
                        BoxSpec::default(),
                        || {},
                    );
                    Box(
                        Modifier::empty().size_points(200.0, 20.0),
                        BoxSpec::default(),
                        || {
                            Box(
                                Modifier::empty()
                                    .size_points(25.0, 10.0)
                                    .content_description("box"),
                                BoxSpec::default(),
                                || {},
                            );
                        },
                    );
                    cranpose_ui::widgets::FlowRow(
                        Modifier::empty().width(200.0),
                        cranpose_ui::widgets::FlowRowSpec::default(),
                        || {
                            Box(
                                Modifier::empty()
                                    .size_points(60.0, 10.0)
                                    .content_description("flow"),
                                BoxSpec::default(),
                                || {},
                            );
                        },
                    );
                },
            );
        });
    });
    let layout = layout_composition(&mut composition, Size::new(200.0, 300.0));
    for (tag, expected) in [
        ("first", 180.0),
        ("second", 150.0),
        ("column", 160.0),
        ("box", 175.0),
        ("flow", 140.0),
    ] {
        assert_eq!(tagged(layout.root(), tag).rect.x, expected, "{tag}");
    }
}

#[test]
fn relative_offsets_follow_the_provider_and_absolute_offsets_keep_physical_coordinates() {
    let modifier = Modifier::empty().offset(15.0, 2.0).size_points(20.0, 10.0);
    let mut composition = cranpose_ui::run_test_composition(move || {
        let modifier = modifier.clone();
        ProvideLayoutDirection(LayoutDirection::Rtl, move || {
            Box(
                Modifier::empty().size_points(200.0, 50.0),
                BoxSpec::default(),
                move || {
                    Box(
                        modifier.clone().content_description("relative"),
                        BoxSpec::default(),
                        || {},
                    );
                    Box(
                        Modifier::empty()
                            .absolute_offset(15.0, 2.0)
                            .size_points(20.0, 10.0)
                            .content_description("absolute"),
                        BoxSpec::default(),
                        || {},
                    );
                },
            );
        });
    });
    let layout = layout_composition(&mut composition, Size::new(200.0, 50.0));
    assert_eq!(tagged(layout.root(), "relative").rect.x, 165.0);
    assert_eq!(tagged(layout.root(), "absolute").rect.x, 195.0);
    assert_eq!(tagged(layout.root(), "relative").rect.y, 2.0);
}

#[test]
fn lazy_lists_update_positions_after_direction_changes_and_scroll() {
    let mut composition = cranpose_ui::run_test_composition(|| {});
    let direction = cranpose_core::MutableState::with_runtime(
        LayoutDirection::Ltr,
        composition.runtime_handle(),
    );
    let row_state = std::rc::Rc::new(std::cell::Cell::new(None));
    let captured_state = row_state.clone();
    composition
        .render(992, move || {
            DirectionalLists(direction, captured_state.clone());
        })
        .expect("list composition");
    for (mode, expected) in [
        (LayoutDirection::Ltr, [0.0, 0.0, 0.0, 20.0]),
        (LayoutDirection::Rtl, [180.0, 170.0, 180.0, 150.0]),
        (LayoutDirection::Ltr, [0.0, 0.0, 0.0, 20.0]),
        (LayoutDirection::Rtl, [180.0, 170.0, 180.0, 150.0]),
    ] {
        direction.set_value(mode);
        while composition
            .process_invalid_scopes()
            .expect("direction change")
        {}
        let layout = layout_composition(&mut composition, Size::new(200.0, 100.0));
        for (tag, expected) in [
            "vertical first",
            "vertical second",
            "horizontal first",
            "horizontal second",
        ]
        .into_iter()
        .zip(expected)
        {
            assert_eq!(
                tagged(layout.root(), tag).rect.x,
                expected,
                "{mode:?}: {tag}"
            );
        }
    }
    row_state
        .get()
        .expect("row state")
        .dispatch_scroll_delta(-10.0);
    while composition.process_invalid_scopes().expect("scroll") {}
    let layout = layout_composition(&mut composition, Size::new(200.0, 100.0));
    assert_eq!(tagged(layout.root(), "horizontal first").rect.x, 190.0);
    assert_eq!(tagged(layout.root(), "horizontal second").rect.x, 160.0);
}

#[cranpose_ui::composable]
fn DirectionalLists(
    direction: cranpose_core::MutableState<LayoutDirection>,
    captured_state: std::rc::Rc<std::cell::Cell<Option<cranpose_foundation::lazy::LazyListState>>>,
) {
    ProvideLayoutDirection(direction.value(), move || {
        Column(
            Modifier::empty().width(200.0),
            ColumnSpec::default(),
            move || {
                cranpose_ui::LazyColumn(
                    Modifier::empty().size_points(200.0, 30.0),
                    rememberLazyListState(),
                    cranpose_ui::LazyColumnSpec::default(),
                    |scope| {
                        scope.items(LazyItems::new(1), |_| {
                            Box(
                                Modifier::empty()
                                    .size_points(20.0, 10.0)
                                    .content_description("vertical first"),
                                BoxSpec::default(),
                                || {},
                            );
                            Box(
                                Modifier::empty()
                                    .size_points(30.0, 10.0)
                                    .content_description("vertical second"),
                                BoxSpec::default(),
                                || {},
                            );
                        });
                    },
                );
                let state = rememberLazyListState();
                captured_state.set(Some(state));
                cranpose_ui::LazyRow(
                    Modifier::empty().size_points(200.0, 20.0),
                    state,
                    cranpose_ui::LazyRowSpec::default(),
                    |scope| {
                        scope.items(LazyItems::new(6), |index| {
                            if index == 0 {
                                Box(
                                    Modifier::empty()
                                        .size_points(20.0, 10.0)
                                        .content_description("horizontal first"),
                                    BoxSpec::default(),
                                    || {},
                                );
                                Box(
                                    Modifier::empty()
                                        .size_points(30.0, 10.0)
                                        .content_description("horizontal second"),
                                    BoxSpec::default(),
                                    || {},
                                );
                            } else {
                                Box(
                                    Modifier::empty().size_points(50.0, 10.0),
                                    BoxSpec::default(),
                                    || {},
                                );
                            }
                        });
                    },
                );
            },
        );
    });
}