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