Skip to content

Instantly share code, notes, and snippets.

@sowbug
Last active December 13, 2022 21:31
Show Gist options
  • Save sowbug/40148c249136a037cc3b4b814e9de129 to your computer and use it in GitHub Desktop.
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
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