gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
mod action_converters;
mod color_editor;
mod copy_dialog;
mod editor;
mod feedback_lines;
mod intent;
mod learning;
mod list_state;
mod midi_details;
mod pickers;
mod rule_card;
mod rule_list;
mod rules;
mod side_tabs;
mod status_line;
mod test_panel;
mod token_editors;
mod top_bar;
mod value_source_converters;
mod widgets;

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::{EditorContext, EditorState, 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 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>,
    editor: EditorState,
    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>,
}

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::CentralPanel::default().show(ui, |ui| {
                        self.tree.common_settings(ui, &mut self.dirty, collections);

                        match self.tree.selected() {
                            TreeSelection::Asset(controller) => {
                                ui.separator();
                                let others: Vec<OtherController> = collections
                                    .get::<MidiController>()
                                    .map(|collection| {
                                        collection
                                            .shared()
                                            .filter(|other| other.id != controller.id)
                                            .map(|other| OtherController {
                                                id: other.id,
                                                name: other.name().to_owned(),
                                                ports: other.data.default_for_ports.clone(),
                                            })
                                            .collect()
                                    })
                                    .unwrap_or_default();
                                let mut take_over = None;
                                let test_state = self
                                    .test_state_by_controller
                                    .entry(controller.id)
                                    .or_default();
                                midi_controller_editor(
                                    ui,
                                    controller,
                                    &mut self.editor,
                                    EditorContext {
                                        test_state,
                                        test_command_sender,
                                        dirty: &mut self.dirty,
                                        project,
                                        diagnostics: &diagnostics,
                                        learn_state,
                                        events: &mut self.events,
                                        others: &others,
                                        take_over: &mut take_over,
                                    },
                                );
                                if let Some((other, port)) = take_over {
                                    stop_using_port(collections, other, &port);
                                }
                            }
                            _ => {
                                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;
        }
    }
}

/// Another controller, for the devices it is used for.
#[derive(Debug, Clone)]
pub(crate) struct OtherController {
    pub(crate) id: gled_model::asset_id::AssetId<MidiController>,
    pub(crate) name: String,
    pub(crate) ports: Vec<String>,
}

/// Saves controller `id` without `port` among its devices.
fn stop_using_port(
    collections: &mut Collections,
    id: gled_model::asset_id::AssetId<MidiController>,
    port: &str,
) {
    use crate::assets::AssetEdits;
    let Some(other) = gled_model::asset::Asset::get(id, collections) else {
        return;
    };
    let mut other = (*other).clone();
    other
        .data
        .default_for_ports
        .retain(|other_port| other_port != port);
    other.save(collections);
}

#[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);
    }
}