Last active
December 13, 2022 21:31
-
-
Save sowbug/40148c249136a037cc3b4b814e9de129 to your computer and use it in GitHub Desktop.
One way to wrap an Iced subscription around a long-running task in another CPU thread. Probably wrong. See https://github.com/iced-rs/iced/discussions/1600
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use iced::{ | |
executor, time, | |
widget::{button, column, container, text}, | |
window, Application, Command, Event as IcedEvent, Settings, Theme, | |
}; | |
use iced_native::futures::channel::mpsc; | |
use iced_native::subscription::{self, Subscription}; | |
use std::thread::JoinHandle; | |
enum ThingSubscriptionState { | |
Start, | |
Ready(JoinHandle<()>, mpsc::Receiver<ThingEvent>), | |
Ending(JoinHandle<()>), | |
Idle, | |
} | |
#[derive(Debug)] | |
enum ThingInput { | |
Start, | |
Quit, | |
} | |
#[derive(Clone)] | |
enum ThingEvent { | |
Ready(mpsc::Sender<ThingInput>), | |
ProgressReport(i32), | |
Quit, | |
} | |
impl std::fmt::Debug for ThingEvent { | |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
match self { | |
Self::Ready(_) => f.debug_tuple("Ready").finish(), | |
Self::ProgressReport(i) => write!(f, "ProgressReport {}", i), | |
ThingEvent::Quit => write!(f, "Quit"), | |
} | |
} | |
} | |
#[derive(Default)] | |
struct Thing { | |
counter: i32, | |
done: bool, | |
} | |
impl Thing { | |
pub fn new() -> Self { | |
Default::default() | |
} | |
pub fn work(&mut self) { | |
// This sleep represents a bunch of CPU-intensive work that isn't | |
// amenable to .await. | |
std::thread::sleep(time::Duration::from_millis(100)); | |
self.counter += 1; | |
println!("Thing counter is {}", self.counter); | |
} | |
pub fn update(&mut self, input: ThingInput) -> Option<ThingEvent> { | |
match input { | |
ThingInput::Start => { | |
println!("Thing Received ThingInput::Start"); | |
self.counter = 0; | |
None | |
} | |
ThingInput::Quit => { | |
println!("Thing Received ThingInput::Quit"); | |
self.done = true; | |
Some(ThingEvent::Quit) | |
} | |
} | |
} | |
pub fn done(&self) -> bool { | |
self.done | |
} | |
fn x(&self) -> i32 { | |
self.counter | |
} | |
fn subscription() -> Subscription<ThingEvent> { | |
subscription::unfold( | |
std::any::TypeId::of::<Thing>(), | |
ThingSubscriptionState::Start, | |
|state| async move { | |
match state { | |
ThingSubscriptionState::Start => { | |
// This channel lets the app send messages to the closure. | |
let (sender, mut receiver) = mpsc::channel::<ThingInput>(1024); | |
// This channel surfaces event messages from Thing as subscription events. | |
let (mut output_sender, output_receiver) = | |
mpsc::channel::<ThingEvent>(1024); | |
let handler = std::thread::spawn(move || { | |
let mut thing = Thing::new(); | |
loop { | |
thing.work(); | |
if let Ok(Some(input)) = receiver.try_next() { | |
println!("Subscription got message {:?}", input); | |
if let Some(event) = thing.update(input) { | |
output_sender.try_send(event); | |
} | |
} | |
output_sender.try_send(ThingEvent::ProgressReport(thing.x())); | |
if thing.done() { | |
println!("Subscription exiting loop"); | |
break; | |
} | |
} | |
}); | |
( | |
Some(ThingEvent::Ready(sender)), | |
ThingSubscriptionState::Ready(handler, output_receiver), | |
) | |
} | |
ThingSubscriptionState::Ready(handler, mut output_receiver) => { | |
let (is_done, event) = if let Ok(Some(event)) = output_receiver.try_next() { | |
if let ThingEvent::Quit = event { | |
println!("Subscription peeked at ThingEvent::Quit"); | |
(true, Some(event)) | |
} else { | |
(false, Some(event)) | |
} | |
} else { | |
(false, None) | |
}; | |
if is_done { | |
println!("Subscription is forwarding ThingEvent::Quit to app"); | |
(event, ThingSubscriptionState::Ending(handler)) | |
} else { | |
( | |
event, | |
ThingSubscriptionState::Ready(handler, output_receiver), | |
) | |
} | |
} | |
ThingSubscriptionState::Ending(handler) => { | |
println!("Subscription ThingState::Ending"); | |
if let Ok(_) = handler.join() { | |
println!("Subscription handler.join()"); | |
} | |
// See https://github.com/iced-rs/iced/issues/1348 | |
return (None, ThingSubscriptionState::Idle); | |
} | |
ThingSubscriptionState::Idle => { | |
println!("Subscription ThingState::Idle"); | |
// I took this line from | |
// https://github.com/iced-rs/iced/issues/336, but I | |
// don't understand why it helps. I think it's necessary | |
// for the system to get a chance to process all the | |
// subscription results. | |
let _: () = iced::futures::future::pending().await; | |
(None, ThingSubscriptionState::Idle) | |
} | |
} | |
}, | |
) | |
} | |
} | |
// Loader is a stub to show how the app bootstraps itself from nothing to having | |
// whatever persistent state it needs to run. This example would be sufficient | |
// without it. | |
struct Loader {} | |
impl Loader { | |
async fn load() -> bool { | |
true | |
} | |
} | |
#[derive(Clone, Debug)] | |
enum AppMessage { | |
Loaded(bool), | |
StartButtonPressed, | |
StopButtonPressed, | |
ThingEvent(ThingEvent), | |
Event(IcedEvent), | |
} | |
#[derive(Default)] | |
struct MyApp { | |
should_exit: bool, | |
sender: Option<mpsc::Sender<ThingInput>>, | |
x: i32, | |
done_with_thing: bool, | |
} | |
impl Application for MyApp { | |
type Message = AppMessage; | |
type Theme = Theme; | |
type Executor = executor::Default; | |
type Flags = (); | |
fn new(_flags: Self::Flags) -> (Self, iced::Command<Self::Message>) { | |
( | |
Self::default(), | |
Command::perform(Loader::load(), AppMessage::Loaded), | |
) | |
} | |
fn title(&self) -> String { | |
"App Sandbox".to_string() | |
} | |
fn update(&mut self, message: Self::Message) -> iced::Command<Self::Message> { | |
match message { | |
AppMessage::Loaded(load_success) => { | |
if load_success { | |
*self = MyApp::default(); | |
} else { | |
todo!() | |
} | |
Command::none() | |
} | |
AppMessage::StartButtonPressed => { | |
if let Some(sender) = &mut self.sender { | |
sender.try_send(ThingInput::Start); | |
} | |
Command::none() | |
} | |
AppMessage::StopButtonPressed => { | |
if let Some(sender) = &mut self.sender { | |
sender.try_send(ThingInput::Quit); | |
} | |
Command::none() | |
} | |
AppMessage::ThingEvent(message) => { | |
match message { | |
ThingEvent::Ready(mut sender) => { | |
sender.try_send(ThingInput::Start); | |
self.sender = Some(sender); | |
} | |
ThingEvent::ProgressReport(value) => { | |
self.x = value; | |
} | |
ThingEvent::Quit => { | |
println!("App received ThingEvent::Quit"); | |
self.done_with_thing = true; | |
} | |
} | |
Command::none() | |
} | |
AppMessage::Event(event) => { | |
if let IcedEvent::Window(window::Event::CloseRequested) = event { | |
println!("App got window::Event::CloseRequested"); | |
if let Some(sender) = &mut self.sender { | |
sender.try_send(ThingInput::Quit); | |
} | |
self.should_exit = true; | |
} | |
Command::none() | |
} | |
} | |
} | |
fn view(&self) -> iced::Element<'_, Self::Message, iced::Renderer<Self::Theme>> { | |
let content = column![ | |
text(format!("Progress: {}", self.x).to_string()), | |
button("Start").on_press(AppMessage::StartButtonPressed), | |
button("Stop").on_press(AppMessage::StopButtonPressed) | |
]; | |
container(content).into() | |
} | |
fn subscription(&self) -> iced::Subscription<Self::Message> { | |
if self.done_with_thing { | |
iced_native::subscription::events().map(AppMessage::Event) | |
} else { | |
Subscription::batch([ | |
Thing::subscription().map(AppMessage::ThingEvent), | |
iced_native::subscription::events().map(AppMessage::Event), | |
]) | |
} | |
} | |
fn should_exit(&self) -> bool { | |
// We need to override this to ask the Thing thread to quit when the | |
// user asks to close the app. Otherwise the app will linger in a zombie | |
// state after the main window goes away. | |
self.should_exit | |
} | |
} | |
pub fn main() -> iced::Result { | |
let need_close_requested = true; | |
let settings = Settings { | |
exit_on_close_request: !need_close_requested, | |
..Settings::default() | |
}; | |
MyApp::run(settings) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment