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
71fn 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
961fn 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}