-
Notifications
You must be signed in to change notification settings - Fork 569
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
Conversation
There was a problem hiding this 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?
@@ -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]) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
andOPEN_WINDOW
to useTarget
- Fix open and save menu items
- Make
Selector
type safe - Split
Selector
intoSelector
andSelectorOnce
- 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
/// | ||
/// This includes a function that can build the root widget, as well as other | ||
/// window properties such as the title. | ||
pub struct AppStateWindowDesc { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 TypeId
s 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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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>); |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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
andCommand::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 returnOption
instead ofResult
. - Submitting and retrieving becomes a no-brainer as the compiler checks it for you.
- Could allow merging
Command::new
andCommand::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.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
downcast
seems reasonable.
This changes
Selector
toSelector<T>
whereT
is the selectors payload.There was a Zulip discussion about why
Command
s rely on purely dynamic typing whileEnv
uses statically typedKey
s.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.