gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The physical controls of a mapping: its stored list, completed with a
//! control for every message a binding uses that the list lacks, so older
//! mappings, saved before controls had names, get named controls too.

use gled_model::asset::midi_controller::{
    MidiControl, MidiControllerMapping, MidiInputAction, MidiOutputBindingKind, MidiTrigger,
};

/// What kind of thing a control is, which decides the actions and
/// feedback suggested for it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ControlKind {
    Button,
    /// A button that shows scene colors.
    Pad,
    Fader,
    Knob,
    Encoder,
}

/// A control with its kind.
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct Control {
    pub(crate) name: String,
    pub(crate) trigger: MidiTrigger,
    pub(crate) kind: ControlKind,
    /// Named after its message ("Note 24"): neither stored nor named by
    /// its bindings.
    pub(crate) named_by_message: bool,
}

/// The mapping's controls: the stored ones first, then one for each
/// message a binding uses that no stored control sends.
pub(crate) fn controls_of(mapping: &MidiControllerMapping) -> Vec<Control> {
    let mut controls: Vec<Control> = mapping
        .controls
        .iter()
        .map(|control| Control {
            name: control.name.clone(),
            trigger: control.trigger.clone(),
            kind: ControlKind::Button,
            named_by_message: false,
        })
        .collect();

    for binding in &mapping.input_bindings {
        let trigger = press_trigger(&binding.trigger);
        if control_index(&controls, &trigger).is_none() {
            controls.push(derived_control(&binding.name, trigger));
        }
    }
    for binding in &mapping.output_bindings {
        let Some(trigger) = output_trigger(&binding.kind) else {
            continue;
        };
        if output_control(&controls, &binding.kind).is_none() {
            controls.push(derived_control(&binding.name, trigger));
        }
    }

    for control in &mut controls {
        control.kind = guess_kind(mapping, &control.trigger);
    }
    controls
}

/// The control a message belongs to: the one sending it, or the button it
/// releases.
pub(crate) fn control_index(controls: &[Control], trigger: &MidiTrigger) -> Option<usize> {
    let press = press_trigger(trigger);
    controls
        .iter()
        .position(|control| same_message(&control.trigger, &press))
}

/// The control whose light, motor fader or meter `kind` drives: the one
/// sending the output's message, or for a meter the fader of its strip.
pub(crate) fn output_control(controls: &[Control], kind: &MidiOutputBindingKind) -> Option<usize> {
    match kind {
        MidiOutputBindingKind::Meter(meter) => controls
            .iter()
            .position(|control| control.trigger.status == 0xE0 | (meter.strip & 0x07)),
        _ => output_trigger(kind).and_then(|trigger| control_index(controls, &trigger)),
    }
}

/// The message a value or color output sends, as a trigger; a meter has
/// none of its own.
pub(crate) fn output_trigger(kind: &MidiOutputBindingKind) -> Option<MidiTrigger> {
    let (status, data1) = match kind {
        MidiOutputBindingKind::Value(value) => (value.status, value.data1),
        MidiOutputBindingKind::SceneColorValue(color) => (color.status, color.data1),
        MidiOutputBindingKind::Meter(meter) => (0xE0 | (meter.strip & 0x07), 0),
    };
    Some(MidiTrigger {
        status,
        data1,
        match_data1: match_data1_for_status(status),
    })
}

/// A Note Off as the Note On of its note, so a button bound to its release
/// is the same control as one bound to its press.
pub(crate) fn press_trigger(trigger: &MidiTrigger) -> MidiTrigger {
    if trigger.status & 0xF0 == 0x80 {
        MidiTrigger {
            status: 0x90 | (trigger.status & 0x0F),
            ..trigger.clone()
        }
    } else {
        trigger.clone()
    }
}

/// Whether messages of `status` tell controls apart by their first data
/// byte; pitch bend, channel pressure and program change do not.
pub(crate) fn match_data1_for_status(status: u8) -> bool {
    !matches!(status & 0xF0, 0xC0 | 0xD0 | 0xE0)
}

fn same_message(a: &MidiTrigger, b: &MidiTrigger) -> bool {
    a.status == b.status && (!a.match_data1 || !b.match_data1 || a.data1 == b.data1)
}

/// A control for `trigger` named by the binding name `binding_name`.
fn derived_control(binding_name: &str, trigger: MidiTrigger) -> Control {
    let message = message_name(&trigger);
    let name = name_prefix(binding_name).unwrap_or_else(|| message.clone());
    Control {
        named_by_message: name == message,
        name,
        trigger,
        kind: ControlKind::Button,
    }
}

/// The control part of a binding name such as "Fader 1: scene 1 opacity",
/// "S 1 release: end flash" or "Rec 1 light: whether โ€ฆ", as the window
/// names bindings.
fn name_prefix(name: &str) -> Option<String> {
    const PARTS: [&str; 7] = [
        " release",
        " light",
        " motor fader",
        " meter",
        " ring",
        " pad color",
        " value",
    ];
    let (prefix, _) = name.split_once(':')?;
    let prefix = prefix.trim();
    let prefix = PARTS
        .iter()
        .find_map(|part| prefix.strip_suffix(part))
        .unwrap_or(prefix)
        .trim_end();
    (!prefix.is_empty()).then(|| prefix.to_owned())
}

/// A name for a control from its message, such as "Note 24" or
/// "Pitch bend ch 3".
pub(crate) fn message_name(trigger: &MidiTrigger) -> String {
    let channel = (trigger.status & 0x0F) + 1;
    let on_channel = |name: String| {
        if channel == 1 {
            name
        } else {
            format!("{name} ยท ch {channel}")
        }
    };
    match trigger.status & 0xF0 {
        0x80 | 0x90 => on_channel(format!("Note {}", trigger.data1)),
        0xA0 => on_channel(format!("Aftertouch {}", trigger.data1)),
        0xB0 => on_channel(format!("CC {}", trigger.data1)),
        0xC0 => format!("Program change ch {channel}"),
        0xD0 => format!("Pressure ch {channel}"),
        0xE0 => format!("Pitch bend ch {channel}"),
        _ => format!("Message {}", trigger.status),
    }
}

fn guess_kind(mapping: &MidiControllerMapping, trigger: &MidiTrigger) -> ControlKind {
    match trigger.status & 0xF0 {
        0xE0 => ControlKind::Fader,
        0xB0 => {
            let relative = mapping.input_bindings.iter().any(|binding| {
                same_message(&binding.trigger, trigger) && relative_action(&binding.action)
            });
            if relative {
                ControlKind::Encoder
            } else {
                ControlKind::Knob
            }
        }
        _ => {
            let shows_colors = mapping.output_bindings.iter().any(|binding| {
                matches!(binding.kind, MidiOutputBindingKind::SceneColorValue(_))
                    && output_trigger(&binding.kind)
                        .is_some_and(|output| same_message(&press_trigger(&output), trigger))
            });
            if shows_colors {
                ControlKind::Pad
            } else {
                ControlKind::Button
            }
        }
    }
}

/// Whether an action reads an endless encoder's turns.
pub(crate) fn relative_action(action: &MidiInputAction) -> bool {
    matches!(
        action,
        MidiInputAction::SetMainDimmerRelative { .. }
            | MidiInputAction::SetSpeedRelative { .. }
            | MidiInputAction::SelectSceneRelative { .. }
            | MidiInputAction::SelectSceneRow { .. }
            | MidiInputAction::SelectSceneColumn { .. }
            | MidiInputAction::SelectSceneEffectRelative { .. }
    )
}

/// The stored control for `control`, to save a renamed or learned one.
pub(crate) fn stored(control: &Control) -> MidiControl {
    MidiControl {
        name: control.name.clone(),
        trigger: control.trigger.clone(),
    }
}

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

    fn named<'a>(controls: &'a [Control], name: &str) -> &'a Control {
        controls
            .iter()
            .find(|control| control.name == name)
            .unwrap_or_else(|| panic!("no control {name}"))
    }

    #[test]
    fn old_mappings_name_their_controls_from_the_binding_names() {
        let controls = controls_of(&p1x());
        assert_eq!(named(&controls, "Fader 3").trigger.status, 0xE2);
        assert_eq!(named(&controls, "Fader 3").kind, ControlKind::Fader);
        assert_eq!(named(&controls, "Encoder 1").kind, ControlKind::Encoder);
        assert_eq!(named(&controls, "S 1").kind, ControlKind::Button);
        assert_eq!(named(&controls, "Encoder 1 push").trigger.data1, 32);
    }

    /// The P1-X has 4 encoders with a push, and 8 strips of a fader and
    /// three buttons; every binding belongs to one of those 40 controls.
    #[test]
    fn the_p1x_has_one_control_per_physical_control() {
        let controls = controls_of(&p1x());
        assert_eq!(
            controls.len(),
            40,
            "{:#?}",
            controls.iter().map(|c| &c.name).collect::<Vec<_>>()
        );
    }

    #[test]
    fn a_button_bound_to_its_release_is_one_control() {
        let controls = controls_of(&p1x());
        assert_eq!(
            controls
                .iter()
                .filter(|control| control.name == "S 1")
                .count(),
            1
        );
        assert!(controls.iter().all(|control| control.name != "S 1 release"));
    }

    #[test]
    fn controls_without_named_bindings_are_named_by_their_message() {
        let controls = controls_of(&apc40());
        assert!(controls.iter().any(|control| control.name == "Note 32"));
        assert_eq!(named(&controls, "Note 32").kind, ControlKind::Pad);
        assert!(controls.iter().any(|control| control.name == "CC 7 ยท ch 2"));
    }

    #[test]
    fn stored_controls_keep_their_names() {
        let mut mapping = p1x();
        mapping.controls = vec![MidiControl {
            name: "Strip 3 fader".to_owned(),
            trigger: MidiTrigger {
                status: 0xE2,
                data1: 0,
                match_data1: false,
            },
        }];
        let controls = controls_of(&mapping);
        assert_eq!(controls[0].name, "Strip 3 fader");
        assert!(controls.iter().all(|control| control.name != "Fader 3"));
    }
}