use eframe::egui;
use nord_usb::wire::{AllocationUnit, Status};
use nord_usb::ObjectClass;
use crate::app::{accent, warn};
use crate::device::DeviceState;
use crate::queue::Queue;
use crate::workspace::Workspace;
pub const TROUGH: f32 = 5.0;
const CROWDED: f32 = 0.9;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Meter {
pub used: u64,
pub total: u64,
pub queued: u64,
}
impl Meter {
pub fn filled(&self) -> f32 {
match self.total {
0 => 0.0,
total => (self.used as f32 / total as f32).clamp(0.0, 1.0),
}
}
pub fn incoming(&self) -> f32 {
match self.total {
0 => 0.0,
total => (self.queued as f32 / total as f32).clamp(0.0, 1.0 - self.filled()),
}
}
pub fn crowded(&self) -> bool {
self.filled() > CROWDED
}
}
fn fills(class: ObjectClass, unit: Option<AllocationUnit>, banks: usize) -> bool {
(class.is_library() && unit.is_some()) || banks > 1
}
pub fn meter(
class: ObjectClass,
inventory: &[Status],
unit: Option<AllocationUnit>,
banks: usize,
queue: &Queue,
workspace: &Workspace,
) -> Option<Meter> {
if !fills(class, unit, banks) {
return None;
}
let status = inventory.iter().find(|status| status.class == class)?;
if status.total() == 0 {
return None;
}
let (used, total) = match status.slots() {
Some(slots) => (u64::from(status.count), u64::from(slots)),
None => (u64::from(status.used), status.total()),
};
Some(Meter {
used,
total,
queued: incoming(class, status.slots().is_some(), unit, queue, workspace),
})
}
fn incoming(
class: ObjectClass,
by_slot: bool,
unit: Option<AllocationUnit>,
queue: &Queue,
workspace: &Workspace,
) -> u64 {
queue
.entries()
.iter()
.filter(|held| held.class == class && held.replaces.occupant().is_none())
.filter_map(|held| match by_slot {
true => Some(1),
false => {
let bytes = workspace.get(held.id)?.bytes.len();
unit?.blocks_for(bytes).ok().map(u64::from)
}
})
.sum()
}
pub fn free_space(
class: ObjectClass,
inventory: &[Status],
unit: Option<AllocationUnit>,
) -> Option<String> {
let status = inventory.iter().find(|status| status.class == class)?;
let (free, total) = (status.available(), status.total());
Some(match unit {
Some(unit) => format!(
"{} free of {}",
measure(free.saturating_mul(u64::from(unit.get()))),
measure(total.saturating_mul(u64::from(unit.get()))),
),
None => format!("{free} of {total} free, in units this folder counts in"),
})
}
pub fn free_bytes(class: ObjectClass, device: &DeviceState) -> Option<u64> {
let unit = device.allocation_unit(class)?;
let status = device
.inventory
.iter()
.find(|status| status.class == class)?;
Some(status.available().saturating_mul(u64::from(unit.get())))
}
pub fn constraint(queue: &Queue, workspace: &Workspace, device: &DeviceState) -> Option<String> {
let (name, bytes, class) = queue
.entries()
.iter()
.filter_map(|held| {
let entity = workspace.get(held.id)?;
Some((entity.name.clone(), entity.bytes.len() as u64, held.class))
})
.max_by_key(|(_, bytes, _)| *bytes)?;
let free = free_bytes(class, device)?;
let verdict = match bytes <= free {
true => "it fits",
false => "it does not fit",
};
Some(format!(
"{name} is {} and {} is free — {verdict}.",
measure(bytes),
measure(free)
))
}
pub fn measure(bytes: u64) -> String {
let (figure, unit) = scaled(bytes, bytes);
format!("{figure} {unit}")
}
pub fn measure_out_of(part: u64, whole: u64) -> String {
let (part, unit) = scaled(part, whole);
let (whole, _) = scaled(whole, whole);
format!("{part}/{whole} {unit}")
}
fn scaled(bytes: u64, scale: u64) -> (String, &'static str) {
const K: f64 = 1024.0;
let held = bytes as f64;
let scale = scale as f64;
if scale < K {
return (bytes.to_string(), "B");
}
if scale < K * K {
return (format!("{:.1}", held / K), "kB");
}
(format!("{:.1}", held / (K * K)), "MB")
}
pub fn bar(ui: &mut egui::Ui, meter: Meter) {
let (rect, _) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), TROUGH),
egui::Sense::hover(),
);
let visuals = ui.visuals().clone();
let painter = ui.painter().clone();
painter.rect_filled(rect, 1.0, visuals.extreme_bg_color);
let filled = rect.width() * meter.filled();
let tone = match meter.crowded() {
true => warn(&visuals),
false => accent(&visuals),
};
if filled > 0.0 {
painter.rect_filled(
egui::Rect::from_min_size(rect.min, egui::vec2(filled, rect.height())),
1.0,
tone,
);
}
let incoming = rect.width() * meter.incoming();
if incoming > 0.0 {
painter.rect_filled(
egui::Rect::from_min_size(
egui::pos2(rect.left() + filled, rect.top()),
egui::vec2(incoming, rect.height()),
),
1.0,
warn(&visuals),
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::{pretend_allocation_unit, Device};
use crate::log::Log;
use crate::queue::enqueue;
use crate::workspace::{Fresh, Origin};
use nord_usb::Location;
fn status(class: ObjectClass, count: u32, free: u32, used: u32) -> Status {
Status {
class,
count,
free,
used,
dirty: 0,
spare: 0,
}
}
fn at(slot: u32) -> Location {
Location { bank: 0, slot }
}
#[test]
fn a_slot_folder_meters_items_and_counts_only_what_would_fill_a_slot() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let class = ObjectClass::Program;
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 inventory = [status(class, 100, 300 * 121, 100 * 121)];
device.pretend_scanned(class, 1, &["", "", "Africa Split"]);
let mut queue = Queue::default();
for slot in 0..3 {
let id = workspace.ingest(
format!("sound {slot}"),
Origin::Fresh,
bytes.clone(),
&mut log,
);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(slot),
);
}
let held =
meter(class, &inventory, None, 4, &queue, &workspace).expect("the class was read");
assert_eq!(held.used, 100);
assert_eq!(held.total, 400);
assert_eq!(held.queued, 2, "the third replaces what is there");
assert_eq!(held.filled(), 0.25);
assert_eq!(held.incoming(), 2.0 / 400.0);
assert!(!held.crowded());
}
#[test]
fn a_library_meters_blocks_and_has_no_meter_at_all_without_its_unit() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let class = ObjectClass::Sample;
let inventory = [status(class, 84, 64, 1472)];
device.pretend_scanned(class, 1, &[""]);
let id = workspace.ingest("a sample".into(), Origin::Fresh, vec![0; 300_000], &mut log);
let mut queue = Queue::default();
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(0),
);
assert_eq!(meter(class, &inventory, None, 1, &queue, &workspace), None);
let unit = pretend_allocation_unit(class, 131_064);
let known = meter(class, &inventory, Some(unit), 1, &queue, &workspace).unwrap();
assert_eq!(known.used, 1472);
assert_eq!(known.total, 1536);
assert_eq!(known.queued, 3);
assert!(known.crowded(), "1472 of 1536 is past nine tenths");
}
#[test]
fn a_partition_that_counts_nothing_at_all_has_no_meter() {
let ctx = egui::Context::default();
let workspace = Workspace::new(ctx);
let queue = Queue::default();
let class = ObjectClass::Piano;
let unit = Some(pretend_allocation_unit(class, 261_632));
let inventory = [
status(class, 0, 0, 0),
status(ObjectClass::Program, 1, 9, 1),
];
let drawn = |class, unit, banks| meter(class, &inventory, unit, banks, &queue, &workspace);
assert_eq!(drawn(class, unit, 1), None);
assert!(drawn(ObjectClass::Program, None, 4).is_some());
}
#[test]
fn only_a_partition_that_can_fill_gets_a_meter() {
let ctx = egui::Context::default();
let mut device = Device::new(ctx.clone());
let workspace = Workspace::new(ctx);
let queue = Queue::default();
let (one, many, library) = (ObjectClass::Live, ObjectClass::Program, ObjectClass::Sample);
let inventory = [
status(one, 1, 4, 1),
status(many, 100, 300, 100),
status(library, 84, 64, 1472),
];
device.pretend_geometry(one, &[("Live", 5)]);
device.pretend_geometry(many, &[("1", 50), ("2", 50), ("3", 50), ("4", 50)]);
device.pretend_geometry(library, &[("Samp Lib", 1)]);
let unit = |class| Some(pretend_allocation_unit(class, 1));
let drawn = |class, unit| {
meter(
class,
&inventory,
unit,
device.state.banks(class),
&queue,
&workspace,
)
.is_some()
};
assert!(!drawn(one, unit(one)), "one bank of fixed slots");
assert!(drawn(many, unit(many)), "more than one bank");
assert!(drawn(many, None), "a bank division needs no unit");
assert!(
drawn(library, unit(library)),
"one bank, and counted in bytes"
);
assert!(!drawn(library, None), "nothing counts bytes without a unit");
}
#[test]
fn the_queued_segment_stops_at_the_end_of_the_trough() {
let full = Meter {
used: 380,
total: 400,
queued: 500,
};
assert_eq!(full.filled(), 0.95);
assert!((full.filled() + full.incoming() - 1.0).abs() < f32::EPSILON);
let unread = Meter {
used: 0,
total: 0,
queued: 4,
};
assert_eq!((unread.filled(), unread.incoming()), (0.0, 0.0));
}
#[test]
fn the_binding_constraint_is_the_largest_thing_waiting_against_the_room_left() {
let ctx = egui::Context::default();
let mut workspace = Workspace::new(ctx.clone());
let mut device = Device::new(ctx);
let mut log = Log::default();
let class = ObjectClass::Sample;
let mut queue = Queue::default();
assert_eq!(constraint(&queue, &workspace, &device.state), None);
device.pretend_scanned(class, 1, &["", ""]);
device.state.inventory.push(status(class, 84, 64, 1472));
for (name, size, slot) in [("small", 4096, 0), ("Grand", 5_347_738, 1)] {
let id = workspace.ingest(name.into(), Origin::Fresh, vec![0; size], &mut log);
enqueue(
&workspace,
&mut device,
&mut queue,
&mut log,
id,
class,
at(slot),
);
}
assert_eq!(constraint(&queue, &workspace, &device.state), None);
device.pretend_partitions(&crate::device::ELECTRO5);
assert_eq!(
constraint(&queue, &workspace, &device.state).as_deref(),
Some("Grand is 5.1 MB and 8.0 MB is free — it fits.")
);
device.state.inventory.clear();
device.state.inventory.push(status(class, 84, 8, 1528));
assert!(constraint(&queue, &workspace, &device.state)
.is_some_and(|said| said.ends_with("it does not fit.")));
}
#[test]
fn free_space_reads_in_bytes_only_once_the_allocation_unit_has_arrived() {
let class = ObjectClass::Sample;
let inventory = [status(class, 84, 64, 1472)];
assert_eq!(
free_space(class, &inventory, None).as_deref(),
Some("64 of 1536 free, in units this folder counts in")
);
assert_eq!(
free_space(
class,
&inventory,
Some(pretend_allocation_unit(class, 131_064))
)
.as_deref(),
Some("8.0 MB free of 192.0 MB")
);
assert_eq!(free_space(ObjectClass::Program, &inventory, None), None);
}
}