Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: switch layout engine and improve layout experience #374

Open
3 tasks
hasezoey opened this issue Aug 6, 2023 · 8 comments
Open
3 tasks

RFC: switch layout engine and improve layout experience #374

hasezoey opened this issue Aug 6, 2023 · 8 comments
Labels
Type: Enhancement New feature or request

Comments

@hasezoey
Copy link
Contributor

hasezoey commented Aug 6, 2023

Problem

The current layout engine cassowary-rs has been inactive since 2018 and layout problems have come up here and there (see #354)

Current behavior

For example, the current behavior of cassowary-rs (at least on how it is used in current master), is:
if there is more space available and there are elements that could expand, only one gets expanded to the remaining size (or may not expand at all) and which of the elements to expand is not marked as "consistent" by the engine.

Also a problem currently is how "lower than required space" is handled, which is currently rather undefined behavior (ex #354 (comment)) and in specific cases may even panic sometimes (ex #354 (comment))

Collection of solutions

(none yet decided on)

Open Questions

  • Decide on which engine to switch to:
  • Decide on how to approach the switch:
    • switch it transparently (ex directly switch out cassowary-rs, without modifying public API and have no changes to the user of ratatui)
    • switch so that the engine (ex taffy) would be directly exposed
    • switch so that there is no layout engine by default and could be chosen by the user
    • some incremental updating (like first transparently, then then over time expose more)
  • If we go with something like taffy, then how should it be implemented?
    • because taffy requires a main instance (taffy::Taffy), and adding new nodes (leafs) onto there and then backwards until there is a root-node (or maybe use a custom LayoutTree? maybe see this, documentation is not great about that)
    • also should the nodes themself be stored outside of the "main" loop (via something like feat(widgets)!: make widgets optionally reusable #122)? because they dont need to be re-defined over and over, only re-computed)
  • anything else to add here?

References / notes

PS: this is my first RFC-like issue, please tell me if i missed something or should change

@hasezoey hasezoey added the Type: Enhancement New feature or request label Aug 6, 2023
@joshka
Copy link
Member

joshka commented Aug 7, 2023

Looks good. I'd suggest adding a couple of things:

  • I've suggested taffy before as a solution worth investigating, but haven't personally used it. This definitely needs someone to explore it as an option and report back.
  • Because layout is just something that generates Rects, prototypes can be implemented directly in user code or an external library before adding it to this crate.
  • It's worth doing a dig to see if there are any other solutions in various tui-rs / ratatui apps / libs

#122 is a bit stalled as it's a difficult change to make.

@hasezoey
Copy link
Contributor Author

hasezoey commented Aug 7, 2023

I've suggested taffy before as a solution worth investigating, but haven't personally used it. This definitely needs someone to explore it as an option and report back.

i have tried using taffy in a limited sense, where it works great (after having figured out what options i need to set), though note that i used it as a calculator to have all Table::widths calculated to fill all space (combined fixed length and expandable)

though this alone is not enough testing, and i also dont know if it is the best way to have a new Taffy instance for every time we currently have a separate Layout construction

the code i am speaking of
let column1 = taffy
  .new_leaf(Style {
    size: Size {
      width:  points(4.0),
      height: auto(),
    },
    ..Default::default()
  })
  .unwrap();
let column2 = taffy
  .new_leaf(Style {
    flex_grow: 1.0,
    ..Default::default()
  })
  .unwrap();

let column3 = taffy
  .new_leaf(Style {
    size: Size {
      width:  points(4.0),
      height: auto(),
    },
    ..Default::default()
  })
  .unwrap();

let column4 = taffy
  .new_leaf(Style {
    flex_grow: 1.0,
    ..Default::default()
  })
  .unwrap();

let column5 = taffy
  .new_leaf(Style {
    flex_grow: 1.0,
    ..Default::default()
  })
  .unwrap();

let root_node = taffy
  .new_with_children(
    Style {
      size: Size {
        width:  Dimension::Percent(1.0),
        height: auto(),
      },
      ..Default::default()
    },
    &[column1, column2, column3, column4, column5],
  )
  .unwrap();

taffy
  .compute_layout(
    root_node,
    Size {
      width:  AvailableSpace::Definite(width as f32),
      height: AvailableSpace::Definite(1 as f32),
    },
  )
  .unwrap();

let widths = vec![
  Constraint::Length(taffy.layout(column1).unwrap().size.width as u16),
  Constraint::Length(taffy.layout(column2).unwrap().size.width as u16),
  Constraint::Length(taffy.layout(column3).unwrap().size.width as u16),
  Constraint::Length(taffy.layout(column4).unwrap().size.width as u16),
  Constraint::Length(taffy.layout(column5).unwrap().size.width as u16),
]

let table = widgets::Table::new(rows)
  .header(header)
  .block(widgets::Block::default().borders(Borders::ALL).title(block_title))
  .highlight_symbol(">> ")
  .highlight_style(Style::default().add_modifier(Modifier::REVERSED))
  .widths(&widths);

@joshka
Copy link
Member

joshka commented Aug 7, 2023

Looks neat. I wonder how easy it would be to impl From<Constraint> for Style and then an equivalent split method.

You inspired me to have a bit of a play with this for the styles example and it cleans up the gaps (e.g. this allocates 16 cells across 2 rows of a Rect):

fn layout_named_colors(area: Rect) -> Result<Vec<Rect>> {
    let mut taffy = Taffy::new();
    let grid = repeat_with(|| taffy.new_leaf(Default::default()).unwrap())
        .take(16)
        .collect_vec();
    let root = taffy.new_with_children(
        taffy::style::Style {
            display: Display::Grid,
            size: Size::from_points(area.width as f32, area.height as f32),
            grid_template_columns: vec![auto(); 8],
            ..Default::default()
        },
        &grid,
    )?;
    taffy.compute_layout(root, min_content())?;
    Ok(grid
        .iter()
        .map(|col| {
            let layout = taffy.layout(*col).unwrap();
            Rect::new(
                layout.location.x as u16 + area.x,
                layout.location.y as u16 + area.y,
                layout.size.width as u16,
                layout.size.height as u16,
            )
        })
        .collect_vec())
}

Made with VHS

I suspect that a min_size layout around a fixed size root node (either grid or flex layout) is how the outermost layer of most UIs would be built.

I did see something about Cache types in the docs, but couldn't find detail on how this was implemented.

It seems a bit more complex than the current layout, so we might want to still keep that interface to hide the complexity (or implement something that works well for the sorts of UIs that work in TUIs.

The cool thing about taffy is that I think it will allow us to size widgets based on content rather than content based on widgets (it looks like there is the ability to pass in calculation functions etc.).

Perhaps an approach might be to make it a feature flag and gradually move things over to use it?

@TieWay59
Copy link
Contributor

TieWay59 commented Aug 7, 2023

Hi, I may not know much about taffy, but I believe taffy tends to bring huge change to Ratatui. Is it easy to keep the user API the same as the current version? It seems quite a breaking change.

@joshka
Copy link
Member

joshka commented Aug 7, 2023

Yep - I'd definitely still like the currently layout API to do the same thing as it already does. My intuition is that mapping the current user API onto taffy instead of cassowary code would probably be possible. I knew nothing about taffy other than it existed yesterday. It took a few tries to get that rendering right, as the docs are sparse and assume a reasonable knowledge of flexbox / grid layout concepts.

The change would probably break minor things, but in ways that fix buggy behavior e.g.:

image

@hasezoey
Copy link
Contributor Author

hasezoey commented Aug 7, 2023

Hi, I may not know much about taffy, but I believe taffy tends to bring huge change to Ratatui. Is it easy to keep the user API the same as the current version? It seems quite a breaking change.

yes it would be possible to do it non-breakingly, see Open Questions Decide on how to approach the switch: (on the original issue description)

though a "plain transparent" approach would not break the current API (aside from maybe some edge cases), but would leave a lot of configuration options on the table that taffy would offer.
for example, if we would leave the API the same, then you would have to do something like in #374 (comment), and the calculations would likely be done twice (if not more) and would not be as convenient.

maybe we could consider doing a "Wrapper" type, that both implements a From<Constraints>(ratatui Constraint, apply ratatui taffy calculations) and a From<taffy::Layout>(taffy layout, directly use that instead of doing another calculation)

EDIT: though the API would be able to change without breakage, i would still prefer having that in a "major" version and mention that it changed

@kdheepak
Copy link
Collaborator

For completeness, there's casuarius which appears to be a more maintained version of cassowary-rs: https://github.com/schell/casuarius

@joshka
Copy link
Member

joshka commented Aug 11, 2023

In #393 I updated the layout example to show a bunch of combinations of how constraints interact (currently). I'm not sure whether we want to merge that PR, but this be useful as a good visual check for equivalency (and be an easy way to describe any issues that we hit with taffy)

example

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants