Lifetime woes implementing ratatui::Widget for a reference

I have this helper struct:

use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::prelude::{Style, Text, Widget};
use ratatui::widgets::Paragraph;

/// A table.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Table<const COLS: usize, const ROWS: usize, T> {
    elements: [[T; COLS]; ROWS],
}

impl<const COLS: usize, const ROWS: usize, T> From<[[T; COLS]; ROWS]> for Table<COLS, ROWS, T> {
    fn from(elements: [[T; COLS]; ROWS]) -> Self {
        Self { elements }
    }
}

impl<'a, const COLS: usize, const ROWS: usize, T> Widget for &'a Table<COLS, ROWS, T>
where
    &'a T: Into<Text<'a>>,
{
    fn render(self, area: Rect, buf: &mut Buffer) {
        // Build 3 rows inside inner block
        let rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints(
                [Constraint::Percentage(
                    u16::try_from(100usize / ROWS).expect("Percentage always fits."),
                ); ROWS],
            )
            .split(area);

        for (row_area, row) in rows.iter().zip(&self.elements) {
            let cols = Layout::default()
                .direction(Direction::Horizontal)
                .constraints(
                    [Constraint::Percentage(
                        u16::try_from(100usize / COLS).expect("Percentage always fits."),
                    ); COLS],
                )
                .split(*row_area);

            for (cell_area, cell) in cols.iter().zip(row) {
                let cell = Paragraph::new(cell)
                    .style(Style::default())
                    .alignment(Alignment::Center);

                cell.render(*cell_area, buf);
            }
        }
    }`
}

However, I cannot seem to get the lifetime constraints right on impl Widget:

   Compiling uml-tui v0.1.0 (/home/rne/Projekte/usb-missile-launcher/tui)
error[E0599]: the method `render` exists for struct `table::Table<3, 3, &str>`, but its trait bounds were not satisfied
   --> tui/src/app.rs:131:76
    |
131 |         Table::from([["", "^", ""], ["<", "<Enter>", ">"], ["", "v", ""]]).render(inner, buf);
    |                                                                            ^^^^^^ method cannot be called on `table::Table<3, 3, &str>` due to unsatisfied trait bounds
    |
   ::: tui/src/table.rs:8:1
    |
  8 | pub struct Table<const COLS: usize, const ROWS: usize, T> {
    | --------------------------------------------------------- method `render` not found for this struct
    |
note: trait bound `&&str: Into<Text<'_>>` was not satisfied
   --> tui/src/table.rs:20:12
    |
 18 | impl<'a, const COLS: usize, const ROWS: usize, T> Widget for &'a Table<COLS, ROWS, T>
    |                                                   ------     ------------------------
 19 | where
 20 |     &'a T: Into<Text<'a>>,
    |            ^^^^^^^^^^^^^^ unsatisfied trait bound introduced here
    = help: items from traits can only be used if the trait is implemented and in scope
    = note: the following traits define an item `render`, perhaps you need to implement one of them:
            candidate #1: `Widget`
            candidate #2: `ratatui::prelude::StatefulWidget`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `uml-tui` (bin "uml-tui") due to 1 previous error

I also tried using HRTBs:


impl<'a, const COLS: usize, const ROWS: usize, T> Widget for &'a Table<COLS, ROWS, T>
where
    for<'b> &'b T: Into<Text<'a>>,
{
    fn render(self, area: Rect, buf: &mut Buffer) {
        // Build 3 rows inside inner block
        let rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints(
                [Constraint::Percentage(
                    u16::try_from(100usize / ROWS).expect("Percentage always fits."),
                ); ROWS],
            )
            .split(area);

        for (row_area, row) in rows.iter().zip(&self.elements) {
            let cols = Layout::default()
                .direction(Direction::Horizontal)
                .constraints(
                    [Constraint::Percentage(
                        u16::try_from(100usize / COLS).expect("Percentage always fits."),
                    ); COLS],
                )
                .split(*row_area);

            for (cell_area, cell) in cols.iter().zip(row) {
                let cell = Paragraph::new(cell)
                    .style(Style::default())
                    .alignment(Alignment::Center);

                cell.render(*cell_area, buf);
            }
        }
    }
}

Resulting in:

   Compiling uml-tui v0.1.0 (/home/rne/Projekte/usb-missile-launcher/tui)
error[E0599]: the method `render` exists for struct `table::Table<3, 3, &str>`, but its trait bounds were not satisfied
   --> tui/src/app.rs:131:76
    |
131 |         Table::from([["", "^", ""], ["<", "<Enter>", ">"], ["", "v", ""]]).render(inner, buf);
    |                                                                            ^^^^^^ method cannot be called on `table::Table<3, 3, &str>` due to unsatisfied trait bounds
    |
   ::: tui/src/table.rs:8:1
    |
  8 | pub struct Table<const COLS: usize, const ROWS: usize, T> {
    | --------------------------------------------------------- method `render` not found for this struct
    |
note: trait bound `&'b &str: Into<Text<'_>>` was not satisfied
   --> tui/src/table.rs:20:20
    |
 18 | impl<'a, const COLS: usize, const ROWS: usize, T> Widget for &'a Table<COLS, ROWS, T>
    |                                                   ------     ------------------------
 19 | where
 20 |     for<'b> &'b T: Into<Text<'a>>,
    |                    ^^^^^^^^^^^^^^ unsatisfied trait bound introduced here
    = help: items from traits can only be used if the trait is implemented and in scope
    = note: the following traits define an item `render`, perhaps you need to implement one of them:
            candidate #1: `Widget`
            candidate #2: `ratatui::prelude::StatefulWidget`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `uml-tui` (bin "uml-tui") due to 1 previous error

How can I convince the borrow checker, that this works for any type of reference?

The problem seems to be that you're storing T = &'a strs, but can't name that lifetime in the Widget implementation as it's part of T. Some potential (untested) workarounds that don't involve changing upstream are:

// Implement when `T = &'a U`
impl<'a, const C: usize, const R: usize, U> Widget for &Table<C, R, &'a U>
where
    &'a U: Into<Text<'a>>
// Implement for concrete types (with a macro probably) -- as there doesn't seem
// to be many types that satisfy your `&'? _: Into<Text<'?>>` bound anyway.
impl<'a, const C: usize, const R: usize, U> Widget for &Table<C, R, &'a str> ...
impl<'a, const C: usize, const R: usize, U> Widget for &Table<C, R, &'a Masked<'_>> ...
impl<'a, const C: usize, const R: usize, U> Widget for &'a Table<C, R, Masked<'_>> ...

// (The fact that sometimes the lifetime is outside `Table` and sometimes is
// inside `Table` in this sketch is strongly related to the problem at hand.)
  • Make your own trait for generating Paragraph<'_>s and implement it for the things you care about, and use that instead of Into<Text<'a>>[1]

  • Carry around some phantom lifetime and use T: Into<Text<'phantom_lifetime>> + Clone, probably not a great idea


  1. probably similar in effect as the last suggestion ↩︎

I had to cheat a bit by adding an additional constraint for the type to implement Copy (it's pass midnight over here :sweat_smile:), but at least I got it to compile:

use ratatui::buffer::Buffer;
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
use ratatui::prelude::{Style, Text, Widget};
use ratatui::widgets::Paragraph;

/// A table.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct Table<const COLS: usize, const ROWS: usize, T> {
    elements: [[T; COLS]; ROWS],
}

impl<const COLS: usize, const ROWS: usize, T> From<[[T; COLS]; ROWS]> for Table<COLS, ROWS, T> {
    fn from(elements: [[T; COLS]; ROWS]) -> Self {
        Self { elements }
    }
}

impl<'b, 'a: 'b, const COLS: usize, const ROWS: usize, T> Widget for &'a Table<COLS, ROWS, T>
where
    T: Into<Text<'b>> + Copy,
{
    fn render<'c>(self, area: Rect, buf: &'c mut Buffer) {
        // Build 3 rows inside inner block
        let rows = Layout::default()
            .direction(Direction::Vertical)
            .constraints(
                [Constraint::Percentage(
                    u16::try_from(100usize / ROWS).expect("Percentage always fits."),
                ); ROWS],
            )
            .split(area);

        for (row_area, row) in rows.iter().zip(&self.elements) {
            let cols = Layout::default()
                .direction(Direction::Horizontal)
                .constraints(
                    [Constraint::Percentage(
                        u16::try_from(100usize / COLS).expect("Percentage always fits."),
                    ); COLS],
                )
                .split(*row_area);

            for (cell_area, cell) in cols.iter().zip(row) {
                let cell = Paragraph::new(*cell)
                    .style(Style::default())
                    .alignment(Alignment::Center);

                cell.render(*cell_area, buf);
            }
        }
    }
}

Thanks. I came up with this also, but I didn't want Copy or Clone bounds.
Since I'm not currently reusing tables anyway, I'll stay with an implementation for just Table (no reference) for now. Introducing further traits for my current scenario is overkill, but it's good to know that this is an option for future projects.

Heh, I didn't think 'b would be constrained there.

(Probably this is just noise as you went another route, but)

I guess lifetimes don't have to be constrained, so @firebits.io suggestion (which is basically my last bullet point without the PhantomData) can probably be implemented like so...

impl<'b, const COLS: usize, const ROWS: usize, T> Widget for &Table<COLS, ROWS, T>
where
    T: Into<Text<'b>> + Copy,
{

...though in practice I think this is the same as implementing for T = &U due to the Copy bound, if we only consider the implementing types from ratatui itself.

The issue is that I wanted one generic implementation that would work for any T that satisfies the library's trait bounds. May it be &'static str, &'a str, String, Cow<'b, str> or whatnot.

Certainly :sweat_smile:. It's just noise that I forgot to remove before posting.

I get that, but basically the implementations don't all line up in away amenable to generically implementing for &Table unless you clone.[1]

// Lifetime from outer reference.
//
// Can work with `&Table` (or `&'a Table`), can also work with cloning.
impl<'a> From<&'a Masked<'_>> for Text<'a>
impl<'a> From<&'a str> for Text<'a>

// Any lifetime (non-`Copy`, non-reference type).
//
// Can work with `&'a Table` via projection, can also work with cloning.
impl From<String> for Text<'_>

// Lifetime from non-`Copy` (non-reference) type.
//
// These won't work with `&Table` without cloning, except `Cow<'_, str>`
// specifically could work with `&'a Table` via projection.
//
// (Getting a `&str` from the others would change their semantics --
// drop styling, drop masking, etc. -- or isn't possible (`Vec`).)
impl<'a> From<Cow<'a, str>> for Text<'a>
impl<'a> From<Line<'a>> for Text<'a>
impl<'a> From<Masked<'a>> for Text<'a>
impl<'a> From<Span<'a>> for Text<'a>
impl<'a> From<Vec<Line<'a>>> for Text<'a>

Incidentally, I wondered why &Masked<'_> has an Into<Text<'_>> implementation when the rest don't, and the current implementation effectively creates a new String of masked characters. Cost wise that's similar to a clone (with no happy case when the internal Cow is a borrow).

If the others gained From<&Thing<'_>> implementations, most would have to clone at least some data. So while it would technically "solve your problem", it wouldn't necessarily be much of an improvement over cloning yourself.


  1. Being as generic as possible can be the enemy of good... not that I don't have the same inclinations. ↩︎

OTOH if you only care about non-styled, non-masked, stringy data, you could use

// Works with `&str`, `Cow<'_, str>`, `String`, and nested references thereto
impl<const COLS: usize, const ROWS: usize, T> Widget for &Table<COLS, ROWS, T>
where
    T: AsRef<str>,

Or suggest that upstream add the following implementations:

impl<'a> From<&&'a str> for Text<'a>
impl<'a> From<&'a String> for Text<'a>
impl<'a> From<&'a Cow<'_, str>> for Text<'a>

and then &'a T: Into<Text<'a>> will similarly work for &str, String, Cow<'_, str>.

Another approach is to write your own IntoText trait, implement it for all the types you want, and use it as a trait bound instead of Into<Text>. This lets you define recursively dereferencing generic impls that handle && automatically.

trait AsText {
    fn into_text(&self) -> Text<'_>;
}

impl IntoText for str {...}
impl IntoText for String {...}
impl IntoText for Cow<'_, str> {...}
impl<T: ?Sized + IntoText> IntoText for &T {...}  // can't do this with Into
impl<T: ?Sized + IntoText> IntoText for Arc<T> {...}
// ...