gled 2.14.0

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 persistant_state;
pub mod preview;
pub mod project_functions;
pub mod status_bar;
pub mod storage;
pub mod svg;
pub mod timing;

use crate::{
    input::Input,
    midi::state::MidiState,
    pipeline::{
        extract_output::ExtractOutput,
        output_sender::{self, GpuReadyReceiver, OutputSender},
        renderer_callback::RendererCallback,
    },
    storage::{
        asset::{
            Asset,
            project::{DeckPath, Project, scene_instance_path::SceneInstancePathId},
        },
        asset_id::AssetId,
        loading,
    },
    ui::{action::UiAction, viewport_builder::default_viewport_builder, windows::Windows},
};
use eframe::egui_wgpu::Callback;
use egui::{CentralPanel, Rect, UiBuilder, ViewportId, ahash::HashSet};
use persistant_state::PersistantState;
use std::{
    sync::mpsc::Receiver,
    time::{Instant, SystemTime, UNIX_EPOCH},
};
use storage::{show_storage_error, show_storage_loading};
use timing::Timing;

pub struct App {
    pub startup: bool,
    pub windows: Windows,
    pub output_sender: OutputSender,
    pub gpu_ready_receiver: GpuReadyReceiver,
    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: SceneInstancePathId,
    pub git_commit_message: String,
    pub ui_action_receiver: Receiver<UiAction>,
    pub last_title_update: u64,
    pub midi_output_active: bool,
}

impl eframe::App for App {
    fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
        ExtractOutput::trigger_output_sender(&self.output_sender);
        self.gpu_ready_receiver
            .recv()
            .expect("GPU ready sender lost");

        if loading().is_none() && self.startup {
            self.startup = false;
            if let Some(project) = PersistantState::get().last_project_id {
                UiAction::SetProject(project).enqueue();
            }
        }

        self.timing.tick();
        Input::tick();
        self.handle_ui_actions();

        if SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_secs()
            != self.last_title_update
        {
            ctx.send_viewport_cmd(egui::ViewportCommand::Title(format!(
                "gled - {} {}",
                match self.project_id.and_then(Asset::get) {
                    None => "no project loaded".to_owned(),
                    Some(project) => project.name().to_owned(),
                },
                match self.timing.framerate() {
                    Some(fps) => format!("({fps:.1} fps)"),
                    None => String::new(),
                }
            )));
            self.last_title_update = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap()
                .as_secs();
        }

        if let Some(project) = &mut self.project {
            project.render(
                &self.timing,
                self.blackout || self.blackout_hold,
                if PersistantState::effects_always_render() || {
                    PersistantState::always_render_fps() > 0.0
                        && self.last_always_render_fps_frame.elapsed().as_secs_f32()
                            > 1.0 / PersistantState::always_render_fps()
                } {
                    self.last_always_render_fps_frame = Instant::now();
                    true
                } else {
                    false
                },
                self.timing.fade_duration(),
            );
        }

        if self.midi_output_active {
            MidiState {
                blackout: self.blackout || self.blackout_hold,
                beat_flank: self.timing.beat_flank(),
                active_scenes: self
                    .project
                    .as_mut()
                    .map_or_else(Default::default, |project| {
                        project
                            .all_scene_instances_index()
                            .filter_map(
                                |(path, scene)| if scene.active { Some(path) } else { None },
                            )
                            .collect()
                    }),
                available_scenes_grid: self
                    .project
                    .as_mut()
                    .map_or(0, |project| project.scenes_instances_grid.len()),
                available_scenes_quick: self
                    .project
                    .as_mut()
                    .map_or(0, |project| project.scenes_instances_quick.len()),
            }
            .enqueue();
        }

        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]),
                |ctx, _viewport_class| {
                    ctx.input(|input| {
                        if input.viewport().close_requested() {
                            UiAction::CloseWindow(viewport_id).enqueue();
                        }
                    });

                    self.draw_main_window(ctx, Some(viewport_id));
                },
            );
        }

        self.windows
            .update(ctx, &self.timing, self.project.as_mut());

        let callback = Callback::new_paint_callback(Rect::ZERO, RendererCallback);
        ctx.debug_painter().add(callback);
        ctx.request_repaint();
    }
}

impl App {
    pub fn draw_main_window(&mut self, ctx: &egui::Context, viewport_id: Option<ViewportId>) {
        let panel_frame = egui::Frame::new()
            .fill(ctx.style().visuals.window_fill())
            .stroke(ctx.style().visuals.widgets.noninteractive.fg_stroke);

        CentralPanel::default().frame(panel_frame).show(ctx, |ui| {
            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 let Some(loading) = crate::storage::loading() {
                show_storage_loading(&mut ui, loading);
                return;
            }

            if self.project.is_some() {
                egui::TopBottomPanel::bottom("scenes_quick")
                    .resizable(false)
                    .exact_height(PersistantState::effects_size() + 80.0)
                    .show_inside(&mut ui, |ui| self.scenes(ui, DeckPath::Quick));
                egui::SidePanel::left("config")
                    .resizable(false)
                    .exact_width(400.0)
                    .show_inside(&mut ui, |ui| self.config(ui));
                egui::TopBottomPanel::top("preview")
                    .resizable(true)
                    .default_height(150.0)
                    .show_inside(&mut ui, |ui| self.preview(ui));
                egui::CentralPanel::default()
                    .show_inside(&mut ui, |ui| self.scenes(ui, DeckPath::Grid));
            } else {
                self.no_project(&mut ui);
            }
        });
    }
    pub fn new(ui_action_receiver: Receiver<UiAction>) -> Option<Self> {
        let (output_sender, gpu_ready_receiver) =
            output_sender::start().expect("Could not start output sender");

        let app = Self {
            startup: true,
            output_sender,
            gpu_ready_receiver,
            timing: Default::default(),
            last_always_render_fps_frame: Instant::now(),
            blackout: true,
            blackout_hold: false,
            selected_scene_instance: 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,
            last_title_update: 0,
            midi_output_active: false,
        };

        Some(app)
    }
}

pub struct GitUiState {
    pub url: String,
    pub use_passphrase: bool,
    pub passphrase: String,
}

impl Default for GitUiState {
    fn default() -> Self {
        let persistant_state = PersistantState::get();

        Self {
            url: persistant_state.git_url,
            use_passphrase: persistant_state.git_credentials.use_passphrase(),
            passphrase: Default::default(),
        }
    }
}