use gled_model::asset::midi_controller::{
MidiColorSource, MidiControllerMapping, MidiEncoder, MidiInputAction, MidiInputBinding,
MidiOutputBindingKind, MidiTrigger,
};
use super::{Members, member_action, remove_bindings};
use crate::ui::windows::midi_controllers::rules::{
build::Rule,
controls::{Control, ControlKind},
feedback::{feedback_role, pad_target, source_for},
naming::name_rules_of,
sentence::Numbers,
sources::{live_source, matching_source},
};
pub(crate) fn holds(action: &MidiInputAction) -> bool {
matches!(
action,
MidiInputAction::FlashScene { .. }
| MidiInputAction::SetBlackout
| MidiInputAction::SetSceneActive { .. }
)
}
fn release_trigger(press: &MidiTrigger) -> Option<MidiTrigger> {
(press.status & 0xF0 == 0x90).then_some(MidiTrigger {
status: 0x80 | (press.status & 0x0F),
data1: press.data1,
match_data1: true,
})
}
pub(crate) fn set_action(
mapping: &mut MidiControllerMapping,
controls: &[Control],
members: &Members,
template: &MidiInputAction,
numbers: &dyn Numbers,
) -> Vec<String> {
let mut notes = Vec::new();
let mut removed_inputs = Vec::new();
let mut new_inputs = Vec::new();
for (rule, n) in members.each() {
let action = member_action(template, numbers, n);
let control = &controls[rule.control];
let old = rule
.press
.or(rule.release)
.map(|index| mapping.input_bindings[index].action.clone());
match (rule.press, rule.release) {
(None, Some(release)) => mapping.input_bindings[release].action = action.clone(),
(Some(press), release) => {
mapping.input_bindings[press].action = action.clone();
let button = matches!(control.kind, ControlKind::Button | ControlKind::Pad);
match release {
Some(release) if holds(&action) && button => {
mapping.input_bindings[release].action = action.clone();
}
Some(release) => removed_inputs.push(release),
None if holds(&action) && button => {
if let Some(trigger) = release_trigger(&control.trigger) {
new_inputs.push(MidiInputBinding {
trigger,
action: action.clone(),
..mapping.input_bindings[press].clone()
});
}
}
None => {}
}
}
(None, None) => {
let mut action = action.clone();
if let Some(encoder) = encoder_mut(&mut action)
&& let Some(known) = known_encoder(mapping, control)
{
*encoder = known;
}
let touch = mapping
.input_bindings
.iter()
.find(|binding| binding.trigger == control.trigger)
.and_then(|binding| binding.touch.clone());
let button = matches!(control.kind, ControlKind::Button | ControlKind::Pad);
if holds(&action)
&& button
&& let Some(trigger) = release_trigger(&control.trigger)
{
new_inputs.push(MidiInputBinding {
name: String::new(),
trigger,
action: action.clone(),
mode: rule.mode,
touch: None,
});
}
new_inputs.push(MidiInputBinding {
name: String::new(),
trigger: control.trigger.clone(),
action,
mode: rule.mode,
touch,
});
}
}
for &output in &rule.feedback {
let role = feedback_role(controls, rule, &mapping.output_bindings[output].kind);
match &mut mapping.output_bindings[output].kind {
MidiOutputBindingKind::SceneColorValue(color) => {
let MidiColorSource::SceneColor { target } = &mut color.source;
if *target == pad_target(old.as_ref()) {
*target = pad_target(Some(&action));
}
}
kind => {
let Some(source) = kind.source_mut() else {
continue;
};
if !shows(source, old.as_ref()) {
continue;
}
match source_for(role, Some(&action)) {
Some(new) => *source = new,
None => notes.push(format!(
"{}'s {} still shows what it showed before",
control.name,
role.name().to_lowercase()
)),
}
}
}
}
}
remove_bindings(mapping, removed_inputs, Vec::new());
mapping.input_bindings.extend(new_inputs);
name_rules_of(mapping, controls, &members.keys());
notes.dedup();
notes
}
fn encoder_mut(action: &mut MidiInputAction) -> Option<&mut MidiEncoder> {
use MidiInputAction as A;
match action {
A::SetMainDimmerRelative { encoder }
| A::SetSpeedRelative { encoder }
| A::SelectSceneRelative { encoder }
| A::SelectSceneRow { encoder }
| A::SelectSceneColumn { encoder }
| A::SelectSceneEffectRelative { encoder } => Some(encoder),
_ => None,
}
}
fn known_encoder(mapping: &MidiControllerMapping, control: &Control) -> Option<MidiEncoder> {
let mut encoders = mapping.input_bindings.iter().filter_map(|binding| {
let mut action = binding.action.clone();
encoder_mut(&mut action)
.copied()
.map(|encoder| (binding.trigger == control.trigger, encoder))
});
let all: Vec<(bool, MidiEncoder)> = encoders.by_ref().collect();
all.iter()
.find(|(same, _)| *same)
.or_else(|| all.first())
.map(|(same, encoder)| {
if *same {
*encoder
} else {
MidiEncoder {
steps_per_unit: 1,
..*encoder
}
}
})
}
fn shows(
source: &gled_model::asset::midi_controller::MidiValueSource,
action: Option<&MidiInputAction>,
) -> bool {
let Some(matching) = action.and_then(matching_source) else {
return false;
};
*source == matching || *source == live_source(matching)
}
pub(crate) fn set_while_held(
mapping: &mut MidiControllerMapping,
controls: &[Control],
members: &Members,
held: bool,
) {
let mut removed = Vec::new();
let mut added = Vec::new();
for rule in &members.rules {
let Some(press) = rule.press else {
continue;
};
match (held, rule.release) {
(true, None) => {
if let Some(trigger) = release_trigger(&controls[rule.control].trigger) {
added.push(MidiInputBinding {
trigger,
..mapping.input_bindings[press].clone()
});
}
}
(false, Some(release)) => removed.push(release),
_ => {}
}
}
remove_bindings(mapping, removed, Vec::new());
mapping.input_bindings.extend(added);
name_rules_of(mapping, controls, &members.keys());
}
pub(crate) fn set_mode(mapping: &mut MidiControllerMapping, members: &Members, mode: Option<u8>) {
for rule in &members.rules {
for index in rule.press.iter().chain(&rule.release) {
mapping.input_bindings[*index].mode = mode;
}
for index in &rule.feedback {
mapping.output_bindings[*index].mode = mode;
}
}
}
pub(crate) fn set_touch(
mapping: &mut MidiControllerMapping,
members: &Members,
touch: Option<MidiTrigger>,
) {
for (position, rule) in members.rules.iter().enumerate() {
let Some(press) = rule.press else {
continue;
};
mapping.input_bindings[press].touch = touch.clone().map(|touch| MidiTrigger {
data1: touch
.data1
.saturating_add(u8::try_from(position).unwrap_or(u8::MAX)),
..touch
});
}
}
pub(crate) fn remove_action(mapping: &mut MidiControllerMapping, members: &Members) {
let inputs = members
.rules
.iter()
.flat_map(|rule| rule.press.into_iter().chain(rule.release))
.collect();
remove_bindings(mapping, inputs, Vec::new());
}
pub(crate) fn remove_rules(mapping: &mut MidiControllerMapping, members: &Members) {
let inputs = members
.rules
.iter()
.flat_map(|rule| rule.press.into_iter().chain(rule.release))
.collect();
let outputs = members
.rules
.iter()
.flat_map(|rule| rule.feedback.iter().copied())
.collect();
remove_bindings(mapping, inputs, outputs);
}
pub(crate) fn rule_action<'a>(
mapping: &'a MidiControllerMapping,
rule: &Rule,
) -> Option<&'a MidiInputAction> {
rule.press
.or(rule.release)
.map(|index| &mapping.input_bindings[index].action)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::windows::midi_controllers::rules::{
build::build_rules,
controls::controls_of,
groups::{GroupNumbers, Place, find_groups},
sentence::Plain,
test_mappings::p1x,
};
use gled_model::asset::midi_controller::{MidiEffectTarget, MidiSceneTarget, MidiValueSource};
fn group_from(
mapping: &MidiControllerMapping,
name: &str,
mode: Option<u8>,
) -> (
Vec<Control>,
Members,
crate::ui::windows::midi_controllers::rules::groups::RuleGroup,
) {
let controls = controls_of(mapping);
let rules = build_rules(mapping, &controls);
let group = find_groups(mapping, &controls, &rules)
.into_iter()
.find(|group| {
controls[rules[group.rules[0]].control].name == name
&& rules[group.rules[0]].mode == mode
})
.unwrap_or_else(|| panic!("no group from {name}"));
let members = Members::of_group(&rules, &group);
(controls, members, group)
}
#[test]
fn flash_buttons_turned_into_toggles_lose_their_release_and_light_the_scene_state() {
let mut mapping = p1x();
let inputs_before = mapping.input_bindings.len();
let (controls, members, group) = group_from(&mapping, "S 1", Some(0));
let numbers = GroupNumbers {
group: &group,
place: Place::Action,
};
let template = MidiInputAction::ToggleSceneActive {
target: MidiSceneTarget::Row { col: 0 },
};
let notes = set_action(&mut mapping, &controls, &members, &template, &numbers);
assert!(notes.is_empty(), "{notes:?}");
assert_eq!(mapping.input_bindings.len(), inputs_before - 8);
let controls = controls_of(&mapping);
let rules = build_rules(&mapping, &controls);
let s3 = rules
.iter()
.find(|rule| controls[rule.control].name == "S 3" && rule.mode == Some(0))
.unwrap_or_else(|| panic!("no S 3"));
assert_eq!(s3.release, None);
assert_eq!(
rule_action(&mapping, s3),
Some(&MidiInputAction::ToggleSceneActive {
target: MidiSceneTarget::Row { col: 2 }
})
);
assert_eq!(
mapping.output_bindings[s3.feedback[0]].kind.source(),
Some(&MidiValueSource::SceneActive {
target: MidiSceneTarget::Row { col: 2 }
})
);
}
#[test]
fn a_fader_given_a_new_value_moves_its_motor_fader_with_it() {
let mut mapping = p1x();
let controls = controls_of(&mapping);
let rules = build_rules(&mapping, &controls);
let fader = rules
.iter()
.find(|rule| controls[rule.control].name == "Fader 1" && rule.mode == Some(1))
.cloned()
.unwrap_or_else(|| panic!("no Fader 1"));
let template = MidiInputAction::SetSceneEffectSpeedExponent {
target: MidiSceneTarget::Selected,
effect: MidiEffectTarget::Selected,
};
let notes = set_action(
&mut mapping,
&controls,
&Members::single(fader.clone()),
&template,
&Plain,
);
let motor = &mapping.output_bindings[fader.feedback[0]];
assert_eq!(
motor.kind.source(),
Some(&MidiValueSource::SceneEffectSpeedExponent {
target: MidiSceneTarget::Selected,
effect: MidiEffectTarget::Selected,
})
);
assert!(
motor.name.starts_with("Fader 1 motor fader:"),
"{}",
motor.name
);
assert!(notes.is_empty(), "{notes:?}");
assert_eq!(
mapping.output_bindings[fader.feedback[1]].kind.source(),
motor.kind.source()
);
}
#[test]
fn a_button_made_to_flash_gets_a_release() {
let mut mapping = p1x();
let (controls, members, group) = group_from(&mapping, "Rec 1", Some(0));
let inputs_before = mapping.input_bindings.len();
let numbers = GroupNumbers {
group: &group,
place: Place::Action,
};
set_action(
&mut mapping,
&controls,
&members,
&MidiInputAction::FlashScene {
target: MidiSceneTarget::Row { col: 0 },
},
&numbers,
);
assert_eq!(mapping.input_bindings.len(), inputs_before + 8);
let controls = controls_of(&mapping);
let rules = build_rules(&mapping, &controls);
assert!(
rules
.iter()
.filter(|rule| controls[rule.control].name.starts_with("Rec"))
.all(|rule| rule.while_held(&mapping))
);
}
#[test]
fn a_new_encoder_rule_turns_like_the_encoder_elsewhere() {
let mut mapping = p1x();
let controls = controls_of(&mapping);
let encoder1 = controls
.iter()
.position(|control| control.name == "Encoder 1")
.unwrap_or_else(|| panic!("no Encoder 1"));
let rule = Rule {
control: encoder1,
mode: Some(2),
press: None,
release: None,
feedback: Vec::new(),
};
set_action(
&mut mapping,
&controls,
&Members::single(rule),
&MidiInputAction::SelectSceneColumn {
encoder: MidiEncoder::default(),
},
&Plain,
);
let added = mapping
.input_bindings
.iter()
.find(|binding| binding.mode == Some(2))
.unwrap_or_else(|| panic!("no rule in the new mode"));
let MidiInputAction::SelectSceneColumn { encoder } = added.action else {
panic!("not a column step: {:?}", added.action);
};
assert_eq!(encoder.steps_per_unit, 8);
assert_eq!(
encoder.encoding,
gled_model::asset::midi_controller::MidiRelativeEncoding::SignMagnitude
);
assert!(
added
.name
.starts_with("Encoder 1: steps through the columns"),
"{}",
added.name
);
}
#[test]
fn a_new_hold_rule_on_a_button_works_while_held() {
let mut mapping = p1x();
let controls = controls_of(&mapping);
let rec1 = controls
.iter()
.position(|control| control.name == "Rec 1")
.unwrap_or_else(|| panic!("no Rec 1"));
let rule = Rule {
control: rec1,
mode: None,
press: None,
release: None,
feedback: Vec::new(),
};
set_action(
&mut mapping,
&controls,
&Members::single(rule),
&MidiInputAction::SetBlackout,
&Plain,
);
let rules = build_rules(&mapping, &controls);
let made = rules
.iter()
.find(|rule| rule.control == rec1 && rule.mode.is_none())
.unwrap_or_else(|| panic!("no rule for Rec 1 in all modes"));
assert!(made.while_held(&mapping));
}
}