a description of the problem

this was originally written as a discussion aid for contributors to Druid; this prelude is intended to give a simple sketch of the motivating problem.

At the heart of Druid is the Widget trait. A Druid application is made up of a tree of widgets; some of these are ui elements (a button, a label, a checkbox), some of these are collections of other widgets (a row, a column, a split-view) and some of these are single-child containers, that wrap another widget and modify some aspect of its behaviour, appearance, or layout.

The Padding widget is a simple example: it takes its child, and it adds padding. There are many other single-child container widgets. Layout widgets add padding or align a child in available space; decorating widgets draw backgrounds or borders; and controller widgets modify event handling and behaviour.

For the purpose of this write-up (and the code samples contained within), we will work with a very simplified version of the widget trait, that looks like this:

/// A context passed during event handling
struct EventCtx;

/// Events that can happen
enum Event {}

trait Widget<T>: 'static {
    fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &T) {}
}

The real Widget trait is more complex, but this is adequate for our needs.

To illustrate our problem, let’s add some widgets: a Label widget that has no children, and a few single-child containers that have, well, a single child:

struct Padding<T> {
    child: Box<dyn Widget<T>>,
}

struct Background<T> {
    child: Box<dyn Widget<T>>,
}

struct Label {
    text: String,
}

impl<T> Padding<T> {
    fn new(child: impl Widget<T>) -> Self {
        Self {
            child: Box::new(child),
        }
    }
}

impl<T> Background<T> {
    fn new(child: impl Widget<T>) -> Self {
        Self {
            child: Box::new(child),
        }
    }
}

impl Label {
    fn new(text: String) -> Self {
        Self { text }
    }

    fn set_text(&mut self, text: String) {
        self.text = text;
    }
}

impl<T: 'static> Widget<T> for Padding<T> {}
impl<T: 'static> Widget<T> for Background<T> {}
impl<T: 'static> Widget<T> for Label {}

(link to playground)

In use, we might see something like this:

let my_widget: Padding<()> = Padding::new(Background::new(Label::new("hello!")));

The problem we seek to address is: how can we call access (or otherwise modify) the label, through the layers of containers?

In this write-up I am going to describe some of the options we have if we want to try and reach down to a concrete child through arbitrary layers of containers.

Option 1: criminal dereferencing

To begin, it should be noted that we have two options for the type signature of a single-child container. The first is to make it generic over the type of its child, and the second is to turn the child into a trait object. This second option is what I’ve done in the code above; the other version would look more like,

/// Previous version: a trait object
struct Padding<T> {
    child: Box<dyn Widget<T>>,
}

/// Alternative: generic over the widget
struct Background<T, W> {
    child: WidgetPod<T, W>,
}

For the purpose of this example, WidgetPod is a type that implements a bunch of common widget behaviour. T is the type of the data handled by this widget. Ignore WidgetPod; the only important thing here is that we are generic over T, (some data type) and W, which is some widget.

So: back to deref. One option we have is to only support the generic versions, and then implement Deref and DerefMut for single-child containers, allowing them to deref to their children.

impl<T, W: Widget<T>> std::ops::Deref for Padding<T, W> {
    type Target = W;
    fn deref(&self) -> &Self::Target {
        &self.child
    }
}

impl<T, W: Widget<T>> std::ops::DerefMut for Padding<T, W> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.child
    }
}

fn main() {
    let mut widget = Padding::new(Background::new(Label::new("hi!".to_string())));
    widget.set_text("no problem!".to_string())
}

(link to playground).

pros:

  • This “just works”: if you have an Align<Label>, you can call label methods directly.
  • normal method syntax!
  • this is all statically dispatched, so it’s probably fast?
  • you can call methods on any widget in the chain; if you call set_background on Align<Container<Label>> it will resolve to the Container::set_background method; but if you call set_text it will resolve to Label::set_text.

cons:

  • this is not what deref is for, and it is generally frowned upon.
  • compile times are hurt (including for incremental builds) because everything is monomorphized.
  • binary size is hurt for the same reason
  • methods can shadow each other; if both Container and Label have set_color methods, we can’t pick which one we call

conclusion: this isn’t pretty, but I wouldn’t rule it out; if there are no alternatives it might be worth it.

Option 2: child() + Any

Another approach to this problem involves adding two new methods to the Widget trait itself:

trait Widget<T> {
    // ... other methods
    fn child(&self) -> Option<&dyn Widget<T>> {
        None
    }

    fn as_any(&self) -> &dyn Any;
}

The first of these methods would only be implemented by single-child containers; they would return their child.

The second of these methods would be implemented by all widgets, and would return a reference to self, but cast to the Any trait.

So: we have our two methods that are implemented by all widgets. What we can then do is,

/// A helper trait for dynamically accessing a child that is wrapped in
/// an arbitrary number of container widgets.
///
/// This is a separate trait because it is not object safe.
trait AnyWidget<T: 'static>: Widget<T> {
    fn downcast_child<C: Widget<T>>(&self) -> Option<&C> {
        if let Some(child) = self.as_any().downcast_ref::<C>() {
            return Some(child);
        }

        let mut child = self.child();
        loop {
            match child {
                None => return None,
                Some(inner) => match inner.as_any().downcast_ref::<C>() {
                    Some(child) => return Some(child),
                    None => child = inner.child(),
                }
            }
        }
    }
}

fn main() {
    let widget: Padding<()> = Padding::new(Background::new(Label::new("hi!".to_string())));
    let _label = widget.downcast_child::<Label>().unwrap();
}

(full link to playground)

And ta-da! We fetch our inner label through an arbitrary number of intermediating containers.

An additional auto-impl trick

An annoying thing about this solution is that we can’t provide a default impl for the as_any method, for object-safety reasons: Any requires things be Sized, but if we require that widgets be Sized then they aren’t object-safe.

We can get around this, though, with a bit of type tetris:

trait AsAny {
    fn as_any(&self) -> &dyn Any;
}

impl<T: 'static> AsAny for T {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

trait Widget<T>: AsAny {
    // ... other methods
    fn as_any(&self) -> &dyn Any {
        <Self as AsAny>::as_any(self)
    }
}

(full link to playground)

pros

  • fairly uninvasive: single-child containers will have to implement the child() method, but other widgets won’t change
  • It’s not elegant, but it… works?

cons

  • I lied. It doesn’t work. Or rather, it works only if you’re fine with immutable references. Given how downcast_child is implemented, it relies on self.child() taking &self (not &mut self) and the borrow checker will not let us write a mutating version. This is a problem, because most of the methods we want to use this to call are methods that mutate self. So: back to the drawing board.

Option 2a: keep digging

We could just require widgets use interior mutability (RefCell, Cell, etc) for all of their internal fields, and then we wouldn’t need to worry about the borrow-checker’s complaints.

This would feel gross, and would impose a strange and hard to explain constraint on widget authors. Worse, it injures my pride. Let’s keep looking.

Option 3: dynamic leaf instead of dynamic child

There’s a small tweak we can make to downcast_child that allows us to have a safe mutable version. Instead of the child method on widget, we can add a leaf (and leaf_mut) method instead, which looks like,

trait Widget<T>: AsAny {
    // ... other methods
    fn leaf(&self) -> &dyn Widget<T>;
    fn leaf_mut(&mut self) -> &mut dyn Widget<T>;
}

(link to playground)

Where the child method would return Some if the widget had a child, leaf is the inverse; it should return self if self does not have a child. Containers just call leaf() recursively on their children until someone returns themselves.

pros

  • works, no unsafe, allows mutable and immutable access
  • doesn’t force monomorphization
  • direct access to leaf widgets; can call methods directly

cons

  • boilerplate: seemingly no good way to provide impls for leaf/leaf_mut
  • no access to intermediate widgets; can’t get to Padding in Align<Padding

conclusion: this is probably the best option so far…

Option 4: fake dynamic dispatch (Command)

Changing gears a bit: we have an existing mechanism in Druid for sending arbitrary commands to widgets, without knowing their specific type: the Command type. This is basically a ‘selector’ that identifies the command (a unique string) and an optional argument. Currently this exists as part of Druid’s event handling mechanism; Command is one of the types of events that a widget can receive.

What if we just made a top level command method on widgets?

trait Widget<T> {
    // .. other widget methods
    fn command(&mut self, ctx: CommandCtx, cmd: Command, data: &T, env: &Env);
}

// concrete widgets would handle some commands:
impl<T> Widget<T> for TextBox {
    fn command(&mut self, ctx: CommandCtx, cmd: Command, data: &T, env: &Env) {
        if cmd.is(SELECT_ALL) {
            self.select_all();
        } else if let Some(text) = cmd.get(INSERT_TEXT) {
            self.insert(text);
        }
    }
}

// container widgets should pass them along to their children:
impl<T> Widget<T> for Align<T> {
    fn command(&mut self, ctx: CommandCtx, cmd: Command, data: &T, env: &Env) {
        self.child.command(ctx, cmd, data, env);
    }
}

pros

  • extends an existing mechanism; widgets already handle some commands, and this would build on that
  • can target any widget in a chain (although will stop at the first widget that handles the command)
  • well-scoped; only behaviour that we want to expose in this way would be available (approaches that just access a reference/mutable reference to the inner widget can likely call methods that are not intended to be called dynamically?)
  • less public API: instead of exposing a bunch of public methods, widgets would only need to expose the commands they support
  • simple delegation: a widget like Button, which manages an inner widget (Label) could easily forward commands to that child, which would handle them as normal

cons

  • commands don’t have return values. We could hack this in (return Option<Any>) but the ergonomics are pretty bad.
  • support limited to commands explicitly handled by the child; can’t just do arbitrary things with a &mut reference.

conclusion worth consideration! The main use case we have is for mutating inner widgets; we’re less concerned about getters. It’s also nice that this works with an existing mechanism, and would be a good pressure for us to standardize on widgets expressing their behaviour in terms of commands, which is something that might be good.

Option 5: fake dynamic dispatch: a big ‘ole trait

what if we just put every method that any widget has onto the Widget trait?

That is: every single public method that a widget defined would be added to Widget (or maybe a separate trait, like WidgetMethods). These methods would be noops by default; containers would call them on their children; and children would implement only those methods that they used.

pros

  • sort of delightfully anarachic
  • it “just works”, maybe?

cons

  • may not actually be possible, I haven’t tried
  • terrifyingly anarchic
  • strict limitations on types that can be used in trait methods, if we want to maintain object safety (no generics)

Option n: what about proc macros?

Procedural macros are something I have explicitly avoided bringing in to this discussion so far, but it is definitely worth mentioning them; they open up a wide range of other possible solutions, and could also be used to improve the ergonomics of some of the solutions already discussed.

This may be a direction we want to explore eventually, but it dramatically expands our scope, and this write-up is long enough already. That said, it is worth mentioning that this is another possible option.

Conclusion

These are the main ideas I’ve come up with for accessing widgets dynamically through a chain of containers. None of these mechanisms are perfect, but they’re all at least interesting, as demonstrations of some of the forms of pseudo-dynamism available to us.

Forced to choose right now I think it would probably be between #3 and #4; that is, between the version of the ‘Any’ trick that returns leaf widgets, or doubling down on Command.