gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The MIDI controllers plugged into this interface's own machine, or
//! offered by its browser, while it is connected to a remote engine (beside
//! its own engine, the engine hosts the ports). Driven once per frame with
//! the engine's state as this interface sees it; what the controllers do
//! goes to the engine as this interface's actions.

use std::{collections::BTreeSet, sync::Arc};

use gled_midi::{
    HostEvent, MidiRuntimeSnapshot, MidiSource, SnapshotCache,
    client::{ActionSink, ClientHost, ClientMidi, Frame},
    live_values, port_matches,
};
use gled_protocol::{
    action::UiAction,
    message::ClientMessage,
    midi::TestCommand,
    state::{ClientInput, EngineEvent},
};

use super::App;
use crate::ui::frame_requests::LocalMidiStatus;

#[derive(Debug)]
pub(crate) struct LocalMidi {
    host: ClientHost,
    /// The mappings, rebuilt when the engine's project or assets changed.
    snapshots: SnapshotCache<(u64, u64)>,
    input_ports: Vec<String>,
    output_ports: Vec<String>,
    /// Whether the test panel's device is one of these ports, so its
    /// commands go here instead of to the engine.
    test_device_here: bool,
}

impl LocalMidi {
    /// What the windows show of these ports.
    pub(super) fn status(&self) -> LocalMidiStatus {
        LocalMidiStatus {
            availability: Some(self.host.availability()),
            ports: self
                .input_ports
                .iter()
                .chain(&self.output_ports)
                .cloned()
                .collect::<BTreeSet<_>>(),
        }
    }
}

impl App {
    /// Starts hosting this machine's ports once connected to a remote
    /// engine, then drives them with this frame's state.
    pub(super) fn drive_local_midi(&mut self, ctx: &egui::Context) {
        if self.local_midi.is_none() && self.connection.is_remote() {
            let ctx = ctx.clone();
            // A controller's actions go out at once, unnumbered, even while a
            // long frame keeps the interface busy.
            let direct = self.connection.background_sender().map(|send| {
                Box::new(move |actions: Vec<UiAction>| {
                    for action in actions {
                        send(ClientMessage::Input(ClientInput::Action {
                            sequence: 0,
                            action,
                        }));
                    }
                }) as ActionSink
            });
            let mut host = ClientHost::new(move || ctx.request_repaint(), direct);
            if self.settings.midi_access_wanted() {
                host.request_access();
            }
            self.local_midi = Some(LocalMidi {
                host,
                snapshots: SnapshotCache::default(),
                input_ports: Vec::new(),
                output_ports: Vec::new(),
                test_device_here: false,
            });
        }
        let Some(local) = &mut self.local_midi else {
            return;
        };
        let replica = &self.model.replica;
        let state = &replica.state;
        let project = state.project.as_deref();
        let snapshot = local
            .snapshots
            .get((replica.project_changes, replica.assets_changes), || {
                MidiRuntimeSnapshot::build(project, &replica.assets)
            });
        let beat_progression = self.timing.beat_progression_now();
        let live = live_values::selected_scene_live_values(
            project,
            state.selected_scene_instance,
            &replica.assets,
            &*self.model.sound,
            beat_progression,
        );
        let source = MidiSource {
            project: state.project.clone(),
            selected_scene_instance: state.selected_scene_instance,
            selected_scene_effect: state.selected_scene_effect,
            selected_scene_live: Arc::new(live),
            blackout: state.blackout || state.blackout_hold,
            beats_per_minute: self.timing.beats_per_minute(),
            beat_progression,
            midi_modes: state.midi_modes.clone(),
        };
        for event in local.host.tick(Frame { source, snapshot }) {
            match event {
                // Unnumbered, as the desktop sends them: a numbered action
                // holds back the engine's project until it is applied, which
                // a moving fader never lets happen.
                HostEvent::Actions(actions) => {
                    for action in actions {
                        ClientInput::Action {
                            sequence: 0,
                            action,
                        }
                        .enqueue();
                    }
                }
                HostEvent::Monitor(event) => {
                    let _ = self.midi_monitor_sender.send(event);
                }
                HostEvent::Learn(message) => self.midi_learn_state.feed(message),
                HostEvent::Error(message) => self.model.events.push(EngineEvent::Error(message)),
                HostEvent::InputPorts(ports) => local.input_ports = ports,
                HostEvent::OutputPorts(ports) => local.output_ports = ports,
            }
        }
        self.midi_output_active |= !local.output_ports.is_empty();
    }

    /// Lets this machine's ports stream to the monitor and capture for
    /// learning while a window asks the engine to.
    pub(super) fn set_local_midi_gates(&self, watching: bool, learning: bool) {
        if let Some(local) = &self.local_midi {
            local.host.gates().set(watching, learning);
        }
    }

    /// Asks the browser for its MIDI devices, and remembers that the user
    /// wants them.
    pub(super) fn request_local_midi_access(&mut self) {
        if let Some(local) = &mut self.local_midi {
            local.host.request_access();
            self.settings.want_midi_access();
        }
    }

    /// Sends a test panel command to the port it is for: one of this
    /// machine's, or the engine's.
    pub(super) fn route_test_command(&mut self, command: TestCommand) {
        let Some(local) = &mut self.local_midi else {
            ClientInput::MidiTest(command).enqueue();
            return;
        };
        match &command {
            TestCommand::SetTestDevice { port_name } => {
                local.test_device_here = local
                    .output_ports
                    .iter()
                    .any(|port| port_matches(port, port_name));
                // The other side stops testing the previous device.
                let other = TestCommand::UnsetTestDevice;
                if local.test_device_here {
                    ClientInput::MidiTest(other).enqueue();
                } else {
                    local.host.test(other);
                }
            }
            TestCommand::UnsetTestDevice => {
                local.host.test(TestCommand::UnsetTestDevice);
                ClientInput::MidiTest(command).enqueue();
                return;
            }
            TestCommand::SendOutput { .. } | TestCommand::ClearOutput { .. } => {}
        }
        if local.test_device_here {
            local.host.test(command);
        } else {
            ClientInput::MidiTest(command).enqueue();
        }
    }
}