gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! What the controller window calls each feedback source, and which
//! feedback shows what an action changes.

use gled_model::asset::midi_controller::{
    MidiEffectParameter, MidiInputAction, MidiSceneTarget, MidiValueSource, MidiValueSourceKind,
};
use strum::IntoEnumIterator;

/// Whether a source is on or off, or a level; lights suit the first,
/// motor faders and meters the second.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ValueKind {
    OnOff,
    Level,
}

/// A feedback source as the palette and the rule sentences show it.
#[derive(Debug, Clone, Copy)]
pub(crate) struct SourceInfo {
    pub(crate) kind: MidiValueSourceKind,
    pub(crate) group: &'static str,
    pub(crate) label: &'static str,
    /// The sentence after "light shows" or "meter shows"; `{@…}` is the
    /// source word, `{beat}` and `{blackout}` its settings.
    pub(crate) phrase: &'static str,
    pub(crate) value: ValueKind,
}

/// How `kind` reads in the palette and in rule sentences.
pub(crate) fn source_info(kind: MidiValueSourceKind) -> SourceInfo {
    use MidiValueSourceKind as K;
    use ValueKind::{Level, OnOff};
    let (group, label, phrase, value) = match kind {
        K::SceneActive => ("Scene", "Scene is on", "whether {target} {@is on}", OnOff),
        K::SceneFlashed => (
            "Scene",
            "Scene is flashed",
            "whether {target} {@is flashed}",
            OnOff,
        ),
        K::SceneSelected => (
            "Scene",
            "Scene is selected",
            "whether {target} {@is selected}",
            OnOff,
        ),
        K::SceneOpacity => (
            "Scene",
            "Scene opacity",
            "{@the opacity of} {target}",
            Level,
        ),
        K::SceneOpacityLive => (
            "Scene",
            "Scene opacity (live)",
            "{@the live opacity of} {target}",
            Level,
        ),
        K::SceneInputDimmer => (
            "Scene",
            "Scene input dimmer",
            "{@the input dimmer of} {target}",
            Level,
        ),
        K::SceneBeatOffset => (
            "Scene",
            "Scene beat offset",
            "{@the beat offset of} {target}",
            Level,
        ),
        K::SceneIgnoreMainDimmer => (
            "Scene",
            "Scene ignores main dimmer",
            "whether {target} {@ignores the main dimmer}",
            OnOff,
        ),
        K::SceneSetOffsetOnFlash => (
            "Scene",
            "Scene sets offset on flash",
            "whether {target} {@sets its offset on flash}",
            OnOff,
        ),
        K::SelectedSceneOpacity => (
            "Selected scene",
            "Selected scene opacity",
            "{@the opacity of the selected scene}",
            Level,
        ),
        K::SelectedSceneOpacityLive => (
            "Selected scene",
            "Selected scene opacity (live)",
            "{@the live opacity of the selected scene}",
            Level,
        ),
        K::SelectedSceneDistributed => (
            "Selected scene",
            "Which scene is selected (0–127)",
            "{@which scene is selected} (spread 0–127)",
            Level,
        ),
        K::SelectedSceneInputDimmer => (
            "Selected scene",
            "Selected scene input dimmer",
            "{@the input dimmer of the selected scene}",
            Level,
        ),
        K::SelectedSceneBeatOffset => (
            "Selected scene",
            "Selected scene beat offset",
            "{@the beat offset of the selected scene}",
            Level,
        ),
        K::SelectedSceneIgnoreMainDimmer => (
            "Selected scene",
            "Selected scene ignores main dimmer",
            "whether the selected scene {@ignores the main dimmer}",
            OnOff,
        ),
        K::SelectedSceneSetOffsetOnFlash => (
            "Selected scene",
            "Selected scene sets offset on flash",
            "whether the selected scene {@sets its offset on flash}",
            OnOff,
        ),
        K::SelectedEffectDistributed => (
            "Animation",
            "Which animation is selected (0–127)",
            "{@which animation is selected} (spread 0–127)",
            Level,
        ),
        K::EffectSelected => (
            "Animation",
            "Animation is selected",
            "whether {animation} {@is selected}",
            OnOff,
        ),
        K::SelectedSceneEffectLive => (
            "Animation",
            "Animation value (live)",
            "{@the live} {parameter} of {effect} in the selected scene",
            Level,
        ),
        K::SceneEffectSettingF32 => (
            "Animation",
            "Animation setting",
            "{@the value of} {setting} of {effect} in {target}",
            Level,
        ),
        K::SceneEffectOpacity => (
            "Animation",
            "Animation opacity",
            "{@the opacity of} {effect} in {target}",
            Level,
        ),
        K::SceneEffectColorShift => (
            "Animation",
            "Animation color shift",
            "{@the color shift of} {effect} in {target}",
            Level,
        ),
        K::SceneEffectBeatProgression => (
            "Animation",
            "Animation beat progression",
            "{@the beat progression of} {effect} in {target}",
            Level,
        ),
        K::SceneEffectBeatOffset => (
            "Animation",
            "Animation beat offset",
            "{@the beat offset of} {effect} in {target}",
            Level,
        ),
        K::SceneEffectSpeedExponent => (
            "Animation",
            "Animation speed",
            "{@the speed of} {effect} in {target}",
            Level,
        ),
        K::SceneEffectLayout => (
            "Animation",
            "Animation layout",
            "{@the layout of} {effect} in {target}",
            Level,
        ),
        K::MainDimmer => ("Global", "Main dimmer", "{@the main dimmer}", Level),
        K::Blackout => ("Global", "Blackout", "{@blackout} {blackout}", OnOff),
        K::BeatFlankPulse => ("Global", "Beat pulse", "{@the beat}: {beat}", OnOff),
        K::ModeActive => (
            "Global",
            "Mode is active",
            "whether mode {mode} {@is active}",
            OnOff,
        ),
    };
    SourceInfo {
        kind,
        group,
        label,
        phrase,
        value,
    }
}

/// All feedback sources in palette order.
pub(crate) fn sources() -> impl Iterator<Item = SourceInfo> {
    MidiValueSourceKind::iter().map(source_info)
}

/// The feedback that shows what `action` changes: a toggle's light shows
/// whether its scene is on, a fader's motor follows the value it sets.
pub(crate) fn matching_source(action: &MidiInputAction) -> Option<MidiValueSource> {
    use MidiInputAction as A;
    use MidiValueSource as S;
    let selected = |target: &MidiSceneTarget| *target == MidiSceneTarget::Selected;
    Some(match action {
        A::SetMainDimmer | A::SetMainDimmerRelative { .. } => S::MainDimmer,
        A::SetBlackout | A::ToggleBlackout => S::Blackout {
            inverted: false,
            blink: false,
        },
        A::Tap => S::BeatFlankPulse {
            start_beat: 0.0,
            end_beat: 0.5,
            blackout_blink_value: None,
            every_beat: true,
        },
        A::SelectSceneDistributed => S::SelectedSceneDistributed,
        A::SelectSceneEffectDistributed => S::SelectedEffectDistributed,
        A::SelectScene { target } => S::SceneSelected {
            target: target.clone(),
        },
        A::SelectMode { mode } => S::ModeActive { mode: *mode },
        A::SelectSceneEffect { index } => S::EffectSelected { index: *index },
        A::ToggleSceneActive { target } | A::SetSceneActive { target } => S::SceneActive {
            target: target.clone(),
        },
        A::FlashScene { target } => S::SceneFlashed {
            target: target.clone(),
        },
        A::SetSceneOpacity { target } if selected(target) => S::SelectedSceneOpacity,
        A::SetSceneOpacity { target } => S::SceneOpacity {
            target: target.clone(),
        },
        A::SetSceneInputDimmer { target } if selected(target) => S::SelectedSceneInputDimmer,
        A::SetSceneInputDimmer { target } => S::SceneInputDimmer {
            target: target.clone(),
        },
        A::SetSceneBeatOffset { target } if selected(target) => S::SelectedSceneBeatOffset,
        A::SetSceneBeatOffset { target } => S::SceneBeatOffset {
            target: target.clone(),
        },
        A::SetSceneIgnoreMainDimmer { target } if selected(target) => {
            S::SelectedSceneIgnoreMainDimmer
        }
        A::SetSceneIgnoreMainDimmer { target } => S::SceneIgnoreMainDimmer {
            target: target.clone(),
        },
        A::SetSceneSetOffsetOnFlash { target } if selected(target) => {
            S::SelectedSceneSetOffsetOnFlash
        }
        A::SetSceneSetOffsetOnFlash { target } => S::SceneSetOffsetOnFlash {
            target: target.clone(),
        },
        A::SetSceneEffectSettingF32 {
            target,
            effect,
            setting_index,
        } => S::SceneEffectSettingF32 {
            target: target.clone(),
            effect: *effect,
            setting_index: *setting_index,
        },
        A::SetSceneEffectOpacity { target, effect } => S::SceneEffectOpacity {
            target: target.clone(),
            effect: *effect,
        },
        A::SetSceneEffectColorShift { target, effect } => S::SceneEffectColorShift {
            target: target.clone(),
            effect: *effect,
        },
        A::SetSceneEffectBeatProgression { target, effect } => S::SceneEffectBeatProgression {
            target: target.clone(),
            effect: *effect,
        },
        A::SetSceneEffectBeatOffset { target, effect } => S::SceneEffectBeatOffset {
            target: target.clone(),
            effect: *effect,
        },
        A::SetSceneEffectSpeedExponent { target, effect } => S::SceneEffectSpeedExponent {
            target: target.clone(),
            effect: *effect,
        },
        A::SetSceneEffectLayout { target, effect } => S::SceneEffectLayout {
            target: target.clone(),
            effect: *effect,
        },
        A::SetSpeedAdd
        | A::SetSpeedMultiply
        | A::SetSpeedRelative { .. }
        | A::SelectSceneRelative { .. }
        | A::SelectSceneRow { .. }
        | A::SelectSceneColumn { .. }
        | A::SelectSceneEffectRelative { .. } => return None,
    })
}

/// The live variant of `source` for a meter, which shows the value as it
/// is now, with curves and sound applied; `source` where there is none.
pub(crate) fn live_source(source: MidiValueSource) -> MidiValueSource {
    use MidiEffectParameter as P;
    use MidiValueSource as S;
    let selected = |target: &MidiSceneTarget| *target == MidiSceneTarget::Selected;
    let live = |effect, parameter| S::SelectedSceneEffectLive { effect, parameter };
    match source {
        S::SelectedSceneOpacity => S::SelectedSceneOpacityLive,
        S::SceneOpacity { target } => S::SceneOpacityLive { target },
        S::SceneEffectOpacity { target, effect } if selected(&target) => live(effect, P::Opacity),
        S::SceneEffectColorShift { target, effect } if selected(&target) => {
            live(effect, P::ColorShift)
        }
        S::SceneEffectBeatProgression { target, effect } if selected(&target) => {
            live(effect, P::BeatProgression)
        }
        S::SceneEffectBeatOffset { target, effect } if selected(&target) => {
            live(effect, P::BeatOffset)
        }
        S::SceneEffectSettingF32 {
            target,
            effect,
            setting_index,
        } if selected(&target) => match P::setting(setting_index) {
            Some(parameter) => live(effect, parameter),
            None => S::SceneEffectSettingF32 {
                target,
                effect,
                setting_index,
            },
        },
        other => other,
    }
}

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

    #[test]
    fn a_toggle_light_shows_whether_its_scene_is_on() {
        let target = MidiSceneTarget::Row { col: 2 };
        assert_eq!(
            matching_source(&MidiInputAction::ToggleSceneActive {
                target: target.clone()
            }),
            Some(MidiValueSource::SceneActive { target })
        );
    }

    #[test]
    fn a_tap_light_pulses_on_every_beat() {
        assert!(matches!(
            matching_source(&MidiInputAction::Tap),
            Some(MidiValueSource::BeatFlankPulse {
                every_beat: true,
                ..
            })
        ));
    }
}