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)
- Inline + immediate mode (Similar to imgui/conrod):
let mut count = 0; { if (Button::new(format!("{}", count)) { count += 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()); }
- 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
-
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.
-
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.
-
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.