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 {}
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())
}
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
onAlign<Container<Label>>
it will resolve to theContainer::set_background
method; but if you callset_text
it will resolve toLabel::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
andLabel
haveset_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();
}
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)
}
}
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 onself.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>;
}
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.