gtk-rs / gtk3-rs

Rust bindings for GTK 3
https://gtk-rs.org
MIT License
508 stars 90 forks source link

Add automatic signal connection in combination with gtk::Builder #128

Open sdroege opened 4 years ago

sdroege commented 4 years ago

The idea here would be to annotate the signal handler functions with a procedural macro that would export the signature of the function so it can be checked at runtime.

E.g.

#[gtk::signal_handler("my_button_clicked")]
fn button_clicked_handler(&self, button: &gtk::Button) { ... }

This would then expand to something like

#[no_mangle]
pub fn gtk_rs_priv_my_button_clicked(&self, args: &[glib::Value]) -> Option<glib::Value> {
    let button = args[0].get::<gtk::Button>().expect("Wrong type").expect("Unexpected None");
    self.button_clicked_handler(&button);
}

fn button_clicked_handler(&self, button: &gtk::Button) { ... }

The nullability of the arguments would have to be detected/handled automatically based on the types.

Now for connecting the whole thing we would do a new function on BuilderExt that calls builder.connect_signals(), assumes/requires that &self is cloneable, and then via dlsym (etc) checks for each name if gtk_rs_priv_NAME does exist, e.g. gtk_rs_priv_my_button_clicked.

Instead of only supporting a &self first parameter we should also allow arbitrary other types that are cloneable, which shouldn't be too hard in addition either.

sdroege commented 4 years ago

Could maybe make use of https://crates.io/crates/label for this

piegamesde commented 3 years ago

The basic problem is that we have a function with a fixed signature of dynamic types that needs to downcast to static types and call the special function. My current best solution to this is by implementing a common trait for that function:

pub trait SignalCallback {
    fn call(&self, args: &[glib::Value]) -> Option<glib::Value>;
}

impl SignalCallback for Fn<&gtk::Button> {
    fn call(&self, args: &[glib::Value]) -> Option<glib::Value> {
        let button = args[0].get::<gtk::Button>().expect("Wrong type").expect("Unexpected None");
        self(&button);
        None
    }
}

…

let callback: impl SignalCallback = button_clicked_handler as SignalCallback;

Obviously this has a few drawbacks and I left out a few important things like the actual self parameter of the callback. You can do this either by adding this: Any or by making the trait itself generic: SimpleCallback<Button>.

piegamesde commented 3 years ago

Some prior art: https://idanarye.github.io/woab/woab/derive.BuilderSignal.html

It's a derive macro that for an enum with one variant per signal type.

ids1024 commented 3 years ago

Looking at pygobject, this seems to be done with gtk_widget_class_set_connect_func on GTK3 and GtkBuilderScope with GTK4.

Could maybe make use of https://crates.io/crates/label for this

That uses the ctor crate. Not sure if that limits platform compatibility in an important way, but the approach seems potentially problematic: https://github.com/jonay2000/label/issues/28

It seems better to instead use a proc macro on the impl block, with something like this:

#[gtk::composite_template]
impl Foo {
    #[signal]
    foo_bar(&self, widget: &Self::Class) {}
}

Though if it's on impl Foo instead of impl WidgetImpl for Foo, that also leaves the issue of how to automatically call the function registering the signals, other than explicitly like in https://github.com/gtk-rs/gtk-rs/pull/269.

jdonszelmann commented 3 years ago

Hi, I'm the maker of that crate :). The reason it uses ctor is because otherwise you can't really register some item in code against some other part of the code. How would using a proc macro on the impl block change this? Luckily ctor is quite cross-platform

image

What's unfortunate about it is that explicit support for every platform has to be added. Most platforms just do it the same way linux does, but that needs to be reassessed for each platform. Luckily this does seem quite easy image

ids1024 commented 3 years ago

How would using a proc macro on the impl block change this?

My idea is that with the example code I gave, every function being registered would be within the same block, processed through one invocation of a gtk::composite_template macro. So there would be no need to share global state between different invocations of a macro, and it can be handled at once.

jdonszelmann commented 3 years ago

ah, you just make some local state for every implementation. And you annotate the functions you want. Though It'd be awesome to see my library used in some larger project, that's I think an objectively better approach.