gled-protocol 2.31.0

What gled's user interfaces and its engine send each other
Documentation
//! Actions: every change to the show, from any source. User interfaces, OSC
//! and MIDI enqueue them; the engine applies them.
//!
//! `UiAction::enqueue` puts an action into the global queue the engine reads,
//! unless the calling thread routes its actions elsewhere: a user interface
//! sends its actions numbered to the engine, so it knows when they are applied
//! (see `route_actions_on_this_thread`).

use gled_model::{
    asset::{
        animation::{Animation, argument::ArgumentKindId},
        curve::Curve,
        midi_controller::MidiController,
        output_device::routing::OutputRoutings,
        palette::Palette,
        project::{
            GridHighlight, Project,
            io::{ArtnetConfig, ArtnetControlConfig, ChannelOverwrites, OscConfig},
            scene_instance_path::SceneInstanceUnion,
        },
        scene::{Scene, color::SceneInstanceColor, grid::GridLocation, instance::SceneInstance},
    },
    asset_id::AssetId,
    input::event::InputEvent,
    patching::Patch,
    sound::AudioDeviceId,
};
use kanal::{Receiver, Sender, unbounded};
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::{
    cell::RefCell,
    collections::{BTreeSet, HashMap},
    sync::Arc,
};

static ACTION_SENDER: OnceCell<Sender<UiAction>> = OnceCell::new();

/// The project's own trigger inputs.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TriggerInputs {
    pub tap: BTreeSet<InputEvent>,
    pub blackout: BTreeSet<InputEvent>,
    pub blackout_hold: BTreeSet<InputEvent>,
    pub half: BTreeSet<InputEvent>,
    pub double: BTreeSet<InputEvent>,
}

impl TriggerInputs {
    pub fn of(project: &Project) -> Self {
        Self {
            tap: project.tap_input_events.clone(),
            blackout: project.blackout_input_events.clone(),
            blackout_hold: project.blackout_hold_input_events.clone(),
            half: project.half_input_events.clone(),
            double: project.double_input_events.clone(),
        }
    }

    pub fn apply_to(self, project: &mut Project) {
        project.tap_input_events = self.tap;
        project.blackout_input_events = self.blackout;
        project.blackout_hold_input_events = self.blackout_hold;
        project.half_input_events = self.half;
        project.double_input_events = self.double;
    }
}

/// Where a thread's actions go.
type ActionRoute = Box<dyn Fn(UiAction)>;

thread_local! {
    /// Where actions enqueued on this thread go instead of the global queue.
    /// The user interface's thread sends its actions to the engine in order
    /// with its other input, see `route_actions_on_this_thread`.
    static ACTION_ROUTE: RefCell<Option<ActionRoute>> = const { RefCell::new(None) };
}

/// Sends every action enqueued on the calling thread to `route` from now on.
pub fn route_actions_on_this_thread(route: impl Fn(UiAction) + 'static) {
    ACTION_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum UiAction {
    // Whole settings, sent for direct edits in the interface (see
    // `ui::project_edits`).
    SetProjectPalette(Box<Option<Palette>>),
    /// The patch the fixture editor applied.
    SetProjectPatch(Box<Patch>),
    SetGridSize(usize, usize),
    SetGridHighlight(GridHighlight),
    SetProjectChannelOverwrites(ChannelOverwrites),
    SetProjectOutputRoutings(Arc<OutputRoutings>),
    SetProjectArtnetConfig(Box<ArtnetConfig>),
    SetProjectTriggerInputs(Box<TriggerInputs>),
    SetMidiActiveMappings(HashMap<String, Option<AssetId<MidiController>>>),
    SetProjectArtnetControl(ArtnetControlConfig),
    SetProjectOsc(OscConfig),
    /// Puts a scene instance at a grid location, or empties it.
    ReplaceSceneInstance(GridLocation, Option<Box<SceneInstance>>),
    CloseProject,
    /// The output routings of the running output (not yet saved in the project).
    SetOutputRoutings(Arc<OutputRoutings>),

    AddScene(GridLocation, AssetId<Scene>),
    SetProject(AssetId<Project>),
    SelectScene(SceneInstanceUnion),
    SelectSceneByLocation(GridLocation),
    DeleteSceneInstance {
        location: GridLocation,
    },
    DeleteSceneInstancePath(SceneInstanceUnion),
    CloneSceneInstance(GridLocation),
    CloneSceneInstancePath(SceneInstanceUnion),
    ReloadShaderCode(Option<AssetId<Animation>>),
    Tap,
    SpeedAdd(f32),
    SpeedMultiply(f32),
    SetBlackout(bool),
    /// Set opacity for a scene instance
    ///
    /// This also activates the scene instance
    SetSceneOpacity(SceneInstanceUnion, f32),
    ToggleSceneActive(SceneInstanceUnion),
    SetSceneActive(SceneInstanceUnion, bool),
    SetSceneFlash(SceneInstanceUnion, bool),
    SetMainDimmer(f32),
    MidiOutputActive(bool),
    SwapScenes(GridLocation, GridLocation),
    SetSceneName(SceneInstanceUnion, String),
    SetSceneColor(SceneInstanceUnion, SceneInstanceColor),
    SetSceneInputDimmer(SceneInstanceUnion, f32),
    SetSceneIgnoreMainDimmer(SceneInstanceUnion, bool),
    SetSceneBeatOffset(SceneInstanceUnion, f32),
    SetSceneSetOffsetOnFlash(SceneInstanceUnion, bool),
    /// Set only the multiplier of the opacity curve (preserves the curve asset)
    SetSceneOpacityMultiplier(SceneInstanceUnion, f32),
    /// Set only the curve asset for the opacity (preserves the multiplier)
    SetSceneOpacityCurve(SceneInstanceUnion, Option<AssetId<Curve>>),
    /// Set only the multiplier of the beat_offset curve (preserves the curve asset)
    SetSceneBeatOffsetMultiplier(SceneInstanceUnion, f32),
    /// Set only the curve asset for beat_offset (preserves the multiplier)
    SetSceneBeatOffsetCurve(SceneInstanceUnion, Option<AssetId<Curve>>),
    SetSceneActivationInput(SceneInstanceUnion, Option<InputEvent>),
    SetSceneFlashInput(SceneInstanceUnion, Option<InputEvent>),
    SetSceneDimmerInput(SceneInstanceUnion, Option<InputEvent>),
    SetScenePaletteOverwriteFromAsset(SceneInstanceUnion, Option<Option<AssetId<Palette>>>),
    SetScenePaletteOverwritePrimary(SceneInstanceUnion, [f32; 3]),
    SetScenePaletteOverwriteSecondary(SceneInstanceUnion, [f32; 3]),
    SetScenePaletteOverwriteGradient(SceneInstanceUnion, usize, [f32; 3]),
    SetProjectPaletteFromAsset(Option<AssetId<Palette>>),
    SetProjectPalettePrimary([f32; 3]),
    SetProjectPaletteSecondary([f32; 3]),
    SetProjectPaletteGradient(usize, [f32; 3]),
    SetProjectArtnetControlActive(bool),
    SetProjectArtnetControlUniverse(u16),
    SetProjectOscActive(bool),
    SetProjectOscPort(u16),
    AddProjectTapInputArtnet(u16),
    RemoveProjectTapInputArtnet(u16),
    ClearProjectTapInput,
    AddProjectBlackoutInputArtnet(u16),
    RemoveProjectBlackoutInputArtnet(u16),
    ClearProjectBlackoutInput,
    AddProjectBlackoutHoldInputArtnet(u16),
    RemoveProjectBlackoutHoldInputArtnet(u16),
    ClearProjectBlackoutHoldInput,
    AddProjectHalfInputArtnet(u16),
    RemoveProjectHalfInputArtnet(u16),
    ClearProjectHalfInput,
    AddProjectDoubleInputArtnet(u16),
    RemoveProjectDoubleInputArtnet(u16),
    ClearProjectDoubleInput,
    SetSceneEffectOpacity(SceneInstanceUnion, usize, f32),
    SetSceneEffectOpacityMultiplier(SceneInstanceUnion, usize, f32),
    SetSceneEffectOpacityCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
    SetSceneEffectColorShift(SceneInstanceUnion, usize, f32),
    SetSceneEffectColorShiftMultiplier(SceneInstanceUnion, usize, f32),
    SetSceneEffectColorShiftCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
    SetSceneEffectBeatProgression(SceneInstanceUnion, usize, f32),
    SetSceneEffectBeatProgressionMultiplier(SceneInstanceUnion, usize, f32),
    SetSceneEffectBeatProgressionCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
    SetSceneEffectBeatOffset(SceneInstanceUnion, usize, f32),
    SetSceneEffectBeatOffsetMultiplier(SceneInstanceUnion, usize, f32),
    SetSceneEffectBeatOffsetCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
    SetSceneEffectSpeedExponent(SceneInstanceUnion, usize, i32),
    /// Stack index into `Patch::layout_refs` (`LayoutRef::stack_index`): 0 is
    /// the topology, then the layouts in stack order, so a knob can sweep
    /// through them. `None` makes the effect inherit its tile's layout.
    SetSceneEffectLayout(SceneInstanceUnion, usize, Option<usize>),
    SetSceneEffectAnimation(SceneInstanceUnion, usize, Option<AssetId<Animation>>),
    SetSceneEffectAnimationConfigU32(SceneInstanceUnion, usize, usize, u32),
    SetSceneEffectAnimationConfigF32(SceneInstanceUnion, usize, usize, f32),
    AddSceneEffect(SceneInstanceUnion),
    RemoveSceneEffect(SceneInstanceUnion, usize),
    CloneSceneEffect(SceneInstanceUnion, usize),
    SetAnimationShaderCode(AssetId<Animation>, String),
    AddAnimationArgument(AssetId<Animation>),
    RemoveAnimationArgument(AssetId<Animation>, usize),
    SetAnimationArgumentName(AssetId<Animation>, usize, String),
    SetAnimationArgumentKind(AssetId<Animation>, usize, ArgumentKindId),
    Error(String),
    SetAudioDevice(Option<AudioDeviceId>),
}

impl UiAction {
    /// Creates the global queue. Call once; a second call returns `None`.
    pub fn init_queue() -> Option<Receiver<UiAction>> {
        let (sender, receiver) = unbounded();
        ACTION_SENDER.set(sender).ok()?;
        Some(receiver)
    }

    pub fn enqueue(self) {
        let action = ACTION_ROUTE.with(|cell| match cell.borrow().as_ref() {
            Some(route) => {
                route(self);
                None
            }
            None => Some(self),
        });
        let Some(action) = action else {
            return;
        };
        match ACTION_SENDER.get() {
            Some(sender) => {
                if sender.send(action).is_err() {
                    tracing::warn!("The action queue has no receiver any more");
                }
            }
            None => tracing::warn!("Dropped {action:?}: the action queue is not set up"),
        }
    }
}