multithreadinguser-interfacerustcrossbeam

How to send messages to iced application


I have an iced application that I want to control through a web UI. For this I use axum and spawn it in a different tokio task. I would like to send a message from the axum endpoint to the iced application, but I can neither invoke the update method of the iced application nor can I listen to the crossbeam channel inside the iced application.

My code looks like this right now:

mod messages;
mod ui;
mod web;

use crossbeam_channel::unbounded;
use iced::Application;
use iced::Settings;
use tokio;

#[tokio::main]
async fn main() -> iced::Result {
    let (s, r) = unbounded::<messages::WebToUIMessage>();
    let server_handle = tokio::task::spawn(async { web::run_server(s).await });

    let result = ui::MediaHub::run(Settings::default());
    // We abort the server when the UI finishes
    server_handle.abort();
    result
}

Solution

  • You can pass a channel receiver through a Flags struct when starting an iced app. Then in the subscription function use iced::subscription::unfold to convert the received channel messages into iced messages that the UI will respond to. Runnable code is as follows:

    use iced::{widget::text, Application, Command, Element, Settings, Subscription};
    use tokio::sync::mpsc;
    use std::cell::RefCell;
    
    fn main() -> iced::Result {
        let (sender, receiver) = mpsc::unbounded_channel::<i32>();
    
        std::thread::spawn(move || {
            for i in 0.. {
                sender.send(i).unwrap();
                std::thread::sleep(std::time::Duration::from_millis(200));
            }
        });
    
        Ui::run(Settings::with_flags(UiFlags { receiver }))
    }
    
    struct UiFlags {
        receiver: mpsc::UnboundedReceiver<i32>,
    }
    
    struct Ui {
        receiver: RefCell<Option<mpsc::UnboundedReceiver<i32>>>,
        num: i32,
    }
    
    #[derive(Debug, Clone)]
    enum Message {
        ExternalMessageReceived(i32),
    }
    
    impl Application for Ui {
        type Executor = iced::executor::Default;
        type Message = Message;
        type Theme = iced::Theme;
        type Flags = UiFlags;
    
        fn new(flags: UiFlags) -> (Self, Command<Message>) {
            let app = Ui {
                receiver: RefCell::new(Some(flags.receiver)),
                num: 0,
            };
            (app, Command::none())
        }
    
        fn title(&self) -> String {
            String::from("External Message Example")
        }
    
        fn update(&mut self, message: Message) -> Command<Message> {
            match message {
                Message::ExternalMessageReceived(num) => {
                    self.num = num;
                }
            }
            Command::none()
        }
    
        fn subscription(&self) -> Subscription<Message> {
            iced::subscription::unfold(
                "led changes",
                self.receiver.take(),
                move |mut receiver| async move {
                    let num = receiver.as_mut().unwrap().recv().await.unwrap();
                    (Some(Message::ExternalMessageReceived(num)), receiver)
                },
            )
        }
    
        fn view(&self) -> Element<Message> {
            text(self.num).into()
        }
    }