gled-ui 2.33.0

gled's egui interface, shared by the desktop app and the browser
Documentation
use super::App;
use crate::{key_code::key_code, ui::project_edits::project_edits};
use gled_model::{
    asset::{output_device::routing::OutputRoutings, project::Project},
    input::key_code::KeyCode,
};
use gled_protocol::action::UiAction;
use gled_protocol::state::{ClientInput, EngineEvent, ServerSettings};
use std::collections::BTreeSet;
use std::sync::Arc;

/// What the interface last exchanged with the engine, to tell its own edits
/// from the engine's state.
#[derive(Debug, Default)]
pub(super) struct EngineSync {
    /// The project as last adopted, plus the edits sent since.
    project: Option<Project>,
    /// The replica's `project_changes` when the engine's project was last
    /// compared with `project`.
    compared_changes: u64,
    /// Set when edits were sent, until the engine's project is compared with
    /// `project` again.
    edits_sent: bool,
    /// The output routings as last sent or adopted.
    routings: Arc<OutputRoutings>,
    /// The held keys as last sent.
    keys: Option<BTreeSet<KeyCode>>,
    /// The engine's lamp layout the preview was last built for.
    layout_generation: u64,
}

impl App {
    /// Sends what was edited directly since the last frame, as actions. A
    /// frame without edits copies nothing: no actions mean equal projects.
    pub(super) fn send_local_changes(&mut self) {
        let patch_applied = self.windows.fixture_window.take_patch_applied();
        let actions = project_edits(&self.sync.project, &self.project, patch_applied);
        if !actions.is_empty() || self.sync.project.is_some() != self.project.is_some() {
            self.sync.project = self.project.clone();
            self.sync.edits_sent = true;
        }
        for action in actions {
            action.enqueue();
        }
        if self.output_config.routings != self.sync.routings {
            self.sync.routings = self.output_config.routings.clone();
            UiAction::SetOutputRoutings(self.output_config.routings.clone()).enqueue();
        }
    }

    /// Feeds what the engine published into the client model, as the same
    /// messages a remote interface gets.
    pub(super) fn receive_engine_messages(&mut self) {
        for message in self.connection.receive() {
            self.model.apply(message);
        }
        if std::mem::take(&mut self.model.welcomed) {
            self.connection.reset_sequence();
        }
    }

    /// Takes the engine's newest state from the model. The project and the
    /// routings are adopted only once the engine has applied everything sent,
    /// so edits do not jump back for a frame.
    pub(super) fn receive_engine_state(&mut self) {
        if !std::mem::take(&mut self.model.state_changed) || !self.model.replica.complete {
            return;
        }
        let replica = std::mem::take(&mut self.model.replica);
        let state = &replica.state;

        self.project_id = state.project_id;
        self.selected_scene_instance = state.selected_scene_instance;
        self.controller_line = state.controller_line;
        self.mode_highlight.follow(state.last_mode_switch);
        self.touched_controls.clone_from(&state.touched_controls);
        // The engine resets the effect with the scene or project; a pick
        // here is taken over once the engine applied it.
        if replica.applied_sequence >= self.connection.sent_sequence() {
            self.selected_scene_effect_editor
                .follow_engine(state.selected_scene_effect);
        }
        self.network_stats = (
            f64::from(state.status.network_in),
            f64::from(state.status.network_out),
        );
        self.blackout = state.blackout;
        self.blackout_hold = state.blackout_hold;
        self.midi_output_active = state.midi_output_active;
        self.timing.update(state.beat, state.tap_count);
        self.output_config.universes = state.output_config.universes.clone();

        if replica.applied_sequence >= self.connection.sent_sequence() {
            // Both projects were equal when last compared; they can only
            // differ if the engine's changed or edits were sent since.
            let may_differ =
                replica.project_changes != self.sync.compared_changes || self.sync.edits_sent;
            self.sync.compared_changes = replica.project_changes;
            self.sync.edits_sent = false;
            if (may_differ && state.project.as_deref() != self.sync.project.as_ref())
                || state.output_config.routings != self.sync.routings
            {
                if state.project.is_none() && self.project.is_some() {
                    self.windows.external_device_settings.close();
                    self.windows.output_routings.close();
                    self.windows.shortcuts.close();
                }
                self.project = state.project.as_deref().cloned();
                self.sync.project = state.project.as_deref().cloned();
                self.windows.fixture_window.project_replaced();
                self.output_config.routings = state.output_config.routings.clone();
                self.sync.routings = state.output_config.routings.clone();
            }
        }
        // The engine counts the patch layouts it applied: the preview draws
        // the new one, which the project just adopted holds.
        if state.layout_generation != self.sync.layout_generation {
            self.sync.layout_generation = state.layout_generation;
            self.ui_preview.indices.request_rebuild();
        }
        self.model.replica = replica;
    }

    /// The settings of the machine running the engine.
    pub(crate) fn server_settings(&self) -> &ServerSettings {
        &self.model.replica.state.status.settings
    }

    /// Sends the server settings with `change` applied.
    pub(crate) fn change_server_settings(&self, change: impl FnOnce(&mut ServerSettings)) {
        let mut settings = self.server_settings().clone();
        change(&mut settings);
        ClientInput::SetServerSettings(settings).enqueue();
    }

    pub(super) fn handle_engine_events(&mut self) {
        for event in std::mem::take(&mut self.model.events) {
            match event {
                EngineEvent::ShaderReloaded(animation) => {
                    self.thumbnails.invalidate(animation);
                    self.windows.scenes.invalidate_pipelines(animation);
                }
                EngineEvent::Error(error) => {
                    #[cfg(not(target_arch = "wasm32"))]
                    if let Err(err) = notify_rust::Notification::new()
                        .summary("Error")
                        .body(&error)
                        .icon("application-gled")
                        .show()
                    {
                        tracing::error!("Could not show notification: {err:?}");
                    }
                    self.windows.errors.entries.push(error);
                }
                EngineEvent::Picture { path, picture } => {
                    crate::canvas::pictures::receive(path, picture);
                }
            }
        }
    }

    /// Sends the held keys when they changed. While a text field has the
    /// keyboard, the keys are not sent, as before: typing triggers nothing.
    pub(super) fn send_keys(&mut self, ctx: &egui::Context) {
        if ctx.egui_wants_keyboard_input() {
            return;
        }
        let keys: BTreeSet<KeyCode> =
            ctx.input(|input| input.keys_down.iter().copied().map(key_code).collect());
        if self.sync.keys.as_ref() != Some(&keys) {
            self.sync.keys = Some(keys.clone());
            ClientInput::Keys(keys).enqueue();
        }
    }
}