Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Statically typed selectors #908

Closed
wants to merge 19 commits into from
Closed

Statically typed selectors #908

wants to merge 19 commits into from

Conversation

luleyleo
Copy link
Collaborator

@luleyleo luleyleo commented May 8, 2020

This changes Selector to Selector<T> where T is the selectors payload.

There was a Zulip discussion about why Commands rely on purely dynamic typing while Env uses statically typed Keys.

I found myself typing 'Must have ThisType as payload` for pretty much every selector I've defined, so adding a type to it seemed like the logical consequence.

This also fixes some issues with menu items submitting wrong commands or showing wrong labels.

This still lacks in documentation and I haven't thoroughly checked if everything works, but at least for multiwin it does work better now.

Also I have plans to introduce OneShotSelector<T> for one-shot commands and I'm not sure if it should be part of this PR or go into a separate one.

I've opened this as a draft for now so that I have a better overview of the changes made and where I have to add documentation. Also I can see if there are errors on mac or windows and if someone wants to give some feedback, that would be welcome as well.

druid/src/command.rs Outdated Show resolved Hide resolved
Copy link
Member

@cmyr cmyr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, a bunch of thoughts and observations; I think the general idea is good but there's also a lot going on here, and it's hard to address it all. It might make some sense to try and break this up a bit if that's not too tricky, such as (for instance) having the one-shot changes be a separate patch?

druid/src/command.rs Outdated Show resolved Hide resolved
druid/src/command.rs Outdated Show resolved Hide resolved
@@ -47,6 +47,7 @@ While some features like the clipboard, menus or file dialogs are not yet availa
- Timer events will only be delivered to the widgets that requested them. ([#831] by [@sjoshid])
- `Event::Wheel` now contains a `MouseEvent` structure. ([#895] by [@teddemunnik])
- `AppDelegate::command` now receives a `Target` instead of a `&Target`. ([#909] by [@xStrom])
- `SHOW_WINDOW` and `OPEN_WINDOW` no longer require a `WindowId` as payload, but they must `Target` the correct window. ([#???] by [@finnerale])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe this can just be a separate patch?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose this could be split into

  • Updating SHOW_WINDOW and OPEN_WINDOW to use Target
  • Fix open and save menu items
  • Make Selector type safe
  • Split Selector into Selector and SelectorOnce
  • Add type-safe? methods to EventCtx to create menus and windows.

Splitting the last two out would cause quite some duplicated / wasted work tho, as SelectorOnce influences error and return types, while menus creation decides over adding wrapper types or new methods.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even the first one means that I'll have to first declare it to take WidgetId and than remove that in a separate PR again.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yea, if things are truly entwined than doing it together makes sense, but if there are good dividing points that does make review a lot easier.

druid/examples/identity.rs Show resolved Hide resolved
///
/// This includes a function that can build the root widget, as well as other
/// window properties such as the title.
pub struct AppStateWindowDesc {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming: I would call this AnyWindowDesc.

Philosophy: I get that this is a problem that needs to be solved for this PR to work, but I also think it's worth thinking about on its own.

What we need:

  • some kind of type erasure or 'implicit T' for certain types of objects, like window descriptions and menus
  • not to confuse the user
  • to clutter the namespace as little as possible

What's the best way to do this? I'm not totally sure.

If we could have some internal type that did what this type is doing, that would be great. One way this might work is if we had convenience methods for things like making a new window or setting a menu; for instance creating a new window might just use a method on EventCtx, and then the user never needs to create the command or actually interact with the type erased type?

It might also be that we require a public type (that appears in the signature of the selector, for instance) but we don't actually need the user to ever interact with it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I though about some kind of wrapper type but I don't think it is possible with the Rust type system.
What I wanted to make more obvious: MenuDesc and others must represent the application state.
Adding methods for this to EventCtx seems like a possible solution.
Doing so could even allow equipping EventCtx with the TypeIds of eg. MenuDesc<AppState> to check it right away and panic if the type is wrong. That way the back trace will show exactly where the mismatch happened.
But if we want to keep it as a Command I doubt we can get around some kind of specific wrapper type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the nice thing is that on EventCtx the method could take a concrete MenuDesc.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How so? I mean EventCtx does not know what the AppState is, so how is it supposed to be typed for it?
If this could be done statically, than that would be absolutely my favorite.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mm you're right I was thinking that EventCtx was paramaterized over T, but even in that case it doesn't tell us what the root data is.

/// The argument was expected to be reusable and wasn't, or vice-versa.
WrongVariant,
/// The argument could not be downcast to the specified type.
IncorrectType,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is still possible in the case of a name collision, isn't it?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, and it will panic just like Env does.

/// variant of some generic item represents the application state.
/// Examples are `MenuDesc<T>` and `AppStateMenuDesc`.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AppStateTypeError {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have an 'errors' module? I might move that there, I'm not sure.

I'd also maybe be more general, and call it a DowncastError or something?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have one, but it could be interesting to see if it would be beneficial.
As for the name, DowncastError is less specific but would be more in line with the other names.

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Selector(&'static str);
#[derive(Debug, PartialEq, Eq)]
pub struct OneShotSelector<T>(SelectorSymbol, PhantomData<*const T>);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not totally sure about this; what it comes down to is whether or not we expect external people to need to use this, or whether it's an implementation detail used in a few places inside druid. Again, I think that API simplicity is important, and I think the more concepts we introduce the more work we need to do to explain what they're for and how to use them. My feeling is that 95+% of the time, the user doesn't need to know about one-shot commands?

A kind of gross way we could encode one-shotness would be in the selector symbol itself, like we could have some keyword that prefaced one-shot commands...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the ideas I had where:

Leave it as it is.

  • IMO the worst choice as it makes it less obvious how a command is supposed to be used, and would require you to look up every commands doc in order to find out how the hell you are supposed to submit it and how to retrieve the object. Also Command::get and Command::take will either have to panic or require an additional error, which would add to the interface at least as much noise as a new type.

Selector(key: &str, is_once: bool) and a Selector::once constructor.

  • Not much better than the previous one.

Encoding it in the selector symbol.

  • Again, does not add anything really.

Having a separate type for it.

  • Allows Command::get to return Option instead of Result.
  • Submitting and retrieving becomes a no-brainer as the compiler checks it for you.
  • Could allow merging Command::new and Command::once if we want to.
  • No need to look up a selectors docs to know how to use it (unless it does something very special)
  • Those who do not care about one shot commands do not have to use this type

Which is why I very much like this design.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if another option is to encode it in the type param? Like have some type SingleUse<T>, and then you're required to explicitly take from that... I don't know if I like this, just spitballing.

/// A platform-agnostic description of an application, window, or context
/// menu. The user has to guarantee that this represents a `MenuDesc<T>` where `T` is the users
/// `AppState`.
pub struct AppStateMenuDesc {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto, just AnyMenuDesc if we're going this route

}

impl AppStateMenuDesc {
pub(crate) fn realize<T: Any>(&self) -> Result<&MenuDesc<T>, AppStateTypeError> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't mind this name, but I might just call it downcast or to_concrete? (ditto the other one)

Copy link
Collaborator Author

@luleyleo luleyleo May 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

downcast seems reasonable.

@luleyleo
Copy link
Collaborator Author

Splitting this up was a great idea, and resulted in a far better design.

Thanks a lot @cmyr and @xStrom for your help and reviews!

This was completed with #993 an can thus be closed as successful.

@luleyleo luleyleo closed this May 28, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants