use std::{time::Duration, thread::ThreadId}; use adw::prelude::ButtonExt; use gtk::prelude::WidgetExt; use relm4::*; /// A message that can be provided to the runner #[derive(Debug)] pub enum RunMsg { Toggle, Pass(ThreadId), } /// A message that the runner can forward to the app #[derive(Debug)] pub enum RunOutput { Advance, } /// The runner's internal state pub struct RunModel { running: bool, id: Option, } #[relm4::component(pub)] impl SimpleComponent for RunModel { type Widgets = RunWidgets; type Init = (); type Input = RunMsg; type Output = RunOutput; view! { #[name = "button"] >k::Button { #[watch] set_tooltip_text: Some(if model.running { "Pause" } else { "Play" }), #[watch] set_icon_name: if model.running { "media-playback-stop-symbolic" } else { "media-playback-start-symbolic" }, connect_clicked[sender] => move |_| { sender.input(RunMsg::Toggle); }, } } fn init( _init: Self::Init, root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { let model = RunModel { running: false, id: None, }; let widgets = view_output!(); ComponentParts { model, widgets } } fn update( &mut self, input: RunMsg, sender: ComponentSender, ) { match input { // Toggle the play-pause state of the runner RunMsg::Toggle => { self.running = !self.running; } // Called every 500ms when running RunMsg::Pass(id) => { if self.id != Some(id) {return} } } /* Here, to prevent the possibility of a user pressing the play button twice in quick * succession, spawning two threads that are both advancing the game, the runner keeps * track of which thread id is the most recently spawned. If a thread detects that it is * not the most recent, it will close. */ if self.running { sender.output(RunOutput::Advance).unwrap(); self.id = Some(std::thread::spawn(move || { // Note that timing is handled by the application, not the library. Another // developer could change it std::thread::sleep(Duration::from_millis(500)); sender.input(RunMsg::Pass(std::thread::current().id())); }).thread().id()); } } }