kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use super::locale::{Locale, Text as T};
use super::{operations, platform, style, window_events};
use iced::widget::{Space, button, column, container, progress_bar, text};
use iced::{Element, Length, Settings, Size, Subscription, Task, Theme, alignment, theme};
use std::path::PathBuf;
use std::time::Duration;

#[path = "uninstaller_chrome.rs"]
mod uninstaller_chrome;

const ACTION_HEIGHT: f32 = 44.0;

#[derive(Debug, Clone)]
enum Message {
    UninstallProgressTick,
    UninstallFinished(Result<(), String>),
    ClosePressed,
    RemovalScheduled(Result<(), String>),
    WindowOpened(iced::window::Id),
    WindowDragStart,
    WindowMinimize,
    WindowClose,
}

struct Uninstaller {
    install_dir: PathBuf,
    uninstalling: bool,
    closing: bool,
    pending_uninstall: Option<PathBuf>,
    progress: f32,
    result: Option<Result<(), String>>,
    close_error: Option<String>,
    window_id: Option<iced::window::Id>,
    locale: Locale,
}

pub fn run(install_dir: PathBuf) -> iced::Result {
    iced::application(
        move || Uninstaller::new(install_dir.clone()),
        Uninstaller::update,
        Uninstaller::view,
    )
    .title(title)
    .theme(theme)
    .subscription(Uninstaller::subscription)
    .style(app_style)
    .settings(Settings {
        antialiasing: true,
        ..Settings::default()
    })
    .window(iced::window::Settings {
        size: Size::new(620.0, 420.0),
        min_size: Some(Size::new(560.0, 360.0)),
        position: iced::window::Position::Centered,
        decorations: false,
        exit_on_close_request: false,
        ..iced::window::Settings::default()
    })
    .run()
}

fn theme(_state: &Uninstaller) -> Theme {
    Theme::Dark
}

fn title(state: &Uninstaller) -> String {
    state.locale.t(T::WindowTitleUninstaller).to_owned()
}

fn app_style(_state: &Uninstaller, _theme: &Theme) -> theme::Style {
    theme::Style {
        background_color: style::BLACK,
        text_color: style::TEXT,
    }
}

impl Uninstaller {
    fn new(install_dir: PathBuf) -> (Self, Task<Message>) {
        (
            Self {
                install_dir: install_dir.clone(),
                uninstalling: true,
                closing: false,
                pending_uninstall: Some(install_dir),
                progress: 0.08,
                result: None,
                close_error: None,
                window_id: None,
                locale: Locale::system(),
            },
            Task::none(),
        )
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::UninstallProgressTick => {
                if let Some(install_dir) = self.pending_uninstall.take() {
                    self.progress = 0.18;
                    return Task::perform(
                        async move { operations::prepare_uninstall(&install_dir) },
                        Message::UninstallFinished,
                    );
                }
                if self.uninstalling {
                    self.progress = if self.progress >= 0.92 {
                        0.18
                    } else {
                        self.progress + 0.08
                    };
                }
            }
            Message::UninstallFinished(result) => {
                self.uninstalling = false;
                self.pending_uninstall = None;
                self.progress = 1.0;
                self.result = Some(result);
            }
            Message::ClosePressed | Message::WindowClose => {
                return self.close_after_uninstall();
            }
            Message::RemovalScheduled(result) => {
                self.closing = false;
                match result {
                    Ok(()) => return self.close_window(),
                    Err(error) => {
                        self.close_error = Some(error);
                    }
                }
            }
            Message::WindowOpened(id) => {
                self.window_id = Some(id);
                return iced::window::run(id, |window| platform::set_rounded_corners(window))
                    .discard();
            }
            Message::WindowDragStart => {
                return self.window_id.map_or_else(Task::none, iced::window::drag);
            }
            Message::WindowMinimize => {
                return self
                    .window_id
                    .map_or_else(Task::none, |id| iced::window::minimize(id, true));
            }
        }
        Task::none()
    }

    fn subscription(&self) -> Subscription<Message> {
        let window_events = Subscription::batch([
            iced::window::open_events().map(Message::WindowOpened),
            iced::event::listen_with(|event, _status, _window| {
                window_events::close_request(event).then_some(Message::WindowClose)
            }),
        ]);
        if self.uninstalling {
            Subscription::batch([
                window_events,
                iced::time::every(Duration::from_millis(120))
                    .map(|_| Message::UninstallProgressTick),
            ])
        } else {
            window_events
        }
    }

    fn view(&self) -> Element<'_, Message> {
        column![
            uninstaller_chrome::title_bar(self.uninstalling, self.closing),
            self.content()
        ]
        .width(Length::Fill)
        .height(Length::Fill)
        .into()
    }

    fn content(&self) -> Element<'_, Message> {
        let mut content = column![
            self.header(),
            self.path_panel(),
            self.status_panel(),
            Space::new().height(Length::Fill),
        ]
        .spacing(14)
        .padding(22);

        if let Some(error) = &self.close_error {
            content = content.push(
                text(error)
                    .font(style::FONT)
                    .size(12)
                    .color(style::WHITE)
                    .width(Length::Fill),
            );
        }

        if !self.uninstalling {
            content = content.push(self.close_button());
        }

        container(content)
            .width(Length::Fill)
            .height(Length::Fill)
            .into()
    }

    fn header(&self) -> Element<'_, Message> {
        column![
            text(self.locale.t(T::UninstallerTitle))
                .font(style::FONT_BOLD)
                .size(30)
                .color(style::TEXT),
            text(self.locale.t(T::UninstallerSubtitle))
                .font(style::FONT)
                .size(15)
                .color(style::MUTED),
        ]
        .spacing(4)
        .into()
    }

    fn path_panel(&self) -> Element<'_, Message> {
        container(
            column![
                text(self.locale.t(T::InstallFolder))
                    .font(style::FONT_BOLD)
                    .size(13)
                    .color(style::WHITE),
                text(self.install_dir.display().to_string())
                    .font(style::FONT)
                    .size(13)
                    .color(style::MUTED)
                    .width(Length::Fill),
            ]
            .spacing(6),
        )
        .padding(14)
        .style(style::soft_panel)
        .width(Length::Fill)
        .into()
    }

    fn status_panel(&self) -> Element<'_, Message> {
        let (title, body) = match self.result.as_ref() {
            Some(Ok(())) => (self.locale.t(T::Removed), self.locale.t(T::RemovedBody)),
            Some(Err(error)) => (self.locale.t(T::UninstallFailed), error.as_str()),
            None => (self.locale.t(T::Removing), self.locale.t(T::RemovingBody)),
        };

        container(
            column![
                text(title)
                    .font(style::FONT_BOLD)
                    .size(17)
                    .color(style::WHITE),
                text(body)
                    .font(style::FONT)
                    .size(13)
                    .color(style::MUTED)
                    .width(Length::Fill),
                progress_bar(0.0..=1.0, self.progress)
                    .girth(Length::Fixed(8.0))
                    .style(style::progress),
            ]
            .spacing(10),
        )
        .padding(14)
        .style(style::panel)
        .width(Length::Fill)
        .into()
    }

    fn close_button(&self) -> Element<'_, Message> {
        let label = if self.closing {
            self.locale.t(T::ClosingEllipsis)
        } else {
            self.locale.t(T::Close)
        };
        button(
            container(
                text(label)
                    .font(style::FONT_BOLD)
                    .size(16)
                    .align_x(alignment::Horizontal::Center),
            )
            .width(Length::Fill)
            .height(Length::Fill)
            .align_x(alignment::Horizontal::Center)
            .align_y(alignment::Vertical::Center),
        )
        .padding(0)
        .width(Length::Fill)
        .height(Length::Fixed(ACTION_HEIGHT))
        .style(style::primary_button)
        .on_press_maybe((!self.closing).then_some(Message::ClosePressed))
        .into()
    }

    fn close_after_uninstall(&mut self) -> Task<Message> {
        if self.uninstalling || self.closing {
            return Task::none();
        }
        if matches!(self.result, Some(Ok(()))) {
            self.closing = true;
            self.close_error = None;
            let install_dir = self.install_dir.clone();
            return Task::perform(
                async move { operations::schedule_install_dir_removal(&install_dir) },
                Message::RemovalScheduled,
            );
        }
        self.close_window()
    }

    fn close_window(&self) -> Task<Message> {
        self.window_id.map_or_else(iced::exit, iced::window::close)
    }
}