pub mod config;
pub mod effects;
pub mod menu;
pub mod no_project;
pub mod persistent_state;
pub mod preview;
pub mod project_functions;
pub mod render_thread;
pub mod status_bar;
pub mod storage;
pub mod svg;
pub mod timing;
use crate::input::osc::{OSCHandler, OscConfig};
use crate::storage::is_loading;
use crate::{
audio::{AudioPool, sound_data::SoundData},
input::{Input, external_control::ExternalControlState},
output_state::ProjectState,
pipeline::{
extract_output::{ExtractOutput, OutputConfig, PreviewOutput},
gpu::Gpu,
preview::Preview,
preview_indices::PreviewIndices,
renderer_callback::RendererCallback,
},
storage::{
asset::{Asset, palette::Palette, project::Project, scene::grid::GridLocation},
asset_id::AssetId,
collections::Collections,
},
ui::{
action::UiAction, asset_tree::AssetTree, scene_effect_editor::SceneEffectEditorState,
window_common::default_viewport_builder, windows::Windows,
},
};
use eframe::egui_wgpu::Callback;
use egui::{CentralPanel, Id, LayerId, Rect, Ui, UiBuilder, ViewportId};
use kanal::{Receiver, Sender};
use persistent_state::PersistentState;
use render_thread::{RenderFeedback, RenderSnapshot, RenderThread};
use std::collections::HashSet;
use std::sync::Arc;
use std::time::{Duration, Instant};
use storage::{show_storage_error, show_storage_loading};
use timing::Timing;
use wgpu::CommandEncoderDescriptor;
pub struct App {
pub startup: bool,
pub windows: Windows,
pub timing: Timing,
pub project: Option<Project>,
pub project_id: Option<AssetId<Project>>,
pub other_main_windows: HashSet<ViewportId>,
pub blackout: bool,
pub blackout_hold: bool,
pub selected_scene_instance: GridLocation,
pub selected_scene_effect_editor: SceneEffectEditorState,
pub git_commit_message: String,
pub ui_action_receiver: Receiver<UiAction>,
pub midi_monitor_receiver: kanal::Receiver<crate::midi::monitor::MidiMonitorEvent>,
pub test_command_sender: Sender<crate::midi::runtime::TestCommand>,
pub midi_learn_state: crate::midi::learn::LearnState,
pub last_title: String,
pub midi_output_active: bool,
pub palette_asset_tree: AssetTree<Palette>,
pub palette_asset_tree_id: Option<Id>,
pub collections: Collections,
pub network_stats: (f64, f64),
pub network_stats_receiver: Receiver<(f64, f64)>,
pub persistent_state: PersistentState,
pub output_config: OutputConfig,
pub render_thread: RenderThread,
pub preview_output_receiver: Receiver<PreviewOutput>,
pub preview_has_output: bool,
pub last_matrix_frame: Instant,
pub external_control_state: ExternalControlState,
pub audio_pool: AudioPool,
pub sound_data: SoundData,
pub osc_handler: Option<Arc<OSCHandler>>,
}
impl eframe::App for App {
#[cfg_attr(feature = "profiling", profiling::function)]
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if let Ok(Some(network_stats)) = self.network_stats_receiver.try_recv() {
self.network_stats = network_stats;
}
self.receive_render_feedback();
self.persistent_state.update();
self.sound_data.update();
self.timing.tick(&self.persistent_state);
self.collections.update();
if !is_loading() && self.startup {
self.startup = false;
if let Some(project) = self.persistent_state.last_project_id() {
UiAction::SetProject(project).enqueue();
}
}
Input::tick();
self.handle_ui_actions();
let title = format!(
"gled - {}",
match self
.project_id
.and_then(|id| Asset::get(id, &self.collections))
{
None => "No project".to_string(),
Some(project) => project.name().to_string(),
}
);
if title != self.last_title {
ctx.send_viewport_cmd(egui::ViewportCommand::Title(title.clone()));
self.last_title = title;
}
self.external_control_state
.process_events(&mut self.project, &self.collections);
if let Some(project) = &mut self.project {
project.update_input_state(self.timing.beat_progression());
}
self.render_thread.send_snapshot(RenderSnapshot {
project: self.project.clone(),
blackout: self.blackout || self.blackout_hold,
output_config: self.output_config.clone(),
fps_limit: self.persistent_state.fps_limit(),
});
self.render_thumbnails_and_preview();
ProjectState {
project: self.project.clone(),
selected_scene_instance: self.selected_scene_instance,
blackout: self.blackout || self.blackout_hold,
beats_per_minute: self.timing.beats_per_minute(),
beat_progression: self.timing.beat_progression(),
}
.enqueue();
}
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
self.draw_main_window(&ctx, None);
let viewport_ids = self.other_main_windows.clone();
for viewport_id in viewport_ids {
ctx.show_viewport_immediate(
viewport_id,
default_viewport_builder()
.with_inner_size([1300.0, 1024.0])
.with_drag_and_drop(true)
.with_min_inner_size([300.0, 200.0]),
|ui, _viewport_class| {
ui.ctx().input(|input| {
if input.viewport().close_requested() {
UiAction::CloseWindow(viewport_id).enqueue();
}
});
self.draw_main_window(ui.ctx(), Some(viewport_id));
},
);
}
self.windows.update(
&ctx,
&self.timing,
&mut self.project,
&mut self.collections,
&mut self.persistent_state,
&mut self.output_config,
&mut self.sound_data,
&self.midi_monitor_receiver,
&self.test_command_sender,
&mut self.midi_learn_state,
);
ctx.request_repaint_after(
self.matrix_interval()
.saturating_sub(self.last_matrix_frame.elapsed()),
);
}
}
impl App {
pub(crate) fn set_selected_scene_instance(&mut self, location: GridLocation) {
self.selected_scene_instance = location;
self.selected_scene_effect_editor.reset();
}
fn receive_render_feedback(&mut self) {
while let Some(feedback) = self.render_thread.try_recv_feedback() {
match feedback {
RenderFeedback::FrameStats { fps } => self.timing.set_framerate(fps),
}
}
}
fn matrix_interval(&self) -> Duration {
let limit = self.persistent_state.matrix_fps_limit();
if limit.is_infinite() {
Duration::ZERO
} else {
Duration::from_secs_f32(1.0 / limit.max(1.0))
}
}
#[cfg_attr(feature = "profiling", profiling::function)]
fn render_thumbnails_and_preview(&mut self) {
let gpu = Gpu::ui();
let mut encoder = gpu
.device
.create_command_encoder(&CommandEncoderDescriptor {
label: Some("Render thumbnails and preview"),
});
let matrix_interval = self.matrix_interval();
if let Some(project) = &mut self.project {
project.ensure_matrix_pipelines(&gpu, &self.collections);
if self.last_matrix_frame.elapsed() >= matrix_interval {
self.last_matrix_frame = Instant::now();
project.render_matrices(
&gpu,
&mut encoder,
&self.timing.beat_state(),
&self.collections,
&self.sound_data,
);
}
}
let mut latest_output = None;
while let Ok(Some(output)) = self.preview_output_receiver.try_recv() {
latest_output = Some(output);
}
match (self.project.is_some(), latest_output) {
(true, Some(output)) => {
Preview::upload_output(&gpu.queue, &output);
self.preview_has_output = true;
}
(false, _) if self.preview_has_output => {
Preview::clear_output(&gpu.queue);
self.preview_has_output = false;
}
_ => {}
}
PreviewIndices::get().prepare(&gpu.queue);
PreviewIndices::get().run(&mut encoder);
Preview::run(&mut encoder);
RendererCallback::try_add(encoder.finish());
}
#[cfg_attr(feature = "profiling", profiling::function)]
pub fn draw_main_window(&mut self, ctx: &egui::Context, viewport_id: Option<ViewportId>) {
let panel_frame = egui::Frame::new()
.fill(ctx.global_style().visuals.window_fill())
.stroke(ctx.global_style().visuals.widgets.noninteractive.fg_stroke);
let mut root_ui = Ui::new(
ctx.clone(),
Id::new((ctx.viewport_id(), "main_window_panel")),
UiBuilder::new()
.layer_id(LayerId::background())
.max_rect(ctx.content_rect()),
);
root_ui.set_clip_rect(ctx.content_rect());
CentralPanel::default()
.frame(panel_frame)
.show(&mut root_ui, |ui| {
if viewport_id.is_none() {
let callback = Callback::new_paint_callback(Rect::ZERO, RendererCallback);
ui.painter().add(callback);
}
let mut ui = ui.new_child(UiBuilder::new().max_rect(ui.max_rect().shrink(4.0)));
self.menu(&mut ui, viewport_id);
self.status_bar(&mut ui, viewport_id);
if let Some(error) = crate::storage::error() {
show_storage_error(&mut ui, error);
return;
} else if is_loading() {
show_storage_loading(&mut ui);
return;
}
if self.project.is_some() {
egui::Panel::left("config")
.resizable(false)
.exact_size(400.0)
.show(&mut ui, |ui| self.config(ui));
egui::Panel::top("preview")
.resizable(true)
.default_size(200.0)
.min_size(200.0)
.show(&mut ui, |ui| self.preview(ui));
egui::CentralPanel::default().show(&mut ui, |ui| self.scenes(ui));
} else {
self.no_project(&mut ui);
}
});
}
pub fn new(
ui_action_receiver: Receiver<UiAction>,
network_stats_receiver: Receiver<(f64, f64)>,
midi_monitor_receiver: kanal::Receiver<crate::midi::monitor::MidiMonitorEvent>,
test_command_sender: Sender<crate::midi::runtime::TestCommand>,
midi_learn_receiver: Receiver<[u8; 3]>,
extract_output: ExtractOutput,
preview_output_receiver: Receiver<PreviewOutput>,
artnet_control_receiver: Receiver<Vec<u8>>,
audio_pool: AudioPool,
) -> Option<Self> {
let timing = Timing::default();
let persistent_state = PersistentState::default();
let render_thread = RenderThread::spawn(
&crate::wgpu_render_state().adapter,
timing.link(),
extract_output,
persistent_state.fps_limit(),
)
.map_err(|err| tracing::error!("Could not start render thread: {err:#}"))
.ok()?;
let app = Self {
startup: true,
timing,
blackout: true,
blackout_hold: false,
selected_scene_instance: Default::default(),
selected_scene_effect_editor: Default::default(),
project: Default::default(),
project_id: Default::default(),
windows: Default::default(),
other_main_windows: Default::default(),
git_commit_message: Default::default(),
ui_action_receiver,
midi_monitor_receiver,
test_command_sender,
midi_learn_state: crate::midi::learn::LearnState::new(midi_learn_receiver),
last_title: Default::default(),
midi_output_active: false,
palette_asset_tree: AssetTree {
only_asset_selection: true,
..Default::default()
},
palette_asset_tree_id: None,
collections: Default::default(),
network_stats: Default::default(),
network_stats_receiver,
persistent_state,
output_config: OutputConfig::default(),
render_thread,
preview_output_receiver,
preview_has_output: false,
last_matrix_frame: Instant::now(),
external_control_state: ExternalControlState::new(artnet_control_receiver),
audio_pool,
sound_data: Default::default(),
osc_handler: None,
};
Some(app)
}
pub fn apply_osc_config(&mut self, config: OscConfig) {
if let Some(handler) = self.osc_handler.take() {
handler.stop();
}
if config.active {
self.osc_handler = OSCHandler::start(config.port).ok();
}
}
}
pub struct GitUiState {
pub url: String,
pub use_passphrase: bool,
pub passphrase: String,
}
impl Default for GitUiState {
fn default() -> Self {
let persistent_state = PersistentState::default();
Self {
url: persistent_state.git_url(),
use_passphrase: persistent_state.git_credentials().use_passphrase(),
passphrase: Default::default(),
}
}
}