gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
use super::binding_helpers::{add_matching_output_binding, output_binding_status_data1};
use super::color_editor::render_color_mappings_section;
use super::input_column::render_input_column;
use super::learn_capture::apply_learn_captures;
use super::modes_editor::{BindingFilter, default_ports_section, modes_section};
use super::output_column::render_output_column;
use super::test_panel::render_test_device_box;
use super::{MidiControllerTestState, iconized};
use crate::midi_learn::{LearnState, MidiPortDiagnostics};
use crate::ui::frame_requests;
use egui::Ui;
use egui_phosphor_icons::icons;
use gled_model::{
    asset::{
        Asset,
        midi_controller::{MidiController, normalize_controller_key},
    },
    collection::Collections,
};
use gled_protocol::midi::TestCommand;
use kanal::Sender;

pub(super) fn midi_controller_editor(
    ui: &mut Ui,
    controller: &mut Asset<MidiController>,
    test_state: &mut MidiControllerTestState,
    test_command_sender: &Sender<TestCommand>,
    dirty: &mut bool,
    project: &mut Option<gled_model::asset::project::Project>,
    collections: &mut Collections,
    filter: &mut BindingFilter,
    diagnostics: &[MidiPortDiagnostics],
    learn_state: &mut LearnState,
) {
    learn_state.flush_timeouts();
    apply_learn_captures(controller, dirty, collections, learn_state);

    egui::ScrollArea::vertical().show(ui, |ui| {
        ui.horizontal(|ui| {
            ui.label(iconized(ui, icons::FUNNEL, " Filter:"));
            ui.text_edit_singleline(&mut filter.text);
            if !filter.text.is_empty() && ui.button(icons::X).clicked() {
                filter.text.clear();
            }
        });
        ui.add_space(4.0);

        let controller_id = controller.id;
        let id = controller_id.to_string();
        let known_ports: Vec<String> = diagnostics
            .iter()
            .map(|port| port.port_name.clone())
            .chain(frame_requests::local_midi().ports)
            .collect();
        default_ports_section(
            ui,
            &mut controller.data.default_for_ports,
            &known_ports,
            dirty,
            &id,
        );
        let mapping = &mut controller.data.mapping;
        modes_section(ui, mapping, filter, dirty, &id);
        ui.add_space(4.0);
        let test_device_selected = test_state.selected_output_port.is_some();
        let hovered_status_data1 = test_state.hovered_status_data1;
        let mut hovered_status_data1_next = None;

        let selected_test_port = render_test_device_box(
            ui,
            test_state,
            diagnostics,
            controller_id,
            test_command_sender,
        );
        if let Some(port_name) = selected_test_port {
            disconnect_project_mapping_for_test_port(project, &port_name);
        }
        ui.add_space(6.0);

        test_state.value_preview_binding_indices.clear();
        test_state.blackout_blink_preview_binding_indices.clear();

        let mut remove_input = None;
        let mut pending_matching_output = Vec::new();
        let mut remove_output = None;

        ui.columns(2, |cols| {
            let (left, right) = cols.split_at_mut(1);
            let (ri, pmo) = render_input_column(
                &mut left[0],
                &mut mapping.input_bindings,
                &mapping.modes,
                filter,
                hovered_status_data1,
                &mut hovered_status_data1_next,
                dirty,
                controller_id,
                learn_state,
            );
            remove_input = ri;
            pending_matching_output = pmo;
            remove_output = render_output_column(
                &mut right[0],
                &mut mapping.output_bindings,
                &mut mapping.color_mappings,
                &mapping.modes,
                test_device_selected,
                filter,
                hovered_status_data1,
                &mut hovered_status_data1_next,
                &mut test_state.value_preview_binding_indices,
                &mut test_state.blackout_blink_preview_binding_indices,
                dirty,
                controller_id,
                learn_state,
            );
        });

        test_state.hovered_status_data1 = hovered_status_data1_next;

        ui.separator();
        render_color_mappings_section(
            ui,
            &mut mapping.color_mappings,
            &mapping.output_bindings,
            test_command_sender,
            dirty,
            controller_id,
        );

        for input in &pending_matching_output {
            if add_matching_output_binding(mapping, input) {
                *dirty = true;
            }
        }

        if let Some(index) = remove_input {
            mapping.input_bindings.remove(index);
            *dirty = true;
        }

        if let Some(index) = remove_output {
            // If this output was being previewed, clear it
            if (test_state.value_preview_binding_indices.contains(&index)
                || test_state
                    .value_preview_binding_indices_previous
                    .contains(&index)
                || test_state
                    .blackout_blink_preview_binding_indices
                    .contains(&index)
                || test_state
                    .blackout_blink_preview_binding_indices_previous
                    .contains(&index))
                && let Some((status, data1)) = output_binding_status_data1(
                    &mapping
                        .output_bindings
                        .get(index)
                        .cloned()
                        .unwrap_or_default(),
                    &mapping.color_mappings,
                )
            {
                let _ = test_command_sender.send(TestCommand::ClearOutput { status, data1 });
            }
            mapping.output_bindings.remove(index);
            *dirty = true;
        }

        // Emit commands for preview state transitions
        emit_test_output_commands(&mapping.output_bindings, test_state, test_command_sender);
    });
}

fn disconnect_project_mapping_for_test_port(
    project: &mut Option<gled_model::asset::project::Project>,
    selected_port_name: &str,
) {
    let Some(project) = project.as_mut() else {
        return;
    };

    let normalized_selected = normalize_controller_key(selected_port_name);
    project
        .midi_active_mappings
        .retain(|port_name, _| normalize_controller_key(port_name) != normalized_selected);
}

/// Emit SendOutput/ClearOutput commands based on changes in preview binding indices
fn emit_test_output_commands(
    bindings: &[gled_model::asset::midi_controller::MidiOutputBinding],
    test_state: &mut MidiControllerTestState,
    test_command_sender: &Sender<TestCommand>,
) {
    use gled_model::asset::midi_controller::{MidiOutputBindingKind, MidiValueSource};

    // Send active preview values for all currently previewed value bindings.
    for &index in &test_state.value_preview_binding_indices {
        if let Some(binding) = bindings.get(index)
            && let MidiOutputBindingKind::Value(value_output) = &binding.kind
        {
            let _ = test_command_sender.send(TestCommand::SendOutput {
                status: value_output.status,
                data1: value_output.data1,
                value: value_output.active_value,
            });
        }
    }

    // Detect newly inactive bindings (in previous but not in current)
    for &index in &test_state.value_preview_binding_indices_previous {
        if !test_state.value_preview_binding_indices.contains(&index)
            && let Some(binding) = bindings.get(index)
            && let MidiOutputBindingKind::Value(value_output) = &binding.kind
        {
            let _ = test_command_sender.send(TestCommand::ClearOutput {
                status: value_output.status,
                data1: value_output.data1,
            });
        }
    }

    test_state.value_preview_binding_indices_previous =
        test_state.value_preview_binding_indices.clone();

    // Send blackout blink preview values.
    for &index in &test_state.blackout_blink_preview_binding_indices {
        if let Some(binding) = bindings.get(index)
            && let MidiOutputBindingKind::Value(value_output) = &binding.kind
            && let MidiValueSource::BeatFlankPulse {
                blackout_blink_value: Some(v),
                ..
            } = &value_output.source
        {
            let _ = test_command_sender.send(TestCommand::SendOutput {
                status: value_output.status,
                data1: value_output.data1,
                value: *v,
            });
        }
    }

    // Clear blackout blink previews that became inactive.
    for &index in &test_state.blackout_blink_preview_binding_indices_previous {
        if !test_state
            .blackout_blink_preview_binding_indices
            .contains(&index)
            && let Some(binding) = bindings.get(index)
            && let MidiOutputBindingKind::Value(value_output) = &binding.kind
        {
            let _ = test_command_sender.send(TestCommand::ClearOutput {
                status: value_output.status,
                data1: value_output.data1,
            });
        }
    }

    test_state.blackout_blink_preview_binding_indices_previous =
        test_state.blackout_blink_preview_binding_indices.clone();
}