Brainstorming redesigns

Basically historical reasons. If I’d redesign Ratatui entirely based on current knowledge:

  • Widgets would be implement multiple methods for layout, handling events, rendering, etc.
  • Widget methods would have access to a context that helps them play nicely with each other
  • Buffer would be a trait, to make it possible to give portions of a frame to write to instead of the entire frame
  • A bunch of other things.

But we’ve got what we’ve got, and so we implement things in an additive way that hopefully avoids breaking the world.

@joshka, you wrote this in an issue, and it interests me because it’s so different. Can you go into detail in all of this?

For each bullet point:

  1. How would this all work? What events would go to the widgets? Just crossterm events?
  2. What would be in these contexts for each method? How would they be used? Would rendering be done using this Buffer trait you talk about below?
  3. How? This idea especially interests me. When it’s a trait, how will it be so that you can only give portions of the screen to draw?
  4. What other things?

If there’s any extra info you have for each you can give it, the questions are just to provoke thoughts.

I feel like you’ve got some great ideas here. Thanks for your work!

1 Like

The context of the answer was:

On a sidenote, I wonder why it was never considered to use Frame instead of Buffer as argument to render().

Basically historical reasons

Ratatui didn’t pop into being as Ratatui, it was a fork of tui-rs which came before it. I only got involved in Ratatui after the fork happened so I don’t have much insight into why certain choices were made, and they likely made sense at the time. To understand them I’d recommend digging into the commit and PR history of the original repo.

Widgets would be implement multiple methods for layout, handling events, rendering,

Much of my thinking on this comes from Announcing Masonry 0.1, and my vision for Rust UI · PoignardAzur as well as pretty much everything in https://raphlinus.github.io/ and other related Xilem blogs (| Homepage for the Linebender organization). It would be difficult to summarize all that other than to say that using Widget as a simple function that takes an area and a buffer and writes things is really anemic. It focuses heavily on just one portion of the output side of a UI, and ignores layout, interaction with events, and user interaction. Likely the missing pieces look like extra methods on traits to implement those parts. But there’s lots of prior art in this area. In particular a redesigned TUI should draw heavily from BubbleTea and Textualize, but bear in mind that Rust’s borrow checker plays a part in designing things and such a redesign should look to egui, imgui, dioxus, etc. for inspiration on various things too. There’d likely still be a Widget trait, and there’d be a much greater focus on the compositional ideas (containers, grids, flex layout etc)

Widget methods would have access to a context that helps them play nicely with each other

The context on this statement is that one of the things that is a problem for the current widget trait is that it’s difficult to improve and add cross cutting functionality to. This is due to the limited parameters - a buffer and an area. If instead of these parameters we had a WidgetContext or RenderContext struct, then we’d be able to add more fields as they become necessary. E.g. event related fields, layout related fields, application context. This would allow such ideas as performing a layout step for all widgets first, working out the area that the widget will be drawn into based on all the other various parts of state, and then in another method doing the actual drawing based on the previous calculations.

Buffer would be a trait, to make it possible to give portions of a frame to write to instead of the entire frame

One of the things this opens up is that buffers can now be implemented in a way where the backing store of the buffer is not just a contiguous vector. Maybe we want that to be an ndarray for some performance reason. Maybe we want that to be implemented in a way where writes are a passthrough for some areas and noop for others. E.g., tui-scrollview currently has to allocate a full sized buffer and then copy the region that is displayed. Instead it might be nice to just ignore anything not currently being displayed and write directly to the actual buffer. Ndarray types allow mutable views onto regions of a larger array, which if we reimplemented buffer on top of that could make it impossible for widgets to draw outside of their specific allocated space (right now they don’t have to respect the area parameter at all).

A bunch of other things.

I don’t have a good list of what these other things are. But they’re mainly around making Ratatui a more full app development framework rather than a library. The answer to this basically comes down to looking at other TUI frameworks and seeing what parts are easy that are difficult in Ratatui, and then working backwards from that to work out what we’d need to make a Rust TUI as easy to do the same things. Do this enough times and you’ll build up a good list of redesignable parts in Ratatui.

I hope this helps understand that post a bit better.

Something that I find also helps in thinking about these things is to sit down with ChatGPT (or your LLM of choice) and LLMsplain up a design of a TUI framework from first principles. LLMs are good at telling you obvious things in simple ways, which makes them sometimes good for validating what is the right way that something should work. I’ve often seen them hallucinate a method or struct name that looks right and would be a better name / design / approach than the actual design. Do this enough times and you’ll get a simple and obvious framework that works closer to how people might expect it to than you might imagine upfront.

1 Like

Really great answer! But I still don’t get why Buffer should be a trait. What is this trait being implemented on?

Wouldn’t you have something more like passing in a struct to the render function that has its size and position set to what the Widget needs, so it can only draw in that area? And then that struct containing a mutable view of the main ndarray with that size?

Anything that’s reasonable to write an adapter for. Right now that’s a Vec, but this might make sense to be implemented as a ndarray::Array2 or ArrayView2, or some struct that sends bytes directly to the screen immediately rather than passing through the diff / flush mechanism (or even some struct that sends bytes over a network). We might also use this to introduce an approach where it’s possible to store entire spans of text at once instead of just storing text character by character.

These are all hypothetical though until there’s a real need to solve some actual problem, while simultaneously we evaluate that the tradeoff of breaking backwards compatibility worth making rather than not solving that problem.