gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The bar above the rules: learn, undo and redo, and where the
//! controller's lights and devices stand; and the editor's keys.

use egui::{Button, Color32, Key, KeyboardShortcut, Modifiers, RichText, Ui};
use egui_phosphor_icons::icons;
use gled_model::asset::midi_controller::MidiController;

use super::MidiControllerTestState;
use super::list_state::{LearnRequest, ListState};
use super::side_tabs::SideTab;
use crate::canvas::edit::Edit;
use crate::midi_learn::LearnState;

const UNDO: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Z);
const REDO: KeyboardShortcut =
    KeyboardShortcut::new(Modifiers::COMMAND.plus(Modifiers::SHIFT), Key::Z);
const REDO_Y: KeyboardShortcut = KeyboardShortcut::new(Modifiers::COMMAND, Key::Y);

const LISTENING: Color32 = Color32::from_rgb(200, 60, 60);

pub(super) struct Bar<'a, 'e> {
    pub(super) list: &'a mut ListState,
    pub(super) learning: &'a mut bool,
    pub(super) tab: &'a mut SideTab,
    pub(super) edit: &'a mut Edit<'e, MidiController>,
    pub(super) learn_state: &'a mut LearnState,
    pub(super) test_state: &'a MidiControllerTestState,
}

pub(super) fn top_bar(ui: &mut Ui, bar: Bar) {
    ui.horizontal_wrapped(|ui| {
        let listening = *bar.learning || bar.learn_state.active_request().is_some();
        let learn = if listening {
            ui.add(
                Button::new(
                    RichText::new("Listening — move a control  (Esc stops)").color(Color32::WHITE),
                )
                .fill(LISTENING),
            )
        } else {
            ui.button(super::iconized(ui, icons::RECORD, " Learn  (L)"))
                .on_hover_text("Then move or press controls on your device; each one gets a rule")
        };
        if learn.clicked() {
            if listening {
                stop_learning(bar.learning, bar.learn_state);
            } else {
                bar.list.learn_request = Some(LearnRequest::NewControls);
            }
        }

        ui.separator();
        if ui
            .add_enabled(
                bar.edit.can_undo(),
                Button::new(icons::ARROW_COUNTER_CLOCKWISE),
            )
            .on_hover_text(format!("Undo ({})", ui.ctx().format_shortcut(&UNDO)))
            .clicked()
        {
            bar.edit.undo();
        }
        if ui
            .add_enabled(bar.edit.can_redo(), Button::new(icons::ARROW_CLOCKWISE))
            .on_hover_text(format!("Redo ({})", ui.ctx().format_shortcut(&REDO)))
            .clicked()
        {
            bar.edit.redo();
        }

        ui.separator();
        let ports = &bar.edit.value.default_for_ports;
        let used = if ports.is_empty() {
            RichText::new("Not used automatically for any device — choose…")
                .color(ui.visuals().warn_fg_color)
        } else {
            let names: Vec<String> = ports
                .iter()
                .map(|port| super::test_panel::short_port_name(port))
                .collect();
            RichText::new(format!("Used automatically for {}", names.join(", ")))
        };
        if ui
            .link(used)
            .on_hover_text("Choose devices in the Devices tab")
            .clicked()
        {
            *bar.tab = SideTab::Devices;
        }
        ui.separator();
        let lights = bar
            .test_state
            .selected_output_port
            .as_deref()
            .map_or_else(|| "nothing".to_owned(), super::test_panel::short_port_name);
        if ui
            .link(format!("Test lights on: {lights}"))
            .on_hover_text("Point at a light in the rules to light it on this device")
            .clicked()
        {
            *bar.tab = SideTab::Devices;
        }
    });
}

fn stop_learning(learning: &mut bool, learn_state: &mut LearnState) {
    *learning = false;
    learn_state.disarm();
}

/// L learns, / finds, Escape stops learning, and undo and redo.
pub(super) fn keys(
    ui: &mut Ui,
    list: &mut ListState,
    learning: &mut bool,
    edit: &mut Edit<MidiController>,
    learn_state: &mut LearnState,
) {
    let typing = ui.ctx().text_edit_focused();
    let (undo, redo) = ui.input_mut(|input| {
        (
            input.consume_shortcut(&UNDO),
            input.consume_shortcut(&REDO) || input.consume_shortcut(&REDO_Y),
        )
    });
    if !typing && undo {
        edit.undo();
    }
    if !typing && redo {
        edit.redo();
    }
    if typing {
        return;
    }
    let (learn, find, escape) = ui.input(|input| {
        (
            input.key_pressed(Key::L) && input.modifiers.is_none(),
            input.key_pressed(Key::Slash),
            input.key_pressed(Key::Escape),
        )
    });
    if learn {
        if *learning || learn_state.active_request().is_some() {
            stop_learning(learning, learn_state);
        } else {
            list.learn_request = Some(LearnRequest::NewControls);
        }
    }
    if find {
        list.focus_find = true;
    }
    if escape && !egui::Popup::is_any_open(ui.ctx()) {
        stop_learning(learning, learn_state);
    }
}