gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
use gled_protocol::state::{ClientInput, ServerSettings, ServerStatus, StorageCommand};
use std::path::PathBuf;

use crate::ui::window_common::{gled_window_frame, raise, tool_window_builder};
use egui::{
    Button, Color32, ComboBox, Context, Id, Layout, RichText, TextEdit, UiKind, Vec2, ViewportId,
};
use egui_flex::{Flex, item};
#[cfg(not(target_arch = "wasm32"))]
use home::home_dir;
use kanal::{Receiver, Sender, unbounded};

/// The asset repository's settings, which belong to the machine running the
/// engine: every change goes there.
#[derive(Debug)]
pub(crate) struct GitConfigWindow {
    open: bool,
    /// Set while the window still has to be brought forward.
    raise: bool,
    /// The URL being edited, starting from the engine's when the window opens.
    url: Option<String>,
    passphrase: String,
    /// The key path being typed (browser only).
    #[cfg(target_arch = "wasm32")]
    key: Option<String>,
    /// Key files picked in the file dialog, which runs on its own thread.
    picked_keys: (Sender<PathBuf>, Receiver<PathBuf>),
}

impl Default for GitConfigWindow {
    fn default() -> Self {
        Self {
            open: false,
            raise: false,
            url: None,
            passphrase: String::new(),
            #[cfg(target_arch = "wasm32")]
            key: None,
            picked_keys: unbounded(),
        }
    }
}

impl GitConfigWindow {
    #[cfg_attr(feature = "profiling", profiling::function)]
    pub(crate) fn update(&mut self, ctx: &Context, status: &ServerStatus) {
        if !self.open {
            return;
        }
        let settings = &status.settings;
        while let Ok(Some(key)) = self.picked_keys.1.try_recv() {
            send_settings(settings, |settings| {
                settings.git_private_key = Some(key.display().to_string());
            });
        }

        ctx.show_viewport_immediate(
            ViewportId(Id::new("git config window")),
            tool_window_builder("Git configuration")
                .with_inner_size(Vec2::new(500.0, 220.0))
                .with_min_inner_size(Vec2::new(500.0, 220.0))
                .with_resizable(false)
                .with_max_inner_size(Vec2::new(500.0, 220.0)),
            |ctx, _viewport_class| {
                raise(ctx, &mut self.raise);
                ctx.input(|input| {
                    if input.viewport().close_requested()
                        || crate::ui::window_common::embedded_close_clicked("Git configuration")
                    {
                        self.close();
                    }
                });

                gled_window_frame(ctx, "Git configuration", |ui| {
                    ui.with_layout(Layout::top_down_justified(egui::Align::Center), |ui| {
                        ui.label(
                            RichText::new("Any change here can nuke all you progress!")
                                .color(Color32::RED)
                                .heading(),
                        );
                    });
                    ui.add_space(10.0);
                    ui.horizontal(|ui| {
                        ui.label("Repository:");
                        ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
                            ui.label("Branch:");
                        });
                    });
                    let mut url = self.url.clone().unwrap_or_else(|| settings.git_url.clone());
                    let mut reset = false;
                    ui.horizontal(|ui| {
                        ui.text_edit_singleline(&mut url);
                        if let Some(mut current) = status.current_branch.clone() {
                            ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
                                ComboBox::new("branch", "").selected_text(&current).show_ui(
                                    ui,
                                    |ui| {
                                        for branch in &status.branches {
                                            if ui
                                                .selectable_value(
                                                    &mut current,
                                                    branch.clone(),
                                                    branch.clone(),
                                                )
                                                .changed()
                                            {
                                                ui.close_kind(UiKind::Menu);
                                                ClientInput::Storage(StorageCommand::SwitchBranch(
                                                    branch.clone(),
                                                ))
                                                .enqueue();
                                            };
                                        }
                                    },
                                );
                            });
                        }
                    });

                    Flex::horizontal().show(ui, |flex| {
                        if flex
                            .add(
                                item().grow(1.0),
                                Button::new("Apply").fill(Color32::DARK_RED),
                            )
                            .on_hover_text(
                                "This deletes all assets on disk and starts from scratch by \
                                 cloning the repository!",
                            )
                            .clicked()
                        {
                            let url = url.clone();
                            send_settings(settings, |settings| settings.git_url = url);
                            ClientInput::Storage(StorageCommand::Reclone).enqueue();
                            reset = true;
                        }

                        if flex.add(item().grow(1.0), Button::new("Reset")).clicked() {
                            reset = true;
                        }
                    });
                    self.url = (!reset).then_some(url);

                    ui.add_space(30.0);

                    // A key file on this machine: only on the desktop, which
                    // runs next to the engine.
                    #[cfg(not(target_arch = "wasm32"))]
                    ui.horizontal(|ui| {
                        ui.vertical_centered_justified(|ui| {
                            let choose_private_key_text = match &settings.git_private_key {
                                Some(path) => format!("Choose private key (current: {path})"),
                                None => "Choose private key".to_owned(),
                            };

                            if ui.button(choose_private_key_text).clicked() {
                                let picked = self.picked_keys.0.clone();
                                std::thread::Builder::new()
                                    .name("gled:git:choose_key".to_string())
                                    .spawn(move || {
                                        #[cfg(feature = "profiling")]
                                        profiling::register_thread!("git_choose_private_key");

                                        let mut file_dialog =
                                            rfd::FileDialog::new().set_title("Choose private key");
                                        if let Some(home) = home_dir() {
                                            file_dialog =
                                                file_dialog.set_directory(home.join(".ssh"))
                                        }
                                        if let Some(path) = file_dialog.pick_file() {
                                            let _ = picked.send(path);
                                        }
                                    })
                                    .ok();
                            }
                        });
                    });

                    // The key file lives on the machine running the engine,
                    // which the browser cannot browse: its path is typed in.
                    #[cfg(target_arch = "wasm32")]
                    ui.horizontal(|ui| {
                        ui.label("Private key on the server:");
                        let mut key = self.key.clone().unwrap_or_else(|| {
                            settings.git_private_key.clone().unwrap_or_default()
                        });
                        let response = ui.add(
                            TextEdit::singleline(&mut key)
                                .hint_text("/var/lib/gled/ssh/id_ed25519"),
                        );
                        if response.lost_focus() {
                            send_settings(settings, |settings| {
                                settings.git_private_key =
                                    (!key.trim().is_empty()).then(|| key.trim().to_owned());
                            });
                            self.key = None;
                        } else {
                            self.key = response.has_focus().then_some(key);
                        }
                    });

                    ui.horizontal(|ui| {
                        let mut use_passphrase = settings.git_use_passphrase;
                        if ui.checkbox(&mut use_passphrase, "Use passphrase").changed() {
                            send_settings(settings, |settings| {
                                settings.git_use_passphrase = use_passphrase;
                            });
                        }

                        if use_passphrase {
                            ui.label("Passphrase:");
                            if ui
                                .add(
                                    TextEdit::singleline(&mut self.passphrase)
                                        .hint_text("Please enter key password")
                                        .password(true),
                                )
                                .lost_focus()
                                && ui.ctx().input(|input| input.key_pressed(egui::Key::Enter))
                            {
                                ClientInput::Storage(StorageCommand::SetGitPassphrase(
                                    std::mem::take(&mut self.passphrase),
                                ))
                                .enqueue();
                            }
                        }
                    });
                });
            },
        );
    }

    pub(crate) fn open(&mut self) {
        self.open = true;
        self.raise = true;
        self.url = None;
    }

    pub(crate) fn close(&mut self) {
        self.open = false;
    }
}

/// Sends the server settings with `change` applied.
fn send_settings(settings: &ServerSettings, change: impl FnOnce(&mut ServerSettings)) {
    let mut settings = settings.clone();
    change(&mut settings);
    ClientInput::SetServerSettings(settings).enqueue();
}