chore: move unstable widget refs to ratatui (#1491)

These are less stable than the non-ref traits as we have not yet
committed to the exact API. This change moves them to ratatui from
ratatui-core.

To facilitate this:
- implementations of WidgetRef for all internal widgets are removed and
  replaced with implementations of Widget for references to those
  widgets.
- Widget is now implemented for Option<W> where W: Widget, allowing for
  rendering of optional widgets.
- The blanket implementation of Widget for WidgetRef is reversed, to be
  a blanket implementation of WidgetRef for all &W where W: Widget.

BREAKING CHANGE: implementations of WidgetRef no longer have a blanket
implementation of Widget, so Widgets should generally implement the
Widget trait on a reference to the widget rather than implementing
WidgetRef directly. This has the advantage of not requiring unstable
features to be enabled.

Part of: https://github.com/ratatui/ratatui/issues/1388

Co-authored-by: Orhun Parmaksız <orhunparmaksiz@gmail.com>
This commit is contained in:
Josh McKinney 2024-11-18 14:19:21 -08:00 committed by GitHub
parent 46902f5587
commit a41c97b413
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 348 additions and 329 deletions

View file

@ -76,6 +76,21 @@ This is a quick summary of the sections below:
## Unreleased (0.30.0) ## Unreleased (0.30.0)
### `WidgetRef` no longer has a blanket implementation of Widget
Previously there was a blanket implementation of Widget for WidgetRef. This has been reversed to
instead be a blanket implementation of WidgetRef for all &W where W: Widget. Any widgets that
previously implemented WidgetRef directly should now instead implement Widget for a reference to the
type.
```diff
-impl WidgetRef for Foo {
- fn render_ref(&self, area: Rect, buf: &mut Buffer)
+impl Widget for &Foo {
+ fn render(self, area: Rect, buf: &mut Buffer)
}
```
### The `From` impls for backend types are now replaced with more specific traits [#1464] ### The `From` impls for backend types are now replaced with more specific traits [#1464]
[#1464]: https://github.com/ratatui/ratatui/pull/1464 [#1464]: https://github.com/ratatui/ratatui/pull/1464

1
Cargo.lock generated
View file

@ -2129,7 +2129,6 @@ dependencies = [
"compact_str", "compact_str",
"document-features", "document-features",
"indoc", "indoc",
"instability",
"itertools 0.13.0", "itertools 0.13.0",
"lru", "lru",
"palette", "palette",

View file

@ -12,54 +12,96 @@ command = ["cargo", "check", "--all-features", "--color", "always"]
need_stdout = false need_stdout = false
[jobs.check-all] [jobs.check-all]
command = ["cargo", "check", "--all-targets", "--all-features", "--color", "always"] command = [
"cargo",
"check",
"--all-targets",
"--all-features",
"--color",
"always",
]
need_stdout = false need_stdout = false
[jobs.check-crossterm] [jobs.check-crossterm]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "crossterm"] command = [
"cargo",
"check",
"--color",
"always",
"--all-targets",
"--no-default-features",
"--features",
"crossterm",
]
need_stdout = false need_stdout = false
[jobs.check-termion] [jobs.check-termion]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termion"] command = [
"cargo",
"check",
"--color",
"always",
"--all-targets",
"--no-default-features",
"--features",
"termion",
]
need_stdout = false need_stdout = false
[jobs.check-termwiz] [jobs.check-termwiz]
command = ["cargo", "check", "--color", "always", "--all-targets", "--no-default-features", "--features", "termwiz"] command = [
"cargo",
"check",
"--color",
"always",
"--all-targets",
"--no-default-features",
"--features",
"termwiz",
]
need_stdout = false need_stdout = false
[jobs.clippy] [jobs.clippy]
command = [ command = ["cargo", "clippy", "--all-targets", "--color", "always"]
"cargo", "clippy",
"--all-targets",
"--color", "always",
]
need_stdout = false need_stdout = false
[jobs.test] [jobs.test]
command = [ command = [
"cargo", "test", "cargo",
"test",
"--all-features", "--all-features",
"--color", "always", "--color",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 "always",
"--",
"--color",
"always", # see https://github.com/Canop/bacon/issues/124
] ]
need_stdout = true need_stdout = true
[jobs.test-unit] [jobs.test-unit]
command = [ command = [
"cargo", "test", "cargo",
"test",
"--lib", "--lib",
"--all-features", "--all-features",
"--color", "always", "--color",
"--", "--color", "always", # see https://github.com/Canop/bacon/issues/124 "always",
"--",
"--color",
"always", # see https://github.com/Canop/bacon/issues/124
] ]
need_stdout = true need_stdout = true
[jobs.doc] [jobs.doc]
command = [ command = [
"cargo", "+nightly", "doc", "cargo",
"-Zunstable-options", "-Zrustdoc-scrape-examples", "+nightly",
"doc",
"-Zunstable-options",
"-Zrustdoc-scrape-examples",
"--all-features", "--all-features",
"--color", "always", "--color",
"always",
"--no-deps", "--no-deps",
] ]
env.RUSTDOCFLAGS = "--cfg docsrs" env.RUSTDOCFLAGS = "--cfg docsrs"
@ -69,10 +111,14 @@ need_stdout = false
# to the previous job # to the previous job
[jobs.doc-open] [jobs.doc-open]
command = [ command = [
"cargo", "+nightly", "doc", "cargo",
"-Zunstable-options", "-Zrustdoc-scrape-examples", "+nightly",
"doc",
"-Zunstable-options",
"-Zrustdoc-scrape-examples",
"--all-features", "--all-features",
"--color", "always", "--color",
"always",
"--no-deps", "--no-deps",
"--open", "--open",
] ]
@ -82,19 +128,40 @@ on_success = "job:doc" # so that we don't open the browser at each change
[jobs.coverage] [jobs.coverage]
command = [ command = [
"cargo", "llvm-cov", "cargo",
"--lcov", "--output-path", "target/lcov.info", "llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--all-features", "--all-features",
"--color", "always", "--color",
"always",
] ]
[jobs.coverage-unit-tests-only] [jobs.coverage-unit-tests-only]
command = [ command = [
"cargo", "llvm-cov", "cargo",
"--lcov", "--output-path", "target/lcov.info", "llvm-cov",
"--lcov",
"--output-path",
"target/lcov.info",
"--lib", "--lib",
"--all-features", "--all-features",
"--color", "always", "--color",
"always",
]
[jobs.hack]
command = [
"cargo",
"hack",
"test",
"--lib",
"--each-feature",
# "--all-targets",
"--workspace",
"--color",
"always",
] ]
# You may define here keybindings that would be specific to # You may define here keybindings that would be specific to
@ -102,7 +169,7 @@ command = [
# Shortcuts to internal functions (scrolling, toggling, etc.) # Shortcuts to internal functions (scrolling, toggling, etc.)
# should go in your personal global prefs.toml file instead. # should go in your personal global prefs.toml file instead.
[keybindings] [keybindings]
# alt-m = "job:my-job" ctrl-h = "job:hack"
ctrl-c = "job:check-crossterm" ctrl-c = "job:check-crossterm"
ctrl-t = "job:check-termion" ctrl-t = "job:check-termion"
ctrl-w = "job:check-termwiz" ctrl-w = "job:check-termwiz"

View file

@ -29,16 +29,6 @@ underline-color = []
## This is useful if you want to save themes to a file. ## This is useful if you want to save themes to a file.
serde = ["dep:serde", "bitflags/serde", "compact_str/serde"] serde = ["dep:serde", "bitflags/serde", "compact_str/serde"]
## enable all unstable features.
unstable = ["unstable-widget-ref"]
## enables the [`WidgetRef`] and [`StatefulWidgetRef`] traits which are experimental and may change
## in the future.
##
## [`WidgetRef`]: widgets::WidgetRef
## [`StatefulWidgetRef`]: widgets::StatefulWidgetRef
unstable-widget-ref = []
[package.metadata.docs.rs] [package.metadata.docs.rs]
all-features = true all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
@ -48,7 +38,6 @@ bitflags = "2.3"
cassowary = "0.3" cassowary = "0.3"
compact_str = "0.8.0" compact_str = "0.8.0"
document-features = { workspace = true, optional = true } document-features = { workspace = true, optional = true }
instability.workspace = true
indoc.workspace = true indoc.workspace = true
itertools.workspace = true itertools.workspace = true
lru = "0.12.0" lru = "0.12.0"

View file

@ -501,80 +501,75 @@ mod tests {
} }
#[rstest] #[rstest]
#[case(ColorDebugKind::Foreground, Color::Black, ".black()")] #[case(Color::Black, ".black()")]
#[case(ColorDebugKind::Foreground, Color::Red, ".red()")] #[case(Color::Red, ".red()")]
#[case(ColorDebugKind::Foreground, Color::Green, ".green()")] #[case(Color::Green, ".green()")]
#[case(ColorDebugKind::Foreground, Color::Yellow, ".yellow()")] #[case(Color::Yellow, ".yellow()")]
#[case(ColorDebugKind::Foreground, Color::Blue, ".blue()")] #[case(Color::Blue, ".blue()")]
#[case(ColorDebugKind::Foreground, Color::Magenta, ".magenta()")] #[case(Color::Magenta, ".magenta()")]
#[case(ColorDebugKind::Foreground, Color::Cyan, ".cyan()")] #[case(Color::Cyan, ".cyan()")]
#[case(ColorDebugKind::Foreground, Color::Gray, ".gray()")] #[case(Color::Gray, ".gray()")]
#[case(ColorDebugKind::Foreground, Color::DarkGray, ".dark_gray()")] #[case(Color::DarkGray, ".dark_gray()")]
#[case(ColorDebugKind::Foreground, Color::LightRed, ".light_red()")] #[case(Color::LightRed, ".light_red()")]
#[case(ColorDebugKind::Foreground, Color::LightGreen, ".light_green()")] #[case(Color::LightGreen, ".light_green()")]
#[case(ColorDebugKind::Foreground, Color::LightYellow, ".light_yellow()")] #[case(Color::LightYellow, ".light_yellow()")]
#[case(ColorDebugKind::Foreground, Color::LightBlue, ".light_blue()")] #[case(Color::LightBlue, ".light_blue()")]
#[case(ColorDebugKind::Foreground, Color::LightMagenta, ".light_magenta()")] #[case(Color::LightMagenta, ".light_magenta()")]
#[case(ColorDebugKind::Foreground, Color::LightCyan, ".light_cyan()")] #[case(Color::LightCyan, ".light_cyan()")]
#[case(ColorDebugKind::Foreground, Color::White, ".white()")] #[case(Color::White, ".white()")]
#[case( #[case(Color::Indexed(10), ".fg(Color::Indexed(10))")]
ColorDebugKind::Foreground, #[case(Color::Rgb(255, 0, 0), ".fg(Color::Rgb(255, 0, 0))")]
Color::Indexed(10), fn stylize_debug_foreground(#[case] color: Color, #[case] expected: &str) {
".fg(Color::Indexed(10))" let debug = color.stylize_debug(ColorDebugKind::Foreground);
)] assert_eq!(format!("{debug:?}"), expected);
#[case( }
ColorDebugKind::Foreground,
Color::Rgb(255, 0, 0), #[rstest]
".fg(Color::Rgb(255, 0, 0))" #[case(Color::Black, ".on_black()")]
)] #[case(Color::Red, ".on_red()")]
#[case(ColorDebugKind::Background, Color::Black, ".on_black()")] #[case(Color::Green, ".on_green()")]
#[case(ColorDebugKind::Background, Color::Red, ".on_red()")] #[case(Color::Yellow, ".on_yellow()")]
#[case(ColorDebugKind::Background, Color::Green, ".on_green()")] #[case(Color::Blue, ".on_blue()")]
#[case(ColorDebugKind::Background, Color::Yellow, ".on_yellow()")] #[case(Color::Magenta, ".on_magenta()")]
#[case(ColorDebugKind::Background, Color::Blue, ".on_blue()")] #[case(Color::Cyan, ".on_cyan()")]
#[case(ColorDebugKind::Background, Color::Magenta, ".on_magenta()")] #[case(Color::Gray, ".on_gray()")]
#[case(ColorDebugKind::Background, Color::Cyan, ".on_cyan()")] #[case(Color::DarkGray, ".on_dark_gray()")]
#[case(ColorDebugKind::Background, Color::Gray, ".on_gray()")] #[case(Color::LightRed, ".on_light_red()")]
#[case(ColorDebugKind::Background, Color::DarkGray, ".on_dark_gray()")] #[case(Color::LightGreen, ".on_light_green()")]
#[case(ColorDebugKind::Background, Color::LightRed, ".on_light_red()")] #[case(Color::LightYellow, ".on_light_yellow()")]
#[case(ColorDebugKind::Background, Color::LightGreen, ".on_light_green()")] #[case(Color::LightBlue, ".on_light_blue()")]
#[case(ColorDebugKind::Background, Color::LightYellow, ".on_light_yellow()")] #[case(Color::LightMagenta, ".on_light_magenta()")]
#[case(ColorDebugKind::Background, Color::LightBlue, ".on_light_blue()")] #[case(Color::LightCyan, ".on_light_cyan()")]
#[case(ColorDebugKind::Background, Color::LightMagenta, ".on_light_magenta()")] #[case(Color::White, ".on_white()")]
#[case(ColorDebugKind::Background, Color::LightCyan, ".on_light_cyan()")] #[case(Color::Indexed(10), ".bg(Color::Indexed(10))")]
#[case(ColorDebugKind::Background, Color::White, ".on_white()")] #[case(Color::Rgb(255, 0, 0), ".bg(Color::Rgb(255, 0, 0))")]
#[case( fn stylize_debug_background(#[case] color: Color, #[case] expected: &str) {
ColorDebugKind::Background, let debug = color.stylize_debug(ColorDebugKind::Background);
Color::Indexed(10), assert_eq!(format!("{debug:?}"), expected);
".bg(Color::Indexed(10))" }
)]
#[case(
ColorDebugKind::Background,
Color::Rgb(255, 0, 0),
".bg(Color::Rgb(255, 0, 0))"
)]
#[cfg(feature = "underline-color")] #[cfg(feature = "underline-color")]
#[case( #[rstest]
ColorDebugKind::Underline, #[case(Color::Black, ".underline_color(Color::Black)")]
Color::Black, #[case(Color::Red, ".underline_color(Color::Red)")]
".underline_color(Color::Black)" #[case(Color::Green, ".underline_color(Color::Green)")]
)] #[case(Color::Yellow, ".underline_color(Color::Yellow)")]
#[cfg(feature = "underline-color")] #[case(Color::Blue, ".underline_color(Color::Blue)")]
#[case(ColorDebugKind::Underline, Color::Red, ".underline_color(Color::Red)")] #[case(Color::Magenta, ".underline_color(Color::Magenta)")]
#[cfg(feature = "underline-color")] #[case(Color::Cyan, ".underline_color(Color::Cyan)")]
#[case( #[case(Color::Gray, ".underline_color(Color::Gray)")]
ColorDebugKind::Underline, #[case(Color::DarkGray, ".underline_color(Color::DarkGray)")]
Color::Green, #[case(Color::LightRed, ".underline_color(Color::LightRed)")]
".underline_color(Color::Green)" #[case(Color::LightGreen, ".underline_color(Color::LightGreen)")]
)] #[case(Color::LightYellow, ".underline_color(Color::LightYellow)")]
#[cfg(feature = "underline-color")] #[case(Color::LightBlue, ".underline_color(Color::LightBlue)")]
#[case( #[case(Color::LightMagenta, ".underline_color(Color::LightMagenta)")]
ColorDebugKind::Underline, #[case(Color::LightCyan, ".underline_color(Color::LightCyan)")]
Color::Yellow, #[case(Color::White, ".underline_color(Color::White)")]
".underline_color(Color::Yellow)" #[case(Color::Indexed(10), ".underline_color(Color::Indexed(10))")]
)] #[case(Color::Rgb(255, 0, 0), ".underline_color(Color::Rgb(255, 0, 0))")]
fn stylize_debug(#[case] kind: ColorDebugKind, #[case] color: Color, #[case] expected: &str) { fn stylize_debug_underline(#[case] color: Color, #[case] expected: &str) {
let debug = color.stylize_debug(kind); let debug = color.stylize_debug(ColorDebugKind::Underline);
assert_eq!(format!("{debug:?}"), expected); assert_eq!(format!("{debug:?}"), expected);
} }
} }

View file

@ -9,7 +9,7 @@ use crate::{
layout::{Alignment, Rect}, layout::{Alignment, Rect},
style::{Style, Styled}, style::{Style, Styled},
text::{Span, StyledGrapheme, Text}, text::{Span, StyledGrapheme, Text},
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
/// A line of text, consisting of one or more [`Span`]s. /// A line of text, consisting of one or more [`Span`]s.
@ -683,21 +683,19 @@ impl<'a> Extend<Span<'a>> for Line<'a> {
impl Widget for Line<'_> { impl Widget for Line<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Line<'_> { impl Widget for &Line<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_with_alignment(area, buf, None); self.render_with_alignment(area, buf, None);
} }
} }
impl Line<'_> { impl Line<'_> {
/// An internal implementation method for `WidgetRef::render_ref` /// An internal implementation method for `Widget::render` that allows the parent widget to
/// /// define a default alignment, to be used if `Line::alignment` is `None`.
/// Allows the parent widget to define a default alignment, to be
/// used if `Line::alignment` is `None`.
pub(crate) fn render_with_alignment( pub(crate) fn render_with_alignment(
&self, &self,
area: Rect, area: Rect,
@ -749,7 +747,7 @@ fn render_spans(spans: &[Span], mut area: Rect, buf: &mut Buffer, span_skip_widt
if area.is_empty() { if area.is_empty() {
break; break;
} }
span.render_ref(area, buf); span.render(area, buf);
let span_width = u16::try_from(span_width).unwrap_or(u16::MAX); let span_width = u16::try_from(span_width).unwrap_or(u16::MAX);
area = area.indent_x(span_width); area = area.indent_x(span_width);
} }
@ -1321,7 +1319,7 @@ mod tests {
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する" "🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得する"
); );
let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1)); let mut buf = Buffer::empty(Rect::new(0, 0, 83, 1));
line.render_ref(buf.area, &mut buf); line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([ assert_eq!(buf, Buffer::with_lines([
"🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 " "🦀 RFC8628 OAuth 2.0 Device Authorization GrantでCLIからGithubのaccess tokenを取得 "
])); ]));
@ -1358,7 +1356,7 @@ mod tests {
) { ) {
let line = Line::from("1234🦀7890").alignment(alignment); let line = Line::from("1234🦀7890").alignment(alignment);
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1)); let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf); line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected])); assert_eq!(buf, Buffer::with_lines([expected]));
} }
@ -1411,7 +1409,7 @@ mod tests {
}; };
let line = Line::from(value).centered(); let line = Line::from(value).centered();
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1)); let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf); line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected])); assert_eq!(buf, Buffer::with_lines([expected]));
} }
@ -1429,7 +1427,7 @@ mod tests {
// Fill buffer with stuff to ensure the output is indeed padded // Fill buffer with stuff to ensure the output is indeed padded
let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::new("X")); let mut buf = Buffer::filled(Rect::new(0, 0, 10, 1), Cell::new("X"));
let area = Rect::new(2, 0, 6, 1); let area = Rect::new(2, 0, 6, 1);
line.render_ref(area, &mut buf); line.render(area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected])); assert_eq!(buf, Buffer::with_lines([expected]));
} }
@ -1447,7 +1445,7 @@ mod tests {
let area = Rect::new(0, 0, buf_width, 1); let area = Rect::new(0, 0, buf_width, 1);
// Fill buffer with stuff to ensure the output is indeed padded // Fill buffer with stuff to ensure the output is indeed padded
let mut buf = Buffer::filled(area, Cell::new("X")); let mut buf = Buffer::filled(area, Cell::new("X"));
line.render_ref(buf.area, &mut buf); line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected])); assert_eq!(buf, Buffer::with_lines([expected]));
} }
@ -1478,7 +1476,7 @@ mod tests {
fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) { fn render_truncates_flag(#[case] buf_width: u16, #[case] expected: &str) {
let line = Line::from("🇺🇸1234"); let line = Line::from("🇺🇸1234");
let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1)); let mut buf = Buffer::empty(Rect::new(0, 0, buf_width, 1));
line.render_ref(buf.area, &mut buf); line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected])); assert_eq!(buf, Buffer::with_lines([expected]));
} }
@ -1502,7 +1500,7 @@ mod tests {
assert!(line.width() >= min_width); assert!(line.width() >= min_width);
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1)); let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
line.render_ref(buf.area, &mut buf); line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected])); assert_eq!(buf, Buffer::with_lines([expected]));
} }
@ -1526,7 +1524,7 @@ mod tests {
assert!(line.width() >= min_width); assert!(line.width() >= min_width);
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1)); let mut buf = Buffer::empty(Rect::new(0, 0, 32, 1));
line.render_ref(buf.area, &mut buf); line.render(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([expected])); assert_eq!(buf, Buffer::with_lines([expected]));
} }

View file

@ -8,7 +8,7 @@ use crate::{
layout::Rect, layout::Rect,
style::{Style, Styled}, style::{Style, Styled},
text::{Line, StyledGrapheme}, text::{Line, StyledGrapheme},
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
/// Represents a part of a line that is contiguous and where all characters share the same style. /// Represents a part of a line that is contiguous and where all characters share the same style.
@ -418,12 +418,12 @@ impl<'a> Styled for Span<'a> {
impl Widget for Span<'_> { impl Widget for Span<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Span<'_> { impl Widget for &Span<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area); let area = area.intersection(buf.area);
if area.is_empty() { if area.is_empty() {
return; return;

View file

@ -6,7 +6,7 @@ use crate::{
layout::{Alignment, Rect}, layout::{Alignment, Rect},
style::{Style, Styled}, style::{Style, Styled},
text::{Line, Span}, text::{Line, Span},
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
/// A string split over one or more lines. /// A string split over one or more lines.
@ -730,12 +730,12 @@ impl fmt::Display for Text<'_> {
impl Widget for Text<'_> { impl Widget for Text<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf)
} }
} }
impl WidgetRef for Text<'_> { impl Widget for &Text<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area); let area = area.intersection(buf.area);
buf.set_style(area, self.style); buf.set_style(area, self.style);
for (line, line_area) in self.iter().zip(area.rows()) { for (line, line_area) in self.iter().zip(area.rows()) {

View file

@ -3,10 +3,6 @@
//! render UI elements on the screen. //! render UI elements on the screen.
pub use self::{stateful_widget::StatefulWidget, widget::Widget}; pub use self::{stateful_widget::StatefulWidget, widget::Widget};
#[instability::unstable(feature = "widget-ref")]
pub use self::{stateful_widget_ref::StatefulWidgetRef, widget_ref::WidgetRef};
mod stateful_widget; mod stateful_widget;
mod stateful_widget_ref;
mod widget; mod widget;
mod widget_ref;

View file

@ -1,5 +1,4 @@
use super::WidgetRef; use crate::{buffer::Buffer, layout::Rect, style::Style};
use crate::{buffer::Buffer, layout::Rect};
/// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`]. /// A `Widget` is a type that can be drawn on a [`Buffer`] in a given [`Rect`].
/// ///
@ -11,7 +10,7 @@ use crate::{buffer::Buffer, layout::Rect};
/// themselves. This allows you to store a reference to a widget and render it later. Widget crates /// themselves. This allows you to store a reference to a widget and render it later. Widget crates
/// should consider also doing this to allow for more flexibility in how widgets are used. /// should consider also doing this to allow for more flexibility in how widgets are used.
/// ///
/// In Ratatui 0.26.0, we also added an unstable [`WidgetRef`] trait and implemented this on all the /// In Ratatui 0.26.0, we also added an unstable `WidgetRef` trait and implemented this on all the
/// internal widgets. In addition to the above benefit of rendering references to widgets, this also /// internal widgets. In addition to the above benefit of rendering references to widgets, this also
/// allows you to render boxed widgets. This is useful when you want to store a collection of /// allows you to render boxed widgets. This is useful when you want to store a collection of
/// widgets with different types. You can then iterate over the collection and render each widget. /// widgets with different types. You can then iterate over the collection and render each widget.
@ -72,7 +71,7 @@ pub trait Widget {
/// drawing the text to the screen. /// drawing the text to the screen.
impl Widget for &str { impl Widget for &str {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
} }
} }
@ -82,7 +81,15 @@ impl Widget for &str {
/// on a [`Buffer`] within the bounds of a given [`Rect`]. /// on a [`Buffer`] within the bounds of a given [`Rect`].
impl Widget for String { impl Widget for String {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); buf.set_stringn(area.x, area.y, self, area.width as usize, Style::new());
}
}
impl<W: Widget> Widget for Option<W> {
fn render(self, area: Rect, buf: &mut Buffer) {
if let Some(widget) = self {
widget.render(area, buf);
}
} }
} }

View file

@ -15,9 +15,6 @@ edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
[features] [features]
## enables `unstable-widget-ref` as default
default = ["unstable-widget-ref"]
## enables serialization and deserialization of style and color types using the [`serde`] crate. ## enables serialization and deserialization of style and color types using the [`serde`] crate.
## This is useful if you want to save themes to a file. ## This is useful if you want to save themes to a file.
serde = ["dep:serde", "ratatui-core/serde"] serde = ["dep:serde", "ratatui-core/serde"]
@ -32,11 +29,7 @@ all-widgets = ["calendar"]
calendar = ["dep:time"] calendar = ["dep:time"]
## Enable all unstable features. ## Enable all unstable features.
unstable = ["unstable-rendered-line-info", "unstable-widget-ref"] unstable = ["unstable-rendered-line-info"]
## enables the [`WidgetRef`](ratatui_core::widgets::WidgetRef) and
## [`StatefulWidgetRef`](ratatui_core::widgets::StatefulWidgetRef) traits which are experimental and may change
unstable-widget-ref = ["ratatui-core/unstable-widget-ref"]
## Enables the [`Paragraph::line_count`](paragraph::Paragraph::line_count) ## Enables the [`Paragraph::line_count`](paragraph::Paragraph::line_count)
## [`Paragraph::line_width`](paragraph::Paragraph::line_width) methods ## [`Paragraph::line_width`](paragraph::Paragraph::line_width) methods

View file

@ -6,7 +6,7 @@ use ratatui_core::{
style::{Style, Styled}, style::{Style, Styled},
symbols::{self}, symbols::{self},
text::Line, text::Line,
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
pub use self::{bar::Bar, bar_group::BarGroup}; pub use self::{bar::Bar, bar_group::BarGroup};
@ -600,15 +600,15 @@ impl BarChart<'_> {
impl Widget for BarChart<'_> { impl Widget for BarChart<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for BarChart<'_> { impl Widget for &BarChart<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area); let inner = self.block.inner_if_some(area);
if inner.is_empty() || self.data.is_empty() || self.bar_width == 0 { if inner.is_empty() || self.data.is_empty() || self.bar_width == 0 {

View file

@ -12,7 +12,7 @@ use ratatui_core::{
style::{Style, Styled}, style::{Style, Styled},
symbols::border, symbols::border,
text::Line, text::Line,
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
pub use self::{ pub use self::{
@ -603,12 +603,12 @@ impl<'a> Block<'a> {
impl Widget for Block<'_> { impl Widget for Block<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Block<'_> { impl Widget for &Block<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let area = area.intersection(buf.area); let area = area.intersection(buf.area);
if area.is_empty() { if area.is_empty() {
return; return;
@ -744,7 +744,7 @@ impl Block<'_> {
..titles_area ..titles_area
}; };
buf.set_style(title_area, self.titles_style); buf.set_style(title_area, self.titles_style);
title.render_ref(title_area, buf); title.render(title_area, buf);
// bump the width of the titles area to the left // bump the width of the titles area to the left
titles_area.width = titles_area titles_area.width = titles_area
@ -785,7 +785,7 @@ impl Block<'_> {
..titles_area ..titles_area
}; };
buf.set_style(title_area, self.titles_style); buf.set_style(title_area, self.titles_style);
title.render_ref(title_area, buf); title.render(title_area, buf);
// bump the titles area to the right and reduce its width // bump the titles area to the right and reduce its width
titles_area.x = titles_area.x.saturating_add(title_width + 1); titles_area.x = titles_area.x.saturating_add(title_width + 1);
@ -808,7 +808,7 @@ impl Block<'_> {
..titles_area ..titles_area
}; };
buf.set_style(title_area, self.titles_style); buf.set_style(title_area, self.titles_style);
title.render_ref(title_area, buf); title.render(title_area, buf);
// bump the titles area to the right and reduce its width // bump the titles area to the right and reduce its width
titles_area.x = titles_area.x.saturating_add(title_width + 1); titles_area.x = titles_area.x.saturating_add(title_width + 1);

View file

@ -15,7 +15,7 @@ use ratatui_core::{
layout::{Alignment, Constraint, Layout, Rect}, layout::{Alignment, Constraint, Layout, Rect},
style::Style, style::Style,
text::{Line, Span}, text::{Line, Span},
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
use time::{Date, Duration, OffsetDateTime}; use time::{Date, Duration, OffsetDateTime};
@ -136,13 +136,13 @@ impl<'a, DS: DateStyler> Monthly<'a, DS> {
impl<DS: DateStyler> Widget for Monthly<'_, DS> { impl<DS: DateStyler> Widget for Monthly<'_, DS> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl<DS: DateStyler> WidgetRef for Monthly<'_, DS> { impl<DS: DateStyler> Widget for &Monthly<'_, DS> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area); let inner = self.block.inner_if_some(area);
self.render_monthly(inner, buf); self.render_monthly(inner, buf);
} }

View file

@ -22,7 +22,7 @@ use ratatui_core::{
style::{Color, Style}, style::{Color, Style},
symbols::{self, Marker}, symbols::{self, Marker},
text::Line as TextLine, text::Line as TextLine,
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
pub use self::{ pub use self::{
@ -747,16 +747,16 @@ where
F: Fn(&mut Context), F: Fn(&mut Context),
{ {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl<F> WidgetRef for Canvas<'_, F> impl<F> Widget for &Canvas<'_, F>
where where
F: Fn(&mut Context), F: Fn(&mut Context),
{ {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let canvas_area = self.block.inner_if_some(area); let canvas_area = self.block.inner_if_some(area);
if canvas_area.is_empty() { if canvas_area.is_empty() {
return; return;

View file

@ -7,7 +7,7 @@ use ratatui_core::{
style::{Color, Style, Styled}, style::{Color, Style, Styled},
symbols::{self}, symbols::{self},
text::Line, text::Line,
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
use strum::{Display, EnumString}; use strum::{Display, EnumString};
@ -971,16 +971,16 @@ impl<'a> Chart<'a> {
impl Widget for Chart<'_> { impl Widget for Chart<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Chart<'_> { impl Widget for &Chart<'_> {
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let chart_area = self.block.inner_if_some(area); let chart_area = self.block.inner_if_some(area);
let Some(layout) = self.layout(chart_area) else { let Some(layout) = self.layout(chart_area) else {
return; return;

View file

@ -1,9 +1,5 @@
//! The [`Clear`] widget allows you to clear a certain area to allow overdrawing (e.g. for popups). //! The [`Clear`] widget allows you to clear a certain area to allow overdrawing (e.g. for popups).
use ratatui_core::{ use ratatui_core::{buffer::Buffer, layout::Rect, widgets::Widget};
buffer::Buffer,
layout::Rect,
widgets::{Widget, WidgetRef},
};
/// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups). /// A widget to clear/reset a certain area to allow overdrawing (e.g. for popups).
/// ///
@ -35,12 +31,12 @@ pub struct Clear;
impl Widget for Clear { impl Widget for Clear {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Clear { impl Widget for &Clear {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
for x in area.left()..area.right() { for x in area.left()..area.right() {
for y in area.top()..area.bottom() { for y in area.top()..area.bottom() {
buf[(x, y)].reset(); buf[(x, y)].reset();

View file

@ -5,7 +5,7 @@ use ratatui_core::{
style::{Color, Style, Styled}, style::{Color, Style, Styled},
symbols::{self}, symbols::{self},
text::{Line, Span}, text::{Line, Span},
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
use crate::block::{Block, BlockExt}; use crate::block::{Block, BlockExt};
@ -153,14 +153,14 @@ impl<'a> Gauge<'a> {
impl Widget for Gauge<'_> { impl Widget for Gauge<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Gauge<'_> { impl Widget for &Gauge<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area); let inner = self.block.inner_if_some(area);
self.render_gauge(inner, buf); self.render_gauge(inner, buf);
} }
@ -386,14 +386,14 @@ impl<'a> LineGauge<'a> {
impl Widget for LineGauge<'_> { impl Widget for LineGauge<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for LineGauge<'_> { impl Widget for &LineGauge<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let gauge_area = self.block.inner_if_some(area); let gauge_area = self.block.inner_if_some(area);
if gauge_area.is_empty() { if gauge_area.is_empty() {
return; return;

View file

@ -1,7 +1,7 @@
use ratatui_core::{ use ratatui_core::{
buffer::Buffer, buffer::Buffer,
layout::Rect, layout::Rect,
widgets::{StatefulWidget, StatefulWidgetRef, Widget, WidgetRef}, widgets::{StatefulWidget, Widget},
}; };
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -12,14 +12,14 @@ use crate::{
impl Widget for List<'_> { impl Widget for List<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
WidgetRef::render_ref(&self, area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for List<'_> { impl Widget for &List<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = ListState::default(); let mut state = ListState::default();
StatefulWidgetRef::render_ref(self, area, buf, &mut state); StatefulWidget::render(self, area, buf, &mut state);
} }
} }
@ -27,24 +27,16 @@ impl StatefulWidget for List<'_> {
type State = ListState; type State = ListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidgetRef::render_ref(&self, area, buf, state); StatefulWidget::render(&self, area, buf, state);
} }
} }
// Note: remove this when StatefulWidgetRef is stabilized and replace with the blanket impl
impl StatefulWidget for &List<'_> { impl StatefulWidget for &List<'_> {
type State = ListState; type State = ListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidgetRef::render_ref(self, area, buf, state);
}
}
impl StatefulWidgetRef for List<'_> {
type State = ListState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let list_area = self.block.inner_if_some(area); let list_area = self.block.inner_if_some(area);
if list_area.is_empty() { if list_area.is_empty() {
@ -113,7 +105,7 @@ impl StatefulWidgetRef for List<'_> {
} else { } else {
row_area row_area
}; };
item.content.render_ref(item_area, buf); Widget::render(&item.content, item_area, buf);
for j in 0..item.content.height() { for j in 0..item.content.height() {
// if the item is selected, we need to display the highlight symbol: // if the item is selected, we need to display the highlight symbol:

View file

@ -5,7 +5,7 @@ use ratatui_core::{
layout::{Alignment, Position, Rect}, layout::{Alignment, Position, Rect},
style::{Style, Styled}, style::{Style, Styled},
text::{Line, StyledGrapheme, Text}, text::{Line, StyledGrapheme, Text},
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
@ -419,14 +419,14 @@ impl<'a> Paragraph<'a> {
impl Widget for Paragraph<'_> { impl Widget for Paragraph<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Paragraph<'_> { impl Widget for &Paragraph<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area); let inner = self.block.inner_if_some(area);
self.render_paragraph(inner, buf); self.render_paragraph(inner, buf);
} }

View file

@ -6,7 +6,7 @@ use ratatui_core::{
layout::Rect, layout::Rect,
style::{Style, Styled}, style::{Style, Styled},
symbols::{self}, symbols::{self},
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
use strum::{Display, EnumString}; use strum::{Display, EnumString};
@ -326,13 +326,13 @@ impl<'a> Styled for Sparkline<'a> {
impl Widget for Sparkline<'_> { impl Widget for Sparkline<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Sparkline<'_> { impl Widget for &Sparkline<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area); let inner = self.block.inner_if_some(area);
self.render_sparkline(inner, buf); self.render_sparkline(inner, buf);
} }

View file

@ -7,7 +7,7 @@ use ratatui_core::{
layout::{Constraint, Flex, Layout, Rect}, layout::{Constraint, Flex, Layout, Rect},
style::{Style, Styled}, style::{Style, Styled},
text::Text, text::Text,
widgets::{StatefulWidget, StatefulWidgetRef, Widget, WidgetRef}, widgets::{StatefulWidget, Widget},
}; };
pub use self::{cell::Cell, highlight_spacing::HighlightSpacing, row::Row, state::TableState}; pub use self::{cell::Cell, highlight_spacing::HighlightSpacing, row::Row, state::TableState};
@ -752,12 +752,12 @@ impl<'a> Table<'a> {
impl Widget for Table<'_> { impl Widget for Table<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
WidgetRef::render_ref(&self, area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Table<'_> { impl Widget for &Table<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default(); let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state); StatefulWidget::render(self, area, buf, &mut state);
} }
@ -771,20 +771,12 @@ impl StatefulWidget for Table<'_> {
} }
} }
// Note: remove this when StatefulWidgetRef is stabilized and replace with the blanket impl
impl StatefulWidget for &Table<'_> { impl StatefulWidget for &Table<'_> {
type State = TableState; type State = TableState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
StatefulWidgetRef::render_ref(self, area, buf, state);
}
}
impl StatefulWidgetRef for Table<'_> {
type State = TableState;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let table_area = self.block.inner_if_some(area); let table_area = self.block.inner_if_some(area);
if table_area.is_empty() { if table_area.is_empty() {
return; return;
@ -910,7 +902,7 @@ impl Table<'_> {
..row_area ..row_area
}; };
buf.set_style(selection_area, row.style); buf.set_style(selection_area, row.style);
highlight_symbol.render_ref(selection_area, buf); highlight_symbol.render(selection_area, buf);
}; };
for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) { for ((x, width), cell) in columns_widths.iter().zip(row.cells.iter()) {
cell.render( cell.render(

View file

@ -3,7 +3,7 @@ use ratatui_core::{
layout::Rect, layout::Rect,
style::{Style, Styled}, style::{Style, Styled},
text::Text, text::Text,
widgets::WidgetRef, widgets::Widget,
}; };
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
@ -163,7 +163,7 @@ impl<'a> Cell<'a> {
impl Cell<'_> { impl Cell<'_> {
pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) { pub(crate) fn render(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.content.render_ref(area, buf); Widget::render(&self.content, area, buf);
} }
} }

View file

@ -6,7 +6,7 @@ use ratatui_core::{
style::{Modifier, Style, Styled}, style::{Modifier, Style, Styled},
symbols::{self}, symbols::{self},
text::{Line, Span}, text::{Line, Span},
widgets::{Widget, WidgetRef}, widgets::Widget,
}; };
use crate::block::{Block, BlockExt}; use crate::block::{Block, BlockExt};
@ -364,14 +364,14 @@ impl<'a> Styled for Tabs<'a> {
impl Widget for Tabs<'_> { impl Widget for Tabs<'_> {
fn render(self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf); Widget::render(&self, area, buf);
} }
} }
impl WidgetRef for Tabs<'_> { impl Widget for &Tabs<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style); buf.set_style(area, self.style);
self.block.render_ref(area, buf); self.block.as_ref().render(area, buf);
let inner = self.block.inner_if_some(area); let inner = self.block.inner_if_some(area);
self.render_tabs(inner, buf); self.render_tabs(inner, buf);
} }

View file

@ -21,8 +21,8 @@ indoc = "2"
instability.workspace = true instability.workspace = true
itertools.workspace = true itertools.workspace = true
palette = { version = "0.7.6", optional = true } palette = { version = "0.7.6", optional = true }
ratatui-core = { workspace = true, features = ["unstable-widget-ref"] } ratatui-core = { workspace = true }
ratatui-widgets = { workspace = true, features = ["unstable-widget-ref"] } ratatui-widgets = { workspace = true }
serde = { workspace = true, optional = true } serde = { workspace = true, optional = true }
strum.workspace = true strum.workspace = true
termwiz = { version = "0.22.0", optional = true } termwiz = { version = "0.22.0", optional = true }
@ -163,7 +163,7 @@ unstable-rendered-line-info = ["ratatui-widgets/unstable-rendered-line-info"]
## ##
## [`WidgetRef`]: widgets::WidgetRef ## [`WidgetRef`]: widgets::WidgetRef
## [`StatefulWidgetRef`]: widgets::StatefulWidgetRef ## [`StatefulWidgetRef`]: widgets::StatefulWidgetRef
unstable-widget-ref = ["ratatui-core/unstable-widget-ref"] unstable-widget-ref = []
## Enables getting access to backends' writers. ## Enables getting access to backends' writers.
unstable-backend-writer = [] unstable-backend-writer = []

View file

@ -113,7 +113,7 @@ impl Frame<'_> {
/// ///
/// let block = Block::new(); /// let block = Block::new();
/// let area = Rect::new(0, 0, 5, 5); /// let area = Rect::new(0, 0, 5, 5);
/// frame.render_widget_ref(block, area); /// frame.render_widget_ref(&block, area);
/// # } /// # }
/// ``` /// ```
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
@ -181,7 +181,7 @@ impl Frame<'_> {
/// let mut state = ListState::default().with_selected(Some(1)); /// let mut state = ListState::default().with_selected(Some(1));
/// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]); /// let list = List::new(vec![ListItem::new("Item 1"), ListItem::new("Item 2")]);
/// let area = Rect::new(0, 0, 5, 5); /// let area = Rect::new(0, 0, 5, 5);
/// frame.render_stateful_widget_ref(list, area, &mut state); /// frame.render_stateful_widget_ref(&list, area, &mut state);
/// # } /// # }
/// ``` /// ```
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]

View file

@ -29,8 +29,6 @@
//! [`Canvas`]: crate::widgets::canvas::Canvas //! [`Canvas`]: crate::widgets::canvas::Canvas
pub use ratatui_core::widgets::{StatefulWidget, Widget}; pub use ratatui_core::widgets::{StatefulWidget, Widget};
#[instability::unstable(feature = "widget-ref")]
pub use ratatui_core::widgets::{StatefulWidgetRef, WidgetRef};
// TODO remove this module once title etc. are gone // TODO remove this module once title etc. are gone
pub use ratatui_widgets::block; pub use ratatui_widgets::block;
#[cfg(feature = "widget-calendar")] #[cfg(feature = "widget-calendar")]
@ -51,3 +49,8 @@ pub use ratatui_widgets::{
table::{Cell, HighlightSpacing, Row, Table, TableState}, table::{Cell, HighlightSpacing, Row, Table, TableState},
tabs::Tabs, tabs::Tabs,
}; };
#[instability::unstable(feature = "widget-ref")]
pub use {stateful_widget_ref::StatefulWidgetRef, widget_ref::WidgetRef};
mod stateful_widget_ref;
mod widget_ref;

View file

@ -1,31 +1,34 @@
use ratatui_core::widgets::StatefulWidget;
use crate::{buffer::Buffer, layout::Rect}; use crate::{buffer::Buffer, layout::Rect};
/// A `StatefulWidgetRef` is a trait that allows rendering a stateful widget by reference. /// A `StatefulWidgetRef` is a trait that allows rendering a stateful widget by reference.
/// ///
/// This is the stateful equivalent of `WidgetRef`. It is useful when you want to store a reference /// This is the stateful equivalent of `WidgetRef`. It is useful when you need to store a reference
/// to a stateful widget and render it later. It also allows you to render boxed stateful widgets. /// to a stateful widget and render it later. It also allows you to render boxed stateful widgets.
/// ///
/// This trait was introduced in Ratatui 0.26.0 and is implemented for all the internal stateful /// This trait was introduced in Ratatui 0.26.0. It is currently marked as unstable as we are still
/// widgets. It is currently marked as unstable as we are still evaluating the API and may make /// evaluating the API and may make changes in the future. See
/// changes in the future. See <https://github.com/ratatui/ratatui/issues/1287> for more /// <https://github.com/ratatui/ratatui/issues/1287> for more information.
/// information.
/// ///
/// A blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef` /// A blanket implementation of `StatefulWidgetRef` for `&W` where `W` implements `StatefulWidget`
/// is provided. /// is provided. Most of the time you will want to implement `StatefulWidget` against a reference to
/// the widget instead of implementing `StatefulWidgetRef` directly.
/// ///
/// See the documentation for [`WidgetRef`] for more information on boxed widgets. /// See the documentation for [`WidgetRef`] for more information on boxed widgets. See the
/// See the documentation for [`StatefulWidget`] for more information on stateful widgets. /// documentation for [`StatefulWidget`] for more information on stateful widgets.
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] { /// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui::widgets::StatefulWidgetRef;
/// use ratatui_core::{ /// use ratatui_core::{
/// buffer::Buffer, /// buffer::Buffer,
/// layout::Rect, /// layout::Rect,
/// style::Stylize, /// style::Stylize,
/// text::Line, /// text::Line,
/// widgets::{StatefulWidget, StatefulWidgetRef, Widget}, /// widgets::{StatefulWidget, Widget},
/// }; /// };
/// ///
/// struct PersonalGreeting; /// struct PersonalGreeting;
@ -64,20 +67,18 @@ pub trait StatefulWidgetRef {
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State); fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State);
} }
// Note: while StatefulWidgetRef is marked as unstable, the blanket implementation of StatefulWidget /// Blanket implementation of `StatefulWidgetRef` for `&W` where `W` implements `StatefulWidget`.
// cannot be implemented as W::State is effectively pub(crate) and not accessible from outside the ///
// crate. Once stabilized, this blanket implementation can be added and the specific implementations /// This allows you to render a stateful widget by reference.
// on Table and List can be removed. impl<W, State> StatefulWidgetRef for &W
// where
// /// Blanket implementation of `StatefulWidget` for `&W` where `W` implements `StatefulWidgetRef`. for<'a> &'a W: StatefulWidget<State = State>,
// /// {
// /// This allows you to render a stateful widget by reference. type State = State;
// impl<W: StatefulWidgetRef> StatefulWidget for &W { fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
// type State = W::State; self.render(area, buf, state);
// fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { }
// StatefulWidgetRef::render_ref(self, area, buf, state); }
// }
// }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -98,35 +99,23 @@ mod tests {
struct PersonalGreeting; struct PersonalGreeting;
impl StatefulWidgetRef for PersonalGreeting { impl StatefulWidget for &PersonalGreeting {
type State = String; type State = String;
fn render_ref(&self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
Line::from(format!("Hello {state}")).render(area, buf); Line::from(format!("Hello {state}")).render(area, buf);
} }
} }
#[rstest] #[rstest]
fn render_ref(mut buf: Buffer, mut state: String) { fn render_ref(mut buf: Buffer, mut state: String) {
let widget = PersonalGreeting; let widget = &PersonalGreeting;
widget.render_ref(buf.area, &mut buf, &mut state); widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "])); assert_eq!(buf, Buffer::with_lines(["Hello world "]));
} }
// Note this cannot be tested until the blanket implementation of StatefulWidget for &W
// where W implements StatefulWidgetRef is added. (see the comment in the blanket
// implementation for more).
// /// This test is to ensure that the blanket implementation of `StatefulWidget` for `&W`
// where /// `W` implements `StatefulWidgetRef` works as expected.
// #[rstest]
// fn stateful_widget_blanket_render(mut buf: Buffer, mut state: String) {
// let widget = &PersonalGreeting;
// widget.render(buf.area, &mut buf, &mut state);
// assert_eq!(buf, Buffer::with_lines(["Hello world "]));
// }
#[rstest] #[rstest]
fn box_render_render(mut buf: Buffer, mut state: String) { fn box_render_ref(mut buf: Buffer, mut state: String) {
let widget = Box::new(PersonalGreeting); let widget = Box::new(&PersonalGreeting);
widget.render_ref(buf.area, &mut buf, &mut state); widget.render_ref(buf.area, &mut buf, &mut state);
assert_eq!(buf, Buffer::with_lines(["Hello world "])); assert_eq!(buf, Buffer::with_lines(["Hello world "]));
} }

View file

@ -24,12 +24,8 @@ use crate::{buffer::Buffer, layout::Rect, style::Style};
/// ///
/// ```rust /// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] { /// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui_core::{ /// use ratatui::widgets::WidgetRef;
/// buffer::Buffer, /// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
/// layout::Rect,
/// text::Line,
/// widgets::{Widget, WidgetRef},
/// };
/// ///
/// struct Greeting; /// struct Greeting;
/// ///
@ -85,9 +81,12 @@ pub trait WidgetRef {
} }
/// This allows you to render a widget by reference. /// This allows you to render a widget by reference.
impl<W: WidgetRef> Widget for &W { impl<W> WidgetRef for &W
fn render(self, area: Rect, buf: &mut Buffer) { where
self.render_ref(area, buf); for<'a> &'a W: Widget,
{
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
self.render(area, buf);
} }
} }
@ -127,12 +126,8 @@ impl WidgetRef for String {
/// ///
/// ```rust /// ```rust
/// # #[cfg(feature = "unstable-widget-ref")] { /// # #[cfg(feature = "unstable-widget-ref")] {
/// use ratatui_core::{ /// use ratatui::widgets::WidgetRef;
/// buffer::Buffer, /// use ratatui_core::{buffer::Buffer, layout::Rect, text::Line, widgets::Widget};
/// layout::Rect,
/// text::Line,
/// widgets::{Widget, WidgetRef},
/// };
/// ///
/// struct Parent { /// struct Parent {
/// child: Option<Child>, /// child: Option<Child>,
@ -177,44 +172,37 @@ mod tests {
struct Farewell; struct Farewell;
impl WidgetRef for Greeting { impl Widget for &Greeting {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
Line::from("Hello").render(area, buf); Line::from("Hello").render(area, buf);
} }
} }
impl WidgetRef for Farewell { impl Widget for &Farewell {
fn render_ref(&self, area: Rect, buf: &mut Buffer) { fn render(self, area: Rect, buf: &mut Buffer) {
Line::from("Goodbye").right_aligned().render(area, buf); Line::from("Goodbye").right_aligned().render(area, buf);
} }
} }
/// Ensure that the blanket implementation of `WidgetRef` for `&W` where `W` implements
/// `Widget` works as expected.
#[rstest] #[rstest]
fn render_ref(mut buf: Buffer) { fn render_ref(mut buf: Buffer) {
let widget = Greeting;
widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "]));
}
/// Ensure that the blanket implementation of `Widget` for `&W` where `W` implements
/// `WidgetRef` works as expected.
#[rstest]
fn render_widget(mut buf: Buffer) {
let widget = &Greeting; let widget = &Greeting;
widget.render(buf.area, &mut buf); widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "])); assert_eq!(buf, Buffer::with_lines(["Hello "]));
} }
#[rstest] #[rstest]
fn render_ref_box(mut buf: Buffer) { fn render_ref_box(mut buf: Buffer) {
let widget: Box<dyn WidgetRef> = Box::new(Greeting); let widget: Box<dyn WidgetRef> = Box::new(&Greeting);
widget.render_ref(buf.area, &mut buf); widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "])); assert_eq!(buf, Buffer::with_lines(["Hello "]));
} }
#[rstest] #[rstest]
fn render_ref_box_vec(mut buf: Buffer) { fn render_ref_box_vec(mut buf: Buffer) {
let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(Greeting), Box::new(Farewell)]; let widgets: Vec<Box<dyn WidgetRef>> = vec![Box::new(&Greeting), Box::new(&Farewell)];
for widget in widgets { for widget in widgets {
widget.render_ref(buf.area, &mut buf); widget.render_ref(buf.area, &mut buf);
} }
@ -223,14 +211,14 @@ mod tests {
#[rstest] #[rstest]
fn render_ref_some(mut buf: Buffer) { fn render_ref_some(mut buf: Buffer) {
let widget = Some(Greeting); let widget = Some(&Greeting);
widget.render_ref(buf.area, &mut buf); widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines(["Hello "])); assert_eq!(buf, Buffer::with_lines(["Hello "]));
} }
#[rstest] #[rstest]
fn render_ref_none(mut buf: Buffer) { fn render_ref_none(mut buf: Buffer) {
let widget: Option<Greeting> = None; let widget: Option<&Greeting> = None;
widget.render_ref(buf.area, &mut buf); widget.render_ref(buf.area, &mut buf);
assert_eq!(buf, Buffer::with_lines([" "])); assert_eq!(buf, Buffer::with_lines([" "]));
} }