gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! A controller's modes and the devices it is the default for: the
//! sections above its bindings, the mode of each binding, and the filter
//! that shows the bindings of one mode.

use std::collections::BTreeSet;

use egui::{ComboBox, DragValue, Ui};
use egui_phosphor_icons::icons;
use gled_model::asset::midi_controller::{
    MidiControllerMapping, MidiMode, MidiModeHighlight, normalize_controller_key,
};
use strum::IntoEnumIterator;

use super::iconized;

/// Which bindings the window lists.
#[derive(Debug, Default)]
pub(crate) struct BindingFilter {
    pub(super) text: String,
    /// Only the bindings that work in this mode; all while `None`.
    pub(super) mode: Option<u8>,
}

impl BindingFilter {
    pub(super) fn shows_mode(&self, binding_mode: Option<u8>) -> bool {
        match (self.mode, binding_mode) {
            (Some(shown), Some(mode)) => shown == mode,
            _ => true,
        }
    }
}

/// The mode list: names, what each lights up when chosen, and the filter.
pub(super) fn modes_section(
    ui: &mut Ui,
    mapping: &mut MidiControllerMapping,
    filter: &mut BindingFilter,
    dirty: &mut bool,
    id: &str,
) {
    ui.horizontal(|ui| {
        ui.label(iconized(ui, icons::STACK, " Modes"));
        if ui
            .button(iconized(ui, icons::PLUS, " Add Mode"))
            .on_hover_text("Bindings can then work in one mode only; a Select Mode input switches")
            .clicked()
        {
            mapping.modes.push(MidiMode {
                name: format!("Mode {}", mapping.modes.len() + 1),
                highlight: None,
            });
            *dirty = true;
        }
    });
    if mapping.modes.is_empty() {
        filter.mode = None;
        return;
    }

    let mut remove = None;
    egui::Grid::new(format!("{id}_modes")).show(ui, |ui| {
        for index in 0..mapping.modes.len() {
            let Ok(mode) = u8::try_from(index) else {
                break;
            };
            let uses = mapping.mode_uses(mode);
            let Some(entry) = mapping.modes.get_mut(index) else {
                break;
            };
            ui.label(if index == 0 {
                format!("{} (start)", index + 1)
            } else {
                format!("{}", index + 1)
            });
            if ui.text_edit_singleline(&mut entry.name).changed() {
                *dirty = true;
            }
            ui.label("Highlights");
            let label = entry
                .highlight
                .map_or_else(|| "Nothing".to_owned(), |highlight| highlight.to_string());
            ComboBox::new(format!("{id}_mode_highlight_{index}"), "")
                .selected_text(label)
                .show_ui(ui, |ui| {
                    if ui
                        .selectable_value(&mut entry.highlight, None, "Nothing")
                        .changed()
                    {
                        *dirty = true;
                    }
                    for highlight in MidiModeHighlight::iter() {
                        if ui
                            .selectable_value(
                                &mut entry.highlight,
                                Some(highlight),
                                highlight.to_string(),
                            )
                            .changed()
                        {
                            *dirty = true;
                        }
                    }
                });
            let response = ui.add_enabled(uses == 0, egui::Button::new(icons::TRASH));
            if uses > 0 {
                response.on_disabled_hover_text(format!("{uses} bindings use this mode"));
            } else if response.clicked() {
                remove = Some(mode);
            }
            ui.end_row();
        }
    });
    if let Some(mode) = remove
        && mapping.remove_unused_mode(mode)
    {
        filter.mode = None;
        *dirty = true;
    }

    ui.horizontal(|ui| {
        ui.label("Show bindings of");
        ComboBox::new(format!("{id}_mode_filter"), "")
            .selected_text(mode_label(filter.mode, &mapping.modes))
            .show_ui(ui, |ui| {
                ui.selectable_value(&mut filter.mode, None, "All Modes");
                for (index, mode) in mapping.modes.iter().enumerate() {
                    if let Ok(index) = u8::try_from(index) {
                        ui.selectable_value(&mut filter.mode, Some(index), &mode.name);
                    }
                }
            });
    });
}

/// The mode a binding works in, shown only while the mapping has modes.
pub(super) fn binding_mode_editor(
    ui: &mut Ui,
    binding_mode: &mut Option<u8>,
    modes: &[MidiMode],
    dirty: &mut bool,
    id: String,
) {
    if modes.is_empty() {
        return;
    }
    ui.horizontal(|ui| {
        ui.label("Works in");
        ComboBox::new(id, "")
            .selected_text(mode_label(*binding_mode, modes))
            .show_ui(ui, |ui| {
                if ui
                    .selectable_value(binding_mode, None, "All Modes")
                    .changed()
                {
                    *dirty = true;
                }
                for (index, mode) in modes.iter().enumerate() {
                    if let Ok(index) = u8::try_from(index)
                        && ui
                            .selectable_value(binding_mode, Some(index), &mode.name)
                            .changed()
                    {
                        *dirty = true;
                    }
                }
            });
    });
}

/// The mode a Select Mode input switches to or a Mode Active output shows.
pub(super) fn mode_picker(
    ui: &mut Ui,
    mode: &mut u8,
    modes: &[MidiMode],
    dirty: &mut bool,
    id: String,
) {
    ui.horizontal(|ui| {
        ui.label("Mode");
        if modes.is_empty() {
            if ui.add(DragValue::new(mode).range(0..=15)).changed() {
                *dirty = true;
            }
            ui.small("Add modes above to pick them by name");
            return;
        }
        ComboBox::new(id, "")
            .selected_text(mode_label(Some(*mode), modes))
            .show_ui(ui, |ui| {
                for (index, entry) in modes.iter().enumerate() {
                    if let Ok(index) = u8::try_from(index)
                        && ui.selectable_value(mode, index, &entry.name).changed()
                    {
                        *dirty = true;
                    }
                }
            });
    });
}

/// The devices that get this controller while a project assigns them none.
pub(super) fn default_ports_section(
    ui: &mut Ui,
    ports: &mut Vec<String>,
    known_ports: &[String],
    dirty: &mut bool,
    id: &str,
) {
    ui.horizontal_wrapped(|ui| {
        ui.label(iconized(ui, icons::PLUGS_CONNECTED, " Default for"))
            .on_hover_text(
                "These devices get this controller as soon as they are connected, \
                 unless the project assigns them another one or None",
            );
        let mut remove = None;
        for (index, port) in ports.iter().enumerate() {
            ui.label(port);
            if ui.small_button(icons::X).clicked() {
                remove = Some(index);
            }
        }
        if let Some(index) = remove {
            ports.remove(index);
            *dirty = true;
        }
        let addable: BTreeSet<String> = known_ports
            .iter()
            .map(|port| normalize_controller_key(port))
            .filter(|port| !ports.contains(port))
            .collect();
        if ports.is_empty() && addable.is_empty() {
            ui.small("no device yet; connect one to add it");
        }
        if !addable.is_empty() {
            ComboBox::new(format!("{id}_default_port"), "")
                .selected_text("Add Device…")
                .show_ui(ui, |ui| {
                    for port in addable {
                        if ui.selectable_label(false, &port).clicked() {
                            ports.push(port);
                            *dirty = true;
                        }
                    }
                });
        }
    });
}

fn mode_label(mode: Option<u8>, modes: &[MidiMode]) -> String {
    match mode {
        None => "All Modes".to_owned(),
        Some(mode) => modes
            .get(usize::from(mode))
            .map_or_else(|| format!("Mode {}", mode + 1), |mode| mode.name.clone()),
    }
}