antoyo / relm

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

How to implement callbacks #203

Closed MGlolenstine closed 4 years ago

MGlolenstine commented 4 years ago

Let's say that I have a counter implemented using a "+" and a "-" buttons, with current value written between them. Whenever I press on the "+" button, the value increases and for "-" decreases by 1.

Now, how would I capture that value and send it back to the "main view", if this "counter" is a Widget, that's included in the top view?

To make it by an example, how would I pass the cur_number to the Counter widget and get it called, every time the number changes?

I did try manually implementing the Counter struct and giving it an FnMut variable type, but then I also had to initialize a new window, add it as root and then the widget didn't want to work as I wanted it to.

Would I be able to add a constructor, that'd take a callback as an argument without having to create a new window and still have it initialized with the #[widget]. I think that an example like this is missing and it'd be really nice to have it included in the repository.

Widget:

use gtk::{
    ButtonExt,
    LabelExt,
    OrientableExt,
    WidgetExt,
};
use gtk::Orientation::{Vertical};
use relm::{Widget};
use relm_derive::{widget, Msg};
use CounterMsg::*;

pub struct CounterModel {
    counter: i32,
}

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

#[widget]
impl Widget for Counter {
    fn model() -> CounterModel {
        CounterModel {
            counter: 0,
        }
    }

    fn update(&mut self, event: CounterMsg) {
        match event {
            Decrement => {
                self.model.counter -= 1;
                cur_number(self.model.counter);
            },
            Increment => {
                self.model.counter += 1;
                cur_number(self.model.counter);
            },
        }
    }

    view! {
        gtk::Box {
            orientation: Vertical,
            gtk::Button {
                label: "+",
                property_height_request: 100,
                clicked => Increment,
            },
            gtk::Label {
                label: "0",
                vexpand: true,
                text: &self.model.counter.to_string(),
            },
            gtk::Button {
                label: "-",
                property_height_request: 100,
                clicked => Decrement,
            },
        }
    }
}

Main view:

use crate::utils::widgets::counter::Counter;
use gtk::Orientation::*;
use gtk::{ContainerExt, GtkWindowExt, OrientableExt};
use gtk::{Inhibit, WidgetExt};
use relm::Widget;
use relm_derive::{widget, Msg};

#[derive(Msg)]
pub enum Msg {
    Quit,
}

pub struct Model {}

#[widget]
impl Widget for Win {
    fn model() -> Model {
        Model {}
    }

    fn update(&mut self, event: Msg) {
        match event {
            Msg::Quit => gtk::main_quit(),
        }
    }

    view! {
        gtk::Window {
            property_default_width: 480,
            property_default_height: 272,
            border_width: 0,
            position: gtk::WindowPosition::Center,
            resizable: false,
            decorated: false,
            gtk::Box {
                orientation: Vertical,
                Counter{}
            },
            delete_event(_, _) => (Msg::Quit, Inhibit(false)),
        }
    }
}

use crate::utils::gpio::Gpio;
pub fn cur_number(number: i32) {
    if number > 10 || number < -10 {
        Gpio::number_to_pin(69).set_state(true);
        println!("Test light should be on!");
    } else {
        Gpio::number_to_pin(69).set_state(false);
        println!("Test light should be off!");
    }
}
antoyo commented 4 years ago

If you want to send a state to another widget, to need to use message passing. Look at this example.

MGlolenstine commented 4 years ago

Ok, so I basically have to send something to the fn update inside the widget? So I could send a callback function using the Msg passing with a function as an argument and then just save it in the widget?

antoyo commented 4 years ago

Actually, no. You would need to do something like:

#[derive(Msg)]
pub enum CounterMsg {
    Decrement,
    Increment,
    SendValue(i32),
}

#[widget]
impl Widget for Counter {
    fn model(relm: &Relm<Self>, _: ()) -> CounterModel {
        CounterModel {
            counter: 0,
            relm: relm.clone(),
        }
    }

    fn update(&mut self, event: CounterMsg) {
        match event {
            Decrement => {
                self.model.counter -= 1;
                self.model.relm.emit(SendValue(self.model.counter));
            },
            Increment => {
                self.model.counter += 1;
                self.model.relm.emit(SendValue(self.model.counter));
            },
        }
    }

    view! {
        gtk::Box {
            orientation: Vertical,
            gtk::Button {
                label: "+",
                property_height_request: 100,
                clicked => Increment,
            },
            gtk::Label {
                label: "0",
                vexpand: true,
                text: &self.model.counter.to_string(),
            },
            gtk::Button {
                label: "-",
                property_height_request: 100,
                clicked => Decrement,
            },
        }
    }
}

and you receive it in the window like this:

#[widget]
impl Widget for Win {
    fn model() -> Model {
        Model {}
    }

    fn update(&mut self, event: Msg) {
        match event {
            Msg::Quit => gtk::main_quit(),
        }
    }

    view! {
        gtk::Window {
            // …
            gtk::Box {
                orientation: Vertical,
                Counter{
                    SendValue(value) => println!("value"), // TODO: do something with the value, could send a message to do stuff in the update method.
                }
            },
            delete_event(_, _) => (Msg::Quit, Inhibit(false)),
        }
    }
}
MGlolenstine commented 4 years ago

Oh so a parent propagation with the emit. Thanks for your help!

MGlolenstine commented 4 years ago

@antoyo I'm having some slight problems with the emit calls, as it says that Relm<Counter>, that's the widget one, doesn't implement it.

no method named `emit` found for struct `relm::state::Relm<utils::widgets::counter::Counter>` in the current scope

method not found in `relm::state::Relm<utils::widgets::counter::Counter>`

EDIT

My CounterModel looks like this

pub struct CounterModel {
    counter: i32,
    relm: Relm<Counter>,
}
antoyo commented 4 years ago

Sorry, the syntax is:

self.model.relm.stream().emit(Msg);

I forgot the call to stream().

MGlolenstine commented 4 years ago

It works perfectly now!

Thanks!