gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Learning controls and following the hardware: a learned control gets
//! a rule with its action palette open, or takes a new message, or
//! switches modes; a control moved on the hardware highlights its rule.

use gled_midi::monitor::label;
use gled_model::asset::midi_controller::{MidiController, MidiControllerMapping, MidiTrigger};
use gled_protocol::midi::MidiMonitorEvent;

use super::list_state::{LearnRequest, ListState, ModeFilter};
use super::rules::{
    Control, ControlKind, Plain, Rule, action_sentence, control_index, edit, message_name, text,
};
use crate::midi_learn::{LearnState, MidiLearnRequest, MidiLearnTarget};

/// A rule being made for a learned control, until an action is picked.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Pending {
    pub(crate) trigger: MidiTrigger,
    pub(crate) mode: Option<u8>,
}

/// Arms learning for what the list asked.
pub(crate) fn start_learning(
    learn_state: &mut LearnState,
    controller_id: gled_model::asset_id::AssetId<MidiController>,
    request: LearnRequest,
    state: &ListState,
) {
    let target = match request {
        LearnRequest::NewControls => MidiLearnTarget::NewControl(match state.filter {
            ModeFilter::Mode(mode) => Some(mode),
            ModeFilter::All | ModeFilter::Everywhere => None,
        }),
        LearnRequest::Control(control) => MidiLearnTarget::Control(control),
        LearnRequest::ModeSwitch(mode) => MidiLearnTarget::ModeSwitch(mode),
    };
    learn_state.arm(MidiLearnRequest {
        controller_id,
        target,
    });
}

/// What a capture did, for the status line.
pub(crate) enum Learned {
    /// A new rule waits for its action.
    Pending(Pending),
    /// An existing rule, to show.
    Existing(usize),
    Message(String),
}

/// Takes the controls learned for `controller`.
pub(crate) fn take_captures(
    learn_state: &mut LearnState,
    controller_id: gled_model::asset_id::AssetId<MidiController>,
    mapping: &mut MidiControllerMapping,
    controls: &mut Vec<Control>,
) -> Vec<Learned> {
    let mut learned = Vec::new();
    while let Some(capture) = learn_state.pop_capture() {
        if capture.controller_id != controller_id {
            continue;
        }
        match capture.target {
            MidiLearnTarget::NewControl(mode) => match control_index(controls, &capture.trigger) {
                Some(control)
                    if super::rules::build_rules(mapping, controls)
                        .iter()
                        .any(|rule| rule.control == control && rule.mode == mode) =>
                {
                    learned.push(Learned::Existing(control));
                }
                _ => learned.push(Learned::Pending(Pending {
                    trigger: capture.trigger,
                    mode,
                })),
            },
            MidiLearnTarget::Control(control) => {
                edit::relearn_control(mapping, controls, control, capture.trigger.clone());
                learned.push(Learned::Message(format!(
                    "{} now sends {}",
                    controls
                        .get(control)
                        .map_or("The control", |control| control.name.as_str()),
                    message_name(&capture.trigger)
                )));
            }
            MidiLearnTarget::ModeSwitch(mode) => {
                let control = with_control(controls, &capture.trigger);
                let before = edit::make_mode_switch(mapping, controls, control, mode);
                let name = &controls[control].name;
                let mode_name = mapping
                    .modes
                    .get(usize::from(mode))
                    .map_or("", |mode| mode.name.as_str());
                learned.push(Learned::Message(match before {
                    Some(before) => {
                        format!("{name} now switches to {mode_name} (before it {before})")
                    }
                    None => format!("{name} now switches to {mode_name}"),
                }));
            }
        }
    }
    learned
}

/// The index of the control sending `trigger`, added if it is new.
pub(crate) fn with_control(controls: &mut Vec<Control>, trigger: &MidiTrigger) -> usize {
    if let Some(index) = control_index(controls, trigger) {
        return index;
    }
    controls.push(new_control(trigger));
    controls.len() - 1
}

/// A control learned from `trigger`, named after its message.
pub(crate) fn new_control(trigger: &MidiTrigger) -> Control {
    let kind = match trigger.status & 0xF0 {
        0xE0 => ControlKind::Fader,
        0xB0 => ControlKind::Knob,
        _ => ControlKind::Button,
    };
    Control {
        name: message_name(trigger),
        trigger: trigger.clone(),
        kind,
        named_by_message: true,
    }
}

/// What a message from the hardware does, by the control it came from;
/// `None` for messages not worth a word (releases, touches).
pub(crate) fn hardware_message(
    event: &MidiMonitorEvent,
    mapping: &MidiControllerMapping,
    controls: &[Control],
    rules: &[Rule],
    filter: ModeFilter,
) -> Option<(Option<usize>, String)> {
    if event.label != label::MIDI {
        return None;
    }
    let [status, data1, value] = match event.bytes.as_slice() {
        [status, data1, value, ..] => [*status, *data1, *value],
        _ => return None,
    };
    if status & 0xF0 == 0x80 || (status & 0xF0 == 0x90 && value == 0) {
        return None;
    }
    let trigger = MidiTrigger {
        status,
        data1,
        match_data1: true,
    };
    if mapping.input_bindings.iter().any(|binding| {
        binding
            .touch
            .as_ref()
            .is_some_and(|touch| touch.matches(status, data1))
    }) {
        return None;
    }
    let Some(control) = control_index(controls, &trigger) else {
        return Some((
            None,
            format!(
                "{} on {} does nothing yet — press Learn and move it to give it a rule",
                message_name(&trigger),
                event.port_name
            ),
        ));
    };
    let shown_mode = match filter {
        ModeFilter::Mode(mode) => Some(mode),
        ModeFilter::All | ModeFilter::Everywhere => None,
    };
    let rule = rules
        .iter()
        .filter(|rule| rule.control == control && rule.press.or(rule.release).is_some())
        .min_by_key(|rule| (rule.mode != shown_mode, rule.mode.is_some()));
    let name = &controls[control].name;
    Some((
        Some(control),
        match rule.and_then(|rule| rule.press.or(rule.release)) {
            Some(binding) => format!(
                "{name} {}",
                text(&action_sentence(
                    &mapping.input_bindings[binding].action,
                    &mapping.modes,
                    &Plain
                ))
            ),
            None => format!("{name} does nothing yet"),
        },
    ))
}