gtk3-rs
gtk3-rs copied to clipboard
Add automatic signal connection in combination with gtk::Builder
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: >k::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: >k::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.
Could maybe make use of https://crates.io/crates/label for this
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<>k::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>
.
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.
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.
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
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
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.
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.