gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Editing a mapping's modes and its named controls.

use gled_model::asset::midi_controller::{
    MidiControllerMapping, MidiInputAction, MidiMode, MidiValueSource,
};

use super::store_controls;
use crate::ui::windows::midi_controllers::rules::{
    controls::{Control, control_index},
    naming::name_rules_of,
};

/// Adds a mode named `name`; returns its index.
pub(crate) fn add_mode(mapping: &mut MidiControllerMapping, name: &str) -> u8 {
    mapping.modes.push(MidiMode {
        name: name.to_owned(),
        highlight: None,
    });
    u8::try_from(mapping.modes.len() - 1).unwrap_or(u8::MAX)
}

/// Renames mode `mode`, and the bindings whose names tell it.
pub(crate) fn rename_mode(
    mapping: &mut MidiControllerMapping,
    controls: &[Control],
    mode: u8,
    name: &str,
) {
    let Some(entry) = mapping.modes.get_mut(usize::from(mode)) else {
        return;
    };
    entry.name = name.to_owned();
    let switches: Vec<(usize, Option<u8>)> = mapping
        .input_bindings
        .iter()
        .filter(|binding| matches!(binding.action, MidiInputAction::SelectMode { mode: switched } if switched == mode))
        .filter_map(|binding| control_index(controls, &binding.trigger).map(|control| (control, binding.mode)))
        .collect();
    name_rules_of(mapping, controls, &switches);
}

/// Moves mode `from` to position `to`; bindings keep their mode.
pub(crate) fn move_mode(mapping: &mut MidiControllerMapping, from: u8, to: u8) {
    let count = mapping.modes.len();
    if usize::from(from) >= count || usize::from(to) >= count || from == to {
        return;
    }
    let mode = mapping.modes.remove(usize::from(from));
    mapping.modes.insert(usize::from(to), mode);
    let moved = |index: u8| {
        if index == from {
            to
        } else if from < to && index > from && index <= to {
            index - 1
        } else if to < from && index >= to && index < from {
            index + 1
        } else {
            index
        }
    };
    for binding in &mut mapping.input_bindings {
        binding.mode = binding.mode.map(moved);
        if let MidiInputAction::SelectMode { mode } = &mut binding.action {
            *mode = moved(*mode);
        }
    }
    for binding in &mut mapping.output_bindings {
        binding.mode = binding.mode.map(moved);
        if let Some(MidiValueSource::ModeActive { mode }) = binding.kind.source_mut() {
            *mode = moved(*mode);
        }
    }
}

/// Why a mode cannot be removed: rules work in it, or buttons switch to it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct ModeInUse {
    pub(crate) bindings: usize,
}

/// Removes mode `mode` if nothing uses it.
pub(crate) fn remove_mode(mapping: &mut MidiControllerMapping, mode: u8) -> Result<(), ModeInUse> {
    if mapping.remove_unused_mode(mode) {
        Ok(())
    } else {
        Err(ModeInUse {
            bindings: mapping.mode_uses(mode),
        })
    }
}

/// Renames control `control`, and the bindings named after it.
pub(crate) fn rename_control(
    mapping: &mut MidiControllerMapping,
    controls: &mut [Control],
    control: usize,
    name: &str,
) {
    let Some(entry) = controls.get_mut(control) else {
        return;
    };
    entry.name = name.trim().to_owned();
    entry.named_by_message = false;
    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);
}

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

    #[test]
    fn a_used_mode_stays() {
        let mut mapping = p1x();
        assert!(remove_mode(&mut mapping, 0).is_err());
        assert_eq!(mapping.modes.len(), 2);
        let grid = add_mode(&mut mapping, "Grid");
        assert_eq!(remove_mode(&mut mapping, grid), Ok(()));
    }

    #[test]
    fn moving_a_mode_keeps_what_works_in_it() {
        let mut mapping = p1x();
        let controls = controls_of(&mapping);
        let before = build_rules(&mapping, &controls)
            .iter()
            .filter(|rule| rule.mode == Some(1))
            .count();
        move_mode(&mut mapping, 1, 0);
        assert_eq!(mapping.modes[0].name, "Animations");
        let after = build_rules(&mapping, &controls)
            .iter()
            .filter(|rule| rule.mode == Some(0))
            .count();
        assert_eq!(before, after);
        assert!(
            mapping
                .input_bindings
                .iter()
                .any(|binding| binding.name.starts_with("Encoder 2 push")
                    && binding.action == MidiInputAction::SelectMode { mode: 0 })
        );
    }

    #[test]
    fn a_renamed_control_keeps_its_name_and_renames_its_bindings() {
        let mut mapping = p1x();
        let mut controls = controls_of(&mapping);
        let fader = controls
            .iter()
            .position(|control| control.name == "Fader 3")
            .unwrap_or_else(|| panic!("no Fader 3"));
        rename_control(&mut mapping, &mut controls, fader, "Strip 3");
        assert!(
            mapping
                .controls
                .iter()
                .any(|control| control.name == "Strip 3")
        );
        assert!(
            mapping
                .input_bindings
                .iter()
                .any(|binding| binding.name.starts_with("Strip 3:"))
        );
        assert!(
            controls_of(&mapping)
                .iter()
                .any(|control| control.name == "Strip 3")
        );
    }
}