Git Product home page Git Product logo

relm4's Introduction

CI Matrix Relm4 on crates.io Relm4 docs Relm4 book Minimum Rust version 1.75 dependency status

An idiomatic GUI library inspired by Elm and based on gtk4-rs. Relm4 is a new version of relm that's built from scratch and is compatible with GTK4 and libadwaita.

Why Relm4

We believe that GUI development should be easy, productive and delightful.
The gtk4-rs crate already provides everything you need to write modern, beautiful and cross-platform applications. Built on top of this foundation, Relm4 makes developing more idiomatic, simpler and faster and enables you to become productive in just a few hours.

Our goals

  • ⏱️ Productivity
  • Simplicity
  • 📎 Outstanding documentation
  • 🔧 Maintainability

Documentation

Dependencies

Relm4 depends on GTK4: How to install GTK4 and Rust

Ecosystem

Use this in to your Cargo.toml:

# Core library
relm4 = "0.8"
# Optional: reusable components
relm4-components = "0.8"
# Optional: icons (more info at https://github.com/Relm4/icons)
relm4-icons = "0.8.0"

Features

The relm4 crate has four feature flags:

Flag Purpose Default
macros Enable macros by re-exporting relm4-macros
libadwaita Improved support for libadwaita -
libpanel Improved support for libpanel -
gnome_46 Enable all version feature flags of all dependencies to match the GNOME 46 SDK -
gnome_45 Enable all version feature flags of all dependencies to match the GNOME 45 SDK -
gnome_44 Enable all version feature flags of all dependencies to match the GNOME 44 SDK -
gnome_43 Enable all version feature flags of all dependencies to match the GNOME 43 SDK -
gnome_42 Enable all version feature flags of all dependencies to match the GNOME 42 SDK

The macros feature is a default feature.

Examples

Several example applications are available at examples/.

A simple counter app

Simple app screenshot light Simple app screenshot dark

use gtk::prelude::*;
use relm4::prelude::*;

struct App {
    counter: u8,
}

#[derive(Debug)]
enum Msg {
    Increment,
    Decrement,
}

#[relm4::component]
impl SimpleComponent for App {
    type Init = u8;
    type Input = Msg;
    type Output = ();

    view! {
        gtk::Window {
            set_title: Some("Simple app"),
            set_default_size: (300, 100),

            gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
                set_spacing: 5,
                set_margin_all: 5,

                gtk::Button {
                    set_label: "Increment",
                    connect_clicked => Msg::Increment,
                },

                gtk::Button {
                    set_label: "Decrement",
                    connect_clicked => Msg::Decrement,
                },

                gtk::Label {
                    #[watch]
                    set_label: &format!("Counter: {}", model.counter),
                    set_margin_all: 5,
                }
            }
        }
    }

    // Initialize the component.
    fn init(
        counter: Self::Init,
        root: Self::Root,
        sender: ComponentSender<Self>,
    ) -> ComponentParts<Self> {
        let model = App { counter };

        // Insert the code generation of the view! macro here
        let widgets = view_output!();

        ComponentParts { model, widgets }
    }

    fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
        match msg {
            Msg::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            Msg::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
    }
}

fn main() {
    let app = RelmApp::new("relm4.example.simple");
    app.run::<App>(0);
}

Projects using Relm4

License

Licensed under either of

at your option.

Contribution

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Feedback and contributions are highly appreciated!

relm4's People

Contributors

aaronerhardt avatar ayush1325 avatar azymohliad avatar chriskilding avatar crsov avatar dependabot[bot] avatar edfloreshz avatar ejaa3 avatar euclio avatar kianmeng avatar lynnesbian avatar maksymshcherbak avatar marmistrz avatar mglolenstine avatar mmstick avatar mskorkowski avatar myuujiku avatar paveloom avatar pentamassiv avatar pjungkamp avatar sanpii avatar sashinexists avatar sgued avatar songww avatar tronta avatar tsmweb avatar vlinkz avatar vorot93 avatar weclaw1 avatar wizard-28 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

relm4's Issues

New component macro does not allow using watch! on the root widget

In the 'simple' example, I'd like to change the window title to always show the counter value.

view! {
        gtk::Window {
            set_title: watch!(Some(&model.counter.to_string())),
            set_default_width: 300,
            set_default_height: 100,
      ...

The component macro does not allow it though:

error[E0609]: no field `_gtk_window_4` on type `&mut AppWidgets`
  --> src/main.rs:25:9
   |
25 |         gtk::Window {
   |         ^^^ unknown field
   |
   = note: available fields are: `_gtk_box_3`, `_gtk_button_0`, `_gtk_button_1`, `_gtk_label_2`

This behavior was possible in 0.4 version.

`try_remove ()` may return true when nothing is removed

WidgetPlus::try_remove() may return true when nothing is removed. It only checks that self is one of a set number of supported widgets, and then calls the appropriate remove() function. However, there are no guarantees that the child is actually removed. Each of these APIs are a no-op when you pass a widget that's not one of their children, and it's still possible to do that with try_remove().

How to use #[relm4_macros::widget] with initializer

I want to add a scale widget. How can I add this in the macro with the proper initialization

pub fn with_range(
    orientation: Orientation,
    min: f64,
    max: f64,
    step: f64
) -> Scale

At least I have not seen how I can add those parameters afterwards.
Maybe you could add in the documentation how to do that.

Idea for book: responsive design GTK example?

I think the first thing that pops into my brain looking at trackers is, "How do I make a responsive layout that looks good when on a small screen like PinePhone device and desktop?". Example could be like a calculator that looks good when window size is small and different when big.

components are appended after the gtk widgets

I'm learning rust so please be lenient with me...

When appending components to the gtk::Box together with other gtk::widgets, components are added at the end.

#[widget]
impl Widgets<MainWindowModel, ()> for MainWindow {
    view! {
        gtk::ApplicationWindow {
            set_title: Some("main window"),
            set_default_width: 600,
            set_default_height: 300,

            set_child = Some(&gtk::Box) {
                set_margin_all: 0,
                set_orientation: gtk::Orientation::Horizontal,
                
                // append = &gtk::Box {
                    // set_orientation: gtk::Orientation::Vertical,

                    append: component!(components.profile_view.root_widget()),
                // },
                append = &gtk::Paned {
                    set_orientation: gtk::Orientation::Horizontal,
                    set_position: 250,
                    set_shrink_start_child: false,
                    set_shrink_end_child: false,
                    set_start_child = &gtk::Button{
                        set_label: "sources",
                        set_hexpand: false,

                    },
                    set_end_child = &gtk::Paned {
                        set_orientation: gtk::Orientation::Horizontal,
                        set_position: 250,
                        set_shrink_start_child: false,
                        set_shrink_end_child: false,
                        set_start_child = &gtk::Button {
                            set_label: "items",
                        },
                        set_end_child: component!(components.item_view.root_widget()),

                    }

                },
                
                
            }
        }
    }
}

Leads to the main window looking like:

main_window_orig

While I would expect

component_in_a_box

Which can be achieved by uncommenting the box wrapping around the append: component!(components.profile_view.root_widget()),.

My profile view widget looks like

#[widget(pub)]
impl<ParentModel> Widgets<ProfileViewModel, ParentModel> for ProfileView 
    where
        ParentModel: ProfileViewParent,
        ParentModel::Widgets: ProfileViewParentWidgets,
{
    
    view!{
        gtk::Box{
            set_orientation: gtk::Orientation::Vertical,
            set_margin_all: 5,
            set_spacing: 5,
            factory!(model.profiles),
        }
    }
}

Since root of the ProfileView is a vertical gtk::Box I would expect wrapping it with another vertical box to be idempotent in terms of view (ignoring margins, paddings, borders, etc...).


Factory for profile buttons:

impl FactoryPrototype for ProfileViewItem {
    type Factory = FactoryVecDeque<Self>;
    type Widgets = ProfileViewItemWidgets;
    type Root = gtk::Button;
    type View = gtk::Box;
    type Msg = ProfileViewMsg;

    fn position(&self, _index: &Rc<DynamicIndex>) {}

    fn update(&self, _index: &Rc<DynamicIndex>, _widgets: &ProfileViewItemWidgets) {}

    fn get_root(widgets: &ProfileViewItemWidgets) -> &gtk::Button {
        &widgets.profile_button
    }

    fn generate(
        &self,
        _index: &Rc<DynamicIndex>,
        _sender: Sender<ProfileViewMsg>
    ) -> ProfileViewItemWidgets {
        let mut graphemes = UnicodeSegmentation::graphemes(
            self.profile.name.as_str(),
            true,
        );
        let c = match graphemes.next() {
            Some(v) => v,
            None => "?"
        };

        let profile_button = gtk::Button::with_label(c);

        profile_button.add_css_class("flat");

        ProfileViewItemWidgets {
            profile_button,
        }
    }
}

micro components

Let's take micro components discussion here.

To make live easier I'm pasting original comments about the micro component way of working here:

I already mentioned "micro components". In this case two micro components would have to share state (using Rc<RefCell> which usually works fine). If micro components then had an update_view() method, you wouldn't even need to send a message but could just call that method.
For this a tracker that supports multiple views would also be interesting for efficient UI updates.

Anyway, to make my idea more tangible, I've written down some Rust-like code that represents the concept of "micro-components". Micro-components are "loose" so they need the parent to attach the root widget to its widgets. They don't need to store a clone of their parents sender but have a "data" field that holds (currently immutable) data that can be used to store senders and other stuff according to the needs of the user (to make them more flexible). They can easily be modified by their parent component and also handle messages. The view function can be called manually by using update_view(). Of course there are some possible panics but they should be easy to debug and the only other option to RefCell would be unsafe code.

What do you think? Would that make things easier for you? Is there a use-case that isn't properly covered?

struct MicroComponent<Model: MicroModel> {
   model: Rc<RefCell<Model>>,
   widgets: Rc<RefCell<Model::Widgets>>,
   data: Mode::Data, // for additional data such as senders to other components
   sender: Sender<Model::Msg>,
}

impl<Model: MicroModel> MicroComponent<Model> {
   fn new(model: Model, data: Model::Data) -> Self { ... }
   fn update_view(&self) { ... }
   fn model(&self) -> &Model { ... }
   fn model_mut(&self) -> &mut Model { ... }
   fn widgets(&self) -> &Model::Widgets { ... }
   fn send(&self, msg: Model::Msg) { ... }
   fn sender(&self) -> Sender<Model::Msg> { ... }
}

trait MicroModel {
   type Msg;
   type Widgets: MicroWidgets<Self>;
   type Data;
   
   fn update(&mut self, msg: Self::Msg, data: &Data, sender: Sender<Self::Msg>);
}

trait MicroWidgets<Model: MicroModel> {
   type Root;
   
   fn init_view(&mut self, model: &Model, sender: Sender<Mode::Msg>) -> Self;
   fn view(&mut self, model: &Model, sender: Sender<Mode::Msg>);
   fn root_widget(&self) -> Self::Root;
}

Unable to use expressions that start with literals in the view macro

I can't use expressions that start with literals in the view macro, after the :, for some reason.

relm4::view! {
        mut vec = Vec::new() {
            push: "foo".to_string(),
        }
    }

This fails:

error: expected `,`. Did you confuse `=` with`:`?
  --> src\main.rs:57:24
   |
57 |             push: "foo".to_string(),
   |                        ^

Any other usage of literals seems to work fine, the following examples compile:

let s = "foo".to_string();
relm4::view! {
    mut vec = Vec::new() {
        push: s, // variable
    }
}
relm4::view! {
        mut vec = Vec::new() {
            push: (|| "foo".to_string())(), // closure (conditions and loops also work)
        }
    }
relm4::view! {
        mut vec = Vec::new() {
            push: "foo", // just the literal
        }
    }

The same problem with number and boolean literals.
The issue exists on both 0.4 and the latest git.

Do not use gtk::Application's command-line argument handling by default

relm4 invokes gtk::Application::run which will attempt to automatically handle command-line arguments from the environment. This leads to the surprising behavior that if you supply any arguments to a relm4 application, you'll get a Glib-GIO-CRITICAL warning "This application can not open files." and the application will immediately exit.

I'd like to propose that RelmApp::run instead invokes gtk::Application::run_with_args(&[]), and provide a RelmApp::run_with_args() for users that want to opt-in to gtk::Application's argument handling.

If this sounds reasonable, I'm happy to open a PR.

Revive relm4-store

  • Test a 0.5 beta version in relm4-store to find potential shortcomings in the API

Support for dynamically created components ?

As far as I am aware, factory collections exist in relm4 to create dynamic view and manage them in various layouts, but the dynamically created widgets can be complicated, so I want to create them into a component and build them with the view! macro. I wonder if it's possible to use factory collection in conjunction with component ?

Inconsistent behavior between FactoryVec and FactoryVecDeque

I'm trying to create a widget that adds and removes an arbitrary number of children at the position before some hard-coded widgets. I tried modifying the factory example like so:

        gtk::ApplicationWindow {
            set_default_width: 300,
            set_default_height: 200,
            set_child = Some(&gtk::Box) {
                set_orientation: gtk::Orientation::Vertical,
                set_margin_all: 5,
                set_spacing: 5,
                factory!(model.counters),
                append = &gtk::Button {
                    set_label: "Add",
                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Add);
                    }
                },
                append = &gtk::Button {
                    set_label: "Remove",
                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Remove);
                    }
                },
                append = &gtk::Button {
                    set_label: "I'M LAST",
                },
            }
        }

If I use FactoryVec for my storage, the new widgets are added and removed at the last position (after the "I'M LAST" label). However, if I use FactoryVecDeque for my storage, the widgets are added at the first position (before the "Add" button). The latter behavior happens to be what I want, but I wanted to open this issue to see if this is something I can rely on, or if there's a better way to accomplish what I want.

Relm4 logo is cut off on its right

Hello,

Thanks for this crate, I am interested in using it in the near future for GTK app development.

One of the first things that I noticed in the README is that the SVG logo is cut off on the right side. This may be because the font rendering on my system differs from yours. One solution would be to convert the text to paths (if not done yet) in order to be sure that the rendering is system-independent.

Here is a screenshot of the SVG file from Github, with the cutoff on its right side:

Capture d’écran de 2021-08-13 11-47-05

Thanks in advance for your time.

`component!` doesn't work with method calls

In the components section of the book is shown how to add a component using the property syntax.
i.e.

main_window = gtk::ApplicationWindow {
    set_titlebar: component!(Some(components.header.root_widget())),
}

When trying to do the same for widgets that add children with a method like append the macro complains about expecting a curly bracket.
i.e.

gtk::Box {
    append = component!(components.0.root_widget()),
}

"Advanced factories" example doesn't compile

I copy-pasted the whole "Advanced factories" example code from https://aaronerhardt.github.io/relm4-book/book/factory_advanced.html#the-complete-code into a test project, invoked cargo run and got the following errors:

error[E0053]: method `generate` has an incompatible type for trait
  --> src/main.rs:97:31
   |
97 |     fn generate(&self, index: &Rc<DynamicIndex>, sender: Sender<AppMsg>) -> FactoryWidgets {
   |                               ^^^^^^^^^^^^^^^^^
   |                               |
   |                               expected struct `DynamicIndex`, found struct `Rc`
   |                               help: change the parameter type to match the trait: `&DynamicIndex`
   |
   = note: expected fn pointer `fn(&Counter, &DynamicIndex, relm4::Sender<_>) -> FactoryWidgets`
              found fn pointer `fn(&Counter, &Rc<DynamicIndex>, relm4::Sender<_>) -> FactoryWidgets`

error[E0053]: method `position` has an incompatible type for trait
   --> src/main.rs:149:32
    |
149 |     fn position(&self, _index: &Rc<DynamicIndex>) {}
    |                                ^^^^^^^^^^^^^^^^^
    |                                |
    |                                expected struct `DynamicIndex`, found struct `Rc`
    |                                help: change the parameter type to match the trait: `&DynamicIndex`
    |
    = note: expected fn pointer `fn(&Counter, &DynamicIndex)`
               found fn pointer `fn(&Counter, &Rc<DynamicIndex>)`

error[E0053]: method `update` has an incompatible type for trait
   --> src/main.rs:151:30
    |
151 |     fn update(&self, _index: &Rc<DynamicIndex>, widgets: &FactoryWidgets) {
    |                              ^^^^^^^^^^^^^^^^^
    |                              |
    |                              expected struct `DynamicIndex`, found struct `Rc`
    |                              help: change the parameter type to match the trait: `&DynamicIndex`
    |
    = note: expected fn pointer `fn(&Counter, &DynamicIndex, &FactoryWidgets)`
               found fn pointer `fn(&Counter, &Rc<DynamicIndex>, &FactoryWidgets)`

`Rc<DynamicIndex>` cannot be used `with_new_thread`

I was trying to setup somethig like the following:

AppModel (App)
|-- DataModel (Component)

AppMsg (AppModel::Msg) contains a Weak<DynamicIndex> in one of the possible messages because I have a FactoryVecDeque. That means I cannot relm4::RelmComponent::with_new_thread(...) for DataModel since ParentModel::Msg: !Send.

My workaround for that was to create an intermediate component InterModel such that InterModel::Msg: Send because it does not need Weak / Rc fields. Like this:

AppModel (App)
|-- InterModel (Component)
|---- DataModel (Component)

The only drawback that I find is that I have to "repeat" a message to comunicate from AppModel -> DataModel and viceversa (through InterModel).

Is this approach OK? or is there a more idiomatic way to achieve this?

PS: I looked into Message Handlers, but it seemed that I would have had to spawn a new thread, that also required AppModel::Msg: Send.

failed to load source for dependency `struct-tracker`

I tried running one of the examples as is and got the following:

cargo run --example trackers
    Updating crates.io index
    Updating git repository `https://github.com/AaronErhardt/Struct-Tracker`
error: failed to get `struct-tracker` as a dependency of package `relm4 v0.1.0 (/var/home/peter/relm4)`

Caused by:
  failed to load source for dependency `struct-tracker`

Caused by:
  Unable to update https://github.com/AaronErhardt/Struct-Tracker

Caused by:
  failed to find branch `master`

Caused by:
  cannot locate remote-tracking branch 'origin/master'; class=Reference (4); code=NotFound (-3)

Port examples to the "next" branch

The bold and italic entries are most important, the rest could be replaced or added later. The libadwaita examples (not listed here) should be ported as some point, too.

  • actions
  • alert
  • components
  • components_old
  • drawing
  • entry
  • entry_tracker
  • factory
  • factory_advanced
  • factory_advanced_manual
  • factory_manual
  • future
  • grid_factory
  • list
  • macro_reference
  • menu
  • micro_components
  • micro_components_manual
  • non_blocking_async
  • non_blocking_async_manual
  • open_button
  • popover
  • save_dialog
  • simple
  • simple_manual
  • stack
  • stack_factory
  • stack_switcher
  • stateful_msg_handler
  • to_do
  • tokio
  • tracker

relm4_macros: Assign multiple top level widgets in a view

I have custom widgets which can't be assigned inside the main view of a new custom widget, because you can't append a custom widget while also assigning it to a variable. Mainly because these require &* instead of & to deref to a GTK widget.

relm4_macros::view! {
    model_info = InfoLabel {
        set_description: &crate::fl!("model-and-version"),
        set_value: &crate::fl!("unknown")
    }
}

relm4_macros::view! {
    serial_info = InfoLabel {
        set_description: &crate::fl!("serial-number"),
        set_value: &crate::fl!("unknown")
    }
}

relm4_macros::view! {
    os_info = InfoLabel {
        set_description: &crate::fl!("os-version"),
        set_value: &crate::fl!("unknown")
    }
}

relm4_macros::view! {
    root = gtk::Box {
        set_hexpand: true,
        append: image = &gtk::Image {

        },

        append: list = &gtk::ListBox {
            append: &*model_info,

            append: &*serial_info,
            append: &*os_info,
        }
    }
}

InfoLabel:

use relm4::gtk::{self, prelude::*};

// Uses shrinkwraprs crate
#[derive(Debug, Shrinkwrap)]
pub struct InfoLabel {
    #[shrinkwrap(main_field)]
    root: gtk::Box,
    description: gtk::Label,
    value: gtk::Label
}

impl Default for InfoLabel {
    fn default() -> Self {
        relm4_macros::view! {
            root = info_box() -> gtk::Box {
                append: description = &description_label() -> gtk::Label {},
    
                append: value = &gtk::Label {
                    set_halign: gtk::Align::End,
                    set_valign: gtk::Align::Center,
                }
            }
        }

        Self { root, description, value }
    }
}

impl InfoLabel {
    pub fn set_description(&self, description: &str) {
        self.description.set_text(description);
    }

    pub fn set_value(&self, value: &str) {
        self.value.set_text(value);
    }
}

pub fn description_label() -> gtk::Label {
    relm4_macros::view! {
        label = gtk::Label {
            set_halign: gtk::Align::Start,
            set_hexpand: true,
            set_valign: gtk::Align::Center,
            set_ellipsize: gtk::pango::EllipsizeMode::End,
        }
    }

    label
}

pub fn info_box() -> gtk::Box {
    relm4_macros::view! {
        container = gtk::Box {
            set_orientation: gtk::Orientation::Horizontal,
            set_margin_start: 20,
            set_margin_end: 20,
            set_margin_top: 8,
            set_margin_bottom: 8,
            set_spacing: 24
        }
    }

    container
}

How can I get the value of the Entry?

I want to get the user_name and passwords' values when I click the btn submit.

                 append = &gtk::Entry {
                        //set_buffer: &model.entry,
                        set_tooltip_text: Some("username"),
                        connect_activate(sender) => move |_| {
                            //send!(sender, AppMsg::get_user_name);
                        },
                    },
                 },

I want to get the Entry value when I click the sumit btn.

             append = &gtk::Box {
                    set_orientation: gtk::Orientation::Horizontal,
                    set_spacing: 10,
                    append = &gtk::Button {
                        set_label: "login",
                        connect_clicked(sender) => move |_| {
                           // **how can I get the Entry value here?**
                            send!(sender, AppMsg::Login((model.user_name, model.password)));
                        },
                    },
                    append = &gtk::Button {
                        set_label: "reg",
                        connect_clicked(sender) => move |_| {
                            send!(sender, AppMsg::Reg);
                        },
                    },
                },

relm4-components fails to build

When I build the master branch I receive an error in alert component that default is undefined in MessageDialog

no function or associated item named `default` found for struct `MessageDialog` in the current scope

Expanded AlertWidgets

    /// Widgets of the alert component
    pub struct AlertWidgets {
        #[allow(missing_docs)]
        pub dialog: gtk::MessageDialog,
    }
    impl<ParentModel, Conf> relm4::Widgets<AlertModel<Conf>, ParentModel> for AlertWidgets
    where
        ParentModel: AlertParent,
        ParentModel::Widgets: ParentWindow,
        Conf: AlertConfig,
    {
        type Root = gtk::MessageDialog;
        /// Initialize the UI.
        fn init_view(
            model: &AlertModel<Conf>,
            parent_widgets: &<ParentModel as ::relm4::Model>::Widgets,
            sender: ::gtk::glib::Sender<<AlertModel<Conf> as ::relm4::Model>::Msg>,
        ) -> Self {
            let dialog = gtk::MessageDialog::default();
            dialog.set_transient_for(parent_widgets.parent_window().as_ref());
            dialog.set_message_type(gtk::MessageType::Question);
            dialog.set_visible(model.is_active);
            dialog.set_text(Some(model.settings.text));
            dialog.set_secondary_text(model.settings.secondary_text.as_deref());
            dialog.set_modal(model.settings.is_modal);
            dialog.add_button(model.settings.confirm_label, gtk::ResponseType::Accept);
            dialog.add_button(model.settings.cancel_label, gtk::ResponseType::Cancel);
            {
                #[allow(clippy::redundant_clone)]
                let sender = sender.clone();
                dialog.connect_response(move |_, response| {
                    sender
                        .send(AlertMsg::Response(response))
                        .expect("Receiver was dropped!");
                });
            }
            if let Some(option_label) = &model.settings.option_label {
                dialog.add_button(option_label, gtk::ResponseType::Other(0));
            }
            if model.settings.destructive_accept {
                let accept_widget = dialog
                    .widget_for_response(gtk::ResponseType::Accept)
                    .expect("No button for accept response set");
                accept_widget.add_css_class("destructive-action");
            }
            Self { dialog }
        }
        fn connect_components(
            &self,
            model: &AlertModel<Conf>,
            components: &<AlertModel<Conf> as ::relm4::Model>::Components,
        ) {
        }
        /// Return the root widget.
        fn root_widget(&self) -> Self::Root {
            self.dialog.clone()
        }
        /// Update the view to represent the updated model.
        fn view(
            &mut self,
            model: &AlertModel<Conf>,
            sender: ::gtk::glib::Sender<<AlertModel<Conf> as ::relm4::Model>::Msg>,
        ) {
            self.dialog.set_visible(model.is_active);
        }
    }
}

Only default being called is in the first line of init_view. As gtk-rs docs indicate MessageDialog doens't have default method.

Visuals not going back

Today while I was playing with components example (cargo run --example components --package=relm4-examples) if found an issue where toggle buttons are not going back.

If I click buttons slowly click button-pause-click other button-pause-click other button-pause... then all works fine. I've wasted for this test 5min to check if all is working fine.

Screenshot from 2021-11-05 23-23-40
Screenshot from 2021-11-05 23-24-24
Screenshot from 2021-11-05 23-24-41

But when I start clicking fast on buttons. Sometimes clicking twice. Go back and forth between them. In a few seconds I'm ending up with

Screenshot from 2021-11-05 23-25-34
Screenshot from 2021-11-05 23-25-56
Screenshot from 2021-11-05 23-26-17
Screenshot from 2021-11-05 23-26-30

Something similar happens when I scroll fast. In list example (cargo run --example list --package=relm4-examples) if I use a mouse wheel to scroll over the minimum position of the list I get this nice gtk glow at the top hinting hey, that's it (I can't capture it since it will disappear before I can take a screen shot).

If I use a mouse wheel to go fast over the minimum position of the list, then the glow will become permanent. Using mouse wheel I can select the level of the hint and now I know that there are 6 of them (including lack of hint). What's more if ui ends in a state when the hint is permanent on top then it's also in permanent on bottom.

Screenshot from 2021-11-05 23-27-24
Screenshot from 2021-11-05 23-28-08
Screenshot from 2021-11-06 00-10-23

Last image was captured with focus being on input after typing a few letters

Screenshot from 2021-11-05 23-29-01

relm4_macros: view needs parentheses for basic equality statements

This is an annoying quirk in relm4_macros::view!, which is easier to explain with examples.

This does not work:

#[derive(PartialEq, Eq)]
enum ExampleEnum {
	A,
	B
}

fn example(value: ExampleEnum) -> gtk4::CheckButton {
	view! {
		checkbox = gtk4::CheckButton {
			set_active: value == ExampleEnum::A
		}
	}
	checkbox
}

It errors like this:

error: expected identifier
  --> applets/cosmic-applet-graphics/src/main.rs:99:54
   |
99 |                         set_active: current_graphics == Graphics::Integrated,
   |

However, if you add parentheses around it, it works fine:

#![allow(unused_parens, clippy::double_parens)] // needed for a quirk in the view! macro

#[derive(PartialEq, Eq)]
enum ExampleEnum {
	A,
	B
}

fn example(value: ExampleEnum) -> gtk4::CheckButton {
	view! {
		checkbox = gtk4::CheckButton {
			set_active: (value == ExampleEnum::A)
		}
	}
	checkbox
}

This, while it doesn't impact functionality, is rather annoying to have deal with, especially disabling otherwise useful lints to avoid getting warnings.

I use relm 4 0.2.1 ,menu demo doesn't exist.

example/menu.rs

I find this demo in the 0.4.0-beta.3 。

I copy it into my project, but this method doesn't exist.

menu! {

image

How can I pass data to the main_menu

add_child: intro_label = &gtk::Box {
append = &gtk::MenuButton {
set_menu_model: Some(&main_menu),
},
},

New component macro is dependent on a variable name 'root'

The 'simple' example compiles, but when I change name of the variable 'root' to anything else, it doesn't.

  // Initialize the UI.
    fn init_parts(
        counter: Self::InitParams,
        window: &Self::Root, // changed 'root' to 'window'
        input: &mut Sender<Self::Input>,
        _output: &mut Sender<Self::Output>,
    ) -> ComponentParts<Self, Self::Widgets> {

Error:

error[E0425]: cannot find value `root` in this scope
  --> src/main.rs:25:9
   |
25 |         gtk::Window {
   |         ^^^ not found in this scope

For more information about this error, try `rustc --explain E0425`.

Support adw::TabView properly

I tried to get started with relm4 by porting one of my applications, but unfortunately I hit a major roadblock in form of adw::TabView, which proved to be challenging to fit into relm4's scaffold. The natural relm4 way to operate a TabView seems to be through factories:

struct Page {
    foo: String,
}

#[relm4::factory_prototype]
impl FactoryPrototype for Page {
    type Factory = FactoryVec<Self>;
    type Widgets = FactoryWidgets;
    type View = adw::TabView;
    type Msg = AppMsg;

    view! {
        gtk::Label {
            set_label: &self.foo,
        }
    }

    fn position(&self, _index: &usize) {}
}

struct AppModel {
    pages: FactoryVec<Page>,
}

#[relm4::widget]
impl Widgets<AppModel, ()> for AppWidgets {
    view! {
        /* ... */
        &gtk::Box {
            set_orientation: gtk::Orientation::Vertical,

            append = &adw::TabBar {
                set_autohide: false,
                set_view: Some(&tab_view),
            },

            append: tab_view = &adw::TabView {
                factory!(model.pages),
            },
        },
        /* ... */
    }
}

However, there are two problems. The first one is that you want your Page prototype to control not just the page's child widget, but also the properties of the adw::TabPage itself, such as title or loading or icon, ideally though a mechanism similar to view! {} with change tracking and all. This could be worked around by giving the prototype some way to access to adw::TabPage from view(); something similar is needed for dialog components that get access to their parent's window.

The second problem is unfortunately much more difficult. adw::TabView is far from just a view; it has its own internal model of the tabs, which it actively manages. An adw::TabBar lets the user close, rearrange, detach, attach, move to and from a different tab view, any tab, which needs to somehow be reflected back onto our AppModel's pages field. adw::TabView provides signals such as page-attached, page-detached, page-reordered that notify us of the changes that we need to propagate to our own pages, however:

  • Changing our own pages will result in the factory trying to "apply" these changes to the TabView "again". There are a few widgets that already have this issue, e.g. gtk::DropDown, which in this relm4 example is handled by listening to its notify callback and reflecting the changes on the model. Here double-state-application is not a problem because gtk::DropDown will simply check that we're setting it the same value and avoid emitting the notify callback, and thus an infinite loop. adw::TabView has much more complex interactions that need careful handling.
  • adw::TabView operates on adw::TabPages and their child widgets: you can look up an adw::TabPage by the child widget, and you get an adw::TabPage from the callbacks. This corresponds to the GTK architecture where widgets contain all their state, but not to the relm4 architecture where the state (the model) is separated from the view (the widget). Therefore there needs to be some way to get the model back from an adw::TabPage (and it needs to support receiving a TabPage from some other TabView elsewhere in the app). One idea I came up with is a wrapper bin widget that stores a pointer to the model (i.e. as a OnceCell<Box<dyn Any>>) and allows relm4 to retrieve it back; I'm not sure if that's the best approach.

I tried to get some sort of hackish solution to adw::TabView working within the bounds of relm4, but I couldn't really get past the roadblocks I kept hitting. I believe it needs some rearchitecturing of relm4 to support these kinds of widgets.

Simplest way to display a list of strings

I wanted to create a Relm4 app with a static list view, displaying some text in each line. I stumbled upon this example - https://github.com/AaronErhardt/relm4/blob/main/relm4-examples/examples/list.rs. I was surprised how much boilerplate has to be implemented just to display a number:

We need to create GIntegerObject, implement ObjectSubclass and ObjectImpl for it, subclass it in IntegerObject, implement its creation with glib::Object, then for gtk::ListView it is required to provide data with gio::ListStore and to setup gtk::SignalListItemFactory for mapping between a few property expressions and a widget.

I was wondering it this is really a bare minimum or is there a simpler way to display a list of strings? I know it was much easier when I was using gtk::TreeView and gtk::ListStore from GTK3 Rust bindings - there was no need to create new classes or implement any traits, while mapping to widgets was done simply by choosing a cell renderer. I am also aware this question may be aimed more towards GTK than Relm, but the example exists in this repository.

examples are not compiling for me

when I am in the example directory and call e.g. cargo run --example tracker I get the following errors:

 ~/relm4/relm4-examples   main  cargo run --example tracker                                    (main|✔)
   Compiling relm4 v0.2.0 (/home/peter/Projekte/relm4)
   Compiling relm4-components v0.2.0 (/home/relm4/relm4-components)
error[E0107]: missing generics for struct `OpenButtonModel`
  --> relm4-components/src/open_button/mod.rs:31:12
   |
31 | pub struct OpenButtonModel<Conf: OpenButtonConfig + 'static> {
   |            ^^^^^^^^^^^^^^^ expected 1 generic argument
   |
note: struct defined here, with 1 generic parameter: `Conf`
  --> relm4-components/src/open_button/mod.rs:31:12
   |
31 | pub struct OpenButtonModel<Conf: OpenButtonConfig + 'static> {
   |            ^^^^^^^^^^^^^^^ ----
help: add missing generic argument
   |
31 | pub struct OpenButtonModel<Conf><Conf: OpenButtonConfig + 'static> {
   |            ^^^^^^^^^^^^^^^^^^^^^

error[E0107]: missing generics for struct `OpenDialogModel`
  --> relm4-components/src/open_dialog.rs:36:12
   |
36 | pub struct OpenDialogModel<Conf> {
   |            ^^^^^^^^^^^^^^^ expected 1 generic argument
   |
note: struct defined here, with 1 generic parameter: `Conf`
  --> relm4-components/src/open_dialog.rs:36:12
   |
36 | pub struct OpenDialogModel<Conf> {
   |            ^^^^^^^^^^^^^^^ ----
help: add missing generic argument
   |
36 | pub struct OpenDialogModel<Conf><Conf> {
   |            ^^^^^^^^^^^^^^^^^^^^^

error[E0107]: missing generics for struct `SaveDialogModel`
  --> relm4-components/src/save_dialog.rs:39:12
   |
39 | pub struct SaveDialogModel<Conf: SaveDialogConfig> {
   |            ^^^^^^^^^^^^^^^ expected 1 generic argument
   |
note: struct defined here, with 1 generic parameter: `Conf`
  --> relm4-components/src/save_dialog.rs:39:12
   |
39 | pub struct SaveDialogModel<Conf: SaveDialogConfig> {
   |            ^^^^^^^^^^^^^^^ ----
help: add missing generic argument
   |
39 | pub struct SaveDialogModel<Conf><Conf: SaveDialogConfig> {
   |            ^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0107`.
error: could not compile `relm4-components` due to 3 previous errors

Transient dialogs shift on subsequent view calls

The following can be observed in the calc-trainer example.

Pressing "Start" starts a timer, after which a transient DialogMsg widget appears.

First call:
1

Second and subsequent calls:
2

Same happens with the AboutDialog and ShortcutsWindow widgets in my project.

I'm not sure whether it's a Relm4 issue or not, but I couldn't find the same behavior in Rnote (gtk4-rs + libadwaita-rs), for example.

Alternative to gtk-test

Does relm4 offer any alternative to gtk-test crate? It's very useful for automated integration tests of the whole application, where it allows inspecting the state of widgets and interacting with them. The application does not need to expose the widgets on its own. Unfortunately, that crate supports only gtk3.

Edit: I stumbled upon relm-test, but it's also only for gtk3 through relm, not relm4.

widget macro doesn't work with reexports

I've created a library crate with bunch of reexports to have one place to rule all external dependencies related to relm and gtk.

pub use gtk;
pub use gtk::glib as glib;
pub use libadwaita;
pub use relm4;
pub use relm4_macros;
pub use relm4_components;
pub use tracker;

If I use relm widget macro from my reexports I receive

    | #[relm4_macros::widget(pub)]
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ use of undeclared crate or module `relm4`
    |
    = note: this error originates in the attribute macro `relm4_macros::widget` (in Nightly builds, run with -Z macro-backtrace for more info)
    | #[widget(pub)]
    | ^^^^^^^^^^^^^^ could not find `gtk` in the list of imported crates
    |
    = note: this error originates in the attribute macro `widget` (in Nightly builds, run with -Z macro-backtrace for more info)

If I build my app with -Z macro-backtrace then it points to lib.rs:105 in relm4-macros.

Even adding use for gtk and relm4 like below doesn't help.

use my_reexports::gtk;
use my_reexports::relm4;

Is only workaround to abandon widget macro?

Creating micro-components "early" causes a segfault

Hello,

The following code causes a segfault:

use relm4::factory::{FactoryVec, FactoryPrototype};
use relm4::{gtk, adw, send, AppUpdate, Model, RelmApp, Widgets, Sender, MicroComponent, MicroWidgets, MicroModel};
use relm4::gtk::prelude::*;
use relm4::adw::prelude::*;
use relm4::util::widget_plus::WidgetPlus;


#[derive(Debug)]
struct ActOverview;

impl MicroModel for ActOverview {
    type Msg = ();
    type Widgets = ActOverviewWidgets;
    type Data = ();

    fn update(&mut self, _msg: (), _data: &(), _sender: Sender<Self::Msg>) {
    }
}

#[relm4::micro_widget]
#[derive(Debug)]
impl MicroWidgets<ActOverview> for ActOverviewWidgets {
    view! {
        gtk::Box {
            set_orientation: gtk::Orientation::Vertical,
        }
    }
}


fn main() {
    MicroComponent::new(ActOverview, ());
}

I believe this happens because MicroComponent::new calls some GTK methods before gtk::init got called. This reproducer is obviously quite synthetic (you wouldn't normally do this X)). What I wanted to do was prepopulate a model with a Vec<MicroComponent<ActOverview>> containing some pre-created micro-components (the vec would then get modified during the lifetime of the program). What's the recommended way of populating a model with some microcomponent on startup?

FactoryVecDeque crash after clear and push

If you modify the factory_advanced example like so:

diff --git a/relm4-examples/examples/factory_advanced.rs b/relm4-examples/examples/factory_advanced.rs
index 0dbae91..287b471 100644
--- a/relm4-examples/examples/factory_advanced.rs
+++ b/relm4-examples/examples/factory_advanced.rs
@@ -41,7 +41,10 @@ impl AppUpdate for AppModel {
                 });
             }
             AppMsg::RemoveLast => {
-                self.counters.pop_back();
+                self.counters.clear();
+                self.counters.push_front(Counter {
+                    value: 0,
+                });
             }
             AppMsg::CountAt(weak_index) => {
                 if let Some(index) = weak_index.upgrade() {

Then click on "add" twice and then "remove", the application will crash with the following stack trace:

thread 'main' panicked at 'index out of bounds: the len is 2 but the index is 2', /home/euclio/repos/relm4/src/factory/collections/factory_vec_deque.rs:331:13
stack backtrace:
   0: rust_begin_unwind
             at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:517:5
   1: core::panicking::panic_fmt
             at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/panicking.rs:100:14
   2: core::panicking::panic_bounds_check
             at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/panicking.rs:76:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index_mut
             at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/slice/index.rs:190:14
   4: core::slice::index::<impl core::ops::index::IndexMut<I> for [T]>::index_mut
             at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/slice/index.rs:26:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::IndexMut<I>>::index_mut
             at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/alloc/src/vec/mod.rs:2508:9
   6: relm4::factory::collections::factory_vec_deque::FactoryVecDeque<Data>::compile_changes
             at /home/euclio/repos/relm4/src/factory/collections/factory_vec_deque.rs:331:13
   7: <relm4::factory::collections::factory_vec_deque::FactoryVecDeque<Data> as relm4::factory::Factory<Data,View>>::generate
             at /home/euclio/repos/relm4/src/factory/collections/factory_vec_deque.rs:347:26
   8: <factory_advanced::AppWidgets as relm4::traits::Widgets<factory_advanced::AppModel,()>>::view
             at ./examples/factory_advanced.rs:216:9
   9: relm4::app::RelmApp<Model>::with_app::{{closure}}
             at /home/euclio/repos/relm4/src/app.rs:83:17
  10: glib::main_context_channel::dispatch
             at /home/euclio/.cargo/registry/src/github.com-1ecc6299db9ec823/glib-0.14.8/src/main_context_channel.rs:242:20
  11: g_main_context_dispatch
  12: <unknown>
  13: g_main_context_iteration
  14: g_application_run
  15: <O as gio::application::ApplicationExtManual>::run_with_args
             at /home/euclio/.cargo/registry/src/github.com-1ecc6299db9ec823/gio-0.14.8/src/application.rs:30:13
  16: <O as gio::application::ApplicationExtManual>::run
             at /home/euclio/.cargo/registry/src/github.com-1ecc6299db9ec823/gio-0.14.8/src/application.rs:23:9
  17: relm4::app::RelmApp<Model>::run
             at /home/euclio/repos/relm4/src/app.rs:37:9
  18: factory_advanced::main
             at ./examples/factory_advanced.rs:231:5
  19: core::ops::function::FnOnce::call_once
             at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:227:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

Expected behavior: the app does not crash. All existing counters are removed and a new counter is added.

Issue with reusing components twice

I've created a fairly complex reusable component which I would like to use twice with different settings in the same view. The way in which components are initiated makes it hard to select proper configuration of the component.

I've simplified the case and created a gist with example issue and solution I came up with. https://gist.github.com/mskorkowski/ae88013c4d19b6479270aeeaa08e8416

Current state of relm4 makes it hard to reuse components more then once in the widget.

I don't like the solution I've created because

  1. It moves configuration of the component away from the creation step (Components::init_components)
  2. Overly complex and verbose
  3. Produces polution in the namespace if component is used many times in application

What I would like to, is

  1. ComponentUpdate::init_model to take component settings struct as an argument instead of parent model
  2. RelmComponent::new had additional argument for component settings struct

Ordering issues when using ":" and "=" with the same operation in view macro

Look at this example:

#[relm4::widget]
impl Widgets<AppModel, ()> for AppWidgets {
    fn pre_init() {
        relm4::view! {
            gtk_box = gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
                append: &gtk::Label::new(Some("A")),
                append = &gtk::Label {
                    set_label: "B",
                },
                append: &gtk::Label::new(Some("C")),
                inline_css: b"background-color:red",
            }
        }
    }
    view! {
        window = gtk::ApplicationWindow {
            set_child = Some(&gtk::Box) {
                set_orientation: gtk::Orientation::Horizontal,
                append: &gtk_box,
                append = &gtk::Box {
                    set_orientation: gtk::Orientation::Vertical,
                    append: &gtk::Label::new(Some("A")),
                    append = &gtk::Label {
                        set_label: "B",
                    },
                    append: &gtk::Label::new(Some("C")),
                    inline_css: b"background-color:blue",
                },
            }
        }
    }
}

In the view macro, we combine ":" and "=" to append labels to the boxes, and to append boxes to the main window box.

Note that boxes are created with the same code, just in different places:

Red box - created in pre_init, added with ":"

relm4::view! {
    gtk_box = gtk::Box {
        set_orientation: gtk::Orientation::Vertical,
	append: &gtk::Label::new(Some("A")),
	append = &gtk::Label {
            set_label: "B",
	},
	append: &gtk::Label::new(Some("C")),
	inline_css: b"background-color:red",
    }
}

Blue box

  • created/added in place with "="
append = &gtk::Box {
    set_orientation: gtk::Orientation::Vertical,
    append: &gtk::Label::new(Some("A")),
    append = &gtk::Label {
        set_label: "B",
    },
    append: &gtk::Label::new(Some("C")),
    inline_css: b"background-color:blue",
},			

The results:

order

The order of elements isn't something I expect, when I look at the code:

  1. I expect the box order to be red->blue, not the opposite.
  2. I expect the label order to be A->B->C, not B->A->C or A->C->B.
  3. As the boxes were created with the same code, I expect them to have the same order of children.

Although I'm not sure if combining ":" and "=" is something you would want to do in an application, it's still a bit confusing.

relm4 fails to compile on Windows because of Linux-only elements

If you take a look at https://github.com/gtk-rs/gtk4-rs/blob/master/gtk4/Gir.toml, you will see some elements are generated with cfg_condition = "target_os = \"linux\""

Those elements are:

PageSetupUnixDialog
PrintJob
PrintUnixDialog
Printer

These widgets are exported in https://github.com/AaronErhardt/relm4/blob/main/src/util/default_widgets.rs, which causes relm4 compilation to fail on Windows (and likely other platforms, but I am running into this on Windows)

Update relm4-components for the new component trait

It'd be nice to support relm4-components in 0.5 from day one. These are the currently available components:

  • open_button
  • alert
  • open_dialog
  • save_dialog

And of course, more reusable components are always welcome!

Data store for relm4

What?

Data store is collection of business model records. To show the content of the data store you create a data store view. data store view just makes sure proper data from a store are visible. data store is responsible for owning records. Whenever data store is changed it notifies the view.

As I see it

  • Data store is closest to worker
  • Data store view is closest to factory

Related issues

Why?

  1. I'm writing an application where I need to show tons of data (data set which is 100-200GB is norm)
  2. I need synchronize view between components showing related state

My first attempt was to write custom factories, list views, components, workers, etc... It works fine except state management is super painful. I was passing tons of events between components which are far away to synchronize what is visible and how. If I would like to test wherever all the messages are passed across components when I modify component tree is disaster.

Doing fairly incomplete implementation of the data store by extracting bits of my app allowed me to reduce amount of messages in system by around 60. Now in all places where I had managed to change the code to use data store/data store view I have only one dummy message for each component instance to trigger the redraw.

What's more factories provided by relm4 doesn't support grouping nor accessing the widgets in the factory which for example I need for toggle buttons.

What's the issue?

I have to issues with my current code

  1. This extra event per component routing required to trigger redraw
  2. I've an issue with cyclic dependency required by components depending on the store view of the parent

Implementation of the data store and data store view is in super early phase but if you can give me some pointers how to make it cooperate better with relm4 I would be super grateful.

The code for current version can be found in https://github.com/mskorkowski/relm4-store

Issue 1: extra event per component routing required to trigger redraw

All examples are kind of simple todo application.

If you run cargo run --package=relm4-store-examples --example todo_3-early it will simple to do application where list of todos is duplicated. If you modify the left list, right will be automatically updated and vice versa.

In the source code in relm4-store-examples/examples/todo_3-early/view/main_window.rs in implementation of AppUpdate there is

impl AppUpdate for MainWindowViewModel {
    fn update(
        &mut self, 
        msg: Self::Msg , 
        components: &Self::Components, 
        _sender: Sender<Self::Msg>
    ) -> bool {
        match msg {
            MainWindowMsg::LeftListUpdate => {
                //force redraw of the components
                components.right_list.send(TaskMsg::Reload).unwrap();
            },
            MainWindowMsg::RightListUpdate => {
                //force redraw of the components
                components.left_list.send(TaskMsg::Reload).unwrap();
            }
        }
        true
    }
}

Those LeftListUpdate and RightListUpdate are events I would love to get rid of since data store notifies data store view about the changes.

I have a filling that I'm missing something obvious in terms of architecture.

Issue 2: dependency required by components depending on the store view of the parent

I've created generic pagination component which as part of it's configuration takes a data store view. It can be found relm4-store/relm4-store-components/src/pagination/mod.rs.

The issue I have is that whenever record is added to the data store I should notify pagination component that total page count might have changed. Now it would require to have a weak rc (or something simillar) to instance of the relm4::Model for pagination component which is totally unsound.

My guts are telling me that solving first issue will also solve this one.


When the code get's mature enough and if you think it aligns with relm4 I'm willing to donate/merge the code. If you find this issue out of scope/going wrong way/boring fill free to close it. Having answers to this questions would give me more confidence in implementing rest of requirements.

Alternative Approach

Tried implementing a prototype to explain this alternative approach. Here's a rough draft of some of the differences:

  • No differentiation between app components and widget components
  • Combines the MicroComponents and Components API together into a more flexible Component trait
  • Doesn't require a Model trait -- implementing Component is enough to have a fully functioning component
  • Uses channels for sending events to and from the component, so components do not need to know any type information about a parent type, which makes them reusable
  • Zero use of Rc and RefCell, because all state is owned by the component's inner event loop.
  • Watches for the destroy event on the root widget to hang up the component's event loop.

The implementation:

// Copyright 2022 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0

use gtk4::prelude::*;
use tokio::sync::mpsc;

pub type Sender<T> = mpsc::UnboundedSender<T>;
pub type Receiver<T> = mpsc::UnboundedReceiver<T>;

/// A newly-registered component which supports destructuring the handle
/// by forwarding or ignoring outputs from the component.
pub struct Registered<W: Clone + AsRef<gtk4::Widget>, I, O> {
    /// Handle to the component that was registered.
    pub handle: Handle<W, I>,

    /// The outputs being received by the component.
    pub receiver: Receiver<O>,
}

impl<W: Clone + AsRef<gtk4::Widget>, I: 'static, O: 'static> Registered<W, I, O> {
    /// Forwards output events to the designated sender.
    pub fn forward<X: 'static, F: (Fn(O) -> X) + 'static>(
        self,
        sender: Sender<X>,
        transform: F,
    ) -> Handle<W, I> {
        let Registered { handle, receiver } = self;
        forward(receiver, sender, transform);
        handle
    }

    /// Ignore outputs from the component and take the handle.
    pub fn ignore(self) -> Handle<W, I> {
        self.handle
    }
}

/// Handle to an active widget component in the system.
pub struct Handle<W: Clone + AsRef<gtk4::Widget>, I> {
    /// The widget that this component manages.
    pub widget: W,

    /// Used for emitting events to the component.
    pub sender: Sender<I>,
}

impl<W: Clone + AsRef<gtk4::Widget>, I> Widget<W> for Handle<W, I> {
    fn widget(&self) -> &W {
        &self.widget
    }
}

impl<W: Clone + AsRef<gtk4::Widget>, I> Handle<W, I> {
    pub fn emit(&self, event: I) {
        let _ = self.sender.send(event);
    }
}

/// Used to drop the component's event loop when the managed widget is destroyed.
enum InnerMessage<T> {
    Drop,
    Message(T),
}

/// Provides a convenience function for getting a widget out of a type.
pub trait Widget<W: Clone + AsRef<gtk4::Widget>> {
    fn widget(&self) -> &W;
}

/// The basis of a COSMIC widget.
///
/// A component takes care of constructing the UI of a widget, managing an event-loop
/// which handles signals from within the widget, and supports forwarding messages to
/// the consumer of the component.
pub trait Component: Sized + 'static {
    /// The arguments that are passed to the init_view method.
    type InitialArgs;

    /// The message type that the component accepts as inputs.
    type Input: 'static;

    /// The message type that the component provides as outputs.
    type Output: 'static;

    /// The widget that was constructed by the component.
    type RootWidget: Clone + AsRef<gtk4::Widget>;

    /// The type that's used for storing widgets created for this component.
    type Widgets: 'static;

    /// Initializes the component and attaches it to the default local executor.
    ///
    /// Spawns an event loop on `glib::MainContext::default()`, which exists
    /// for as long as the root widget remains alive.
    fn register(
        mut self,
        args: Self::InitialArgs,
    ) -> Registered<Self::RootWidget, Self::Input, Self::Output> {
        let (mut sender, in_rx) = mpsc::unbounded_channel::<Self::Input>();
        let (mut out_tx, output) = mpsc::unbounded_channel::<Self::Output>();

        let (mut widgets, widget) = self.init_view(args, &mut sender, &mut out_tx);

        let handle = Handle {
            widget,
            sender: sender.clone(),
        };

        let (inner_tx, mut inner_rx) = mpsc::unbounded_channel::<InnerMessage<Self::Input>>();

        handle.widget.as_ref().connect_destroy({
            let sender = inner_tx.clone();
            move |_| {
                let _ = sender.send(InnerMessage::Drop);
            }
        });

        spawn_local(async move {
            while let Some(event) = inner_rx.recv().await {
                match event {
                    InnerMessage::Message(event) => {
                        self.update(&mut widgets, event, &mut sender, &mut out_tx);
                    }

                    InnerMessage::Drop => break,
                }
            }
        });

        forward(in_rx, inner_tx, |event| InnerMessage::Message(event));

        Registered {
            handle,
            receiver: output,
        }
    }

    /// Creates the initial view and root widget.
    fn init_view(
        &mut self,
        args: Self::InitialArgs,
        sender: &mut Sender<Self::Input>,
        out_sender: &mut Sender<Self::Output>,
    ) -> (Self::Widgets, Self::RootWidget);

    /// Handles input messages and enables the programmer to update the model and view.
    fn update(
        &mut self,
        widgets: &mut Self::Widgets,
        event: Self::Input,
        sender: &mut Sender<Self::Input>,
        outbound: &mut Sender<Self::Output>,
    );
}

/// Convenience function for `Component::register()`.
pub fn register<C: Component>(
    model: C,
    args: C::InitialArgs,
) -> Registered<C::RootWidget, C::Input, C::Output> {
    model.register(args)
}

/// Convenience function for forwarding events from a receiver to different sender.
pub fn forward<I: 'static, O: 'static, F: (Fn(I) -> O) + 'static>(
    mut receiver: Receiver<I>,
    sender: Sender<O>,
    transformer: F,
) {
    spawn_local(async move {
        while let Some(event) = receiver.recv().await {
            if sender.send(transformer(event)).is_err() {
                break;
            }
        }
    })
}

/// Convenience function for launching an application.
pub fn run<F: Fn(gtk4::Application) + 'static>(func: F) {
    use gtk4::prelude::*;
    let app = gtk4::Application::new(None, Default::default());

    app.connect_activate(move |app| func(app.clone()));

    app.run();
}

/// Convenience function for spawning tasks on the local executor
pub fn spawn_local<F: std::future::Future<Output = ()> + 'static>(func: F) {
    gtk4::glib::MainContext::default().spawn_local(func);
}

Creation of an InfoButton component:

// Copyright 2022 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0

use ccs::*;
use gtk::prelude::*;
use gtk4 as gtk;

pub enum InfoButtonInput {
    SetDescription(String),
}

pub enum InfoButtonOutput {
    Clicked,
}

pub struct InfoButtonWidgets {
    description: gtk::Label,
}

#[derive(Default)]
pub struct InfoButton;

impl Component for InfoButton {
    type InitialArgs = (String, String, gtk::SizeGroup);
    type RootWidget = gtk::Box;
    type Input = InfoButtonInput;
    type Output = InfoButtonOutput;
    type Widgets = InfoButtonWidgets;

    fn init_view(
        &mut self,
        (desc, button_label, sg): Self::InitialArgs,
        _sender: &mut Sender<InfoButtonInput>,
        out_sender: &mut Sender<InfoButtonOutput>,
    ) -> (InfoButtonWidgets, gtk::Box) {
        relm4_macros::view! {
            root = info_box() -> gtk::Box {
                append: description = &gtk::Label {
                    set_label: &desc,
                    set_halign: gtk::Align::Start,
                    set_hexpand: true,
                    set_valign: gtk::Align::Center,
                    set_ellipsize: gtk::pango::EllipsizeMode::End,
                },

                append: button = &gtk::Button {
                    set_label: &button_label,

                    connect_clicked(out_sender) => move |_| {
                        let _ = out_sender.send(InfoButtonOutput::Clicked);
                    }
                }
            }
        }

        sg.add_widget(&button);

        (InfoButtonWidgets { description }, root)
    }

    fn update(
        &mut self,
        widgets: &mut InfoButtonWidgets,
        event: InfoButtonInput,
        _sender: &mut Sender<InfoButtonInput>,
        _out_sender: &mut Sender<InfoButtonOutput>,
    ) {
        match event {
            InfoButtonInput::SetDescription(value) => {
                widgets.description.set_text(&value);
            }
        }
    }
}

pub fn info_box() -> gtk::Box {
    relm4_macros::view! {
        container = gtk::Box {
            set_orientation: gtk::Orientation::Horizontal,
            set_margin_start: 20,
            set_margin_end: 20,
            set_margin_top: 8,
            set_margin_bottom: 8,
            set_spacing: 24
        }
    }

    container
}

Creating and maintaining InfoButton components inside of an App component

// Copyright 2022 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0

use crate::components::{InfoButton, InfoButtonInput, InfoButtonOutput};
use ccs::*;
use gtk::prelude::*;
use gtk4 as gtk;

/// The model where component state is stored.
#[derive(Default)]
pub struct App {
    pub counter: usize,
}

/// Widgets that are initialized in the view.
pub struct AppWidgets {
    list: gtk::ListBox,
    destroyable: Option<Handle<gtk::Box, InfoButtonInput>>,
    counter: Handle<gtk::Box, InfoButtonInput>,
}

/// An input event that is used to update the model.
pub enum AppEvent {
    Destroy,
    Increment,
}

/// Components are the glue that wrap everything together.
impl Component for App {
    type InitialArgs = gtk::Application;
    type Input = AppEvent;
    type Output = ();
    type Widgets = AppWidgets;
    type RootWidget = gtk::ApplicationWindow;

    fn init_view(
        &mut self,
        app: gtk::Application,
        sender: &mut Sender<AppEvent>,
        _out_sender: &mut Sender<()>,
    ) -> (AppWidgets, gtk::ApplicationWindow) {
        let button_group = gtk::SizeGroup::new(gtk::SizeGroupMode::Both);

        // Create an `InfoButton` component.
        let destroyable = InfoButton::default()
            .register((String::new(), "Destroy".into(), button_group.clone()))
            .forward(sender.clone(), |event| match event {
                InfoButtonOutput::Clicked => AppEvent::Destroy,
            });

        // Instruct the component to update its description.
        let _ = destroyable.emit(InfoButtonInput::SetDescription(
            "Click this button to destroy me".into(),
        ));

        // Create a counter component, too.
        let counter = InfoButton::default()
            .register(("Click me too".into(), "Click".into(), button_group))
            .forward(sender.clone(), |event| match event {
                InfoButtonOutput::Clicked => AppEvent::Increment,
            });

        // Construct the view for this component, attaching the component's widget.
        relm4_macros::view! {
            window = gtk::ApplicationWindow {
                set_application: Some(&app),
                set_child = Some(&gtk::Box) {
                    set_halign: gtk::Align::Center,
                    set_size_request: args!(400, -1),
                    set_orientation: gtk::Orientation::Vertical,

                    append: list = &gtk::ListBox {
                        set_selection_mode: gtk::SelectionMode::None,
                        set_hexpand: true,

                        append: destroyable.widget(),
                        append: counter.widget(),
                    },
                }
            }
        }

        window.show();

        (
            AppWidgets {
                list,
                counter,
                destroyable: Some(destroyable),
            },
            window,
        )
    }

    /// Updates the view
    fn update(
        &mut self,
        widgets: &mut AppWidgets,
        event: AppEvent,
        _sender: &mut Sender<AppEvent>,
        _outbound: &mut Sender<()>,
    ) {
        match event {
            AppEvent::Increment => {
                self.counter += 1;

                widgets
                    .counter
                    .emit(InfoButtonInput::SetDescription(format!(
                        "Clicked {} times",
                        self.counter
                    )));
            }

            AppEvent::Destroy => {
                // Components are kept alive by their root GTK widget.
                if let Some(handle) = widgets.destroyable.take() {
                    if let Some(parent) = handle.widget().parent() {
                        widgets.list.remove(&parent);
                    }
                }
            }
        }
    }
}

With application launched via

// Copyright 2022 System76 <[email protected]>
// SPDX-License-Identifier: MPL-2.0

extern crate cosmic_component_system as ccs;

mod components;

use self::components::App;
use ccs::Component;
use gtk4 as gtk;

fn main() {
    ccs::run(|app| {
        App::default().register(app.clone());
    });
}

factory key should be unsized

In data store there is an id type which is being used to identify the records in the store. It's generic in terms of values which are used. By default I'm using uuid's since they are easily unique. But it's widespread to use i32 or something bigger as ids. So I need to provide ability to change what kind of values are stored inside.

This on the other hand interferes with Factory::Key which is Sized by default.

PR: #52

Expose `menu!` macro beyond `view!`

The menu macro is only usable from inside view!, but it could be useful even in manually constructed views. Is it possible to expose its functionality outside of view!?

Add macOS and Windows to CI?

I really like the look of the relm4 API, but I can't figure out how to run the examples on either macOS or Windows. Everything worked great out of the box in Ubuntu. I've never written a GTK app before and I've read through the linked GTK4 docs, but I still can't work out how to get the correct files where they need to be for relm4 or gtk-rs or whatever else to find what it needs. I think a lot of this documentation assumes you've previously written GTK apps and just need to update for GTK4.

In any event, I'm not having much success getting started. It would be immensely helpful if the GitHub Actions configuration built the examples on macOS and Windows since that would show a scripted approach to running everything successfully.

Names of the new component parts

Current state

Component initialization

Bridge (+Payload, Fuselage) --launch--> Fairing --detach--> Controller

The problem I see with these names is that they are hardly known to people who learned English as a second language-

Bridge

  • Gives you access to the root widget
  • Can be converted into Fairing with a given Payload
  • During the conversion, it spawns the runtime that handles incoming messages (and runs commands)

Fairing

  • Either spawn a new runtime to forward messages from the component with a given sender
  • Or connect a function that receives messages from the component and can forward the messages
  • Or do nothing
  • Converts itself into an controller

Controller

  • Gives you access to the root widget
  • Allows you to send messages to the component
  • This is what you want to keep in your model

Alternative names

  • Alternative rocket analogy: Base Frame (+Payload, Parts) --assemble--> Rocket/Aircraft --launch--> Controller
  • Classical programming terms: Builder (+InitParams, ComponentParts) --build--> Connector --connect/get_handle--> Handle

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.