gled-ui 2.31.2

gled's egui interface, shared by the desktop app and the browser
Documentation
mod action_converters;
mod binding_helpers;
mod color_editor;
mod editor;
mod input_column;
mod learn_capture;
mod monitor_panel;
mod output_column;
mod test_panel;
mod value_editor;
mod value_source_converters;

use crate::midi_learn::MidiPortDiagnostics;
use crate::{
    midi_learn::LearnState,
    ui::{
        asset_tree::{AssetTree, TREE_WIDTH, TreeSelection},
        window_common::{gled_window_frame, raise, tool_window_builder},
    },
};
use editor::midi_controller_editor;
use egui::epaint::text::{LayoutJob, TextFormat};
use egui::{Align, Context, FontSelection, Id, Ui, Vec2, ViewportId};
use egui_phosphor_icons::Icon;
use gled_midi::monitor::label;
use gled_model::{asset::midi_controller::MidiController, collection::Collections};
use gled_protocol::midi::MidiMonitorEvent;
use gled_protocol::midi::TestCommand;
use kanal::{Receiver, Sender};
use monitor_panel::show_live_midi_monitor;
use std::collections::{HashMap, HashSet, VecDeque};

const MAX_EVENTS: usize = 1000;
fn iconized(ui: &Ui, icon: Icon, text: &str) -> LayoutJob {
    let mut layout_job = LayoutJob::default();
    icon.regular().append_to(
        &mut layout_job,
        ui.style(),
        FontSelection::Default,
        Align::Center,
    );
    layout_job.append(text, 0.0, TextFormat::default());
    layout_job
}

#[derive(Debug, Default)]
pub(crate) struct MidiControllersWindow {
    open: bool,
    /// Set while the window still has to be brought forward.
    raise: bool,
    dirty: bool,
    tree: AssetTree<MidiController>,
    filter: String,
    events: VecDeque<MidiMonitorEvent>,
    test_state_by_controller:
        HashMap<gled_model::asset_id::AssetId<MidiController>, MidiControllerTestState>,
    /// The engine was told to stop the test output since the window closed,
    /// so a closed window sends that once, not every frame.
    test_output_released: bool,
}

#[derive(Debug, Clone, Default)]
struct MidiControllerTestState {
    selected_output_port: Option<String>,
    value_preview_binding_indices: HashSet<usize>,
    value_preview_binding_indices_previous: HashSet<usize>,
    blackout_blink_preview_binding_indices: HashSet<usize>,
    blackout_blink_preview_binding_indices_previous: HashSet<usize>,
    hovered_status_data1: Option<(u8, u8)>,
}

impl MidiControllersWindow {
    fn poll(&mut self, receiver: &Receiver<MidiMonitorEvent>) {
        while let Ok(Some(event)) = receiver.try_recv() {
            if self.events.len() >= MAX_EVENTS {
                self.events.pop_front();
            }
            self.events.push_back(event);
        }
    }

    pub(crate) fn diagnostics(&self) -> Vec<MidiPortDiagnostics> {
        let mut by_port: HashMap<String, MidiPortDiagnostics> = HashMap::new();
        for event in &self.events {
            let entry =
                by_port
                    .entry(event.port_name.clone())
                    .or_insert_with(|| MidiPortDiagnostics {
                        port_name: event.port_name.clone(),
                        ..Default::default()
                    });
            match event.label.as_str() {
                label::INPUT_CONNECTED => entry.input_connected = true,
                label::OUTPUT_CONNECTED => entry.output_connected = true,
                label::INPUT_CONNECT_FAILED => entry.input_connect_failed += 1,
                label::OUTPUT_CONNECT_FAILED => entry.output_connect_failed += 1,
                label::MIDI => entry.message_count += 1,
                _ => {}
            }
        }
        let mut list: Vec<_> = by_port.into_values().collect();
        list.sort_by(|a, b| a.port_name.cmp(&b.port_name));
        list
    }
    #[cfg_attr(feature = "profiling", profiling::function)]
    pub(crate) fn update(
        &mut self,
        ctx: &Context,
        project: &mut Option<gled_model::asset::project::Project>,
        collections: &mut Collections,
        receiver: &Receiver<MidiMonitorEvent>,
        test_command_sender: &Sender<TestCommand>,
        learn_state: &mut LearnState,
    ) {
        if self.open {
            crate::ui::frame_requests::watch_midi();
        }
        learn_state.set_streaming_enabled(self.open);
        self.poll(receiver);

        if !self.open {
            self.release_test_output(test_command_sender);
            return;
        }
        self.test_output_released = false;

        let diagnostics = self.diagnostics();
        ctx.show_viewport_immediate(
            ViewportId(Id::new("midi controllers window")),
            tool_window_builder("MIDI Controllers")
                .with_inner_size(Vec2::new(1400.0, 740.0))
                .with_min_inner_size(Vec2::new(1240.0, 740.0)),
            |ctx, _viewport_class| {
                raise(ctx, &mut self.raise);
                ctx.input(|input| {
                    if input.viewport().close_requested()
                        || crate::ui::window_common::embedded_close_clicked("MIDI Controllers")
                    {
                        self.open = false;
                        learn_state.set_streaming_enabled(false);
                    }
                });

                gled_window_frame(ctx, "MIDI Controllers", |ui| {
                    egui::Panel::left("midi controllers tree")
                        .exact_size(TREE_WIDTH)
                        .resizable(false)
                        .show(ui, |ui| {
                            if self.tree.show(
                                ui,
                                ui.make_persistent_id("midi_controllers_tree"),
                                collections,
                            ) {
                                self.dirty = false;
                            }
                        });

                    egui::Panel::right("midi_monitor_panel")
                        .exact_size(330.0)
                        .resizable(false)
                        .show(ui, |ui| {
                            show_live_midi_monitor(ui, &mut self.events, &diagnostics);
                        });

                    egui::CentralPanel::default().show(ui, |ui| {
                        self.tree.common_settings(ui, &mut self.dirty, collections);

                        match self.tree.selected() {
                            TreeSelection::Asset(controller) => {
                                ui.separator();
                                let test_state = self
                                    .test_state_by_controller
                                    .entry(controller.id)
                                    .or_default();
                                midi_controller_editor(
                                    ui,
                                    controller,
                                    test_state,
                                    test_command_sender,
                                    &mut self.dirty,
                                    project,
                                    collections,
                                    &mut self.filter,
                                    &diagnostics,
                                    learn_state,
                                );
                            }
                            _ => {
                                for state in self.test_state_by_controller.values_mut() {
                                    state.value_preview_binding_indices.clear();
                                }
                            }
                        }
                    });
                });
            },
        );
    }

    pub(crate) fn open(&mut self) {
        self.open = true;
        self.raise = true;
    }

    /// Stops the test output once after the window closed.
    fn release_test_output(&mut self, test_command_sender: &Sender<TestCommand>) {
        if !self.test_output_released {
            let _ = test_command_sender.send(TestCommand::UnsetTestDevice);
            self.test_output_released = true;
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn a_closed_window_stops_the_test_output_once() {
        let (sender, receiver) = kanal::unbounded();
        let mut window = MidiControllersWindow::default();

        window.release_test_output(&sender);
        window.release_test_output(&sender);
        assert_eq!(receiver.len(), 1);

        // Opened and closed again: stopped once more.
        window.test_output_released = false;
        window.release_test_output(&sender);
        assert_eq!(receiver.len(), 2);
    }
}