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