Canonical way to change non-moved state from a closure/event?


#1

Hello, first off I apologize if this is a vague question but I will try to explain as best as I can. I’ve been trying to learn Rust for a little while by writing a GUI framework and one problem I haven’t figure out a nice solution for yet is how to handle events such as a mouse click on a button which can mutate ‘external state’. I’ll list methods I have tried or considered and I’m curious if there’s a better way.

The problem

I would like to have a struct Button which has events such as on_click for which you can register functions that will be called when executed. For example, a ‘counting button’ which increments every time it’s clicked.

Possible solutions (with pseudo code)

  1. Inline + immediate mode (Similar to imgui/conrod):
let mut count = 0;
{
    if (Button::new(format!("{}", count)) {
        count += 1;
    }
}
  1. Closures + Rc<RefCell<>>
let count = Rc::new(RefCell::new(0)); // Could be Cell here but not in general case
let button = Button::new();
let count_cl = count.clone();
button.on_click_fn(Box::new(move |button| {
    count_cl.borrow_mut() += 1;
    button.set_text(format!("{}", count_cl.borrow());
}
  1. Message passing via async channels (thanks to futures-rs/tokio)
    I’ll skip writing buggy pseudo code for this as I haven’t figured out an elegant and non-verbose way to code it yet, but essentially each object that will handle events will have an async (tx, rx) channel and it will return the tx upon creation (instead of itself) and then spawn a task that takes the object mutably waiting and executing upon events using the rx. Events will also allow to be passed on if requested by other objects. This seems a bit like the actor model, but I’m not really familiar enough with it to say for sure.

Thoughts

  1. Method one is simply feels too limiting if you want to create a retained mode guy. I could be wrong, but I don’t see how I would use it. It also encourages writing a single giant function.

  2. Method two works but feels somewhat clunky and verbose. Also it requires things to be copyable to be acted upon which seems like a limitation.

  3. In terms of logical clarity, message passing seems and feels great. But there are two downsides as far as I see: 1) performance, which I will ignore for now and 2) insane amounts of boilerplate for every single widget/interaction with a widget event. This is in large part due to lack of traditional inheritance in Rust. As far as I can tell there is no way to annotate something as a ‘widget’ and it will automatically get all the boilerplate things like channels created (unless maybe writing a complex macro?). Best I can see is creating a Widget trait which will have a fn get_rx_channel() and then implement some boilerplate functions like starting a task using that, but only handles a tiny part of the problem.

Closing

Again sorry for the super long first post. Outside of this problem and lack of ‘state inheritance’ (which I think something like trait fields would alleviate) I want to say Rust has been an absolute pleasure to work with! Any thoughts would be appreciated. Cheers.


#2

Number 2 would be annoying also because you’d probably run afoul of having multiple mutable borrows of &self (Button) when delivering the callback to registered FnMuts (I assume these closures would take a “&mut Button” as an arg). Right?


#3

For that I implemented the Button’s internal data like so Rc<RefCell<ButtonData>>. It’s not nice (requires a lot of borrow_mut’s, which I guess also has a small perf penalty) but it does handle the problem of multiple mut borrows. I guess the usage is also sort of telling a lie because the Button returned in Button::new() is immutable even though its data does mutate via the RefCells.


#4

Yeah, it’s just dynamic borrows aren’t all that great (although obviously required in some cases). I figured while you were listing pros/cons, I thought the need to use RefCell and dynamic borrows would be on that list :slight_smile: