gled-model 2.34.0

The data model of gled: projects, scenes, effects, animations, curves and assets
Documentation
//! A mapping's modes: named sets of bindings a controller switches between,
//! such as a P1-X that works on a grid row or on the selected scene's
//! effects. A binding without a mode works in all of them.

use serde::{Deserialize, Serialize};
use strum::{Display, EnumIter};

use super::{MidiControllerMapping, MidiInputAction, MidiValueSource};

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
pub struct MidiMode {
    pub name: String,
    /// The part of the interface that lights up briefly when the controller
    /// switches to this mode, so the player sees what it works on now.
    #[serde(default)]
    pub highlight: Option<MidiModeHighlight>,
}

/// A part of the interface a mode works on. It reads as the label the
/// controller window shows.
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Display, EnumIter)]
pub enum MidiModeHighlight {
    #[strum(serialize = "Scene Grid")]
    SceneGrid,
    #[strum(serialize = "Scene Settings")]
    SceneSettings,
}

/// Whether a binding of `binding_mode` works while `active` is the mode.
pub fn binding_in_mode(binding_mode: Option<u8>, active: u8) -> bool {
    binding_mode.is_none_or(|mode| mode == active)
}

impl MidiControllerMapping {
    /// How many bindings belong to mode `mode` or switch to or show it.
    pub fn mode_uses(&self, mode: u8) -> usize {
        let inputs = self.input_bindings.iter().filter(|binding| {
            binding.mode == Some(mode)
                || matches!(binding.action, MidiInputAction::SelectMode { mode: selected } if selected == mode)
        });
        let outputs = self.output_bindings.iter().filter(|binding| {
            binding.mode == Some(mode)
                || binding
                    .kind
                    .source()
                    .is_some_and(|source| matches!(source, MidiValueSource::ModeActive { mode: shown } if *shown == mode))
        });
        inputs.count() + outputs.count()
    }

    /// Removes mode `mode` if no binding uses it; the modes after it move
    /// down by one, in the bindings that name them too.
    pub fn remove_unused_mode(&mut self, mode: u8) -> bool {
        if usize::from(mode) >= self.modes.len() || self.mode_uses(mode) > 0 {
            return false;
        }
        self.modes.remove(usize::from(mode));
        let shift = |index: &mut u8| {
            if *index > mode {
                *index -= 1;
            }
        };
        for binding in &mut self.input_bindings {
            if let Some(index) = binding.mode.as_mut() {
                shift(index);
            }
            if let MidiInputAction::SelectMode { mode: index } = &mut binding.action {
                shift(index);
            }
        }
        for binding in &mut self.output_bindings {
            if let Some(index) = binding.mode.as_mut() {
                shift(index);
            }
            if let Some(MidiValueSource::ModeActive { mode: index }) = binding.kind.source_mut() {
                shift(index);
            }
        }
        true
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::asset::midi_controller::{
        MidiInputBinding, MidiOutputBinding, MidiOutputBindingKind, MidiValueOutput,
    };

    fn mapping() -> MidiControllerMapping {
        MidiControllerMapping {
            modes: ["Rows", "Spare", "Animations"]
                .map(|name| MidiMode {
                    name: name.to_owned(),
                    highlight: None,
                })
                .to_vec(),
            input_bindings: vec![
                MidiInputBinding {
                    mode: Some(2),
                    ..Default::default()
                },
                MidiInputBinding {
                    action: MidiInputAction::SelectMode { mode: 2 },
                    ..Default::default()
                },
            ],
            output_bindings: vec![MidiOutputBinding {
                kind: MidiOutputBindingKind::Value(MidiValueOutput {
                    source: MidiValueSource::ModeActive { mode: 2 },
                    ..Default::default()
                }),
                ..Default::default()
            }],
            ..Default::default()
        }
    }

    #[test]
    fn a_binding_without_a_mode_works_in_every_mode() {
        assert!(binding_in_mode(None, 3));
        assert!(binding_in_mode(Some(1), 1));
        assert!(!binding_in_mode(Some(1), 0));
    }

    #[test]
    fn removing_a_mode_moves_the_later_ones_down() {
        let mut mapping = mapping();
        assert!(mapping.remove_unused_mode(1));
        let names: Vec<&str> = mapping
            .modes
            .iter()
            .map(|mode| mode.name.as_str())
            .collect();
        assert_eq!(names, ["Rows", "Animations"]);
        assert_eq!(mapping.input_bindings[0].mode, Some(1));
        assert_eq!(
            mapping.input_bindings[1].action,
            MidiInputAction::SelectMode { mode: 1 }
        );
        assert_eq!(
            mapping.output_bindings[0].kind.source(),
            Some(&MidiValueSource::ModeActive { mode: 1 })
        );
    }

    #[test]
    fn a_mode_in_use_stays() {
        let mut mapping = mapping();
        assert_eq!(mapping.mode_uses(2), 3);
        assert!(!mapping.remove_unused_mode(2));
        assert_eq!(mapping.modes.len(), 3);
    }
}