Thanks for asking the question, and welcome to the forums!
This is kinda a funny thing related to why I’m a Ratatui maintainer. Back in April last year I was learning rust by making a rust based Mastodon tui called tooters. One of the big things that I struggled with working out was exactly this problem - showing an auth screen, dealing with things being async, and then getting into the application proper once the auth process was completed. Mostly my self-induced complications were due to bringing an OOP mindset to rust (i.e. screens are all the same, why can’t I just inherit implementation details …)
I ended up making a few (unrelated) PRs to Ratatui and then doing a whole bunch more.
In tooters I solved the screen problem using a State enum and a a variable per state on my root widget. This is just one way, I think I’d probably solve it a bit differently if redoing it today (it’s on my someday list), as I was just getting to understand ownership and borrowing while trying to wrangle tokio and various async ideas and learning rust.
An updated version would move the auth state data out of the screen specific struct and into another struct that can be removed from the screen when the auth screen is no longer visible. I’d still implement this as an enum with all the screens included and have logic to choose which screen is rendered and when to change between them. I’d mostly avoid looking for ways to do virtual dispatch to solve this problem, and be fine with this being a match statement that chooses which screen to render.
The easiest way to do this is really something like:
enum Screen {
ScreenA(ScreenA),
ScreenB(ScreenB),
}
struct ScreenA {
data_for_screen_a: ...
}
struct ScreenB {
data_for_screen_b: ...
}
And your rendering looks a bit like:
impl Widget for &Screen {
fn render(self, area: Rect, buf &mut Buffer) {
// render anything common to all screens
match self {
Self::ScreenA(screen_a) => screen_a.render(area, buf);
Self::ScreenB(screen_b) => screen_b.render(area, buf);
}
}
}
impl Widget for &ScreenA {
...
}
impl Widget for &ScreenB {
...
}
You can also avoid widgets altogether and just pass the Frame
that you get from Terminal.draw()
around the methods. Some people take a more OOP approach and some are more imperative. YMMV.
And then your app looks something like:
struct App {
screen: Screen,
}
impl App {
fn run(&self) {
// main loop etc.
// contains logic to choose which screen is visible,
// move data in / out of the screen as necessary, etc.
...
terminal.draw(|frame| frame.render_widget(self.screen))?;
}
}
Sometimes you might find that it makes sense to store some common screen data on Screen. Don’t be afraid to refactor to a struct with Screen
→ ScreenKind
or something similar.
You can see some examples of this idea in demo2 in the Ratatui repo (which uses tabs not screens, but the idea is similar), and the crates-tui app, which calls this idea a Mode
.
I’d also encourage you to look at a few different Ratatui apps and find patterns you like and steal the ideas. Each is a little different as Ratatui being a library rather than a framework (you call us, we don’t call you). I know that my approach in tooters led to some ideas in Yazi, and some of those ideas in Yazi have filtered back into the way I think about things in Ratatui apps.