gled-ui 2.32.1

gled's egui interface, shared by the desktop app and the browser
Documentation
mod midi_mappings;
mod settings_menu;

use crate::midi_learn::MidiPortDiagnostics;
use crate::ui::asset::CollectionsChangeButton;
use crate::ui::window_common::{gled_window_frame, raise, tool_window_builder};
use chrono::Local;
use egui::{
    Button, ComboBox, Context, DragValue, Id, Layout, RichText, Slider, Ui, Vec2, ViewportId,
    WidgetText,
};
use egui_phosphor_icons::icons;
use gled_model::asset::project::Project;
use gled_model::asset::{Asset, output_device::routing::OutputRouting};
use gled_model::collection::Collections;
use gled_model::patching::fixture::MAX_UNIVERSE;
use gled_protocol::action::UiAction;
use gled_protocol::state::ServerStatus;
use midi_mappings::{
    collect_midi_controller_rows, collect_midi_mapping_options, midi_mapping_settings,
};
use settings_menu::SettingsMenu;

#[derive(Debug, Default)]
pub(crate) struct ExternalDeviceSettings {
    open: bool,
    /// Set while the window still has to be brought forward.
    raise: bool,
    selected_submenu: String,
}

impl ExternalDeviceSettings {
    #[cfg_attr(feature = "profiling", profiling::function)]
    pub(crate) fn update(
        &mut self,
        ctx: &Context,
        project: &mut Option<Project>,
        collections: &mut Collections,
        midi_diagnostics: &[MidiPortDiagnostics],
        status: &ServerStatus,
    ) {
        if !self.open {
            return;
        }
        let mut project = if let Some(project) = project {
            project
        } else {
            return;
        };

        ctx.show_viewport_immediate(
            ViewportId(Id::new("external devices settings window")),
            tool_window_builder("External Devices")
                .with_inner_size(Vec2::new(800.0, 500.0))
                .with_min_inner_size(Vec2::new(800.0, 500.0))
                .with_resizable(false)
                .with_minimize_button(false)
                .with_maximize_button(true),
            |ctx, _viewport_class| {
                raise(ctx, &mut self.raise);
                ctx.input(|input| {
                    if input.viewport().close_requested()
                        || crate::ui::window_common::embedded_close_clicked("External Devices")
                    {
                        self.open = false;
                    }
                });

                gled_window_frame(ctx, "External Devices", |ui| {
                    let midi_mapping_rows =
                        collect_midi_controller_rows(project, midi_diagnostics, collections);
                    let midi_mapping_options = collect_midi_mapping_options(collections);
                    let settings_menu = SettingsMenu::new(&mut project, &mut self.selected_submenu)
                        .add_submenu("Audio Input".into(), |ui, project| {
                            audio_input_settings(ui, project)
                        })
                        .add_submenu("MIDI Mappings".into(), |ui, project| {
                            midi_mapping_settings(
                                ui,
                                project,
                                &midi_mapping_rows,
                                &midi_mapping_options,
                            )
                        })
                        .add_submenu("Artnet Bridge".into(), |ui, project| {
                            artnet_bridge_settings(ui, project, collections, status)
                        })
                        .add_submenu("Artnet Control".into(), |ui, project| {
                            artnet_control_input_settings(ui, project)
                        })
                        .add_submenu("Artnet Trigger".into(), |ui, project| {
                            artnet_trigger_settings(ui, project, status)
                        })
                        .add_submenu("OSC Input".into(), |ui, project| osc_settings(ui, project));
                    ui.add(settings_menu);
                });
            },
        );
    }

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

    pub(crate) fn close(&mut self) {
        self.open = false;
    }
}

fn audio_input_settings(ui: &mut Ui, state: &mut Project) {
    let project = state;
    ui.heading("Audio Input Device");
    let mut selected = project.audio_input_device.clone();
    let old_selected = selected.clone();

    // The engine's machine scans for devices while this list shows.
    ui.selectable_value(&mut selected, None, "None");
    for (id, label) in crate::ui::frame_requests::devices().audio_inputs {
        ui.selectable_value(&mut selected, Some(id), label);
    }

    if selected != old_selected {
        tracing::info!("Audio input device changed to {:?}", selected);
        {
            UiAction::SetAudioDevice(selected).enqueue();
        }
    }
}

fn artnet_control_input_settings(ui: &mut Ui, project: &mut Project) {
    // A project setting; the engine applies changes to the Art-Net input.
    let config = &mut project.artnet_control_config;
    ui.heading("Artnet Control");
    ui.checkbox(&mut config.active, "Active");
    ui.add(DragValue::new(&mut config.universe).range(0..=MAX_UNIVERSE));
}

/// Edits a copy of the running Art-Net config, which also shows what arrived;
/// a change goes into the project, from which the engine applies it.
fn artnet_bridge_settings(
    ui: &mut Ui,
    project: &mut Project,
    collections: &mut Collections,
    status: &ServerStatus,
) {
    let mut config = status.artnet.clone();
    ui.heading("Artnet Bridge");
    ui.add_space(3.0);

    if config.bridge.is_empty() {
        ui.label("No artnet data received..");
    }

    egui::ScrollArea::vertical()
        .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible)
        .id_salt("artnet_bridge_output_scroll")
        .show(ui, |ui| {
            ui.with_layout(Layout::top_down_justified(egui::Align::Min), |ui| {
                let mut hovered_output_routing = None;

                for (universe, bridge) in config.bridge.iter_mut() {
                    ui.label(RichText::new(format!("Universe: {universe}")).heading());

                    ui.horizontal(|ui| {
                        ui.label(format!("Updates: {}", bridge.updates));
                        ui.label(
                            bridge
                                .last_data_at
                                .with_timezone(&Local)
                                .format("(Last: %H:%M:%S)")
                                .to_string(),
                        );

                        bridge
                            .output_routing
                            .device
                            .collections_change_button(ui, collections);

                        if let Some(device) = bridge
                            .output_routing
                            .device
                            .and_then(|asset_id| Asset::get(asset_id, collections))
                        {
                            let device_universes = device.data.universes();
                            if !device_universes.is_empty() {
                                ComboBox::new(format!("{universe}_universe"), "")
                                    .selected_text(match bridge.output_routing.universe {
                                        None => WidgetText::from("No universe selected"),
                                        Some(universe) => WidgetText::from(universe.to_string()),
                                    })
                                    .width(150.0)
                                    .show_ui(ui, |ui| {
                                        for device_universe in device_universes {
                                            let res = ui.selectable_value(
                                                &mut bridge.output_routing.universe,
                                                Some(*device_universe),
                                                device_universe.to_string(),
                                            );
                                            if res.hovered() {
                                                hovered_output_routing = Some(OutputRouting {
                                                    device: Some(device.id),
                                                    universe: Some(*device_universe),
                                                });
                                            }
                                        }
                                    });
                            }

                            ComboBox::new(format!("{universe}_merge"), "")
                                .selected_text(match bridge.htp {
                                    true => "HTP",
                                    false => "LTP",
                                })
                                .width(50.0)
                                .show_ui(ui, |ui| {
                                    ui.selectable_value(&mut bridge.htp, true, "HTP");
                                    ui.selectable_value(&mut bridge.htp, false, "LTP");
                                });

                            if ui.add(Button::new(icons::X)).clicked() {
                                bridge.output_routing = OutputRouting::default();
                            }
                        }
                    });
                }

                crate::ui::frame_requests::hover_output_routing(hovered_output_routing);
            });
        });
    if config != status.artnet {
        project.artnet_config = config;
    }
}

fn artnet_trigger_settings(ui: &mut Ui, project: &mut Project, status: &ServerStatus) {
    let mut config = status.artnet.clone();
    ui.heading("Artnet Trigger");
    ui.add_space(3.0);

    ui.separator();
    ui.label("Universe");
    ui.add(Slider::new(&mut config.universe, 0..=MAX_UNIVERSE));
    if config != status.artnet {
        project.artnet_config = config;
    }
}

fn osc_settings(ui: &mut Ui, project: &mut Project) {
    let osc_config = project.osc_config;
    ui.heading("OSC");
    let mut active = osc_config.active;
    if ui.checkbox(&mut active, "Active").changed() {
        UiAction::SetProjectOscActive(active).enqueue();
    }
    ui.horizontal(|ui| {
        ui.label("Port");
        let mut port = osc_config.port;
        let response = ui.add(DragValue::new(&mut port).range(1..=65535));
        if (response.lost_focus() || response.drag_stopped()) && port != osc_config.port {
            UiAction::SetProjectOscPort(port).enqueue();
        }
    });
}