gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! The small editors that open from a rule's words: which scene, which
//! animation or setting, a mode, an encoder's clicks, a beat window. In a
//! group a number is the same for every control or counts up with them.

use egui::{DragValue, Ui};
use gled_model::asset::midi_controller::{
    MidiEffectParameter, MidiEffectTarget, MidiEncoder, MidiMode, MidiRelativeEncoding,
    MidiSceneTarget,
};
use strum::IntoEnumIterator;

use super::intent::SlotNumbering;
use super::rules::{Numbering, Numbers, Slot};

/// The first member of a group being edited: its `n` and name, to say
/// what counting means ("Fader 5 gets 1, Fader 6 gets 2 …").
#[derive(Debug, Clone, Copy)]
pub(crate) struct GroupFirst<'a> {
    pub(crate) n: i32,
    pub(crate) name: &'a str,
}

/// A number shown from 1: the same for all members, or counting with them.
fn counted_number(
    ui: &mut Ui,
    label: &str,
    value: &mut u8,
    slot: Slot,
    numbering: &mut SlotNumbering,
    group: Option<GroupFirst>,
    max: u8,
) -> bool {
    let mut changed = false;
    let mut current = numbering.numbering(slot);
    ui.horizontal(|ui| {
        ui.label(label);
        match (group, current) {
            (Some(first), Numbering::Counting { offset }) => {
                let mut start = first.n + offset;
                ui.label(format!("counts up: {} gets", first.name));
                if ui
                    .add(DragValue::new(&mut start).range(1..=i32::from(max) + 1))
                    .changed()
                {
                    current = Numbering::Counting {
                        offset: start - first.n,
                    };
                    changed = true;
                }
            }
            _ => {
                let mut shown = u16::from(*value) + 1;
                if ui
                    .add(DragValue::new(&mut shown).range(1..=u16::from(max) + 1))
                    .changed()
                {
                    *value = u8::try_from(shown - 1).unwrap_or(max);
                    changed = true;
                }
            }
        }
        if let Some(first) = group {
            let mut counting = matches!(current, Numbering::Counting { .. });
            if ui
                .checkbox(&mut counting, "counts up")
                .on_hover_text(format!(
                    "Each control gets its own number: {} gets the number shown, the next one more",
                    first.name
                ))
                .changed()
            {
                current = if counting {
                    Numbering::Counting {
                        offset: i32::from(*value) + 1 - first.n,
                    }
                } else {
                    Numbering::Same
                };
                changed = true;
            }
        }
    });
    if changed {
        numbering.0.retain(|(numbered, _)| *numbered != slot);
        numbering.0.push((slot, current));
    }
    changed
}

/// Which scene: the selected one, one in the selected row or column, a
/// grid position or a quick scene.
pub(crate) fn target_picker(
    ui: &mut Ui,
    target: &mut MidiSceneTarget,
    numbering: &mut SlotNumbering,
    group: Option<GroupFirst>,
) -> bool {
    let mut changed = false;
    ui.horizontal_wrapped(|ui| {
        let options = [
            ("The selected scene", MidiSceneTarget::Selected),
            ("In the selected row", MidiSceneTarget::Row { col: 0 }),
            ("In the selected column", MidiSceneTarget::Column { row: 0 }),
            ("Grid position", MidiSceneTarget::Grid { row: 0, col: 0 }),
            ("Quick scene", MidiSceneTarget::Quick { index: 0 }),
        ];
        for (label, option) in options {
            let same_kind = std::mem::discriminant(target) == std::mem::discriminant(&option);
            if ui.selectable_label(same_kind, label).clicked() && !same_kind {
                *target = keep_numbers(target, option);
                changed = true;
            }
        }
    });
    match target {
        MidiSceneTarget::Selected => {
            ui.weak("Follows the scene selected in gled");
        }
        MidiSceneTarget::Row { col } => {
            changed |= counted_number(ui, "Column", col, Slot::Column, numbering, group, 63);
        }
        MidiSceneTarget::Column { row } => {
            changed |= counted_number(ui, "Row", row, Slot::Row, numbering, group, 63);
        }
        MidiSceneTarget::Grid { row, col } => {
            changed |= counted_number(ui, "Row", row, Slot::Row, numbering, group, 63);
            changed |= counted_number(ui, "Column", col, Slot::Column, numbering, group, 63);
        }
        MidiSceneTarget::Quick { index } => {
            changed |= counted_number(ui, "Quick scene", index, Slot::Quick, numbering, group, 63);
        }
    }
    changed
}

/// `option` with the row and column of `current` where both have them.
fn keep_numbers(current: &MidiSceneTarget, option: MidiSceneTarget) -> MidiSceneTarget {
    let (row, col) = match current {
        MidiSceneTarget::Grid { row, col } => (*row, *col),
        MidiSceneTarget::Row { col } => (0, *col),
        MidiSceneTarget::Column { row } => (*row, 0),
        MidiSceneTarget::Quick { index } => (0, *index),
        MidiSceneTarget::Selected => (0, 0),
    };
    match option {
        MidiSceneTarget::Row { .. } => MidiSceneTarget::Row { col },
        MidiSceneTarget::Column { .. } => MidiSceneTarget::Column { row },
        MidiSceneTarget::Grid { .. } => MidiSceneTarget::Grid { row, col },
        MidiSceneTarget::Quick { .. } => MidiSceneTarget::Quick { index: col },
        MidiSceneTarget::Selected => MidiSceneTarget::Selected,
    }
}

/// Which animation of the scene: the selected one or a position.
pub(crate) fn effect_picker(
    ui: &mut Ui,
    effect: &mut MidiEffectTarget,
    numbering: &mut SlotNumbering,
    group: Option<GroupFirst>,
) -> bool {
    let mut changed = false;
    ui.horizontal(|ui| {
        let selected = *effect == MidiEffectTarget::Selected;
        if ui
            .selectable_label(selected, "The selected animation")
            .clicked()
            && !selected
        {
            *effect = MidiEffectTarget::Selected;
            changed = true;
        }
        if ui.selectable_label(!selected, "Animation number").clicked() && selected {
            *effect = MidiEffectTarget::Index { index: 0 };
            changed = true;
        }
    });
    if let MidiEffectTarget::Index { index } = effect {
        changed |= counted_number(ui, "Animation", index, Slot::Effect, numbering, group, 31);
    }
    changed
}

/// An animation setting, 1–6.
pub(crate) fn setting_picker(
    ui: &mut Ui,
    setting: &mut u8,
    numbering: &mut SlotNumbering,
    group: Option<GroupFirst>,
) -> bool {
    let changed = counted_number(ui, "Setting", setting, Slot::Setting, numbering, group, 5);
    ui.weak("The animation's own sliders, in the order its editor shows them");
    changed
}

/// An animation picked by a button.
pub(crate) fn animation_picker(
    ui: &mut Ui,
    index: &mut u8,
    numbering: &mut SlotNumbering,
    group: Option<GroupFirst>,
) -> bool {
    counted_number(
        ui,
        "Animation",
        index,
        Slot::Animation,
        numbering,
        group,
        31,
    )
}

/// One of the mapping's modes.
pub(crate) fn mode_picker(ui: &mut Ui, mode: &mut u8, modes: &[MidiMode]) -> bool {
    let mut changed = false;
    if modes.is_empty() {
        ui.weak("No modes yet — add one in the Modes tab");
    }
    for (index, entry) in modes.iter().enumerate() {
        let index = u8::try_from(index).unwrap_or(u8::MAX);
        if ui.selectable_label(*mode == index, &entry.name).clicked() && *mode != index {
            *mode = index;
            changed = true;
        }
    }
    changed
}

/// How fast an endless encoder turns through what it changes.
pub(crate) fn encoder_picker(ui: &mut Ui, encoder: &mut MidiEncoder) -> bool {
    let mut changed = false;
    ui.horizontal(|ui| {
        ui.label("Clicks per step");
        changed |= ui
            .add(DragValue::new(&mut encoder.steps_per_unit).range(1..=64))
            .changed();
        ui.weak("higher = slower");
    });
    ui.horizontal(|ui| {
        ui.label("The encoder sends");
        for encoding in MidiRelativeEncoding::iter() {
            let label = match encoding {
                MidiRelativeEncoding::TwosComplement => "127 for one click left",
                MidiRelativeEncoding::SignMagnitude => "65 for one click left",
            };
            if ui
                .selectable_label(encoder.encoding == encoding, label)
                .on_hover_text(encoding.to_string())
                .clicked()
                && encoder.encoding != encoding
            {
                encoder.encoding = encoding;
                changed = true;
            }
        }
    });
    changed
}

/// A beat light's window, with presets.
pub(crate) fn beat_picker(
    ui: &mut Ui,
    start: &mut f32,
    end: &mut f32,
    every_beat: &mut bool,
    blackout_blink: &mut Option<u8>,
) -> bool {
    let mut changed = false;
    ui.horizontal_wrapped(|ui| {
        for (label, preset) in [
            ("Every beat, first half", (0.0, 0.5, true)),
            ("Once per bar: beat 1", (0.0, 1.0, false)),
            ("First half of the bar", (0.0, 2.0, false)),
        ] {
            let current = (*start, *end, *every_beat) == preset;
            if ui.selectable_label(current, label).clicked() && !current {
                (*start, *end, *every_beat) = preset;
                changed = true;
            }
        }
    });
    ui.horizontal(|ui| {
        changed |= ui.radio_value(every_beat, true, "every beat").changed();
        changed |= ui
            .radio_value(every_beat, false, "once per bar (4 beats)")
            .changed();
    });
    let period = if *every_beat { 1.0 } else { 4.0 };
    ui.horizontal(|ui| {
        ui.label("Lit from");
        changed |= ui
            .add(DragValue::new(start).range(0.0..=period).speed(0.05))
            .changed();
        ui.label("to");
        changed |= ui
            .add(DragValue::new(end).range(0.0..=period).speed(0.05))
            .changed();
        ui.weak(if *every_beat {
            "of each beat"
        } else {
            "beats into the bar"
        });
    });
    let mut blink = blackout_blink.is_some();
    if ui.checkbox(&mut blink, "Blink during blackout").changed() {
        *blackout_blink = blink.then_some(127);
        changed = true;
    }
    changed
}

/// A blackout light: lit during blackout or outside it, steady or blinking.
pub(crate) fn blackout_picker(ui: &mut Ui, inverted: &mut bool, blink: &mut bool) -> bool {
    let mut changed = false;
    changed |= ui
        .radio_value(inverted, false, "Lit during blackout")
        .changed();
    changed |= ui
        .radio_value(inverted, true, "Lit when not in blackout")
        .changed();
    changed |= ui.checkbox(blink, "Blinking").changed();
    changed
}

/// Which live value of an animation a meter shows.
pub(crate) fn parameter_picker(ui: &mut Ui, parameter: &mut MidiEffectParameter) -> bool {
    let mut changed = false;
    for option in MidiEffectParameter::iter() {
        if ui
            .selectable_label(*parameter == option, option.to_string().to_lowercase())
            .clicked()
            && *parameter != option
        {
            *parameter = option;
            changed = true;
        }
    }
    changed
}