Suggestion to make Subscriptions more useful

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 }),
    ])