gled-ui 2.33.0

gled's egui interface, shared by the desktop app and the browser
Documentation
use crate::midi_learn::{LearnState, MidiLearnRequest, MidiLearnTarget};
use egui::{ComboBox, DragValue, Ui};
use egui_phosphor_icons::icons;
use gled_model::asset::midi_controller::{
    MidiController, MidiInputAction, MidiInputActionKind, MidiInputBinding, MidiMode, MidiTrigger,
};
use strum::IntoEnumIterator;

use super::{
    action_converters::{
        effect_target_editor, encoder_editor, input_action_from_kind, scene_target_editor,
    },
    binding_helpers::{input_binding_matches_filter, midi_status_label},
    iconized,
    modes_editor::{BindingFilter, binding_mode_editor, mode_picker},
};

/// An output binding to create for an input binding's control.
pub(super) struct MatchingOutput {
    pub(super) name: String,
    pub(super) status: u8,
    pub(super) data1: u8,
    pub(super) action: MidiInputAction,
    pub(super) mode: Option<u8>,
}

pub(super) fn render_input_column(
    ui: &mut Ui,
    bindings: &mut Vec<MidiInputBinding>,
    modes: &[MidiMode],
    filter: &BindingFilter,
    hovered_status_data1: Option<(u8, u8)>,
    hovered_status_data1_next: &mut Option<(u8, u8)>,
    dirty: &mut bool,
    controller_id: gled_model::asset_id::AssetId<MidiController>,
    learn_state: &mut LearnState,
) -> (Option<usize>, Vec<MatchingOutput>) {
    let mut remove_input = None;
    let mut pending_matching_output = Vec::new();
    let mut duplicate_inputs = Vec::new();

    ui.horizontal(|ui| {
        ui.label(icons::ARROW_SQUARE_IN);
        ui.heading("Input Bindings");
    });
    ui.horizontal(|ui| {
        if ui
            .button(iconized(ui, icons::PLUS, " Add Input Binding"))
            .clicked()
        {
            bindings.insert(
                0,
                MidiInputBinding {
                    mode: filter.mode,
                    ..Default::default()
                },
            );
            *dirty = true;
        }
    });

    for (index, binding) in bindings.iter_mut().enumerate() {
        if !filter.shows_mode(binding.mode) || !input_binding_matches_filter(binding, &filter.text)
        {
            continue;
        }
        let status_data1 = (binding.trigger.status, binding.trigger.data1);
        let highlight = hovered_status_data1.is_some_and(|hovered| hovered == status_data1);
        let base_stroke = ui.visuals().widgets.noninteractive.bg_stroke;
        let response = egui::Frame::group(ui.style())
            .stroke(if highlight {
                egui::Stroke::new(base_stroke.width, ui.visuals().selection.stroke.color)
            } else {
                base_stroke
            })
            .show(ui, |ui| {
                ui.horizontal(|ui| {
                    ui.label(format!("Input {}", index + 1));
                    let learning = learn_state.active_request().is_some_and(|request| {
                        request.controller_id == controller_id
                            && request.target == MidiLearnTarget::InputBinding(index)
                    });
                    if ui
                        .button(if learning {
                            iconized(ui, icons::MICROPHONE, " Waiting for MIDI...")
                        } else {
                            iconized(ui, icons::MICROPHONE, " Learn")
                        })
                        .clicked()
                    {
                        learn_state.arm(MidiLearnRequest {
                            controller_id,
                            target: MidiLearnTarget::InputBinding(index),
                        });
                    }
                    if ui
                        .button(iconized(ui, icons::PLUS, " Duplicate"))
                        .clicked()
                    {
                        duplicate_inputs.push(binding.clone());
                    }
                    if ui.button(iconized(ui, icons::TRASH, " Remove")).clicked() {
                        remove_input = Some(index);
                    }
                });

                if ui.text_edit_singleline(&mut binding.name).changed() {
                    *dirty = true;
                }
                binding_mode_editor(
                    ui,
                    &mut binding.mode,
                    modes,
                    dirty,
                    format!("{controller_id}_input_mode_{index}"),
                );

                ui.horizontal(|ui| {
                    ui.label("Status");
                    if ui
                        .add(DragValue::new(&mut binding.trigger.status).range(0..=255))
                        .changed()
                    {
                        *dirty = true;
                    }
                    ui.label("Data1");
                    if ui
                        .add(DragValue::new(&mut binding.trigger.data1).range(0..=127))
                        .changed()
                    {
                        *dirty = true;
                    }
                });

                ui.label(format!(
                    "Detected Message: {}",
                    midi_status_label(binding.trigger.status)
                ));

                if ui
                    .checkbox(
                        &mut binding.trigger.match_data1,
                        "Match Data1 (disable for Pitch Bend style controls)",
                    )
                    .changed()
                {
                    *dirty = true;
                }
                touch_editor(ui, &mut binding.touch, dirty);

                let mut action_kind = MidiInputActionKind::from(&binding.action);
                ComboBox::new(format!("{}_input_kind_{index}", controller_id), "")
                    .selected_text(action_kind.to_string())
                    .show_ui(ui, |ui| {
                        for candidate in MidiInputActionKind::iter() {
                            let response = ui.selectable_value(
                                &mut action_kind,
                                candidate,
                                candidate.to_string(),
                            );
                            if candidate == MidiInputActionKind::SetSceneEffectLayout {
                                response.on_hover_text(
                                    "0 lets the effect follow its tile's layout, 1 is the topology, \
                                     2 and up the layouts in stack order",
                                );
                            }
                        }
                    });

                let before = binding.action.clone();
                binding.action = input_action_from_kind(action_kind, binding.action.clone());
                if binding.action != before {
                    *dirty = true;
                }

                match &mut binding.action {
                    gled_model::asset::midi_controller::MidiInputAction::SelectScene { target }
                    | gled_model::asset::midi_controller::MidiInputAction::ToggleSceneActive {
                        target
                    }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneActive { target }
                    | gled_model::asset::midi_controller::MidiInputAction::FlashScene { target }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneOpacity { target }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneInputDimmer {
                        target
                    }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneBeatOffset {
                        target
                    }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneIgnoreMainDimmer {
                        target
                    }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneSetOffsetOnFlash {
                        target
                    }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectSettingF32 {
                        target,
                        ..
                    }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectOpacity { target, .. }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectColorShift { target, .. }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectBeatProgression { target, .. }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectBeatOffset { target, .. }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectSpeedExponent { target, .. }
                    | gled_model::asset::midi_controller::MidiInputAction::SetSceneEffectLayout { target, .. } => {
                        scene_target_editor(
                            ui,
                            target,
                            dirty,
                            format!("{}_input_target_{index}", controller_id),
                        );
                    }
                    _ => {}
                }

                match &mut binding.action {
                    MidiInputAction::SetSceneEffectSettingF32 {
                        effect,
                        setting_index,
                        ..
                    } => {
                        effect_target_editor(
                            ui,
                            effect,
                            dirty,
                            format!("{controller_id}_input_effect_{index}"),
                        );
                        ui.horizontal(|ui| {
                            ui.label("Setting Index");
                            if ui.add(DragValue::new(setting_index).range(0..=5)).changed() {
                                *dirty = true;
                            }
                        });
                    }
                    MidiInputAction::SetSceneEffectOpacity { effect, .. }
                    | MidiInputAction::SetSceneEffectColorShift { effect, .. }
                    | MidiInputAction::SetSceneEffectBeatProgression { effect, .. }
                    | MidiInputAction::SetSceneEffectBeatOffset { effect, .. }
                    | MidiInputAction::SetSceneEffectSpeedExponent { effect, .. }
                    | MidiInputAction::SetSceneEffectLayout { effect, .. } => {
                        effect_target_editor(
                            ui,
                            effect,
                            dirty,
                            format!("{controller_id}_input_effect_{index}"),
                        );
                    }
                    MidiInputAction::SelectMode { mode } => {
                        mode_picker(
                            ui,
                            mode,
                            modes,
                            dirty,
                            format!("{controller_id}_input_select_mode_{index}"),
                        );
                    }
                    MidiInputAction::SelectSceneEffect { index: effect_index } => {
                        ui.horizontal(|ui| {
                            ui.label("Effect Index");
                            if ui.add(DragValue::new(effect_index).range(0..=255)).changed() {
                                *dirty = true;
                            }
                        });
                    }
                    MidiInputAction::SetMainDimmerRelative { encoder }
                    | MidiInputAction::SetSpeedRelative { encoder }
                    | MidiInputAction::SelectSceneRelative { encoder }
                    | MidiInputAction::SelectSceneRow { encoder }
                    | MidiInputAction::SelectSceneColumn { encoder }
                    | MidiInputAction::SelectSceneEffectRelative { encoder } => {
                        encoder_editor(
                            ui,
                            encoder,
                            dirty,
                            format!("{controller_id}_input_encoding_{index}"),
                        );
                    }
                    _ => {}
                }

                if matches!(
                    binding.action,
                    gled_model::asset::midi_controller::MidiInputAction::SelectSceneDistributed
                ) {
                    ui.small("Maps value 0..127 evenly across existing scenes in row-major order.");
                }

                if ui
                    .button(iconized(ui, icons::PLUS, " Create Matching Output Binding"))
                    .clicked()
                {
                    pending_matching_output.push(MatchingOutput {
                        name: binding.name.clone(),
                        status: binding.trigger.status,
                        data1: binding.trigger.data1,
                        action: binding.action.clone(),
                        mode: binding.mode,
                    });
                }
            })
            .response;
        if response.hovered() {
            *hovered_status_data1_next = Some(status_data1);
        }
    }

    if !duplicate_inputs.is_empty() {
        bindings.extend(duplicate_inputs);
        *dirty = true;
    }

    (remove_input, pending_matching_output)
}

/// The note a touch-sensitive control sends while a finger rests on it.
fn touch_editor(ui: &mut Ui, touch: &mut Option<MidiTrigger>, dirty: &mut bool) {
    let mut touch_sensitive = touch.is_some();
    if ui
        .checkbox(&mut touch_sensitive, "Touch note")
        .on_hover_text(
            "A note the control sends while it is touched, such as a motor fader's; \
             the interface outlines what the binding changes meanwhile",
        )
        .changed()
    {
        *touch = touch_sensitive.then_some(MidiTrigger {
            status: 0x90,
            data1: 0,
            match_data1: true,
        });
        *dirty = true;
    }
    if let Some(touch) = touch {
        ui.horizontal(|ui| {
            ui.label("Touch Status");
            if ui
                .add(DragValue::new(&mut touch.status).range(0x90..=0x9F))
                .changed()
            {
                *dirty = true;
            }
            ui.label("Note");
            if ui
                .add(DragValue::new(&mut touch.data1).range(0..=127))
                .changed()
            {
                *dirty = true;
            }
        });
    }
}