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;

#[test]
fn scroll_view_renders_offset_viewport() {
    let console = Console::new();
    let mut options = console.options().clone();
    options.size = (14, 3);
    options.max_width = 14;
    options.max_height = 3;

    let list = ListView::new(vec![
        "item 1".to_string(),
        "item 2".to_string(),
        "item 3".to_string(),
        "item 4".to_string(),
    ]);
    let mut scroll = ScrollView::new(list).height(3);
    scroll.scroll_to(1);

    let buf = FrameBuffer::from_renderable(&console, &options, &scroll, None);
    insta::assert_snapshot!(buf.debug_dump());
}

#[derive(Debug, Clone)]
struct NoIntrinsicHeightWidget;

impl NoIntrinsicHeightWidget {
    fn new() -> Self {
        Self
    }
}

impl Widget for NoIntrinsicHeightWidget {
    fn render(
        &self,
        _console: &rich_rs::Console,
        options: &rich_rs::ConsoleOptions,
    ) -> rich_rs::Segments {
        let mut out = rich_rs::Segments::new();
        let height = options.size.1.max(1);
        for index in 0..height {
            out.push(rich_rs::Segment::new(format!("line {:02}", index)));
            if index + 1 < height {
                out.push(rich_rs::Segment::line());
            }
        }
        out
    }
}

#[test]
fn scroll_view_scrolls_children_without_intrinsic_height() {
    let console = Console::new();
    let mut options = console.options().clone();
    options.size = (12, 3);
    options.max_width = 12;
    options.max_height = 3;

    let mut scroll = ScrollView::new(NoIntrinsicHeightWidget::new()).height(3);

    let before = FrameBuffer::from_renderable(&console, &options, &scroll, None);
    let before_lines = before.as_plain_lines();
    assert!(before_lines[0].starts_with("line 00"));

    scroll.scroll_by(1);
    let after = FrameBuffer::from_renderable(&console, &options, &scroll, None);
    let after_lines = after.as_plain_lines();
    assert!(after_lines[0].starts_with("line 01"));
}

#[derive(Debug, Clone)]
struct StretchWidget;

impl StretchWidget {
    fn new() -> Self {
        Self
    }
}

impl Widget for StretchWidget {
    fn render(
        &self,
        _console: &rich_rs::Console,
        options: &rich_rs::ConsoleOptions,
    ) -> rich_rs::Segments {
        let mut out = rich_rs::Segments::new();
        let height = options.size.1.max(1);
        for index in 0..height {
            out.push(rich_rs::Segment::new(format!("stretch {:02}", index)));
            if index + 1 < height {
                out.push(rich_rs::Segment::line());
            }
        }
        out
    }
}

#[test]
fn scroll_view_caps_offset_for_stretch_children() {
    let console = Console::new();
    let mut options = console.options().clone();
    options.size = (14, 3);
    options.max_width = 14;
    options.max_height = 3;

    let mut scroll = ScrollView::new(StretchWidget::new()).height(3);
    let _ = FrameBuffer::from_renderable(&console, &options, &scroll, None);
    scroll.scroll_by(1000);

    let buf = FrameBuffer::from_renderable(&console, &options, &scroll, None);
    let lines = buf.as_plain_lines();
    assert!(
        lines[0].starts_with("stretch 03"),
        "expected capped offset line, got {:?}",
        lines[0]
    );
}