gled 2.28.5

gled is an application for creating animations and effects on artnet or dmx installations
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 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, 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 std::collections::HashSet;
use std::sync::Arc;
use std::time::Instant;
use storage::{show_storage_error, show_storage_loading};
use timing::Timing;

pub struct App {
    pub startup: bool,
    pub windows: Windows,
    pub timing: Timing,
    pub last_always_render_fps_frame: Instant,
    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 extract_output: ExtractOutput,
    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) {
        #[cfg(feature = "profiling")]
        {
            crate::WGPU_PROFILER
                .lock()
                .end_frame()
                .expect("Could not end WGPU profiler frame");
            let latest_profiler_results = crate::WGPU_PROFILER
                .lock()
                .process_finished_frame(crate::wgpu_render_state().queue.get_timestamp_period());
            wgpu_profiler::puffin::output_frame_to_puffin(
                &mut crate::PUFFIN_GPU_PROFILER.lock(),
                latest_profiler_results.as_deref().unwrap_or_default(),
            );
            crate::PUFFIN_GPU_PROFILER.lock().new_frame();
        }

        if let Ok(Some(network_stats)) = self.network_stats_receiver.try_recv() {
            self.network_stats = network_stats;
        }
        self.persistent_state.update();
        self.sound_data.update();
        self.timing
            .tick(self.persistent_state.fps_limit(), &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;
        }

        // update the program state from network signals
        self.external_control_state
            .process_events(&mut self.project, &self.collections);

        self.render_project();
        // Optionally render (and therefore output) a second time on the same
        // displayed frame. The beat position is re-sampled in between so the
        // extra render reflects the freshest timing, doubling the Art-Net/DMX
        // output rate without waiting for another surface present.
        if self.persistent_state.double_render() {
            self.timing.refresh_beat();
            self.render_project();
        }

        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.extract_output,
            &mut self.sound_data,
            &self.midi_monitor_receiver,
            &self.test_command_sender,
            &mut self.midi_learn_state,
        );

        ctx.request_repaint();
    }
}

impl App {
    pub(crate) fn set_selected_scene_instance(&mut self, location: GridLocation) {
        self.selected_scene_instance = location;
        self.selected_scene_effect_editor.reset();
    }

    #[cfg_attr(feature = "profiling", profiling::function)]
    fn render_project(&mut self) {
        if let Some(project) = &mut self.project {
            project.render(
                &self.timing,
                self.blackout || self.blackout_hold,
                if self.persistent_state.effects_always_render() && {
                    self.last_always_render_fps_frame.elapsed().as_secs_f32() > 1.0 / 30.0
                } {
                    self.last_always_render_fps_frame = Instant::now();
                    true
                } else {
                    false
                },
                &self.collections,
                &mut self.extract_output,
                &self.sound_data,
            );
        }
    }

    #[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,
        artnet_control_receiver: Receiver<Vec<u8>>,
        audio_pool: AudioPool,
    ) -> Option<Self> {
        let app = Self {
            startup: true,
            timing: Default::default(),
            last_always_render_fps_frame: Instant::now(),
            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: Default::default(),
            extract_output,
            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(),
        }
    }
}