An idiomatic GUI library inspired by Elm and based on gtk4-rs

Related tags

GUI gtk elm relm gtk4
Overview

Relm4

CI Matrix Relm4 on crates.io Relm4 docs Relm4 book Minimum Rust version 1.56

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.

Ecosystem

Relm4 has two crates that extend the core functionality:

  • relm4-macros provides a widget macro that simplifies UI creation
  • relm4-components is a collections of reusable components you can easily integrate into your application

Add this to your Cargo.toml:

gtk = { version = "0.3", package = "gtk4" }
relm4 = "0.2"
relm4-macros = "0.2"
relm4-components = "0.2"

Features

The relm4 crate has two feature flags:

  • tokio-rt: Adds the AsyncWorker type that uses an async update function
  • libadwaita: Improved support for libadwaita

Examples

Several example applications are available at relm4-examples/.

📸 Screenshots from the example apps

A simple counter app

Simple app screenshot light Simple app screenshot dark

use gtk::prelude::{BoxExt, ButtonExt, GtkWindowExt, OrientableExt};
use relm4::{send, AppUpdate, Model, RelmApp, Sender, WidgetPlus, Widgets};

#[derive(Default)]
struct AppModel {
    counter: u8,
}

enum AppMsg {
    Increment,
    Decrement,
}

impl Model for AppModel {
    type Msg = AppMsg;
    type Widgets = AppWidgets;
    type Components = ();
}

impl AppUpdate for AppModel {
    fn update(&mut self, msg: AppMsg, _components: &(), _sender: Sender<AppMsg>) -> bool {
        match msg {
            AppMsg::Increment => {
                self.counter = self.counter.wrapping_add(1);
            }
            AppMsg::Decrement => {
                self.counter = self.counter.wrapping_sub(1);
            }
        }
        true
    }
}

#[relm4_macros::widget]
impl Widgets<AppModel, ()> for AppWidgets {
    view! {
        gtk::ApplicationWindow {
            set_title: Some("Simple app"),
            set_default_width: 300,
            set_default_height: 100,
            set_child = Some(&gtk::Box) {
                set_orientation: gtk::Orientation::Vertical,
                set_margin_all: 5,
                set_spacing: 5,

                append = &gtk::Button {
                    set_label: "Increment",
                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Increment);
                    },
                },
                append = &gtk::Button {
                    set_label: "Decrement",
                    connect_clicked(sender) => move |_| {
                        send!(sender, AppMsg::Decrement);
                    },
                },
                append = &gtk::Label {
                    set_margin_all: 5,
                    set_label: watch! { &format!("Counter: {}", model.counter) },
                }
            },
        }
    }
}

fn main() {
    let model = AppModel::default();
    let app = RelmApp::new(model);
    app.run();
}

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!

Comments
  • Data store for relm4

    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

    • #53
    • ~~#49~~

    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.

    help wanted 
    opened by mskorkowski 29
  • Alternative Approach

    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());
        });
    }
    
    opened by mmstick 18
  • Add timer to calc-trainer example

    Add timer to calc-trainer example

    Some first implementation for the timer. With some help of a kind guy, I got an easy setting of the button working.

    With the timer I'm kind of lost. I tried to add a message handler. Not sure if this makes sense. Maybe you could point me in the right direction.

    opened by tronta 16
  • New component macro is dependent on a variable name 'root'

    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`.
    
    opened by MaksymShcherbak 11
  • core: Add asynchronous components

    core: Add asynchronous components

    Summary

    Add asynchronous components that run on the GLib main context.

    Checklist

    • [x] cargo fmt
    • [x] cargo clippy
    • [x] cargo test
    • [x] updated CHANGES.md
    waits-on-review-medium 
    opened by AaronErhardt 10
  • widget macro doesn't work with reexports

    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?

    opened by mskorkowski 10
  • Issue with reusing components twice

    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
    opened by mskorkowski 10
  • Feature: ability to move view! macro to a separate file

    Feature: ability to move view! macro to a separate file

    The view! { ... } macro can be gigantic sometimes, and it's not always reasonable/easy to split things into smaller components. Having view! { ... } in a separate file (from the rest of the component implementation) can make the code much easier to navigate and maintain in such cases. Additionally, it would provide even cleaner separation between declarative and imperative parts.

    enhancement 
    opened by azymohliad 9
  • feat!: Configurable component stopping

    feat!: Configurable component stopping

    • Pro: Removes the OnDestroy trait and Root: OnDestroy restriction.
    • Pro: Can kill the component externally from the controller
    • Pro: Can kill the component through any means necessary
    • Con: Requires that the kill signal be attached by the component author.
      • Comes with sender.stop_with_widget() and sender.stop_with_native_dialog() to ease that burden.
    opened by mmstick 9
  • new approach: Limitations on `Component::Root`

    new approach: Limitations on `Component::Root`

    Root requires a type to be OnDestroy, which is currently only implemented for gtk widgets. This makes it impossible to make gtk::FileChooserNative the component root. If we could fix this, it would benefit #141 and we won't have to use non-native dialogs.

    opened by MaksymShcherbak 9
  • How to use #[relm4_macros::widget] with initializer

    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.

    opened by tronta 9
  • Change the link of the docs of all crates to docs.rs

    Change the link of the docs of all crates to docs.rs

    Summary

    This will make crates.io link to the docs hosted on docs.rs.

    Checklist

    • [x] cargo fmt
    • [x] cargo clippy
    • [x] cargo test
    • [x] updated CHANGES.md
    opened by pentamassiv 0
  • Document extra `deref` needed with `widget_template`

    Document extra `deref` needed with `widget_template`

    The widget template macro seems to generate impl Deref for the root widget. This means in some cases, it might be needed to use * or deref when the custom widget needs to be passed around and used as Widget.

    For an example, here is a code that will cause error:

    • Custom Widget
    #[relm4::widget_template(pub)]
    impl WidgetTemplate for LoadingPage {
        view! {
            gtk::Spinner {
                start: (),
                set_halign: gtk::Align::Center,
                set_valign: gtk::Align::Center,
                set_vexpand: true,
            }
        }
    }
    
    • Error code
    // Simple Async Component
    fn init_loading_widgets(root: &mut Self::Root) -> Option<LoadingWidgets> {
        view! {
            #[local_ref]
            root {
                #[name(spinner)]
                #[template]
                widgets::loading::LoadingPage,
            }
        }
        // Can be fixed by using Some(LoadingWidgets::new(root, &*spinner))
        Some(LoadingWidgets::new(root, spinner))
    }
    
    • Error:
    error[E0277]: the trait bound `LoadingPage: AsRef<relm4::gtk4::Widget>` is not satisfied
      --> src/vndb/mod.rs:57:40
       |
    57 |         Some(LoadingWidgets::new(root, spinner))
       |              -------------------       ^^^^^^^ the trait `AsRef<relm4::gtk4::Widget>` is not implemented for `LoadingPage`
       |              |
       |              required by a bound introduced by this call
       |
    note: required by a bound in `LoadingWidgets::new`
      --> /var/home/ayush/Documents/Programming/WeebTk/weebtk/_build/cargo-home/registry/src/github.com-1ecc6299db9ec823/relm4-0.5.0-rc.1/src/loading_widgets.rs:74:12
       |
    74 |         W: AsRef<C::Child>,
       |            ^^^^^^^^^^^^^^^ required by this bound in `LoadingWidgets::new`
    
    opened by Ayush1325 0
  • Relm4 re-exports other crates by default

    Relm4 re-exports other crates by default

    Currently, relm4 seems to re-export the following creates without any feature flag:

    1. once_cell
    2. async_trait
    3. tokio

    I'm not quite sure why this is being done. While the gtk crate is also re-exported, it makes sense due to the relationship between relm4 and gtk.

    However, the other 3 seem to be internal details, and it is probably possible to get rid of at least once_cell and async_trait once those features stabilize upstream. So I think they should not be re-exported without any good reason.

    PS: I did check making them internal pub(crate) and it doesn't cause any problems at least in building

    opened by Ayush1325 4
  • Support for Bidirectional Data Binding

    Support for Bidirectional Data Binding

    Currently, relm4 only seems to support one-way data binding using #[watch]. I think this can be great when a property can be manipulated by both Rust code and Gtk GUI (eg: window size).

    opened by Ayush1325 0
  • Example of Saving Window State

    Example of Saving Window State

    Saving the window state is a fairly common use case in applications since GTK does not provide this by default. The gtk-rs docs contain an example of doing this.

    Initially, I tried using SimpleComponent::shutdown, to achieve this. However, it does not seem to be called for ApplicationWindow. So I think it would be a good idea for docs to include an example of implementing this in relm4.

    I would prefer to use gsettings for saving but alternate suggestions are also welcome.

    opened by Ayush1325 0
Owner
Aaron Erhardt
Aaron Erhardt
A cross-platform GUI library for Rust, inspired by Elm

Iced A cross-platform GUI library for Rust focused on simplicity and type-safety. Inspired by Elm. Features Simple, easy-to-use, batteries-included AP

Héctor Ramón 17.5k Jan 2, 2023
A cross-platform GUI library for Rust, inspired by Elm

Iced A cross-platform GUI library for Rust focused on simplicity and type-safety. Inspired by Elm. Features Simple, easy-to-use, batteries-included AP

null 17.5k Dec 28, 2022
Neovim GUI written in Rust, using relm4 and gtk4-rs

Reovim Neovim GUI written in Rust, using relm4 and gtk4-rs. Thanks Neovide Configuration To setup font add next line to init.vim set guifont=Cascadia\

songww 70 Dec 13, 2022
Unofficial Linux QQ client, based on GTK4 and libadwaita, developed with Rust and Relm4.

GTK QQ (WIP) Unofficial Linux QQ client, based on GTK4 and libadwaita, developed with Rust and Relm4. Screenshots Light Dark Note The two screenshots

Lomírus 198 Dec 24, 2022
Simple and portable (but not inflexible) GUI library in C that uses the native GUI technologies of each platform it supports.

libui: a portable GUI library for C This README is being written. Status It has come to my attention that I have not been particularly clear about how

Pietro Gagliardi 10.4k Dec 31, 2022
An Anime Game Launcher variant written on Rust, GTK4 and libadwaita, using Anime Game Core library

An Anime Game Launcher GTK The launcher variant written on Rust, GTK4 and libadwaita, using Anime Game Core library You could also try the main branch

An Anime Team 77 Jan 9, 2023
Honkers Launcher variant written on Rust, GTK4 and libadwaita, using Anime Game Core library

You could also try the main branch Development Folder Description ui Blueprint UI files ui/.dist UI files compiled by the blueprint src Rust source co

An Anime Team 9 Nov 2, 2022
A simple note taking application written in Rust and GTK4

Rnote A simple note taking application written in Rust and GTK4. Rnote aims to be a simple but functional note taking application for freehand drawing

Felix Zwettler 3.4k Jan 5, 2023
Unsafe bindings and a safe wrapper for gtk4-layer-shell, automatically generated from a .gir file

gtk4-layer-shell: gtk4-layer-shell-sys: gtk4-layer-shell This is the safe wrapper for gtk4-layer-shell, automatically generated from its .gir file. Fo

null 3 Apr 30, 2023
A Modern, Open Source GTK4 ebook manager powered by Rust.

Bookx An MVP in progress: An ebook reader with .epub support Context menu for each book (delete, rename book, info) On click switch the carousal to th

Anurag Dhadse 12 Dec 28, 2022
GUI based tool to sort and categorize images written in Rust

ImageSieve GUI based tool to sort out images based on similarity, categorize them according to their creation date and archive them in a target folder

Florian Fetz 67 Dec 14, 2022
A simple GUI version of the pH calibration tool written in egui, based on the eframe template.

caliphui A simple GUI version of the pH calibration tool written in egui, based on the eframe template. Usage Native binaries are provided under relea

Peter Dunne 0 Dec 29, 2021
A lightweight cross-platform system-monitoring fltk gui application based on sysinfo

Sysinfo-gui A lightweight cross-platform system-monitoring fltk gui application based on sysinfo. The UI design is inspired by stacer. The svg icons a

Mohammed Alyousef 22 Dec 31, 2022
A cross-platform GUI library for Rust focused on simplicity and type-safety

A cross-platform GUI library for Rust, inspired by Elm

Héctor Ramón 17.5k Jan 8, 2023
An easy-to-use, 2D GUI library written entirely in Rust.

Conrod An easy-to-use, 2D GUI library written entirely in Rust. Guide What is Conrod? A Brief Summary Screenshots and Videos Feature Overview Availabl

PistonDevelopers 3.3k Jan 1, 2023
Rust bindings for the FLTK GUI library.

fltk-rs Rust bindings for the FLTK Graphical User Interface library. The FLTK crate is a crossplatform lightweight gui library which can be statically

Mohammed Alyousef 1.1k Jan 9, 2023
Clear Coat is a Rust wrapper for the IUP GUI library.

Clear Coat Clear Coat is a Rust wrapper for the IUP GUI library. IUP uses native controls and has Windows and GTK backends. A macOS backend has been o

Jordan Miner 18 Feb 13, 2021
A single-header ANSI C immediate mode cross-platform GUI library

Nuklear This is a minimal-state, immediate-mode graphical user interface toolkit written in ANSI C and licensed under public domain. It was designed a

Immediate Mode UIs, Nuklear, etc. 6.9k Jan 8, 2023
The bindings to the Nuklear 2D immediate GUI library.

nuklear-rust The bindings to the Nuklear 2D immediate GUI library. Currently beta. Drawing backends: gfx-pre-ll for GFX 3D drawing engine (examples: O

Serhii Plyhun 332 Dec 27, 2022