use gled_model::asset::midi_controller::{
MidiControllerMapping, MidiInputAction, MidiInputBinding, MidiOutputBinding,
MidiOutputBindingKind, MidiTrigger, MidiValueSource,
};
use super::{member_action, member_output, remove_bindings, store_controls};
use crate::ui::windows::midi_controllers::rules::{
build::{Rule, build_rules},
controls::{Control, message_name, press_trigger},
feedback::{FeedbackRole, new_feedback},
naming::name_rules_of,
sentence::{Numbers, Plain, action_sentence, text},
};
pub(crate) fn copy_rule(
mapping: &mut MidiControllerMapping,
controls: &[Control],
rule: &Rule,
n: i32,
targets: &[(usize, i32)],
numbers: &dyn Numbers,
) -> usize {
let from = &controls[rule.control];
let mut inputs = Vec::new();
let mut outputs = Vec::new();
for &(target, target_n) in targets {
let to = &controls[target];
let mut copy_input = |index: usize, release: bool| {
let binding = &mapping.input_bindings[index];
let trigger = if release {
MidiTrigger {
status: 0x80 | (to.trigger.status & 0x0F),
data1: to.trigger.data1,
match_data1: true,
}
} else {
to.trigger.clone()
};
let touch = binding.touch.clone().map(|touch| MidiTrigger {
data1: shift(touch.data1, target_n - n),
..touch
});
inputs.push(MidiInputBinding {
name: String::new(),
trigger,
action: member_action(&binding.action, numbers, target_n),
mode: binding.mode,
touch,
});
};
if let Some(press) = rule.press {
copy_input(press, false);
}
if let Some(release) = rule.release {
copy_input(release, true);
}
for &output in &rule.feedback {
let binding = &mapping.output_bindings[output];
let kind = member_output(&retarget(&binding.kind, from, to), numbers, target_n);
outputs.push(MidiOutputBinding {
name: String::new(),
kind,
mode: binding.mode,
});
}
}
let replaced: Vec<Rule> = build_rules(mapping, controls)
.into_iter()
.filter(|other| {
other.mode == rule.mode && targets.iter().any(|(target, _)| *target == other.control)
})
.collect();
remove_bindings(
mapping,
replaced
.iter()
.flat_map(|other| other.press.into_iter().chain(other.release))
.collect(),
replaced
.iter()
.flat_map(|other| other.feedback.iter().copied())
.collect(),
);
mapping.input_bindings.extend(inputs);
mapping.output_bindings.extend(outputs);
let edited: Vec<(usize, Option<u8>)> = targets
.iter()
.map(|(target, _)| (*target, rule.mode))
.collect();
name_rules_of(mapping, controls, &edited);
replaced.len()
}
fn retarget(kind: &MidiOutputBindingKind, from: &Control, to: &Control) -> MidiOutputBindingKind {
let channel = to.trigger.status & 0x0F;
let mut kind = kind.clone();
match &mut kind {
MidiOutputBindingKind::Value(value) => {
value.status = (value.status & 0xF0) | channel;
if value.data1 == from.trigger.data1 {
value.data1 = to.trigger.data1;
}
}
MidiOutputBindingKind::SceneColorValue(color) => {
color.status = (color.status & 0xF0) | channel;
color.data1 = to.trigger.data1;
}
MidiOutputBindingKind::Meter(meter) => meter.strip = channel & 0x07,
}
kind
}
fn shift(value: u8, by: i32) -> u8 {
u8::try_from((i32::from(value) + by).clamp(0, 127)).unwrap_or(0)
}
pub(crate) fn relearn_control(
mapping: &mut MidiControllerMapping,
controls: &mut [Control],
control: usize,
trigger: MidiTrigger,
) {
let Some(old) = controls.get(control).cloned() else {
return;
};
let mut new = old.clone();
new.trigger = press_trigger(&trigger);
if old.named_by_message {
new.name = message_name(&new.trigger);
}
for rule in build_rules(mapping, controls) {
if rule.control != control {
continue;
}
if let Some(press) = rule.press {
mapping.input_bindings[press].trigger = new.trigger.clone();
}
if let Some(release) = rule.release {
mapping.input_bindings[release].trigger = MidiTrigger {
status: 0x80 | (new.trigger.status & 0x0F),
data1: new.trigger.data1,
match_data1: true,
};
}
for &output in &rule.feedback {
let kind = retarget(&mapping.output_bindings[output].kind, &old, &new);
mapping.output_bindings[output].kind = kind;
}
}
controls[control] = new;
store_controls(mapping, controls);
let modes: Vec<(usize, Option<u8>)> = std::iter::once(None)
.chain((0..mapping.modes.len()).map(|mode| u8::try_from(mode).ok()))
.map(|mode| (control, mode))
.collect();
name_rules_of(mapping, controls, &modes);
}
pub(crate) fn make_mode_switch(
mapping: &mut MidiControllerMapping,
controls: &[Control],
control: usize,
mode: u8,
) -> Option<String> {
let previous: Vec<Rule> = build_rules(mapping, controls)
.into_iter()
.filter(|rule| rule.control == control && rule.mode.is_none())
.collect();
let before = previous
.iter()
.find_map(|rule| rule.press.or(rule.release))
.map(|index| {
text(&action_sentence(
&mapping.input_bindings[index].action,
&mapping.modes,
&Plain,
))
});
remove_bindings(
mapping,
previous
.iter()
.flat_map(|rule| rule.press.into_iter().chain(rule.release))
.collect(),
previous
.iter()
.flat_map(|rule| rule.feedback.iter().copied())
.collect(),
);
let switch = &controls[control];
mapping.input_bindings.push(MidiInputBinding {
name: String::new(),
trigger: switch.trigger.clone(),
action: MidiInputAction::SelectMode { mode },
mode: None,
touch: None,
});
mapping.output_bindings.push(MidiOutputBinding {
name: String::new(),
kind: new_feedback(
switch,
FeedbackRole::Light,
MidiValueSource::ModeActive { mode },
gled_model::asset::midi_controller::MidiSceneTarget::Selected,
"",
),
mode: None,
});
name_rules_of(mapping, controls, &[(control, None)]);
before
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::windows::midi_controllers::rules::{
controls::controls_of,
edit::Members,
edit::actions::{rule_action, set_action},
groups::Numbering,
numbers::Slot,
test_mappings::p1x,
};
use gled_model::asset::midi_controller::MidiSceneTarget;
struct ColumnFrom(i32);
impl Numbers for ColumnFrom {
fn numbering(&self, slot: Slot) -> Numbering {
match slot {
Slot::Column => Numbering::Counting { offset: self.0 },
_ => Numbering::Same,
}
}
}
fn rule_named(
mapping: &MidiControllerMapping,
controls: &[Control],
name: &str,
mode: Option<u8>,
) -> Rule {
build_rules(mapping, controls)
.into_iter()
.find(|rule| controls[rule.control].name == name && rule.mode == mode)
.unwrap_or_else(|| panic!("no {name} in {mode:?}"))
}
#[test]
fn a_copied_rule_counts_from_an_offset() {
let mut mapping = p1x();
let controls = controls_of(&mapping);
let select5 = rule_named(&mapping, &controls, "Select 5", Some(1));
set_action(
&mut mapping,
&controls,
&Members::single(select5.clone()),
&MidiInputAction::SelectScene {
target: MidiSceneTarget::Row { col: 0 },
},
&Plain,
);
let select5 = rule_named(&mapping, &controls, "Select 5", Some(1));
let targets: Vec<(usize, i32)> = ["Select 6", "Select 7", "Select 8"]
.iter()
.zip(6..)
.map(|(name, n)| {
let index = controls
.iter()
.position(|control| control.name == *name)
.unwrap_or(0);
(index, n)
})
.collect();
let replaced = copy_rule(
&mut mapping,
&controls,
&select5,
5,
&targets,
&ColumnFrom(-4),
);
assert_eq!(replaced, 3);
let select8 = rule_named(&mapping, &controls, "Select 8", Some(1));
assert_eq!(
rule_action(&mapping, &select8),
Some(&MidiInputAction::SelectScene {
target: MidiSceneTarget::Row { col: 3 }
})
);
assert_eq!(
mapping.output_bindings[select8.feedback[0]].kind.source(),
Some(&MidiValueSource::SceneSelected {
target: MidiSceneTarget::Row { col: 3 }
})
);
let select4 = rule_named(&mapping, &controls, "Select 4", Some(1));
assert_eq!(
rule_action(&mapping, &select4),
Some(&MidiInputAction::SelectSceneEffect { index: 3 })
);
}
#[test]
fn a_button_made_a_mode_switch_says_what_it_did_before() {
let mut mapping = p1x();
let controls = controls_of(&mapping);
let encoder4 = controls
.iter()
.position(|control| control.name == "Encoder 4 push")
.unwrap_or_else(|| panic!("no Encoder 4 push"));
let before = make_mode_switch(&mut mapping, &controls, encoder4, 2);
assert_eq!(before.as_deref(), Some("taps the tempo"));
let rule = rule_named(&mapping, &controls, "Encoder 4 push", None);
assert_eq!(
rule_action(&mapping, &rule),
Some(&MidiInputAction::SelectMode { mode: 2 })
);
assert_eq!(
mapping.output_bindings[rule.feedback[0]].kind.source(),
Some(&MidiValueSource::ModeActive { mode: 2 })
);
}
}