Skip to main content

gled_model/asset/midi_controller/
mode.rs

1//! A mapping's modes: named sets of bindings a controller switches between,
2//! such as a P1-X that works on a grid row or on the selected scene's
3//! effects. A binding without a mode works in all of them.
4
5use serde::{Deserialize, Serialize};
6use strum::{Display, EnumIter};
7
8use super::{MidiControllerMapping, MidiInputAction, MidiValueSource};
9
10#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
11pub struct MidiMode {
12    pub name: String,
13    /// The part of the interface that lights up briefly when the controller
14    /// switches to this mode, so the player sees what it works on now.
15    #[serde(default)]
16    pub highlight: Option<MidiModeHighlight>,
17}
18
19/// A part of the interface a mode works on. It reads as the label the
20/// controller window shows.
21#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Display, EnumIter)]
22pub enum MidiModeHighlight {
23    #[strum(serialize = "Scene Grid")]
24    SceneGrid,
25    #[strum(serialize = "Scene Settings")]
26    SceneSettings,
27}
28
29/// Whether a binding of `binding_mode` works while `active` is the mode.
30pub fn binding_in_mode(binding_mode: Option<u8>, active: u8) -> bool {
31    binding_mode.is_none_or(|mode| mode == active)
32}
33
34impl MidiControllerMapping {
35    /// How many bindings belong to mode `mode` or switch to or show it.
36    pub fn mode_uses(&self, mode: u8) -> usize {
37        let inputs = self.input_bindings.iter().filter(|binding| {
38            binding.mode == Some(mode)
39                || matches!(binding.action, MidiInputAction::SelectMode { mode: selected } if selected == mode)
40        });
41        let outputs = self.output_bindings.iter().filter(|binding| {
42            binding.mode == Some(mode)
43                || binding
44                    .kind
45                    .source()
46                    .is_some_and(|source| matches!(source, MidiValueSource::ModeActive { mode: shown } if *shown == mode))
47        });
48        inputs.count() + outputs.count()
49    }
50
51    /// Removes mode `mode` if no binding uses it; the modes after it move
52    /// down by one, in the bindings that name them too.
53    pub fn remove_unused_mode(&mut self, mode: u8) -> bool {
54        if usize::from(mode) >= self.modes.len() || self.mode_uses(mode) > 0 {
55            return false;
56        }
57        self.modes.remove(usize::from(mode));
58        let shift = |index: &mut u8| {
59            if *index > mode {
60                *index -= 1;
61            }
62        };
63        for binding in &mut self.input_bindings {
64            if let Some(index) = binding.mode.as_mut() {
65                shift(index);
66            }
67            if let MidiInputAction::SelectMode { mode: index } = &mut binding.action {
68                shift(index);
69            }
70        }
71        for binding in &mut self.output_bindings {
72            if let Some(index) = binding.mode.as_mut() {
73                shift(index);
74            }
75            if let Some(MidiValueSource::ModeActive { mode: index }) = binding.kind.source_mut() {
76                shift(index);
77            }
78        }
79        true
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86    use crate::asset::midi_controller::{
87        MidiInputBinding, MidiOutputBinding, MidiOutputBindingKind, MidiValueOutput,
88    };
89
90    fn mapping() -> MidiControllerMapping {
91        MidiControllerMapping {
92            modes: ["Rows", "Spare", "Animations"]
93                .map(|name| MidiMode {
94                    name: name.to_owned(),
95                    highlight: None,
96                })
97                .to_vec(),
98            input_bindings: vec![
99                MidiInputBinding {
100                    mode: Some(2),
101                    ..Default::default()
102                },
103                MidiInputBinding {
104                    action: MidiInputAction::SelectMode { mode: 2 },
105                    ..Default::default()
106                },
107            ],
108            output_bindings: vec![MidiOutputBinding {
109                kind: MidiOutputBindingKind::Value(MidiValueOutput {
110                    source: MidiValueSource::ModeActive { mode: 2 },
111                    ..Default::default()
112                }),
113                ..Default::default()
114            }],
115            ..Default::default()
116        }
117    }
118
119    #[test]
120    fn a_binding_without_a_mode_works_in_every_mode() {
121        assert!(binding_in_mode(None, 3));
122        assert!(binding_in_mode(Some(1), 1));
123        assert!(!binding_in_mode(Some(1), 0));
124    }
125
126    #[test]
127    fn removing_a_mode_moves_the_later_ones_down() {
128        let mut mapping = mapping();
129        assert!(mapping.remove_unused_mode(1));
130        let names: Vec<&str> = mapping
131            .modes
132            .iter()
133            .map(|mode| mode.name.as_str())
134            .collect();
135        assert_eq!(names, ["Rows", "Animations"]);
136        assert_eq!(mapping.input_bindings[0].mode, Some(1));
137        assert_eq!(
138            mapping.input_bindings[1].action,
139            MidiInputAction::SelectMode { mode: 1 }
140        );
141        assert_eq!(
142            mapping.output_bindings[0].kind.source(),
143            Some(&MidiValueSource::ModeActive { mode: 1 })
144        );
145    }
146
147    #[test]
148    fn a_mode_in_use_stays() {
149        let mut mapping = mapping();
150        assert_eq!(mapping.mode_uses(2), 3);
151        assert!(!mapping.remove_unused_mode(2));
152        assert_eq!(mapping.modes.len(), 3);
153    }
154}