with version 0.13.1 the creation of Subscriptions are limited. One cannot pass any initialising state because Subscription::run
does not accept a closure.
So why is this a problem? Let’s have a look at the websocket example, in there plenty of things are hard coded inside the subscription, for example the websocket server address or port.
In real world applications, such things are configurable to some degree. Usually I would pass this configurability into the application state. From there I would like to pass it down to the Subscription creation.
In my specific case my subscription should talk to a serial device, so the device name and the baudrate are things a user can provide (via gui or cli) this value is then stored in the app state. From there I would like to pass it to the Subscription
creation.
So I suggest to add a new constructor for iced::Subscription
that allows for passing in a closure. This way I could pass in those variables, at least at the time where the subscription is created.
Later on I would like to alter / replace the subscription whenever values in the app state are changing.
You can inject external state into your subscription via Subscription::with
: Subscription in iced - Rust
do you have an example on how I can then retrieve it from within the subscription?
Ah sorry, I think I misunderstood what you’d like to do. If I understand correctly, the iced master version provides Subscription::run_with
for you to do what you asked. You might be able to replicate it on the stable version using subscription::from_recipe
.
I do something like
#[derive(Debug, Clone)]
pub enum Message {
Project(usize, project::Message),
// ...
}
impl App {
// ...
pub fn subscription(&self) -> Subscription<Message> {
self.active_project() // returns Option<(usize, &project::Project)>
.map(|(id, project)| {
project
.subscription()
.with(id)
.map(|(id, msg)| Message::Project(id, msg))
})
.unwrap_or(Subscription::none())
}
}
This is unfortunately not quite what I’m aiming for. Let me give the concrete example:
fn subscription(state: &SerialCommanderGui) -> Subscription<Message> {
// these values should be passed to `crate::serial_events::subscribe_to_serial`
let device = state.selected_device.clone();
let baudrate = state.nano.baudrate.clone();
Subscription::batch([
Subscription::run(crate::subscription::connect_logger),
Subscription::run(crate::serial_events::subscribe_to_serial),
])
}
// in `crate::serial_events`
pub fn subscribe_to_serial(device: String, baudrate: u32) -> impl Stream<Item = Message> {
stream::channel(100, move |mut output| async move {
let mut port = match serialport::new(device, baudrate)
.timeout(std::time::Duration::from_millis(150))
.open()
{
Ok(port) => port,
Err(e) => {
output.send(
Message::SerialEvent(SerialEvent::Error(format!("Failed to open port: {}", e)))).await.ok();
panic!("bad stuff.. ");
}
};
// .. more stuff that I will skip
}
}
- the part that is problematic is that
subscribe_to_serial
has 2 arguments
- also when wrapping
subscribe_to_serial
in a closure it’s not working, e.g.:
Subscription::run(move || crate::serial_events::subscribe_to_serial(device, baudrate)),
Thanks a lot for this hint. It took me a bit to find how this is accessible (iced feature = “advanced”) and then how to work with it. But I guess I got it working.
For the reference:
fn subscription(state: &SerialCommanderGui) -> iced::Subscription<Message> {
use iced::advanced::subscription::from_recipe;
// these values should be passed to `crate::serial_events::subscribe_to_serial`
let device = state.selected_device.clone();
let baudrate = state.nano.baudrate.clone();
Subscription::batch([
Subscription::run(crate::subscription::connect_logger),
from_recipe(SerialSubscription { device, baudrate }),
])
}
// in the crate::serial_events mod
#[derive(Debug, Clone, Hash)]
pub struct SerialSubscription {
pub device: String,
pub baudrate: u32,
}
impl Recipe for SerialSubscription {
type Output = Message;
fn stream(
self: Box<Self>,
_input: EventStream,
) -> iced::futures::stream::BoxStream<'static, Self::Output> {
let device = self.device.clone();
let baudrate = self.baudrate;
let stream = subscribe_to_serial(device, baudrate);
stream.boxed()
}
/// This is clearly a hack, but it works for now.
fn hash(&self, state: &mut iced::advanced::subscription::Hasher) {
state.write(self.device.as_bytes());
}
}
pub fn subscribe_to_serial(device: String, baudrate: u32) -> impl Stream<Item = Message> {
stream::channel(100, move |mut output| async move {
let mut port = match serialport::new(device, baudrate)
.timeout(std::time::Duration::from_millis(150))
.open()
{
Ok(port) => port,
Err(e) => {
output
.send(Message::SerialEvent(SerialEvent::Error(format!(
"Failed to open port: {}",
e
))))
.await
.ok();
panic!("bad stuff");
}
};
// more stuff that is irrelevant
}
}
- introducing
SerialSubscription
to accept the arguments coming from the app state
- impl
Recipe
for SerialSubscription
so that from_recipe
can turn it into a Subscription
1 Like
For ergonomical reasons it would be awesome if impl Recipe
would come with an auto implementation for “into subscription” so that the below would be possible:
Subscription::batch([
Subscription::run(crate::subscription::connect_logger),
Subscription::from(SerialSubscription { device, baudrate }),
])