Giter Site home page Giter Site logo

Alternative Approach about relm4 HOT 18 CLOSED

relm4 avatar relm4 commented on May 19, 2024
Alternative Approach

from relm4.

Comments (18)

mmstick avatar mmstick commented on May 19, 2024 1

I've updated the example with my findings from porting a COSMIC settings panel.

It is possible to use set_transient_for if Component::InitialArgs or the model itself contains the gtk::Window. It should also be possible to get the top level window from any GTK widget, too.

from relm4.

mmstick avatar mmstick commented on May 19, 2024 1

This is what I have for an ElmComponent:

while let Some(event) = inner_rx.recv().await {
    match event {
        InnerMessage::Message(event) => {
            if let Some(command) = Self::update(&mut model, event, &mut input, &mut output) {
                let input = input.clone();
                tokio::spawn(async move {
                    if let Some(event) = Self::command(command).await {
                        let _ = input.send(event);
                    }
                });
            }

            Self::update_view(&mut model, &mut widgets, &mut input, &mut output);
        }

        InnerMessage::Drop => break,
    }
}
/// Handles input messages and enables the programmer to update the model and view.
fn update(
    model: &mut Self,
    message: Self::Input,
    input: &mut Sender<Self::Input>,
    output: &mut Sender<Self::Output>,
) -> Option<Self::Command>;

/// Update the UI
fn update_view(
    model: &mut Self,
    widgets: &mut Self::Widgets,
    input: &mut Sender<Self::Input>,
    output: &mut Sender<Self::Output>
);

/// A command to perform in a background thread.
async fn command(message: Self::Command) -> Option<Self::Input>;

Requires using async-trait, but commands are run in the background and optionally return an input message to be consumed by the update function.

from relm4.

AaronErhardt avatar AaronErhardt commented on May 19, 2024

Looks very interesting. I really like this approach in general, especially the forward and destruction mechanisms.

Yet, some parts of this approach violate the philosophy of Relm4. After all, Relm4 is inspired by Elm and Elm is very strict about the separation of application state and UI elements. And I agree with Elm in this point. In my experience mixing state and UI leads to an absolutely unmaintainable mess with lot's of hidden side effects. What looks like unnecessary complexity in regular components is there for a good purpose.

That said, there are cases where this separation has clear disadvantages, for example input forms, where sending updates for each input field can cause the message type to become extremely large.

And there is one other constraint that made components a bit more complex: Dialogs often need to set a transient widget. In your approach it wouldn't be possible to call set_transient_for with a widget from the parent component, if the dialog isn't the root of the components.

Maybe we can come up with a solution that combines the best of both worlds.

from relm4.

AaronErhardt avatar AaronErhardt commented on May 19, 2024

As I said, I think this approach has many good ideas, but as long as it mixes application logic and widget updates, I can't simply use it as the default component type.

Just so that we are on the same page, what are your goals with Relm4 and this approach? I think it's best if we talk about this so that we can coordinate our efforts.

from relm4.

mmstick avatar mmstick commented on May 19, 2024

I have my experiment here at the moment. I've added a macro for generating components using this approach, and have a port of the Support panel we made for GNOME Control Center in GTK3. The component that manages the support panel specifically is here, but I'm still experimenting with different ideas, such as separating UI updates from state updates with UI messages, but I'm still not completely sold on the complexity of enforcing the separation.

We are going to be using GTK4 for COSMIC applications, which of course are also entirely written in Rust. Where GNOME is standardizing around libadwaita as their platform library, libcosmic is what we're developing as our platform library. As a platform library, it will offer a range of GTK widgets that are used across all COSMIC applications, with a first class Rust API.

With this approach I have something that precisely models how I normally develop GTK widgets by hand with GTK-rs. Spawn an event loop and capture all state and widgets into it with message passing to control their behaviors. We have some new hires on the team that don't have the same level of experience with GTK, so I'd prefer if everyone that gets involved today and in the future has a better framework to use as a starting point that takes care of all the boilerplate involved in transforming GTK into something fit for Rust applications. But which also creates these components in a way that you can use them in any GTK widget or application without having to adopt a specific approach wholesale top to bottom.

The goal in mind is to make it as simple as possible to write reusable widgets in Rust using best practices for Rust application development. But it should require as little boilerplate as possible because it should be simple for anyone unfamiliar with Rust or GUI development to build widgets and applications. Using GTK-rs directly requires a lot of boilerplate and knowledge of how to set up message passing between widgets and components. Relm4's view macro resolves most of the view-construction ergonomics that I previously used cascade for.

I have started out by trying to rewrite a simpler settings panel in Relm4, but struggled a lot to get something working after half a day. I finally got it working with MicroModel+MicroWidget components, but it was very heavy on matching up traits and types, and overall had no use for the ordinary components.

from relm4.

AaronErhardt avatar AaronErhardt commented on May 19, 2024

Thanks for letting me know!

I agree that reusable components are not ideal in the current state and require quite a few traits. I think using input parameters instead of passing the parent model and parent widgets could help with that.
Also, separation of application state and UI isn't a hard requirement to get something added to Relm4 but it won't be the default.

I'm very interested to see what you come up with :)
In general, I hope we can work together on making the experience with gtk-rs even better and simpler.

from relm4.

mmstick avatar mmstick commented on May 19, 2024

Sure, I would prefer if we can come up with some reusable bits

from relm4.

AaronErhardt avatar AaronErhardt commented on May 19, 2024

I've been thinking about simplifying the Componenttrait.

pub struct ComponentParts<Model, Widgets> {
    model: Model,
    widgets: Widgets,
}

pub trait Component {
    type Msg;
    type Widgets;
    type Root;
    type InitParams;

    fn init_root() -> Self::Root;

    fn init(params: Self::InitParams, root_widget: &Self::Root) -> ComponentParts<Self, Self::Widgets>;

    /* update + view similar to current components */
}

This especially simplifies the initialization. You're still able to access the root widget of child components when creating the widgets of the component so you can connect them in the right order in the view macro. Also, the parent model is not necessary anymore and components are simply stored in the model.

An implementation for a simple component (that itself has a simple button component as child) could look like this then:

pub enum AppMsg {
    Increment,
    Decrement,
}

pub struct AppModel {
    // state
    counter: u8,
    // components
    button: RelmComponent<Button>,
}

pub struct AppWidgets {
    label: gtk::Label,
}

impl Component for AppModel {
    type Msg = AppMsg;
    type Widgets = AppWidgets;
    type Root = gtk::Box;
    type InitParams = u8;

    // Just initialize the root to make it available early for any parent component.
    fn init_root() -> Self::Root {
        gtk::Box::default()
    }

    fn init(params: Self::InitParams, root_widget: &Self::Root) -> ComponentParts<Self::Model, Self::Widgets> {
        // Returns a preliminary type that can already return the root widget of the component.
        let button = RelmComponent::init_root();

        // Initialize widgets
        relm4::view! {
            label = gtk::Label {
                set_text: "Some text",
            }
        }

        // Append everything on the root widget (this can be added as new syntax to the view macro, eventually)
        root_widget.append(&label);
        root_widget.append(button.root_widget());

        // The button component takes (u8, &str) as InitParam, for example.
        // This is trivial in this example but in case we want to use `set_transient_for` with a widget from the parent
        // we are now after the widget initialization and ready to pass widgets as parameters for components.
        let button = button.finish((0, "Button text"));
       
        // Initialize the model with the InitParams
        let model = AppWidget {
            counter: params,
            button,
        }

        // Initialize the widgets
        let widgets = AppWidgets {
            label,
        }

        ComponentParts {
            model,
            widgets,
        }
    }

    /* update + view similar to current components */
}

This is just a mock up that I quickly wrote down. But I think it looks very promising. It's the best approach to simplify components that still have separation of UI and application state.

It's heavily inspired by your ideas and would even allow us to create another Component trait that can opt-out of the separation stuff to make things even easier for beginners while providing the same interface through RelmComponent.

I'm not entirely sure if that actually works, but if it does we might have an approach that's very flexible and should suit us both.

Let me know what you think :)

EDIT: I noticed that I forgot the sender types, but I think the example code still makes sense. Also, most parts from your example would work in this architecture as well. Or in other words, this is actually just a smaller version of your approach with more flexible initialization and separation of UI and application state. If I'm not mistaken, that could be all that's needed to reach feature parity with current components.

from relm4.

mmstick avatar mmstick commented on May 19, 2024

I will experiment with this idea and tell you what I find.

from relm4.

mmstick avatar mmstick commented on May 19, 2024

Perhaps fn init_root() wouldn't be necessary if Self::Root requires that Root implements Default.

from relm4.

AaronErhardt avatar AaronErhardt commented on May 19, 2024

You're right but then it would be awkward if a root widget doesn't implement Default. I already added a lot of Default impls to gtk-rs but maybe someone comes up with a rare use-case where Default isn't implemented.

We could still go that route and recommend wrapper types for things that don't implement Default, or we could work with macros, just like the widget macro that automatically implements root_widget for you. In the macro it would then be default if the user omits it or the user's code if it's specified.

from relm4.

mmstick avatar mmstick commented on May 19, 2024

You're right about not enforcing the requirement of a trait. I have just updated my implementation with a similar approach.

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

use crate::*;

/// The pieces that make up the state of the component.
pub struct ComponentInner<Model, Widgets, Input, Output> {
    pub model: Model,
    pub widgets: Widgets,
    pub input: Sender<Input>,
    pub output: Sender<Output>,
}

/// 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 InitParams;

    /// 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 Root: Clone + AsRef<gtk4::Widget>;

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

    /// Initializes the root widget
    fn init_root() -> Self::Root;

    fn init_inner(
        params: Self::InitParams,
        root_widget: &Self::Root,
        input: Sender<Self::Input>,
        output: Sender<Self::Output>,
    ) -> ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>;

    /// 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 init(params: Self::InitParams) -> Registered<Self::Root, Self::Input, Self::Output> {
        let (sender, in_rx) = mpsc::unbounded_channel::<Self::Input>();
        let (out_tx, output) = mpsc::unbounded_channel::<Self::Output>();

        let root = Self::init_root();

        let mut component = Self::init_inner(params, &root, sender, out_tx);

        let handle = Handle {
            widget: root,
            sender: component.input.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 component, event);
                    }

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

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

        Registered {
            handle,
            receiver: output,
        }
    }

    /// Handles input messages and enables the programmer to update the model and view.
    #[allow(unused_variables)]
    fn update(
        component: &mut ComponentInner<Self, Self::Widgets, Self::Input, Self::Output>,
        message: Self::Input,
    ) {
    }
}

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

Updated the macro to utilize this better

pub enum MyCustomInputMessage {
    Variant1,
    Variant2,
}

component! {
    // The model stores the state of this component.
    pub struct MyCustomModel {
        pub state: String,
    }

    // Widgets managed by the view are stored here.
    pub struct MyCustomWidgets {
        description: gtk::Label,
    }

    // The type of the input sender
    type Input = MyCustomInputMessage;

    // The type of the output sender
    type Output = ();

    // Declares the root widget and how it should be constructed.
    type Root = gtk::Box {
        ccs::view! {
            root = gtk::Box {
                set_orientation: gtk::Orientation::Vertical,
            }
        }

        root
    };

    // Constructs the inner component's model and widgets, using the
    // initial parameter given by `args`.
    fn init(args: (), root, input, output) {
        let description = gtk::Label::new();

        root.append(&description);

        ComponentInner {
            model: MyCustomModel { state: String::new() },
            widgets: MyCustomWidgets { description },
            input,
            output
        }
    }

    // Where events are received, with `component` is the `ComponentInner`,
    // and `event` is a `MyCustomInputMessage` which was just received.
    fn update(component, event) {
        match event {
            MyCustomInputMessage::Variant1 => {

            }

            MyCustomInputMessage::Variant2 => {

            }
        }
    }
}

Components can be created and have their output events forwarded:

let counter = InfoButton::init("Clicked 0 times".into(), "Click".into())
    .forward(input.clone(), |event| match event {
        InfoButtonOutput::Clicked => AppEvent::Increment
    });

In process of updating my support panel.

from relm4.

mmstick avatar mmstick commented on May 19, 2024

I'm also thinking of an alternative to the update function which consumes the model, and returns a tuple containing the updated model and a command to perform. I think this is closer to the Elm design.

from relm4.

AaronErhardt avatar AaronErhardt commented on May 19, 2024

Commands are an interesting concept, that could work great with async functions to fetch resources or something similar that needs to run in the background. At least that seems to be the use-case for commands in Elm.

But I don't think it makes sense to consume and return the model. Elm is purely functional and has a compiler to deal with optimization, but Rust usually makes a memcpy when you move an object, which is not ideal. I think &mut is fine and it also makes it easier to track changes with something like the tracker crate.

from relm4.

AaronErhardt avatar AaronErhardt commented on May 19, 2024

I really like the updated version of the new approach.

Yet, I think the macro should be reduced to the relevant parts. The model isn't necessary in the macro and the widgets type can be created by the macro automatically, at least that's what the widget macro does currently. The root widget could also be managed by the macro unless the user wants to overwrite.

from relm4.

mmstick avatar mmstick commented on May 19, 2024

Updated the support panel to use ElmComponent. If we could have a similar API in relm4 I could deprecate my current implementation.

from relm4.

AaronErhardt avatar AaronErhardt commented on May 19, 2024

The API looks good to me.

I think it's important now that we specify a final Component trait definition, bring in into Relm4 for testing and to gather feedback and then we can start planning and implementing the macros to make it as convenient as possible.

from relm4.

mmstick avatar mmstick commented on May 19, 2024

Sure, I'll do that today

from relm4.

Related Issues (20)

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.