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

feat(widgets)!: make widgets optionally reusable #122

Closed
wants to merge 1 commit into from

Conversation

bolshoytoster
Copy link

In some situations, ratatui's immediate-mode rendering can be inefficient, i.e. for a list, you have to clone either the vec or it's items again each frame to make the new list object.

This commit makes it so the {,Stateful}Widget::render functions only take self by immutable reference, meaning they're not consumed and can be reused.

The Frame::render_{,stateful_}widget functions take widget by value though, and changing it would break all code using it, so I've added separate functions: render_{,stateful_}widget_reusable functions which take the widget by reference, allowing it to be reused.

This could also allow StatefulWidgets to store their state themselves, rather than relying on the user to store it.

I also had to remove any takes in the render functions since they would invalidate the widget.

I also ran cargo fmt with the default config to remove formatting accidentally done under my config. I can undo the formatting if needed.

@sophacles
Copy link
Collaborator

The Vec arguments require lots of allocs every time they are constructed then consumed, and that can get expensive. The other stuff, not so much (see also my comment on #16, which is a similar PR, tl;dr the widgets are just a way to build function argumetns). I wonder if it would make sense to instead have (e.g.) List be generic on IntoIterator<Item=&ListItem> or similar.

@orhun
Copy link
Sponsor Member

orhun commented Apr 15, 2023

Hey, can you revert the formatting changes?

@bolshoytoster
Copy link
Author

@orhun bam.

@joshka
Copy link
Member

joshka commented May 3, 2023

I like this change - it enables some things that were a pain to work around the widget being consumed on render calls or when you can get a reference but it's more difficult to get something consumable (e.g. joshka@e56d8ea where I can easily call render on a boxed dyn widget which means the List Widget can be extended to display any widget and not just specific ones).

One possible other approach to implementing a new method would to actually break callers by making render_widget and render_stateful_widget actually borrow the widget (and probably area too) instead of having two methods. We're in a spot where it could be reasonable to make a choice to break consumers here if the outcome enables many more scenarios and makes the library more ergonomic - this is probably controversial and I'd defer to those who have more experience than I do on this.

If we keep the two methods naming, do you think render_widget_as_ref or render_as_borrowed() might be more idiomatic than _reusable?

@bolshoytoster
Copy link
Author

@joshka

One possible other approach to implementing a new method would to actually break callers by making render_widget and render_stateful_widget actually borrow the widget (and probably area too) instead of having two methods.

Would it be possible to have them accept either a Widget or a &Widget (something like AsRef<Widget>), that would probably be the best option, but I'm not sure if that would work.

If we keep the two methods naming, do you think render_widget_as_ref or render_as_borrowed() might be more idiomatic than _reusable?

You're absolutely right, _reusable was mainly because I couldn't think of a better name at the time.

@joshka
Copy link
Member

joshka commented May 3, 2023

I couldn’t quite get AsRed to work - if you can that would be definitely be the best way to make this work.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=f2fb5c4961dbd7201ad60ea912e44ff0

@bolshoytoster
Copy link
Author

@joshka I think you got in the playground is because Widget isn't local, you could try adding it to src/widgets/mod.rs and see if that works.

@joshka joshka changed the title Makes widgets optionally reusable feat(widgets): make widgets optionally reusable May 7, 2023
@joshka joshka marked this pull request as draft May 26, 2023 19:01
@joshka joshka added the Type: Breaking Change This change will cause application code to break and must be noted in the breaking changes docs etc. label May 26, 2023
@aschey
Copy link

aschey commented Jun 1, 2023

Unfortunately, you can't make a blanket implementation for a foreign trait like that. What you can do is make your own trait that mimics AsRef and make a blanket implementation for that: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ff64715562c78e2d9891bb508530ce3f

@joshka
Copy link
Member

joshka commented Jun 2, 2023

Unfortunately, you can't make a blanket implementation for a foreign trait like that. What you can do is make your own trait that mimics AsRef and make a blanket implementation for that: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ff64715562c78e2d9891bb508530ce3f

Nice! That sounds like a realistic approach!

@joshka joshka force-pushed the main branch 2 times, most recently from 229d3eb to 907bf3e Compare June 2, 2023 03:18
@codecov
Copy link

codecov bot commented Jun 2, 2023

Codecov Report

Merging #122 (faa0444) into main (358b50b) will increase coverage by 0.15%.
The diff coverage is 96.77%.

@@            Coverage Diff             @@
##             main     #122      +/-   ##
==========================================
+ Coverage   81.63%   81.79%   +0.15%     
==========================================
  Files          34       34              
  Lines        6574     6648      +74     
==========================================
+ Hits         5367     5438      +71     
- Misses       1207     1210       +3     
Impacted Files Coverage Δ
src/widgets/clear.rs 0.00% <0.00%> (ø)
src/widgets/mod.rs 72.22% <85.71%> (+5.55%) ⬆️
src/terminal.rs 63.32% <100.00%> (+7.67%) ⬆️
src/widgets/barchart.rs 79.71% <100.00%> (ø)
src/widgets/block.rs 82.33% <100.00%> (ø)
src/widgets/calendar.rs 82.30% <100.00%> (ø)
src/widgets/canvas/mod.rs 92.15% <100.00%> (ø)
src/widgets/chart.rs 95.93% <100.00%> (ø)
src/widgets/gauge.rs 86.95% <100.00%> (+0.19%) ⬆️
src/widgets/list.rs 97.93% <100.00%> (ø)
... and 4 more

@joshka
Copy link
Member

joshka commented Jun 2, 2023

Thanks for the fix @aschey. That approach works pretty nicely (I updated code in this PR to use that approach).

I think it's worth playing with this more to understand some of the pros and cons. We probably want to also make the area Rect param pass by ref also (that's particularly useful when rendering inner blocks).

In some situations, ratatui's render functions can make it difficult to
implement specific behavior or can make it difficult and inefficient to
reuse widgets. E.g. for a list, you have to clone either the vec or it's
items again each frame to make the new list object. Another example is
rendering a widget in a loop, where you have to clone the widget each
time.

This commit makes it so the `Widget`/`StatefulWidget``::render`
functions only take `self` by immutable reference, meaning they're not
consumed and can be reused.

The `Frame::render_{,stateful_}widget` functions take `widget` by value
though. To allow these functions to be called with references or values,
we added a new trait `AsWidgetRef` which is implemented for `&W` and `W`
where `W: Widget`/`StatefulWidget. This trait is then used to call the
`render` functions. Thus backwards compatibility is maintained for the
`Frame` API.

This also removes any calls to `take()` in the `render` functions since
this cannot happen when the widget is passed by reference.

BREAKING CHANGE: `Widget`/`StatefulWidget`'s `render` functions now take
`self` by immutable reference instead of by value. To update your custom
widgets simply add `&` before the `self` parameter in the `render`
function.
@joshka
Copy link
Member

joshka commented Jun 2, 2023

Updated Widget list PoC based on this: joshka@e911bee

asciicast

@joshka
Copy link
Member

joshka commented Jun 2, 2023

I wonder if it would make sense to instead have (e.g.) List be generic on IntoIterator<Item=&ListItem> or similar.

Playing with this in the Widget List PoC and noticing what I needed to change in the tests to make them work nicely, I definitely agree with that @sophacles

@joshka joshka marked this pull request as ready for review June 3, 2023 03:38
@sayanarijit
Copy link
Member

This PR looks good. The scope is limited. We may need to accompany this change with many other related PRs, but as far as this PR goes, LGTM.

@kdheepak kdheepak changed the title feat(widgets): make widgets optionally reusable feat(widgets)!: make widgets optionally reusable Sep 5, 2023
@deifactor
Copy link
Contributor

In terms of the Widget trait, if we want to make this change then it's better to just bite the bullet and change the type signature of render. It'll result in a breaking change for calls to Frame::render_widget, but it means that removing support for render-by-move won't result in having to do another change to rename render_as_ref to render down the line.

Copy link
Sponsor Member

@orhun orhun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 from me. It is a change that user will definitely benefit in the long run.

joshka added a commit that referenced this pull request Jan 20, 2024
Many widgets can be rendered without changing their state.

This commit implements The `Widget` trait for various references to
widgets and changes their implementations to be immutable.

This allows us to render widgets without consuming them by passing a ref
to the widget when calling `Frame::render_widget()`.

```rust
// this might be stored in a struct
let paragraph = Paragraph::new("Hello world!");

let [left, right] = area.split(&Layout::horizontal([20, 20]));
frame.render_widget(&paragraph, left);
frame.render_widget(&paragraph, right); // we can reuse the widget
```

- Clear
- Block
- Tabs
- Sparkline
- Paragraph
- Gauge
- Calendar

Other widgets will be implemented in follow up commits.

Fixes: #164
Replaces PRs: #122 and
  #16
Enables: #132
Validated as a viable working solution by: #836
@joshka
Copy link
Member

joshka commented Jan 20, 2024

Closing with deference to #833 which achieves the same result by implementing Widget for &Block etc.

@joshka joshka closed this Jan 20, 2024
joshka added a commit that referenced this pull request Jan 24, 2024
Many widgets can be rendered without changing their state.

This commit implements The `Widget` trait for various references to
widgets and changes their implementations to be immutable.

This allows us to render widgets without consuming them by passing a ref
to the widget when calling `Frame::render_widget()`.

```rust
// this might be stored in a struct
let paragraph = Paragraph::new("Hello world!");

let [left, right] = area.split(&Layout::horizontal([20, 20]));
frame.render_widget(&paragraph, left);
frame.render_widget(&paragraph, right); // we can reuse the widget
```

- Clear
- Block
- Tabs
- Sparkline
- Paragraph
- Gauge
- Calendar

Other widgets will be implemented in follow up commits.

Fixes: #164
Replaces PRs: #122 and
  #16
Enables: #132
Validated as a viable working solution by: #836
joshka added a commit that referenced this pull request Jan 24, 2024
Many widgets can be rendered without changing their state.

This commit implements The `Widget` trait for references to
widgets and changes their implementations to be immutable.

This allows us to render widgets without consuming them by passing a ref
to the widget when calling `Frame::render_widget()`.

```rust
// this might be stored in a struct
let paragraph = Paragraph::new("Hello world!");

let [left, right] = area.split(&Layout::horizontal([20, 20]));
frame.render_widget(&paragraph, left);
frame.render_widget(&paragraph, right); // we can reuse the widget
```

Implemented for all widgets except BarChart (which has an implementation
that modifies the internal state and requires a rewrite to fix.

Other widgets will be implemented in follow up commits.

Fixes: #164
Replaces PRs: #122 and
#16
Enables: #132
Validated as a viable working solution by:
#836
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Type: Breaking Change This change will cause application code to break and must be noted in the breaking changes docs etc.
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

None yet

7 participants