use egui::{ComboBox, Grid, RichText, ScrollArea, Ui};
use egui_phosphor_icons::icons;
use gled_midi::monitor::label;
use gled_model::asset::midi_controller::{MidiController, MidiModeHighlight, MidiTrigger};
use gled_protocol::midi::MidiMonitorEvent;
use std::collections::VecDeque;
use strum::IntoEnumIterator;
use super::intent::Intent;
use super::list_state::{LearnRequest, ListState};
use super::rules::{Control, control_index, message_name};
use crate::midi_learn::MidiPortDiagnostics;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum SideTab {
#[default]
Monitor,
Colors,
Modes,
Devices,
}
impl SideTab {
fn label(self) -> &'static str {
match self {
Self::Monitor => "Monitor",
Self::Colors => "Colors",
Self::Modes => "Modes",
Self::Devices => "Devices",
}
}
}
pub(crate) fn tab_bar(ui: &mut Ui, tab: &mut SideTab) {
ui.horizontal(|ui| {
for option in [
SideTab::Monitor,
SideTab::Colors,
SideTab::Modes,
SideTab::Devices,
] {
ui.selectable_value(tab, option, option.label());
}
});
ui.separator();
}
pub(crate) fn monitor_tab(
ui: &mut Ui,
events: &mut VecDeque<MidiMonitorEvent>,
diagnostics: &[MidiPortDiagnostics],
controls: &[Control],
) {
ui.label(RichText::new("Ports").strong());
if diagnostics.is_empty() {
ui.weak("No MIDI ports seen yet. Connect a controller.");
}
for port in diagnostics {
ui.group(|ui| {
ui.label(&port.port_name);
let state = |connected: bool| {
if connected {
"connected"
} else {
"not connected"
}
};
ui.weak(format!(
"input {} · output {} · {} messages",
state(port.input_connected),
state(port.output_connected),
port.message_count
));
if port.input_connect_failed + port.output_connect_failed > 0 {
ui.colored_label(
ui.visuals().warn_fg_color,
format!(
"{} failed attempts to connect — is another program using the port?",
port.input_connect_failed + port.output_connect_failed
),
);
}
});
}
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.label(RichText::new("Incoming MIDI").strong());
if ui.small_button("Clear").clicked() {
events.clear();
}
});
ui.weak("Move a control: its message shows here with the control it belongs to.");
ScrollArea::vertical()
.id_salt("midi monitor")
.show(ui, |ui| {
Grid::new("midi monitor rows").striped(true).show(ui, |ui| {
for event in events
.iter()
.rev()
.filter(|event| event.label == label::MIDI)
.take(200)
{
let [status, data1, value] = match event.bytes.as_slice() {
[status, data1, value, ..] => [*status, *data1, *value],
[status, data1] => [*status, *data1, 0],
_ => continue,
};
let trigger = MidiTrigger {
status,
data1,
match_data1: true,
};
ui.label(message_name(&trigger));
ui.label(value.to_string());
let control = control_index(controls, &trigger).map(|index| &controls[index]);
match control {
Some(control) => ui.label(&control.name),
None => ui.weak("not mapped"),
};
ui.end_row();
}
});
});
}
pub(crate) fn modes_tab(
ui: &mut Ui,
controller: &MidiController,
intents: &mut Vec<Intent>,
state: &mut ListState,
) {
let modes = &controller.mapping.modes;
ui.add(
egui::Label::new(
"Modes let the same controls do different things. A button switches between them; \
the first mode is active at start.",
)
.wrap(),
);
ui.add_space(4.0);
for (index, mode) in modes.iter().enumerate() {
let Ok(index) = u8::try_from(index) else {
continue;
};
ui.group(|ui| {
ui.set_width(ui.available_width());
ui.horizontal(|ui| {
let id = ui.make_persistent_id(("mode name", index));
let mut name: String = ui
.data(|data| data.get_temp(id))
.unwrap_or_else(|| mode.name.clone());
let field = ui.add(egui::TextEdit::singleline(&mut name).desired_width(200.0));
if field.lost_focus() && name.trim() != mode.name && !name.trim().is_empty() {
intents.push(Intent::RenameMode {
mode: index,
name: name.trim().to_owned(),
});
}
if field.has_focus() {
ui.data_mut(|data| data.insert_temp(id, name));
} else {
ui.data_mut(|data| data.remove::<String>(id));
}
if index > 0
&& ui
.small_button(icons::ARROW_UP)
.on_hover_text("Move up")
.clicked()
{
intents.push(Intent::MoveMode {
from: index,
to: index - 1,
});
}
if usize::from(index) + 1 < modes.len()
&& ui
.small_button(icons::ARROW_DOWN)
.on_hover_text("Move down")
.clicked()
{
intents.push(Intent::MoveMode {
from: index,
to: index + 1,
});
}
});
if index == 0 {
ui.weak("The controller starts in this mode");
}
ui.horizontal(|ui| {
ui.label("Highlights in gled");
let label = |highlight: Option<MidiModeHighlight>| {
highlight
.map_or_else(|| "Nothing".to_owned(), |highlight| highlight.to_string())
};
ComboBox::from_id_salt(("mode highlight", index))
.width(130.0)
.selected_text(label(mode.highlight))
.show_ui(ui, |ui| {
for option in
std::iter::once(None).chain(MidiModeHighlight::iter().map(Some))
{
if ui
.selectable_label(mode.highlight == option, label(option))
.clicked()
{
intents.push(Intent::SetHighlight {
mode: index,
highlight: option,
});
}
}
});
})
.response
.on_hover_text(
"What lights up in gled for a moment when the controller switches to this mode",
);
if ui
.button("Choose the button that switches to it…")
.on_hover_text("Then press the button on your controller")
.clicked()
{
state.learn_request = Some(LearnRequest::ModeSwitch(index));
}
let uses = controller.mapping.mode_uses(index);
if uses == 0 {
if ui
.button(super::iconized(ui, icons::TRASH, " Remove mode"))
.clicked()
{
intents.push(Intent::RemoveMode(index));
}
} else {
ui.add(
egui::Label::new(
RichText::new(format!(
"Can't be removed while {uses} bindings work in it or switch to it; \
move or delete those first"
))
.weak(),
)
.wrap(),
);
}
});
}
}
pub(crate) fn devices_tab(
ui: &mut Ui,
ports: &mut Vec<String>,
known_ports: &[String],
others: &[super::OtherController],
take_over: &mut Option<(gled_model::asset_id::AssetId<MidiController>, String)>,
) -> bool {
let mut changed = false;
ui.label(RichText::new("Use automatically for").strong());
ui.weak(
"A connected device on one of these ports gets this controller, \
unless the project assigns it another one or none.",
);
let mut remove = None;
for (index, port) in ports.iter().enumerate() {
ui.horizontal(|ui| {
ui.label(super::test_panel::short_port_name(port))
.on_hover_text(port);
if ui
.small_button(icons::X)
.on_hover_text("Stop using it for this device")
.clicked()
{
remove = Some(index);
}
});
for other in others.iter().filter(|other| other.ports.contains(port)) {
ui.horizontal_wrapped(|ui| {
ui.colored_label(
ui.visuals().warn_fg_color,
format!("{} is used for it too; gled takes one of them", other.name),
);
if ui
.small_button("Take over")
.on_hover_text(format!("Stop using {} for this device", other.name))
.clicked()
{
*take_over = Some((other.id, port.clone()));
}
});
}
}
if let Some(index) = remove {
ports.remove(index);
changed = true;
}
let addable: Vec<String> = known_ports
.iter()
.map(|port| gled_model::asset::midi_controller::normalize_controller_key(port))
.filter(|port| !ports.contains(port))
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect();
if addable.is_empty() {
if ports.is_empty() {
ui.weak("No device connected yet; connect one to add it.");
}
} else {
ComboBox::from_id_salt("add default port")
.selected_text("Add a connected device…")
.show_ui(ui, |ui| {
for port in addable {
if ui
.selectable_label(false, super::test_panel::short_port_name(&port))
.on_hover_text(&port)
.clicked()
{
ports.push(port);
changed = true;
}
}
});
}
changed
}