gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The lines under a rule's sentence: what its light, motor fader, meter
//! or pad color shows, each clickable, and adding more.

use egui::{Popup, PopupCloseBehavior, RichText, Ui};
use egui_phosphor_icons::icons;
use gled_model::asset::midi_controller::{MidiColorSource, MidiOutputBindingKind, MidiValueSource};

use super::intent::Intent;
use super::pickers::target_picker;
use super::rule_card::{Cards, Shown};
use super::rules::{
    FeedbackRole, Place, TokenKind, ValueKind, feedback_role, roles_for, source_sentence,
    target_text,
};
use super::token_editors::source_token_editor;
use super::widgets::token::{sentence, token, token_color};

/// A line per light, motor fader, meter or pad color, and "+ feedback".
pub(crate) fn feedback_lines(ui: &mut Ui, cards: &mut Cards, shown: &Shown) {
    let first = shown.first().clone();
    let mapping = cards.mapping;
    for (position, &output) in first.feedback.iter().enumerate() {
        let kind = &mapping.output_bindings[output].kind;
        let role = feedback_role(cards.controls, &first, kind);
        let line = ui.horizontal_wrapped(|ui| {
            ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
            ui.add_space(22.0);
            ui.weak(RichText::new(role.label()).color(ui.visuals().weak_text_color()));
            match kind {
                MidiOutputBindingKind::SceneColorValue(color) => {
                    pad_color_line(ui, cards, shown, position, color);
                }
                kind => {
                    if let Some(source) = kind.source() {
                        source_line(ui, cards, shown, position, source, role);
                    }
                }
            }
            if ui
                .small_button(icons::X)
                .on_hover_text(format!("Remove the {}", role.name().to_lowercase()))
                .clicked()
            {
                cards.intents.push(Intent::RemoveFeedback {
                    members: shown.members.clone(),
                    position,
                });
            }
        });
        if ui.rect_contains_pointer(line.response.rect) {
            cards.previewed.extend(
                shown
                    .members
                    .rules
                    .iter()
                    .filter_map(|rule| rule.feedback.get(position)),
            );
        }
    }
    add_feedback_button(ui, cards, shown);
}

fn source_line(
    ui: &mut Ui,
    cards: &mut Cards,
    shown: &Shown,
    position: usize,
    source: &MidiValueSource,
    role: FeedbackRole,
) {
    let mapping = cards.mapping;
    let place = Place::Feedback(position);
    let numbers = shown.numbers(place);
    let pieces = source_sentence(source, &mapping.modes, numbers.as_ref());
    let prefer = if matches!(role, FeedbackRole::Light | FeedbackRole::PadColor) {
        ValueKind::OnOff
    } else {
        ValueKind::Level
    };
    let numbering = shown.numbering(place);
    for (kind, response) in sentence(ui, &pieces) {
        let edited = source_token_editor(
            &response,
            kind,
            source,
            &numbering,
            &mapping.modes,
            prefer,
            shown.group_first(cards.controls),
        );
        if let Some((source, numbering)) = edited {
            let numbering = if kind == TokenKind::Main {
                shown.numbering(place)
            } else {
                numbering
            };
            cards.intents.push(Intent::SetSource {
                members: shown.members.clone(),
                position,
                source,
                numbering,
            });
        }
    }
}

/// "pad shows the color of grid scene row 1, column n using Colors".
fn pad_color_line(
    ui: &mut Ui,
    cards: &mut Cards,
    shown: &Shown,
    position: usize,
    color: &gled_model::asset::midi_controller::MidiSceneColorValueOutput,
) {
    let mapping = cards.mapping;
    let MidiColorSource::SceneColor { target } = &color.source;
    let numbers = shown.numbers(Place::Feedback(position));
    let text = target_text(target, numbers.as_ref());
    let response = token(ui, &text, token_color(TokenKind::Target));
    let numbering = shown.numbering(Place::Feedback(position));
    let group_first = shown.group_first(cards.controls);
    Popup::menu(&response)
        .close_behavior(PopupCloseBehavior::CloseOnClickOutside)
        .show(|ui| {
            let mut edited = target.clone();
            let mut numbering = numbering.clone();
            if target_picker(ui, &mut edited, &mut numbering, group_first) {
                // A pad color's scene goes with the action's in a group;
                // single pads take it directly.
                for rule in &shown.members.rules {
                    if let Some(&output) = rule.feedback.get(position)
                        && let MidiOutputBindingKind::SceneColorValue(mut color) =
                            mapping.output_bindings[output].kind.clone()
                    {
                        color.source = MidiColorSource::SceneColor {
                            target: edited.clone(),
                        };
                        cards.intents.push(Intent::SetFeedbackKind {
                            output,
                            kind: MidiOutputBindingKind::SceneColorValue(color),
                        });
                    }
                }
            }
        });
    ui.label("using");
    let name = if color.mapping_name.is_empty() {
        "no color mapping"
    } else {
        &color.mapping_name
    };
    let response = token(ui, name, token_color(TokenKind::Parameter));
    Popup::menu(&response)
        .close_behavior(PopupCloseBehavior::CloseOnClickOutside)
        .show(|ui| {
            if mapping.color_mappings.is_empty() {
                ui.weak("No color mappings yet — add one in the Colors tab");
            }
            for named in &mapping.color_mappings {
                if ui
                    .selectable_label(named.name == color.mapping_name, &named.name)
                    .clicked()
                {
                    for rule in &shown.members.rules {
                        if let Some(&output) = rule.feedback.get(position)
                            && let MidiOutputBindingKind::SceneColorValue(mut color) =
                                mapping.output_bindings[output].kind.clone()
                        {
                            color.mapping_name = named.name.clone();
                            cards.intents.push(Intent::SetFeedbackKind {
                                output,
                                kind: MidiOutputBindingKind::SceneColorValue(color),
                            });
                        }
                    }
                    ui.close();
                }
            }
        });
}

/// "+ light for Scene launch 1" and the other kinds the control has.
fn add_feedback_button(ui: &mut Ui, cards: &mut Cards, shown: &Shown) {
    let first = shown.first();
    let control = &cards.controls[first.control];
    let present: Vec<FeedbackRole> = first
        .feedback
        .iter()
        .map(|&output| {
            feedback_role(
                cards.controls,
                first,
                &cards.mapping.output_bindings[output].kind,
            )
        })
        .collect();
    let roles: Vec<FeedbackRole> = roles_for(control.kind)
        .iter()
        .copied()
        .filter(|role| !present.contains(role) || *role == FeedbackRole::Value)
        .collect();
    ui.horizontal(|ui| {
        ui.add_space(22.0);
        let suggested = roles
            .first()
            .copied()
            .filter(|role| *role != FeedbackRole::Value);
        if let Some(role) = suggested
            && ui
                .small_button(format!(
                    "+ {} for {}",
                    role.name().to_lowercase(),
                    control.name
                ))
                .on_hover_text(role.description())
                .clicked()
        {
            cards.intents.push(Intent::AddFeedback {
                members: shown.members.clone(),
                role,
            });
        }
        let more = ui.small_button("+ other feedback");
        Popup::menu(&more).show(|ui| {
            for role in roles_for_any() {
                if ui
                    .button(role.name())
                    .on_hover_text(role.description())
                    .clicked()
                {
                    cards.intents.push(Intent::AddFeedback {
                        members: shown.members.clone(),
                        role,
                    });
                }
            }
        });
    });
}

fn roles_for_any() -> [FeedbackRole; 6] {
    [
        FeedbackRole::Light,
        FeedbackRole::PadColor,
        FeedbackRole::MotorFader,
        FeedbackRole::Meter,
        FeedbackRole::Ring,
        FeedbackRole::Value,
    ]
}