pixelmosh 4.2.2

PNG corrupter
Documentation
use adw::{prelude::*, subclass::prelude::*};
use glib::{Object, clone};
use gtk::{EntryIconPosition::Secondary, License, gio, glib};

use libmosh::err::MoshError;

use std::sync::Arc;

use crate::gui::base::Base;

mod imp;

glib::wrapper! {
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::ApplicationWindow, adw::ApplicationWindow, gtk::Window, gtk::Widget,
        @implements gio::ActionGroup, gio::ActionMap, gtk::Accessible, gtk::Buildable,
                    gtk::ConstraintTarget, gtk::Native, gtk::Root, gtk::ShortcutManager;
}

#[derive(PartialEq)]
enum Mode {
    Normal,
    Rewind,
    Seed,
}

impl Window {
    pub fn new(app: &adw::Application) -> Self {
        Object::builder::<Window>()
            .property("application", app)
            .build()
    }

    #[cfg(debug_assertions)]
    fn setup_debug(&self) {
        self.add_css_class("devel");
    }

    fn setup_actions(&self) {
        let action_about = gio::SimpleAction::new("about", None);
        action_about.connect_activate(clone!(
            #[weak(rename_to = window)]
            self,
            move |_, _| {
                let dialog = Window::about_dialog();
                dialog.present(Some(&window));
            }
        ));

        let action_minimize = gio::SimpleAction::new("minimize", None);
        action_minimize.connect_activate(clone!(
            #[weak(rename_to = window)]
            self,
            move |_, _| {
                window.minimize();
            }
        ));

        let action_maximize = gio::SimpleAction::new("maximize", None);
        action_maximize.connect_activate(clone!(
            #[weak(rename_to = window)]
            self,
            move |_, _| {
                if window.is_maximized() {
                    window.unmaximize();
                } else {
                    window.maximize();
                }
            }
        ));

        let action_close = gio::SimpleAction::new("close", None);
        action_close.connect_activate(clone!(
            #[weak(rename_to = window)]
            self,
            move |_, _| {
                window.close();
            }
        ));

        let action_style_manager = gio::SimpleAction::new("toggle-color-scheme", None);
        action_style_manager.connect_activate(clone!(
            #[weak(rename_to = window)]
            self,
            move |_, _| {
                window.toggle_color_scheme();
            }
        ));

        self.add_action(&action_about);
        self.add_action(&action_minimize);
        self.add_action(&action_maximize);
        self.add_action(&action_close);
        self.add_action(&action_style_manager);
    }

    fn setup_callbacks(&self) {
        self.imp().btn_ansi.connect_toggled(clone!(
            #[weak(rename_to = window)]
            self,
            move |button| {
                window.set_ansi(button.is_active());
            }
        ));

        self.imp().seed.connect_activate(clone!(
            #[weak(rename_to = window)]
            self,
            move |_| {
                window.mosh(Mode::Seed);
            }
        ));

        self.imp().seed.connect_icon_release(clone!(
            #[weak(rename_to = window)]
            self,
            move |_, _| {
                window.mosh(Mode::Seed);
            }
        ));

        self.imp().seed.connect_changed(clone!(
            #[weak(rename_to = window)]
            self,
            move |_| {
                let seed = &window.imp().seed;
                if seed.buffer().text().to_string().is_empty() {
                    seed.set_icon_sensitive(Secondary, false);
                } else {
                    seed.set_icon_sensitive(Secondary, true);
                }
            }
        ));

        self.set_stack();
    }

    fn skip_placeholder(&self) {
        self.imp().stack.set_visible_child_name("main");
    }

    fn set_stack(&self) {
        self.imp().stack.set_visible_child_name("placeholder");
    }

    fn set_instructions(&self) {
        self.imp().stack.set_visible_child_name("instructions");
    }

    fn set_color_type(&self, label: &str) {
        self.imp().color_type.set_label(label);
    }

    fn set_ansi(&self, value: bool) {
        self.imp().base.lock().unwrap().set_ansi(value);
    }

    fn parse_seed(&self) {
        let buffer = &self.imp().seed.buffer();
        let seed = buffer.text().to_string();
        let filtered_seed: String = seed.chars().filter(|c| c.is_numeric()).collect();

        if filtered_seed.parse::<u64>().is_err()
            || filtered_seed.is_empty()
            || filtered_seed.len() > u64::MAX.try_into().unwrap()
        {
            self.imp().base.lock().unwrap().new_seed();
            self.update_seed();
        } else {
            self.imp()
                .base
                .lock()
                .unwrap()
                .set_seed(filtered_seed.parse::<u64>().unwrap());
        }
    }

    fn update_seed(&self) {
        let seed = self.imp().base.lock().unwrap().get_seed();
        self.imp().seed.buffer().set_text(seed.to_string());
    }

    fn update_picture(&self) {
        let mut base = self.imp().base.lock().unwrap();
        let texture = base.get_texture();
        self.imp().picture.set_paintable(Some(&texture));
    }

    fn busy(&self, is_busy: bool) {
        let window = self.imp();
        let seed = &window.seed;

        if is_busy {
            window.spinner.set_visible(true);
            window.btn_rewind.set_sensitive(false);
            window.btn_mosh.set_sensitive(false);
            window.btn_open.set_sensitive(false);
            window.btn_save.set_sensitive(false);
            window.btn_menu.set_sensitive(false);
        } else {
            window.spinner.set_visible(false);
            window.btn_mosh.set_sensitive(true);
            window.btn_open.set_sensitive(true);
            window.btn_save.set_sensitive(true);
            window.btn_menu.set_sensitive(true);

            if window.base.lock().unwrap().settings.is_some() {
                window.btn_rewind.set_sensitive(true);
            }

            if seed.buffer().text().to_string().is_empty() {
                seed.set_icon_sensitive(Secondary, false);
            } else {
                seed.set_icon_sensitive(Secondary, true);
            }
        }
    }

    fn mosh(&self, mode: Mode) {
        self.busy(true);

        let (sender, receiver) = async_channel::bounded(1);
        let (sender_e, receiver_e) = async_channel::bounded(1);

        if mode == Mode::Seed {
            self.parse_seed();
        }

        let base = Arc::clone(&self.imp().base);
        gio::spawn_blocking(move || {
            let mut thread_base = base.lock().unwrap();

            match mode {
                Mode::Normal => {
                    thread_base.save_settings();
                    thread_base.new_seed();
                    match thread_base.mosh_file() {
                        Ok(()) => sender.send_blocking(true).unwrap(),
                        Err(error) => sender_e.send_blocking(error).unwrap(),
                    }
                }
                Mode::Rewind => {
                    thread_base.load_settings();
                    match thread_base.mosh_file() {
                        Ok(()) => sender.send_blocking(true).unwrap(),
                        Err(error) => sender_e.send_blocking(error).unwrap(),
                    }
                }
                Mode::Seed => match thread_base.mosh_file() {
                    Ok(()) => sender.send_blocking(true).unwrap(),
                    Err(error) => sender_e.send_blocking(error).unwrap(),
                },
            }
        });

        glib::spawn_future_local(clone!(
            #[weak(rename_to = window_clone)]
            self,
            async move {
                while let Ok(show_image) = receiver.recv().await {
                    if show_image {
                        window_clone.busy(false);
                        window_clone.update_seed();
                        window_clone.update_picture();
                    }
                }
            }
        ));

        glib::spawn_future_local(clone!(
            #[weak(rename_to = window_clone)]
            self,
            async move {
                while let Ok(error) = receiver_e.recv().await {
                    window_clone.busy(false);
                    window_clone.show_message(&format!("Failed: {error}"), 10);
                }
            }
        ));
    }

    fn load_file(&self, file: &gio::File) {
        self.skip_placeholder();
        self.imp().btn_ansi.set_active(false);
        self.busy(true);
        let (sender, receiver) = async_channel::bounded(1);
        let base_arc = Arc::clone(&self.imp().base);
        let base_clone = base_arc.clone();
        let file_copy = file.clone();

        gio::spawn_blocking(move || {
            let mut base = base_clone.lock().unwrap();
            base.save_settings();

            if base.open_file(&file_copy.path().unwrap()).is_ok() {
                sender.send_blocking(true).unwrap();
            } else {
                sender.send_blocking(false).unwrap();
            }
        });

        glib::spawn_future_local(clone!(
            #[weak(rename_to = window_clone)]
            self,
            async move {
                while let Ok(image_loaded) = receiver.recv().await {
                    let base_clone_w = base_arc.clone();
                    if image_loaded {
                        let main_base = base_clone_w.lock().unwrap();
                        let data = &main_base.core.data;
                        let settings = &main_base.settings.clone().unwrap();

                        window_clone
                            .imp()
                            .picture
                            .set_paintable(Some(&Base::generate_texture(data, settings)));
                    } else {
                        window_clone.set_instructions();
                    }

                    window_clone.busy(false);
                }
            }
        ));
    }

    fn save_file(&self, file: &gio::File) -> Result<(), MoshError> {
        self.imp()
            .base
            .lock()
            .unwrap()
            .save_file(&file.path().unwrap())?;

        Ok(())
    }

    fn toggle_color_scheme(&self) {
        if self.imp().style_manager.is_dark() {
            self.imp()
                .style_manager
                .set_color_scheme(adw::ColorScheme::ForceLight);
        } else {
            self.imp()
                .style_manager
                .set_color_scheme(adw::ColorScheme::ForceDark);
        }
    }

    pub fn add_toast(&self, toast: adw::Toast) {
        self.imp().toast_overlay.add_toast(toast);
    }

    pub fn show_message(&self, message: &str, timeout: u32) {
        let toast = adw::Toast::new(message);
        toast.set_timeout(timeout);
        self.add_toast(toast);
    }

    fn about_dialog() -> adw::AboutDialog {
        let about_dialog = adw::AboutDialog::builder()
            .application_name("PIXELMOSH")
            .version(crate::version())
            .license_type(License::MitX11)
            .website(env!("CARGO_PKG_REPOSITORY"))
            .comments(env!("CARGO_PKG_DESCRIPTION"))
            .build();

        about_dialog.add_link(
            "Release Notes",
            "https://github.com/charlesrocket/pixelmosh/blob/trunk/CHANGELOG.md",
        );

        about_dialog
    }
}