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::NotePattern;
17use midilab::manufacturer::akai::mpd226::Preset;
18use midilab::manufacturer::akai::mpd226::control::Pad;
19use midilab::manufacturer::akai::mpd226::control::value_kind::AfterTouchKind;
20use midilab::manufacturer::akai::mpd226::control::value_kind::GateValue;
21use midilab::manufacturer::akai::mpd226::control::value_kind::Midi2Din;
22use midilab::manufacturer::akai::mpd226::control::value_kind::PadColor;
23use midilab::manufacturer::akai::mpd226::control::value_kind::PadKind;
24use midilab::manufacturer::akai::mpd226::control::value_kind::PresetName;
25use midilab::manufacturer::akai::mpd226::control::value_kind::PresetSlot;
26use midilab::manufacturer::akai::mpd226::control::value_kind::SwingKind;
27use midilab::manufacturer::akai::mpd226::control::value_kind::Tempo;
28use midilab::manufacturer::akai::mpd226::control::value_kind::TimeDivision;
29use midilab::manufacturer::akai::mpd226::control::value_kind::TransportKind;
30use midilab::manufacturer::akai::mpd226::control::value_kind::TriggerKind;
31use midilab::manufacturer::akai::mpd226::repository::PadRepository;
32use midilab::message::AppMsg;
33use midilab::message::UiEffect;
34use midilab::message::UiMsg;
35use midilab::message::UserMsg;
36use midilab::midi::Note;
37use midilab::scale::Octave;
38use midilab::scale::PitchClass;
39use midilab::scale::ScaleKind;
40use midilab::scale::ScaleSequence;
41use midilab::scale::SequenceDirection;
42use tokio::sync::mpsc::UnboundedReceiver;
43use tokio::sync::mpsc::UnboundedSender;
44
45#[allow(unused)]
46pub struct AkaiMpd226Editor {
47    ui_state: UiState,
48    outbox: Vec<AppMsg>,
49    app_tx: UnboundedSender<AppMsg>,
50    ui_rx: UnboundedReceiver<UiMsg>,
51}
52
53impl AkaiMpd226Editor {
54    pub fn new(app_tx: UnboundedSender<AppMsg>, ui_rx: UnboundedReceiver<UiMsg>) -> Self {
55        Self {
56            ui_state: UiState::default(),
57            outbox: Vec::new(),
58            app_tx,
59            ui_rx,
60        }
61    }
62
63    fn poll_ui_msgs(&mut self) {
64        while let Ok(msg) = self.ui_rx.try_recv() {
65            match msg {
66                UiMsg::UpdatePreset(preset) => {
67                    self.ui_state.preset = Some(*preset);
68                }
69
70                UiMsg::UserMsg(e) => {
71                    self.ui_state.user_error = Some(e);
72                }
73            }
74        }
75    }
76}
77
78impl eframe::App for AkaiMpd226Editor {
79    fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
80        self.poll_ui_msgs();
81
82        CentralPanel::default().show(ctx, |ui| {
83            ui.vertical(|ui| {
84                render_header(ui, &mut self.ui_state, &mut self.outbox);
85                ui.add_space(16.0);
86                render_editor(ui, &mut self.ui_state);
87                ui.add_space(16.0);
88                render_pad_patterns(ui, &mut self.ui_state);
89            });
90        });
91
92        for msg in self.outbox.drain(..) {
93            let _ = self.app_tx.send(msg);
94        }
95
96        ctx.request_repaint_after(std::time::Duration::from_millis(17));
97    }
98}
99
100const APP_X: f32 = 1295.;
101const APP_Y: f32 = 1024.;
102pub const APP_DIMENSIONS: Vec2 = Vec2 { x: APP_X, y: APP_Y };
103
104const PAD_X: f32 = 64.;
105const PAD_Y: f32 = 64.;
106const PAD_DIMENSIONS: Vec2 = Vec2 { x: PAD_X, y: PAD_Y };
107
108const BANKS: [&str; 4] = ["A", "B", "C", "D"];
109
110#[derive(Clone, Copy, PartialEq, Eq)]
111pub enum UserSelection {
112    Pad { id: usize },
113}
114
115pub struct MidiStatus {
116    pub success: bool,
117    pub message: String,
118    pub timestamp: Instant,
119}
120
121#[derive(Default)]
122pub struct UiState {
123    pub selected_item: Option<UserSelection>,
124    pub preset: Option<Preset>,
125    pub note_mapping: NoteMappingState,
126    pub off_color_mapping: ColorMappingState,
127    pub on_color_mapping: ColorMappingState,
128    pub user_error: Option<UserMsg>,
129}
130
131pub struct NoteMappingState {
132    pub scale_seq: ScaleSequence,
133    pub starting_from_pad: usize,
134    pub tonic_highlighting_enabled: bool,
135    pub tonic_color: PadColor,
136}
137impl Default for NoteMappingState {
138    fn default() -> Self {
139        Self {
140            scale_seq: ScaleSequence {
141                tonic: PitchClass::C,
142                scale: ScaleKind::Chromatic,
143                direction: SequenceDirection::Ascending,
144                octave: Octave::O4,
145                length: 64,
146            },
147            starting_from_pad: 0,
148            tonic_highlighting_enabled: true,
149            tonic_color: PadColor::Red,
150        }
151    }
152}
153
154pub struct ColorMappingState {
155    pub color: PadColor,
156    pub length: usize,
157    pub starting_from_pad: usize,
158}
159impl Default for ColorMappingState {
160    fn default() -> Self {
161        Self {
162            color: PadColor::Off,
163            length: 64,
164            starting_from_pad: 0,
165        }
166    }
167}
168
169fn render_header(ui: &mut Ui, ui_state: &mut UiState, outbox: &mut Vec<AppMsg>) {
170    const ROW_SPACING: f32 = 8.0;
171
172    if let Some(preset) = &mut ui_state.preset {
173        ui.horizontal(|ui| {
174            ui.label("Slot:");
175            ComboBox::from_id_salt("header_preset_slot")
176                .selected_text(format!("{:?}", preset.global.preset_slot))
177                .show_ui(ui, |ui| {
178                    for slot in PresetSlot::iter() {
179                        if ui
180                            .selectable_value(
181                                &mut preset.global.preset_slot,
182                                slot,
183                                format!("{:?}", slot),
184                            )
185                            .clicked()
186                        {
187                            preset.global.preset_slot = slot;
188                        }
189                    }
190                });
191
192            ui.separator();
193
194            ui.label("Name:");
195
196            let mut text = preset
197                .global
198                .preset_name
199                .0
200                .iter()
201                .map(|&b| if b.is_ascii() { b as char } else { ' ' })
202                .collect::<String>()
203                .trim_end()
204                .to_string();
205
206            ui.add(
207                TextEdit::singleline(&mut text)
208                    .char_limit(8)
209                    .desired_width(80.0),
210            );
211
212            let mut buf = [b' '; 8];
213            for (i, b) in text.bytes().filter(|b| b.is_ascii()).take(8).enumerate() {
214                buf[i] = b;
215            }
216            preset.global.preset_name = PresetName(buf);
217
218            ui.separator();
219
220            ui.label("Tempo:");
221            let mut tempo_val = preset.global.tempo.0 as u32;
222            if ui
223                .add(DragValue::new(&mut tempo_val).range(30..=300))
224                .changed()
225            {
226                preset.global.tempo = Tempo(tempo_val as u16);
227            }
228
229            ui.separator();
230
231            ui.label("Division:");
232            ComboBox::from_id_salt("header_time_division")
233                .selected_text(format!("{}", preset.global.time_division))
234                .show_ui(ui, |ui| {
235                    for variant in TimeDivision::iter() {
236                        if ui
237                            .selectable_value(
238                                &mut preset.global.time_division,
239                                variant,
240                                format!("{}", variant),
241                            )
242                            .clicked()
243                        {
244                            preset.global.time_division = variant;
245                        }
246                    }
247                });
248        });
249
250        ui.add_space(ROW_SPACING);
251
252        ui.horizontal(|ui| {
253            ui.label("Div Switch:");
254            ComboBox::from_id_salt("header_time_division_switch")
255                .selected_text(format!("{}", preset.global.time_division_switch))
256                .show_ui(ui, |ui| {
257                    for variant in TriggerKind::iter() {
258                        if ui
259                            .selectable_value(
260                                &mut preset.global.time_division_switch,
261                                variant,
262                                format!("{}", variant),
263                            )
264                            .clicked()
265                        {
266                            preset.global.time_division_switch = variant;
267                        }
268                    }
269                });
270
271            ui.separator();
272
273            ui.label("Repeat:");
274            ComboBox::from_id_salt("header_note_repeat_switch")
275                .selected_text(format!("{}", preset.global.note_repeat_switch))
276                .show_ui(ui, |ui| {
277                    for variant in TriggerKind::iter() {
278                        if ui
279                            .selectable_value(
280                                &mut preset.global.note_repeat_switch,
281                                variant,
282                                format!("{}", variant),
283                            )
284                            .clicked()
285                        {
286                            preset.global.note_repeat_switch = variant;
287                        }
288                    }
289                });
290
291            ui.separator();
292
293            ui.label("Gate:");
294            let mut gate_val = preset.global.gate as u8;
295            if ui
296                .add(DragValue::new(&mut gate_val).range(0..=100))
297                .changed()
298                && let Ok(g) = GateValue::try_from(gate_val)
299            {
300                preset.global.gate = g;
301            }
302
303            ui.separator();
304
305            ui.label("Swing:");
306            ComboBox::from_id_salt("header_swing")
307                .selected_text(format!("{}", preset.global.swing))
308                .show_ui(ui, |ui| {
309                    for variant in SwingKind::iter() {
310                        if ui
311                            .selectable_value(
312                                &mut preset.global.swing,
313                                variant,
314                                format!("{}", variant),
315                            )
316                            .clicked()
317                        {
318                            preset.global.swing = variant;
319                        }
320                    }
321                });
322
323            ui.separator();
324
325            ui.label("Transport:");
326            ComboBox::from_id_salt("header_transport")
327                .selected_text(format!("{}", preset.global.transport))
328                .show_ui(ui, |ui| {
329                    for variant in TransportKind::iter() {
330                        if ui
331                            .selectable_value(
332                                &mut preset.global.transport,
333                                variant,
334                                format!("{}", variant),
335                            )
336                            .clicked()
337                        {
338                            preset.global.transport = variant;
339                        }
340                    }
341                });
342        });
343    } else {
344        ui.label("No preset loaded");
345    }
346
347    ui.add_space(ROW_SPACING);
348
349    ui.horizontal(|ui| {
350        if let Some(preset) = &ui_state.preset
351            && ui.button("Load from device").clicked()
352        {
353            ui_state.user_error = None;
354            outbox.push(AppMsg::Ui(UiEffect::RequestPresetFromDevice(
355                preset.global.preset_slot,
356            )));
357        }
358
359        if let Some(preset) = ui_state.preset
360            && ui.button("Send to device").clicked()
361        {
362            ui_state.user_error = None;
363            outbox.push(AppMsg::Ui(UiEffect::SendPresetToDevice(Box::new(preset))));
364        }
365
366        if let Some(status) = &ui_state.user_error {
367            let color = match status.kind {
368                midilab::message::UserMsgKind::Status => Color32::GREEN,
369                midilab::message::UserMsgKind::Error => Color32::RED,
370            };
371
372            ui.colored_label(color, &status.msg);
373        }
374    });
375}
376
377fn render_pad_patterns(ui: &mut Ui, ui_state: &mut UiState) {
378    ui.horizontal(|ui| {
379        ui.set_min_height(128.0);
380        render_note_mapping(ui, ui_state);
381
382        ui.add_space(32.0);
383
384        render_color_mapping(ui, ui_state);
385
386        ui.add_space(32.0);
387
388        render_on_color_mapping(ui, ui_state);
389
390        ui.add_space(32.0);
391
392        pad_compare_table(ui, ui_state);
393    });
394}
395
396fn render_note_mapping(ui: &mut Ui, ui_state: &mut UiState) {
397    ui.vertical(|ui| {
398        ui.label("Note Mapping");
399
400        ui.horizontal(|ui| {
401            ui.label("Tonic");
402            ComboBox::from_id_salt("note_mapping_tonic")
403                .selected_text(ui_state.note_mapping.scale_seq.tonic.to_string())
404                .show_ui(ui, |ui| {
405                    for p in PitchClass::iter() {
406                        if ui
407                            .selectable_value(
408                                &mut ui_state.note_mapping.scale_seq.tonic,
409                                p,
410                                p.to_string(),
411                            )
412                            .clicked()
413                        {
414                            ui_state.note_mapping.scale_seq.tonic = p;
415                        }
416                    }
417                });
418        });
419
420        ui.horizontal(|ui| {
421            ui.label("Scale");
422            ComboBox::from_id_salt("note_mapping_scale")
423                .selected_text(ui_state.note_mapping.scale_seq.scale.to_string())
424                .show_ui(ui, |ui| {
425                    for s in ScaleKind::iter() {
426                        if ui
427                            .selectable_value(
428                                &mut ui_state.note_mapping.scale_seq.scale,
429                                s,
430                                s.to_string(),
431                            )
432                            .clicked()
433                        {
434                            ui_state.note_mapping.scale_seq.scale = s;
435                        }
436                    }
437                });
438        });
439
440        ui.horizontal(|ui| {
441            ui.label("Octave");
442            ComboBox::from_id_salt("note_mapping_octave")
443                .selected_text(ui_state.note_mapping.scale_seq.octave.to_string())
444                .show_ui(ui, |ui| {
445                    for s in Octave::iter() {
446                        if ui
447                            .selectable_value(
448                                &mut ui_state.note_mapping.scale_seq.octave,
449                                s,
450                                s.to_string(),
451                            )
452                            .clicked()
453                        {
454                            ui_state.note_mapping.scale_seq.octave = s;
455                        }
456                    }
457                });
458        });
459
460        ui.horizontal(|ui| {
461            ui.label("Direction");
462            ComboBox::from_id_salt("note_mapping_direction")
463                .selected_text(ui_state.note_mapping.scale_seq.direction.to_string())
464                .show_ui(ui, |ui| {
465                    for s in SequenceDirection::iter() {
466                        if ui
467                            .selectable_value(
468                                &mut ui_state.note_mapping.scale_seq.direction,
469                                s,
470                                s.to_string(),
471                            )
472                            .clicked()
473                        {
474                            ui_state.note_mapping.scale_seq.direction = s;
475                        }
476                    }
477                });
478        });
479
480        ui.horizontal(|ui| {
481            ui.label("Starting from Pad");
482
483            ui.add(DragValue::new(&mut ui_state.note_mapping.starting_from_pad).range(0..=63))
484        });
485
486        ui.horizontal(|ui| {
487            ui.label("Length");
488
489            ui.add(DragValue::new(&mut ui_state.note_mapping.scale_seq.length).range(1..=64))
490        });
491
492        ui.add_space(8.0);
493
494        ui.checkbox(
495            &mut ui_state.note_mapping.tonic_highlighting_enabled,
496            "Tonic highlighting",
497        );
498        if ui_state.note_mapping.tonic_highlighting_enabled {
499            ui.horizontal(|ui| {
500                ui.label("Tonic color");
501                ComboBox::from_id_salt("tonic_highlight_color")
502                    .selected_text(ui_state.note_mapping.tonic_color.to_string())
503                    .show_ui(ui, |ui| {
504                        for c in PadColor::iter() {
505                            if ui
506                                .selectable_value(
507                                    &mut ui_state.note_mapping.tonic_color,
508                                    c,
509                                    c.to_string(),
510                                )
511                                .clicked()
512                            {
513                                ui_state.note_mapping.tonic_color = c;
514                            }
515                        }
516                    });
517            });
518        }
519
520        ui.add_space(8.0);
521
522        let resp = ui.button("Set pattern");
523        if resp.clicked()
524            && let Some(preset) = ui_state.preset.as_mut()
525        {
526            let scale_seq = ui_state.note_mapping.scale_seq;
527            preset.pads.set_note_pattern(
528                ui_state.note_mapping.starting_from_pad,
529                NotePattern::Scale(scale_seq),
530            );
531
532            if ui_state.note_mapping.tonic_highlighting_enabled {
533                let tonic_color = (scale_seq.tonic, ui_state.note_mapping.tonic_color);
534                preset.pads.highlight_tonics(
535                    ui_state.note_mapping.starting_from_pad,
536                    scale_seq.length,
537                    tonic_color,
538                );
539            }
540        }
541    });
542}
543
544fn render_color_mapping(ui: &mut Ui, ui_state: &mut UiState) {
545    ui.vertical(|ui| {
546        ui.label("Color Mapping");
547
548        ui.horizontal(|ui| {
549            ui.label("Color");
550            ComboBox::from_id_salt("color_mapping_color")
551                .selected_text(ui_state.off_color_mapping.color.to_string())
552                .show_ui(ui, |ui| {
553                    for c in PadColor::iter() {
554                        if ui
555                            .selectable_value(
556                                &mut ui_state.off_color_mapping.color,
557                                c,
558                                c.to_string(),
559                            )
560                            .clicked()
561                        {
562                            ui_state.off_color_mapping.color = c;
563                        }
564                    }
565                });
566        });
567
568        ui.horizontal(|ui| {
569            ui.label("Starting from Pad");
570            ui.add(DragValue::new(&mut ui_state.off_color_mapping.starting_from_pad).range(0..=63))
571        });
572
573        ui.horizontal(|ui| {
574            ui.label("Length");
575            ui.add(DragValue::new(&mut ui_state.off_color_mapping.length).range(1..=64))
576        });
577
578        let resp = ui.button("Set color");
579        if resp.clicked()
580            && let Some(preset) = ui_state.preset.as_mut()
581        {
582            preset.pads.set_off_color_pattern(
583                ui_state.off_color_mapping.starting_from_pad,
584                ui_state.off_color_mapping.length,
585                ColorPattern::Contiguous(ui_state.off_color_mapping.color),
586            );
587        }
588    });
589}
590
591fn render_on_color_mapping(ui: &mut Ui, ui_state: &mut UiState) {
592    ui.vertical(|ui| {
593        ui.label("On Color Mapping");
594
595        ui.horizontal(|ui| {
596            ui.label("Color");
597            ComboBox::from_id_salt("on_color_mapping_color")
598                .selected_text(ui_state.on_color_mapping.color.to_string())
599                .show_ui(ui, |ui| {
600                    for c in PadColor::iter() {
601                        if ui
602                            .selectable_value(
603                                &mut ui_state.on_color_mapping.color,
604                                c,
605                                c.to_string(),
606                            )
607                            .clicked()
608                        {
609                            ui_state.on_color_mapping.color = c;
610                        }
611                    }
612                });
613        });
614
615        ui.horizontal(|ui| {
616            ui.label("Starting from Pad");
617            ui.add(DragValue::new(&mut ui_state.on_color_mapping.starting_from_pad).range(0..=63))
618        });
619
620        ui.horizontal(|ui| {
621            ui.label("Length");
622            ui.add(DragValue::new(&mut ui_state.on_color_mapping.length).range(1..=64))
623        });
624
625        let resp = ui.button("Set color");
626        if resp.clicked()
627            && let Some(preset) = ui_state.preset.as_mut()
628        {
629            preset.pads.set_on_color_pattern(
630                ui_state.on_color_mapping.starting_from_pad,
631                ui_state.on_color_mapping.length,
632                ColorPattern::Contiguous(ui_state.on_color_mapping.color),
633            );
634        }
635    });
636}
637
638fn render_all_pad_banks(
639    ui: &mut Ui,
640    selected_pad: &mut Option<UserSelection>,
641    pad_repo: &mut PadRepository,
642) {
643    let banks: Vec<Vec<Pad>> = pad_repo
644        .pads
645        .chunks(16)
646        .map(|chunk| chunk.to_vec())
647        .collect();
648
649    ui.add_space(16.0);
650    ui.horizontal(|ui| {
651        ui.add_space(16.0);
652        for (bank_id, bank) in banks.into_iter().enumerate() {
653            let bank_label = BANKS[bank_id].to_string();
654            render_pad_bank(ui, selected_pad, bank, bank_label);
655            ui.add_space(32.0);
656        }
657    });
658}
659
660fn render_pad_bank(
661    ui: &mut Ui,
662    selected_pad: &mut Option<UserSelection>,
663    pads: Vec<Pad>,
664    label: String,
665) {
666    let reordered: Vec<Pad> = pads.chunks(4).rev().flatten().cloned().collect();
667
668    ui.vertical(|ui| {
669        ui.label(format!("Bank {}", label));
670        ui.add_space(8.0);
671        Grid::new(format!("pad_bank_{}", label))
672            .num_columns(4)
673            .spacing([8.0, 8.0])
674            .show(ui, |ui| {
675                for (i, pad) in reordered.into_iter().enumerate() {
676                    render_pad(ui, selected_pad, pad);
677                    if (i + 1) % 4 == 0 {
678                        ui.end_row();
679                    }
680                }
681            });
682    });
683}
684
685fn render_pad(ui: &mut Ui, selected_pad: &mut Option<UserSelection>, pad: Pad) {
686    let (rect, resp) = ui.allocate_exact_size(PAD_DIMENSIONS, egui::Sense::click());
687
688    ui.painter().rect_filled(rect, 4.0, Color32::DARK_GRAY);
689
690    if let Some(UserSelection::Pad { id }) = selected_pad
691        && pad.id == *id
692    {
693        ui.painter().rect_stroke(
694            rect,
695            4.0,
696            egui::Stroke::new(1.5, Color32::WHITE),
697            egui::StrokeKind::Outside,
698        );
699    }
700
701    let half_w = rect.width() * 0.5;
702    let half_h = rect.height() * 0.5;
703
704    let tl = egui::Rect::from_min_size(rect.min, vec2(half_w, half_h));
705    let tr = egui::Rect::from_min_size(rect.min + vec2(half_w, 0.0), vec2(half_w, half_h));
706    let bl = egui::Rect::from_min_size(rect.min + vec2(0.0, half_h), vec2(half_w, half_h));
707    let br = egui::Rect::from_min_size(rect.min + vec2(half_w, half_h), vec2(half_w, half_h));
708
709    ui.painter().text(
710        tl.center(),
711        egui::Align2::CENTER_CENTER,
712        pad.id.to_string(),
713        egui::FontId::proportional(12.0),
714        Color32::WHITE,
715    );
716
717    ui.painter().text(
718        tr.center(),
719        egui::Align2::CENTER_CENTER,
720        format!("♩{}", pad.note),
721        egui::FontId::proportional(12.0),
722        Color32::WHITE,
723    );
724
725    let (r, g, b) = *pad.off_color.as_rgb_color();
726    let off_color = Color32::from_rgb(r, g, b);
727    ui.painter().rect_filled(bl.shrink(4.0), 4.0, off_color);
728
729    let (r, g, b) = *pad.on_color.as_rgb_color();
730    let on_color = Color32::from_rgb(r, g, b);
731    ui.painter().rect_filled(br.shrink(4.0), 4.0, on_color);
732
733    if resp.clicked() {
734        click_pad(pad.id, selected_pad);
735    }
736}
737
738fn render_editor(ui: &mut Ui, ui_state: &mut UiState) {
739    if let Some(preset) = &mut ui_state.preset {
740        render_all_pad_banks(ui, &mut ui_state.selected_item, &mut preset.pads);
741    } else {
742        ui.label("no pad repo");
743    }
744}
745
746fn click_pad(id: usize, selected_pad: &mut Option<UserSelection>) {
747    if *selected_pad == Some(UserSelection::Pad { id }) {
748        *selected_pad = None;
749    } else {
750        *selected_pad = Some(UserSelection::Pad { id });
751    }
752}
753
754fn pad_compare_table(ui: &mut Ui, ui_state: &mut UiState) {
755    ui.vertical(|ui| {
756        let idx_label = match ui_state.selected_item {
757            Some(UserSelection::Pad { id }) => id.to_string(),
758            None => "None".to_string(),
759        };
760        ui.label(format!("Selected Pad: {}", idx_label));
761
762        if let Some(UserSelection::Pad { id: index }) = ui_state.selected_item
763            && let Some(preset) = &mut ui_state.preset
764            && let Some(pad) = preset.pads.pads.iter_mut().find(|p| p.id == index)
765        {
766            render_pad_compare_grid(ui, pad);
767        }
768    });
769}
770
771fn render_pad_compare_grid(ui: &mut Ui, pad: &mut Pad) {
772    Grid::new("pad_compare_grid")
773        .striped(true)
774        .spacing([16.0, 6.0])
775        .show(ui, |ui| {
776            ui.label("Field");
777            ui.label("Value");
778            ui.end_row();
779
780            row_edit_pad_kind(ui, "kind", &mut pad.kind);
781            row_edit_u8(ui, "channel", &mut pad.channel);
782            row_edit_note(ui, "note", &mut pad.note);
783            row_edit_midi2din(ui, "midi2din", &mut pad.midi2din);
784            row_edit_trigger_kind(ui, "trigger", &mut pad.trigger);
785            row_edit_aftertouch_kind(ui, "aftertouch", &mut pad.aftertouch);
786            row_edit_u8(ui, "program", &mut pad.program);
787            row_edit_u8(ui, "msb", &mut pad.msb);
788            row_edit_u8(ui, "lsb", &mut pad.lsb);
789            row_edit_pad_color(ui, "off color", &mut pad.off_color);
790            row_edit_pad_color(ui, "on color", &mut pad.on_color);
791        });
792}
793
794fn row_edit_u8(ui: &mut Ui, name: &str, value: &mut u8) {
795    ui.label(name);
796    let mut val = *value as u32;
797    if ui.add(DragValue::new(&mut val).range(0..=127)).changed() {
798        *value = val as u8;
799    }
800    ui.end_row();
801}
802
803fn row_edit_note(ui: &mut Ui, name: &str, value: &mut Note) {
804    ui.label(name);
805    let mut val = *value as u32;
806    if ui.add(DragValue::new(&mut val).range(0..=127)).changed()
807        && let Ok(note) = Note::try_from(val as u8)
808    {
809        *value = note;
810    }
811    ui.end_row();
812}
813
814fn row_edit_pad_kind(ui: &mut Ui, name: &str, value: &mut PadKind) {
815    ui.label(name);
816    ComboBox::from_id_salt(name)
817        .selected_text(format!("{}", value))
818        .show_ui(ui, |ui| {
819            for variant in PadKind::iter() {
820                ui.selectable_value(value, variant, format!("{}", variant));
821            }
822        });
823    ui.end_row();
824}
825
826fn row_edit_trigger_kind(ui: &mut Ui, name: &str, value: &mut TriggerKind) {
827    ui.label(name);
828    ComboBox::from_id_salt(name)
829        .selected_text(format!("{}", value))
830        .show_ui(ui, |ui| {
831            for variant in TriggerKind::iter() {
832                ui.selectable_value(value, variant, format!("{}", variant));
833            }
834        });
835    ui.end_row();
836}
837
838fn row_edit_aftertouch_kind(ui: &mut Ui, name: &str, value: &mut AfterTouchKind) {
839    ui.label(name);
840    ComboBox::from_id_salt(name)
841        .selected_text(format!("{}", value))
842        .show_ui(ui, |ui| {
843            for variant in AfterTouchKind::iter() {
844                ui.selectable_value(value, variant, format!("{}", variant));
845            }
846        });
847    ui.end_row();
848}
849
850fn row_edit_pad_color(ui: &mut Ui, name: &str, value: &mut PadColor) {
851    ui.label(name);
852    ComboBox::from_id_salt(name)
853        .selected_text(format!("{}", value))
854        .show_ui(ui, |ui| {
855            for variant in PadColor::iter() {
856                ui.selectable_value(value, variant, format!("{}", variant));
857            }
858        });
859    ui.end_row();
860}
861
862fn row_edit_midi2din(ui: &mut Ui, name: &str, value: &mut Midi2Din) {
863    ui.label(name);
864    ComboBox::from_id_salt(name)
865        .selected_text(format!("{}", value))
866        .show_ui(ui, |ui| {
867            for variant in Midi2Din::iter() {
868                ui.selectable_value(value, variant, format!("{}", variant));
869            }
870        });
871    ui.end_row();
872}