use gled_model::asset::midi_controller::{
MidiControllerMapping, MidiInputAction, MidiModeHighlight, MidiNamedSceneColorMapping,
MidiOutputBindingKind, MidiSceneColorMessage, MidiSceneColorMessageMap, MidiTrigger,
MidiValueSource,
};
use super::rules::{Control, FeedbackRole, Members, Numbering, Numbers, Rule, Slot, edit};
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct SlotNumbering(pub(crate) Vec<(Slot, Numbering)>);
impl Numbers for SlotNumbering {
fn numbering(&self, slot: Slot) -> Numbering {
self.0
.iter()
.find(|(numbered, _)| *numbered == slot)
.map_or(Numbering::Same, |(_, numbering)| *numbering)
}
}
#[derive(Debug, Clone)]
pub(crate) enum Intent {
SetAction {
members: Members,
action: MidiInputAction,
numbering: SlotNumbering,
},
RemoveAction(Members),
RemoveRules(Members),
SetWhileHeld {
members: Members,
held: bool,
},
SetMode {
members: Members,
mode: Option<u8>,
},
SetTouch {
members: Members,
touch: Option<MidiTrigger>,
},
SetSource {
members: Members,
position: usize,
source: MidiValueSource,
numbering: SlotNumbering,
},
SetFeedbackKind {
output: usize,
kind: MidiOutputBindingKind,
},
AddFeedback {
members: Members,
role: FeedbackRole,
},
RemoveFeedback {
members: Members,
position: usize,
},
Copy {
rule: Rule,
n: i32,
targets: Vec<(usize, i32)>,
new_controls: Vec<(MidiTrigger, i32)>,
numbering: SlotNumbering,
},
RenameControl {
control: usize,
name: String,
},
AddMode(String),
RenameMode {
mode: u8,
name: String,
},
SetHighlight {
mode: u8,
highlight: Option<MidiModeHighlight>,
},
MoveMode {
from: u8,
to: u8,
},
RemoveMode(u8),
}
pub(crate) fn apply(
intent: Intent,
mapping: &mut MidiControllerMapping,
controls: &mut Vec<Control>,
) -> Option<String> {
match intent {
Intent::SetAction {
members,
action,
numbering,
} => {
let notes = edit::set_action(mapping, controls, &members, &action, &numbering);
(!notes.is_empty()).then(|| notes.join("; "))
}
Intent::RemoveAction(members) => {
edit::remove_action(mapping, &members);
Some(removed("action", members.rules.len()))
}
Intent::RemoveRules(members) => {
edit::remove_rules(mapping, &members);
Some(removed("rule", members.rules.len()))
}
Intent::SetWhileHeld { members, held } => {
edit::set_while_held(mapping, controls, &members, held);
None
}
Intent::SetMode { members, mode } => {
edit::set_mode(mapping, &members, mode);
None
}
Intent::SetTouch { members, touch } => {
edit::set_touch(mapping, &members, touch);
None
}
Intent::SetSource {
members,
position,
source,
numbering,
} => {
edit::set_source(mapping, controls, &members, position, &source, &numbering);
None
}
Intent::SetFeedbackKind { output, kind } => {
if let Some(binding) = mapping.output_bindings.get_mut(output) {
binding.kind = kind;
}
None
}
Intent::AddFeedback { members, role } => {
if role == FeedbackRole::PadColor && mapping.color_mappings.is_empty() {
mapping.color_mappings.push(default_colors());
}
let mapping_name = mapping
.color_mappings
.first()
.map(|color| color.name.clone())
.unwrap_or_default();
let followed = edit::add_feedback(mapping, controls, &members, role, &mapping_name);
(!followed).then(|| {
format!(
"The new {} shows the selected scene's opacity; click it to choose what it shows",
role.name().to_lowercase()
)
})
}
Intent::RemoveFeedback { members, position } => {
edit::remove_feedback(mapping, &members, position);
None
}
Intent::Copy {
rule,
n,
mut targets,
new_controls,
numbering,
} => {
for (trigger, number) in new_controls {
let control = super::learning::with_control(controls, &trigger);
if !targets.iter().any(|(target, _)| *target == control) {
targets.push((control, number));
}
}
let count = targets.len();
let replaced = edit::copy_rule(mapping, controls, &rule, n, &targets, &numbering);
Some(match replaced {
0 => format!("Copied to {count} controls"),
replaced => format!("Copied to {count} controls, replacing {replaced} rules"),
})
}
Intent::RenameControl { control, name } => {
edit::rename_control(mapping, controls, control, &name);
None
}
Intent::AddMode(name) => {
edit::add_mode(mapping, &name);
None
}
Intent::RenameMode { mode, name } => {
edit::rename_mode(mapping, controls, mode, &name);
None
}
Intent::SetHighlight { mode, highlight } => {
if let Some(entry) = mapping.modes.get_mut(usize::from(mode)) {
entry.highlight = highlight;
}
None
}
Intent::MoveMode { from, to } => {
edit::move_mode(mapping, from, to);
None
}
Intent::RemoveMode(mode) => match edit::remove_mode(mapping, mode) {
Ok(()) => None,
Err(in_use) => Some(format!(
"This mode is still used by {} bindings; move or delete them first",
in_use.bindings
)),
},
}
}
fn removed(what: &str, count: usize) -> String {
if count == 1 {
format!("Removed 1 {what} — Ctrl+Z brings it back")
} else {
format!("Removed {count} {what}s — Ctrl+Z brings them back")
}
}
fn default_colors() -> MidiNamedSceneColorMapping {
let bright = MidiSceneColorMessageMap::default();
let value = |value| MidiSceneColorMessage { value };
let dim = MidiSceneColorMessageMap {
red: value(7),
green: value(23),
blue: value(47),
white: value(1),
orange: value(11),
yellow: value(15),
purple: value(51),
pink: value(55),
black: value(0),
};
MidiNamedSceneColorMapping {
name: "Colors".to_owned(),
active: bright.clone(),
inactive: dim,
flashed: bright,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::windows::midi_controllers::rules::{build_rules, controls_of, find_groups};
use gled_model::asset::midi_controller::{MidiInputBinding, MidiSceneTarget};
#[test]
fn a_pad_copied_to_the_next_notes_makes_a_row_of_pads() {
let mut mapping = MidiControllerMapping {
input_bindings: vec![MidiInputBinding {
trigger: MidiTrigger {
status: 0x90,
data1: 32,
match_data1: true,
},
action: MidiInputAction::ToggleSceneActive {
target: MidiSceneTarget::Grid { row: 0, col: 0 },
},
..Default::default()
}],
..Default::default()
};
let mut controls = controls_of(&mapping);
let rule = build_rules(&mapping, &controls)[0].clone();
apply(
Intent::AddFeedback {
members: Members::single(rule),
role: FeedbackRole::PadColor,
},
&mut mapping,
&mut controls,
);
assert_eq!(mapping.color_mappings.len(), 1);
let rule = build_rules(&mapping, &controls)[0].clone();
let new_controls = (33..40)
.zip(2..)
.map(|(note, n)| {
let trigger = MidiTrigger {
status: 0x90,
data1: note,
match_data1: true,
};
(trigger, n)
})
.collect();
apply(
Intent::Copy {
rule,
n: 1,
targets: Vec::new(),
new_controls,
numbering: SlotNumbering(vec![
(Slot::Row, Numbering::Same),
(Slot::Column, Numbering::Counting { offset: 0 }),
]),
},
&mut mapping,
&mut controls,
);
let controls = controls_of(&mapping);
let rules = build_rules(&mapping, &controls);
assert_eq!(rules.len(), 8);
let last = rules
.iter()
.find(|rule| controls[rule.control].trigger.data1 == 39)
.unwrap_or_else(|| panic!("no rule for note 39"));
let action = &mapping.input_bindings[last.press.unwrap_or_default()].action;
assert_eq!(
*action,
MidiInputAction::ToggleSceneActive {
target: MidiSceneTarget::Grid { row: 0, col: 7 }
}
);
let pad = &mapping.output_bindings[last.feedback[0]].kind;
let MidiOutputBindingKind::SceneColorValue(color) = pad else {
panic!("no pad color: {pad:?}");
};
assert_eq!(
color.source,
gled_model::asset::midi_controller::MidiColorSource::SceneColor {
target: MidiSceneTarget::Grid { row: 0, col: 7 }
}
);
assert_eq!(color.data1, 39);
assert_eq!(find_groups(&mapping, &controls, &rules).len(), 1);
}
}