use std::collections::VecDeque;
use egui::{Panel, RichText, ScrollArea, Ui};
use gled_model::{
asset::{
Asset,
midi_controller::{MidiController, MidiOutputBindingKind, normalize_controller_key},
},
asset_id::AssetId,
};
use gled_protocol::midi::{MidiMonitorEvent, TestCommand};
use kanal::Sender;
use super::MidiControllerTestState;
use super::color_editor::colors_tab;
use super::intent::{Intent, apply};
use super::learning::{
Learned, Pending, hardware_message, new_control, start_learning, take_captures,
};
use super::list_state::{LearnRequest, ListState};
use super::rule_card::Cards;
use super::rule_list::rule_list;
use super::rules::{Rule, build_rules, control_index, controls_of, find_groups};
use super::side_tabs::{SideTab, devices_tab, modes_tab, monitor_tab, tab_bar};
use super::status_line::StatusLine;
use super::test_panel::render_test_device_box;
use crate::canvas::{edit::Edit, undo::UndoStack};
use crate::midi_learn::{LearnState, MidiPortDiagnostics};
use crate::ui::frame_requests;
#[derive(Debug, Default)]
pub(super) struct EditorState {
list: ListState,
undo: UndoStack<MidiController>,
status: StatusLine,
tab: SideTab,
controller: Option<AssetId<MidiController>>,
learning: bool,
pending: Option<Pending>,
followed_until: u128,
}
pub(super) struct EditorContext<'a> {
pub(super) test_state: &'a mut MidiControllerTestState,
pub(super) test_command_sender: &'a Sender<TestCommand>,
pub(super) dirty: &'a mut bool,
pub(super) project: &'a mut Option<gled_model::asset::project::Project>,
pub(super) diagnostics: &'a [MidiPortDiagnostics],
pub(super) learn_state: &'a mut LearnState,
pub(super) events: &'a mut VecDeque<MidiMonitorEvent>,
pub(super) others: &'a [super::OtherController],
pub(super) take_over: &'a mut Option<(AssetId<MidiController>, String)>,
}
pub(super) fn midi_controller_editor(
ui: &mut Ui,
controller: &mut Asset<MidiController>,
state: &mut EditorState,
context: EditorContext,
) {
if state.controller != Some(controller.id) {
state.controller = Some(controller.id);
state.list.reset();
state.undo.clear();
state.pending = None;
state.learning = false;
context.learn_state.disarm();
}
context.learn_state.flush_timeouts();
let controller_id = controller.id;
let mut edit = Edit::new(&mut controller.data, &mut state.undo);
let mut controls = controls_of(&edit.value.mapping);
let mut intents: Vec<Intent> = Vec::new();
let learned = edit.apply(|controller| {
take_captures(
context.learn_state,
controller_id,
&mut controller.mapping,
&mut controls,
)
});
for learned in learned {
match learned {
Learned::Pending(pending) => {
let index = control_index(&controls, &pending.trigger);
let control = index.unwrap_or(controls.len());
state.list.open_palette = Some((control, pending.mode));
state.list.scroll_to = Some(control);
let name = index.map_or_else(
|| super::rules::message_name(&pending.trigger),
|index| controls[index].name.clone(),
);
state
.status
.edit(ui, format!("Learned {name} — pick what it does"));
state.pending = Some(pending);
}
Learned::Existing(control) => {
state.list.scroll_to = Some(control);
state.list.highlight = Some((control, ui.input(|input| input.time)));
state.status.edit(
ui,
format!("{} already has a rule here", controls[control].name),
);
}
Learned::Message(message) => state.status.edit(ui, message),
}
}
if let Some(pending) = &state.pending
&& control_index(&controls, &pending.trigger).is_none()
{
controls.push(new_control(&pending.trigger));
}
let mut rules = build_rules(&edit.value.mapping, &controls);
if let Some(pending) = state.pending.clone() {
let control = control_index(&controls, &pending.trigger).unwrap_or(0);
let exists = rules.iter().any(|rule| {
rule.control == control
&& rule.mode == pending.mode
&& rule.press.or(rule.release).is_some()
});
let palette_closed =
state.list.open_palette.is_none() && !egui::Popup::is_any_open(ui.ctx());
if exists {
state.pending = None;
} else if palette_closed {
state.status.edit(
ui,
format!(
"{} sends {} — no rule made",
controls[control].name,
super::rules::message_name(&pending.trigger)
),
);
state.pending = None;
} else {
rules.push(Rule {
control,
mode: pending.mode,
press: None,
release: None,
feedback: Vec::new(),
});
}
}
let groups = find_groups(&edit.value.mapping, &controls, &rules);
follow_hardware(
ui,
&mut state.list,
&mut state.status,
&mut state.followed_until,
context.events,
&edit.value.mapping,
&controls,
&rules,
);
super::top_bar::keys(
ui,
&mut state.list,
&mut state.learning,
&mut edit,
context.learn_state,
);
let mut previewed = Vec::new();
Panel::right("midi controller tabs")
.exact_size(340.0)
.resizable(false)
.show(ui, |ui| {
tab_bar(ui, &mut state.tab);
match state.tab {
SideTab::Monitor => monitor_tab(ui, context.events, context.diagnostics, &controls),
SideTab::Colors => {
let mut colors = edit.value.mapping.color_mappings.clone();
let changed = ScrollArea::vertical()
.show(ui, |ui| {
colors_tab(
ui,
&mut colors,
&edit.value.mapping.output_bindings,
context.test_command_sender,
)
})
.inner;
if changed {
edit.apply(|controller| controller.mapping.color_mappings = colors);
}
}
SideTab::Modes => {
ScrollArea::vertical().show(ui, |ui| {
modes_tab(ui, edit.value, &mut intents, &mut state.list)
});
}
SideTab::Devices => {
let known: Vec<String> = context
.diagnostics
.iter()
.map(|port| port.port_name.clone())
.chain(frame_requests::local_midi().ports)
.collect();
let mut ports = edit.value.default_for_ports.clone();
if devices_tab(ui, &mut ports, &known, context.others, context.take_over) {
edit.apply(|controller| controller.default_for_ports = ports);
}
ui.add_space(12.0);
ui.label(RichText::new("Test lights").strong());
ui.weak("Pick a connected device to light what you point at in the rules.");
let picked = render_test_device_box(
ui,
context.test_state,
context.diagnostics,
controller_id,
context.test_command_sender,
);
if let Some(port) = picked {
disconnect_project_mapping_for_test_port(context.project, &port);
}
}
}
});
super::top_bar::top_bar(
ui,
super::top_bar::Bar {
list: &mut state.list,
learning: &mut state.learning,
tab: &mut state.tab,
edit: &mut edit,
learn_state: context.learn_state,
test_state: context.test_state,
},
);
{
let mut cards = Cards {
mapping: &edit.value.mapping,
controls: &controls,
rules: &rules,
state: &mut state.list,
intents: &mut intents,
previewed: &mut previewed,
};
super::rule_list::mode_chips(ui, &mut cards);
}
ui.separator();
let status_height = 24.0;
ScrollArea::vertical()
.max_height(ui.available_height() - status_height)
.auto_shrink([false, false])
.show(ui, |ui| {
let mut cards = Cards {
mapping: &edit.value.mapping,
controls: &controls,
rules: &rules,
state: &mut state.list,
intents: &mut intents,
previewed: &mut previewed,
};
rule_list(ui, &mut cards, &groups);
});
ui.separator();
state.status.show(ui);
for intent in intents {
let follow = match &intent {
Intent::SetAction { members, .. } | Intent::SetMode { members, .. }
if members.rules.len() == 1 =>
{
Some(members.rules[0].control)
}
_ => None,
};
if let Some(control) = follow {
state.list.scroll_to = Some(control);
state.list.highlight = Some((control, ui.input(|input| input.time)));
}
let message =
edit.apply(|controller| apply(intent, &mut controller.mapping, &mut controls));
if let Some(message) = message {
state.status.edit(ui, message);
}
}
if let Some(request) = state.list.learn_request.take() {
state.learning = request == LearnRequest::NewControls;
start_learning(context.learn_state, controller_id, request, &state.list);
}
if state.learning && context.learn_state.active_request().is_none() && state.pending.is_none() {
start_learning(
context.learn_state,
controller_id,
LearnRequest::NewControls,
&state.list,
);
}
context.test_state.value_preview_binding_indices = previewed.into_iter().collect();
*context.dirty |= edit.changed();
emit_test_output_commands(
&controller.data.mapping.output_bindings,
context.test_state,
context.test_command_sender,
);
}
#[allow(clippy::too_many_arguments)]
fn follow_hardware(
ui: &Ui,
list: &mut ListState,
status: &mut StatusLine,
followed_until: &mut u128,
events: &VecDeque<MidiMonitorEvent>,
mapping: &gled_model::asset::midi_controller::MidiControllerMapping,
controls: &[super::rules::Control],
rules: &[Rule],
) {
let newest = events.back().map_or(0, |event| event.timestamp_ms);
if newest <= *followed_until {
return;
}
for event in events
.iter()
.filter(|event| event.timestamp_ms > *followed_until)
{
if let Some((control, message)) =
hardware_message(event, mapping, controls, rules, list.filter)
{
if let Some(control) = control {
list.highlight = Some((control, ui.input(|input| input.time)));
list.scroll_to = Some(control);
}
status.hardware(ui, message);
}
}
*followed_until = newest;
}
fn disconnect_project_mapping_for_test_port(
project: &mut Option<gled_model::asset::project::Project>,
selected_port_name: &str,
) {
let Some(project) = project.as_mut() else {
return;
};
let normalized_selected = normalize_controller_key(selected_port_name);
project
.midi_active_mappings
.retain(|port_name, _| normalize_controller_key(port_name) != normalized_selected);
}
fn emit_test_output_commands(
bindings: &[gled_model::asset::midi_controller::MidiOutputBinding],
test_state: &mut MidiControllerTestState,
test_command_sender: &Sender<TestCommand>,
) {
let message =
|binding: &gled_model::asset::midi_controller::MidiOutputBinding| match &binding.kind {
MidiOutputBindingKind::Value(value) => {
Some((value.status, value.data1, value.active_value))
}
MidiOutputBindingKind::SceneColorValue(color) => Some((color.status, color.data1, 127)),
MidiOutputBindingKind::Meter(meter) => Some((0xD0, meter.strip << 4, 12)),
};
for &index in &test_state.value_preview_binding_indices {
if let Some((status, data1, value)) = bindings.get(index).and_then(message) {
let _ = test_command_sender.send(TestCommand::SendOutput {
status,
data1,
value,
});
}
}
for &index in &test_state.value_preview_binding_indices_previous {
if !test_state.value_preview_binding_indices.contains(&index)
&& let Some((status, data1, _)) = bindings.get(index).and_then(message)
{
let _ = test_command_sender.send(TestCommand::ClearOutput { status, data1 });
}
}
test_state.value_preview_binding_indices_previous =
test_state.value_preview_binding_indices.clone();
}