winio 0.11.0

Single-threaded async GUI runtime based on compio.
Documentation
use std::{ops::Deref, path::PathBuf, time::Duration};

use compio::{runtime::spawn, time::interval};
use url::Url;
use winio::prelude::*;

use crate::{Error, Result};

pub struct MediaPage {
    window: Child<TabViewItem>,
    media: Child<Media>,
    playing: bool,
    play_button: Child<Button>,
    browse_button: Child<Button>,
    time_slider: Child<Slider>,
    time_label: Child<Label>,
    volume_slider: Child<Slider>,
    volume_label: Child<Label>,
    rate_chooser: Child<ComboBox>,
    loop_check: Child<CheckBox>,
}

impl MediaPage {
    fn set_playing(&mut self, v: bool) -> Result<()> {
        self.playing = v;
        self.play_button
            .set_text(if self.playing { "⏸️" } else { "▶️" })?;
        Ok(())
    }
}

#[derive(Debug)]
pub enum MediaPageEvent {
    ShowMessage(MessageBox),
    ChooseFile,
}

#[derive(Debug)]
pub enum MediaPageMessage {
    Noop,
    Tick,
    Volume,
    Time,
    Play,
    ChooseFile,
    OpenFile(PathBuf),
    Rate,
    Loop,
}

impl Component for MediaPage {
    type Error = Error;
    type Event = MediaPageEvent;
    type Init<'a> = ();
    type Message = MediaPageMessage;

    async fn init(_init: Self::Init<'_>, sender: &ComponentSender<Self>) -> Result<Self> {
        init! {
            window: TabViewItem = (()) => {
                text: "Media",
            },
            media: Media = (&window),
            play_button: Button = (&window) => {
                enabled: false,
                text: "▶️"
            },
            browse_button: Button = (&window) => {
                text: "..."
            },
            time_slider: Slider = (&window) => {
                enabled: false,
                tick_pos: TickPosition::TopLeft,
                minimum: 0,
            },
            time_label: Label = (&window) => {
                halign: HAlign::Right,
            },
            volume_slider: Slider = (&window) => {
                enabled: false,
                tick_pos: TickPosition::TopLeft,
                minimum: 0,
                maximum: 100,
                pos: 100,
                freq: 20,
            },
            volume_label: Label = (&window),
            rate_chooser: ComboBox = (&window) => {
                items: ["0.5x", "1x", "1.5x", "2x", "10x"],
                selection: 1,
                enabled: false,
            },
            loop_check: CheckBox = (&window) => {
                text: "Loop",
                enabled: false,
            },
        }
        sender.post(MediaPageMessage::Volume);

        let sender = sender.clone();
        spawn(async move {
            let mut interval = interval(Duration::from_millis(100));
            loop {
                interval.tick().await;
                sender.post(MediaPageMessage::Tick);
            }
        })
        .detach();

        Ok(Self {
            window,
            media,
            playing: false,
            play_button,
            browse_button,
            time_slider,
            time_label,
            volume_slider,
            volume_label,
            rate_chooser,
            loop_check,
        })
    }

    async fn start(&mut self, sender: &ComponentSender<Self>) -> ! {
        start! {
            sender, default: MediaPageMessage::Noop,
            self.volume_slider => {
                SliderEvent::Change => MediaPageMessage::Volume,
            },
            self.time_slider => {
                SliderEvent::Change => MediaPageMessage::Time,
            },
            self.play_button => {
                ButtonEvent::Click => MediaPageMessage::Play,
            },
            self.browse_button => {
                ButtonEvent::Click => MediaPageMessage::ChooseFile,
            },
            self.rate_chooser => {
                ComboBoxEvent::Select => MediaPageMessage::Rate,
            },
            self.loop_check => {
                CheckBoxEvent::Click => MediaPageMessage::Loop,
            },
        }
    }

    async fn update_children(&mut self) -> Result<bool> {
        update_children!(
            self.window,
            self.media,
            self.play_button,
            self.browse_button,
            self.time_slider,
            self.volume_slider,
            self.volume_label
        )
    }

    async fn update(
        &mut self,
        message: Self::Message,
        sender: &ComponentSender<Self>,
    ) -> Result<bool> {
        match message {
            MediaPageMessage::Noop => Ok(false),
            MediaPageMessage::Tick => {
                fn format_duration(dur: Duration) -> String {
                    let secs = dur.as_secs();
                    let hours = secs / 3600;
                    let minutes = (secs % 3600) / 60;
                    let seconds = secs % 60;
                    if hours > 0 {
                        format!("{hours:02}:{minutes:02}:{seconds:02}")
                    } else {
                        format!("{minutes:02}:{seconds:02}")
                    }
                }

                let ct = self.media.current_time()?;
                let ft = self.media.full_time()?;
                if let Some(ft) = ft {
                    let ft_secs = ft.as_secs_f64();
                    self.time_slider.set_freq((ft_secs * 100.0) as usize / 10)?;
                    self.time_slider.set_maximum((ft_secs * 100.0) as _)?;
                    self.time_slider.set_pos((ct.as_secs_f64() * 100.0) as _)?;
                    self.time_label.set_text(format!(
                        "{} / {}",
                        format_duration(ct),
                        format_duration(ft)
                    ))?;
                    if ft == ct {
                        self.set_playing(false)?;
                    }
                } else {
                    self.time_slider.set_maximum(1)?;
                    self.time_slider.set_pos(0)?;
                    self.time_slider.set_freq(1)?;
                    self.time_label.set_text(format_duration(ct))?;
                }
                Ok(true)
            }
            MediaPageMessage::Volume => {
                let pos = self.volume_slider.pos()?;
                self.volume_label.set_text(pos.to_string())?;
                self.media.set_volume(pos as f64 / 100.0)?;
                Ok(true)
            }
            MediaPageMessage::Time => {
                let pos = self.time_slider.pos()?;
                let ft = self.media.full_time()?;
                if ft.is_some() {
                    self.media
                        .set_current_time(Duration::from_secs_f64(pos as f64 / 100.0))?;
                }
                Ok(true)
            }
            MediaPageMessage::Play => {
                if self.playing {
                    self.media.pause()?;
                    self.set_playing(false)?;
                } else {
                    self.media.play()?;
                    self.set_playing(true)?;
                }
                Ok(true)
            }
            MediaPageMessage::ChooseFile => {
                sender.output(MediaPageEvent::ChooseFile);
                Ok(false)
            }
            MediaPageMessage::OpenFile(p) => {
                let url =
                    Url::from_file_path(&p).map_err(|_| std::io::ErrorKind::InvalidFilename)?;
                match self.media.load(url.as_str()).await {
                    Ok(()) => {
                        self.volume_slider.enable()?;
                        self.time_slider.enable()?;
                        self.play_button.enable()?;
                        self.rate_chooser.enable()?;
                        self.rate_chooser.set_selection(1)?;
                        self.loop_check.enable()?;
                        self.loop_check.set_checked(false)?;
                        self.media.play()?;
                        self.set_playing(true)?;
                    }
                    Err(e) => {
                        self.volume_slider.disable()?;
                        self.time_slider.disable()?;
                        self.play_button.disable()?;
                        self.rate_chooser.disable()?;
                        self.loop_check.disable()?;
                        self.set_playing(false)?;
                        sender.output(MediaPageEvent::ShowMessage(
                            MessageBox::new()
                                .buttons(MessageBoxButton::Ok)
                                .style(MessageBoxStyle::Error)
                                .message(format!("Failed to load media file: {}", e)),
                        ));
                    }
                }
                Ok(true)
            }
            MediaPageMessage::Rate => {
                let selection = self.rate_chooser.selection()?;
                let rate = match selection {
                    Some(0) => 0.5,
                    Some(2) => 1.5,
                    Some(3) => 2.0,
                    Some(4) => 10.0,
                    _ => 1.0,
                };
                self.media.set_playback_rate(rate)?;
                Ok(true)
            }
            MediaPageMessage::Loop => {
                self.media.set_looped(self.loop_check.is_checked()?)?;
                Ok(true)
            }
        }
    }

    fn render(&mut self, _sender: &ComponentSender<Self>) -> Result<()> {
        let csize = self.window.size()?;
        {
            let margin = Margin::new_all_same(4.0);

            let mut bottom_bar = layout! {
                StackPanel::new(Orient::Horizontal),
                self.play_button   => { margin: margin },
                self.time_slider   => { margin: margin, grow: true },
                self.time_label    => { margin: margin, valign: VAlign::Center, halign: HAlign::Center },
                self.rate_chooser  => { margin: margin, valign: VAlign::Center },
                self.loop_check    => { margin: margin, valign: VAlign::Center },
                self.volume_slider => { margin: margin, width: 200.0 },
                self.volume_label  => { margin: margin, valign: VAlign::Center, halign: HAlign::Left, width: 20.0 },
                self.browse_button => { margin: margin }
            };
            let mut grid = layout! {
                Grid::from_str("1*", "1*,auto").unwrap(),
                self.media => { column: 0, row: 0 },
                bottom_bar => { column: 0, row: 1 },
            };
            grid.set_size(csize)?;
        }
        Ok(())
    }
}

impl Deref for MediaPage {
    type Target = TabViewItem;

    fn deref(&self) -> &Self::Target {
        &self.window
    }
}