antoyo / relm

Idiomatic, GTK+-based, GUI library, inspired by Elm, written in Rust
MIT License
2.41k stars 79 forks source link

[RFC] Planning the transition to GTK4 (relm4) #289

Closed AaronErhardt closed 2 years ago

AaronErhardt commented 3 years ago

I'm currently working on an app using relm and GTK3 but I'd like to port it to GTK4 before finishing the UI. And since several other people expressed interest in GTK4 support (#256) I tried to hack into relm to get it working with the soon to be released gtk4-rs.

Unfortunately there have been changes that prevent an easy transition to GTK4 for relm. Most importantly, the ContainerExt trait which is used a lot by relm doesn't exist any longer. This isn't an issue of gtk4-rs but rather GTK4 removed the Container type. Consequently this means that all layout containers have different interfaces now.

The question is, what's the best way to integrate GTK4 into relm, especially regarding the view macro? I had the following ideas:

  1. Use a crate to implement an abstraction trait for all layout containers. That'd be quite a lot of work and even implementing a simple add_child() method isn't straightforward for some containers.

  2. Follow the new GTK4 design and embrace different container interfaces. Here's an example of how a view macro could look like with this approach:

    view! {
    #[name="main_window"]
    gtk::Window {
    // calls set_titlebar() on main_window
    titlebar:
        #[name="titlebar"]
        gtk::HeaderBar {
            // calls pack_end() on titlebar
            pack_end: gtk::Button {
                    clicked => Msg::Clicked,
                },
            // calls pack_start() on titlebar
            pack_start:
                #[name="hello_button"]
                gtk::Button {
                    label: "Hello!",
                },
    
            title_widget:
                // calls set_title_widget() on titlebar
                #[name="title"]
                gtk::Box {
                    // calls append() on title
                    append:
                        #[name="title_label_1"]
                        gtk::Label {
                            label: "Title 1",
                        },
    
                    append:
                        #[name="title_label_2"]
                        gtk::Label {
                            label: "Title 2",
                        },
                },
        },
    
    // calls set_child() on main_window
    child:
        #[name="app"]
        gtk::Grid {
            // calls attach() on app
            attach: 
                (gtk::Label {
                    label: "Grid label",
                }, 1, 1, 2, 1),
            /* ... other UI elements ... */
    
        },
    }
    }

I prefer the second idea because it's more idiomatic GTK. Yet with my limited experience with proc macros and GTK I don't know whether it can be implemented like that and if there is a better solution. That's why I'd love to hear what you guys think about this :)

P.S. Here's the link to the current documentation of gtk4-rs

antoyo commented 3 years ago

Yeah, that issue is annoying indeed and I'm not sure what the solution should be.

I was wondering whether we could somehow leverage GtkBuilder or the Glade XML format. I see there's a add_child() method in GtkBuildableIface but I'm not sure how to access it and whether that would work for this use case.

Another idea would be to make the view! macro generate the Glade XML format: while I don't particularly like the idea of generating at compile-time something that would be parsed at run-time, that would solve a lot of issues like some widgets not working in the view! macro. Edit: now that I think of it, maybe it's a bad idea: I guess the XML names might not always match gtk-rs method names so how would we do the type checking?

What are your thoughts on those ideas? Are you familiar with these concepts?

AaronErhardt commented 3 years ago

I don't think XML makes sense in the view macro. The current view macro can not only build the UI, it also creates the widgets member of the model struct using the #[name=...] attribute and can update the view automatically to some extend. I can't image how that would work with XML and as you mentioned the XML names basically have to match the gtk4-rs counterparts for this to make even work.

GtkBuildable would actually be the only alternative to containers in GTK. But I don't think that's a good idea either. GtkBuildable wasn't intended for this use-case and this could potentially involve a lot of unsafe code to work.

Maybe Container traits need to be abandoned at this point. After having a rough look at the code of relm I can't say how important the Container traits are but maybe we can rewrite the parts that are using those traits currently.

Maybe the code could be even simplified by only resembling the existing GTK interfaces in an idiomatic manner. Something similar to my code example above. Ideally this would simply match the gtk4-rs interfaces, so that the proc macro just has to call member functions of a gtk widget. Like this for example:

view! {
#[name="main_window"]
gtk::Window {
    set_child: /* internally calls main_window.set_child(), expects gtk widget */,
    set_title: /* internally calls main_window.set_title(), expects Option<&str> */,
    .....
}

So overall less magic, but just a simple solution to access the interfaces of gtk4-rs. Also this could potentially resolve some of the current "workarounds" like nested view macros and the problems when creating something like popovers.

However, in general the code and the implementation of relm isn't yet fully clear to me so I can't confirm whether my solution would work :/

antoyo commented 3 years ago

Oh, the idea was that we would generate more code than just the XML. Code like:

builder.get_widget("button").connect_clicked(…)

Your last example looks like it would actually require the nested view! macro as in:

view! {
#[name="main_window"]
gtk::Window {
    set_child: view! {
        gtk::Button {
        }
    },
    set_title: /* internally calls main_window.set_title(), expects Option<&str> */,
    .....
}

And I'm not sure how that will solve the other issues.

Another idea would be to use the builders and have relm4 not require any macros (or less macros):

fn view(&self) -> gtk4::Widget {
    gtk4::WindowBuilder::new()
        .title("Title")
        .child(gtk4::ButtonBuilder::new()
            .label("Button")
            .build()
        )
        .build()
}

with a few helper functions to bind properties (maybe even using gtk binding system) and connect events maybe. That would be a big redesign of relm, but might end up being much simpler.

To be honest, I don't have much time to work on relm, these days, but if you want to experiment with the latter approach, you could try incorporating the core of relm in it to see how to do event handling and model management (see this example to learn how to use this core module).

AaronErhardt commented 3 years ago

Thanks for linking the resources. I guess it's a good idea to port the core first, anyway. I'll experiment a little bit over the next days and will keep you up to date with my progress :)

AaronErhardt commented 3 years ago

Hi, I'm back.

It took me longer than expected to finish this because I had to work on some other stuff but now I've got an experimental version of relm working with GTK4. Initially, I planned to rewrite relm but I found it the core API rather unnecessarily complicated (but maybe that's for a reason, I'm no expert of the current relm version). And when I found out that EventStream has a similar implementation in the latest glib bindings I thought I might as well try it out.

And it did work pretty well. I came out with a basic reimplementation of relm in about 100 lines of code. The design has changed a bit but the idea is still the same. Here's the code: https://github.com/AaronErhardt/relm4

I've seen that @MGlolenstine worked on porting relm as well. I think it would make sense to work together on this. I don't think I can contribute a lot over the next weeks due to upcoming exams but I think collaboration makes a lot more sense that porting relm twice :)

antoyo commented 3 years ago

Using Channel is indeed much simpler, but I believe you could get the cyclic reference problem that caused a memory leak in relm before. It seems there's no way to get a weak reference to it. Morever it might be less efficient than the implementation in relm since it uses a Mutex (but that's probably okay, especially since that would allow us to get rid of the Channel type in relm).

The reason it's more complicated in relm is because glib::Channel hides all of the complexity for you ;) .

I pointed @MGlolenstine to here so you two can coordinate.

AaronErhardt commented 2 years ago

I think this issue can be closed now since the first beta versions of Relm4 are available.