Skip to main content

gled_ui/app/
mod.rs

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