winio 0.11.0

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

use compio::runtime::spawn_blocking;
use compio_log::error;
use image::{DynamicImage, ImageReader};
use itertools::Itertools;
use winio::prelude::*;

use crate::{Error, Result};

pub struct GalleryPage {
    window: Child<TabViewItem>,
    canvas: Child<Canvas>,
    scrollbar: Child<ScrollBar>,
    button: Child<Button>,
    entry: Child<Edit>,
    list: Child<ObservableVec<String>>,
    listbox: Child<ListBox>,
    images: Vec<DynamicImage>,
    sel_images: BTreeMap<usize, Option<DrawingImage>>,
}

const MAX_COLUMN: usize = 3;

impl GalleryPage {
    fn update_scrollbar(&mut self) -> Result<()> {
        let pos = self.scrollbar.pos()?;

        let size = self.canvas.size()?;
        let occupy_width = size.width / (MAX_COLUMN as f64);
        let content_width = occupy_width - 10.0;
        let content_height: f64 = self
            .sel_images
            .keys()
            .chunks(MAX_COLUMN)
            .into_iter()
            .map(|images| {
                (images
                    .map(|i| {
                        let image = &self.images[*i];
                        let image_size = Size::new(image.width() as _, image.height() as _);
                        (image_size.height
                            * (content_width / image_size.width.max(image_size.height)))
                            as usize
                    })
                    .max()
                    .unwrap_or_default() as f64)
                    .min(content_width)
                    + 10.0
            })
            .sum();
        self.scrollbar.set_maximum(content_height as _)?;
        self.scrollbar.set_page(size.height as _)?;
        self.scrollbar.set_pos(pos)?;
        Ok(())
    }
}

#[derive(Debug)]
pub enum GalleryPageEvent {
    ShowMessage(MessageBox),
    ChooseFolder,
}

#[derive(Debug)]
pub enum GalleryPageMessage {
    Noop,
    Redraw,
    ChooseFolder,
    OpenFolder(PathBuf),
    OpenError(String),
    Clear,
    Append(PathBuf, DynamicImage),
    List(ObservableVecEvent<String>),
    Select,
    Wheel(Vector),
}

impl Component for GalleryPage {
    type Error = Error;
    type Event = GalleryPageEvent;
    type Init<'a> = ();
    type Message = GalleryPageMessage;

    async fn init(_init: Self::Init<'_>, sender: &ComponentSender<Self>) -> Result<Self> {
        let path = dirs::picture_dir();
        init! {
            window: TabViewItem = (()) => {
                text: "Images",
            },
            canvas: Canvas = (&window),
            scrollbar: ScrollBar = (&window) => {
                orient: Orient::Vertical,
                minimum: 0,
            },
            button: Button = (&window) => {
                text: "...",
            },
            entry: Edit = (&window) => {
                text: path.as_ref()
                          .map(|p| p.to_string_lossy().into_owned())
                          .unwrap_or_default(),
            },
            list: ObservableVec<String> = ([]),
            listbox: ListBox = (&window) => {
                multiple: true,
            },
        }

        if let Some(path) = path {
            let sender = sender.clone();
            spawn_blocking(move || fetch(path, sender)).detach();
        }

        Ok(Self {
            window,
            canvas,
            scrollbar,
            button,
            entry,
            list,
            listbox,
            images: vec![],
            sel_images: BTreeMap::new(),
        })
    }

    async fn start(&mut self, sender: &ComponentSender<Self>) -> ! {
        start! {
            sender, default: GalleryPageMessage::Noop,
            self.canvas => {
                CanvasEvent::MouseWheel(w) => GalleryPageMessage::Wheel(w),
            },
            self.button => {
                ButtonEvent::Click => GalleryPageMessage::ChooseFolder,
            },
            self.list => {
                e => GalleryPageMessage::List(e),
            },
            self.listbox => {
                ListBoxEvent::Select => GalleryPageMessage::Select,
            },
            self.scrollbar => {
                ScrollBarEvent::Change => GalleryPageMessage::Redraw,
            }
        }
    }

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

    async fn update(
        &mut self,
        message: Self::Message,
        sender: &ComponentSender<Self>,
    ) -> Result<bool> {
        self.update_scrollbar()?;
        match message {
            GalleryPageMessage::Noop => Ok(false),
            GalleryPageMessage::Redraw => Ok(true),
            GalleryPageMessage::ChooseFolder => {
                sender.output(GalleryPageEvent::ChooseFolder);
                Ok(false)
            }
            GalleryPageMessage::OpenFolder(p) => {
                self.entry.set_text(p.to_str().unwrap_or_default())?;
                let sender = sender.clone();
                spawn_blocking(move || fetch(p, sender)).detach();
                Ok(true)
            }
            GalleryPageMessage::OpenError(e) => {
                sender.output(GalleryPageEvent::ShowMessage(
                    MessageBox::new()
                        .message(&e)
                        .style(MessageBoxStyle::Error)
                        .buttons(MessageBoxButton::Ok),
                ));
                Ok(false)
            }
            GalleryPageMessage::Clear => {
                self.list.clear();
                self.images.clear();
                self.sel_images.clear();
                Ok(true)
            }
            GalleryPageMessage::Append(path, image) => {
                if let Some(filename) = path.file_name() {
                    self.list.push(filename.to_string_lossy().into_owned());
                    self.images.push(image);
                    Ok(true)
                } else {
                    Ok(false)
                }
            }
            GalleryPageMessage::List(e) => Ok(self
                .listbox
                .emit(ListBoxMessage::from_observable_vec_event(e))
                .await?),
            GalleryPageMessage::Select => {
                for i in 0..self.list.len() {
                    if self.listbox.is_selected(i)? {
                        self.sel_images.entry(i).or_insert(None);
                    } else {
                        self.sel_images.remove(&i);
                    }
                }
                Ok(true)
            }
            GalleryPageMessage::Wheel(w) => {
                let delta = w.y;
                let pos = self.scrollbar.pos()?;
                self.scrollbar
                    .set_pos((pos as f64 - delta).max(0.0) as usize)?;
                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 content_panel = layout! {
                StackPanel::new(Orient::Horizontal),
                self.listbox,
                self.canvas => { grow: true },
                self.scrollbar,
            };
            let mut root_panel = layout! {
                StackPanel::new(Orient::Vertical),
                header_panel,
                content_panel => { grow: true },
            };
            root_panel.set_size(csize)?;
        }

        let pos = self.scrollbar.pos()?;

        let size = self.canvas.size()?;
        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)
        });
        let pen = BrushPen::new(&brush, 1.0);

        let occupy_width = size.width / (MAX_COLUMN as f64);
        let content_width = occupy_width - 10.0;
        for (i, image) in self.sel_images.iter_mut() {
            if image.is_none() {
                let cache = ctx.create_image(self.images[*i].clone())?;
                *image = Some(cache);
            }
        }
        let content_heights = self
            .sel_images
            .values()
            .chunks(MAX_COLUMN)
            .into_iter()
            .map(|images| {
                (images
                    .filter_map(|image| {
                        let image_size = image.as_ref()?.size().ok()?;
                        Some(
                            (image_size.height
                                * (content_width / image_size.width.max(image_size.height)))
                                as usize,
                        )
                    })
                    .max()
                    .unwrap_or_default() as f64)
                    .min(content_width)
            })
            .collect::<Vec<_>>();
        for (i, image) in self.sel_images.values().enumerate() {
            if let Some(image) = image {
                let image_size = image.size()?;
                let c = i % MAX_COLUMN;
                let r = i / MAX_COLUMN;
                let x = c as f64 * occupy_width + 5.0;
                let y =
                    content_heights[..r].iter().map(|h| h + 10.0).sum::<f64>() + 5.0 - pos as f64;
                let content_height = content_heights[r];
                let rect = Rect::new(Point::new(x, y), Size::new(content_width, content_height));
                let rate =
                    (content_width / image_size.width).min(content_height / image_size.height);
                let real_width = image_size.width * rate;
                let real_height = image_size.height * rate;
                let real_x = (content_width - real_width) / 2.0;
                let real_y = (content_height - real_height) / 2.0;
                let real_rect = Rect::new(
                    Point::new(x + real_x, y + real_y),
                    Size::new(real_width, real_height),
                );
                ctx.draw_image(image, real_rect, None)?;
                ctx.draw_rect(&pen, rect)?;
            }
        }
        Ok(())
    }
}

impl Deref for GalleryPage {
    type Target = TabViewItem;

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

fn fetch(path: impl AsRef<Path>, sender: ComponentSender<GalleryPage>) {
    sender.post(GalleryPageMessage::Clear);
    let dir = match path.as_ref().read_dir() {
        Ok(dir) => dir,
        Err(e) => {
            sender.post(GalleryPageMessage::OpenError(e.to_string()));
            return;
        }
    };
    for p in dir {
        let p = match p {
            Ok(e) => e.path(),
            Err(_e) => {
                error!("Failed to read directory entry: {_e:?}");
                continue;
            }
        };
        if let Ok(reader) = ImageReader::open(&p)
            && let Ok(image) = reader.decode()
        {
            sender.post(GalleryPageMessage::Append(p, image));
        }
    }
}