Calling a method with a self parameter on `Box<dyn trait>`

In Ratatui, we have a Widget trait that accepts self

pub trait Widget {
    fn render(self, area: Rect, buf: &mut Buffer);
}

This was inherited from tui-rs, and it's probably not the ideal situation to be in as accepting &self opens up a lot more ways that widgets can be used / stored / etc.

Now, changing this to &self is something we have considered, but that would break all the implementers and callers, so that's not on the table.

We can of course implement the Widget trait for &W instead of W (e.g. impl Widget for &Block) and in fact this makes a bunch of usages now possible.

But I run into a road block with how to get that to work with Box<dyn Widget>. This would be nice to have as it means that we can store e.g. a Vec of different widget types and render them where needed (e.g. different panels each with their own data and display).

#[rstest]
fn widget_box_dyn_render(mut buf: Buffer) {
    let widget: Box<dyn Widget> = Box::new(WidgetInABox);
    widget.render(buf.area, &mut buf);
    assert_eq!(buf, Buffer::with_lines(["Hello               "]));
}
error[E0161]: cannot move a value of type `dyn widgets::Widget`
   --> src/widgets.rs:569:9
    |
569 |         widget.render(buf.area, &mut buf);
    |         ^^^^^^ the size of `dyn widgets::Widget` cannot be statically determined

The one way that I have come up with to make this work is to add a new trait (WidgetRef) that accepts &self and implement that on all our types.

pub(crate) trait WidgetRef {
    fn render_ref(&self, area: Rect, buf: &mut Buffer);
}

I can also add a blanket widget implementation for that so we can just use that (and implement all the widgets WidgetRef with a Widget implementation pointing at that.

impl<W: WidgetRef> Widget for &W {
    fn render(self, area: Rect, buf: &mut Buffer) {
        self.render_ref(area, buf);
    }
}

My question: is there another way to allow a user to call render on a Box<dyn Widget> without changing the Widget trait? Is adding a second trait to achieve this the only way? I've looked at using Box::as_ref() and a few other mechanisms, but haven't found one.

Note: implementing this as an enum instead of a Box<dyn something> is not super meaningful, so I'd prefer to keep that out of scope for now. The trait is implemented in many crates as well as internal to Ratatui and in user apps, so the number of possible implementations would be generally infinite. Also, I want this to be the first step towards enabling widgets to act as containers of other indeterminate widgets of any mixed type (e.g. a group box / form widget that contains other widgets, or a list where every item is a widget).

Relevant GitHub PR for this change with the full details:

1 Like

You can use a helper trait to implement Widget for Box<...>. It involves adding a supertrait to Widget but also a blanket implementation of that supertrait, so it shouldn't break existing code:

trait Widget: BoxWidget {
    fn render(self, area: Rect, buf: &mut Buffer);
}

trait BoxWidget {
    fn render_box(self:Box<Self>, area: Rect, buf: &mut Buffer);
}

impl<W:Widget> BoxWidget for W {
    fn render_box(self:Box<Self>, area: Rect, buf: &mut Buffer) {
        (*self).render(area, buf)
    }
}

impl<W:Widget+?Sized> Widget for Box<W> {
    fn render(self, area:Rect, buf: &mut Buffer)  {
        self.render_box(area, buf)
    }
}
5 Likes

Thanks! This works great.

If you'd prefer, you can also make the blanket implementation the other way around, so that you don't have to refactor all of your existing implementations for &W:

impl<W> WidgetRef for W where for<'a> &'a W: Widget {
    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
        self.render(area, buf)
    }
}

impl<W:WidgetRef+?Sized> Widget for &Box<W> {
    fn render(self, area:Rect, buf: &mut Buffer) {
        (**self).render_ref(area, buf)
    }
}
1 Like

I wonder if there's a further change that does something similar to impl<W> WidgetRef for W where for<'a> &'a W: Widget { for the impl<W:WidgetRef+?Sized> Widget for &Box<W> { line?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.