OK, this is fairly long, because in addition to a practical issue, I’m trying to overcome a conceptual obstacle. I appreciate your patience (if any
)
I’m developing a program for working with sound samples. So, as you might expect, a key part of the functionality is playing sound. I’m having some trouble figuring out how to exchange messages between the GUI and the audio backend (I have an audio module based on rodio, and there’s a Controller struct that handles operations like playing a sound, stopping playback, etc.).
To give a bit of background, I’m fairly new to Rust, and have not worked with async abstractions in any language. So I’m dealing with both the practical issue of how, specifically, to send messages, and with the more abstract issue of how to structure the whole relationship between the GUI and the backend.
I initially developed a prototype with GTK-RS, and I used std::sync::mpsc::{Sender, Receiver} for messaging, and that worked fine. But I decided I didn’t want to work with GTK (too much boilerplate code, and doubts about cross-platform performance), and decided that Iced would be a good way to go. I first ported the code over before Iced 0.14 was released, and my approach seemed to work fine with 0.13. However, I’m now trying to update my code for 0.14, and I find that I can’t instantiate an application the way I intended to.
My main application state currently looks like this:
pub struct MainWindow<'a> {
split_at: f32,
browser: SourceBrowser,
organizer: Organizer,
project: Rc<Project>,
req_tx: TxWrapper<'a, ACReq>,
rsp_rx: RxWrapper<'a, ACRsp>,
playing: Option<String>,
}`
browser and organizer are GUI components which comprise the two panes in a vertical split layout (using the iced_split crate). Project is a non-GUI struct that manages most of the data for a session: audio buffers, file paths, metadata, etc. TxWrapper and RxWrapper contain, respectively, a std::sync::mpsc::Sender and a std::sync::mpsc::Receiver. TBH I don’t remember the exact reason I created these wrappers (probably something about avoiding lifetime problems ;-)), so I’m going to revisit that decision before long, but I also don’t think they’re causing the current problem.
I’m trying to instantiate the application like this:
application(
|| {
MainWindow::new(
Rc::new(project.clone()),
TxWrapper::new(&req_tx),
RxWrapper::new(&rsp_rx)
)
},
MainWindow::update,
MainWindow::view
).run()
This fails with Error 0525. I see that the BootFn has to implement Fn, but my closure is only FnOnce. And furthermore, it appears that the std::sync::mpsc::Receiver is an immovable obstacle, because it cannot be cloned or copied. So I think that it is simply not possible for my main state struct to contain a Receiver (can anyone confirm or deny that this is the case?). So the question is, how can I access incoming messages from the audio backend?
I’ve been looking at the docs for Task and Subscription, and I guess these are what I need to use, but both of those tools have a lot of variations, and I’m having trouble understanding how, specifically, to use them. From the docs, it doesn’t seem completely clear that
these facilities are the solution for my use case.
It seems to me that there is a fundamental difference between what I am doing and all examples I’ve seen of using these constructs. These tools are used to invoke time-consuming
operations, and we want to be able to continue interacting with the GUI while those operations are taking place, right? So far, so good.
The difference I see is: typically, the “time-consuming operation” is something like a complex calculation or a network request, and something in the program depends on the result of that operation. Whereas in the case of playing sounds, the completion of the
“time-consuming operation” doesn’t produce a meaningful result - rather, the time-consuming operation (e.g. playing the sound) is the whole point. So, I don’t suppose this means that Task and Subscription aren’t the right tools for the job, but I think it probably has consequences for exactly how messaging should be implemented. But every example I’ve seen of using these constructs seems to address the more common use case where the point of the
operation is to obtain the result, so I’m having trouble understanding how to work with Task and/or Subscription for my use case.
There are more details I could explain, but I think the above is enough to begin unraveling my issue. Thanks for reading! And if you can point me to examples or docs that might address my use case, I’d greatly appreciate it.