gled-core 2.33.0

The engine of gled without a user interface: inputs, timing, rendering, Art-Net/DMX output and storage
Documentation
//! What MIDI controllers did that the interfaces show: the modes they
//! switched to, with the grid line each mode marked so a mode shows its line
//! again when the controller returns to it, and the faders touched.

use std::collections::{BTreeMap, HashMap};

use gled_model::asset::midi_controller::MidiModeHighlight;
use gled_protocol::state::{GridLine, ModeSwitch, TouchedControl};

#[derive(Debug, Default)]
pub(super) struct Controllers {
    /// The mode of each controller that switched, by port key.
    modes: BTreeMap<String, u8>,
    /// The line marked in a mode when the controller left it.
    lines: HashMap<(String, u8), GridLine>,
    last_switch: Option<ModeSwitch>,
    /// The controls touched now, by the touching port and binding.
    touches: BTreeMap<String, TouchedControl>,
}

impl Controllers {
    /// Switches `port` to `mode` while `line` is marked; returns the line to
    /// mark in the new mode.
    pub(super) fn switch(
        &mut self,
        port: String,
        mode: u8,
        highlight: Option<MidiModeHighlight>,
        line: Option<GridLine>,
    ) -> Option<GridLine> {
        let number = self.last_switch.map_or(1, |switch| switch.number + 1);
        self.last_switch = Some(ModeSwitch { number, highlight });
        let previous = self.modes.insert(port.clone(), mode).unwrap_or(0);
        if previous == mode {
            return line;
        }
        match line {
            Some(line) => self.lines.insert((port.clone(), previous), line),
            None => self.lines.remove(&(port.clone(), previous)),
        };
        self.lines.get(&(port, mode)).copied()
    }

    pub(super) fn modes(&self) -> &BTreeMap<String, u8> {
        &self.modes
    }

    pub(super) fn last_switch(&self) -> Option<ModeSwitch> {
        self.last_switch
    }

    /// `touch` touches `control` now, or lets go with `None`.
    pub(super) fn touch(&mut self, touch: String, control: Option<TouchedControl>) {
        match control {
            Some(control) => self.touches.insert(touch, control),
            None => self.touches.remove(&touch),
        };
    }

    pub(super) fn touched(&self) -> Vec<TouchedControl> {
        self.touches.values().copied().collect()
    }

    /// Every controller back in its first mode and untouched, for another
    /// project. The switches keep counting, so the interfaces see the next
    /// one.
    pub(super) fn reset(&mut self) {
        self.modes.clear();
        self.lines.clear();
        self.touches.clear();
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    const PORT: &str = "iCON P1-X1:iCON P1-X1 MIDI 1";

    #[test]
    fn a_mode_shows_its_line_again_when_the_controller_returns() {
        let mut modes = Controllers::default();
        let line = modes.switch(PORT.to_owned(), 1, None, Some(GridLine::Row));
        assert_eq!(line, None);
        let line = modes.switch(PORT.to_owned(), 0, None, line);
        assert_eq!(line, Some(GridLine::Row));
        assert_eq!(modes.modes().get(PORT), Some(&0));
    }

    #[test]
    fn a_released_touch_is_gone() {
        let mut controllers = Controllers::default();
        controllers.touch("a".to_owned(), Some(TouchedControl::MainDimmer));
        controllers.touch("b".to_owned(), Some(TouchedControl::MainDimmer));
        controllers.touch("a".to_owned(), None);
        assert_eq!(controllers.touched(), [TouchedControl::MainDimmer]);
    }

    #[test]
    fn every_press_is_a_new_switch() {
        let mut modes = Controllers::default();
        let highlight = Some(MidiModeHighlight::SceneGrid);
        let line = modes.switch(PORT.to_owned(), 0, highlight, Some(GridLine::Column));
        assert_eq!(line, Some(GridLine::Column));
        modes.switch(PORT.to_owned(), 0, highlight, line);
        assert_eq!(
            modes.last_switch(),
            Some(ModeSwitch {
                number: 2,
                highlight
            })
        );
    }
}