textual 1.0.0-dev

A reactive TUI framework inspired by the Python Textual library
Documentation
use rich_rs::Console;
use textual::prelude::*;
use textual::render::FrameBuffer;
use textual::widget_tree::WidgetTree;

fn render_once(root: &mut dyn Widget, width: usize, height: usize) -> FrameBuffer {
    let sheet = textual::css::default_widget_stylesheet();
    let _guard = textual::css::set_style_context(sheet);
    let console = Console::new();
    let mut tree = build_widget_tree_from_root(root).expect("tree should build");
    render_tree_to_frame(&mut tree, root, &console, width, height)
}

fn render_with_tree(
    tree: &mut WidgetTree,
    root: &mut dyn Widget,
    console: &Console,
    width: usize,
    height: usize,
) -> FrameBuffer {
    let sheet = textual::css::default_widget_stylesheet();
    let _guard = textual::css::set_style_context(sheet);
    render_tree_to_frame(tree, root, console, width, height)
}

#[test]
fn vertical_alias_stacks_children() {
    let mut vertical = Vertical::new()
        .with_child(Label::new("alpha"))
        .with_child(Label::new("beta"));
    let buf = render_once(&mut vertical, 8, 2);
    let lines = buf.as_plain_lines();
    assert!(lines[0].starts_with("alpha"), "first line: {:?}", lines[0]);
    assert!(lines[1].starts_with("beta"), "second line: {:?}", lines[1]);
}

#[test]
fn vertical_group_alias_stacks_children() {
    let mut vertical = VerticalGroup::new()
        .with_child(Label::new("alpha"))
        .with_child(Label::new("beta"));
    let buf = render_once(&mut vertical, 8, 2);
    let lines = buf.as_plain_lines();
    assert!(lines[0].starts_with("alpha"), "first line: {:?}", lines[0]);
    assert!(lines[1].starts_with("beta"), "second line: {:?}", lines[1]);
}

#[test]
fn center_alias_aligns_children_horizontally() {
    let mut center = Center::new().with_child(Label::new("cat").with_shrink(true));
    let buf = render_once(&mut center, 9, 1);
    let lines = buf.as_plain_lines();
    assert_eq!(lines[0], "   cat   ");
}

#[test]
fn horizontal_group_alias_places_children_in_row() {
    let mut row = HorizontalGroup::new()
        .with_child(Label::new("a"))
        .with_child(Label::new("b"));
    let buf = render_once(&mut row, 8, 1);
    let line = &buf.as_plain_lines()[0];
    assert!(line.contains("a"));
    assert!(line.contains("b"));
}

#[test]
fn right_alias_aligns_children_horizontally() {
    let mut right = Right::new().with_child(Label::new("dog").with_shrink(true));
    let buf = render_once(&mut right, 8, 1);
    let lines = buf.as_plain_lines();
    assert_eq!(lines[0], "     dog");
}

#[test]
fn middle_alias_centers_children_vertically() {
    let mut middle = Middle::new().with_child(Label::new("x"));
    let buf = render_once(&mut middle, 5, 5);
    let lines = buf.as_plain_lines();
    assert_eq!(lines[0], "     ");
    assert_eq!(lines[1], "     ");
    assert_eq!(lines[2], "x    ");
    assert_eq!(lines[3], "     ");
    assert_eq!(lines[4], "     ");
}

#[test]
fn center_middle_centers_both_axes() {
    let mut center_middle = CenterMiddle::new().with_child(Label::new("ok").with_shrink(true));
    let buf = render_once(&mut center_middle, 7, 5);
    let lines = buf.as_plain_lines();
    assert_eq!(lines[2], "  ok   ");
}

#[test]
fn scrollview_is_focusable_and_supports_home_end_actions() {
    let sheet = textual::css::default_widget_stylesheet();
    let _guard = textual::css::set_style_context(sheet);
    let console = Console::new();
    let mut scroll = ScrollView::new(ListView::new(vec![
        "item 1".to_string(),
        "item 2".to_string(),
        "item 3".to_string(),
        "item 4".to_string(),
    ]))
    .height(2);
    assert!(scroll.focusable());
    let mut tree = build_widget_tree_from_root(&mut scroll).expect("tree should build");
    let _ = render_with_tree(&mut tree, &mut scroll, &console, 8, 2);

    let mut ctx = EventCtx::default();
    scroll.on_event(&Event::Action(Action::ScrollEnd), &mut ctx);
    assert!(ctx.handled());
    assert!(scroll.offset_y() > 0);

    let mut ctx = EventCtx::default();
    scroll.on_event(&Event::Action(Action::ScrollHome), &mut ctx);
    assert!(ctx.handled());
    assert_eq!(scroll.offset_y(), 0);
}

#[test]
fn vertical_scroll_supports_home_end_actions() {
    let sheet = textual::css::default_widget_stylesheet();
    let _guard = textual::css::set_style_context(sheet);
    let console = Console::new();
    let mut scroll = VerticalScroll::new()
        .with_child(Static::new("row 1"))
        .with_child(Static::new("row 2"))
        .with_child(Static::new("row 3"))
        .with_child(Static::new("row 4"))
        .height(2);
    assert!(scroll.focusable());
    let mut tree = build_widget_tree_from_root(&mut scroll).expect("tree should build");
    let _ = render_with_tree(&mut tree, &mut scroll, &console, 8, 2);

    let mut ctx = EventCtx::default();
    scroll.on_event(&Event::Action(Action::ScrollEnd), &mut ctx);
    assert!(ctx.handled());
    let _ = render_with_tree(&mut tree, &mut scroll, &console, 8, 2);
    assert!(scroll.scroll_offset().1 > 0);

    let mut ctx = EventCtx::default();
    scroll.on_event(&Event::Action(Action::ScrollHome), &mut ctx);
    assert!(ctx.handled());
    let _ = render_with_tree(&mut tree, &mut scroll, &console, 8, 2);
    assert_eq!(scroll.scroll_offset().1, 0);
}

#[test]
fn scrollable_container_supports_home_end_actions() {
    let sheet = textual::css::default_widget_stylesheet();
    let _guard = textual::css::set_style_context(sheet);
    let console = Console::new();
    let mut scrollable = ScrollableContainer::new()
        .with_child(Static::new("row 1"))
        .with_child(Static::new("row 2"))
        .with_child(Static::new("row 3"))
        .with_child(Static::new("row 4"))
        .height(2);
    assert!(scrollable.focusable());
    let mut tree = build_widget_tree_from_root(&mut scrollable).expect("tree should build");
    let _ = render_with_tree(&mut tree, &mut scrollable, &console, 8, 2);

    let mut ctx = EventCtx::default();
    scrollable.on_event(&Event::Action(Action::ScrollEnd), &mut ctx);
    assert!(ctx.handled());
    let _ = render_with_tree(&mut tree, &mut scrollable, &console, 8, 2);
    assert!(scrollable.scroll_offset().1 > 0);

    let mut ctx = EventCtx::default();
    scrollable.on_event(&Event::Action(Action::ScrollHome), &mut ctx);
    assert!(ctx.handled());
    let _ = render_with_tree(&mut tree, &mut scrollable, &console, 8, 2);
    assert_eq!(scrollable.scroll_offset().1, 0);
}

#[test]
fn horizontal_scroll_is_focusable_and_supports_home_end_actions() {
    let sheet = textual::css::default_widget_stylesheet();
    let _guard = textual::css::set_style_context(sheet);
    let console = Console::new();
    let mut scroll = HorizontalScroll::new()
        .with_child(Label::new("abcdefghi").with_shrink(true))
        .height(1);
    assert!(scroll.focusable());
    let mut tree = build_widget_tree_from_root(&mut scroll).expect("tree should build");
    let _ = render_with_tree(&mut tree, &mut scroll, &console, 6, 1);

    let mut ctx = EventCtx::default();
    scroll.on_event(&Event::Action(Action::ScrollEnd), &mut ctx);
    assert!(ctx.handled());
    let _ = render_with_tree(&mut tree, &mut scroll, &console, 6, 1);
    assert!(scroll.scroll_offset().0 > 0);

    let mut ctx = EventCtx::default();
    scroll.on_event(&Event::Action(Action::ScrollHome), &mut ctx);
    assert!(ctx.handled());
    let _ = render_with_tree(&mut tree, &mut scroll, &console, 6, 1);
    assert_eq!(scroll.scroll_offset().0, 0);
}

#[test]
fn item_grid_renders_cells() {
    let mut grid = ItemGrid::new()
        .with_child(Label::new("a"))
        .with_child(Label::new("b"));
    let buf = render_once(&mut grid, 10, 2);
    let rendered = buf.as_plain_lines().join("\n");
    assert!(rendered.contains('a'));
    assert!(rendered.contains('b'));
}