use crate::midi_learn::{LearnState, MidiLearnRequest, MidiLearnTarget};
use egui::{ComboBox, DragValue, Ui};
use egui_phosphor_icons::icons;
use gled_model::asset::midi_controller::{
MidiController, MidiInputAction, MidiInputActionKind, MidiInputBinding, MidiMode, MidiTrigger,
};
use strum::IntoEnumIterator;
use super::{
action_converters::{
effect_target_editor, encoder_editor, input_action_from_kind, scene_target_editor,
},
binding_helpers::{input_binding_matches_filter, midi_status_label},
iconized,
modes_editor::{BindingFilter, binding_mode_editor, mode_picker},
};
pub(super) struct MatchingOutput {
pub(super) name: String,
pub(super) status: u8,
pub(super) data1: u8,
pub(super) action: MidiInputAction,
pub(super) mode: Option<u8>,
}
pub(super) fn render_input_column(
ui: &mut Ui,
bindings: &mut Vec<MidiInputBinding>,
modes: &[MidiMode],
filter: &BindingFilter,
hovered_status_data1: Option<(u8, u8)>,
hovered_status_data1_next: &mut Option<(u8, u8)>,
dirty: &mut bool,
controller_id: gled_model::asset_id::AssetId<MidiController>,
learn_state: &mut LearnState,
) -> (Option<usize>, Vec<MatchingOutput>) {
let mut remove_input = None;
let mut pending_matching_output = Vec::new();
let mut duplicate_inputs = Vec::new();
ui.horizontal(|ui| {
ui.label(icons::ARROW_SQUARE_IN);
ui.heading("Input Bindings");
});
ui.horizontal(|ui| {
if ui
.button(iconized(ui, icons::PLUS, " Add Input Binding"))
.clicked()
{
bindings.insert(
0,
MidiInputBinding {
mode: filter.mode,
..Default::default()
},
);
*dirty = true;
}
});
for (index, binding) in bindings.iter_mut().enumerate() {
if !filter.shows_mode(binding.mode) || !input_binding_matches_filter(binding, &filter.text)
{
continue;
}
let status_data1 = (binding.trigger.status, binding.trigger.data1);
let highlight = hovered_status_data1.is_some_and(|hovered| hovered == status_data1);
let base_stroke = ui.visuals().widgets.noninteractive.bg_stroke;
let response = egui::Frame::group(ui.style())
.stroke(if highlight {
egui::Stroke::new(base_stroke.width, ui.visuals().selection.stroke.color)
} else {
base_stroke
})
.show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(format!("Input {}", index + 1));
let learning = learn_state.active_request().is_some_and(|request| {
request.controller_id == controller_id
&& request.target == MidiLearnTarget::InputBinding(index)
});
if ui
.button(if learning {
iconized(ui, icons::MICROPHONE, " Waiting for MIDI...")
} else {
iconized(ui, icons::MICROPHONE, " Learn")
})
.clicked()
{
learn_state.arm(MidiLearnRequest {
controller_id,
target: MidiLearnTarget::InputBinding(index),
});
}
if ui
.button(iconized(ui, icons::PLUS, " Duplicate"))
.clicked()
{
duplicate_inputs.push(binding.clone());
}
if ui.button(iconized(ui, icons::TRASH, " Remove")).clicked() {
remove_input = Some(index);
}
});
if ui.text_edit_singleline(&mut binding.name).changed() {
*dirty = true;
}
binding_mode_editor(
ui,
&mut binding.mode,
modes,
dirty,
format!("{controller_id}_input_mode_{index}"),
);
ui.horizontal(|ui| {
ui.label("Status");
if ui
.add(DragValue::new(&mut binding.trigger.status).range(0..=255))
.changed()
{
*dirty = true;
}
ui.label("Data1");
if ui
.add(DragValue::new(&mut binding.trigger.data1).range(0..=127))
.changed()
{
*dirty = true;
}
});
ui.label(format!(
"Detected Message: {}",
midi_status_label(binding.trigger.status)
));
if ui
.checkbox(
&mut binding.trigger.match_data1,
"Match Data1 (disable for Pitch Bend style controls)",
)
.changed()
{
*dirty = true;
}
touch_editor(ui, &mut binding.touch, dirty);
let mut action_kind = MidiInputActionKind::from(&binding.action);
ComboBox::new(format!("{}_input_kind_{index}", controller_id), "")
.selected_text(action_kind.to_string())
.show_ui(ui, |ui| {
for candidate in MidiInputActionKind::iter() {
let response = ui.selectable_value(
&mut action_kind,
candidate,
candidate.to_string(),
);
if candidate == MidiInputActionKind::SetSceneEffectLayout {
response.on_hover_text(
"0 lets the effect follow its tile's layout, 1 is the topology, \
2 and up the layouts in stack order",
);
}
}
});
let before = binding.action.clone();
binding.action = input_action_from_kind(action_kind, binding.action.clone());
if binding.action != before {
*dirty = true;
}
match &mut binding.action {
gled_model::asset::midi_controller::MidiInputAction::SelectScene { target }
| gled_model::asset::midi_controller::MidiInputAction::ToggleSceneActive {
target
}
| gled_model::asset::midi_controller::MidiInputAction::SetSceneActive { target }
| gled_model::asset::midi_controller::MidiInputAction::FlashScene { target }
| gled_model::asset::midi_controller::MidiInputAction::SetSceneOpacity { target }
| gled_model::asset::midi_controller::MidiInputAction::SetSceneInputDimmer {
target
}
| gled_model::asset::midi_controller::MidiInputAction::SetSceneBeatOffset {
target
}
| gled_model::asset::midi_controller::MidiInputAction::SetSceneIgnoreMainDimmer {
target
}
| gled_model::asset::midi_controller::MidiInputAction::SetSceneSetOffsetOnFlash {
target
}
| gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectSettingF32 {
target,
..
}
| gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectOpacity { target, .. }
| gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectColorShift { target, .. }
| gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectBeatProgression { target, .. }
| gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectBeatOffset { target, .. }
| gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectSpeedExponent { target, .. }
| gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectLayout { target, .. } => {
scene_target_editor(
ui,
target,
dirty,
format!("{}_input_target_{index}", controller_id),
);
}
_ => {}
}
match &mut binding.action {
MidiInputAction::SetSceneEffectSettingF32 {
effect,
setting_index,
..
} => {
effect_target_editor(
ui,
effect,
dirty,
format!("{controller_id}_input_effect_{index}"),
);
ui.horizontal(|ui| {
ui.label("Setting Index");
if ui.add(DragValue::new(setting_index).range(0..=5)).changed() {
*dirty = true;
}
});
}
MidiInputAction::SetSceneEffectOpacity { effect, .. }
| MidiInputAction::SetSceneEffectColorShift { effect, .. }
| MidiInputAction::SetSceneEffectBeatProgression { effect, .. }
| MidiInputAction::SetSceneEffectBeatOffset { effect, .. }
| MidiInputAction::SetSceneEffectSpeedExponent { effect, .. }
| MidiInputAction::SetSceneEffectLayout { effect, .. } => {
effect_target_editor(
ui,
effect,
dirty,
format!("{controller_id}_input_effect_{index}"),
);
}
MidiInputAction::SelectMode { mode } => {
mode_picker(
ui,
mode,
modes,
dirty,
format!("{controller_id}_input_select_mode_{index}"),
);
}
MidiInputAction::SelectSceneEffect { index: effect_index } => {
ui.horizontal(|ui| {
ui.label("Effect Index");
if ui.add(DragValue::new(effect_index).range(0..=255)).changed() {
*dirty = true;
}
});
}
MidiInputAction::SetMainDimmerRelative { encoder }
| MidiInputAction::SetSpeedRelative { encoder }
| MidiInputAction::SelectSceneRelative { encoder }
| MidiInputAction::SelectSceneRow { encoder }
| MidiInputAction::SelectSceneColumn { encoder }
| MidiInputAction::SelectSceneEffectRelative { encoder } => {
encoder_editor(
ui,
encoder,
dirty,
format!("{controller_id}_input_encoding_{index}"),
);
}
_ => {}
}
if matches!(
binding.action,
gled_model::asset::midi_controller::MidiInputAction::SelectSceneDistributed
) {
ui.small("Maps value 0..127 evenly across existing scenes in row-major order.");
}
if ui
.button(iconized(ui, icons::PLUS, " Create Matching Output Binding"))
.clicked()
{
pending_matching_output.push(MatchingOutput {
name: binding.name.clone(),
status: binding.trigger.status,
data1: binding.trigger.data1,
action: binding.action.clone(),
mode: binding.mode,
});
}
})
.response;
if response.hovered() {
*hovered_status_data1_next = Some(status_data1);
}
}
if !duplicate_inputs.is_empty() {
bindings.extend(duplicate_inputs);
*dirty = true;
}
(remove_input, pending_matching_output)
}
fn touch_editor(ui: &mut Ui, touch: &mut Option<MidiTrigger>, dirty: &mut bool) {
let mut touch_sensitive = touch.is_some();
if ui
.checkbox(&mut touch_sensitive, "Touch note")
.on_hover_text(
"A note the control sends while it is touched, such as a motor fader's; \
the interface outlines what the binding changes meanwhile",
)
.changed()
{
*touch = touch_sensitive.then_some(MidiTrigger {
status: 0x90,
data1: 0,
match_data1: true,
});
*dirty = true;
}
if let Some(touch) = touch {
ui.horizontal(|ui| {
ui.label("Touch Status");
if ui
.add(DragValue::new(&mut touch.status).range(0x90..=0x9F))
.changed()
{
*dirty = true;
}
ui.label("Note");
if ui
.add(DragValue::new(&mut touch.data1).range(0..=127))
.changed()
{
*dirty = true;
}
});
}
}