Skip to main content

midilab_gui/
akai_mpd226_editor.rs

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