gled-ui 2.34.0

gled's egui interface, shared by the desktop app and the browser
Documentation
//! Copying a rule to other controls of its kind: which ones, and whether
//! each scene, animation or setting number counts up with them. Controls
//! not learned yet are reached through the messages after the rule's own.

use egui::{DragValue, Frame, Ui};
use gled_model::asset::midi_controller::MidiTrigger;

use super::intent::{Intent, SlotNumbering};
use super::rule_card::Cards;
use super::rules::{
    Control, Numbering, Numbers, Rule, Slot, action_numbers_of, control_index, control_number,
    message_name,
};

pub(crate) fn copy_dialog(ui: &mut Ui, cards: &mut Cards, rule: &Rule) {
    let source = &cards.controls[rule.control];
    let n = control_number(source).unwrap_or(1);
    let candidates: Vec<usize> = (0..cards.controls.len())
        .filter(|&index| index != rule.control && same_kind(&cards.controls[index], source))
        .collect();
    let numbers = action_numbers_of(cards.mapping, rule);
    // Controls named like this one, after it: Select 6–8 for Select 5.
    let prefix = name_text(&source.name);
    let siblings: Vec<usize> = candidates
        .iter()
        .copied()
        .filter(|&index| {
            let control = &cards.controls[index];
            name_text(&control.name) == prefix
                && control_number(control).is_some_and(|number| number > n)
        })
        .collect();
    let copy = &mut cards.state.copy;
    if !copy.prepared {
        copy.chosen = siblings.clone();
        copy.numbering = SlotNumbering(
            numbers
                .iter()
                .map(|(slot, value)| {
                    // A grid position counts along its row: the column.
                    let grid = numbers.iter().any(|(other, _)| *other == Slot::Column);
                    if *slot == Slot::Row && grid {
                        return (*slot, Numbering::Same);
                    }
                    let offset = i32::from(*value) + 1 - n;
                    (*slot, Numbering::Counting { offset })
                })
                .collect(),
        );
        copy.prepared = true;
    }

    let mut create = None;
    let mut close = false;
    Frame::NONE
        .fill(ui.visuals().extreme_bg_color)
        .inner_margin(6.0)
        .show(ui, |ui| {
            ui.horizontal(|ui| {
                ui.label(format!("Copy what {} does to", source.name));
                if let Some(label) = range_label(cards.controls, prefix, &siblings)
                    && ui.small_button(label).clicked()
                {
                    copy.chosen = siblings.clone();
                }
                if !candidates.is_empty() && ui.small_button("all").clicked() {
                    copy.chosen = candidates.clone();
                }
                if ui.small_button("none").clicked() {
                    copy.chosen.clear();
                    copy.next = 0;
                }
            });
            if !candidates.is_empty() {
                ui.horizontal_wrapped(|ui| {
                    for &candidate in &candidates {
                        let mut chosen = copy.chosen.contains(&candidate);
                        if ui
                            .checkbox(&mut chosen, &cards.controls[candidate].name)
                            .changed()
                        {
                            if chosen {
                                copy.chosen.push(candidate);
                            } else {
                                copy.chosen.retain(|other| *other != candidate);
                            }
                        }
                    }
                });
            }
            let series = next_messages(source, copy.next);
            ui.horizontal(|ui| {
                ui.label("and the controls sending the next");
                ui.add(DragValue::new(&mut copy.next).range(0..=63));
                ui.label("messages");
                if let (Some(first), Some(last)) = (series.first(), series.last()) {
                    ui.weak(format!(
                        "({} – {})",
                        message_name(first),
                        message_name(last)
                    ));
                }
            })
            .response
            .on_hover_text(
                "For controls not learned yet: pads or buttons sending the following notes, \
                 faders on the following channels",
            );

            let mut targets: Vec<(usize, i32)> = copy
                .chosen
                .iter()
                .enumerate()
                .map(|(position, &target)| {
                    let number = control_number(&cards.controls[target])
                        .unwrap_or(n + i32::try_from(position).unwrap_or(0) + 1);
                    (target, number)
                })
                .collect();
            // The following messages count on from the rule's own.
            let mut new_controls = Vec::new();
            for (step, trigger) in series.iter().enumerate() {
                let number = n + i32::try_from(step).unwrap_or(0) + 1;
                match control_index(cards.controls, trigger) {
                    Some(known) if known == rule.control => {}
                    Some(known) => {
                        if !targets.iter().any(|(target, _)| *target == known) {
                            targets.push((known, number));
                        }
                    }
                    None => new_controls.push((trigger.clone(), number)),
                }
            }

            let first_target = targets
                .first()
                .map(|(target, number)| (cards.controls[*target].name.clone(), *number))
                .or_else(|| {
                    new_controls
                        .first()
                        .map(|(trigger, number)| (message_name(trigger), *number))
                });
            for (slot, value) in &numbers {
                let mut counting =
                    matches!(copy.numbering.numbering(*slot), Numbering::Counting { .. });
                let example = first_target
                    .as_ref()
                    .map_or_else(String::new, |(name, number)| {
                        let gets = i64::from(*value) + 1 + i64::from(number - n);
                        format!(" — {name} gets {gets}")
                    });
                if ui
                    .checkbox(
                        &mut counting,
                        format!("{} counts up{example}", slot_name(*slot)),
                    )
                    .on_hover_text(format!(
                        "Off: every copy gets {} {}",
                        slot_name(*slot).to_lowercase(),
                        u16::from(*value) + 1
                    ))
                    .changed()
                {
                    copy.numbering.0.retain(|(numbered, _)| numbered != slot);
                    let numbering = if counting {
                        Numbering::Counting {
                            offset: i32::from(*value) + 1 - n,
                        }
                    } else {
                        Numbering::Same
                    };
                    copy.numbering.0.push((*slot, numbering));
                }
            }

            ui.horizontal(|ui| {
                let count = targets.len() + new_controls.len();
                if ui
                    .add_enabled(
                        count > 0,
                        egui::Button::new(format!("Create {count} rules")),
                    )
                    .on_hover_text("Replaces what those controls do in this mode")
                    .clicked()
                {
                    create = Some(Intent::Copy {
                        rule: rule.clone(),
                        n,
                        targets,
                        new_controls,
                        numbering: copy.numbering.clone(),
                    });
                    close = true;
                }
                if ui.button("Cancel").clicked() {
                    close = true;
                }
            });
        });
    cards.intents.extend(create);
    if close {
        cards.state.copying = None;
        cards.state.copy = Default::default();
    }
}

/// "Select 6–8" for the controls after "Select 5".
fn range_label(controls: &[Control], prefix: &str, siblings: &[usize]) -> Option<String> {
    let first = *siblings.first()?;
    let last = *siblings.last()?;
    if first == last {
        return Some(controls[first].name.clone());
    }
    Some(format!(
        "{prefix} {}–{}",
        control_number(&controls[first]).unwrap_or_default(),
        control_number(&controls[last]).unwrap_or_default()
    ))
}

/// The `count` messages after the one `control` sends: the next notes or
/// controllers, or for pitch bend the next channels.
fn next_messages(control: &Control, count: u8) -> Vec<MidiTrigger> {
    let trigger = &control.trigger;
    (1..=u16::from(count))
        .filter_map(|step| {
            if trigger.status & 0xF0 == 0xE0 {
                let channel = u16::from(trigger.status & 0x0F) + step;
                let channel = u8::try_from(channel).ok().filter(|channel| *channel < 16)?;
                Some(MidiTrigger {
                    status: 0xE0 | channel,
                    ..trigger.clone()
                })
            } else {
                let data1 = u16::from(trigger.data1) + step;
                let data1 = u8::try_from(data1).ok().filter(|data1| *data1 < 128)?;
                Some(MidiTrigger {
                    data1,
                    ..trigger.clone()
                })
            }
        })
        .collect()
}

/// A control's name without its number: "Select 5" → "Select".
fn name_text(name: &str) -> &str {
    name.trim_end_matches(|ch: char| ch.is_ascii_digit())
        .trim_end()
}

/// Controls a rule can be copied to: the same kind and message type.
fn same_kind(a: &Control, b: &Control) -> bool {
    a.kind == b.kind && a.trigger.status & 0xF0 == b.trigger.status & 0xF0
}

fn slot_name(slot: Slot) -> &'static str {
    match slot {
        Slot::Row => "Row",
        Slot::Column => "Column",
        Slot::Quick => "Quick scene",
        Slot::Effect | Slot::Animation => "Animation",
        Slot::Setting => "Setting",
    }
}