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};
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Kind {
Program,
SetList,
Sample,
Piano,
Live,
Settings,
Synth,
OrganPreset,
PianoPreset,
Performance,
LeadBank,
SampleLibrary,
PipeLibrary,
Bundle,
Project,
Other,
}
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 {
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,
];
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)
}
pub fn home(self) -> Option<ObjectClass> {
HOMES
.iter()
.find(|(kind, _)| *kind == self)
.map(|(_, class)| *class)
}
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",
}
}
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",
},
}
}
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,
}
}
}
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()
}
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()
}
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)
}
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()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Item {
Local(u64),
Folder(u64),
Slot {
class: ObjectClass,
at: Location,
},
Tag(u64),
}
impl Item {
pub fn local(self) -> Option<u64> {
match self {
Item::Local(id) => Some(id),
_ => None,
}
}
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))
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Held {
pub what: Item,
pub kind: Kind,
pub filed: Option<u64>,
pub fits: bool,
}
#[derive(Clone)]
pub struct Carried {
pub head: Held,
pub name: String,
pub rest: Vec<Held>,
}
impl Carried {
pub fn all(&self) -> impl Iterator<Item = Held> + '_ {
std::iter::once(self.head).chain(self.rest.iter().copied())
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Onto {
Computer,
Group(u64),
Slot {
class: ObjectClass,
at: Location,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Landing {
Copy {
class: ObjectClass,
at: Location,
},
Send {
id: u64,
class: ObjectClass,
at: Location,
},
Rearrange {
class: ObjectClass,
from: Location,
to: Location,
},
File {
id: u64,
folder: u64,
},
Unfile {
id: u64,
},
No(&'static str),
}
impl Landing {
pub fn allowed(self) -> bool {
!matches!(self, Landing::No(_))
}
pub(super) fn repeats(self) -> bool {
matches!(
self,
Landing::Copy { .. } | Landing::File { .. } | Landing::Unfile { .. }
)
}
pub(super) fn same(self, other: Landing) -> bool {
std::mem::discriminant(&self) == std::mem::discriminant(&other)
}
}
pub fn landing(carried: &Held, onto: Onto) -> Landing {
match (carried.what, onto) {
(Item::Folder(_) | Item::Tag(_), _) => Landing::No("that is a list, not a sound"),
(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 },
},
(Item::Slot { .. }, Onto::Group(_)) => {
Landing::No("copy it to this computer first, then drag it into the folder")
}
(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,
}
}
}
}
}
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;
#[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 },
}
);
}
#[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 },
}
);
}
#[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"),
}
assert_eq!(
landing(&refused, Onto::Group(1)),
Landing::File {
id: CARRIED,
folder: 1
}
);
}
#[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"
);
}
#[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());
}
}
#[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());
}
#[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());
}
#[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());
}
#[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"),
}
}
}
#[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"),
}
}
}
#[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());
}
}
#[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);
}
#[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);
}
}
#[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:?}");
}
}
}