"TaggedLayouts" to make defining interfaces more streamlined
JCQuintas opened this issue · comments
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
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 Box
ed 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