Skip to main content

gled_ui/app/
mod.rs

1pub(crate) mod client_timing;
2pub(crate) mod config;
3mod connection_banner;
4pub(crate) mod effects;
5mod engine_sync;
6mod local_midi;
7pub(crate) mod menu;
8pub(crate) mod no_project;
9pub(crate) mod preview;
10mod render;
11pub(crate) mod status_bar;
12pub(crate) mod storage;
13
14/// Beat timing, from the model.
15pub(crate) mod timing {
16    pub(crate) use gled_model::timing::BeatState;
17}
18
19use crate::canvas::images::Images;
20use crate::ui::frame_requests::FrameRequests;
21use crate::{
22    extension::AppExtension, link::Connection, model::ClientModel, settings::ClientSettings,
23};
24use crate::{
25    pipeline::{
26        renderer_callback::RendererCallback,
27        thumbnails::{ThumbnailTextures, UiPreview},
28    },
29    ui::{
30        asset_tree::AssetTree, local_action::LocalAction,
31        scene_effect_editor::SceneEffectEditorState, window_common::default_viewport_builder,
32        windows::Windows,
33    },
34};
35use client_timing::ClientTiming;
36use eframe::egui_wgpu::Callback;
37use egui::{CentralPanel, Id, LayerId, Rect, Ui, UiBuilder, ViewportId};
38use engine_sync::EngineSync;
39use gled_gpu::effect_state::EffectPipelines;
40use gled_model::{
41    asset::{Asset, palette::Palette, project::Project, scene::grid::GridLocation},
42    asset_id::AssetId,
43};
44use gled_protocol::action::UiAction;
45use gled_protocol::state::OutputConfig;
46use kanal::{Receiver, Sender};
47use std::collections::HashSet;
48use storage::{show_storage_error, show_storage_loading};
49
50#[derive(Debug)]
51pub struct App {
52    pub(crate) windows: Windows,
53    pub(crate) timing: ClientTiming,
54    /// The link to the engine.
55    pub(crate) connection: Connection,
56    /// What the host adds, such as remote access on the desktop.
57    extension: Option<Box<dyn AppExtension>>,
58    sync: EngineSync,
59    pub(crate) project: Option<Project>,
60    pub(crate) project_id: Option<AssetId<Project>>,
61    pub(crate) other_main_windows: HashSet<ViewportId>,
62    pub(crate) blackout: bool,
63    pub(crate) blackout_hold: bool,
64    pub(crate) selected_scene_instance: GridLocation,
65    pub(crate) selected_scene_effect_editor: SceneEffectEditorState,
66    pub(crate) git_commit_message: String,
67    pub(crate) midi_monitor_receiver: kanal::Receiver<gled_protocol::midi::MidiMonitorEvent>,
68    midi_monitor_sender: Sender<gled_protocol::midi::MidiMonitorEvent>,
69    pub(crate) test_command_sender: Sender<gled_protocol::midi::TestCommand>,
70    test_command_receiver: Receiver<gled_protocol::midi::TestCommand>,
71    pub(crate) midi_learn_state: crate::midi_learn::LearnState,
72    /// The ports of this machine or browser while connected to a remote
73    /// engine.
74    local_midi: Option<local_midi::LocalMidi>,
75    pub(crate) last_title: String,
76    pub(crate) midi_output_active: bool,
77    pub(crate) palette_asset_tree: AssetTree<Palette>,
78    pub(crate) palette_asset_tree_id: Option<Id>,
79    pub(crate) network_stats: (f64, f64),
80    /// This interface's own settings, stored by eframe.
81    pub(crate) settings: ClientSettings,
82    pub(crate) output_config: OutputConfig,
83    /// What the engine published, as a remote interface would see it.
84    pub(crate) model: ClientModel,
85    /// What the windows asked of the engine during the frame.
86    frame_requests: FrameRequests,
87    /// Whether the preview currently shows output that must be cleared when
88    /// the project is closed.
89    pub(crate) preview_has_output: bool,
90    /// Thumbnail pipelines of the project's effects, on the UI device.
91    pub(crate) thumbnails: EffectPipelines,
92    /// egui textures of those thumbnails.
93    pub(crate) thumbnail_textures: ThumbnailTextures,
94    /// The LED preview on the UI device.
95    pub(crate) ui_preview: UiPreview,
96    /// The rig layout's background, drawn behind the LED preview.
97    pub(crate) preview_background: Images,
98}
99
100impl eframe::App for App {
101    fn save(&mut self, storage: &mut dyn eframe::Storage) {
102        eframe::set_value(storage, crate::settings::STORAGE_KEY, &self.settings);
103    }
104
105    #[cfg_attr(feature = "profiling", profiling::function)]
106    fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
107        self.send_local_changes();
108        self.receive_engine_messages();
109        crate::ui::input::set_frame_input_events(std::mem::take(&mut self.model.input_events));
110        for event in std::mem::take(&mut self.model.midi_monitor) {
111            let _ = self.midi_monitor_sender.send(event);
112        }
113        for message in std::mem::take(&mut self.model.midi_learn) {
114            self.midi_learn_state.feed(message);
115        }
116        // The effect picked in the last frame, before the engine's state
117        // could take it back.
118        if let Some(effect) = self.selected_scene_effect_editor.take_change() {
119            UiAction::SelectSceneEffect(effect).enqueue();
120        }
121        self.receive_engine_state();
122        self.drive_local_midi(ctx);
123        self.handle_engine_events();
124        self.handle_local_actions();
125        self.send_keys(ctx);
126
127        let title = format!(
128            "gled - {}",
129            match self
130                .project_id
131                .and_then(|id| Asset::get(id, &self.model.replica.assets))
132            {
133                None => "No project".to_string(),
134                Some(project) => project.name().to_string(),
135            }
136        );
137        if title != self.last_title {
138            ctx.send_viewport_cmd(egui::ViewportCommand::Title(title.clone()));
139            // The browser's backend ignores the command: the tab shows the
140            // page's title.
141            #[cfg(target_arch = "wasm32")]
142            crate::platform::set_page_title(&title);
143            self.last_title = title;
144        }
145
146        self.render_thumbnails_and_preview();
147    }
148
149    fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
150        let ctx = ui.ctx().clone();
151
152        self.draw_main_window(&ctx, None);
153
154        let viewport_ids = self.other_main_windows.clone();
155        for viewport_id in viewport_ids {
156            ctx.show_viewport_immediate(
157                viewport_id,
158                default_viewport_builder()
159                    .with_inner_size([1300.0, 1024.0])
160                    .with_drag_and_drop(true)
161                    .with_min_inner_size([300.0, 200.0]),
162                |ui, _viewport_class| {
163                    ui.ctx().input(|input| {
164                        if input.viewport().close_requested() {
165                            LocalAction::CloseWindow(viewport_id).enqueue();
166                        }
167                    });
168
169                    self.draw_main_window(ui.ctx(), Some(viewport_id));
170                },
171            );
172        }
173
174        if let Some(extension) = &mut self.extension {
175            extension.update(&ctx);
176        }
177        let local_midi = self
178            .local_midi
179            .as_ref()
180            .map(local_midi::LocalMidi::status)
181            .unwrap_or_default();
182        self.frame_requests
183            .begin(&self.model.replica.state.status.devices, local_midi);
184        self.windows.update(
185            &ctx,
186            &self.timing,
187            &mut self.project,
188            &mut self.model.replica.assets,
189            &mut self.settings,
190            &mut self.output_config,
191            &self.model.sound,
192            &self.midi_monitor_receiver,
193            &self.test_command_sender,
194            &mut self.midi_learn_state,
195            &self.model.replica.state.status,
196        );
197        while let Ok(Some(command)) = self.test_command_receiver.try_recv() {
198            self.route_test_command(command);
199        }
200        self.frame_requests
201            .end(self.midi_learn_state.active_request().is_some());
202        let (watching, learning) = self.frame_requests.midi_watching();
203        self.set_local_midi_gates(watching, learning);
204        if self.frame_requests.take_midi_access_request() {
205            self.request_local_midi_access();
206        }
207
208        // The thumbnails move on every frame, so the window repaints
209        // continuously, at the monitor's refresh rate.
210        ctx.request_repaint();
211    }
212}
213
214impl App {
215    /// Selects a scene instance here right away and in the engine.
216    pub(crate) fn set_selected_scene_instance(&mut self, location: GridLocation) {
217        if self.selected_scene_instance != location {
218            self.selected_scene_instance = location;
219            self.selected_scene_effect_editor.reset();
220        }
221        UiAction::SelectSceneByLocation(location).enqueue();
222    }
223
224    /// What the interface waits for before it can show the show: the engine's
225    /// first state, then the asset repository.
226    pub(crate) fn loading(&self) -> Option<String> {
227        if !self.model.replica.complete {
228            return Some(match self.connection.target() {
229                // Its own line; a wrapped one would be stretched.
230                Some(target) => format!("Connecting to the engine\nat {target}"),
231                None => "Connecting to the engine".to_owned(),
232            });
233        }
234        self.model.replica.state.status.storage_task.clone()
235    }
236
237    fn handle_local_actions(&mut self) {
238        for action in LocalAction::take_all() {
239            match action {
240                LocalAction::CloseWindow(viewport_id) => {
241                    self.other_main_windows.remove(&viewport_id);
242                }
243                LocalAction::OpenGitConfigWindow => self.windows.git_config.open(),
244            }
245        }
246    }
247
248    #[cfg_attr(feature = "profiling", profiling::function)]
249    pub(crate) fn draw_main_window(
250        &mut self,
251        ctx: &egui::Context,
252        viewport_id: Option<ViewportId>,
253    ) {
254        let panel_frame = egui::Frame::new()
255            .fill(ctx.global_style().visuals.window_fill())
256            .stroke(ctx.global_style().visuals.widgets.noninteractive.fg_stroke);
257
258        let mut root_ui = Ui::new(
259            ctx.clone(),
260            Id::new((ctx.viewport_id(), "main_window_panel")),
261            UiBuilder::new()
262                .layer_id(LayerId::background())
263                .max_rect(ctx.content_rect()),
264        );
265        root_ui.set_clip_rect(ctx.content_rect());
266
267        CentralPanel::default()
268            .frame(panel_frame)
269            .show(&mut root_ui, |ui| {
270                if viewport_id.is_none() {
271                    let callback = Callback::new_paint_callback(Rect::ZERO, RendererCallback);
272                    ui.painter().add(callback);
273                }
274                let mut ui = ui.new_child(UiBuilder::new().max_rect(ui.max_rect().shrink(4.0)));
275
276                self.menu(&mut ui, viewport_id);
277                self.connection_banner(&mut ui, viewport_id);
278                self.status_bar(&mut ui, viewport_id);
279
280                let status = &self.model.replica.state.status;
281                if let Some(error) = &status.storage_error {
282                    show_storage_error(&mut ui, error, &status.storage_dir);
283                    return;
284                } else if let Some(task) = self.loading() {
285                    show_storage_loading(&mut ui, &task, status.storage_progress);
286                    return;
287                }
288
289                if self.project.is_some() {
290                    egui::Panel::left("config")
291                        .resizable(false)
292                        .exact_size(400.0)
293                        .show(&mut ui, |ui| self.config(ui));
294                    egui::Panel::top("preview")
295                        .resizable(true)
296                        .default_size(200.0)
297                        .min_size(200.0)
298                        .show(&mut ui, |ui| self.preview(ui));
299                    egui::CentralPanel::default().show(&mut ui, |ui| self.scenes(ui));
300                } else {
301                    self.no_project(&mut ui);
302                }
303            });
304    }
305    /// The interface on `connection`, with the host's `extension`.
306    pub fn new(
307        connection: Connection,
308        settings: ClientSettings,
309        extension: Option<Box<dyn AppExtension>>,
310    ) -> Self {
311        // The MIDI window reads the monitor and writes test commands through
312        // channels; the app fills and drains them from the engine's messages.
313        let (midi_monitor_sender, midi_monitor_receiver) = kanal::unbounded();
314        let (test_command_sender, test_command_receiver) = kanal::unbounded();
315        Self {
316            timing: ClientTiming::default(),
317            connection,
318            extension,
319            sync: EngineSync::default(),
320            blackout: true,
321            blackout_hold: false,
322            selected_scene_instance: Default::default(),
323            selected_scene_effect_editor: Default::default(),
324            project: Default::default(),
325            project_id: Default::default(),
326            windows: Default::default(),
327            other_main_windows: Default::default(),
328            git_commit_message: Default::default(),
329            midi_monitor_receiver,
330            midi_monitor_sender,
331            test_command_sender,
332            test_command_receiver,
333            midi_learn_state: crate::midi_learn::LearnState::default(),
334            local_midi: None,
335            last_title: Default::default(),
336            midi_output_active: false,
337            palette_asset_tree: AssetTree {
338                only_asset_selection: true,
339                ..Default::default()
340            },
341            palette_asset_tree_id: None,
342            network_stats: Default::default(),
343            settings,
344            output_config: OutputConfig::default(),
345            model: ClientModel::default(),
346            frame_requests: FrameRequests::default(),
347            preview_has_output: false,
348            thumbnails: EffectPipelines::default(),
349            thumbnail_textures: ThumbnailTextures::default(),
350            ui_preview: UiPreview::new(&crate::wgpu_render_state()),
351            preview_background: Images::default(),
352        }
353    }
354}