ratatui / ratatui

A Rust crate for cooking up terminal user interfaces (TUIs) 👨‍🍳🐀 https://ratatui.rs
https://ratatui.rs
MIT License
10.34k stars 310 forks source link

Ownership in Frame::render_widget #591

Closed cylewitruk closed 11 months ago

cylewitruk commented 11 months ago

Is there a reason why widget: W needs to be owned here? Causing me some headaches when trying to keep different screens in the same field as I can't go from a Box<dyn Widget> to an owned/sized Widget... (i.e. keeping a currently active interchangeable Widget in app state).

https://github.com/ratatui-org/ratatui/blob/c597b87f72bc578cdd65d533506db5bdc49af608/src/terminal.rs#L600

Maybe I'm going about things completely wrong and there's another way to achieve this, but nothing I've found during all of today anyway :sweat:

joshka commented 11 months ago

Historical reasons to some extent. See the following for some more details:

TL;DR: widget was designed for tui-rs to not be retained. It was intentionally designed as if it was a parameter object created again on each Frame and so should generally be lightweight. We've thought a bit about changing this to pass by ref instead, but that might break TUI / Ratatui app and widget in the wild. It could be time to revisit this however.

Put in terms of your problem, there can't be an active Widget because on each frame the Widget is created anew. You'll currently need to store something else for the current state. There are a few options for this that are useful:

For the method that creates a widget you could even fairly easily make the render part of the component by creating a small intermediary like:

struct DynamicWidget<'a> {
    render_fn: Box<dyn FnOnce(Rect, &mut Buffer) + 'a>,
}

impl<'a> DynamicWidget<'a> {
    fn new<F: FnOnce(Rect, &mut Buffer) + 'a>(render_fn: F) -> Self {
        Self {
            render_fn: Box::new(render_fn),
        }
    }
}

impl Widget for DynamicWidget<'_> {
    fn render(self, area: Rect, buf: &mut Buffer) {
        (self.render_fn)(area, buf);
    }
}

struct SomeComponent {
    greeting: String,
}

impl SomeComponent {
    fn to_widget<'a>(&'a self) -> DynamicWidget {
        DynamicWidget::new(|area, buf| self.render(area, buf))
    }

    fn render(&self, area: Rect, buf: &mut Buffer) {
        Paragraph::new(self.greeting.as_str()).render(area, buf);
    }
}

It's also worth taking a look at https://ratatui.rs/concepts/application-patterns/index.html for some ideas about how to handle this as well as looking at how some larger examples structure their code (I was particularly inspired by EDMA when building toot-rs and yazi took some of those ideas a bit further. Working out how to handle multiple views was a difficult thing for me to understand when I first started playing with Ratatui, and there's more examples of this out there (see also @kdheepak's ratatui async template as one option).

P.S. I'm going to convert this to a discussion rather than an issue.