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

render methods should be passed context instead of buffer #1044

Open
kdheepak opened this issue Apr 17, 2024 · 8 comments
Open

render methods should be passed context instead of buffer #1044

kdheepak opened this issue Apr 17, 2024 · 8 comments
Labels
enhancement New feature or request

Comments

@kdheepak
Copy link
Collaborator

kdheepak commented Apr 17, 2024

Problem

Currently, there's no way for the render method to know additional contextual information that would be useful for widgets.

For example, frame count is currently unavailable to the render method. Additional features require more parameters to the method.

Solution

While WidgetRef and StatefulWidgetRef are unstable, I'd like to propose a Context instead of buffer.

 pub trait WidgetRef { 
     fn render_ref(&self, area: Rect, ctx: &mut Context); 
 } 

Context can be defined like so:

pub struct Context {
    buffer: Buffer
}

We can even have Context have the similar methods as Buffer, and instead of

            buf.get_mut(area.left(), area.top())
                .set_symbol(border_top_left)
                .set_style(border_style);

widget authors would write the following instead:

            ctx.get_buf_mut(area.left(), area.top())
                .set_symbol(border_top_left)
                .set_style(border_style);

The main advantage of this is that we can add features to Context in the future without breaking backward compatibility.

For example, Context can contain a frame count.

The main motivation for this idea was having a Palette in Context. For example, if Context is defined as this:

pub struct Context {
    buffer: Buffer,
    frame_count: usize,
    palette: Palette,
}

Imagine a Palette that is defined like so:

struct Palette {
    base: Color,
    primary: Color,
    secondary: Color,
    overlay0: Color,
    overlay1: Color,
    overlay2: Color,
    subtext0: Color,
    subtext1: Color,
    surface0: Color,
    surface1: Color,
    surface2: Color,
    text: Color,
}

impl Palette {
    fn catppuccin() -> Self {
        Palette {
            base: Color::from_str("#eff1f5"),
            primary: Color::from_str("#04a5e5"),
            secondary: Color::from_str("#209fb5"),
            overlay0: Color::from_str("#9ca0b0"),
            overlay1: Color::from_str("#8c8fa1"),
            overlay2: Color::from_str("#7c7f93"),
            subtext0: Color::from_str("#6c6f85"),
            subtext1: Color::from_str("#5c5f77"),
            surface0: Color::from_str("#ccd0da"),
            surface1: Color::from_str("#bcc0cc"),
            surface2: Color::from_str("#acb0be"),
            text: Color::from_str("#4c4f69"),
        }
    }
}

Now, every widget's render_ref method can be passed a context that has palette information and we can make a Ratatui application styled automatically based on themes.

There's probably more use cases for a Context (other user preferences like light mode / dark mode?) that we could expand on in the future if this feature was enabled.

Thoughts?

@kdheepak kdheepak added the enhancement New feature or request label Apr 17, 2024
@kdheepak
Copy link
Collaborator Author

kdheepak commented Apr 17, 2024

The WidgetRef and StatefulWidgetRef methods as well as new methods are being discussed in this issue: #996

Themes are discussed in this issue: #799

I just realized that I already opened an issue / discussion related to this before: #872

@EdJoPaTo
Copy link
Member

A theme would be added to a widget before rendering like a border or character set is done. We shouldn’t mix render and widget configuration.

Otherwise it’s interesting to explore this idea.

@orhun
Copy link
Sponsor Member

orhun commented Apr 17, 2024

I like the idea of adding a Context parameter to the render functions. I think it opens a lot of doors in terms of usability.

My only question is, how can we make it backwards compatible? I'm assuming it is not really possible to do it so. Maybe we can have a From<Buffer> implementation for Context for limiting the breaking change to only:

- render(area, buffer)
+ render(area, buffer.into())

Just throwing some ideas.

@kdheepak
Copy link
Collaborator Author

kdheepak commented Apr 17, 2024

For the average Ratatui user this change won't be breaking. For example, this code will remain unchanged:

    frame.render_widget(
        Paragraph::new("Example of a paragraph that a user writes")
            .alignment(Alignment::Center),
        area,
    );

However, every Ratatui widget developer OR Ratatui user that implements the one of the widgets trait will have to update their code. Currently WidgetRef is unstable so I was proposing only changing that.

If we want to make a change to the Widget trait as well, it might be possible to use Into<Context> as a generic argument like so:

impl Widget for UserDefinedWidget {
    fn render<T>(self, area: Rect, buf: &mut T) where T: Into<Context> { 
        // ...
    }
}

impl WidgetRef for UserDefinedWidget {
    fn render_ref<T>(&self, area: Rect, buf: &mut T) where T: Into<Context> { 
        // ...
    }
}

If we did this, any widget developer will be able to write user_defined_widget.render(area, buffer) if From<Buffer> is implemented for Context but they'll probably still have to update their impl Widget for UserDefinedWidget to use the new generic function definition for the trait. I haven't looked into the impact surface of such a change.

@kdheepak
Copy link
Collaborator Author

Another reason a Context would be useful is during render we can set the cursor at a specific location. Currently with Buffer cursors are handled separately.

This will allow better interfaces for "focus". For example, bubbletea has text_input.focus() which can make the cursor visible and appear in a text field at the appropriate location.

@joshka
Copy link
Member

joshka commented Apr 19, 2024

This is definitely a good area to explore, but also one to do so conservatively. It would be easy to mistake the common elements that all apps need here.

I think it's worth trying a few different ideas outside of ratatui (Edit: in a trait defined in an app) for this and seeing what works and enables the right use cases. The palette example makes me concerned as it implies a pretty big investment in making a design scheme (i.e. choosing the right pieces to palettize). I like that the buffer and frame count have a nicer place to live.

To be clear, I want to see something like this in Ratatui long term definitely, but not done in a hasty manner which drastically changes things without building up the narrative around it first.

It could be worth making this a discussion rather than an issue (the distinction being that an issue gets closed when done, while a discussion can be more long term).

@kdheepak
Copy link
Collaborator Author

I agree that this shouldn’t be done in haste. I think frame count is probably the strongest motivation for this right now.

@thscharler
Copy link

The cursor would do it for me.

And maybe some extra layers of buffers to render Hovers, Combobox and the like.

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

No branches or pull requests

5 participants