Skip to main content

midilab_gui/
akai_mpd226_editor.rs

1use std::time::Instant;
2
3use eframe::egui;
4use eframe::egui::CentralPanel;
5use eframe::egui::Color32;
6use eframe::egui::ComboBox;
7use eframe::egui::Context;
8use eframe::egui::DragValue;
9use eframe::egui::Grid;
10use eframe::egui::TextEdit;
11use eframe::egui::Ui;
12use eframe::egui::Vec2;
13use eframe::egui::vec2;
14use midilab::IntoEnumIterator;
15use midilab::manufacturer::akai::mpd226::ColorPattern;
16use midilab::manufacturer::akai::mpd226::ColorSequence;
17use midilab::manufacturer::akai::mpd226::NotePattern;
18use midilab::manufacturer::akai::mpd226::Preset;
19use midilab::manufacturer::akai::mpd226::control::Dial;
20use midilab::manufacturer::akai::mpd226::control::Fader;
21use midilab::manufacturer::akai::mpd226::control::Pad;
22use midilab::manufacturer::akai::mpd226::control::Switch;
23use midilab::manufacturer::akai::mpd226::control::value_kind::ActiveState;
24use midilab::manufacturer::akai::mpd226::control::value_kind::AfterTouchKind;
25use midilab::manufacturer::akai::mpd226::control::value_kind::DialKind;
26use midilab::manufacturer::akai::mpd226::control::value_kind::FaderKind;
27use midilab::manufacturer::akai::mpd226::control::value_kind::GateValue;
28use midilab::manufacturer::akai::mpd226::control::value_kind::MidiChannel;
29use midilab::manufacturer::akai::mpd226::control::value_kind::PadColor;
30use midilab::manufacturer::akai::mpd226::control::value_kind::PadKind;
31use midilab::manufacturer::akai::mpd226::control::value_kind::PresetName;
32use midilab::manufacturer::akai::mpd226::control::value_kind::PresetSlot;
33use midilab::manufacturer::akai::mpd226::control::value_kind::SwingKind;
34use midilab::manufacturer::akai::mpd226::control::value_kind::SwitchKind;
35use midilab::manufacturer::akai::mpd226::control::value_kind::Tempo;
36use midilab::manufacturer::akai::mpd226::control::value_kind::TimeDivision;
37use midilab::manufacturer::akai::mpd226::control::value_kind::TransportKind;
38use midilab::manufacturer::akai::mpd226::control::value_kind::TriggerKind;
39use midilab::manufacturer::akai::mpd226::repository::DialRepository;
40use midilab::manufacturer::akai::mpd226::repository::FaderRepository;
41use midilab::manufacturer::akai::mpd226::repository::PadRepository;
42use midilab::manufacturer::akai::mpd226::repository::SwitchRepository;
43use midilab::message::AppMsg;
44use midilab::message::UiEffect;
45use midilab::message::UiMsg;
46use midilab::message::UserMsg;
47use midilab::midi::Note;
48use midilab::scale::Octave;
49use midilab::scale::PitchClass;
50use midilab::scale::ScaleKind;
51use midilab::scale::ScaleSequence;
52use midilab::scale::SequenceDirection;
53use tokio::sync::mpsc::UnboundedReceiver;
54use tokio::sync::mpsc::UnboundedSender;
55
56#[allow(unused)]
57pub struct AkaiMpd226Editor {
58    ui_state: UiState,
59    outbox: Vec<AppMsg>,
60    app_tx: UnboundedSender<AppMsg>,
61    ui_rx: UnboundedReceiver<UiMsg>,
62}
63
64impl AkaiMpd226Editor {
65    pub fn new(app_tx: UnboundedSender<AppMsg>, ui_rx: UnboundedReceiver<UiMsg>) -> Self {
66        Self {
67            ui_state: UiState::default(),
68            outbox: Vec::new(),
69            app_tx,
70            ui_rx,
71        }
72    }
73
74    fn poll_ui_msgs(&mut self) {
75        while let Ok(msg) = self.ui_rx.try_recv() {
76            match msg {
77                UiMsg::UpdatePreset(preset) => {
78                    self.ui_state.preset = Some(*preset);
79                }
80
81                UiMsg::UserMsg(e) => {
82                    self.ui_state.user_error = Some(e);
83                }
84            }
85        }
86    }
87}
88
89impl eframe::App for AkaiMpd226Editor {
90    fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
91        self.poll_ui_msgs();
92
93        CentralPanel::default().show(ctx, |ui| {
94            ui.vertical(|ui| {
95                render_header(ui, &mut self.ui_state, &mut self.outbox);
96                ui.add_space(16.0);
97                render_editor(ui, &mut self.ui_state);
98                ui.add_space(16.0);
99                render_pad_patterns(ui, &mut self.ui_state);
100            });
101        });
102
103        for msg in self.outbox.drain(..) {
104            let _ = self.app_tx.send(msg);
105        }
106
107        ctx.request_repaint_after(std::time::Duration::from_millis(17));
108    }
109}
110
111const APP_X: f32 = 1295.;
112const APP_Y: f32 = 1024.;
113pub const APP_DIMENSIONS: Vec2 = Vec2 { x: APP_X, y: APP_Y };
114
115const PAD_X: f32 = 64.;
116const PAD_Y: f32 = 64.;
117const PAD_DIMENSIONS: Vec2 = Vec2 { x: PAD_X, y: PAD_Y };
118
119const DIAL_X: f32 = 48.;
120const DIAL_Y: f32 = 48.;
121const DIAL_DIMENSIONS: Vec2 = Vec2 {
122    x: DIAL_X,
123    y: DIAL_Y,
124};
125
126const FADER_X: f32 = 48.;
127const FADER_Y: f32 = 80.;
128const FADER_DIMENSIONS: Vec2 = Vec2 {
129    x: FADER_X,
130    y: FADER_Y,
131};
132
133const SWITCH_X: f32 = 48.;
134const SWITCH_Y: f32 = 24.;
135const SWITCH_DIMENSIONS: Vec2 = Vec2 {
136    x: SWITCH_X,
137    y: SWITCH_Y,
138};
139
140const BANKS: [&str; 4] = ["A", "B", "C", "D"];
141const CONTROL_BANKS: [&str; 3] = ["A", "B", "C"];
142
143#[derive(Clone, Copy, PartialEq, Eq)]
144pub enum UserSelection {
145    Pad { id: usize },
146    Dial { id: usize },
147    Fader { id: usize },
148    Switch { id: usize },
149}
150
151pub struct MidiStatus {
152    pub success: bool,
153    pub message: String,
154    pub timestamp: Instant,
155}
156
157#[derive(Default)]
158pub struct UiState {
159    pub selected_item: Option<UserSelection>,
160    pub preset: Option<Preset>,
161    pub note_mapping: NoteMappingState,
162    pub off_color_mapping: ColorMappingState,
163    pub on_color_mapping: ColorMappingState,
164    pub user_error: Option<UserMsg>,
165}
166
167pub struct NoteMappingState {
168    pub scale_seq: ScaleSequence,
169    pub starting_from_pad: usize,
170    pub tonic_highlighting_enabled: bool,
171    pub tonic_color: PadColor,
172}
173impl Default for NoteMappingState {
174    fn default() -> Self {
175        Self {
176            scale_seq: ScaleSequence {
177                tonic: PitchClass::C,
178                scale: ScaleKind::Chromatic,
179                direction: SequenceDirection::Ascending,
180                octave: Octave::O4,
181                length: 64,
182            },
183            starting_from_pad: 0,
184            tonic_highlighting_enabled: true,
185            tonic_color: PadColor::Red,
186        }
187    }
188}
189
190pub struct ColorMappingState {
191    pub pattern: ColorPattern,
192    pub length: usize,
193    pub starting_from_pad: usize,
194}
195impl Default for ColorMappingState {
196    fn default() -> Self {
197        Self {
198            pattern: ColorPattern::Contiguous(PadColor::Off),
199            length: 64,
200            starting_from_pad: 0,
201        }
202    }
203}
204
205fn render_header(ui: &mut Ui, ui_state: &mut UiState, outbox: &mut Vec<AppMsg>) {
206    const ROW_SPACING: f32 = 8.0;
207
208    if let Some(preset) = &mut ui_state.preset {
209        ui.horizontal(|ui| {
210            ui.label("Slot:");
211            ComboBox::from_id_salt("header_preset_slot")
212                .selected_text(format!("{:?}", preset.global.preset_slot))
213                .show_ui(ui, |ui| {
214                    for slot in PresetSlot::iter() {
215                        if ui
216                            .selectable_value(
217                                &mut preset.global.preset_slot,
218                                slot,
219                                format!("{:?}", slot),
220                            )
221                            .clicked()
222                        {
223                            preset.global.preset_slot = slot;
224                        }
225                    }
226                });
227
228            ui.separator();
229
230            ui.label("Name:");
231
232            let mut text = preset
233                .global
234                .preset_name
235                .0
236                .iter()
237                .map(|&b| if b.is_ascii() { b as char } else { ' ' })
238                .collect::<String>()
239                .trim_end()
240                .to_string();
241
242            ui.add(
243                TextEdit::singleline(&mut text)
244                    .char_limit(8)
245                    .desired_width(80.0),
246            );
247
248            let mut buf = [b' '; 8];
249            for (i, b) in text.bytes().filter(|b| b.is_ascii()).take(8).enumerate() {
250                buf[i] = b;
251            }
252            preset.global.preset_name = PresetName(buf);
253
254            ui.separator();
255
256            ui.label("Tempo:");
257            let mut tempo_val = preset.global.tempo.0 as u32;
258            if ui
259                .add(DragValue::new(&mut tempo_val).range(30..=300))
260                .changed()
261            {
262                preset.global.tempo = Tempo(tempo_val as u16);
263            }
264
265            ui.separator();
266
267            ui.label("Division:");
268            ComboBox::from_id_salt("header_time_division")
269                .selected_text(format!("{}", preset.global.time_division))
270                .show_ui(ui, |ui| {
271                    for variant in TimeDivision::iter() {
272                        if ui
273                            .selectable_value(
274                                &mut preset.global.time_division,
275                                variant,
276                                format!("{}", variant),
277                            )
278                            .clicked()
279                        {
280                            preset.global.time_division = variant;
281                        }
282                    }
283                });
284        });
285
286        ui.add_space(ROW_SPACING);
287
288        ui.horizontal(|ui| {
289            ui.label("Div Switch:");
290            ComboBox::from_id_salt("header_time_division_switch")
291                .selected_text(format!("{}", preset.global.time_division_switch))
292                .show_ui(ui, |ui| {
293                    for variant in TriggerKind::iter() {
294                        if ui
295                            .selectable_value(
296                                &mut preset.global.time_division_switch,
297                                variant,
298                                format!("{}", variant),
299                            )
300                            .clicked()
301                        {
302                            preset.global.time_division_switch = variant;
303                        }
304                    }
305                });
306
307            ui.separator();
308
309            ui.label("Repeat:");
310            ComboBox::from_id_salt("header_note_repeat_switch")
311                .selected_text(format!("{}", preset.global.note_repeat_switch))
312                .show_ui(ui, |ui| {
313                    for variant in TriggerKind::iter() {
314                        if ui
315                            .selectable_value(
316                                &mut preset.global.note_repeat_switch,
317                                variant,
318                                format!("{}", variant),
319                            )
320                            .clicked()
321                        {
322                            preset.global.note_repeat_switch = variant;
323                        }
324                    }
325                });
326
327            ui.separator();
328
329            ui.label("Gate:");
330            let mut gate_val = preset.global.gate as u8;
331            if ui
332                .add(DragValue::new(&mut gate_val).range(0..=100))
333                .changed()
334                && let Ok(g) = GateValue::try_from(gate_val)
335            {
336                preset.global.gate = g;
337            }
338
339            ui.separator();
340
341            ui.label("Swing:");
342            ComboBox::from_id_salt("header_swing")
343                .selected_text(format!("{}", preset.global.swing))
344                .show_ui(ui, |ui| {
345                    for variant in SwingKind::iter() {
346                        if ui
347                            .selectable_value(
348                                &mut preset.global.swing,
349                                variant,
350                                format!("{}", variant),
351                            )
352                            .clicked()
353                        {
354                            preset.global.swing = variant;
355                        }
356                    }
357                });
358
359            ui.separator();
360
361            ui.label("Transport:");
362            ComboBox::from_id_salt("header_transport")
363                .selected_text(format!("{}", preset.global.transport))
364                .show_ui(ui, |ui| {
365                    for variant in TransportKind::iter() {
366                        if ui
367                            .selectable_value(
368                                &mut preset.global.transport,
369                                variant,
370                                format!("{}", variant),
371                            )
372                            .clicked()
373                        {
374                            preset.global.transport = variant;
375                        }
376                    }
377                });
378        });
379    } else {
380        ui.label("No preset loaded");
381    }
382
383    ui.add_space(ROW_SPACING);
384
385    ui.horizontal(|ui| {
386        if let Some(preset) = &ui_state.preset
387            && ui.button("Load from device").clicked()
388        {
389            ui_state.user_error = None;
390            outbox.push(AppMsg::Ui(UiEffect::RequestPresetFromDevice(
391                preset.global.preset_slot,
392            )));
393        }
394
395        if let Some(preset) = ui_state.preset
396            && ui.button("Send to device").clicked()
397        {
398            ui_state.user_error = None;
399            outbox.push(AppMsg::Ui(UiEffect::SendPresetToDevice(Box::new(preset))));
400        }
401
402        if let Some(status) = &ui_state.user_error {
403            let color = match status.kind {
404                midilab::message::UserMsgKind::Status => Color32::GREEN,
405                midilab::message::UserMsgKind::Error => Color32::RED,
406            };
407
408            ui.colored_label(color, &status.msg);
409        }
410    });
411}
412
413fn render_pad_patterns(ui: &mut Ui, ui_state: &mut UiState) {
414    ui.horizontal(|ui| {
415        ui.set_min_height(128.0);
416        render_note_mapping(ui, ui_state);
417
418        ui.add_space(32.0);
419
420        render_color_mapping(ui, ui_state);
421
422        ui.add_space(32.0);
423
424        render_on_color_mapping(ui, ui_state);
425
426        ui.add_space(32.0);
427
428        selection_compare_table(ui, ui_state);
429    });
430}
431
432fn render_note_mapping(ui: &mut Ui, ui_state: &mut UiState) {
433    ui.vertical(|ui| {
434        ui.label("Note Mapping");
435
436        ui.horizontal(|ui| {
437            ui.label("Tonic");
438            ComboBox::from_id_salt("note_mapping_tonic")
439                .selected_text(ui_state.note_mapping.scale_seq.tonic.to_string())
440                .show_ui(ui, |ui| {
441                    for p in PitchClass::iter() {
442                        if ui
443                            .selectable_value(
444                                &mut ui_state.note_mapping.scale_seq.tonic,
445                                p,
446                                p.to_string(),
447                            )
448                            .clicked()
449                        {
450                            ui_state.note_mapping.scale_seq.tonic = p;
451                        }
452                    }
453                });
454        });
455
456        ui.horizontal(|ui| {
457            ui.label("Scale");
458            ComboBox::from_id_salt("note_mapping_scale")
459                .selected_text(ui_state.note_mapping.scale_seq.scale.to_string())
460                .show_ui(ui, |ui| {
461                    for s in ScaleKind::iter() {
462                        if ui
463                            .selectable_value(
464                                &mut ui_state.note_mapping.scale_seq.scale,
465                                s,
466                                s.to_string(),
467                            )
468                            .clicked()
469                        {
470                            ui_state.note_mapping.scale_seq.scale = s;
471                        }
472                    }
473                });
474        });
475
476        ui.horizontal(|ui| {
477            ui.label("Octave");
478            ComboBox::from_id_salt("note_mapping_octave")
479                .selected_text(ui_state.note_mapping.scale_seq.octave.to_string())
480                .show_ui(ui, |ui| {
481                    for s in Octave::iter() {
482                        if ui
483                            .selectable_value(
484                                &mut ui_state.note_mapping.scale_seq.octave,
485                                s,
486                                s.to_string(),
487                            )
488                            .clicked()
489                        {
490                            ui_state.note_mapping.scale_seq.octave = s;
491                        }
492                    }
493                });
494        });
495
496        ui.horizontal(|ui| {
497            ui.label("Direction");
498            ComboBox::from_id_salt("note_mapping_direction")
499                .selected_text(ui_state.note_mapping.scale_seq.direction.to_string())
500                .show_ui(ui, |ui| {
501                    for s in SequenceDirection::iter() {
502                        if ui
503                            .selectable_value(
504                                &mut ui_state.note_mapping.scale_seq.direction,
505                                s,
506                                s.to_string(),
507                            )
508                            .clicked()
509                        {
510                            ui_state.note_mapping.scale_seq.direction = s;
511                        }
512                    }
513                });
514        });
515
516        ui.horizontal(|ui| {
517            ui.label("Starting from Pad");
518
519            ui.add(DragValue::new(&mut ui_state.note_mapping.starting_from_pad).range(0..=63))
520        });
521
522        ui.horizontal(|ui| {
523            ui.label("Length");
524
525            ui.add(DragValue::new(&mut ui_state.note_mapping.scale_seq.length).range(1..=64))
526        });
527
528        ui.add_space(8.0);
529
530        ui.checkbox(
531            &mut ui_state.note_mapping.tonic_highlighting_enabled,
532            "Tonic highlighting",
533        );
534        if ui_state.note_mapping.tonic_highlighting_enabled {
535            ui.horizontal(|ui| {
536                ui.label("Tonic color");
537                ComboBox::from_id_salt("tonic_highlight_color")
538                    .selected_text(ui_state.note_mapping.tonic_color.to_string())
539                    .show_ui(ui, |ui| {
540                        for c in PadColor::iter() {
541                            if ui
542                                .selectable_value(
543                                    &mut ui_state.note_mapping.tonic_color,
544                                    c,
545                                    c.to_string(),
546                                )
547                                .clicked()
548                            {
549                                ui_state.note_mapping.tonic_color = c;
550                            }
551                        }
552                    });
553            });
554        }
555
556        ui.add_space(8.0);
557
558        let resp = ui.button("Set pattern");
559        if resp.clicked()
560            && let Some(preset) = ui_state.preset.as_mut()
561        {
562            let scale_seq = ui_state.note_mapping.scale_seq;
563            preset.pads.set_note_pattern(
564                ui_state.note_mapping.starting_from_pad,
565                NotePattern::Scale(scale_seq),
566            );
567
568            if ui_state.note_mapping.tonic_highlighting_enabled {
569                let tonic_color = (scale_seq.tonic, ui_state.note_mapping.tonic_color);
570                preset.pads.highlight_tonics(
571                    ui_state.note_mapping.starting_from_pad,
572                    scale_seq.length,
573                    tonic_color,
574                );
575            }
576        }
577    });
578}
579
580fn render_color_mapping(ui: &mut Ui, ui_state: &mut UiState) {
581    ui.vertical(|ui| {
582        ui.label("Off Color Mapping");
583
584        ui.horizontal(|ui| {
585            let is_contiguous = matches!(
586                ui_state.off_color_mapping.pattern,
587                ColorPattern::Contiguous(_)
588            );
589            if ui.selectable_label(is_contiguous, "Contiguous").clicked() && !is_contiguous {
590                ui_state.off_color_mapping.pattern = ColorPattern::Contiguous(PadColor::Off);
591            }
592            if ui.selectable_label(!is_contiguous, "Grouped").clicked() && is_contiguous {
593                ui_state.off_color_mapping.pattern = ColorPattern::Grouped(vec![]);
594            }
595        });
596
597        ui.add_space(4.0);
598
599        render_color_pattern_editor(ui, "off", &mut ui_state.off_color_mapping.pattern);
600
601        ui.add_space(4.0);
602
603        ui.horizontal(|ui| {
604            ui.label("Start Pad");
605            ui.add(DragValue::new(&mut ui_state.off_color_mapping.starting_from_pad).range(0..=63));
606        });
607
608        ui.horizontal(|ui| {
609            ui.label("Length");
610            ui.add(DragValue::new(&mut ui_state.off_color_mapping.length).range(1..=64));
611        });
612
613        if ui.button("Apply").clicked()
614            && let Some(preset) = ui_state.preset.as_mut()
615        {
616            preset.pads.set_off_color_pattern(
617                ui_state.off_color_mapping.starting_from_pad,
618                ui_state.off_color_mapping.length,
619                ui_state.off_color_mapping.pattern.clone(),
620            );
621        }
622    });
623}
624
625fn render_on_color_mapping(ui: &mut Ui, ui_state: &mut UiState) {
626    ui.vertical(|ui| {
627        ui.label("On Color Mapping");
628
629        ui.horizontal(|ui| {
630            let is_contiguous = matches!(
631                ui_state.on_color_mapping.pattern,
632                ColorPattern::Contiguous(_)
633            );
634            if ui.selectable_label(is_contiguous, "Contiguous").clicked() && !is_contiguous {
635                ui_state.on_color_mapping.pattern = ColorPattern::Contiguous(PadColor::Off);
636            }
637            if ui.selectable_label(!is_contiguous, "Grouped").clicked() && is_contiguous {
638                ui_state.on_color_mapping.pattern = ColorPattern::Grouped(vec![]);
639            }
640        });
641
642        ui.add_space(4.0);
643
644        render_color_pattern_editor(ui, "on", &mut ui_state.on_color_mapping.pattern);
645
646        ui.add_space(4.0);
647
648        ui.horizontal(|ui| {
649            ui.label("Start Pad");
650            ui.add(DragValue::new(&mut ui_state.on_color_mapping.starting_from_pad).range(0..=63));
651        });
652
653        ui.horizontal(|ui| {
654            ui.label("Length");
655            ui.add(DragValue::new(&mut ui_state.on_color_mapping.length).range(1..=64));
656        });
657
658        if ui.button("Apply").clicked()
659            && let Some(preset) = ui_state.preset.as_mut()
660        {
661            preset.pads.set_on_color_pattern(
662                ui_state.on_color_mapping.starting_from_pad,
663                ui_state.on_color_mapping.length,
664                ui_state.on_color_mapping.pattern.clone(),
665            );
666        }
667    });
668}
669
670fn render_color_pattern_editor(ui: &mut Ui, id_prefix: &str, pattern: &mut ColorPattern) {
671    match pattern {
672        ColorPattern::Contiguous(color) => {
673            ui.horizontal(|ui| {
674                ui.label("Color");
675                ComboBox::from_id_salt(format!("{}_color", id_prefix))
676                    .selected_text(color.to_string())
677                    .show_ui(ui, |ui| {
678                        for c in PadColor::iter() {
679                            ui.selectable_value(color, c, c.to_string());
680                        }
681                    });
682            });
683        }
684        ColorPattern::Grouped(sequences) => {
685            if ui.button("+ Add").clicked() {
686                sequences.push(ColorSequence {
687                    len: 4,
688                    color: PadColor::Off,
689                });
690            }
691
692            let mut to_remove: Option<usize> = None;
693            for (idx, seq) in sequences.iter_mut().enumerate() {
694                ui.horizontal(|ui| {
695                    ComboBox::from_id_salt(format!("{}_{}_color", id_prefix, idx))
696                        .width(80.0)
697                        .selected_text(seq.color.to_string())
698                        .show_ui(ui, |ui| {
699                            for c in PadColor::iter() {
700                                ui.selectable_value(&mut seq.color, c, c.to_string());
701                            }
702                        });
703
704                    ui.label("x");
705                    ui.add(DragValue::new(&mut seq.len).range(1..=64));
706
707                    if ui.button("X").clicked() {
708                        to_remove = Some(idx);
709                    }
710                });
711            }
712
713            if let Some(idx) = to_remove {
714                sequences.remove(idx);
715            }
716        }
717    }
718}
719
720fn render_all_pad_banks(
721    ui: &mut Ui,
722    selected_item: &mut Option<UserSelection>,
723    pad_repo: &mut PadRepository,
724) {
725    let banks: Vec<Vec<Pad>> = pad_repo
726        .pads
727        .chunks(16)
728        .map(|chunk| chunk.to_vec())
729        .collect();
730
731    ui.add_space(16.0);
732    ui.horizontal(|ui| {
733        ui.add_space(16.0);
734        for (bank_id, bank) in banks.into_iter().enumerate() {
735            let bank_label = BANKS[bank_id].to_string();
736            render_pad_bank(ui, selected_item, bank, bank_label);
737            ui.add_space(32.0);
738        }
739    });
740}
741
742fn render_pad_bank(
743    ui: &mut Ui,
744    selected_item: &mut Option<UserSelection>,
745    pads: Vec<Pad>,
746    label: String,
747) {
748    let reordered: Vec<Pad> = pads.chunks(4).rev().flatten().cloned().collect();
749
750    ui.vertical(|ui| {
751        ui.label(format!("Bank {}", label));
752        ui.add_space(8.0);
753        Grid::new(format!("pad_bank_{}", label))
754            .num_columns(4)
755            .spacing([8.0, 8.0])
756            .show(ui, |ui| {
757                for (i, pad) in reordered.into_iter().enumerate() {
758                    render_pad(ui, selected_item, pad);
759                    if (i + 1) % 4 == 0 {
760                        ui.end_row();
761                    }
762                }
763            });
764    });
765}
766
767fn render_pad(ui: &mut Ui, selected_item: &mut Option<UserSelection>, pad: Pad) {
768    let (rect, resp) = ui.allocate_exact_size(PAD_DIMENSIONS, egui::Sense::click());
769
770    ui.painter().rect_filled(rect, 4.0, Color32::DARK_GRAY);
771
772    if let Some(UserSelection::Pad { id }) = selected_item
773        && pad.id == *id
774    {
775        ui.painter().rect_stroke(
776            rect,
777            4.0,
778            egui::Stroke::new(1.5, Color32::WHITE),
779            egui::StrokeKind::Outside,
780        );
781    }
782
783    let half_w = rect.width() * 0.5;
784    let half_h = rect.height() * 0.5;
785
786    let tl = egui::Rect::from_min_size(rect.min, vec2(half_w, half_h));
787    let tr = egui::Rect::from_min_size(rect.min + vec2(half_w, 0.0), vec2(half_w, half_h));
788    let bl = egui::Rect::from_min_size(rect.min + vec2(0.0, half_h), vec2(half_w, half_h));
789    let br = egui::Rect::from_min_size(rect.min + vec2(half_w, half_h), vec2(half_w, half_h));
790
791    ui.painter().text(
792        tl.center(),
793        egui::Align2::CENTER_CENTER,
794        pad.id.to_string(),
795        egui::FontId::proportional(12.0),
796        Color32::WHITE,
797    );
798
799    ui.painter().text(
800        tr.center(),
801        egui::Align2::CENTER_CENTER,
802        format!("♩{}", pad.note),
803        egui::FontId::proportional(12.0),
804        Color32::WHITE,
805    );
806
807    let (r, g, b) = *pad.off_color.as_rgb_color();
808    let off_color = Color32::from_rgb(r, g, b);
809    ui.painter().rect_filled(bl.shrink(4.0), 4.0, off_color);
810
811    let (r, g, b) = *pad.on_color.as_rgb_color();
812    let on_color = Color32::from_rgb(r, g, b);
813    ui.painter().rect_filled(br.shrink(4.0), 4.0, on_color);
814
815    if resp.clicked() {
816        click_pad(pad.id, selected_item);
817    }
818}
819
820fn render_editor(ui: &mut Ui, ui_state: &mut UiState) {
821    if let Some(preset) = &mut ui_state.preset {
822        render_all_pad_banks(ui, &mut ui_state.selected_item, &mut preset.pads);
823        ui.add_space(16.0);
824        render_all_control_banks(
825            ui,
826            &mut ui_state.selected_item,
827            &mut preset.dials,
828            &mut preset.faders,
829            &mut preset.switches,
830        );
831    } else {
832        ui.label("no pad repo");
833    }
834}
835
836fn click_pad(id: usize, selected_item: &mut Option<UserSelection>) {
837    if *selected_item == Some(UserSelection::Pad { id }) {
838        *selected_item = None;
839    } else {
840        *selected_item = Some(UserSelection::Pad { id });
841    }
842}
843
844fn render_all_control_banks(
845    ui: &mut Ui,
846    selected_item: &mut Option<UserSelection>,
847    dial_repo: &mut DialRepository,
848    fader_repo: &mut FaderRepository,
849    switch_repo: &mut SwitchRepository,
850) {
851    ui.add_space(16.0);
852    ui.horizontal(|ui| {
853        ui.add_space(16.0);
854        for (bank_idx, bank_label) in CONTROL_BANKS.iter().enumerate() {
855            render_control_bank(
856                ui,
857                selected_item,
858                dial_repo,
859                fader_repo,
860                switch_repo,
861                bank_idx,
862                bank_label,
863            );
864            ui.add_space(32.0);
865        }
866    });
867}
868
869fn render_control_bank(
870    ui: &mut Ui,
871    selected_item: &mut Option<UserSelection>,
872    dial_repo: &mut DialRepository,
873    fader_repo: &mut FaderRepository,
874    switch_repo: &mut SwitchRepository,
875    bank_idx: usize,
876    bank_label: &str,
877) {
878    ui.vertical(|ui| {
879        ui.label(format!("Control Bank {}", bank_label));
880        ui.add_space(8.0);
881
882        ui.horizontal(|ui| {
883            for dial_offset in 0..4 {
884                let dial_id = bank_idx * 4 + dial_offset;
885                let dial = dial_repo.0[dial_id];
886                render_dial(ui, selected_item, dial, dial_id);
887                ui.add_space(4.0);
888            }
889        });
890
891        ui.add_space(8.0);
892
893        ui.horizontal(|ui| {
894            for fader_offset in 0..4 {
895                let fader_id = bank_idx * 4 + fader_offset;
896                let fader = fader_repo.0[fader_id];
897                render_fader(ui, selected_item, fader, fader_id);
898                ui.add_space(4.0);
899            }
900        });
901
902        ui.add_space(8.0);
903
904        ui.horizontal(|ui| {
905            for switch_offset in 0..4 {
906                let switch_id = bank_idx * 4 + switch_offset;
907                let switch = switch_repo.0[switch_id];
908                render_switch(ui, selected_item, switch, switch_id);
909                ui.add_space(4.0);
910            }
911        });
912    });
913}
914
915fn render_dial(
916    ui: &mut Ui,
917    selected_item: &mut Option<UserSelection>,
918    _dial: Dial,
919    dial_id: usize,
920) {
921    let (rect, resp) = ui.allocate_exact_size(DIAL_DIMENSIONS, egui::Sense::click());
922
923    ui.painter().rect_filled(rect, 24.0, Color32::DARK_GRAY);
924
925    if let Some(UserSelection::Dial { id }) = selected_item
926        && dial_id == *id
927    {
928        ui.painter().rect_stroke(
929            rect,
930            24.0,
931            egui::Stroke::new(1.5, Color32::WHITE),
932            egui::StrokeKind::Outside,
933        );
934    }
935
936    ui.painter().text(
937        rect.center(),
938        egui::Align2::CENTER_CENTER,
939        dial_id.to_string(),
940        egui::FontId::proportional(12.0),
941        Color32::WHITE,
942    );
943
944    if resp.clicked() {
945        click_dial(dial_id, selected_item);
946    }
947}
948
949fn click_dial(id: usize, selected_item: &mut Option<UserSelection>) {
950    if *selected_item == Some(UserSelection::Dial { id }) {
951        *selected_item = None;
952    } else {
953        *selected_item = Some(UserSelection::Dial { id });
954    }
955}
956
957fn render_fader(
958    ui: &mut Ui,
959    selected_item: &mut Option<UserSelection>,
960    _fader: Fader,
961    fader_id: usize,
962) {
963    let (rect, resp) = ui.allocate_exact_size(FADER_DIMENSIONS, egui::Sense::click());
964
965    ui.painter().rect_filled(rect, 4.0, Color32::DARK_GRAY);
966
967    if let Some(UserSelection::Fader { id }) = selected_item
968        && fader_id == *id
969    {
970        ui.painter().rect_stroke(
971            rect,
972            4.0,
973            egui::Stroke::new(1.5, Color32::WHITE),
974            egui::StrokeKind::Outside,
975        );
976    }
977
978    ui.painter().text(
979        rect.center(),
980        egui::Align2::CENTER_CENTER,
981        fader_id.to_string(),
982        egui::FontId::proportional(10.0),
983        Color32::WHITE,
984    );
985
986    if resp.clicked() {
987        click_fader(fader_id, selected_item);
988    }
989}
990
991fn click_fader(id: usize, selected_item: &mut Option<UserSelection>) {
992    if *selected_item == Some(UserSelection::Fader { id }) {
993        *selected_item = None;
994    } else {
995        *selected_item = Some(UserSelection::Fader { id });
996    }
997}
998
999fn render_switch(
1000    ui: &mut Ui,
1001    selected_item: &mut Option<UserSelection>,
1002    _switch: Switch,
1003    switch_id: usize,
1004) {
1005    let (rect, resp) = ui.allocate_exact_size(SWITCH_DIMENSIONS, egui::Sense::click());
1006
1007    ui.painter().rect_filled(rect, 4.0, Color32::DARK_GRAY);
1008
1009    if let Some(UserSelection::Switch { id }) = selected_item
1010        && switch_id == *id
1011    {
1012        ui.painter().rect_stroke(
1013            rect,
1014            4.0,
1015            egui::Stroke::new(1.5, Color32::WHITE),
1016            egui::StrokeKind::Outside,
1017        );
1018    }
1019
1020    ui.painter().text(
1021        rect.center(),
1022        egui::Align2::CENTER_CENTER,
1023        switch_id.to_string(),
1024        egui::FontId::proportional(10.0),
1025        Color32::WHITE,
1026    );
1027
1028    if resp.clicked() {
1029        click_switch(switch_id, selected_item);
1030    }
1031}
1032
1033fn click_switch(id: usize, selected_item: &mut Option<UserSelection>) {
1034    if *selected_item == Some(UserSelection::Switch { id }) {
1035        *selected_item = None;
1036    } else {
1037        *selected_item = Some(UserSelection::Switch { id });
1038    }
1039}
1040
1041fn selection_compare_table(ui: &mut Ui, ui_state: &mut UiState) {
1042    ui.vertical(|ui| {
1043        let selection_label = match ui_state.selected_item {
1044            Some(UserSelection::Pad { id }) => format!("Pad {}", id),
1045            Some(UserSelection::Dial { id }) => {
1046                let bank = CONTROL_BANKS[id / 4];
1047                let num = (id % 4) + 1;
1048                format!("Dial {}{}", bank, num)
1049            }
1050            Some(UserSelection::Fader { id }) => {
1051                let bank = CONTROL_BANKS[id / 4];
1052                let num = (id % 4) + 1;
1053                format!("Fader {}{}", bank, num)
1054            }
1055            Some(UserSelection::Switch { id }) => {
1056                let bank = CONTROL_BANKS[id / 4];
1057                let num = (id % 4) + 1;
1058                format!("Switch {}{}", bank, num)
1059            }
1060            None => "None".to_string(),
1061        };
1062        ui.label(format!("Selected: {}", selection_label));
1063
1064        if let Some(preset) = &mut ui_state.preset {
1065            match ui_state.selected_item {
1066                Some(UserSelection::Pad { id: index }) => {
1067                    if let Some(pad) = preset.pads.pads.iter_mut().find(|p| p.id == index) {
1068                        render_pad_compare_grid(ui, pad);
1069                    }
1070                }
1071                Some(UserSelection::Dial { id: index }) => {
1072                    if let Some(dial) = preset.dials.0.get_mut(index) {
1073                        render_dial_compare_grid(ui, dial);
1074                    }
1075                }
1076                Some(UserSelection::Fader { id: index }) => {
1077                    if let Some(fader) = preset.faders.0.get_mut(index) {
1078                        render_fader_compare_grid(ui, fader);
1079                    }
1080                }
1081                Some(UserSelection::Switch { id: index }) => {
1082                    if let Some(switch) = preset.switches.0.get_mut(index) {
1083                        render_switch_compare_grid(ui, switch);
1084                    }
1085                }
1086                None => {}
1087            }
1088        }
1089    });
1090}
1091
1092fn render_pad_compare_grid(ui: &mut Ui, pad: &mut Pad) {
1093    Grid::new("pad_compare_grid")
1094        .striped(true)
1095        .spacing([16.0, 6.0])
1096        .show(ui, |ui| {
1097            ui.label("Field");
1098            ui.label("Value");
1099            ui.end_row();
1100
1101            row_edit_pad_kind(ui, "kind", &mut pad.kind);
1102            row_edit_channel(ui, "channel", &mut pad.channel);
1103            row_edit_note(ui, "note", &mut pad.note);
1104            row_edit_midi2din(ui, "midi to din", &mut pad.midi2din);
1105            row_edit_trigger_kind(ui, "trigger", &mut pad.trigger);
1106            row_edit_aftertouch_kind(ui, "aftertouch", &mut pad.aftertouch);
1107            row_edit_u8(ui, "program", &mut pad.program);
1108            row_edit_u8(ui, "msb", &mut pad.msb);
1109            row_edit_u8(ui, "lsb", &mut pad.lsb);
1110            row_edit_pad_color(ui, "off color", &mut pad.off_color);
1111            row_edit_pad_color(ui, "on color", &mut pad.on_color);
1112        });
1113}
1114
1115fn render_dial_compare_grid(ui: &mut Ui, dial: &mut Dial) {
1116    Grid::new("dial_compare_grid")
1117        .striped(true)
1118        .spacing([16.0, 6.0])
1119        .show(ui, |ui| {
1120            ui.label("Field");
1121            ui.label("Value");
1122            ui.end_row();
1123
1124            row_edit_dial_kind(ui, "kind", &mut dial.kind);
1125            row_edit_channel(ui, "channel", &mut dial.channel);
1126            row_edit_u8(ui, "midicc", &mut dial.midicc);
1127            row_edit_u8(ui, "min", &mut dial.min);
1128            row_edit_u8(ui, "max", &mut dial.max);
1129            row_edit_midi2din(ui, "midi to din", &mut dial.midi2din);
1130            row_edit_u8(ui, "msb", &mut dial.msb);
1131            row_edit_u8(ui, "lsb", &mut dial.lsb);
1132            row_edit_u8(ui, "value", &mut dial.value);
1133        });
1134}
1135
1136fn render_fader_compare_grid(ui: &mut Ui, fader: &mut Fader) {
1137    Grid::new("fader_compare_grid")
1138        .striped(true)
1139        .spacing([16.0, 6.0])
1140        .show(ui, |ui| {
1141            ui.label("Field");
1142            ui.label("Value");
1143            ui.end_row();
1144
1145            row_edit_fader_kind(ui, "kind", &mut fader.kind);
1146            row_edit_channel(ui, "channel", &mut fader.channel);
1147            row_edit_u8(ui, "midicc", &mut fader.midicc);
1148            row_edit_u8(ui, "min", &mut fader.min);
1149            row_edit_u8(ui, "max", &mut fader.max);
1150            row_edit_midi2din(ui, "midi to din", &mut fader.midi2din);
1151        });
1152}
1153
1154fn render_switch_compare_grid(ui: &mut Ui, switch: &mut Switch) {
1155    Grid::new("switch_compare_grid")
1156        .striped(true)
1157        .spacing([16.0, 6.0])
1158        .show(ui, |ui| {
1159            ui.label("Field");
1160            ui.label("Value");
1161            ui.end_row();
1162
1163            row_edit_switch_kind(ui, "kind", &mut switch.kind);
1164            row_edit_channel(ui, "channel", &mut switch.channel);
1165            row_edit_u8(ui, "midicc", &mut switch.midicc);
1166            row_edit_trigger_kind(ui, "mode", &mut switch.mode);
1167            row_edit_u8(ui, "prog", &mut switch.prog);
1168            row_edit_u8(ui, "msb", &mut switch.msb);
1169            row_edit_u8(ui, "lsb", &mut switch.lsb);
1170            row_edit_midi2din(ui, "midi to din", &mut switch.midi2din);
1171            row_edit_u8(ui, "note", &mut switch.note);
1172            row_edit_u8(ui, "velo", &mut switch.velo);
1173            row_edit_midi2din(ui, "invert", &mut switch.invert);
1174        });
1175}
1176
1177fn row_edit_u8(ui: &mut Ui, name: &str, value: &mut u8) {
1178    ui.label(name);
1179    let mut val = *value as u32;
1180    if ui.add(DragValue::new(&mut val).range(0..=127)).changed() {
1181        *value = val as u8;
1182    }
1183    ui.end_row();
1184}
1185
1186fn row_edit_note(ui: &mut Ui, name: &str, value: &mut Note) {
1187    ui.label(name);
1188    let mut val = *value as u32;
1189    if ui.add(DragValue::new(&mut val).range(0..=127)).changed()
1190        && let Ok(note) = Note::try_from(val as u8)
1191    {
1192        *value = note;
1193    }
1194    ui.end_row();
1195}
1196
1197fn row_edit_pad_kind(ui: &mut Ui, name: &str, value: &mut PadKind) {
1198    ui.label(name);
1199    ComboBox::from_id_salt(name)
1200        .selected_text(format!("{}", value))
1201        .show_ui(ui, |ui| {
1202            for variant in PadKind::iter() {
1203                ui.selectable_value(value, variant, format!("{}", variant));
1204            }
1205        });
1206    ui.end_row();
1207}
1208
1209fn row_edit_channel(ui: &mut Ui, name: &str, value: &mut MidiChannel) {
1210    ui.label(name);
1211    ComboBox::from_id_salt(name)
1212        .selected_text(format!("{}", value))
1213        .show_ui(ui, |ui| {
1214            for variant in MidiChannel::iter() {
1215                ui.selectable_value(value, variant, format!("{}", variant));
1216            }
1217        });
1218    ui.end_row();
1219}
1220
1221fn row_edit_dial_kind(ui: &mut Ui, name: &str, value: &mut DialKind) {
1222    ui.label(name);
1223    ComboBox::from_id_salt(name)
1224        .selected_text(format!("{:?}", value))
1225        .show_ui(ui, |ui| {
1226            for variant in DialKind::iter() {
1227                ui.selectable_value(value, variant, format!("{:?}", variant));
1228            }
1229        });
1230    ui.end_row();
1231}
1232
1233fn row_edit_fader_kind(ui: &mut Ui, name: &str, value: &mut FaderKind) {
1234    ui.label(name);
1235    ComboBox::from_id_salt(name)
1236        .selected_text(format!("{:?}", value))
1237        .show_ui(ui, |ui| {
1238            for variant in FaderKind::iter() {
1239                ui.selectable_value(value, variant, format!("{:?}", variant));
1240            }
1241        });
1242    ui.end_row();
1243}
1244
1245fn row_edit_switch_kind(ui: &mut Ui, name: &str, value: &mut SwitchKind) {
1246    ui.label(name);
1247    ComboBox::from_id_salt(name)
1248        .selected_text(format!("{:?}", value))
1249        .show_ui(ui, |ui| {
1250            for variant in SwitchKind::iter() {
1251                ui.selectable_value(value, variant, format!("{:?}", variant));
1252            }
1253        });
1254    ui.end_row();
1255}
1256
1257fn row_edit_trigger_kind(ui: &mut Ui, name: &str, value: &mut TriggerKind) {
1258    ui.label(name);
1259    ComboBox::from_id_salt(name)
1260        .selected_text(format!("{}", value))
1261        .show_ui(ui, |ui| {
1262            for variant in TriggerKind::iter() {
1263                ui.selectable_value(value, variant, format!("{}", variant));
1264            }
1265        });
1266    ui.end_row();
1267}
1268
1269fn row_edit_aftertouch_kind(ui: &mut Ui, name: &str, value: &mut AfterTouchKind) {
1270    ui.label(name);
1271    ComboBox::from_id_salt(name)
1272        .selected_text(format!("{}", value))
1273        .show_ui(ui, |ui| {
1274            for variant in AfterTouchKind::iter() {
1275                ui.selectable_value(value, variant, format!("{}", variant));
1276            }
1277        });
1278    ui.end_row();
1279}
1280
1281fn row_edit_pad_color(ui: &mut Ui, name: &str, value: &mut PadColor) {
1282    ui.label(name);
1283    ComboBox::from_id_salt(name)
1284        .selected_text(format!("{}", value))
1285        .show_ui(ui, |ui| {
1286            for variant in PadColor::iter() {
1287                ui.selectable_value(value, variant, format!("{}", variant));
1288            }
1289        });
1290    ui.end_row();
1291}
1292
1293fn row_edit_midi2din(ui: &mut Ui, name: &str, value: &mut ActiveState) {
1294    ui.label(name);
1295    ComboBox::from_id_salt(name)
1296        .selected_text(format!("{}", value))
1297        .show_ui(ui, |ui| {
1298            for variant in ActiveState::iter() {
1299                ui.selectable_value(value, variant, format!("{}", variant));
1300            }
1301        });
1302    ui.end_row();
1303}