Skip to main content

gled_protocol/
action.rs

1//! Actions: every change to the show, from any source. User interfaces, OSC
2//! and MIDI enqueue them; the engine applies them.
3//!
4//! `UiAction::enqueue` puts an action into the global queue the engine reads,
5//! unless the calling thread routes its actions elsewhere: a user interface
6//! sends its actions numbered to the engine, so it knows when they are applied
7//! (see `route_actions_on_this_thread`).
8
9use gled_model::{
10    asset::{
11        animation::{Animation, argument::ArgumentKindId},
12        curve::Curve,
13        midi_controller::MidiController,
14        output_device::routing::OutputRoutings,
15        palette::Palette,
16        project::{
17            GridHighlight, Project,
18            io::{ArtnetConfig, ArtnetControlConfig, ChannelOverwrites, OscConfig},
19            scene_instance_path::SceneInstanceUnion,
20        },
21        scene::{Scene, color::SceneInstanceColor, grid::GridLocation, instance::SceneInstance},
22    },
23    asset_id::AssetId,
24    input::event::InputEvent,
25    patching::Patch,
26    sound::AudioDeviceId,
27};
28use kanal::{Receiver, Sender, unbounded};
29use once_cell::sync::OnceCell;
30use serde::{Deserialize, Serialize};
31use std::{
32    cell::RefCell,
33    collections::{BTreeSet, HashMap},
34    sync::Arc,
35};
36
37static ACTION_SENDER: OnceCell<Sender<UiAction>> = OnceCell::new();
38
39/// The project's own trigger inputs.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct TriggerInputs {
42    pub tap: BTreeSet<InputEvent>,
43    pub blackout: BTreeSet<InputEvent>,
44    pub blackout_hold: BTreeSet<InputEvent>,
45    pub half: BTreeSet<InputEvent>,
46    pub double: BTreeSet<InputEvent>,
47}
48
49impl TriggerInputs {
50    pub fn of(project: &Project) -> Self {
51        Self {
52            tap: project.tap_input_events.clone(),
53            blackout: project.blackout_input_events.clone(),
54            blackout_hold: project.blackout_hold_input_events.clone(),
55            half: project.half_input_events.clone(),
56            double: project.double_input_events.clone(),
57        }
58    }
59
60    pub fn apply_to(self, project: &mut Project) {
61        project.tap_input_events = self.tap;
62        project.blackout_input_events = self.blackout;
63        project.blackout_hold_input_events = self.blackout_hold;
64        project.half_input_events = self.half;
65        project.double_input_events = self.double;
66    }
67}
68
69/// Where a thread's actions go.
70type ActionRoute = Box<dyn Fn(UiAction)>;
71
72thread_local! {
73    /// Where actions enqueued on this thread go instead of the global queue.
74    /// The user interface's thread sends its actions to the engine in order
75    /// with its other input, see `route_actions_on_this_thread`.
76    static ACTION_ROUTE: RefCell<Option<ActionRoute>> = const { RefCell::new(None) };
77}
78
79/// Sends every action enqueued on the calling thread to `route` from now on.
80pub fn route_actions_on_this_thread(route: impl Fn(UiAction) + 'static) {
81    ACTION_ROUTE.with(|cell| *cell.borrow_mut() = Some(Box::new(route)));
82}
83
84#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
85pub enum UiAction {
86    // Whole settings, sent for direct edits in the interface (see
87    // `ui::project_edits`).
88    SetProjectPalette(Box<Option<Palette>>),
89    /// The patch the fixture editor applied.
90    SetProjectPatch(Box<Patch>),
91    SetGridSize(usize, usize),
92    SetGridHighlight(GridHighlight),
93    SetProjectChannelOverwrites(ChannelOverwrites),
94    SetProjectOutputRoutings(Arc<OutputRoutings>),
95    SetProjectArtnetConfig(Box<ArtnetConfig>),
96    SetProjectTriggerInputs(Box<TriggerInputs>),
97    SetMidiActiveMappings(HashMap<String, Option<AssetId<MidiController>>>),
98    SetProjectArtnetControl(ArtnetControlConfig),
99    SetProjectOsc(OscConfig),
100    /// Puts a scene instance at a grid location, or empties it.
101    ReplaceSceneInstance(GridLocation, Option<Box<SceneInstance>>),
102    CloseProject,
103    /// The output routings of the running output (not yet saved in the project).
104    SetOutputRoutings(Arc<OutputRoutings>),
105
106    AddScene(GridLocation, AssetId<Scene>),
107    SetProject(AssetId<Project>),
108    SelectScene(SceneInstanceUnion),
109    SelectSceneByLocation(GridLocation),
110    DeleteSceneInstance {
111        location: GridLocation,
112    },
113    DeleteSceneInstancePath(SceneInstanceUnion),
114    CloneSceneInstance(GridLocation),
115    CloneSceneInstancePath(SceneInstanceUnion),
116    ReloadShaderCode(Option<AssetId<Animation>>),
117    Tap,
118    SpeedAdd(f32),
119    SpeedMultiply(f32),
120    SetBlackout(bool),
121    /// Set opacity for a scene instance
122    ///
123    /// This also activates the scene instance
124    SetSceneOpacity(SceneInstanceUnion, f32),
125    ToggleSceneActive(SceneInstanceUnion),
126    SetSceneActive(SceneInstanceUnion, bool),
127    SetSceneFlash(SceneInstanceUnion, bool),
128    SetMainDimmer(f32),
129    MidiOutputActive(bool),
130    SwapScenes(GridLocation, GridLocation),
131    SetSceneName(SceneInstanceUnion, String),
132    SetSceneColor(SceneInstanceUnion, SceneInstanceColor),
133    SetSceneInputDimmer(SceneInstanceUnion, f32),
134    SetSceneIgnoreMainDimmer(SceneInstanceUnion, bool),
135    SetSceneBeatOffset(SceneInstanceUnion, f32),
136    SetSceneSetOffsetOnFlash(SceneInstanceUnion, bool),
137    /// Set only the multiplier of the opacity curve (preserves the curve asset)
138    SetSceneOpacityMultiplier(SceneInstanceUnion, f32),
139    /// Set only the curve asset for the opacity (preserves the multiplier)
140    SetSceneOpacityCurve(SceneInstanceUnion, Option<AssetId<Curve>>),
141    /// Set only the multiplier of the beat_offset curve (preserves the curve asset)
142    SetSceneBeatOffsetMultiplier(SceneInstanceUnion, f32),
143    /// Set only the curve asset for beat_offset (preserves the multiplier)
144    SetSceneBeatOffsetCurve(SceneInstanceUnion, Option<AssetId<Curve>>),
145    SetSceneActivationInput(SceneInstanceUnion, Option<InputEvent>),
146    SetSceneFlashInput(SceneInstanceUnion, Option<InputEvent>),
147    SetSceneDimmerInput(SceneInstanceUnion, Option<InputEvent>),
148    SetScenePaletteOverwriteFromAsset(SceneInstanceUnion, Option<Option<AssetId<Palette>>>),
149    SetScenePaletteOverwritePrimary(SceneInstanceUnion, [f32; 3]),
150    SetScenePaletteOverwriteSecondary(SceneInstanceUnion, [f32; 3]),
151    SetScenePaletteOverwriteGradient(SceneInstanceUnion, usize, [f32; 3]),
152    SetProjectPaletteFromAsset(Option<AssetId<Palette>>),
153    SetProjectPalettePrimary([f32; 3]),
154    SetProjectPaletteSecondary([f32; 3]),
155    SetProjectPaletteGradient(usize, [f32; 3]),
156    SetProjectArtnetControlActive(bool),
157    SetProjectArtnetControlUniverse(u16),
158    SetProjectOscActive(bool),
159    SetProjectOscPort(u16),
160    AddProjectTapInputArtnet(u16),
161    RemoveProjectTapInputArtnet(u16),
162    ClearProjectTapInput,
163    AddProjectBlackoutInputArtnet(u16),
164    RemoveProjectBlackoutInputArtnet(u16),
165    ClearProjectBlackoutInput,
166    AddProjectBlackoutHoldInputArtnet(u16),
167    RemoveProjectBlackoutHoldInputArtnet(u16),
168    ClearProjectBlackoutHoldInput,
169    AddProjectHalfInputArtnet(u16),
170    RemoveProjectHalfInputArtnet(u16),
171    ClearProjectHalfInput,
172    AddProjectDoubleInputArtnet(u16),
173    RemoveProjectDoubleInputArtnet(u16),
174    ClearProjectDoubleInput,
175    SetSceneEffectOpacity(SceneInstanceUnion, usize, f32),
176    SetSceneEffectOpacityMultiplier(SceneInstanceUnion, usize, f32),
177    SetSceneEffectOpacityCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
178    SetSceneEffectColorShift(SceneInstanceUnion, usize, f32),
179    SetSceneEffectColorShiftMultiplier(SceneInstanceUnion, usize, f32),
180    SetSceneEffectColorShiftCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
181    SetSceneEffectBeatProgression(SceneInstanceUnion, usize, f32),
182    SetSceneEffectBeatProgressionMultiplier(SceneInstanceUnion, usize, f32),
183    SetSceneEffectBeatProgressionCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
184    SetSceneEffectBeatOffset(SceneInstanceUnion, usize, f32),
185    SetSceneEffectBeatOffsetMultiplier(SceneInstanceUnion, usize, f32),
186    SetSceneEffectBeatOffsetCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
187    SetSceneEffectSpeedExponent(SceneInstanceUnion, usize, i32),
188    /// Stack index into `Patch::layout_refs` (`LayoutRef::stack_index`): 0 is
189    /// the topology, then the layouts in stack order, so a knob can sweep
190    /// through them. `None` makes the effect inherit its tile's layout.
191    SetSceneEffectLayout(SceneInstanceUnion, usize, Option<usize>),
192    SetSceneEffectAnimation(SceneInstanceUnion, usize, Option<AssetId<Animation>>),
193    SetSceneEffectAnimationConfigU32(SceneInstanceUnion, usize, usize, u32),
194    SetSceneEffectAnimationConfigF32(SceneInstanceUnion, usize, usize, f32),
195    AddSceneEffect(SceneInstanceUnion),
196    RemoveSceneEffect(SceneInstanceUnion, usize),
197    CloneSceneEffect(SceneInstanceUnion, usize),
198    SetAnimationShaderCode(AssetId<Animation>, String),
199    AddAnimationArgument(AssetId<Animation>),
200    RemoveAnimationArgument(AssetId<Animation>, usize),
201    SetAnimationArgumentName(AssetId<Animation>, usize, String),
202    SetAnimationArgumentKind(AssetId<Animation>, usize, ArgumentKindId),
203    Error(String),
204    SetAudioDevice(Option<AudioDeviceId>),
205}
206
207impl UiAction {
208    /// Creates the global queue. Call once; a second call returns `None`.
209    pub fn init_queue() -> Option<Receiver<UiAction>> {
210        let (sender, receiver) = unbounded();
211        ACTION_SENDER.set(sender).ok()?;
212        Some(receiver)
213    }
214
215    pub fn enqueue(self) {
216        let action = ACTION_ROUTE.with(|cell| match cell.borrow().as_ref() {
217            Some(route) => {
218                route(self);
219                None
220            }
221            None => Some(self),
222        });
223        let Some(action) = action else {
224            return;
225        };
226        match ACTION_SENDER.get() {
227            Some(sender) => {
228                if sender.send(action).is_err() {
229                    tracing::warn!("The action queue has no receiver any more");
230                }
231            }
232            None => tracing::warn!("Dropped {action:?}: the action queue is not set up"),
233        }
234    }
235}