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
15pub(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 pub(crate) connection: Connection,
60 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 pub(crate) controller_line: Option<GridLine>,
71 mode_highlight: controller_feedback::ModeHighlight,
73 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 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 pub(crate) settings: ClientSettings,
92 pub(crate) output_config: OutputConfig,
93 pub(crate) model: ClientModel,
95 frame_requests: FrameRequests,
97 pub(crate) preview_has_output: bool,
100 pub(crate) thumbnails: EffectPipelines,
102 pub(crate) thumbnail_textures: ThumbnailTextures,
104 pub(crate) ui_preview: UiPreview,
106 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 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 #[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 ctx.request_repaint();
221 }
222}
223
224impl App {
225 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 pub(crate) fn loading(&self) -> Option<String> {
237 if !self.model.replica.complete {
238 return Some(match self.connection.target() {
239 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 pub fn new(
328 connection: Connection,
329 settings: ClientSettings,
330 extension: Option<Box<dyn AppExtension>>,
331 ) -> Self {
332 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}