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    /// Selects the scene this many steps away from the selected one, in
111    /// row-major order over the placed scenes; stops at the first and last.
112    SelectSceneRelative(i32),
113    /// Selects an effect of the selected scene by its position. It stays
114    /// selected until another scene is selected, which selects its first.
115    SelectSceneEffect(usize),
116    /// Selects the effect this many steps away in the selected scene; stops
117    /// at the first and last.
118    SelectSceneEffectRelative(i32),
119    DeleteSceneInstance {
120        location: GridLocation,
121    },
122    DeleteSceneInstancePath(SceneInstanceUnion),
123    CloneSceneInstance(GridLocation),
124    CloneSceneInstancePath(SceneInstanceUnion),
125    ReloadShaderCode(Option<AssetId<Animation>>),
126    Tap,
127    SpeedAdd(f32),
128    SpeedMultiply(f32),
129    SetBlackout(bool),
130    ToggleBlackout,
131    /// Set opacity for a scene instance
132    ///
133    /// This also activates the scene instance
134    SetSceneOpacity(SceneInstanceUnion, f32),
135    ToggleSceneActive(SceneInstanceUnion),
136    SetSceneActive(SceneInstanceUnion, bool),
137    SetSceneFlash(SceneInstanceUnion, bool),
138    SetMainDimmer(f32),
139    /// Moves the main dimmer by this much, kept within 0 to 1.
140    AddMainDimmer(f32),
141    MidiOutputActive(bool),
142    SwapScenes(GridLocation, GridLocation),
143    SetSceneName(SceneInstanceUnion, String),
144    SetSceneColor(SceneInstanceUnion, SceneInstanceColor),
145    SetSceneInputDimmer(SceneInstanceUnion, f32),
146    SetSceneIgnoreMainDimmer(SceneInstanceUnion, bool),
147    SetSceneBeatOffset(SceneInstanceUnion, f32),
148    SetSceneSetOffsetOnFlash(SceneInstanceUnion, bool),
149    /// Set only the multiplier of the opacity curve (preserves the curve asset)
150    SetSceneOpacityMultiplier(SceneInstanceUnion, f32),
151    /// Set only the curve asset for the opacity (preserves the multiplier)
152    SetSceneOpacityCurve(SceneInstanceUnion, Option<AssetId<Curve>>),
153    /// Set only the multiplier of the beat_offset curve (preserves the curve asset)
154    SetSceneBeatOffsetMultiplier(SceneInstanceUnion, f32),
155    /// Set only the curve asset for beat_offset (preserves the multiplier)
156    SetSceneBeatOffsetCurve(SceneInstanceUnion, Option<AssetId<Curve>>),
157    SetSceneActivationInput(SceneInstanceUnion, Option<InputEvent>),
158    SetSceneFlashInput(SceneInstanceUnion, Option<InputEvent>),
159    SetSceneDimmerInput(SceneInstanceUnion, Option<InputEvent>),
160    SetScenePaletteOverwriteFromAsset(SceneInstanceUnion, Option<Option<AssetId<Palette>>>),
161    SetScenePaletteOverwritePrimary(SceneInstanceUnion, [f32; 3]),
162    SetScenePaletteOverwriteSecondary(SceneInstanceUnion, [f32; 3]),
163    SetScenePaletteOverwriteGradient(SceneInstanceUnion, usize, [f32; 3]),
164    SetProjectPaletteFromAsset(Option<AssetId<Palette>>),
165    SetProjectPalettePrimary([f32; 3]),
166    SetProjectPaletteSecondary([f32; 3]),
167    SetProjectPaletteGradient(usize, [f32; 3]),
168    SetProjectArtnetControlActive(bool),
169    SetProjectArtnetControlUniverse(u16),
170    SetProjectOscActive(bool),
171    SetProjectOscPort(u16),
172    AddProjectTapInputArtnet(u16),
173    RemoveProjectTapInputArtnet(u16),
174    ClearProjectTapInput,
175    AddProjectBlackoutInputArtnet(u16),
176    RemoveProjectBlackoutInputArtnet(u16),
177    ClearProjectBlackoutInput,
178    AddProjectBlackoutHoldInputArtnet(u16),
179    RemoveProjectBlackoutHoldInputArtnet(u16),
180    ClearProjectBlackoutHoldInput,
181    AddProjectHalfInputArtnet(u16),
182    RemoveProjectHalfInputArtnet(u16),
183    ClearProjectHalfInput,
184    AddProjectDoubleInputArtnet(u16),
185    RemoveProjectDoubleInputArtnet(u16),
186    ClearProjectDoubleInput,
187    SetSceneEffectOpacity(SceneInstanceUnion, usize, f32),
188    SetSceneEffectOpacityMultiplier(SceneInstanceUnion, usize, f32),
189    SetSceneEffectOpacityCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
190    SetSceneEffectColorShift(SceneInstanceUnion, usize, f32),
191    SetSceneEffectColorShiftMultiplier(SceneInstanceUnion, usize, f32),
192    SetSceneEffectColorShiftCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
193    SetSceneEffectBeatProgression(SceneInstanceUnion, usize, f32),
194    SetSceneEffectBeatProgressionMultiplier(SceneInstanceUnion, usize, f32),
195    SetSceneEffectBeatProgressionCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
196    SetSceneEffectBeatOffset(SceneInstanceUnion, usize, f32),
197    SetSceneEffectBeatOffsetMultiplier(SceneInstanceUnion, usize, f32),
198    SetSceneEffectBeatOffsetCurve(SceneInstanceUnion, usize, Option<AssetId<Curve>>),
199    SetSceneEffectSpeedExponent(SceneInstanceUnion, usize, i32),
200    /// Stack index into `Patch::layout_refs` (`LayoutRef::stack_index`): 0 is
201    /// the topology, then the layouts in stack order, so a knob can sweep
202    /// through them. `None` makes the effect inherit its tile's layout.
203    SetSceneEffectLayout(SceneInstanceUnion, usize, Option<usize>),
204    SetSceneEffectAnimation(SceneInstanceUnion, usize, Option<AssetId<Animation>>),
205    SetSceneEffectAnimationConfigU32(SceneInstanceUnion, usize, usize, u32),
206    SetSceneEffectAnimationConfigF32(SceneInstanceUnion, usize, usize, f32),
207    /// Sets a float setting's multiplier, keeping its curve and sound
208    /// trigger; a plain value is set as it is.
209    SetSceneEffectAnimationConfigF32Multiplier(SceneInstanceUnion, usize, usize, f32),
210    AddSceneEffect(SceneInstanceUnion),
211    RemoveSceneEffect(SceneInstanceUnion, usize),
212    CloneSceneEffect(SceneInstanceUnion, usize),
213    SetAnimationShaderCode(AssetId<Animation>, String),
214    AddAnimationArgument(AssetId<Animation>),
215    RemoveAnimationArgument(AssetId<Animation>, usize),
216    SetAnimationArgumentName(AssetId<Animation>, usize, String),
217    SetAnimationArgumentKind(AssetId<Animation>, usize, ArgumentKindId),
218    Error(String),
219    SetAudioDevice(Option<AudioDeviceId>),
220}
221
222impl UiAction {
223    /// Creates the global queue. Call once; a second call returns `None`.
224    pub fn init_queue() -> Option<Receiver<UiAction>> {
225        let (sender, receiver) = unbounded();
226        ACTION_SENDER.set(sender).ok()?;
227        Some(receiver)
228    }
229
230    pub fn enqueue(self) {
231        let action = ACTION_ROUTE.with(|cell| match cell.borrow().as_ref() {
232            Some(route) => {
233                route(self);
234                None
235            }
236            None => Some(self),
237        });
238        let Some(action) = action else {
239            return;
240        };
241        match ACTION_SENDER.get() {
242            Some(sender) => {
243                if sender.send(action).is_err() {
244                    tracing::warn!("The action queue has no receiver any more");
245                }
246            }
247            None => tracing::warn!("Dropped {action:?}: the action queue is not set up"),
248        }
249    }
250}