use std::io::Cursor;
use std::ops::Range;
use eframe::egui;
use nord_usb::wire::ProgramInfo;
use nord_usb::{Location, ObjectClass};
use crate::app::{bad, good, ui as ui_text, warn};
use crate::browser::{cell_ink, Act, Carried, Held, Item, Kind};
use crate::device::{fit, Device, DeviceCmd, DeviceState, Fit, Purpose};
use crate::fields::fields_of;
use crate::icon::{painted, Glyph};
use crate::library::Mark;
use crate::log::Log;
use crate::panel::Track;
use crate::strings::{label, place};
use crate::workspace::{LocalEntity, Workspace};
pub struct Queued {
pub id: u64,
pub class: ObjectClass,
pub at: Location,
pub replaces: Occupancy,
pub diff: Diff,
read: Read,
stamp: u64,
pub failure: Option<String>,
}
enum Read {
Unasked,
Asked,
Answered(Vec<u8>),
}
pub enum Diff {
Pending,
Identical,
Fields(Vec<FieldDiff>),
Bytes { first_at: usize },
Empty,
}
pub struct FieldDiff {
pub path: String,
pub here: String,
pub there: String,
}
pub enum Occupancy {
Unknown,
Vacant,
Held(Occupant),
}
impl Occupancy {
pub fn of(state: &DeviceState, class: ObjectClass, at: Location) -> Occupancy {
match state.slot(class, at) {
Some(Some(info)) => Occupancy::Held(Occupant::of(info)),
Some(None) => Occupancy::Vacant,
None => Occupancy::Unknown,
}
}
pub fn occupant(&self) -> Option<&Occupant> {
match self {
Occupancy::Held(held) => Some(held),
Occupancy::Unknown | Occupancy::Vacant => None,
}
}
pub fn said(&self, class: ObjectClass, at: Location) -> String {
let where_ = place(class, at);
match self {
Occupancy::Held(held) => format!(
"{where_} holds “{}”, {} bytes, which this replaces",
held.name, held.body_len
),
Occupancy::Vacant => format!("{where_} is empty"),
Occupancy::Unknown => format!("{where_} has not been read yet"),
}
}
}
pub struct Occupant {
pub name: String,
pub crc: Option<u32>,
pub body_len: u32,
}
impl Occupant {
fn of(info: &ProgramInfo) -> Occupant {
Occupant {
name: info.name.trim().to_string(),
crc: info.crc32,
body_len: info.body_len,
}
}
fn read(name: &str, bytes: &[u8]) -> Occupant {
let body = nord_usb::envelope::unwrap(bytes)
.map(|read| read.body.0.len())
.unwrap_or(bytes.len());
Occupant {
name: name.trim().to_string(),
crc: None,
body_len: u32::try_from(body).unwrap_or(u32::MAX),
}
}
}
#[derive(Default)]
pub struct Queue {
list: Vec<Queued>,
picked: Option<u64>,
}
enum Put {
Made,
Standing,
Moved(ObjectClass, Location),
Instead(u64),
}
fn read_occupant(device: &mut Device, log: &mut Log, class: ObjectClass, at: Location) {
device.send(
DeviceCmd::Get {
class,
at,
why: Purpose::Compare,
},
log,
);
}
pub fn enqueue(
workspace: &Workspace,
device: &mut Device,
queue: &mut Queue,
log: &mut Log,
id: u64,
class: ObjectClass,
at: Location,
) {
let Some(entity) = workspace.get(id) else {
return;
};
let name = entity.name.clone();
let where_ = place(class, at);
if let Fit::Refuses(why) = fit(&device.state, entity) {
return log.trouble(format!("“{name}” cannot go to {where_}. {why}"));
}
let holds = Occupancy::of(&device.state, class, at);
let displaced = match queue.put(entity, class, at, holds) {
Put::Standing => return,
Put::Made => None,
Put::Moved(was, before) => Some(format!(
"“{name}” is waiting for {where_} rather than {}.",
place(was, before)
)),
Put::Instead(other) => workspace.get(other).map(|other| {
format!(
"“{name}” is waiting for {where_}; “{}” is not any more.",
other.name
)
}),
};
if let Some((class, at)) = queue.unread(id) {
read_occupant(device, log, class, at);
}
log.say(displaced.unwrap_or(format!("“{name}” is waiting to be sent to {where_}.")));
}
pub fn changed(
workspace: &Workspace,
device: &DeviceState,
queue: &Queue,
) -> Vec<(u64, ObjectClass, Location)> {
workspace
.listed()
.filter(|entity| !queue.holds(entity.id))
.filter_map(|entity| {
let (class, at) = entity.link?;
let info = device.slot(class, at).flatten()?;
(crate::library::agrees(entity, class, info, queue) == Some(false))
.then_some((entity.id, class, at))
})
.collect()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct Behind {
pub queued: usize,
pub changed: usize,
pub unsaved: usize,
}
impl Behind {
pub fn of(workspace: &Workspace, device: &DeviceState, queue: &Queue) -> Behind {
Behind {
queued: queue.len(),
changed: changed(workspace, device, queue).len(),
unsaved: workspace
.entities()
.iter()
.filter(|entity| entity.is_unsaved())
.count(),
}
}
pub fn parts(self) -> [(String, Mark); 3] {
[
(format!("{} queued", self.queued), Mark::Differs),
(format!("{} changed", self.changed), Mark::Differs),
(format!("{} unsaved", self.unsaved), Mark::Unsaved),
]
}
pub fn said(self) -> String {
self.parts().map(|(said, _)| said).join(" · ")
}
pub fn action(self) -> String {
format!("Queue {} changed", self.changed)
}
}
pub fn refit(workspace: &Workspace, state: &DeviceState, queue: &mut Queue, log: &mut Log) {
for held in &mut queue.list {
let Some(entity) = workspace.get(held.id) else {
continue;
};
let refusal = match fit(state, entity) {
Fit::Refuses(why) => Some(why),
Fit::Unattached | Fit::Takes | Fit::Warn(_) => None,
};
if let Some(why) = &refusal {
if held.failure.as_ref() != Some(why) {
log.say(format!(
"“{}” cannot go to {}. {why}",
entity.name,
place(held.class, held.at)
));
}
}
held.failure = refusal;
}
}
pub fn reattach(workspace: &Workspace, device: &mut Device, queue: &mut Queue, log: &mut Log) {
for held in &mut queue.list {
let Some(entity) = workspace.get(held.id) else {
continue;
};
held.replaces = Occupancy::of(&device.state, held.class, held.at);
held.read = Read::Unasked;
held.diff = verdict(entity, &held.replaces);
}
for id in queue.ids() {
if let Some((class, at)) = queue.unread(id) {
read_occupant(device, log, class, at);
}
}
}
pub fn queue_changed(workspace: &Workspace, device: &mut Device, queue: &mut Queue, log: &mut Log) {
for (id, class, at) in changed(workspace, &device.state, queue) {
enqueue(workspace, device, queue, log, id, class, at);
}
}
fn verdict(entity: &LocalEntity, replaces: &Occupancy) -> Diff {
let here = entity.container.as_ref().map(|held| held.body_crc32);
match replaces {
Occupancy::Vacant => Diff::Empty,
Occupancy::Held(held) if held.crc.is_some() && held.crc == here => Diff::Identical,
Occupancy::Held(_) | Occupancy::Unknown => Diff::Pending,
}
}
pub fn follow(workspace: &Workspace, device: &mut Device, queue: &mut Queue, log: &mut Log) {
let mut moved = Vec::new();
for held in &mut queue.list {
let Some(entity) = workspace.get(held.id) else {
continue;
};
if entity.stamp == held.stamp {
continue;
}
held.stamp = entity.stamp;
held.diff = match &held.read {
Read::Answered(there) => compare(&entity.bytes, there),
Read::Unasked | Read::Asked => verdict(entity, &held.replaces),
};
moved.push(held.id);
}
for id in moved {
if let Some((class, at)) = queue.unread(id) {
read_occupant(device, log, class, at);
}
}
}
pub fn retarget(
workspace: &Workspace,
device: &mut Device,
queue: &mut Queue,
log: &mut Log,
id: u64,
class: ObjectClass,
at: Location,
) {
if queue.holds(id) {
enqueue(workspace, device, queue, log, id, class, at);
}
}
impl Queue {
fn put(
&mut self,
entity: &LocalEntity,
class: ObjectClass,
at: Location,
replaces: Occupancy,
) -> Put {
if let Some(held) = self
.list
.iter_mut()
.find(|held| (held.id, held.class, held.at) == (entity.id, class, at))
{
held.failure = None;
self.picked = Some(entity.id);
return Put::Standing;
}
let moved = self
.list
.iter()
.find(|held| held.id == entity.id)
.map(|held| (held.class, held.at))
.filter(|held| *held != (class, at));
let instead_of = self
.list
.iter()
.find(|held| (held.class, held.at) == (class, at) && held.id != entity.id)
.map(|held| held.id);
self.list
.retain(|held| held.id != entity.id && (held.class, held.at) != (class, at));
self.list.push(Queued {
id: entity.id,
class,
at,
diff: verdict(entity, &replaces),
replaces,
read: Read::Unasked,
stamp: entity.stamp,
failure: None,
});
self.picked = Some(entity.id);
match (moved, instead_of) {
(Some((was, before)), _) => Put::Moved(was, before),
(None, Some(other)) => Put::Instead(other),
(None, None) => Put::Made,
}
}
fn unread(&mut self, id: u64) -> Option<(ObjectClass, Location)> {
let held = self.list.iter_mut().find(|held| held.id == id)?;
if !matches!(held.diff, Diff::Pending) {
return None;
}
match held.read {
Read::Unasked => {
held.read = Read::Asked;
Some((held.class, held.at))
}
Read::Asked | Read::Answered(_) => None,
}
}
pub fn arrived(
&mut self,
class: ObjectClass,
at: Location,
name: &str,
there: &[u8],
workspace: &Workspace,
) {
let Some(held) = self.waiting_for(class, at) else {
return;
};
if let Occupancy::Unknown = held.replaces {
held.replaces = Occupancy::Held(Occupant::read(name, there));
}
let Some(entity) = workspace.get(held.id) else {
return;
};
held.read = Read::Answered(there.to_vec());
held.stamp = entity.stamp;
held.diff = compare(&entity.bytes, there);
}
pub fn vacant(&mut self, class: ObjectClass, at: Location) {
let Some(held) = self.waiting_for(class, at) else {
return;
};
held.replaces = Occupancy::Vacant;
held.read = Read::Unasked;
held.diff = Diff::Empty;
}
fn waiting_for(&mut self, class: ObjectClass, at: Location) -> Option<&mut Queued> {
self.list
.iter_mut()
.find(|held| (held.class, held.at) == (class, at))
}
pub fn waiting_in(&self, class: ObjectClass) -> Vec<Location> {
self.list
.iter()
.filter(|held| held.class == class)
.map(|held| held.at)
.collect()
}
pub fn waiting(&self, class: ObjectClass, at: Location) -> Option<&Queued> {
self.list
.iter()
.find(|held| (held.class, held.at) == (class, at))
}
pub fn forget(&mut self, id: u64) {
self.list.retain(|held| held.id != id);
if self.picked == Some(id) {
self.picked = self.list.first().map(|held| held.id);
}
}
pub fn clear(&mut self) {
self.list.clear();
self.picked = None;
}
pub fn stumbled(&mut self, class: ObjectClass, why: &str) {
if let Some(held) = self
.list
.iter_mut()
.find(|held| held.class == class && held.failure.is_none())
{
held.failure = Some(why.to_string());
}
}
pub fn holds(&self, id: u64) -> bool {
self.list.iter().any(|held| held.id == id)
}
pub fn ids(&self) -> Vec<u64> {
self.list.iter().map(|held| held.id).collect()
}
pub fn entries(&self) -> &[Queued] {
&self.list
}
pub fn entry(&self, id: u64) -> Option<&Queued> {
self.list.iter().find(|held| held.id == id)
}
pub fn len(&self) -> usize {
self.list.len()
}
pub fn is_empty(&self) -> bool {
self.list.is_empty()
}
}
fn compare(here: &[u8], there: &[u8]) -> Diff {
let body = |bytes: &[u8]| {
nord_usb::envelope::unwrap(bytes)
.ok()
.map(|read| read.body.0)
};
let (mine, held) = match (body(here), body(there)) {
(Some(mine), Some(held)) => (mine, held),
_ => (here.to_vec(), there.to_vec()),
};
let Some(first_at) = parted(&mine, &held) else {
return Diff::Identical;
};
match apart(here, there) {
Some(fields) => Diff::Fields(fields),
None => Diff::Bytes { first_at },
}
}
fn parted(here: &[u8], there: &[u8]) -> Option<usize> {
if let Some(first_at) = here.iter().zip(there).position(|(mine, held)| mine != held) {
return Some(first_at);
}
(here.len() != there.len()).then(|| here.len().min(there.len()))
}
fn apart(here: &[u8], there: &[u8]) -> Option<Vec<FieldDiff>> {
let decode = |bytes: &[u8]| nord_format::from_stream(&mut Cursor::new(bytes)).ok();
let mine = fields_of(&decode(here)?)?;
let held = fields_of(&decode(there)?)?;
let differing: Vec<FieldDiff> = mine
.into_iter()
.filter_map(|field| {
let there = held.iter().find(|other| other.path == field.path)?;
(there.display != field.display).then(|| FieldDiff {
path: field.path,
here: field.display,
there: there.display.clone(),
})
})
.collect();
(!differing.is_empty()).then_some(differing)
}
const ROW: f32 = 22.0;
const PAD: f32 = 8.0;
const GAP: f32 = 6.0;
const GLYPH: f32 = 13.0;
const SMALL: f32 = 11.0;
const CHIP: f32 = 17.0;
const CHIP_PAD: f32 = 5.0;
const NAME: f32 = 12.0;
const MONO: f32 = 10.5;
const ITEMS: f32 = 250.0;
const HEAD: f32 = 20.0;
const DIFF_ROW: f32 = 22.0;
const DIFF_MONO: f32 = 11.0;
pub fn page(
ui: &mut egui::Ui,
queue: &mut Queue,
workspace: &Workspace,
device: &DeviceState,
acts: &mut Vec<Act>,
) {
if queue.is_empty() {
ui.add_space(GAP);
ui.horizontal(|ui| {
ui.add_space(PAD);
ui.label(
egui::RichText::new("Nothing is waiting to be sent.")
.text_style(ui_text())
.weak()
.italics(),
);
});
return;
}
ui.spacing_mut().item_spacing.y = 0.0;
let picked = queue.picked;
let mut clicked = None;
egui::SidePanel::left("queue_items")
.resizable(false)
.exact_width(ITEMS)
.frame(egui::Frame::new())
.show_inside(ui, |ui| {
egui::ScrollArea::vertical()
.id_salt("queue_items")
.auto_shrink([false; 2])
.show(ui, |ui| {
for held in queue.entries() {
let Some(entity) = workspace.get(held.id) else {
continue;
};
let drawn = item(
ui,
held,
entity,
picked == Some(held.id),
device,
queue,
acts,
);
if drawn.clicked() {
clicked = Some(held.id);
}
}
});
});
if let Some(held) = picked.and_then(|id| queue.entry(id)) {
diff(ui, held);
}
if let Some(id) = clicked {
queue.picked = Some(id);
}
}
fn diff(ui: &mut egui::Ui, held: &Queued) {
let border = ui.visuals().widgets.noninteractive.bg_stroke.color;
let edge = ui.max_rect();
ui.painter().vline(
edge.left(),
edge.top()..=edge.bottom(),
egui::Stroke::new(1.0_f32, border),
);
table(ui, held);
}
pub fn table(ui: &mut egui::Ui, held: &Queued) {
let ui = &mut inset(ui);
let width = ui.available_width() - PAD;
let tracks = crate::panel::tracks(width, &DIFF_TRACKS, GAP);
diff_head(ui, width, &tracks);
let Diff::Fields(fields) = &held.diff else {
let (glyph, tint, said) = summarise(held, ui.visuals());
return one_row(ui, width, &tracks, glyph, tint, &said);
};
egui::ScrollArea::vertical()
.id_salt("queue_diff")
.auto_shrink([false; 2])
.show(ui, |ui| {
for field in fields {
field_row(ui, width, &tracks, field);
}
});
}
fn summarise(held: &Queued, visuals: &egui::Visuals) -> (Glyph, egui::Color32, String) {
let quiet = visuals.weak_text_color();
match &held.diff {
Diff::Pending => (
Glyph::Gauge,
quiet,
format!("reading what is in {}…", place(held.class, held.at)),
),
Diff::Empty => (Glyph::CircleCheck, good(visuals), "the slot is free".into()),
Diff::Identical => (
Glyph::Equal,
quiet,
"the instrument already holds these bytes".into(),
),
Diff::Bytes { first_at } => (
Glyph::ArrowRight,
warn(visuals),
format!("bytes differ from {first_at:#06x}"),
),
Diff::Fields(_) => (Glyph::ArrowRight, warn(visuals), String::new()),
}
}
#[allow(clippy::too_many_arguments)]
fn item(
ui: &mut egui::Ui,
held: &Queued,
entity: &LocalEntity,
selected: bool,
device: &DeviceState,
queue: &Queue,
acts: &mut Vec<Act>,
) -> egui::Response {
let (rect, response) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), ROW),
egui::Sense::click_and_drag(),
);
let visuals = ui.visuals().clone();
let painter = ui.painter().clone();
let fill = match (selected, response.hovered()) {
(true, _) => Some(visuals.selection.bg_fill),
(false, true) => Some(visuals.faint_bg_color),
(false, false) => None,
};
if let Some(fill) = fill {
painter.rect_filled(rect, 3.0, fill);
}
let ink = match selected {
true => visuals.selection.stroke.color,
false => visuals.text_color(),
};
let quiet = cell_ink(selected, visuals.weak_text_color(), &visuals);
let box_ = |right: f32| {
egui::Rect::from_center_size(
egui::pos2(right - SMALL / 2.0, rect.center().y),
egui::Vec2::splat(SMALL),
)
};
let unqueue = ui.interact(
box_(rect.right() - PAD),
ui.id().with(("unqueue", held.id)),
egui::Sense::click(),
);
let leaving = match unqueue.hovered() {
true => visuals.text_color(),
false => visuals.weak_text_color(),
};
painted(
ui,
Glyph::X,
box_(rect.right() - PAD),
cell_ink(selected, leaving, &visuals),
);
if unqueue.on_hover_text("remove from the queue").clicked() {
acts.push(Act::Unqueue(held.id));
}
let (glyph, tint, why) = state(held, &visuals);
painted(
ui,
glyph,
box_(rect.right() - PAD - SMALL - GAP),
cell_ink(selected, tint, &visuals),
);
let right = destination(
ui,
held,
rect,
rect.right() - PAD - 2.0 * (SMALL + GAP),
quiet,
device,
queue,
acts,
);
let left = rect.left() + PAD;
painted(
ui,
Kind::of(entity.entity.as_ref()).glyph(),
egui::Rect::from_center_size(
egui::pos2(left + GLYPH / 2.0, rect.center().y),
egui::Vec2::splat(GLYPH),
),
ink,
);
let name_at = left + GLYPH + GAP;
let mut job = egui::text::LayoutJob::simple_singleline(
crate::strings::display_name(&entity.name).to_string(),
egui::FontId::proportional(NAME),
ink,
);
job.wrap = egui::text::TextWrapping::truncate_at_width((right - name_at).max(0.0));
let galley = painter.layout_job(job);
painter.galley(
egui::pos2(name_at, rect.center().y - galley.size().y / 2.0),
galley,
egui::Color32::PLACEHOLDER,
);
if response.double_clicked() {
acts.push(Act::Open(Item::Local(entity.id)));
}
if response.dragged() {
egui::DragAndDrop::set_payload(
ui.ctx(),
Carried {
head: Held {
what: Item::Local(entity.id),
kind: Kind::of(entity.entity.as_ref()),
filed: None,
fits: true,
},
name: entity.name.clone(),
rest: Vec::new(),
},
);
}
response.on_hover_text(format!("{}\n{why}", entity.name))
}
#[allow(clippy::too_many_arguments)]
fn destination(
ui: &mut egui::Ui,
held: &Queued,
row: egui::Rect,
right: f32,
ink: egui::Color32,
device: &DeviceState,
queue: &Queue,
acts: &mut Vec<Act>,
) -> f32 {
let galley = ui.painter().layout_no_wrap(
place(held.class, held.at),
egui::FontId::monospace(MONO),
ink,
);
let box_ = egui::Rect::from_min_size(
egui::pos2(
right - galley.size().x - 2.0 * CHIP_PAD,
row.center().y - CHIP / 2.0,
),
egui::vec2(galley.size().x + 2.0 * CHIP_PAD, CHIP),
);
let chip = ui.interact(
box_,
ui.id().with(("destination", held.id)),
egui::Sense::click(),
);
if chip.hovered() {
ui.painter()
.rect_filled(box_, 2.0, ui.visuals().widgets.hovered.weak_bg_fill);
}
ui.painter().galley(
egui::pos2(
box_.left() + CHIP_PAD,
box_.center().y - galley.size().y / 2.0,
),
galley,
egui::Color32::PLACEHOLDER,
);
let chip = chip.on_hover_text("change where this goes");
egui::Popup::menu(&chip)
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
.width(crate::keyboard::grid_width(PICKER_COLUMNS) + ui.spacing().menu_margin.sum().x)
.show(|ui| {
if let Some(at) = picker(ui, held, device, queue) {
acts.push(Act::Retarget {
id: held.id,
class: held.class,
at,
});
ui.close();
}
});
box_.left() - GAP
}
const PICKER_COLUMNS: usize = 6;
fn salt(held: &Queued) -> egui::Id {
egui::Id::new(("picker", held.class.to_raw(), held.id))
}
fn picker(
ui: &mut egui::Ui,
held: &Queued,
device: &DeviceState,
queue: &Queue,
) -> Option<Location> {
let banks = device.banks_of(held.class);
if banks.is_empty() {
ui.label(
egui::RichText::new("Nothing in this folder has been read.")
.text_style(ui_text())
.weak()
.italics(),
);
return None;
}
let salt = salt(held);
let kept = salt.with("bank");
let mut bank = ui
.data(|data| data.get_temp::<u32>(kept))
.filter(|bank| banks.contains(bank))
.unwrap_or(held.at.bank + 1);
ui.horizontal(|ui| {
for offered in banks.iter().copied() {
if bank_chip(ui, salt.with(("bank", offered)), offered, offered == bank).clicked() {
bank = offered;
ui.data_mut(|data| data.insert_temp(kept, bank));
}
}
});
let slots = device.bank(held.class, bank).unwrap_or_default();
let mut picked = None;
crate::keyboard::grid(ui, PICKER_COLUMNS, slots.len(), |ui, index, rect| {
let at = Location::from_user(bank, index as u32 + 1);
let state = crate::keyboard::State::of(
false,
queue.waiting(held.class, at).is_some(),
slots[index].is_some(),
);
let response = ui.interact(
rect,
salt.with(("slot", at.bank, at.slot)),
egui::Sense::click(),
);
crate::keyboard::paint_cell(
ui,
rect,
at,
slots[index].as_ref(),
state,
at == held.at,
response.hovered(),
);
let occupant = slots[index].as_ref().map(|info| info.name.trim());
let response = response.on_hover_text(match occupant {
Some(name) if !name.is_empty() => format!("{} — {name}", place(held.class, at)),
_ => format!("{} — empty", place(held.class, at)),
});
if response.clicked() {
picked = Some(at);
}
});
picked
}
fn bank_chip(ui: &mut egui::Ui, id: egui::Id, bank: u32, on: bool) -> egui::Response {
let visuals = ui.visuals().clone();
let ink = match on {
true => visuals.selection.stroke.color,
false => visuals.text_color(),
};
let galley = ui
.painter()
.layout_no_wrap(bank.to_string(), egui::FontId::monospace(MONO), ink);
let (box_, _) = ui.allocate_exact_size(
egui::vec2(galley.size().x + 2.0 * CHIP_PAD, CHIP),
egui::Sense::hover(),
);
let response = ui.interact(box_, id, egui::Sense::click());
let fill = match (on, response.hovered()) {
(true, _) => Some(visuals.selection.bg_fill),
(false, true) => Some(visuals.widgets.hovered.weak_bg_fill),
(false, false) => None,
};
if let Some(fill) = fill {
ui.painter().rect_filled(box_, 2.0, fill);
}
ui.painter().galley(
egui::pos2(
box_.left() + CHIP_PAD,
box_.center().y - galley.size().y / 2.0,
),
galley,
egui::Color32::PLACEHOLDER,
);
response
}
fn inset(ui: &mut egui::Ui) -> egui::Ui {
let room = ui.available_rect_before_wrap();
ui.new_child(
egui::UiBuilder::new()
.max_rect(room.with_min_x(room.left() + PAD))
.layout(*ui.layout()),
)
}
const DIFF_TRACKS: [Track; 4] = [
Track::Share(1.4),
Track::Share(1.0),
Track::Px(20.0),
Track::Share(1.0),
];
fn diff_head(ui: &mut egui::Ui, width: f32, tracks: &[Range<f32>]) {
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, HEAD), egui::Sense::hover());
let visuals = ui.visuals().clone();
ui.painter().rect_filled(rect, 0.0, visuals.faint_bg_color);
let ink = crate::app::caption(&visuals);
for (head, track) in ["field", "on this computer", "", "on the keyboard"]
.iter()
.zip(tracks)
{
cut(
ui,
box_of(rect, track),
&head.to_uppercase(),
egui::FontId::proportional(9.5),
ink,
);
}
}
fn field_row(ui: &mut egui::Ui, width: f32, tracks: &[Range<f32>], field: &FieldDiff) {
let (rect, response) =
ui.allocate_exact_size(egui::vec2(width, DIFF_ROW), egui::Sense::hover());
let visuals = ui.visuals().clone();
let mono = egui::FontId::monospace(DIFF_MONO);
let strong = visuals.widgets.active.fg_stroke.color;
let quiet = visuals.weak_text_color();
let name = label(&field.path);
cut(
ui,
box_of(rect, &tracks[0]),
&name,
egui::FontId::proportional(DIFF_MONO),
visuals.text_color(),
);
cut(
ui,
box_of(rect, &tracks[1]),
&field.here,
mono.clone(),
strong,
);
sign(
ui,
box_of(rect, &tracks[2]),
Glyph::ArrowRight,
warn(&visuals),
);
cut(ui, box_of(rect, &tracks[3]), &field.there, mono, quiet);
let _ = response.on_hover_text(format!("{name}: {} → {}", field.there, field.here));
}
fn one_row(
ui: &mut egui::Ui,
width: f32,
tracks: &[Range<f32>],
glyph: Glyph,
tint: egui::Color32,
said: &str,
) {
let (rect, _) = ui.allocate_exact_size(egui::vec2(width, DIFF_ROW), egui::Sense::hover());
sign(ui, box_of(rect, &tracks[2]), glyph, tint);
cut(
ui,
box_of(rect, &tracks[0]),
said,
egui::FontId::proportional(DIFF_MONO),
tint,
);
}
fn box_of(rect: egui::Rect, track: &Range<f32>) -> egui::Rect {
egui::Rect::from_min_max(
egui::pos2(rect.left() + track.start, rect.top()),
egui::pos2(rect.left() + track.end, rect.bottom()),
)
}
fn cut(ui: &egui::Ui, box_: egui::Rect, text: &str, font: egui::FontId, tint: egui::Color32) {
let mut job = egui::text::LayoutJob::simple_singleline(text.to_string(), font, tint);
job.wrap = egui::text::TextWrapping::truncate_at_width(box_.width());
let galley = ui.painter().layout_job(job);
ui.painter().galley(
egui::pos2(box_.left(), box_.center().y - galley.size().y / 2.0),
galley,
egui::Color32::PLACEHOLDER,
);
}
fn sign(ui: &egui::Ui, box_: egui::Rect, glyph: Glyph, tint: egui::Color32) {
painted(
ui,
glyph,
egui::Rect::from_center_size(
egui::pos2(box_.left() + SMALL / 2.0, box_.center().y),
egui::Vec2::splat(SMALL),
),
tint,
);
}
fn state(held: &Queued, visuals: &egui::Visuals) -> (Glyph, egui::Color32, String) {
if let Some(why) = &held.failure {
return (Glyph::CircleAlert, bad(visuals), why.clone());
}
let where_ = place(held.class, held.at);
let said = || held.replaces.said(held.class, held.at);
match (&held.diff, &held.replaces) {
(Diff::Pending, _) => (
Glyph::Gauge,
visuals.weak_text_color(),
format!("reading what is in {where_}"),
),
(Diff::Identical, Occupancy::Held(occupant)) => (
Glyph::CircleCheck,
good(visuals),
format!(
"{where_} already holds these bytes, under the name “{}”",
occupant.name
),
),
(_, Occupancy::Held(_)) => (Glyph::Replace, warn(visuals), said()),
(_, Occupancy::Vacant) => (Glyph::CircleCheck, good(visuals), said()),
(_, Occupancy::Unknown) => (Glyph::CircleDot, visuals.weak_text_color(), said()),
}
}
pub fn aside(said: &str, tint: egui::Color32) -> egui::RichText {
egui::RichText::new(said).monospace().size(9.5).color(tint)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::DeviceEvent;
use crate::log::Log;
use crate::tabs::Tabs;
use crate::workspace::{Fresh, Origin};
fn bench() -> (Workspace, Log, Vec<u8>) {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx);
let mut log = Log::default();
let id = workspace.create(Fresh::Program, &mut log).unwrap();
let bytes = workspace.get(id).unwrap().bytes.clone();
workspace.remove(id, &mut log);
(workspace, log, bytes)
}
fn at(slot: u32) -> Location {
Location { bank: 6, slot }
}
fn replacing(queue: &Queue) -> usize {
queue
.entries()
.iter()
.filter(|held| held.replaces.occupant().is_some())
.count()
}
fn occupant(name: &str, crc: Option<u32>) -> ProgramInfo {
ProgramInfo {
location: at(0),
body_len: 121,
format: "ne5p".into(),
version: 4,
crc32: crc,
name: name.to_string(),
}
}
fn attached(workspace: &Workspace) -> (Device, Tabs) {
let mut device = Device::new(workspace.ctx().clone());
device.pretend_attached();
(device, Tabs::default())
}
fn asked(device: &Device) -> (ObjectClass, Location, Purpose) {
match device.queued().front().expect("a read was queued") {
DeviceCmd::Get { class, at, why, .. } => (*class, *at, *why),
other => panic!("{}", other.label()),
}
}
fn edit(workspace: &mut Workspace, id: u64, log: &mut Log) {
let bytes = workspace.get(id).expect("it is in memory").bytes.clone();
let (_, edited) =
crate::fields::apply(&bytes, &[("center_panel.gain".into(), "96".into())]).unwrap();
workspace.replace_bytes(id, edited, log);
}
#[test]
fn a_body_byte_no_field_claims_still_parts_the_two() {
let (_, _, bytes) = bench();
let read = nord_usb::envelope::unwrap(&bytes).expect("a container");
let (tag, at, version) = (
nord_usb::envelope::tag(&read.header),
nord_usb::envelope::location(&read.header),
read.header.version,
);
let body = read.body.0;
let flipped = |offset: usize| {
let mut other = body.clone();
other[offset] ^= 1;
nord_usb::envelope::wrap(&tag, at, version, &other).expect("it wraps")
};
for offset in 0..body.len() {
assert!(
!matches!(compare(&bytes, &flipped(offset)), Diff::Identical),
"body byte {offset:#06x} differs"
);
}
let unclaimed = (0..body.len()).find(|offset| {
let other = flipped(*offset);
apart(&bytes, &other).is_none()
&& nord_format::from_stream(&mut Cursor::new(&other)).is_ok()
});
let offset = unclaimed.expect("a program body carries bits no field declares");
assert!(matches!(
compare(&bytes, &flipped(offset)),
Diff::Bytes { first_at } if first_at == offset
));
}
fn crc(workspace: &Workspace, id: u64) -> u32 {
workspace
.get(id)
.and_then(|entity| entity.saved.crc32)
.expect("every CBIN container has one")
}
#[test]
fn the_counts_separate_what_the_instrument_lacks_from_what_nothing_saved() {
let (mut workspace, mut log, bytes) = bench();
let class = ObjectClass::Program;
let (mut device, _) = attached(&workspace);
let mut queue = Queue::default();
let off = |slot: u32, workspace: &mut Workspace, log: &mut Log| {
workspace.ingest(
format!("off-{slot}.ne5p"),
Origin::Device {
class,
at: at(slot),
},
bytes.clone(),
log,
)
};
let saved = off(0, &mut workspace, &mut log);
let queued = off(1, &mut workspace, &mut log);
let unsaved = off(2, &mut workspace, &mut log);
let held = crc(&workspace, saved);
device.pretend_bodies(
class,
7,
&[
Some(("off-0", held)),
Some(("off-1", held)),
Some(("off-2", held)),
],
);
let counts =
|workspace: &Workspace, queue: &Queue| Behind::of(workspace, &device.state, queue);
assert_eq!(counts(&workspace, &queue), Behind::default());
for id in [saved, queued] {
edit(&mut workspace, id, &mut log);
workspace.mark_saved(id);
}
edit(&mut workspace, unsaved, &mut log);
device.relink(&mut workspace);
assert_eq!(
counts(&workspace, &queue),
Behind {
queued: 0,
changed: 2,
unsaved: 1
}
);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
queued,
class,
at(1),
);
assert_eq!(
changed(&workspace, &device.state, &queue)
.iter()
.map(|(id, ..)| *id)
.collect::<Vec<_>>(),
vec![saved],
);
}
#[test]
fn queueing_what_changed_makes_one_entry_each() {
let (mut workspace, mut log, bytes) = bench();
let class = ObjectClass::Program;
let (mut device, _) = attached(&workspace);
let mut queue = Queue::default();
let ids: Vec<u64> = (0..2)
.map(|slot| {
workspace.ingest(
format!("off-{slot}.ne5p"),
Origin::Device {
class,
at: at(slot),
},
bytes.clone(),
&mut log,
)
})
.collect();
let homeless = workspace.ingest("typed-here.ne5p".into(), Origin::Fresh, bytes, &mut log);
let held = crc(&workspace, ids[0]);
device.pretend_bodies(class, 7, &[Some(("off-0", held)), Some(("off-1", held))]);
for id in ids.iter().copied().chain([homeless]) {
edit(&mut workspace, id, &mut log);
workspace.mark_saved(id);
}
device.relink(&mut workspace);
queue_changed(&workspace, &mut device, &mut queue, &mut log);
assert_eq!(queue.ids(), ids);
assert_eq!(queue.entry(ids[0]).map(|held| held.at), Some(at(0)));
assert_eq!(queue.entry(ids[1]).map(|held| held.at), Some(at(1)));
assert!(!queue.holds(homeless), "it stands for no slot");
assert_eq!(
Behind::of(&workspace, &device.state, &queue),
Behind {
queued: 2,
changed: 0,
unsaved: 0
},
"the gap is closed, and what closed it is waiting"
);
}
#[test]
fn the_header_counts_all_three_however_many_each_comes_to() {
assert_eq!(
Behind {
queued: 0,
changed: 2,
unsaved: 1
}
.said(),
"0 queued · 2 changed · 1 unsaved"
);
assert_eq!(
Behind {
queued: 3,
changed: 0,
unsaved: 0
}
.said(),
"3 queued · 0 changed · 0 unsaved"
);
assert_eq!(Behind::default().action(), "Queue 0 changed");
}
#[test]
fn each_part_of_the_line_stands_under_the_mark_it_explains() {
let behind = Behind {
queued: 3,
changed: 2,
unsaved: 1,
};
assert_eq!(
behind.parts(),
[
("3 queued".to_string(), Mark::Differs),
("2 changed".to_string(), Mark::Differs),
("1 unsaved".to_string(), Mark::Unsaved),
]
);
assert_eq!(behind.said(), "3 queued · 2 changed · 1 unsaved");
}
#[test]
fn one_entry_per_slot_and_one_per_asset() {
let (mut workspace, mut log, bytes) = bench();
let class = ObjectClass::Program;
let mut asset =
|name: &str| workspace.ingest(name.into(), Origin::Fresh, bytes.clone(), &mut log);
let first = asset("first.ne5p");
let second = asset("second.ne5p");
let mut queue = Queue::default();
let landed = queue.put(
workspace.get(first).unwrap(),
class,
at(0),
Occupancy::Vacant,
);
assert!(matches!(landed, Put::Made));
let landed = queue.put(
workspace.get(second).unwrap(),
class,
at(0),
Occupancy::Vacant,
);
assert!(
matches!(landed, Put::Instead(displaced) if displaced == first),
"one asset per slot"
);
assert_eq!(queue.ids(), vec![second]);
queue.put(
workspace.get(first).unwrap(),
class,
at(1),
Occupancy::Vacant,
);
let landed = queue.put(
workspace.get(first).unwrap(),
class,
at(2),
Occupancy::Vacant,
);
assert!(
matches!(landed, Put::Moved(was, before) if (was, before) == (class, at(1))),
"one slot per asset"
);
assert_eq!(queue.ids(), vec![second, first]);
let landed = queue.put(
workspace.get(first).unwrap(),
class,
at(2),
Occupancy::Vacant,
);
assert!(matches!(landed, Put::Standing));
assert_eq!(queue.ids(), vec![second, first]);
}
#[test]
fn a_failed_write_stays_queued_and_says_why() {
let (mut workspace, mut log, bytes) = bench();
let class = ObjectClass::Program;
let ids: Vec<u64> = (0..3)
.map(|slot| {
workspace.ingest(
format!("sound {slot}"),
Origin::Fresh,
bytes.clone(),
&mut log,
)
})
.collect();
let mut queue = Queue::default();
for (slot, id) in ids.iter().enumerate() {
queue.put(
workspace.get(*id).unwrap(),
class,
at(slot as u32),
Occupancy::Vacant,
);
}
queue.forget(ids[0]);
queue.stumbled(class, "Programs 7:2 is occupied");
assert_eq!(queue.ids(), ids[1..], "the rest are still waiting");
assert_eq!(
queue.entry(ids[1]).and_then(|held| held.failure.as_deref()),
Some("Programs 7:2 is occupied"),
);
assert!(queue.entry(ids[2]).unwrap().failure.is_none());
}
#[test]
fn what_is_owed_is_what_the_queue_holds() {
let (mut workspace, mut log, bytes) = bench();
let id = workspace.ingest(
"Africa-Split.ne5p".into(),
Origin::Device {
class: ObjectClass::Program,
at: at(3),
},
bytes,
&mut log,
);
let mut queue = Queue::default();
assert!(!queue.holds(id));
queue.put(
workspace.get(id).unwrap(),
ObjectClass::Program,
at(3),
Occupancy::Vacant,
);
assert!(queue.holds(id));
queue.forget(id);
assert!(!queue.holds(id) && queue.is_empty());
}
#[test]
fn a_registry_diff_lists_exactly_the_fields_that_differ() {
let (_workspace, _log, here) = bench();
let (_, there) = crate::fields::apply(&here, &[("center_panel.gain".into(), "96".into())])
.expect("the registry takes the set");
assert_ne!(here, there);
let Diff::Fields(fields) = compare(&here, &there) else {
panic!("two programs are two registries");
};
let paths: Vec<&str> = fields.iter().map(|field| field.path.as_str()).collect();
assert_eq!(paths, vec!["center_panel.gain"]);
let field = &fields[0];
assert_ne!(field.here, field.there);
assert_eq!(field.there, "96");
assert!(matches!(compare(&here, &here), Diff::Identical));
}
#[test]
fn a_body_with_no_registry_is_compared_byte_by_byte() {
let here = b"not a Nord file at all".to_vec();
let mut there = here.clone();
there[8] = b'!';
let Diff::Bytes { first_at } = compare(&here, &there) else {
panic!("neither of them decodes");
};
assert_eq!(first_at, 8);
assert!(matches!(compare(&here, &here), Diff::Identical));
let Diff::Bytes { first_at } = compare(&here, &here[..4]) else {
panic!("one is a prefix of the other");
};
assert_eq!(first_at, 4);
}
#[test]
fn what_is_waiting_counts_its_replacements_whatever_the_bytes_turn_out_to_be() {
let (mut workspace, mut log, bytes) = bench();
let ctx = workspace.ctx().clone();
let mut device = Device::new(ctx);
let class = ObjectClass::Program;
device.pretend_scanned(class, 7, &["Africa Split", "Squabble B", "", ""]);
let (_, edited) =
crate::fields::apply(&bytes, &[("center_panel.gain".into(), "96".into())])
.expect("the registry takes the set");
let mut queue = Queue::default();
let mut ids = Vec::new();
for slot in 0..4 {
let held = match slot {
1 => bytes.clone(),
_ => edited.clone(),
};
let id = workspace.ingest(
format!("sound {slot}"),
Origin::Device {
class,
at: at(slot),
},
held,
&mut log,
);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(slot),
);
ids.push(id);
}
assert_eq!((queue.len(), replacing(&queue)), (4, 2));
assert!(matches!(queue.entry(ids[0]).unwrap().diff, Diff::Pending));
assert!(matches!(queue.entry(ids[1]).unwrap().diff, Diff::Pending));
assert!(matches!(queue.entry(ids[2]).unwrap().diff, Diff::Empty));
assert_eq!(device.queued().len(), 2, "one read per occupied slot");
queue.arrived(class, at(0), "Africa Split", &bytes, &workspace);
queue.arrived(class, at(1), "Squabble B", &bytes, &workspace);
let Diff::Fields(fields) = &queue.entry(ids[0]).unwrap().diff else {
panic!("a program against a program is a field list");
};
assert_eq!(
fields.iter().map(|f| f.path.as_str()).collect::<Vec<_>>(),
vec!["center_panel.gain"]
);
assert!(matches!(queue.entry(ids[1]).unwrap().diff, Diff::Identical));
assert_eq!((queue.len(), replacing(&queue)), (4, 2));
}
#[test]
fn an_edit_under_a_waiting_entry_is_diffed_again_without_a_second_read() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, _tabs) = attached(&workspace);
let mut queue = Queue::default();
let class = ObjectClass::Program;
device.pretend_bodies(class, 7, &[Some(("Africa Split", 7))]);
let id = workspace.ingest(
"Africa Split".into(),
Origin::Device { class, at: at(0) },
bytes.clone(),
&mut log,
);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(0),
);
queue.arrived(class, at(0), "Africa Split", &bytes, &workspace);
assert!(matches!(queue.entry(id).unwrap().diff, Diff::Identical));
let reads = device.queued().len();
edit(&mut workspace, id, &mut log);
follow(&workspace, &mut device, &mut queue, &mut log);
let Diff::Fields(fields) = &queue.entry(id).unwrap().diff else {
panic!("a program against a program is a field list");
};
assert_eq!(
fields.iter().map(|f| f.path.as_str()).collect::<Vec<_>>(),
vec!["center_panel.gain"]
);
assert_eq!(device.queued().len(), reads, "the slot was not read again");
follow(&workspace, &mut device, &mut queue, &mut log);
assert!(matches!(queue.entry(id).unwrap().diff, Diff::Fields(_)));
assert_eq!(device.queued().len(), reads);
}
#[test]
fn an_edit_under_an_entry_settled_by_checksum_asks_for_the_read_once() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, _tabs) = attached(&workspace);
let mut queue = Queue::default();
let class = ObjectClass::Program;
let id = workspace.ingest(
"Africa Split".into(),
Origin::Device { class, at: at(0) },
bytes,
&mut log,
);
let held = workspace.get(id).unwrap().saved.crc32.unwrap();
device.pretend_bodies(class, 7, &[Some(("Africa Split", held))]);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(0),
);
assert!(matches!(queue.entry(id).unwrap().diff, Diff::Identical));
assert!(device.queued().is_empty(), "the checksums settled it");
edit(&mut workspace, id, &mut log);
follow(&workspace, &mut device, &mut queue, &mut log);
assert!(matches!(queue.entry(id).unwrap().diff, Diff::Pending));
assert_eq!(asked(&device), (class, at(0), Purpose::Compare));
follow(&workspace, &mut device, &mut queue, &mut log);
assert_eq!(device.queued().len(), 1, "asked for once, not once a frame");
}
#[test]
fn a_type_0_entry_settles_against_the_slot_reporting_its_checksum_without_a_read() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, _tabs) = attached(&workspace);
let mut queue = Queue::default();
let class = ObjectClass::Program;
let id = workspace.ingest(
"Circling Bells.ne5p".into(),
Origin::File("Circling Bells.ne5p".into()),
crate::workspace::as_type_0(&bytes),
&mut log,
);
let held = crc(&workspace, id);
device.pretend_bodies(class, 7, &[Some(("Circling Bells", held))]);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(0),
);
assert!(matches!(queue.entry(id).unwrap().diff, Diff::Identical));
assert!(device.queued().is_empty(), "the checksums settled it");
}
#[test]
fn queueing_an_asset_where_it_is_already_going_asks_and_says_nothing_further() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, _tabs) = attached(&workspace);
let class = ObjectClass::Program;
let mut queue = Queue::default();
device.pretend_bodies(class, 7, &[Some(("Africa Split", 7))]);
let id = workspace.ingest("Jazzy Click B".into(), Origin::Fresh, bytes, &mut log);
for _ in 0..3 {
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(0),
);
}
assert_eq!(queue.len(), 1);
assert_eq!(asked(&device), (class, at(0), Purpose::Compare));
assert_eq!(device.queued().len(), 1, "one read for one destination");
assert_eq!(
log.transcript()
.matches("is waiting to be sent to Programs 7:1")
.count(),
1,
"{}",
log.transcript()
);
}
#[test]
fn edits_made_while_a_compare_read_is_out_do_not_ask_for_it_again() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, _tabs) = attached(&workspace);
let class = ObjectClass::Program;
let mut queue = Queue::default();
device.pretend_bodies(class, 7, &[Some(("Africa Split", 7))]);
let id = workspace.ingest("Jazzy Click B".into(), Origin::Fresh, bytes, &mut log);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(0),
);
assert_eq!(asked(&device), (class, at(0), Purpose::Compare));
for gain in ["96", "97", "98"] {
let held = workspace.get(id).unwrap().bytes.clone();
let (_, edited) =
crate::fields::apply(&held, &[("center_panel.gain".into(), gain.into())]).unwrap();
workspace.replace_bytes(id, edited, &mut log);
follow(&workspace, &mut device, &mut queue, &mut log);
}
assert!(matches!(queue.entry(id).unwrap().diff, Diff::Pending));
assert_eq!(device.queued().len(), 1, "the answer was already coming");
}
#[test]
fn an_entry_waiting_on_a_read_when_the_instrument_went_away_is_asked_again() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, mut tabs) = attached(&workspace);
let class = ObjectClass::Program;
let mut queue = Queue::default();
device.pretend_bodies(class, 7, &[Some(("Africa Split", 7))]);
let id = workspace.ingest("Jazzy Click B".into(), Origin::Fresh, bytes, &mut log);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(0),
);
assert_eq!(asked(&device), (class, at(0), Purpose::Compare));
device.pretend(DeviceEvent::Disconnected { lost: true });
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
assert!(
device.queued().is_empty(),
"the read went with the connection"
);
device.pretend_attached();
device.pretend(DeviceEvent::Partitions(Vec::new()));
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
let held = queue.entry(id).expect("it is still waiting");
assert!(matches!(held.diff, Diff::Pending));
assert!(
held.replaces.occupant().is_none(),
"the last instrument's occupant is no claim about this one"
);
assert_eq!(
asked(&device),
(class, at(0), Purpose::Compare),
"the answer that never came is asked for again"
);
}
#[test]
fn a_slot_in_an_unscanned_bank_is_read_before_it_is_called_free() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, _tabs) = attached(&workspace);
let class = ObjectClass::Program;
let mut queue = Queue::default();
let id = workspace.ingest("Jazzy Click B".into(), Origin::Fresh, bytes, &mut log);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(6),
);
let held = queue.entry(id).unwrap();
assert!(matches!(held.diff, Diff::Pending));
assert!(held.replaces.occupant().is_none());
assert_eq!(asked(&device), (class, at(6), Purpose::Compare));
}
#[test]
fn the_read_of_an_unscanned_slot_settles_what_the_entry_replaces() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, mut tabs) = attached(&workspace);
let class = ObjectClass::Program;
let (_, edited) =
crate::fields::apply(&bytes, &[("center_panel.gain".into(), "96".into())])
.expect("the registry takes the set");
let mut queue = Queue::default();
let mut ids = Vec::new();
for slot in [3, 4] {
let id = workspace.ingest(
format!("sound {slot}"),
Origin::Fresh,
edited.clone(),
&mut log,
);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(slot),
);
ids.push(id);
}
device.pretend(DeviceEvent::Vacant {
class,
at: at(3),
why: Purpose::Compare,
});
device.pretend(DeviceEvent::Got {
name: "Jazzy Click B".into(),
origin: Origin::Device { class, at: at(4) },
bytes,
why: Purpose::Compare,
});
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
let empty = queue.entry(ids[0]).unwrap();
assert!(matches!(empty.diff, Diff::Empty));
assert!(empty.replaces.occupant().is_none());
let taken = queue.entry(ids[1]).unwrap();
let Diff::Fields(fields) = &taken.diff else {
panic!("a program against a program is a field list");
};
assert_eq!(
fields.iter().map(|f| f.path.as_str()).collect::<Vec<_>>(),
vec!["center_panel.gain"]
);
assert_eq!(
taken.replaces.occupant().map(|held| held.name.as_str()),
Some("Jazzy Click B"),
"the read named what it found"
);
assert_eq!((queue.len(), replacing(&queue)), (2, 1));
}
#[test]
fn a_slot_a_walk_has_read_carries_the_name_it_found() {
let (mut workspace, mut log, bytes) = bench();
let (mut device, mut tabs) = attached(&workspace);
let class = ObjectClass::Program;
let mut queue = Queue::default();
let occupied = Location { bank: 0, slot: 6 };
device.pretend(DeviceEvent::BankScanned {
class,
bank: 1,
slots: (0..7)
.map(|slot| {
(slot == occupied.slot).then(|| ProgramInfo {
location: Location { bank: 0, slot },
name: "Jazzy Click B".into(),
..occupant("", None)
})
})
.collect(),
});
device.poll(&mut log, &mut workspace, &mut tabs, &mut queue);
let id = workspace.ingest("Jazzy Click B".into(), Origin::Fresh, bytes, &mut log);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
occupied,
);
let held = queue.entry(id).unwrap();
assert_eq!(
held.replaces.occupant().map(|held| held.name.as_str()),
Some("Jazzy Click B"),
);
assert!(matches!(held.diff, Diff::Pending));
assert_eq!(asked(&device), (class, occupied, Purpose::Compare));
}
#[test]
fn a_scanned_empty_slot_is_free_and_asks_the_instrument_nothing() {
let (mut workspace, mut log, bytes) = bench();
let ctx = workspace.ctx().clone();
let mut device = Device::new(ctx);
let class = ObjectClass::Program;
device.pretend_scanned(class, 7, &["Africa Split", ""]);
let mut queue = Queue::default();
let id = workspace.ingest("Jazzy Click B".into(), Origin::Fresh, bytes, &mut log);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(1),
);
let held = queue.entry(id).unwrap();
assert!(matches!(held.diff, Diff::Empty));
assert!(held.replaces.occupant().is_none());
assert!(device.queued().is_empty(), "nothing to ask about");
}
#[test]
fn retargeting_moves_the_entry_and_displaces_what_was_waiting_there() {
let (mut workspace, mut log, bytes) = bench();
let ctx = workspace.ctx().clone();
let mut device = Device::new(ctx);
let class = ObjectClass::Program;
device.pretend_scanned(class, 7, &["", "Africa Split", ""]);
let mut queue = Queue::default();
let mut asset =
|name: &str| workspace.ingest(name.into(), Origin::Fresh, bytes.clone(), &mut log);
let first = asset("first.ne5p");
let second = asset("second.ne5p");
for (id, slot) in [(first, 0), (second, 1)] {
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(slot),
);
}
assert!(matches!(queue.entry(first).unwrap().diff, Diff::Empty));
retarget(
&workspace,
&mut device,
&mut queue,
&mut log,
first,
class,
at(1),
);
assert_eq!(queue.ids(), vec![first], "one entry per slot and per asset");
let held = queue.entry(first).unwrap();
assert_eq!(held.at, at(1));
assert_eq!(
held.replaces.occupant().map(|held| held.name.as_str()),
Some("Africa Split"),
"what the new slot holds is what it now replaces"
);
retarget(
&workspace,
&mut device,
&mut queue,
&mut log,
second,
class,
at(2),
);
assert_eq!(queue.ids(), vec![first]);
}
#[test]
fn a_click_on_a_bank_chip_leaves_the_picker_open_on_that_bank() {
let ctx = egui::Context::default();
ctx.all_styles_mut(crate::app::metrics);
let mut workspace = Workspace::new(ctx.clone());
let mut log = Log::default();
let mut device = Device::new(ctx.clone());
let class = ObjectClass::Program;
device.pretend_scanned(class, 7, &["Africa Split", "", ""]);
device.pretend_scanned(class, 8, &["", "", ""]);
let bytes = {
let id = workspace.create(Fresh::Program, &mut log).unwrap();
let held = workspace.get(id).unwrap().bytes.clone();
workspace.remove(id, &mut log);
held
};
let id = workspace.ingest("Jazzy Click B".into(), Origin::Fresh, bytes, &mut log);
let mut queue = Queue::default();
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(1),
);
let held = queue.entry(id).expect("it is waiting");
let chip_id = std::cell::Cell::new(egui::Id::NULL);
let draw = |events: Vec<egui::Event>| {
let input = egui::RawInput {
events,
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(600.0, 600.0),
)),
..Default::default()
};
let _ = ctx.run(input, |ctx| {
egui::CentralPanel::default()
.frame(egui::Frame::new())
.show(ctx, |ui| {
chip_id.set(ui.id().with(("destination", held.id)));
let row = ui.max_rect();
destination(
ui,
held,
row,
row.right(),
ui.visuals().text_color(),
&device.state,
&queue,
&mut Vec::new(),
);
});
});
};
let settle = || {
for _ in 0..3 {
draw(Vec::new());
}
};
let click_at = |on: egui::Pos2| {
let press = |pressed| egui::Event::PointerButton {
pos: on,
button: egui::PointerButton::Primary,
pressed,
modifiers: egui::Modifiers::NONE,
};
draw(vec![egui::Event::PointerMoved(on)]);
draw(vec![press(true), press(false)]);
};
let rect_of = |id: egui::Id| ctx.read_response(id).map(|drawn| drawn.rect);
settle();
let chip = rect_of(chip_id.get()).expect("the row drew its destination chip");
click_at(chip.center());
settle();
let bank = rect_of(salt(held).with(("bank", 8_u32)))
.expect("the picker offers every bank that has been read");
click_at(bank.center());
settle();
assert!(
egui::Popup::is_id_open(&ctx, chip_id.get().with("popup")),
"the picker stays up across a bank"
);
assert!(
rect_of(salt(held).with(("slot", 7_u32, 0_u32))).is_some(),
"and it is showing bank 8"
);
}
#[test]
fn a_click_on_the_pickers_cell_answers_with_that_slot() {
let ctx = egui::Context::default();
ctx.all_styles_mut(crate::app::metrics);
let mut workspace = Workspace::new(ctx.clone());
let mut log = Log::default();
let mut device = Device::new(ctx.clone());
let class = ObjectClass::Program;
device.pretend_scanned(class, 7, &["Africa Split", "", ""]);
let bytes = {
let id = workspace.create(Fresh::Program, &mut log).unwrap();
let held = workspace.get(id).unwrap().bytes.clone();
workspace.remove(id, &mut log);
held
};
let id = workspace.ingest("Jazzy Click B".into(), Origin::Fresh, bytes, &mut log);
let mut queue = Queue::default();
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(1),
);
let held = queue.entry(id).expect("it is waiting");
let wanted = at(2);
let draw = |events: Vec<egui::Event>| -> (Option<Location>, Option<egui::Pos2>) {
let input = egui::RawInput {
events,
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(crate::keyboard::grid_width(PICKER_COLUMNS), 300.0),
)),
..Default::default()
};
let mut drawn = (None, None);
let _ = ctx.run(input, |ctx| {
egui::CentralPanel::default()
.frame(egui::Frame::new())
.show(ctx, |ui| {
drawn = (
picker(ui, held, &device.state, &queue),
ctx.read_response(salt(held).with(("slot", wanted.bank, wanted.slot)))
.map(|cell| cell.rect.center()),
);
});
});
drawn
};
let on_cell = draw(Vec::new())
.1
.expect("the picker drew a cell for every slot of the bank");
draw(vec![egui::Event::PointerMoved(on_cell)]);
let press = |pressed| egui::Event::PointerButton {
pos: on_cell,
button: egui::PointerButton::Primary,
pressed,
modifiers: egui::Modifiers::NONE,
};
assert_eq!(draw(vec![press(true), press(false)]).0, Some(wanted));
}
#[test]
fn the_dock_page_paints_every_shape_a_diff_comes_in() {
let ctx = egui::Context::default();
ctx.all_styles_mut(crate::app::metrics);
let mut workspace = Workspace::new(ctx.clone());
let mut log = Log::default();
let mut device = Device::new(ctx.clone());
let class = ObjectClass::Program;
device.pretend_scanned(class, 7, &["Africa Split", "Squabble B", ""]);
let bytes = {
let id = workspace.create(Fresh::Program, &mut log).unwrap();
let held = workspace.get(id).unwrap().bytes.clone();
workspace.remove(id, &mut log);
held
};
let (_, edited) =
crate::fields::apply(&bytes, &[("center_panel.gain".into(), "96".into())])
.expect("the registry takes the set");
let mut queue = Queue::default();
for slot in 0..3 {
let id = workspace.ingest(
format!("a rather long name for sound {slot}"),
Origin::Device {
class,
at: at(slot),
},
edited.clone(),
&mut log,
);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(slot),
);
}
queue.arrived(class, at(1), "Squabble B", &bytes, &workspace);
for width in [430.0_f32, 900.0] {
for picked in queue.ids() {
queue.picked = Some(picked);
let _ = ctx.run(egui::RawInput::default(), |ctx| {
egui::TopBottomPanel::bottom("dock")
.exact_height(crate::shell::DOCK_BODY)
.frame(egui::Frame::new())
.show(ctx, |ui| {
ui.set_width(width);
page(ui, &mut queue, &workspace, &device.state, &mut Vec::new());
});
});
}
}
}
#[test]
fn the_diff_grid_gives_every_column_room_until_there_is_none() {
let laid = |width: f32| crate::panel::tracks(width, &DIFF_TRACKS, GAP);
for width in [90.0_f32, 240.0, 620.0] {
let tracks = laid(width);
for track in &tracks {
assert!(track.start >= 0.0, "{width}: {track:?}");
assert!(track.end >= track.start, "{width}: {track:?}");
assert!(track.end <= width + 0.001, "{width}: {track:?}");
}
let overlap = tracks.windows(2).any(|two| two[1].start < two[0].end);
assert!(!overlap, "{width}");
}
assert!(laid(620.0)
.iter()
.all(|track| track.end - track.start > 1.0));
assert!(laid(4.0).iter().all(|track| track.end >= track.start));
}
}