tuika 0.11.0

The application framework for Rust terminal UIs — flexbox layout, overlays, focus, keymap, components, and safe ratatui interoperability.
Documentation
use tuika::components::{
    Column, Scrollbar, SelectList, SelectState, SelectViewportState, SelectionAnchor, Table,
    VirtualWindow,
};
use tuika::prelude::{Line, Style, Theme};
use tuika::testing::{grid, render};

#[test]
fn virtual_window_clamps_and_centers_without_overflowing() {
    let top = VirtualWindow::new(100, 10, usize::MAX);
    assert_eq!(top.range(), 90..100);
    assert_eq!(top.max_start(), 90);
    assert_eq!(VirtualWindow::max_start_for(100, 10), 90);
    assert!(top.overflows());

    let centered = VirtualWindow::around(100, 9, Some(50));
    assert_eq!(centered.range(), 46..55);
    assert!(centered.contains(50));

    let short = VirtualWindow::around(3, 10, Some(usize::MAX));
    assert_eq!(short.range(), 0..3);
    assert!(!short.overflows());
}

#[test]
fn one_scrollbar_component_handles_both_axes_and_large_collections() {
    let window = VirtualWindow::new(usize::MAX, 10, usize::MAX);
    let vertical = Scrollbar::vertical(window);
    let vertical_grid = grid(&render(&vertical, 1, 10, &Theme::default()));
    assert!(vertical_grid.lines().last().is_some_and(|line| line == ""));

    let horizontal = Scrollbar::horizontal(VirtualWindow::new(100, 5, 50))
        .track_glyph('.')
        .thumb_glyph('#')
        .track_style(Style::default())
        .thumb_style(Style::default());
    assert_eq!(
        grid(&render(&horizontal, 10, 1, &Theme::default())),
        "....#....."
    );
}

#[test]
fn built_in_collections_accept_only_the_visible_data_window() {
    let mut state = SelectState::new();
    state.select(Some(502));
    let window = VirtualWindow::around(1_000, 5, state.selected());
    let items = window
        .range()
        .map(|index| Line::from(format!("item {index}")))
        .collect();
    let list = SelectList::windowed(items, window, &state);
    let list_grid = grid(&render(&list, 14, 5, &Theme::default()));
    assert!(list_grid.contains("item 502"));
    assert!(list_grid.lines().any(|line| line.ends_with(['', ''])));

    let rows = window
        .range()
        .map(|index| vec![Line::from(index.to_string())])
        .collect();
    let table = Table::windowed(vec![Column::flex("id", 1)], rows, window, &state);
    let table_grid = grid(&render(&table, 10, 6, &Theme::default()));
    assert!(table_grid.contains("502"));
    assert!(
        table_grid
            .lines()
            .skip(1)
            .any(|line| line.ends_with(['', '']))
    );
}

/// Both windowing policies are reachable from a stateless host: 20 rows in a
/// 5-row pane, with the selection past the first window.
#[test]
fn selection_anchor_picks_centering_or_edge_following() {
    let rows: Vec<Vec<Line>> = (0..20).map(|i| vec![Line::from(format!("r{i}"))]).collect();
    let mut state = SelectState::new();
    state.select(Some(7));
    let table = |anchor: SelectionAnchor| {
        let table = Table::new(vec![Column::auto("n")], rows.clone(), &state)
            .viewport(5)
            .header(false)
            .selection_anchor(anchor);
        grid(&render(&table, 8, 5, &Theme::default()))
    };

    // Edge trails the selection by one row: rows 3..8.
    let edge = table(SelectionAnchor::Edge);
    assert!(edge.contains("r3") && edge.contains("r7"), "{edge}");
    assert!(!edge.contains("r9"), "{edge}");

    // Center puts it mid-window: rows 5..10.
    let centered = table(SelectionAnchor::Center);
    assert!(
        centered.contains("r5") && centered.contains("r9"),
        "{centered}"
    );
    assert!(!centered.contains("r3"), "{centered}");

    // The same policy as a bare window, against the persistent path's math.
    assert_eq!(VirtualWindow::keeping(20, 5, 0, Some(7)).range(), 3..8);
    let mut viewport = SelectViewportState::new();
    viewport.select(Some(7));
    assert_eq!(viewport.resolve(20, 5).range(), 3..8);
}