drawbar 0.7.1

Your Nord's sounds, in a window: browse, edit and send programs, samples and pianos, in the browser or on the desktop
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
//! What a drag is: the thing being carried, where it may land, and what landing means.
//!
//! Pure rules over the two places a sound can live — nothing here draws a row or touches
//! the instrument, so the whole vocabulary is testable without a frame.

use eframe::egui;
use nord_format::accept::Family;
use nord_format::Entity;
use nord_usb::{Location, ObjectClass};

use crate::device::{read_only, DeviceState};
use crate::icon::Glyph;
use crate::strings::folder;
use crate::workspace::{LocalEntity, Workspace};

/// What an asset is, which is what decides the folder it belongs in.
///
/// One per family of [`Entity`], so a file that decoded is never called a file. The
/// declaration order is [`Kind::ALL`]'s, which is the order any set of kinds is listed
/// in.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Kind {
    Program,
    SetList,
    Sample,
    Piano,
    Live,
    Settings,
    /// A synth patch, on the models that bank them separately from programs.
    Synth,
    OrganPreset,
    PianoPreset,
    /// A Lead performance — the multi-slot layer above that family's programs.
    Performance,
    /// A Lead SysEx bank: the dump itself, or the MIDI file carrying one. The two are
    /// one thing under two containers.
    LeadBank,
    /// An Electro 2 sample library — a whole library rather than one instrument, which
    /// is why it is not a [`Kind::Sample`].
    SampleLibrary,
    /// A C2 pipe-organ library.
    PipeLibrary,
    /// An archive of objects: a bundle, a backup, or a Drum-family bank.
    Bundle,
    /// A Nord Sample Editor project (`.nsmpproj`) — a text file that generates a sample,
    /// and the one kind with no folder on the instrument to send it to.
    Project,
    /// Bytes that did not decode.
    Other,
}

/// The kinds the instrument has a folder for, each under the class that folder is.
///
/// One table, read forwards by [`Kind::home`] and backwards by [`Kind::from_class`], so
/// a class and its kind cannot drift apart.
const HOMES: [(Kind, ObjectClass); 6] = [
    (Kind::Program, ObjectClass::Program),
    (Kind::SetList, ObjectClass::SetList),
    (Kind::Sample, ObjectClass::Sample),
    (Kind::Piano, ObjectClass::Piano),
    (Kind::Live, ObjectClass::Live),
    (Kind::Settings, ObjectClass::Settings),
];

impl Kind {
    /// Every kind, in the order anything showing a set of them shows them.
    pub const ALL: [Kind; 16] = [
        Kind::Program,
        Kind::SetList,
        Kind::Sample,
        Kind::Piano,
        Kind::Live,
        Kind::Settings,
        Kind::Synth,
        Kind::OrganPreset,
        Kind::PianoPreset,
        Kind::Performance,
        Kind::LeadBank,
        Kind::SampleLibrary,
        Kind::PipeLibrary,
        Kind::Bundle,
        Kind::Project,
        Kind::Other,
    ];

    /// What a decoded file is. ⚠️ Exhaustive over [`Entity`], so a family the library
    /// adds is a compile error here rather than another nameless row.
    pub fn of(entity: Option<&Entity>) -> Kind {
        match entity {
            Some(Entity::Program(_)) => Kind::Program,
            Some(Entity::Song(_)) => Kind::SetList,
            Some(Entity::Sample(_)) => Kind::Sample,
            Some(Entity::Piano(_) | Entity::PianoLibrary(_)) => Kind::Piano,
            Some(Entity::Live(_)) => Kind::Live,
            Some(Entity::Settings(_)) => Kind::Settings,
            Some(Entity::Synth(_)) => Kind::Synth,
            Some(Entity::OrganPreset(_)) => Kind::OrganPreset,
            Some(Entity::PianoPreset(_)) => Kind::PianoPreset,
            Some(Entity::Performance(_)) => Kind::Performance,
            Some(Entity::Midi(_) | Entity::Sysex(_)) => Kind::LeadBank,
            Some(Entity::Cne3(_)) => Kind::SampleLibrary,
            Some(Entity::PipeLibrary(_)) => Kind::PipeLibrary,
            Some(Entity::Bundle(_)) => Kind::Bundle,
            Some(Entity::SampleProject(_)) => Kind::Project,
            None => Kind::Other,
        }
    }

    pub fn from_class(class: ObjectClass) -> Kind {
        HOMES
            .iter()
            .find(|(_, held)| *held == class)
            .map_or(Kind::Other, |(kind, _)| *kind)
    }

    /// The folder on the instrument this kind belongs in.
    pub fn home(self) -> Option<ObjectClass> {
        HOMES
            .iter()
            .find(|(kind, _)| *kind == self)
            .map(|(_, class)| *class)
    }

    /// The small word next to a row's name.
    pub fn chip(self) -> &'static str {
        match self {
            Kind::Program => "program",
            Kind::SetList => "set list",
            Kind::Sample => "sample",
            Kind::Piano => "piano",
            Kind::Live => "live",
            Kind::Settings => "settings",
            Kind::Synth => "synth preset",
            Kind::OrganPreset => "organ preset",
            Kind::PianoPreset => "piano preset",
            Kind::Performance => "performance",
            Kind::LeadBank => "lead bank",
            Kind::SampleLibrary => "sample library",
            Kind::PipeLibrary => "pipe library",
            Kind::Bundle => "bundle",
            Kind::Project => "project",
            Kind::Other => "file",
        }
    }

    /// What the tree calls a whole kind of thing.
    pub fn plural(self) -> &'static str {
        match self.home() {
            Some(class) => folder(class),
            None => match self {
                Kind::Synth => "Synth presets",
                Kind::OrganPreset => "Organ presets",
                Kind::PianoPreset => "Piano presets",
                Kind::Performance => "Performances",
                Kind::LeadBank => "Lead banks",
                Kind::SampleLibrary => "Sample libraries",
                Kind::PipeLibrary => "Pipe organ libraries",
                Kind::Bundle => "Bundles",
                Kind::Project => "Sample Editor projects",
                _ => "Other",
            },
        }
    }

    /// The one glyph this kind wears — in the rail, the table, a tab, the queue and the
    /// slot map alike.
    pub fn glyph(self) -> Glyph {
        match self {
            Kind::Program => Glyph::Disc3,
            Kind::SetList => Glyph::ListMusic,
            Kind::Sample => Glyph::AudioWaveform,
            Kind::Piano => Glyph::Piano,
            Kind::Live => Glyph::AudioLines,
            Kind::Settings => Glyph::SlidersHorizontal,
            Kind::Synth => Glyph::Waves,
            Kind::OrganPreset => Glyph::Columns2,
            Kind::PianoPreset => Glyph::CircleDot,
            Kind::Performance => Glyph::Keyboard,
            Kind::LeadBank => Glyph::Save,
            Kind::SampleLibrary => Glyph::LibraryBig,
            Kind::PipeLibrary => Glyph::SlidersVertical,
            Kind::Bundle => Glyph::Folder,
            Kind::Project => Glyph::FolderGit2,
            Kind::Other => Glyph::HardDrive,
        }
    }
}

/// The kinds that exist here: what the list on this computer holds, and what the
/// attached instrument has a folder for, in [`Kind::ALL`] order.
///
/// The union of the two places, because a kind is a way of narrowing what the library
/// shows and the library shows both. A row for a kind neither place holds narrows to
/// nothing.
pub fn kinds_present(workspace: &Workspace, device: &DeviceState) -> Vec<Kind> {
    let here: Vec<Kind> = workspace
        .listed()
        .map(|entity| Kind::of(entity.entity.as_ref()))
        .chain(device.classes().into_iter().map(Kind::from_class))
        .collect();
    Kind::ALL
        .into_iter()
        .filter(|kind| here.contains(kind))
        .collect()
}

/// The family to put in front of an asset's kind word, or nothing where the word alone
/// says what the asset is.
///
/// One answer for the tree and for the library's table, which draw the same word.
pub fn qualifier(
    entity: &LocalEntity,
    kept: &[Family],
    instrument: Option<Family>,
) -> Option<Family> {
    let family = Family::of_tag(&entity.tag());
    qualified(kept, family, instrument)
        .then_some(family)
        .flatten()
}

/// Whether a kind's word needs the family in front of it to say what it is.
///
/// True where the word alone would not settle it: the kept assets are from more than one
/// family, or the asset is not the attached instrument's own. With one family on this
/// computer and that instrument attached, `program` can only mean one thing.
fn qualified(kept: &[Family], asset: Option<Family>, instrument: Option<Family>) -> bool {
    if kept.len() > 1 {
        return true;
    }
    matches!((asset, instrument), (Some(asset), Some(held)) if asset != held)
}

/// The families the assets on this computer are from, in [`Family::ALL`] order.
///
/// Files that name no family — the shared library formats, the carriers, bytes that did
/// not decode — are not one, so a list of samples spans no families at all.
pub fn families_present(workspace: &Workspace) -> Vec<Family> {
    let here: Vec<Family> = workspace
        .listed()
        .filter_map(|entity| Family::of_tag(&entity.tag()))
        .collect();
    Family::ALL
        .into_iter()
        .filter(|family| here.contains(family))
        .collect()
}

/// One row of the tree.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Item {
    Local(u64),
    /// A grouping of local assets. Only a rename and a selection reach it; a folder is
    /// never dragged and never sent anywhere as a thing of its own.
    Folder(u64),
    Slot {
        class: ObjectClass,
        at: Location,
    },
    /// A label on the local list. Like a folder it is renamed rather than dragged, and
    /// unlike a folder an asset wears as many as it is given.
    Tag(u64),
}

impl Item {
    /// The asset on this computer this row stands for, if it is one.
    pub fn local(self) -> Option<u64> {
        match self {
            Item::Local(id) => Some(id),
            _ => None,
        }
    }

    /// Locals, then folders, then slots by class and address, then tags — the order a
    /// selection is walked in, and the order it comes back from the store in.
    fn key(self) -> (u8, u32, u32, u64) {
        match self {
            Item::Local(id) => (0, 0, 0, id),
            Item::Folder(id) => (1, 0, 0, id),
            Item::Slot { class, at } => (2, class.to_raw(), at.bank, u64::from(at.slot)),
            Item::Tag(id) => (3, 0, 0, id),
        }
    }
}

impl Ord for Item {
    fn cmp(&self, other: &Item) -> std::cmp::Ordering {
        self.key().cmp(&other.key())
    }
}

impl PartialOrd for Item {
    fn partial_cmp(&self, other: &Item) -> Option<std::cmp::Ordering> {
        Some(self.cmp(other))
    }
}

/// One row a drag is carrying, and what the rules need to know about it.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Held {
    pub what: Item,
    pub kind: Kind,
    /// The folder it is in, for a local asset. What makes dragging one out of a folder
    /// mean something.
    pub filed: Option<u64>,
    /// Whether the attached instrument takes this asset's format, from
    /// [`crate::device::fit`]. Anything already on the instrument fits it.
    pub fits: bool,
}

/// What is under the pointer while a drag is in progress.
///
/// ⚠️ `head` is the row the pointer went down on and `rest` is the selection it brought
/// with it. The verdict is [`landing`] on the head alone; `rest` follows only where that
/// verdict [`Landing::repeats`].
#[derive(Clone)]
pub struct Carried {
    pub head: Held,
    /// What the ghost says: the pressed row, and how many came with it.
    pub name: String,
    pub rest: Vec<Held>,
}

impl Carried {
    /// The pressed row and everything it brought, in that order.
    pub fn all(&self) -> impl Iterator<Item = Held> + '_ {
        std::iter::once(self.head).chain(self.rest.iter().copied())
    }
}

/// Where a drop would land.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Onto {
    Computer,
    /// One of this computer's own folders.
    Group(u64),
    Slot {
        class: ObjectClass,
        at: Location,
    },
}

/// What a drop would do and everything running it names, or the plain reason it would do
/// nothing.
///
/// The verdict carries the whole of what it decided, so nothing downstream re-derives it
/// from the drag it came from.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Landing {
    /// Device to this computer: a copy comes back.
    Copy {
        class: ObjectClass,
        at: Location,
    },
    /// This computer to a slot.
    Send {
        id: u64,
        class: ObjectClass,
        at: Location,
    },
    /// Slot to slot inside one folder. The instrument swaps them.
    Rearrange {
        class: ObjectClass,
        from: Location,
        to: Location,
    },
    /// Into one of this computer's folders. Nothing leaves this computer.
    File {
        id: u64,
        folder: u64,
    },
    /// Out of the folder it is in, back to the loose part of the list.
    Unfile {
        id: u64,
    },
    No(&'static str),
}

impl Landing {
    pub fn allowed(self) -> bool {
        !matches!(self, Landing::No(_))
    }

    /// Whether everything else the drag carries follows the pressed row.
    ///
    /// ⚠️ A send and a rearrange name **one** destination, and handing several rows to one
    /// slot would write them over each other; those take the pressed row alone.
    pub(super) fn repeats(self) -> bool {
        matches!(
            self,
            Landing::Copy { .. } | Landing::File { .. } | Landing::Unfile { .. }
        )
    }

    /// Whether two verdicts are the same kind of thing, whatever each of them names.
    pub(super) fn same(self, other: Landing) -> bool {
        std::mem::discriminant(&self) == std::mem::discriminant(&other)
    }
}

/// Whether a drag can end where the pointer is, and what it would mean if it did.
pub fn landing(carried: &Held, onto: Onto) -> Landing {
    match (carried.what, onto) {
        // A folder or a tag is a way of seeing the list, not a row that moves.
        (Item::Folder(_) | Item::Tag(_), _) => Landing::No("that is a list, not a sound"),
        // The loose part of the list is a target only for something that is in a folder,
        // which is how one comes back out of one.
        (Item::Local(id), Onto::Computer) => match carried.filed {
            Some(_) => Landing::Unfile { id },
            None => Landing::No("it is already on this computer"),
        },
        (Item::Local(id), Onto::Group(folder)) => match carried.filed == Some(folder) {
            true => Landing::No("it is already in that folder"),
            false => Landing::File { id, folder },
        },
        // The copy would have to land somewhere before it could be filed, and it lands
        // when the instrument answers rather than when the pointer is let go.
        (Item::Slot { .. }, Onto::Group(_)) => {
            Landing::No("copy it to this computer first, then drag it into the folder")
        }
        // A folder this app cannot name is the home of no kind, so the kind check is
        // also what keeps a drop out of one.
        (Item::Local(id), Onto::Slot { class, at }) => {
            if carried.kind.home() != Some(class) {
                Landing::No("that folder holds a different kind of thing")
            } else if !carried.fits {
                Landing::No("the instrument does not take files of that format")
            } else {
                Landing::Send { id, class, at }
            }
        }
        (Item::Slot { class, at }, Onto::Computer) => Landing::Copy { class, at },
        (
            Item::Slot {
                class: from,
                at: was,
            },
            Onto::Slot { class, at },
        ) => {
            if from != class {
                Landing::No("things only move within their own folder")
            } else if read_only(class) {
                Landing::No("nothing here knows what that folder holds")
            } else if was == at {
                Landing::No("it is already there")
            } else {
                Landing::Rearrange {
                    class,
                    from: was,
                    to: at,
                }
            }
        }
    }
}

/// The name of whatever is being dragged, following the pointer.
pub(super) fn ghost(ctx: &egui::Context) {
    let Some(carried) = egui::DragAndDrop::payload::<Carried>(ctx) else {
        return;
    };
    let Some(at) = ctx.pointer_interact_pos() else {
        return;
    };
    let painter = ctx.layer_painter(egui::LayerId::new(
        egui::Order::Tooltip,
        egui::Id::new("drag_ghost"),
    ));
    let where_ = at + egui::vec2(12.0, 6.0);
    let text = painter.layout_no_wrap(
        carried.name.clone(),
        egui::FontId::proportional(12.0),
        ctx.style().visuals.strong_text_color(),
    );
    painter.rect_filled(
        egui::Rect::from_min_size(where_, text.size()).expand(4.0),
        3.0,
        ctx.style().visuals.window_fill,
    );
    painter.galley(where_, text, egui::Color32::PLACEHOLDER);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::browser::bench::{local, onto, slot, CARRIED};
    use crate::strings::folder;

    /// The two crossings the browser exists for.
    #[test]
    fn a_drag_between_the_two_places_copies_one_way_and_sends_the_other() {
        assert_eq!(
            landing(&slot(ObjectClass::Program, 6, 3), Onto::Computer),
            Landing::Copy {
                class: ObjectClass::Program,
                at: Location { bank: 6, slot: 3 },
            }
        );
        assert_eq!(
            landing(&local(Kind::Program), onto(ObjectClass::Program, 6, 3)),
            Landing::Send {
                id: CARRIED,
                class: ObjectClass::Program,
                at: Location { bank: 6, slot: 3 },
            }
        );
    }

    /// An empty slot is a target like any other — that is the whole reason it is a row.
    #[test]
    fn an_empty_slot_is_a_target() {
        assert_eq!(
            landing(&local(Kind::SetList), onto(ObjectClass::SetList, 0, 12)),
            Landing::Send {
                id: CARRIED,
                class: ObjectClass::SetList,
                at: Location { bank: 0, slot: 12 },
            }
        );
    }

    /// A drop of something the attached instrument does not take produces no landing —
    /// the target does not light and the drop says why.
    #[test]
    fn a_drop_of_what_the_instrument_refuses_lands_nowhere() {
        let refused = Held {
            fits: false,
            ..local(Kind::Program)
        };
        match landing(&refused, onto(ObjectClass::Program, 6, 3)) {
            Landing::No(why) => assert!(why.contains("format"), "{why}"),
            other => panic!("{other:?} should have been refused"),
        }
        // It is still a row of this computer's list, so filing it is untouched.
        assert_eq!(
            landing(&refused, Onto::Group(1)),
            Landing::File {
                id: CARRIED,
                folder: 1
            }
        );
    }

    /// A kind's word carries the family only where the word alone would not settle whose
    /// files these are.
    #[test]
    fn the_family_is_named_only_where_it_says_something_the_kind_does_not() {
        let e5 = Some(Family::Electro5);
        let s4 = Some(Family::Stage4);

        assert!(
            !qualified(&[Family::Electro5], e5, e5),
            "one family, its own"
        );
        assert!(
            !qualified(&[Family::Electro5], e5, None),
            "nothing attached"
        );
        assert!(
            !qualified(&[], None, e5),
            "nothing on this computer names one"
        );
        assert!(qualified(&[Family::Electro5, Family::Stage4], e5, None));
        assert!(
            qualified(&[Family::Stage4], s4, e5),
            "not this instrument's"
        );
    }

    /// A folder holds one kind of thing, and the instrument is not asked to sort it out.
    #[test]
    fn a_thing_cannot_be_dropped_into_a_folder_for_another_kind() {
        for kind in [Kind::SetList, Kind::Sample, Kind::Other] {
            assert!(!landing(&local(kind), onto(ObjectClass::Program, 0, 0)).allowed());
        }
    }

    /// Every folder this app can name takes a drop — the two libraries and the buffer
    /// classes alike. A partition it cannot name is the home of no kind, so nothing
    /// carries into one.
    #[test]
    fn a_drop_lands_in_every_folder_this_app_can_name() {
        for class in [
            ObjectClass::Piano,
            ObjectClass::Sample,
            ObjectClass::Live,
            ObjectClass::Settings,
        ] {
            let kind = Kind::from_class(class);
            assert!(
                landing(&local(kind), onto(class, 0, 0)).allowed(),
                "{}",
                folder(class)
            );
        }
        assert!(!landing(
            &local(Kind::from_class(ObjectClass::Piano)),
            onto(ObjectClass::Unknown(9), 0, 0)
        )
        .allowed());
    }

    /// Slot to slot is the instrument's swap, and only inside one folder.
    #[test]
    fn slots_rearrange_only_within_their_own_folder() {
        assert_eq!(
            landing(
                &slot(ObjectClass::Program, 6, 3),
                onto(ObjectClass::Program, 7, 12)
            ),
            Landing::Rearrange {
                class: ObjectClass::Program,
                from: Location { bank: 6, slot: 3 },
                to: Location { bank: 7, slot: 12 },
            }
        );
        assert!(!landing(
            &slot(ObjectClass::Program, 6, 3),
            onto(ObjectClass::SetList, 0, 0)
        )
        .allowed());
    }

    /// Dropping something back where it came from is not a move.
    #[test]
    fn dropping_a_slot_on_itself_does_nothing() {
        assert!(!landing(
            &slot(ObjectClass::Program, 6, 3),
            onto(ObjectClass::Program, 6, 3)
        )
        .allowed());
        assert!(!landing(&local(Kind::Program), Onto::Computer).allowed());
    }

    /// A refusal carries the words the status strip will show, so there is always
    /// something to say.
    #[test]
    fn every_refusal_explains_itself() {
        let cases = [
            landing(&local(Kind::Program), Onto::Computer),
            landing(
                &slot(ObjectClass::Unknown(9), 0, 0),
                onto(ObjectClass::Unknown(9), 1, 0),
            ),
            landing(&local(Kind::Other), onto(ObjectClass::Program, 0, 0)),
            landing(
                &slot(ObjectClass::Program, 0, 0),
                onto(ObjectClass::Sample, 0, 0),
            ),
        ];
        for case in cases {
            match case {
                Landing::No(why) => assert!(!why.is_empty()),
                other => panic!("{other:?} should have been refused"),
            }
        }
    }

    /// A folder is a way of seeing the local list. Something on this computer goes into
    /// one and comes back out of one; nothing off the instrument does either, because
    /// the copy lands when the instrument answers rather than when the pointer is let go.
    #[test]
    fn a_folder_takes_what_is_already_on_this_computer_and_nothing_else() {
        let filed = |folder| Held {
            filed: folder,
            ..local(Kind::Program)
        };
        let into = Landing::File {
            id: CARRIED,
            folder: 1,
        };
        assert_eq!(landing(&filed(None), Onto::Group(1)), into);
        assert_eq!(landing(&filed(Some(2)), Onto::Group(1)), into);
        assert_eq!(
            landing(&filed(Some(1)), Onto::Computer),
            Landing::Unfile { id: CARRIED }
        );

        for refused in [
            landing(&filed(Some(1)), Onto::Group(1)),
            landing(&filed(None), Onto::Computer),
            landing(&slot(ObjectClass::Program, 6, 3), Onto::Group(1)),
        ] {
            match refused {
                Landing::No(why) => assert!(!why.is_empty()),
                other => panic!("{other:?} should have been refused"),
            }
        }
    }

    /// A folder is never the thing being dragged: it is where the list is cut, not a row
    /// that moves.
    #[test]
    fn a_folder_is_not_something_that_is_dragged() {
        let carried = Held {
            what: Item::Folder(1),
            kind: Kind::Program,
            filed: None,
            fits: true,
        };
        for onto in [
            Onto::Computer,
            Onto::Group(2),
            onto(ObjectClass::Program, 6, 3),
        ] {
            assert!(!landing(&carried, onto).allowed());
        }
    }

    /// A folder holds exactly the kind named after it, and a kind the instrument has no
    /// folder for belongs nowhere on it.
    #[test]
    fn every_kind_knows_the_folder_it_belongs_in() {
        let homed: Vec<Kind> = HOMES.iter().map(|(kind, _)| *kind).collect();
        for (kind, class) in HOMES {
            assert_eq!(Kind::from_class(class), kind, "{}", folder(class));
            assert_eq!(kind.home(), Some(class), "{kind:?}");
        }
        for homeless in Kind::ALL.iter().filter(|kind| !homed.contains(kind)) {
            assert_eq!(homeless.home(), None, "{homeless:?}");
        }
        assert_eq!(Kind::from_class(ObjectClass::Unknown(9)), Kind::Other);
    }

    /// One glyph per kind. Two kinds wearing the same one would read as one kind in the
    /// rail, the table, a tab, the queue and the slot map at once.
    #[test]
    fn no_two_kinds_wear_the_same_glyph() {
        let mut seen: Vec<Glyph> = Vec::new();
        for kind in Kind::ALL {
            let glyph = kind.glyph();
            assert!(!seen.contains(&glyph), "{kind:?} repeats {glyph:?}");
            seen.push(glyph);
        }
    }

    /// Every family the library decodes is a kind of its own. Only bytes that did not
    /// decode are a file.
    #[test]
    fn only_what_did_not_decode_is_called_a_file() {
        assert_eq!(Kind::of(None), Kind::Other);
        for kind in Kind::ALL.iter().filter(|kind| **kind != Kind::Other) {
            assert_ne!(kind.chip(), Kind::Other.chip(), "{kind:?}");
            assert_ne!(kind.plural(), Kind::Other.plural(), "{kind:?}");
        }
    }
}