gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Copying a rule to other controls, counting its numbers up with theirs
//! ("Select 5–8 select scene nāˆ’4"), and making a button switch modes.

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},
};

/// Copies `rule`, whose control has number `n`, to each target control
/// with its number, replacing what they did in the rule's mode. Returns
/// how many rules were replaced.
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()
}

/// `kind` sent to the control `to` instead of `from`.
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)
}

/// Makes control `control` the one sending `trigger`: its rules, release
/// and lights move to the new message.
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);
}

/// Makes `control` switch the controller to `mode` in every mode, with a
/// light showing when the mode is active. Returns what it did before.
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:?}"))
    }

    /// Sheet C, task 2: Select 5–8 in Animations jump to the scenes in
    /// columns 1–4 of the selected row, their lights following.
    #[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 })
        );
    }

    /// Sheet A, task 3: Encoder 4's push switches to a new mode instead of
    /// tapping the tempo, and its light shows the mode.
    #[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 })
        );
    }
}