gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
use super::App;
use crate::app::{
    client_timing::LINK_ACTIVE_COLOR,
    controller_feedback::{CONTROLLER_COLOR, controller_modes},
};
use crate::ui::frame_requests;
use egui::{
    Button, DragValue, FontSelection, Label, Layout, Margin, RichText, Slider, Spinner, TextEdit,
    Ui, UiKind, ViewportId,
    containers::menu::{MenuButton, MenuConfig},
};
use egui_flex::{Flex, item};
use egui_phosphor_icons::icons;
use emath::Align;
use epaint::text::{LayoutJob, TextFormat};
use gled_model::asset::project::GridHighlight;
use gled_protocol::state::{ClientInput, StorageCommand};
use strum::IntoEnumIterator;

impl App {
    /// The engine machine's temperature and, when the engine runs on another
    /// machine, which one, followed by this machine's own if it is known.
    fn temperatures(&self, ui: &mut Ui) {
        use crate::ui::temperature::{local, temperature};

        let status = &self.model.replica.state.status;
        if let Some(celsius) = status.temperature {
            let (name, hover) = if self.connection.is_remote() {
                let host = match status.host_name.as_str() {
                    "" => "engine",
                    host => host,
                };
                (
                    Some(host),
                    format!("Hottest temperature sensor of {host}, the machine the engine runs on"),
                )
            } else {
                (
                    None,
                    "Hottest temperature sensor of this computer, which runs the engine".to_owned(),
                )
            };
            ui.add(Label::new(temperature(ui, name, celsius)).selectable(false))
                .on_hover_text(hover);
        }
        if let Some(celsius) = local() {
            ui.add_space(8.0);
            ui.add(Label::new(temperature(ui, Some("this computer"), celsius)).selectable(false))
                .on_hover_text(
                    "Hottest temperature sensor of this computer, which shows the interface",
                );
        }
    }

    /// The mode of each connected controller that has modes.
    fn controller_modes(&self, ui: &mut Ui) {
        let local_ports = frame_requests::local_midi().ports;
        let replica = &self.model.replica;
        for controller in controller_modes(&replica.state, &replica.assets, &local_ports) {
            ui.add_space(8.0);
            let mut layout_job = LayoutJob::default();
            icons::SLIDERS_HORIZONTAL.regular().append_to(
                &mut layout_job,
                ui.style(),
                FontSelection::Default,
                Align::Center,
            );
            layout_job.append(
                &format!(" {} · {}", controller.device, controller.mode),
                0.0,
                TextFormat {
                    color: CONTROLLER_COLOR,
                    ..Default::default()
                },
            );
            ui.add(Label::new(layout_job).selectable(false))
                .on_hover_text(format!(
                    "{}\nController: {}\nMode: {}",
                    controller.port, controller.controller, controller.mode
                ));
        }
    }

    pub(crate) fn status_bar(&mut self, ui: &mut Ui, viewport_id: Option<ViewportId>) {
        egui::Panel::bottom(format!("{viewport_id:?} status bar")).show(ui, |ui| {
            egui::Frame::NONE
                .inner_margin(Margin::from(1.0))
                .show(ui, |ui| {
                    ui.horizontal(|ui| {
                        self.temperatures(ui);
                        self.controller_modes(ui);

                        if let Some(framerate) = self.timing.framerate() {
                            ui.add_space(8.0);
                            let mut layout_job = LayoutJob::default();
                            icons::GAUGE.regular().append_to(
                                &mut layout_job,
                                ui.style(),
                                egui::FontSelection::Default,
                                egui::Align::Center,
                            );
                            layout_job.append(
                                format!(" {framerate:.0} fps",).as_str(),
                                0.0,
                                TextFormat::default(),
                            );
                            ui.add(Label::new(layout_job).selectable(false))
                                .on_hover_text("Render and Art-Net/DMX output frame rate");
                        }

                        let (incoming, outgoing) = self.network_stats;
                        ui.add_space(8.0);
                        let mut layout_job = LayoutJob::default();
                        icons::NETWORK.regular().append_to(
                            &mut layout_job,
                            ui.style(),
                            FontSelection::Default,
                            Align::Center,
                        );
                        layout_job.append("  ", 0.0, TextFormat::default());
                        icons::CARET_DOWN.regular().append_to(
                            &mut layout_job,
                            ui.style(),
                            FontSelection::Default,
                            Align::Center,
                        );
                        layout_job.append(
                            format!(" {:.2} MBit/s  ", incoming).as_str(),
                            0.0,
                            TextFormat::default(),
                        );
                        icons::CARET_UP.regular().append_to(
                            &mut layout_job,
                            ui.style(),
                            FontSelection::Default,
                            Align::Center,
                        );
                        layout_job.append(
                            format!(" {:.2} MBit/s", outgoing).as_str(),
                            0.0,
                            TextFormat::default(),
                        );
                        ui.add(Label::new(layout_job).selectable(false));

                        let connected_ableton_peers = self.model.replica.state.status.link_peers;
                        let show_link_read_only_toggle = connected_ableton_peers > 0
                            || self.server_settings().ableton_link_read_only;

                        if show_link_read_only_toggle {
                            ui.add_space(8.0);
                            if connected_ableton_peers > 0 {
                                let mut layout_job = LayoutJob::default();
                                icons::METRONOME
                                    .regular()
                                    .color(LINK_ACTIVE_COLOR)
                                    .append_to(
                                        &mut layout_job,
                                        ui.style(),
                                        egui::FontSelection::Default,
                                        egui::Align::Center,
                                    );
                                RichText::new(format!(
                                    " {connected_ableton_peers} Ableton Link peer{} ",
                                    if connected_ableton_peers == 1 {
                                        ""
                                    } else {
                                        "s"
                                    }
                                ))
                                .color(LINK_ACTIVE_COLOR)
                                .append_to(
                                    &mut layout_job,
                                    ui.style(),
                                    egui::FontSelection::Default,
                                    egui::Align::Center,
                                );

                                ui.add(Label::new(layout_job).selectable(false));
                            }

                            let mut read_only = self.server_settings().ableton_link_read_only;
                            let changed = ui
                                .scope(|ui| {
                                    let visuals = &mut ui.style_mut().visuals;
                                    visuals.widgets.inactive.bg_stroke.color = LINK_ACTIVE_COLOR;
                                    visuals.widgets.hovered.bg_stroke.color = LINK_ACTIVE_COLOR;
                                    visuals.widgets.active.bg_stroke.color = LINK_ACTIVE_COLOR;
                                    visuals.widgets.inactive.fg_stroke.color = LINK_ACTIVE_COLOR;
                                    visuals.widgets.hovered.fg_stroke.color = LINK_ACTIVE_COLOR;
                                    visuals.widgets.active.fg_stroke.color = LINK_ACTIVE_COLOR;

                                    ui.checkbox(
                                        &mut read_only,
                                        RichText::new("Read-only").color(LINK_ACTIVE_COLOR),
                                    )
                                    .on_hover_text(
                                        "Ignore local tempo changes (tap, x1/2, x2, slider, OSC, MIDI).",
                                    )
                                    .changed()
                                })
                                .inner;

                            if changed {
                                self.change_server_settings(|settings| {
                                    settings.ableton_link_read_only = read_only;
                                });
                            }
                        }

                        #[cfg(all(not(debug_assertions), not(target_arch = "wasm32")))]
                        if let Some(version) = crate::ui::update_check::Update::update_available()
                            && ui.button(format!("Update available: {version}")).clicked()
                            && let Some(url) = crate::ui::update_check::Update::download_url()
                        {
                            ui.ctx().open_url(egui::OpenUrl::new_tab(url));
                        }

                        ui.with_layout(Layout::right_to_left(egui::Align::Center), |ui| {
                            ui.add_space(4.0);
                            self.git_button(ui);
                            ui.add_space(4.0);
                            self.grid_button(ui);
                            #[cfg(not(target_arch = "wasm32"))]
                            match gled_model::asset::curve::polynomial::polynomials_fitting() {
                                0 => (),
                                n => {
                                    ui.add(
                                        Label::new(if n == 1 {
                                            "Fitting one polynomial".to_owned()
                                        } else {
                                            format!("Fitting {n} polynomials")
                                        })
                                        .selectable(false),
                                    );
                                    ui.add(Spinner::new());
                                    ui.add_space(4.0);
                                }
                            }
                        });
                    });
                });
        });
    }

    fn grid_button(&mut self, ui: &mut Ui) {
        if let Some(project) = self.project.as_mut() {
            let mut layout_job = LayoutJob::default();
            icons::SQUARES_FOUR.fill().append_to(
                &mut layout_job,
                ui.style(),
                egui::FontSelection::Default,
                egui::Align::Center,
            );
            layout_job.append(" Grid", 0.0, TextFormat::default());

            ui.menu_button(layout_job, |ui| {
                ui.label("Grid Size");

                let mut grid_width = project.grid_width();
                let mut grid_height = project.grid_height();

                ui.horizontal(|ui| {
                    ui.label("Width");
                    ui.add(DragValue::new(&mut grid_width).range(2..=64));
                    ui.label("Height");
                    ui.add(DragValue::new(&mut grid_height).range(2..=64));
                });

                if grid_width != project.grid_width() || grid_height != project.grid_height() {
                    project.set_grid_size(grid_width, grid_height);

                    let max_col = project.grid_width() - 1;
                    let max_row = project.grid_height() - 1;
                    self.selected_scene_instance.col =
                        self.selected_scene_instance.col.min(max_col);
                    self.selected_scene_instance.row =
                        self.selected_scene_instance.row.min(max_row);
                }

                ui.separator();
                ui.label("Highlight");
                ui.horizontal(|ui| {
                    for highlight in GridHighlight::iter() {
                        ui.selectable_value(
                            &mut project.grid_highlight,
                            highlight,
                            highlight.to_string(),
                        );
                    }
                });

                ui.separator();

                ui.spacing_mut().slider_width = 195.0;

                ui.label("Scene preview size");
                if ui
                    .add(
                        Slider::new(self.settings.effects_size_mut(), 50.0..=500.0)
                            .show_value(false),
                    )
                    .changed()
                {}
            });
            ui.add_space(4.0);
        }
    }

    fn git_button(&mut self, ui: &mut Ui) {
        let mut layout_job = LayoutJob::default();
        icons::GIT_BRANCH.regular().append_to(
            &mut layout_job,
            ui.style(),
            egui::FontSelection::Default,
            egui::Align::Center,
        );
        layout_job.append(
            format!(
                " git{}",
                if self.model.replica.state.status.staged_files > 0 {
                    "*"
                } else {
                    ""
                }
            )
            .as_str(),
            0.0,
            TextFormat::default(),
        );

        MenuButton::from_button(Button::new(layout_job))
            .config(MenuConfig::new().close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside))
            .ui(ui, |ui| {
                self.git_menu(ui);
            });
    }

    fn git_menu(&mut self, ui: &mut Ui) {
        ui.set_max_width(300.0);
        ui.horizontal(|ui| {
            ui.heading("Assets ");
            if self.model.replica.state.status.storage_working {
                ui.add(Spinner::new());
            }
        });

        let staged_files = self.model.replica.state.status.staged_files;
        let working = self.model.replica.state.status.storage_working;
        egui::Frame::NONE
            .inner_margin(Margin::from(6.0))
            .show(ui, |ui| {
                let mut commit = false;
                let mut close_menu = false;
                ui.add_enabled_ui(staged_files > 0 && !working, |ui| {
                    ui.label(if staged_files == 1 {
                        "Commit message for 1 change:".to_owned()
                    } else {
                        format!("Commit message for {staged_files} files:")
                    });
                    commit = ui
                        .add(
                            TextEdit::singleline(&mut self.git_commit_message)
                                .hint_text("Please enter a commit message"),
                        )
                        .lost_focus()
                        && ui.ctx().input(|input| input.key_pressed(egui::Key::Enter));
                });

                Flex::horizontal().show(ui, |flex| {
                    if flex.add(item().grow(1.0), Button::new("Commit")).clicked() {
                        commit = true;
                        close_menu = true;
                    }

                    if flex.add(item().grow(1.0), Button::new("⬆Push")).clicked() {
                        ClientInput::Storage(StorageCommand::Push).enqueue();
                        close_menu = true;
                    }

                    if flex.add(item().grow(1.0), Button::new("⬇Pull")).clicked() {
                        ClientInput::Storage(StorageCommand::Pull).enqueue();
                        close_menu = true;
                    }
                });

                if commit {
                    ClientInput::Storage(StorageCommand::Commit {
                        message: self.git_commit_message.clone(),
                    })
                    .enqueue();
                    self.git_commit_message.clear();
                    close_menu = true;
                }

                if close_menu {
                    ui.close_kind(UiKind::Menu);
                }
            });
    }
}