gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! What opens when a word of a rule is clicked: the action or feedback
//! palette for the main word, a picker for the others. They edit a copy
//! and hand back the changed action or source.

use egui::{Id, Popup, PopupCloseBehavior, Response, Ui};
use gled_model::asset::midi_controller::{
    MidiInputAction, MidiInputActionKind, MidiMode, MidiValueSource, MidiValueSourceKind,
};

use super::action_converters::input_action_from_kind;
use super::intent::SlotNumbering;
use super::pickers::{
    GroupFirst, animation_picker, beat_picker, blackout_picker, effect_picker, encoder_picker,
    mode_picker, parameter_picker, setting_picker, target_picker,
};
use super::rules::{ControlKind, TokenKind, ValueKind, actions, sources};
use super::value_source_converters::value_source_from_kind;
use super::widgets::palette::{PaletteEntry, palette};

/// What the action palette or a picker asks for.
pub(crate) enum ActionChoice {
    Set(MidiInputAction, SlotNumbering),
    RemoveAction,
    DeleteRule,
}

/// The palette entry beyond the actions.
#[derive(Clone)]
enum Pick<T> {
    Kind(T),
    RemoveAction,
    DeleteRule,
}

/// Shows the editor for the word `kind` of `action` while it is open;
/// `open` opens it now (after learning a control).
#[allow(clippy::too_many_arguments)]
pub(crate) fn action_token_editor(
    ui: &mut Ui,
    response: &Response,
    kind: TokenKind,
    action: Option<&MidiInputAction>,
    numbering: &SlotNumbering,
    modes: &[MidiMode],
    control: ControlKind,
    group: Option<GroupFirst>,
    open: bool,
) -> Option<ActionChoice> {
    let popup_id = Popup::default_response_id(response);
    if open {
        Popup::open_id(ui.ctx(), popup_id);
    }
    Popup::menu(response)
        .close_behavior(PopupCloseBehavior::CloseOnClickOutside)
        .show(|ui| {
            if kind == TokenKind::Main {
                return action_palette(ui, popup_id, action, control);
            }
            let mut edited = action?.clone();
            let mut numbering = numbering.clone();
            let changed = edit_action_part(ui, kind, &mut edited, &mut numbering, modes, group);
            changed.then_some(ActionChoice::Set(edited, numbering))
        })
        .and_then(|inner| inner.inner)
}

fn action_palette(
    ui: &mut Ui,
    id: Id,
    action: Option<&MidiInputAction>,
    control: ControlKind,
) -> Option<ActionChoice> {
    let current = action.map(MidiInputActionKind::from);
    let mut entries: Vec<PaletteEntry<Pick<MidiInputActionKind>>> = actions()
        .map(|info| PaletteEntry {
            group: info.group.label(),
            label: info.label.to_owned(),
            description: info.description,
            suggested: info.suits(control),
            current: current == Some(info.kind),
            found: Box::new(move |query| info.matches(query)),
            value: Pick::Kind(info.kind),
        })
        .collect();
    if action.is_some() {
        entries.push(PaletteEntry {
            group: "Remove",
            label: "Does nothing — remove the action".to_owned(),
            description: "Keeps the rule's lights",
            suggested: false,
            current: false,
            found: Box::new(|query| "does nothing remove none clear".contains(query)),
            value: Pick::RemoveAction,
        });
    }
    entries.push(PaletteEntry {
        group: "Remove",
        label: "Delete this rule".to_owned(),
        description: "Removes the action and the lights",
        suggested: false,
        current: false,
        found: Box::new(|query| "delete remove rule".contains(query)),
        value: Pick::DeleteRule,
    });
    match palette(ui, id, &entries)? {
        Pick::Kind(kind) if Some(kind) == current => None,
        Pick::Kind(kind) => {
            let base = action.cloned().unwrap_or(MidiInputAction::Tap);
            Some(ActionChoice::Set(
                input_action_from_kind(kind, base),
                SlotNumbering::default(),
            ))
        }
        Pick::RemoveAction => Some(ActionChoice::RemoveAction),
        Pick::DeleteRule => Some(ActionChoice::DeleteRule),
    }
}

fn edit_action_part(
    ui: &mut Ui,
    kind: TokenKind,
    action: &mut MidiInputAction,
    numbering: &mut SlotNumbering,
    modes: &[MidiMode],
    group: Option<GroupFirst>,
) -> bool {
    use MidiInputAction as A;
    match (kind, action) {
        (TokenKind::Target, action) => {
            target_of(action).is_some_and(|target| target_picker(ui, target, numbering, group))
        }
        (TokenKind::Effect, action) => {
            effect_of(action).is_some_and(|effect| effect_picker(ui, effect, numbering, group))
        }
        (TokenKind::Setting, A::SetSceneEffectSettingF32 { setting_index, .. }) => {
            setting_picker(ui, setting_index, numbering, group)
        }
        (TokenKind::Animation, A::SelectSceneEffect { index }) => {
            animation_picker(ui, index, numbering, group)
        }
        (TokenKind::Mode, A::SelectMode { mode }) => mode_picker(ui, mode, modes),
        (
            TokenKind::Encoder,
            A::SetMainDimmerRelative { encoder }
            | A::SetSpeedRelative { encoder }
            | A::SelectSceneRelative { encoder }
            | A::SelectSceneRow { encoder }
            | A::SelectSceneColumn { encoder }
            | A::SelectSceneEffectRelative { encoder },
        ) => encoder_picker(ui, encoder),
        _ => false,
    }
}

fn target_of(
    action: &mut MidiInputAction,
) -> Option<&mut gled_model::asset::midi_controller::MidiSceneTarget> {
    use MidiInputAction as A;
    match action {
        A::SelectScene { target }
        | A::ToggleSceneActive { target }
        | A::SetSceneActive { target }
        | A::FlashScene { target }
        | A::SetSceneOpacity { target }
        | A::SetSceneInputDimmer { target }
        | A::SetSceneBeatOffset { target }
        | A::SetSceneIgnoreMainDimmer { target }
        | A::SetSceneSetOffsetOnFlash { target }
        | A::SetSceneEffectSettingF32 { target, .. }
        | A::SetSceneEffectOpacity { target, .. }
        | A::SetSceneEffectColorShift { target, .. }
        | A::SetSceneEffectBeatProgression { target, .. }
        | A::SetSceneEffectBeatOffset { target, .. }
        | A::SetSceneEffectSpeedExponent { target, .. }
        | A::SetSceneEffectLayout { target, .. } => Some(target),
        _ => None,
    }
}

fn effect_of(
    action: &mut MidiInputAction,
) -> Option<&mut gled_model::asset::midi_controller::MidiEffectTarget> {
    use MidiInputAction as A;
    match action {
        A::SetSceneEffectSettingF32 { effect, .. }
        | A::SetSceneEffectOpacity { effect, .. }
        | A::SetSceneEffectColorShift { effect, .. }
        | A::SetSceneEffectBeatProgression { effect, .. }
        | A::SetSceneEffectBeatOffset { effect, .. }
        | A::SetSceneEffectSpeedExponent { effect, .. }
        | A::SetSceneEffectLayout { effect, .. } => Some(effect),
        _ => None,
    }
}

/// Shows the editor for the word `kind` of a feedback's `source`.
pub(crate) fn source_token_editor(
    response: &Response,
    kind: TokenKind,
    source: &MidiValueSource,
    numbering: &SlotNumbering,
    modes: &[MidiMode],
    prefer: ValueKind,
    group: Option<GroupFirst>,
) -> Option<(MidiValueSource, SlotNumbering)> {
    let popup_id = Popup::default_response_id(response);
    Popup::menu(response)
        .close_behavior(PopupCloseBehavior::CloseOnClickOutside)
        .show(|ui| {
            if kind == TokenKind::Main {
                let current = MidiValueSourceKind::from(source);
                let entries: Vec<PaletteEntry<MidiValueSourceKind>> = sources()
                    .map(|info| PaletteEntry {
                        group: info.group,
                        label: info.label.to_owned(),
                        description: "",
                        suggested: info.value == prefer,
                        current: current == info.kind,
                        found: Box::new(move |query| {
                            query.split_whitespace().all(|word| {
                                info.label.to_lowercase().contains(word)
                                    || info.group.to_lowercase().contains(word)
                            })
                        }),
                        value: info.kind,
                    })
                    .collect();
                let picked = palette(ui, popup_id, &entries)?;
                return (picked != current).then(|| {
                    (
                        value_source_from_kind(picked, source.clone()),
                        SlotNumbering::default(),
                    )
                });
            }
            let mut edited = source.clone();
            let mut numbering = numbering.clone();
            let changed = edit_source_part(ui, kind, &mut edited, &mut numbering, modes, group);
            changed.then_some((edited, numbering))
        })
        .and_then(|inner| inner.inner)
}

fn edit_source_part(
    ui: &mut Ui,
    kind: TokenKind,
    source: &mut MidiValueSource,
    numbering: &mut SlotNumbering,
    modes: &[MidiMode],
    group: Option<GroupFirst>,
) -> bool {
    use MidiValueSource as S;
    match (kind, source) {
        (
            TokenKind::Target,
            S::SceneOpacity { target }
            | S::SceneOpacityLive { target }
            | S::SceneInputDimmer { target }
            | S::SceneBeatOffset { target }
            | S::SceneIgnoreMainDimmer { target }
            | S::SceneSetOffsetOnFlash { target }
            | S::SceneActive { target }
            | S::SceneFlashed { target }
            | S::SceneSelected { target }
            | S::SceneEffectSettingF32 { target, .. }
            | S::SceneEffectOpacity { target, .. }
            | S::SceneEffectColorShift { target, .. }
            | S::SceneEffectBeatProgression { target, .. }
            | S::SceneEffectBeatOffset { target, .. }
            | S::SceneEffectSpeedExponent { target, .. }
            | S::SceneEffectLayout { target, .. },
        ) => target_picker(ui, target, numbering, group),
        (
            TokenKind::Effect,
            S::SelectedSceneEffectLive { effect, .. }
            | S::SceneEffectSettingF32 { effect, .. }
            | S::SceneEffectOpacity { effect, .. }
            | S::SceneEffectColorShift { effect, .. }
            | S::SceneEffectBeatProgression { effect, .. }
            | S::SceneEffectBeatOffset { effect, .. }
            | S::SceneEffectSpeedExponent { effect, .. }
            | S::SceneEffectLayout { effect, .. },
        ) => effect_picker(ui, effect, numbering, group),
        (TokenKind::Setting, S::SceneEffectSettingF32 { setting_index, .. }) => {
            setting_picker(ui, setting_index, numbering, group)
        }
        (TokenKind::Animation, S::EffectSelected { index }) => {
            animation_picker(ui, index, numbering, group)
        }
        (TokenKind::Parameter, S::SelectedSceneEffectLive { parameter, .. }) => {
            parameter_picker(ui, parameter)
        }
        (TokenKind::Mode, S::ModeActive { mode }) => mode_picker(ui, mode, modes),
        (
            TokenKind::Beat,
            S::BeatFlankPulse {
                start_beat,
                end_beat,
                blackout_blink_value,
                every_beat,
            },
        ) => beat_picker(ui, start_beat, end_beat, every_beat, blackout_blink_value),
        (TokenKind::Blackout, S::Blackout { inverted, blink }) => {
            blackout_picker(ui, inverted, blink)
        }
        _ => false,
    }
}