gled-ui 2.31.0

gled's egui interface, shared by the desktop app and the browser
Documentation
use crate::midi_learn::MidiPortDiagnostics;
use egui::{ComboBox, Ui};
use gled_model::asset::midi_controller::normalize_controller_key;
use gled_model::asset::project::Project;
use gled_model::asset::{Asset, midi_controller::MidiController};
use gled_model::collection::Collections;
use std::collections::BTreeSet;

#[derive(Clone)]
pub(super) struct MidiControllerSelectionRow {
    port_name: String,
    input_connected: bool,
    output_connected: bool,
}

#[derive(Clone)]
pub(super) struct MidiMappingAssetOptions {
    options: Vec<(
        Option<gled_model::asset_id::AssetId<MidiController>>,
        String,
    )>,
}

pub(super) fn midi_mapping_settings(
    ui: &mut Ui,
    project: &mut Project,
    controllers: &[MidiControllerSelectionRow],
    options: &MidiMappingAssetOptions,
) {
    ui.heading("Active MIDI Mapping by Controller");
    ui.add_space(3.0);

    if controllers.is_empty() {
        ui.label("No MIDI controllers detected yet. Open MIDI Controllers window to inspect incoming ports.");
        return;
    }

    if options.options.len() <= 1 {
        ui.small("No MIDI mapping assets available. You can still remove existing assignments.");
    }

    egui::ScrollArea::vertical()
        .id_salt("external_devices_midi_mapping_list")
        .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
        .show(ui, |ui| {
            for controller in controllers {
                ui.separator();
                ui.label(&controller.port_name);
                ui.small(format!(
                    "Input: {} | Output: {}",
                    if controller.input_connected {
                        "connected"
                    } else {
                        "missing"
                    },
                    if controller.output_connected {
                        "connected"
                    } else {
                        "missing"
                    }
                ));

                let current = project.midi_active_mappings.get(&controller.port_name);
                let current = current.or_else(|| {
                    let normalized = normalize_controller_key(&controller.port_name);
                    let mut matches = project
                        .midi_active_mappings
                        .iter()
                        .filter(|(key, _)| normalize_controller_key(key) == normalized)
                        .map(|(_, mapping)| mapping);
                    let first = matches.next()?;
                    if matches.next().is_some() {
                        return None;
                    }
                    Some(first)
                });
                let mut selected_index = options
                    .options
                    .iter()
                    .position(|(controller_id, _)| current.is_some_and(|c| c == controller_id))
                    .unwrap_or(0);

                let original_index = selected_index;

                ComboBox::new(format!("midi_mapping_{}", controller.port_name), "")
                    .selected_text(options.options[selected_index].1.clone())
                    .show_ui(ui, |ui| {
                        for (index, (_, label)) in options.options.iter().enumerate() {
                            ui.selectable_value(&mut selected_index, index, label);
                        }
                    });

                let remove_clicked = selected_index != 0 && ui.button("Remove Mapping").clicked();
                if remove_clicked {
                    selected_index = 0;
                }

                // Only update mapping if user explicitly changed the selection or removed mapping
                let combobox_changed = selected_index != original_index;

                if combobox_changed || remove_clicked {
                    let (controller_id, _) = &options.options[selected_index];

                    // Always use exact port name as the key for specificity
                    // This ensures each port can have its own mapping
                    let exact_key = controller.port_name.clone();

                    // Now set/update the exact mapping
                    match controller_id {
                        Some(controller_id) => {
                            project
                                .midi_active_mappings
                                .insert(exact_key, Some(*controller_id));
                        }
                        None => {
                            // Removing mapping: ensure exact key is removed
                            project.midi_active_mappings.remove(&exact_key);
                        }
                    }
                }
            }
        });
}

pub(super) fn collect_midi_mapping_options(collections: &Collections) -> MidiMappingAssetOptions {
    let mut options = vec![(None, "None".to_owned())];
    let mut controllers: Vec<_> = Asset::<MidiController>::all(collections);
    controllers.sort_by(|a, b| a.name().cmp(b.name()));

    for controller in controllers {
        options.push((Some(controller.id), controller.name().to_owned()));
    }

    MidiMappingAssetOptions { options }
}

pub(super) fn collect_midi_controller_rows(
    project: &Project,
    diagnostics: &[MidiPortDiagnostics],
) -> Vec<MidiControllerSelectionRow> {
    let configured_ports = project
        .midi_active_mappings
        .keys()
        .map(|key| normalize_controller_key(key));

    let mut all_ports: BTreeSet<String> = BTreeSet::new();
    for diag in diagnostics {
        all_ports.insert(normalize_controller_key(&diag.port_name));
    }
    for output_port in crate::ui::frame_requests::devices().midi_outputs {
        all_ports.insert(normalize_controller_key(&output_port));
    }
    for configured in configured_ports {
        all_ports.insert(configured);
    }

    all_ports
        .into_iter()
        .filter(|port_name| !is_gled_midi_port(port_name))
        .map(|port_name| {
            let input_connected = diagnostics.iter().any(|diag| {
                normalize_controller_key(&diag.port_name) == port_name && diag.input_connected
            });
            let output_connected = diagnostics.iter().any(|diag| {
                normalize_controller_key(&diag.port_name) == port_name && diag.output_connected
            });
            MidiControllerSelectionRow {
                port_name,
                input_connected,
                output_connected,
            }
        })
        .collect()
}

fn is_gled_midi_port(port_name: &str) -> bool {
    port_name.contains("gled_read_input") || port_name.contains("gled_write_output")
}