use crate::{
midi_learn::MidiPortDiagnostics,
ui::frame_requests::{self, LocalMidiStatus},
};
use egui::{ComboBox, Ui};
use gled_midi::{client::Availability, default_controller_for_port, is_own_port};
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::asset_id::AssetId;
use gled_model::collection::Collections;
use std::collections::BTreeSet;
#[derive(Clone)]
pub(super) struct MidiControllerSelectionRow {
port_name: String,
input_connected: bool,
output_connected: bool,
here: bool,
default: Option<AssetId<MidiController>>,
}
#[derive(Clone)]
pub(super) struct MidiMappingAssetOptions {
options: Vec<(Option<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);
local_midi_access(ui);
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.here {
"On this computer | "
} else {
""
},
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 assigned = current.is_some();
let default = controller.default;
let shown = current.copied().unwrap_or(default);
let mut selected_index = options
.options
.iter()
.position(|(controller_id, _)| *controller_id == shown)
.unwrap_or(0);
let original_index = selected_index;
let label = |index: usize| {
let label = options.options[index].1.clone();
if !assigned && default.is_some() {
format!("{label} (default)")
} else {
label
}
};
ComboBox::new(format!("midi_mapping_{}", controller.port_name), "")
.selected_text(label(selected_index))
.show_ui(ui, |ui| {
for (index, (_, label)) in options.options.iter().enumerate() {
ui.selectable_value(&mut selected_index, index, label);
}
});
let remove_clicked = assigned
&& ui
.button("Remove Mapping")
.on_hover_text(if default.is_some() {
"Back to the device's default controller"
} else {
"No controller for this device"
})
.clicked();
let exact_key = controller.port_name.clone();
if remove_clicked {
project.midi_active_mappings.remove(&exact_key);
} else if selected_index != original_index {
match options.options[selected_index].0 {
None if default.is_none() => {
project.midi_active_mappings.remove(&exact_key);
}
controller_id => {
project
.midi_active_mappings
.insert(exact_key, controller_id);
}
}
}
}
});
}
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 }
}
fn local_midi_access(ui: &mut Ui) {
match frame_requests::local_midi().availability {
Some(Availability::NeedsPermission) => {
if ui
.button("Connect this browser's MIDI devices")
.on_hover_text("The browser asks whether this page may use them")
.clicked()
{
frame_requests::request_midi_access();
}
}
Some(Availability::Requesting) => {
ui.label("Waiting for the browser to allow MIDIā¦");
}
Some(Availability::Denied) => {
ui.label("The browser blocked MIDI for this page; allow it in the site settings.");
}
Some(Availability::Unsupported) => {
ui.label("This browser has no MIDI; controllers work in Chrome, Edge or Firefox.");
}
Some(Availability::Ready) | None => {}
}
}
pub(super) fn collect_midi_controller_rows(
project: &Project,
diagnostics: &[MidiPortDiagnostics],
collections: &Collections,
) -> Vec<MidiControllerSelectionRow> {
let LocalMidiStatus { ports: here, .. } = frame_requests::local_midi();
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 frame_requests::devices().midi_outputs {
all_ports.insert(normalize_controller_key(&output_port));
}
for port in &here {
all_ports.insert(normalize_controller_key(port));
}
for configured in configured_ports {
all_ports.insert(configured);
}
all_ports
.into_iter()
.filter(|port_name| !is_own_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 {
here: here
.iter()
.any(|port| normalize_controller_key(port) == port_name),
default: default_controller_for_port(&port_name, collections),
port_name,
input_connected,
output_connected,
}
})
.collect()
}