ratatui-org / ratatui

Rust library that's all about cooking up terminal user interfaces (TUIs) 👨‍🍳🐀
https://ratatui.rs
MIT License
8.78k stars 263 forks source link

"TaggedLayouts" to make defining interfaces more streamlined #1057

Open JCQuintas opened 2 months ago

JCQuintas commented 2 months ago

Problem

I'm always struggling to name the variables results of calling area(rect)|split(rect), as you have to do them in order bigger elements to smaller elements and make sure everything aligns.

Example:

let [header, body, footer] = Layout::vertical([
    Constraint::Length(1), // header
    Constraint::Fill(4),   // body
    Constraint::Max(5),    // footer
])
.areas(total_area);

let [books_list, book_test] = Layout::horizontal([
    Constraint::Percentage(10), // books_list
    Constraint::Percentage(90), // book_text
])
.areas(body);

let [scroll_up, scroll_down, previous_page, next_page] = Layout::horizontal([
    Constraint::Ratio(1, 4), // scroll_up
    Constraint::Ratio(1, 4), // scroll_down
    Constraint::Ratio(1, 4), // previous_page
    Constraint::Ratio(1, 4), // next_page
])
.areas(footer);

Solution

I would like a way to define the UI/Constraints in a single structure, and then build/split it only once. With all the constraints being automatically fed to the split/areas behind the scenes, in a way I have a simple way to access the resulting "tagged" areas/layouts.

API Suggestion:

let layout = TaggedLayout::vertical()
    .rows([
        Row::new("header", Constraint::Length(1)),
        Row::new("body", Constraint::Fill(4)).columns([
            Column::new("books_list", Constraint::Percentage(10)),
            Column::new("book_text", Constraint::Percentage(90)),
        ]),
        Row::new("footer", Constraint::Max(5)).columns([
            Column::new("scroll_up", Constraint::Ratio(1, 4)),
            Column::new("scroll_down", Constraint::Ratio(1, 4)),
            Column::new("previous_page", Constraint::Ratio(1, 4)),
            Column::new("next_page", Constraint::Ratio(1, 4)),
        ]),
    ])
    .build(total_area); // HashMap<String, Rect>? Serializer?
                        // possibly other builders like `areas/split` as well?

layout.get("scroll_up").unwrap().intersects(mouse)

this could also allow parts of the UI be defined/built by different parts of the application, or on arbitrary orders, like in the example below, where we first thefine the smaller areas into variables, and only then build the main layout. So smaller elements before bigger elements. Which is the oposite way of the current api.

let header = Row::new("header", Constraint::Length(1));

let body = Row::new("body", Constraint::Fill(4)).columns([
    Column::new("books_list", Constraint::Percentage(10)),
    Column::new("book_text", Constraint::Percentage(90)),
]);

let footer = Row::new("footer", Constraint::Max(5)).columns([
    Column::new("scroll_up", Constraint::Ratio(1, 4)),
    Column::new("scroll_down", Constraint::Ratio(1, 4)),
    Column::new("previous_page", Constraint::Ratio(1, 4)),
    Column::new("next_page", Constraint::Ratio(1, 4)),
]);

let layout = TaggedLayout::vertical()
    .rows([header, body, footer])
    .build(total_area);

Alternatives

Additional context

joshka commented 2 months ago

An alternative solution to this that goes one step further is to create a container type that accepts widgets and constraints together. The recent addition of the WidgetRef trait allows Boxed widgets to be stored in containers alongside the contstraint. A PoC version of this is available in https://docs.rs/ratatui-widgets/latest/ratatui_widgets/stack_container/struct.StackContainer.html

let stack = StackContainer::horizontal().with_widgets(vec![
    (Box::new(Paragraph::new("Left")), Constraint::Fill(1)),
    (Box::new(Paragraph::new("Center")), Constraint::Fill(2)),
    (Box::new(Paragraph::new("Right")), Constraint::Fill(1)),
]);

The one part this doesn't do is handle being able to use the areas for things like mouse click checks, but there might be a nice way to do that if designed right.

Note that ratatui-widgets is in an experimental state, with progress highly dependent on availability of inspiration and motivation. Feel free to borrow and adapt in your own app.

A third alternative is to write a proc macro that adapts some sort of syntax that makes variables and constraints easier to write together. Perhaps something like:

vertical! {
    header => length!(1),
    body => fill!(4, horizontal! {
        books_list => percentage!(90),
        books_test => percentage!(10),
    },
    footer = max!(5, horizontal! {
        scroll_up => fill!(1),
        scroll_down => fill!(1),
        previous_page => fill!(1),
        next_page => fill!(1),
    }
}

There's probably some parts of this that can't easily work, or are better expressed some other way, and perhaps the tagged layout would be the underlying piece that would make such a macro possible to write easily. This could work well with some of the ideas in https://github.com/ratatui-org/ratatui-macros