Skip to content
This repository has been archived by the owner on Mar 4, 2024. It is now read-only.

Add proc macro for binding methods to action maps #266

Closed
wants to merge 1 commit into from

Conversation

andy128k
Copy link
Contributor

Closes #240

use syn::{parse_macro_input, ItemImpl};

#[proc_macro_attribute]
pub fn actions(_attr: TokenStream, item: TokenStream) -> TokenStream {
Copy link
Member

Choose a reason for hiding this comment

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

This could use some documentation with code example(s) :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yea, sure. I just wanted to share what is done at the moment. Also some tests would be great too.

Copy link
Member

Choose a reason for hiding this comment

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

It looks overall good to me, I just can't see how you set the ActionMap, so an example would help figure out how the API is supposed to work :)

Copy link
Member

Choose a reason for hiding this comment

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

Seems good to me too apart from the comments above but an example would be nice :)

@andy128k andy128k force-pushed the action-map branch 2 times, most recently from d143541 to c2e2277 Compare January 17, 2021 16:16
/// }
/// }
///
/// #[gio::actions]
Copy link
Member

Choose a reason for hiding this comment

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

What I was actually thinking of is to rather uses this with subclasses, and have the gio::actions attribute implement ActionMap/ActionGroup for that subclass automatically and then you can add the actions directly to self as it would be an action map.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You still may use this macro with subclasses. Just invoke self.register_actions(&self) in a constructor method.

Copy link
Member

Choose a reason for hiding this comment

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

Sure, I guess we can provide another derive macro like the CompositeTemplate for such use case I guess.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bilelmoussaoui Frankly, I was under impression that Gtk is moving away from inheritance.

The general direction of our API changes has been to emphasize delegation over subclassing.

many widgets are now final classes

Copy link
Member

Choose a reason for hiding this comment

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

@bilelmoussaoui Frankly, I was under impression that Gtk is moving away from inheritance.

The general direction of our API changes has been to emphasize delegation over subclassing.

many widgets are now final classes

You're mis-understanding what the announcement meant I guess. in gtk3 creating a custom widget was a painful process which was simplified in gtk4, and gtk developers expect you to create custom widgets (a subclass of gtk::Widget) where you compose multiple pre-defined widgets.

Copy link
Member

Choose a reason for hiding this comment

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

Without subclassing you can't use composite templates, properties, signals and all what gtk offers :)

Choose a reason for hiding this comment

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

i Frankly, I was under impression that Gtk is moving away from inheritance.

It's more of "everything now will be inheriting from GtkWidget directly rather than 4 different classes deep"

@andy128k andy128k marked this pull request as ready for review January 17, 2021 16:46
@andy128k andy128k force-pushed the action-map branch 3 times, most recently from 71c24ec to c239135 Compare January 17, 2021 18:21
///
/// #[gio::actions]
/// impl MyApplication {
/// fn action1(&self) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
/// fn action1(&self) {
/// #[action]
/// fn action1(&self) {

Would probably be better than taking any and every function and turning it into an action

Copy link
Contributor Author

Choose a reason for hiding this comment

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

They are actions. All methods inside this impl item are actions. It is an impl item who is annotated and produces an ActionMap. E.g. it is possible to have more than one ActionMap implemented by a single type.

Copy link
Member

Choose a reason for hiding this comment

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

I guess that's why I'm asking for a proper example that makes use of most of the features of this :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are two :) One in a doc-test and another in tests.

I also mentioned one in #240. There is a regular impl block with private methods and one more impl block with actions.

Copy link
Member

Choose a reason for hiding this comment

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

I meant a meaningful example how this would be used in practice as part of the examples or examples4 subdirectory.

Or maybe the docs and docs example just has to be extended to cover these questions too.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. I will create an example with some realistic actions.

/// // handle "action_second"
/// }
///
/// // Action with a parameter
Copy link
Member

Choose a reason for hiding this comment

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

Does it also handle return values?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it is ignored. Action's activate signal doesn't expect any return value.

Copy link
Member

Choose a reason for hiding this comment

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

There are stateful actions though. Action::change_state() / Action::get_state()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This macro doesn't cover/implement stateful actions.

Copy link
Member

Choose a reason for hiding this comment

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

Please document that then :)

///
/// // Action with a parameter
/// #[action(parameter_type = "s")]
/// fn action3(&self, param: String) {
Copy link
Member

Choose a reason for hiding this comment

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

What if the parameter type in the attribute and the function parameter don't match? Can we infer one from the other to not require both to be present? You can get the GVariant type from a Rust type in any case at runtime.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It types doesn't match it will be a runtime error (g_critical). I am not sure, it is possible to derive the type without bloating the code (tuples of arbitrary length could be a PITA).

Copy link
Member

Choose a reason for hiding this comment

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

The traits in glib::variant could be used for this. They already handle exactly these things.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, I overlooked that StaticVarianType exists. So, it seems I have something to do this evening :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is done. And parameter_type parameter is removed.

Comment on lines +22 to +29
#[gio::actions]
impl MyActionGroup {
Copy link
Member

Choose a reason for hiding this comment

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

Is this really needed? I would rather have everything on the same Impl and decorate all the actions with #[action...]

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am afraid it is not possible. In this case proc-macro will not be able to generate a single method which gathers all actions into an ActionMap.

Copy link
Member

Choose a reason for hiding this comment

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

You don't need to gather the actions if you assume that self is a glib::IsA<gio::ActionMap> then, you can directly add the actions without requiring an extra method. Of course that would only work for widgets/object that implement ActionMap. Which means gtk::Application & gtk::ApplicationWindow for now. We can then have a proc macro that would automatically implement ActionMap on top of a gobject subclass which would look much cleaner API wise I guess.

Copy link
Member

Choose a reason for hiding this comment

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

Of course that would only work for widgets/object that implement ActionMap.

Or something that implements Deref<Target=ActionMap> :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  1. Requiring that Self implements ActionMap makes this macro useless for my personal projects where I do not use inheritance but newtypes to create abstractions.
  2. Even if Self: ActionMap is assumed, it doesn't help. Code which generates an action should be invoked from somewhere. Attribute proc-macro attached to impl item allows to append a single method (register_actions in this case) and then this method can be called somewhere. Attribute proc-macro attached to each action handler individually will not be able to do so. It can generate another function and that's it. There is no way to inject invocation of these functions (Unless we assume additional requirement. GtkWidget::created-event would fit here if existed 😆 )

Copy link
Member

@bilelmoussaoui bilelmoussaoui Jan 18, 2021

Choose a reason for hiding this comment

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

Here's a rough pseudo code idea of what I had in mind

mod imp {

    #[derive(CompositeTemplate, Actions)]
    #[actions(group="widget")]
    pub struct Widget {

    }

    impl ObjectSubclass for Widget {
        ....

        fn type_init(type_: subclass::InitializedType<Self>) {
            type_.add_interface::<gio::ActionMap>();
            type_.add_interface::<gio::ActionGrouo>();
        }
    }

    impl ObjectImpl for Widget {
        fn constructed(&self, obj: &Self::Type) {
            obj.register_actions();
        }
    }

    # both those traits would be automatically implemented when using #[actions(group....)]
    impl ActionMapImpl for Widget {}
    impl ActionGroupImpl for Widget {}
}

glib::wrapper! {
    pub struct Widget(ObjectSubclass<imp::Widget>) @derive gtk::Widget, @implements gio::ActionMap, gio::ActionGroup;
}


impl Widget {

    #[action]
    fn name(&self, action: &gio::Action, target: Option<&glib::Value>) {

    }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

    #[action]
    fn name(&self, action: &gio::Action, target: Option<&glib::Value>) {

    }

What a macro should emit here? Notice, that it is inside of an impl block and such block can contain functions or constants only. Nothing else.

Copy link
Member

@bilelmoussaoui bilelmoussaoui Jan 18, 2021

Choose a reason for hiding this comment

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

Does it have to emit anything actually? In my idea it was mostly used to identify the action functions so that you can register them later

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Of course, it should emit something. At least the function itself 😆 with stripped out attribute (otherwise it won't compile).

Copy link
Member

Choose a reason for hiding this comment

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

Ah yeah, that's what I meant, as in not do anything else other than what the function contains ^^

@andy128k andy128k force-pushed the action-map branch 3 times, most recently from 9837940 to b991f30 Compare January 21, 2021 03:08
@andy128k
Copy link
Contributor Author

Support of stateful actions is added.

@andy128k andy128k force-pushed the action-map branch 6 times, most recently from fe1a418 to c3e6eb9 Compare January 27, 2021 00:15
@andy128k
Copy link
Contributor Author

I've done what I planned to do with this macro (at least for the first iteration). 😅

There is one minor issue which is easy to resolve, but I'd like to have a feedback/advice before. Here is an excerpt from Gio documentation.

For stateful actions where the state type is equal to the parameter type, the default is to forward them directly to “change-state”. This should allow almost all users of GSimpleAction to connect only one handler or the other.

With current implementation of the macro this will not work.

#[action(name = "my-action", change-state)]
fn my_action_change_state(&self, state: T) -> Option<T> { ... }

Such declaration of a change-state handler will generate: gio::SimpleAction::new_stateful(None, T::default().to_variant()). So, there is no parameter type in the action's definition. Type of a parameter is taken from a handler of activate signal. So, to make it work, activate handler is needed.

#[action(name = "my-action")]
fn my_action_activate(&self, _state: T, parameter: T) -> Option<T> {
    Some(parameter)
}

#[action(name = "my-action", change-state)]
fn my_action_change_state(&self, state: T) -> Option<T> { ... }

Which is just an ugly boilerplate and cancels the original idea to have only one handler.

So, a possible solution is to add one more annotation to a change-state handler. Something like this:

#[action(name = "my-action", change-state, use-same-parameter-type-as-a-state-type)]
fn my_action_change_state(&self, state: T) -> Option<T> { ... }

But I cannot come up with a good name for it (use-same-parameter-type-as-a-state-type is nothing but a joke 😄).

@sdroege
Copy link
Member

sdroege commented Mar 14, 2021

@BrainBlasted does this look good to you API-wise now? I'm not doing much with actionmaps, so don't really know :)

@andy128k
Copy link
Contributor Author

Instead of introducing an opt in flag, I added an opt out one. So, an action parameter type is derived from the state type by default. To opt out a flag no_parameter should be specified.

@andy128k andy128k force-pushed the action-map branch 3 times, most recently from d06b02e to 7337cfc Compare April 4, 2021 01:20
@andy128k andy128k force-pushed the action-map branch 6 times, most recently from 6cd5754 to d8f0e5c Compare April 16, 2021 01:40
@andy128k andy128k force-pushed the action-map branch 2 times, most recently from 9c1afd1 to d51d88f Compare April 20, 2021 22:46
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants