gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Turns a mapping's binding lists into rules: per control and mode the
//! press, the release, and the lights, motor faders and meters that show
//! what they change.

use gled_model::asset::midi_controller::MidiControllerMapping;

use super::controls::{Control, control_index, output_control};

/// What one control does in one mode (or in all of them), with the
/// bindings that make it up, by their index in the mapping's lists.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Rule {
    /// The control, by its index in `controls_of`.
    pub(crate) control: usize,
    pub(crate) mode: Option<u8>,
    /// The input binding on the control's press or movement.
    pub(crate) press: Option<usize>,
    /// The input binding on its release (Note Off), the same action for a
    /// rule that works while held.
    pub(crate) release: Option<usize>,
    /// The output bindings showing it: lights, motor fader, meter.
    pub(crate) feedback: Vec<usize>,
}

impl Rule {
    fn new(control: usize, mode: Option<u8>) -> Self {
        Self {
            control,
            mode,
            press: None,
            release: None,
            feedback: Vec::new(),
        }
    }

    /// Whether the rule acts on press and undoes it on release, as a scene
    /// flashed while its button is held.
    pub(crate) fn while_held(&self, mapping: &MidiControllerMapping) -> bool {
        match (self.press, self.release) {
            (Some(press), Some(release)) => {
                mapping.input_bindings[press].action == mapping.input_bindings[release].action
            }
            _ => false,
        }
    }
}

/// The rules of `mapping`, in the order their first binding appears. Each
/// binding belongs to exactly one rule.
pub(crate) fn build_rules(mapping: &MidiControllerMapping, controls: &[Control]) -> Vec<Rule> {
    let inputs = &mapping.input_bindings;
    let mut rules: Vec<Rule> = Vec::new();

    for (index, binding) in inputs.iter().enumerate() {
        let Some(control) = control_index(controls, &binding.trigger) else {
            continue;
        };
        let same_action = |other: Option<usize>| {
            other.is_some_and(|other| inputs[other].action == binding.action)
        };
        if binding.trigger.status & 0xF0 == 0x80 {
            let pair = rules.iter_mut().find(|rule| {
                rule.control == control
                    && rule.mode == binding.mode
                    && rule.release.is_none()
                    && same_action(rule.press)
            });
            match pair {
                Some(rule) => rule.release = Some(index),
                None => rules.push(Rule {
                    release: Some(index),
                    ..Rule::new(control, binding.mode)
                }),
            }
        } else {
            let pair = rules.iter_mut().find(|rule| {
                rule.control == control
                    && rule.mode == binding.mode
                    && rule.press.is_none()
                    && same_action(rule.release)
            });
            match pair {
                Some(rule) => rule.press = Some(index),
                None => rules.push(Rule {
                    press: Some(index),
                    ..Rule::new(control, binding.mode)
                }),
            }
        }
    }

    for (index, binding) in mapping.output_bindings.iter().enumerate() {
        let Some(control) = output_control(controls, &binding.kind) else {
            continue;
        };
        let rule = rules
            .iter_mut()
            .find(|rule| rule.control == control && rule.mode == binding.mode);
        match rule {
            Some(rule) => rule.feedback.push(index),
            None => rules.push(Rule {
                feedback: vec![index],
                ..Rule::new(control, binding.mode)
            }),
        }
    }

    rules
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ui::windows::midi_controllers::rules::{
        controls::controls_of,
        test_mappings::{apc40, p1x},
    };

    fn assert_each_binding_in_one_rule(mapping: &MidiControllerMapping) {
        let controls = controls_of(mapping);
        let rules = build_rules(mapping, &controls);
        let mut inputs = vec![0; mapping.input_bindings.len()];
        let mut outputs = vec![0; mapping.output_bindings.len()];
        for rule in &rules {
            for index in rule.press.iter().chain(&rule.release) {
                inputs[*index] += 1;
            }
            for index in &rule.feedback {
                outputs[*index] += 1;
            }
        }
        assert!(inputs.iter().all(|count| *count == 1), "inputs {inputs:?}");
        assert!(
            outputs.iter().all(|count| *count == 1),
            "outputs {outputs:?}"
        );
    }

    #[test]
    fn every_binding_of_the_fixtures_belongs_to_one_rule() {
        assert_each_binding_in_one_rule(&p1x());
        assert_each_binding_in_one_rule(&apc40());
    }

    fn rule_of<'a>(
        rules: &'a [Rule],
        controls: &[Control],
        name: &str,
        mode: Option<u8>,
    ) -> &'a Rule {
        rules
            .iter()
            .find(|rule| controls[rule.control].name == name && rule.mode == mode)
            .unwrap_or_else(|| panic!("no rule for {name} in {mode:?}"))
    }

    #[test]
    fn a_flash_on_press_and_release_is_one_rule_while_held() {
        let mapping = p1x();
        let controls = controls_of(&mapping);
        let rules = build_rules(&mapping, &controls);
        let rule = rule_of(&rules, &controls, "S 1", Some(0));
        assert!(rule.while_held(&mapping));
        assert_eq!(rule.feedback.len(), 1);
    }

    #[test]
    fn a_fader_rule_holds_its_motor_fader_and_meter() {
        let mapping = p1x();
        let controls = controls_of(&mapping);
        let rules = build_rules(&mapping, &controls);
        let rule = rule_of(&rules, &controls, "Fader 3", Some(1));
        assert!(rule.press.is_some());
        assert_eq!(rule.feedback.len(), 2);
    }

    #[test]
    fn a_pad_bound_to_its_release_is_a_rule_on_release() {
        let mapping = apc40();
        let controls = controls_of(&mapping);
        let rules = build_rules(&mapping, &controls);
        let rule = rule_of(&rules, &controls, "Note 32", None);
        assert_eq!(rule.press, None);
        assert!(rule.release.is_some());
        assert_eq!(rule.feedback.len(), 1);
    }

    #[test]
    fn the_p1x_has_one_rule_per_control_and_mode() {
        let mapping = p1x();
        let controls = controls_of(&mapping);
        let rules = build_rules(&mapping, &controls);
        // Every mode: 4 encoder pushes, encoders 3 and 4. Rows: encoder 1
        // and 8 × (fader, select, S, rec). Animations: encoders 1 and 2,
        // 8 faders and 8 selects.
        assert_eq!(rules.len(), 6 + 33 + 18);
    }
}