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);
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)| {
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();
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();
}
}
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()
))
}
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()
}
fn name_text(name: &str) -> &str {
name.trim_end_matches(|ch: char| ch.is_ascii_digit())
.trim_end()
}
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",
}
}