cranpose-liquid 0.9.39

Glass controls, themes and motion for Cranpose apps
Documentation
use std::{cell::Cell, rc::Rc};

use cranpose_core::{MutableState, rememberMutableStateOf};
use cranpose_foundation::text::TextFieldState;
use cranpose_liquid::prelude::*;
use cranpose_testing::{create_headless_robot_test, placed_semantics_from_shell};
use cranpose_ui::{Catalog, Column, ColumnSpec, Locale, Modifier, ProvideLocalization, tr};

#[test]
fn changing_language_updates_tab_search_and_action_accessibility_labels() {
    let state_capture = Rc::new(Cell::new(None::<MutableState<Locale>>));
    let capture = state_capture.clone();
    let catalog = Catalog::from_resources(
        "en",
        &[cranpose_ui::localization::Resource {
            locale: "fr",
            namespace: "cranpose-liquid",
            source: "home = Accueil\nsave = Enregistrer",
        }],
    )
    .expect("catalog");
    let mut robot = create_headless_robot_test(440, 400, move || {
        let language = rememberMutableStateOf(|| Locale::parse("en").expect("locale"));
        capture.set(Some(language));
        ProvideLocalization(&catalog, language.value(), || {
            LiquidTheme(LiquidThemeSpec::default(), || {
                Column(
                    Modifier::empty().fill_max_size(),
                    ColumnSpec::default(),
                    || {
                        LiquidSearchField(
                            Modifier::empty().width(320.0),
                            cranpose_core::remember(|| TextFieldState::new(""))
                                .with(|state| *state),
                            LiquidSearchFieldSpec::default(),
                        );
                        GlassButton(
                            Modifier::empty(),
                            GlassButtonSpec::glass(),
                            || {},
                            || {
                                GlassButtonLabel(
                                    tr!("Save", id = "save"),
                                    GlassButtonSpec::glass(),
                                );
                            },
                        );
                        LiquidTabBar(
                            Modifier::empty().width(320.0),
                            LiquidTabBarSpec::default(),
                            0,
                            |_| {},
                            |scope| {
                                scope.tab(cranpose_liquid::icons::SEARCH, tr!("Home", id = "home"));
                            },
                        );
                    },
                );
            });
        });
    });
    robot.shell_mut().set_semantics_enabled(true);
    for (language, expected) in [
        ("en", ["Home", "Save", "Search"]),
        ("fr", ["Accueil", "Enregistrer", "Rechercher"]),
    ] {
        state_capture
            .get()
            .expect("language state")
            .set_value(Locale::parse(language).expect("locale"));
        robot.wait_for_idle();
        let tree = placed_semantics_from_shell(robot.shell_mut()).expect("placed localized UI");
        let nodes = tree.flatten();
        for label in expected {
            assert!(
                nodes
                    .iter()
                    .any(|node| node.label.as_deref() == Some(label)),
                "missing accessible label {label}"
            );
        }
    }
}

#[test]
fn rtl_tabs_remain_visible_and_select_the_logical_index() {
    let selected = Rc::new(Cell::new(usize::MAX));
    let selection = selected.clone();
    let mut robot = create_headless_robot_test(400, 120, move || {
        let selection = selection.clone();
        cranpose_ui::ProvideLayoutDirection(cranpose_ui::LayoutDirection::Rtl, || {
            LiquidTheme(LiquidThemeSpec::default(), move || {
                LiquidTabBar(
                    Modifier::empty().width(390.0),
                    LiquidTabBarSpec::default(),
                    2,
                    move |index| selection.set(index),
                    |tabs| {
                        tabs.tab(cranpose_liquid::icons::BOOKMARK, "Library");
                        tabs.tab(cranpose_liquid::icons::SEARCH, "Scan");
                        tabs.tab(cranpose_liquid::icons::STAR, "Settings");
                    },
                );
            });
        });
    });
    robot.wait_for_idle();
    let mut previous = 400.0;
    for (index, label) in ["Library", "Scan", "Settings"].into_iter().enumerate() {
        let bounds = robot.find_by_text(label).bounds().expect("tab bounds");
        assert!(
            bounds.x >= 0.0 && bounds.x + bounds.width <= 400.0,
            "{label}: {bounds:?}"
        );
        assert!(bounds.x < previous, "tabs follow right-to-left order");
        previous = bounds.x;
        robot.find_by_text(label).click();
        assert_eq!(selected.get(), index, "{label} selects its logical index");
    }
}