Druid: Representing enums in the ui

Created on 1 Apr 2020  路  12Comments  路  Source: linebender/druid

Update: See https://github.com/Finnerale/druid-enums for an implementation of the derive.


I've opened this Issue to continue the discussion about this from #751 and zulip

Currently druid does not offer a good solution to representing enums in the ui.
While structs can be split up into their different fields using Lens, no such feature exists for enums.
Considering how prevalent enums are in Rust, I would argue that druid should provide a good solution for this.

The ideas so far are

  1. Extending Lens to work with enums
  2. Introducing a new derive specifically for enums
  3. Using an immediate mode widget (#751)

I've implemented (examples of) those three here,
also @cmyr expressed similar ideas on #751

My preferred solution would be number 2, introducing a new Match derive similar to Lens which adds a ::matcher() -> MyTypeMatcher function to the deriving type (MyTypeMatcher would be generated).
For example if Option would derive Match:

Option::matcher()
    .some(|| make_widget_some())

Having the matcher as a function on the type should be more intuitive than having to know that there is now also a separate struct called OptionMatcher, especially when exporting (pub use) it.
This matcher would provide one method per variant, as well as a default method to mimic _ =>.

Of course this does not event get close to the power match provides but it should be good enough for most cases. Would be nice if we could collect some ideas here and come up with something truly satisfying.

discussion enhancement

Most helpful comment

Okay, looking these over:

  • the basic impl with no macro looks right to me
  • the proc macro is a bit trickier; the use of the macro will be distinct from the generating of the lenses, so in the tuple case we can't use names; it will have to be data.0, data.1, etc. If we really wanted to get magical I guess we could probably map from the numbered fields to the names that were used? I think this will be confusing, though, if the user wants to do anything fancier than this. So I think it is best to be explicit about the fact that we're creating tuples under the covers.

about discriminants: I'd totally overlooked mem::discriminant. This will solve one problem.

All 12 comments

I'd like to try and be more explicit about exactly what problem we're trying to solve. To me, it feels like in the general case a given widget should expect a specific form of data.

Given this, the most natural form I can imagine for a widget that is displaying an enum is for that widget to be like a generalization of the current Either widget; it would be a container that managed a child, with the type of the child determined by the variant.

Importantly, this means that given an enum like,

enum Wat {
    One(String),
    Two(usize),
}

Our outer widget would be a Widget<Wat>, and its inner widget would be either a Widget<String> or a Widget<usize>, depending on the variant.

One thing jumps out immediately: there is a distinction, for the sake of Data, between when the data has changed and when the enum variant has changed. If the data changes from Wat::Two(5) to Wat::Two(42), this should be transparent to the inner widget; it should just look like a normal update. If, however, it changes from Wat::Two(_) to Wat::One(_), we need to replace the inner widget, and likely do the WidgetAdded dance.

I think this means that one thing our custom macro needs to do is add some sort of same_variant method to our enum.

The other thing we need to is come up with some way of letting the user describe the widget that will be returned for each variant.

A current hurdle, for me, is that (I think) ideally the enum should be transparent to the inner widget. That is, the inner widget should have some concrete Data type that is not the enum, but is the contents of some variant of the enum. This is easy enough for a unit or a tuple variant (we just have the data be () or some tuple) but it's annoying for struct variants; we either have to generate a variant automatically (which will be annoying, because it will have some generated name) or we will need to provide some mechanism for the user to describe what type the outer type should be converted into.

It may be that we disallow struct variants, and ask people to use a tuple enum with a struct field, as in:

// boo
enum One {
    Thing { face_count: usize, face_length: f64 },
}

// yay
enum Two {
    Thing(Thing),
}

Thing { face_count: usize, face_length: f64 }

... or something.

I think this is a solvable problem, but I would like the solution to be one that really fits the druid model; my main reservation with the immediate mode idea is that we really lose out on the power of Data. That said, I think that if we figure something out here it will end up being a very powerful tool.

This isn't something I'm going to personally be spending much time on in the immediate future, but I'm glad that you're interested in it and I'm happy to provide whatever guidance and moral support I can.

I think I'll implement a simple version of such a proc-macro and see how it works out.
More ideas / opinions are still very much welcome though.

I'm happy to do a call or something if you want to talk through some of this.

I think the place to start would be a sketch of what code we want the proc macro to produce.

Then try and figure out what the components are. For instance: I think we are going to want some sort of 'unwrapping variant lens', that extracts a given variant from an enum.

I'm happy to do a call or something if you want to talk through some of this.

Sounds good :+1:

Writing down the ideas which came up:


Lens for enums

#[derive(Clone, Data, Lens)]
enum Event {
    Click(f32, f32),
    Key(char),
}

fn event_widget() -> impl Widget<Event> {
    // Matcher recreates its child whenever its determinant changes
    // Matcher::new takes Fn(&T) -> impl Widget<T>
    druid::Matcher::new(event_widget_impl)
}

fn event_widget_impl(event: &Event) -> impl Widget<Event> {
    // standard Rust code and behaviour such as exhaustiveness
    match event {
        Event::Click(_, _) => Label::dynamic(|data, _| {
            format!("x: {}, y: {}", data.0, data.1)
        }).lens(Event::click),
        // IT'S A TRAP! The `Label` will not update when 'c' changes
        Event::Key(c) => Label::new(format!("key: {}", c))
            .lens(Even::key),
    }
}

A proc-macro match

Based on Lens for enums

fn event_widget() -> druid::Matcher<Event> {
    match_widget! { Event, // could this type hint be ommited?
        Event::Click(x, y) => Label::dynamic(|data, _| {
            format!("x: {}, y: {}", data.x, data.y) // named touple?
        }),
        Event::Key(_) => Label::dynamic(|data, _| format!("key: {}", data))),
    }
}

fn event_widget() -> impl Widget<Event> {
    match_widget! { Event,
        Event::Click(x, y) => Label::dynamic(|data, _| {
            format!("x: {}, y: {}", x, y) // not valid or changed to data.x?
        }),
        _ => Label::dynamic(|data: &Event, _| format!("key: unhandled"))),
    }
}

A proc-macro derive

#[derive(Clone, Data, Match)]
enum Event { .. }

fn event_widget() -> impl Widget<Event> {
    Event::matcher()
        .click(Label::dynamic(|data, _| {
            format!("x: {}, y: {}", data.0, data.1)
        ))
        .key(Label::dynamic(|data, _| {
            format!("key: {}", data))
        })
}

fn event_widget() -> impl Widget<Event> {
    Event::matcher()
        .key(Label::dynamic(|data, _| {
            format!("key: {}", data))
        })
        .default(Label::new("Unhandled Event"))
    }
}

fn event_widget() -> impl Widget<Event> {
    // Will emit warning for missing variant
    // Event::Click at runtime
    Event::matcher()
        .key(Label::dynamic(|data, _| {
            format!("key: {}", data))
        })
    }
}

@cmyr I think those were the most fundamental ideas I'll explore for now, if I missed something fundamental that came up please remind me, my head is a bit overflowing with ideas right now :sweat_smile:

There is approach which might be worth considering/thinking over, which is to generate an auxiliary unitary enum, and using discriminator values, to make some kind of array of widgets.

Discriminator values can be set up by arbitrary constant functions even, here is basically a silly example, anyhow there might be an custom derive approach which generates these kind of enum VariantIDEnum as an associated trait.

Anyhow another thing to mull over, as though there weren't enough of those :dagger:

https://gist.github.com/rust-play/14f8c850f09f9cf8d0829a6e87806b8a

@ratmice Sorry but I can not follow what you mean :sweat_smile:
You mean something like this https://doc.rust-lang.org/stable/std/mem/struct.Discriminant.html?

array of widgets

Something in this direction?

Sorry, I'm not terribly clear as I haven't thought through the whole thing entirely,
In general the thought was: enum Foo { A(u32), B(String) } cannot have discriminators because they have non-unitary variants... but we could still derive a custom

impl Foo {
  enum FooVariantID {
     A = Foo::Widgets::WidgetID1,
     B = Foo::Widgets::WidgetID2
  }
}

mapping some collection of widgets to the original enums variants. I think mem::Discriminant is opaque and we need transparency here so we can map an widget->enum variant via the discriminant.

Anyhow, it is what came to mind...

Okay, looking these over:

  • the basic impl with no macro looks right to me
  • the proc macro is a bit trickier; the use of the macro will be distinct from the generating of the lenses, so in the tuple case we can't use names; it will have to be data.0, data.1, etc. If we really wanted to get magical I guess we could probably map from the numbered fields to the names that were used? I think this will be confusing, though, if the user wants to do anything fancier than this. So I think it is best to be explicit about the fact that we're creating tuples under the covers.

about discriminants: I'd totally overlooked mem::discriminant. This will solve one problem.

I've setup a repo where I'll push my progress on this:
https://github.com/Finnerale/druid-enums

Short update on this:
I've decided to go with a derive macro, as it turned out to be simpler implementation wise and more robust in practice.
To make using it easier, I set up a new repo for just this macro here: https://github.com/Finnerale/druid-enums

So far it supports any enums with unit or tuple variants that also implement Data.
(thanks @derekdreery for implementing most of it.)

It is still missing support for generics, good docs and tests.

I'm not sure though, whether I should publish it as a separate crate, or leave it as a repo until it reaches a quality where it can be merged into Druid.

Aah I was going to post my fork, but you are already using it - perfect. :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Finnerale picture Finnerale  路  5Comments

madig picture madig  路  5Comments

raphlinus picture raphlinus  路  4Comments

timothyhollabaugh picture timothyhollabaugh  路  3Comments

Finnerale picture Finnerale  路  3Comments