winio 0.11.0

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

use compio::{runtime::spawn, time::timeout};
use cyper::Client;
use winio::prelude::*;

use crate::{Error, Result};

pub struct NetPage {
    window: Child<TabViewItem>,
    canvas: Child<Canvas>,
    button: Child<Button>,
    entry: Child<Edit>,
    client: Client,
    text: NetFetchStatus,
}

#[derive(Debug)]
pub enum NetFetchStatus {
    Loading,
    Complete(String),
    Error(String),
    Timedout,
}

#[derive(Debug)]
pub enum NetPageEvent {}

#[derive(Debug)]
pub enum NetPageMessage {
    Noop,
    Go,
    Fetch(NetFetchStatus),
}

impl Component for NetPage {
    type Error = Error;
    type Event = NetPageEvent;
    type Init<'a> = ();
    type Message = NetPageMessage;

    async fn init(_init: Self::Init<'_>, sender: &ComponentSender<Self>) -> Result<Self> {
        let url = "https://www.example.com/";
        init! {
            window: TabViewItem = (()) => {
                text: "Networking",
            },
            canvas: Canvas = (&window),
            button: Button = (&window) => {
                text: "Go",
            },
            entry: Edit = (&window) => {
                text: url,
            },
        }

        let client = Client::new();

        let url = url.to_string();
        spawn(fetch(client.clone(), url, sender.clone())).detach();

        Ok(Self {
            window,
            canvas,
            button,
            entry,
            text: NetFetchStatus::Loading,
            client,
        })
    }

    async fn start(&mut self, sender: &ComponentSender<Self>) -> ! {
        start! {
            sender, default: NetPageMessage::Noop,
            self.button => {
                ButtonEvent::Click => NetPageMessage::Go,
            },
            self.entry => {},
        }
    }

    async fn update_children(&mut self) -> Result<bool> {
        update_children!(self.window, self.canvas, self.entry, self.button)
    }

    async fn update(
        &mut self,
        message: Self::Message,
        sender: &ComponentSender<Self>,
    ) -> Result<bool> {
        match message {
            NetPageMessage::Noop => Ok(false),
            NetPageMessage::Go => {
                spawn(fetch(
                    self.client.clone(),
                    self.entry.text()?,
                    sender.clone(),
                ))
                .detach();
                Ok(false)
            }
            NetPageMessage::Fetch(status) => {
                self.text = status;
                Ok(true)
            }
        }
    }

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

        {
            let mut header_panel = layout! {
                StackPanel::new(Orient::Horizontal),
                self.entry => { grow: true },
                self.button
            };
            let mut root_panel = layout! {
                StackPanel::new(Orient::Vertical),
                header_panel,
                self.canvas => { grow: true },
            };
            root_panel.set_size(csize)?;
        }

        let mut ctx = self.canvas.context()?;
        let is_dark = ColorTheme::current()? == ColorTheme::Dark;
        let brush = SolidColorBrush::new(if is_dark {
            Color::new(255, 255, 255, 255)
        } else {
            Color::new(0, 0, 0, 255)
        });
        ctx.draw_str(
            &brush,
            DrawingFontBuilder::new()
                .halign(HAlign::Left)
                .valign(VAlign::Top)
                .family("Courier New")
                .size(12.0)
                .build(),
            Point::zero(),
            match &self.text {
                NetFetchStatus::Loading => "Loading...",
                NetFetchStatus::Complete(s) => s.as_str(),
                NetFetchStatus::Error(e) => e.as_str(),
                NetFetchStatus::Timedout => "Timed out.",
            },
        )?;
        Ok(())
    }
}

impl Deref for NetPage {
    type Target = TabViewItem;

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

async fn fetch_impl(client: Client, url: String) -> Result<NetFetchStatus, NetFetchStatus> {
    let req = client
        .get(url)
        .map_err(|e| NetFetchStatus::Error(e.to_string()))?;
    let res = timeout(Duration::from_secs(8), req.send())
        .await
        .map_err(|_| NetFetchStatus::Timedout)?
        .map_err(|e| NetFetchStatus::Error(e.to_string()))?;
    let s = res
        .text()
        .await
        .map_err(|e| NetFetchStatus::Error(e.to_string()))?;
    Ok(NetFetchStatus::Complete(s))
}

async fn fetch(client: Client, url: String, sender: ComponentSender<NetPage>) {
    sender.post(NetPageMessage::Fetch(NetFetchStatus::Loading));
    let status = match fetch_impl(client.clone(), url.clone()).await {
        Ok(s) => s,
        Err(e) => e,
    };
    sender.post(NetPageMessage::Fetch(status));
}