use std::collections::{HashMap, HashSet};
use eframe::egui;
use nord_format::fields::{ControlKind, Field, PackedOrder, Unit};
use nord_format::panel::{Panel, Section as Placed, Selection};
use super::controls::{self, Ctx, Sets};
use super::keys::RADIUS;
use super::panel::{PianoLookup, PIANO_MODEL};
use crate::app;
use crate::icon::{icon, Glyph};
use crate::workspace::LocalEntity;
use crate::{drawbar_widget, knob, led, strings};
const TRANSPOSE_ENABLED: &str = "center_panel.transpose_enabled";
const TRANSPOSE: &str = "center_panel.transpose";
const LABEL: f32 = 9.5;
const DOT: f32 = 6.0;
const CARD_TITLE: f32 = 11.5;
const CHIP: f32 = 20.0;
const CHIP_TEXT: f32 = 11.0;
const COUNT_TEXT: f32 = 9.5;
const READING: f32 = 10.0;
const MENU_MAX: usize = 12;
const SLOTS: [(&str, &str, &str); 3] = [
("_wheel", "Wheel", "W"),
("_aftertouch", "Aftertouch", "AT"),
("_ctrl_pedal", "Control pedal", "P"),
];
#[derive(Default)]
pub struct State {
lens: Option<usize>,
active: Option<String>,
jump: Option<String>,
tops: Vec<(String, f32)>,
view_top: f32,
settled: Vec<Field>,
pending: Vec<String>,
read: Option<(u64, u64, Option<u32>)>,
}
impl State {
pub fn follow(&mut self, entity: &LocalEntity) {
let read = (entity.id, entity.stamp, entity.saved.crc32);
if self.read == Some(read) {
return;
}
self.read = Some(read);
self.settled = crate::fields::decoded(&entity.saved.bytes).unwrap_or_default();
self.pending = crate::fields::changed(&entity.saved.bytes, &entity.bytes);
}
pub fn pending(&self) -> &[String] {
&self.pending
}
pub fn settled(&self) -> &[Field] {
&self.settled
}
#[cfg(test)]
pub(super) fn pretend_lens(&mut self, slot: usize) {
self.lens = Some(slot);
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Shape {
Authored { exhaustive: bool },
Menus,
Flat,
}
pub struct Doc<'a> {
sections: Vec<Sect<'a>>,
leftovers: Vec<&'a Field>,
idle: Vec<&'static str>,
shape: Shape,
shown: HashSet<&'a str>,
picks: HashSet<&'a str>,
morphs: HashMap<&'a str, [Option<&'a Field>; SLOTS.len()]>,
fields: usize,
slots: usize,
}
struct Sect<'a> {
key: String,
title: String,
fields: Vec<&'a Field>,
nested: Vec<Sect<'a>>,
pick: Option<&'a Selection>,
selected: bool,
idle: Vec<&'static str>,
count: usize,
}
struct Part<'a> {
field: &'a Field,
morphs: [Option<&'a Field>; SLOTS.len()],
}
enum Cell<'a> {
One(Part<'a>),
Register(Vec<Part<'a>>),
Transpose,
}
pub fn of<'a>(decoded: &nord_format::Entity, fields: &'a [Field]) -> Doc<'a> {
let morphs = slots_of(fields);
let mut doc = match nord_format::panel::of(decoded) {
Some(layout) => authored(layout, fields, morphs),
None if crate::fields::is_electro5_settings(decoded) => menus(fields, morphs),
None => flat(fields, morphs),
};
doc.fields = fields.len();
doc.slots = fields
.iter()
.filter(|field| matches!(field.spec.control, ControlKind::Morph { .. }))
.count();
for section in &mut doc.sections {
count(section, &doc.morphs);
}
doc.shown = doc.sections.iter().flat_map(paths).collect();
let lensed = lensed(&doc.morphs, &doc.shown);
doc.shown.extend(lensed);
doc.picks = doc.sections.iter().flat_map(selectors).collect();
doc
}
impl Doc<'_> {
pub fn shows(&self, path: &str) -> bool {
self.shown.contains(path)
}
pub fn shape(&self) -> Shape {
self.shape
}
pub fn tally(&self) -> (usize, usize) {
(self.fields, self.slots)
}
pub fn unplaced(&self) -> (usize, usize) {
let named = self
.leftovers
.iter()
.filter(|field| strings::known(&field.path))
.count();
(self.leftovers.len(), named)
}
}
fn slots_of(fields: &[Field]) -> HashMap<&str, [Option<&Field>; SLOTS.len()]> {
let at: HashMap<&str, &Field> = fields
.iter()
.map(|field| (field.path.as_str(), field))
.collect();
let mut out: HashMap<&str, [Option<&Field>; SLOTS.len()]> = HashMap::new();
for slot in fields {
let Some(parent) = slot.spec.morph_parent() else {
continue;
};
let Some(parent) = at.get(parent.as_str()) else {
continue;
};
let Some(which) = which_slot(&slot.path) else {
continue;
};
out.entry(parent.path.as_str()).or_default()[which] = Some(slot);
}
out
}
fn which_slot(path: &str) -> Option<usize> {
let leaf = path.rsplit('.').next().unwrap_or(path);
SLOTS
.iter()
.position(|(suffix, _, _)| leaf.ends_with(suffix))
}
fn authored<'a>(
layout: &'a Panel,
fields: &'a [Field],
morphs: HashMap<&'a str, [Option<&'a Field>; SLOTS.len()]>,
) -> Doc<'a> {
let resolved = layout.resolve(fields);
let mut sections = Vec::new();
let mut idle = Vec::new();
for (nth, placed) in resolved.sections.iter().enumerate() {
if !placed.relevant {
idle.push(placed.group.title);
continue;
}
let mut nested = Vec::new();
let mut under = Vec::new();
hoist(&placed.groups, None, fields, &mut nested, &mut under);
sections.push(Sect {
key: format!("s{nth}"),
title: placed.group.title.to_string(),
fields: placed.fields.clone(),
nested,
pick: None,
selected: true,
idle: under,
count: 0,
});
}
let named: Vec<&Field> = resolved
.leftovers
.iter()
.copied()
.filter(|field| strings::known(&field.path))
.collect();
if !named.is_empty() {
sections.push(plain_sect(
"also".to_string(),
strings::Section::Other.title(),
named,
));
}
Doc {
sections,
leftovers: resolved.leftovers,
idle,
shape: Shape::Authored {
exhaustive: layout.exhaustive,
},
shown: HashSet::new(),
picks: HashSet::new(),
morphs,
fields: 0,
slots: 0,
}
}
fn hoist<'a>(
groups: &[Placed<'a>],
under: Option<&str>,
fields: &'a [Field],
into: &mut Vec<Sect<'a>>,
idle: &mut Vec<&'static str>,
) {
for group in groups {
if !group.relevant {
idle.push(group.group.title);
continue;
}
let title = match under {
Some(parent) => format!("{parent} · {}", group.group.title),
None => group.group.title.to_string(),
};
let pick = group.group.selected_by.as_ref();
into.push(Sect {
key: String::new(),
title: title.clone(),
fields: group.fields.clone(),
nested: Vec::new(),
pick,
selected: pick.is_some_and(|selection| selection.selected(fields)),
idle: Vec::new(),
count: 0,
});
hoist(&group.groups, Some(&title), fields, into, idle);
}
}
fn menus<'a>(
fields: &'a [Field],
morphs: HashMap<&'a str, [Option<&'a Field>; SLOTS.len()]>,
) -> Doc<'a> {
let sections = strings::SETTINGS_SECTIONS
.iter()
.enumerate()
.filter_map(|(nth, section)| {
let rows: Vec<&Field> = fields
.iter()
.filter(|field| strings::section(&field.path) == *section)
.collect();
(!rows.is_empty()).then(|| plain_sect(format!("m{nth}"), section.title(), rows))
})
.collect();
Doc {
sections,
leftovers: Vec::new(),
idle: Vec::new(),
shape: Shape::Menus,
shown: HashSet::new(),
picks: HashSet::new(),
morphs,
fields: 0,
slots: 0,
}
}
fn flat<'a>(
fields: &'a [Field],
morphs: HashMap<&'a str, [Option<&'a Field>; SLOTS.len()]>,
) -> Doc<'a> {
let sections = prefixes(fields)
.into_iter()
.enumerate()
.map(|(nth, group)| plain_sect(format!("f{nth}"), &group.title, group.rows))
.collect();
Doc {
sections,
leftovers: Vec::new(),
idle: Vec::new(),
shape: Shape::Flat,
shown: HashSet::new(),
picks: HashSet::new(),
morphs,
fields: 0,
slots: 0,
}
}
fn plain_sect<'a>(key: String, title: &str, rows: Vec<&'a Field>) -> Sect<'a> {
Sect {
key,
title: title.to_string(),
fields: rows,
nested: Vec::new(),
pick: None,
selected: true,
idle: Vec::new(),
count: 0,
}
}
struct Group<'a> {
key: String,
title: String,
rows: Vec<&'a Field>,
}
fn prefixes(fields: &[Field]) -> Vec<Group<'_>> {
let mut out: Vec<Group> = Vec::new();
for field in fields {
let prefix = field.path.rsplit_once('.').map_or("", |(head, _)| head);
match out.last_mut() {
Some(group) if group.key == prefix => group.rows.push(field),
_ => out.push(Group {
key: prefix.to_string(),
title: match prefix.is_empty() {
true => strings::UNPREFIXED.to_string(),
false => strings::title(prefix),
},
rows: vec![field],
}),
}
}
out.into_iter().flat_map(divide).collect()
}
const SPLIT_ABOVE: usize = 128;
fn divide(group: Group<'_>) -> Vec<Group<'_>> {
if group.rows.len() <= SPLIT_ABOVE {
return vec![group];
}
let mut out: Vec<Group> = Vec::new();
for field in group.rows {
let leaf = field.path.rsplit('.').next().unwrap_or(&field.path);
let word = leaf.split('_').next().unwrap_or(leaf);
let key = format!("{}.{word}", group.key);
match out.iter().position(|part| part.key == key) {
Some(at) => out[at].rows.push(field),
None => out.push(Group {
title: match group.key.is_empty() {
true => strings::title(word),
false => format!("{} — {word}", group.title),
},
key,
rows: vec![field],
}),
}
}
out
}
fn count(section: &mut Sect<'_>, morphs: &HashMap<&str, [Option<&Field>; SLOTS.len()]>) {
let mut total = section.fields.len();
for field in §ion.fields {
total += morphs.get(field.path.as_str()).map_or(0, |slots| {
slots.iter().filter(|slot| slot.is_some()).count()
});
}
for nested in &mut section.nested {
count(nested, morphs);
total += nested.count;
}
section.count = total;
}
fn selectors<'a>(section: &Sect<'a>) -> Vec<&'a str> {
let mut out: Vec<&str> = section
.pick
.map(|selection| selection.field)
.into_iter()
.collect();
for nested in §ion.nested {
out.extend(selectors(nested));
}
out
}
fn lensed<'a>(
morphs: &HashMap<&'a str, [Option<&'a Field>; SLOTS.len()]>,
shown: &HashSet<&'a str>,
) -> Vec<&'a str> {
let mut out = Vec::new();
for (parent, slots) in morphs {
if shown.contains(parent) {
out.extend(
slots
.iter()
.copied()
.flatten()
.map(|slot| slot.path.as_str()),
);
}
}
out
}
fn paths<'a>(section: &Sect<'a>) -> Vec<&'a str> {
let mut out: Vec<&str> = section
.fields
.iter()
.map(|field| field.path.as_str())
.collect();
for nested in §ion.nested {
out.extend(paths(nested));
}
out
}
pub fn nav(ui: &mut egui::Ui, state: &mut State, doc: &Doc<'_>) {
if doc.sections.is_empty() {
return;
}
let quiet = app::caption(ui.visuals());
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = egui::vec2(4.0, 2.0);
for section in &doc.sections {
let active = state.active.as_deref() == Some(section.key.as_str());
if nav_chip(ui, §ion.title, §ion.count.to_string(), active).clicked() {
state.jump = Some(section.key.clone());
}
}
if doc.slots == 0 {
return;
}
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.spacing_mut().item_spacing.x = 4.0;
for (nth, (_, word, _)) in SLOTS.iter().enumerate().rev() {
let count = stored_targets(doc, nth);
if nav_chip(ui, word, &count.to_string(), state.lens == Some(nth)).clicked() {
state.lens = Some(nth);
}
}
if nav_chip(ui, "Panel", "", state.lens.is_none()).clicked() {
state.lens = None;
}
ui.label(
egui::RichText::new("MORPH")
.font(egui::FontId::proportional(COUNT_TEXT))
.color(quiet),
);
});
});
}
fn stored_targets(doc: &Doc<'_>, slot: usize) -> usize {
doc.morphs
.values()
.filter(|slots| slots[slot].is_some_and(|field| !is_neutral(field)))
.count()
}
fn nav_chip(ui: &mut egui::Ui, title: &str, count: &str, active: bool) -> egui::Response {
let visuals = ui.visuals().clone();
let painter = ui.painter().clone();
let ink = match active {
true => visuals.text_color(),
false => visuals.weak_text_color(),
};
let word = painter.layout_no_wrap(
title.to_string(),
egui::FontId::proportional(CHIP_TEXT),
ink,
);
let tail = (!count.is_empty()).then(|| {
painter.layout_no_wrap(
count.to_string(),
egui::FontId::monospace(COUNT_TEXT),
match active {
true => app::accent(&visuals),
false => app::caption(&visuals),
},
)
});
let width = 16.0 + word.size().x + tail.as_ref().map_or(0.0, |laid| 5.0 + laid.size().x);
let (rect, response) = ui.allocate_exact_size(egui::vec2(width, CHIP), egui::Sense::click());
if active || response.hovered() {
let fill = match active {
true => visuals.widgets.active.weak_bg_fill,
false => visuals.widgets.hovered.weak_bg_fill,
};
painter.rect_filled(rect, RADIUS, fill);
}
let mut x = rect.left() + 8.0;
painter.galley(
egui::pos2(x, rect.center().y - word.size().y / 2.0),
word.clone(),
ink,
);
x += word.size().x + 5.0;
if let Some(tail) = tail {
painter.galley(
egui::pos2(x, rect.center().y - tail.size().y / 2.0),
tail,
ink,
);
}
response
}
pub fn body(
ui: &mut egui::Ui,
ctx: &Ctx,
state: &mut State,
doc: &Doc<'_>,
piano: &mut PianoLookup,
sets: &mut Sets,
) -> bool {
let mut to_advanced = false;
state.view_top = ui.clip_rect().top();
let mut tops = Vec::with_capacity(doc.sections.len());
if state.lens.is_some() {
banner(ui, state);
}
for section in &doc.sections {
let top = ui.cursor().top();
if state.jump.as_deref() == Some(section.key.as_str()) {
ui.scroll_to_rect(
egui::Rect::from_min_size(ui.cursor().min, egui::vec2(1.0, 1.0)),
Some(egui::Align::TOP),
);
}
tops.push((section.key.clone(), top));
to_advanced |= drew(ui, ctx, state, doc, section, piano, sets);
ui.add_space(6.0);
ui.separator();
}
state.jump = None;
state.tops = tops;
state.active = active(state);
to_advanced |= foot(ui, doc);
to_advanced
}
fn active(state: &State) -> Option<String> {
state
.tops
.iter()
.rfind(|(_, top)| *top <= state.view_top + 1.0)
.or_else(|| state.tops.first())
.map(|(key, _)| key.clone())
}
fn banner(ui: &mut egui::Ui, state: &mut State) {
let Some(slot) = state.lens else { return };
let visuals = ui.visuals().clone();
let drawn = egui::Frame::new()
.fill(visuals.window_fill)
.inner_margin(egui::Margin::symmetric(8, 6))
.show(ui, |ui| {
ui.set_width(ui.available_width());
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
icon(ui, Glyph::ScanEye, 13.0, app::accent(&visuals));
ui.label(
egui::RichText::new(format!(
"Showing {} targets. A lit outline is a control with a morph stored, a \
grey one is neutral, and editing here writes the morph slot rather than \
the panel value.",
SLOTS[slot].1.to_lowercase()
))
.font(egui::FontId::proportional(CHIP_TEXT))
.color(visuals.text_color()),
);
if ui
.small_button("Back to the panel")
.on_hover_text("the values the instrument holds with nothing moving")
.clicked()
{
state.lens = None;
}
});
});
let rule = egui::Stroke::new(1.0_f32, app::accent(&visuals));
let rect = drawn.response.rect;
ui.painter()
.hline(rect.x_range(), rect.bottom() - 0.5, rule);
}
fn drew(
ui: &mut egui::Ui,
ctx: &Ctx,
state: &State,
doc: &Doc<'_>,
section: &Sect<'_>,
piano: &mut PianoLookup,
sets: &mut Sets,
) -> bool {
let quiet = app::caption(ui.visuals());
let reading = match section.count {
1 => "1 field".to_string(),
n => format!("{n} fields"),
};
controls::heading(ui, §ion.title, "", Some((&reading, quiet)));
if section.fields.iter().any(|field| field.path == PIANO_MODEL) {
piano.ui(ui);
}
cells(ui, ctx, state, doc, §ion.fields, piano, sets);
let (alternatives, cards): (Vec<&Sect>, Vec<&Sect>) = section
.nested
.iter()
.partition(|nested| nested.pick.is_some());
for card in cards {
egui::Frame::new()
.fill(ui.visuals().window_fill)
.stroke(egui::Stroke::new(
1.0_f32,
ui.visuals().widgets.noninteractive.bg_stroke.color,
))
.corner_radius(RADIUS)
.inner_margin(egui::Margin::same(8))
.outer_margin(egui::Margin::symmetric(12, 4))
.show(ui, |ui| {
ui.set_width(ui.available_width() - 24.0);
card_title(ui, &card.title, None);
cells(ui, ctx, state, doc, &card.fields, piano, sets);
});
}
if !alternatives.is_empty() {
side_by_side(ui, ctx, state, doc, &alternatives, piano, sets);
}
idle_line(ui, §ion.idle)
}
fn side_by_side(
ui: &mut egui::Ui,
ctx: &Ctx,
state: &State,
doc: &Doc<'_>,
alternatives: &[&Sect<'_>],
piano: &mut PianoLookup,
sets: &mut Sets,
) {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = egui::vec2(10.0, 10.0);
for alternative in alternatives {
let stroke = match alternative.selected {
true => app::accent(ui.visuals()),
false => ui.visuals().widgets.noninteractive.bg_stroke.color,
};
egui::Frame::new()
.fill(ui.visuals().window_fill)
.stroke(egui::Stroke::new(1.0_f32, stroke))
.corner_radius(RADIUS)
.inner_margin(egui::Margin::same(8))
.show(ui, |ui| {
if let Some(selection) = alternative.pick {
if card_title(ui, &alternative.title, Some(alternative.selected))
&& !alternative.selected
{
sets.push((selection.field.to_string(), selection.value.to_string()));
}
}
if !alternative.selected {
ui.set_opacity(0.45);
}
cells(ui, ctx, state, doc, &alternative.fields, piano, sets);
});
}
});
}
fn card_title(ui: &mut egui::Ui, title: &str, playing: Option<bool>) -> bool {
let visuals = ui.visuals().clone();
let mut clicked = false;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 8.0;
if let Some(playing) = playing {
let lit = match playing {
true => app::accent(&visuals),
false => app::unlit(&visuals),
};
clicked |= app::dot(ui, lit, 8.0).clicked();
}
clicked |= ui
.add(
egui::Label::new(
egui::RichText::new(title)
.font(egui::FontId::new(CARD_TITLE, app::bold()))
.color(visuals.text_color()),
)
.sense(match playing.is_some() {
true => egui::Sense::click(),
false => egui::Sense::hover(),
}),
)
.clicked();
match playing {
Some(true) => {
ui.label(
egui::RichText::new("playing")
.font(egui::FontId::proportional(READING))
.color(app::good(&visuals)),
);
}
Some(false) => {
clicked |= ui
.add(
egui::Label::new(
egui::RichText::new("select")
.font(egui::FontId::proportional(READING))
.color(app::caption(&visuals)),
)
.sense(egui::Sense::click()),
)
.on_hover_text("the other stays stored, it is simply not the one playing")
.clicked();
}
None => {}
}
});
ui.add_space(4.0);
clicked
}
fn idle_line(ui: &mut egui::Ui, idle: &[&'static str]) -> bool {
if idle.is_empty() {
return false;
}
let quiet = app::caption(ui.visuals());
let mut asked = false;
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 6.0;
ui.add_space(12.0);
icon(ui, Glyph::EyeOff, 11.0, quiet);
ui.label(
egui::RichText::new(format!(
"{} {} stored but not in use for the state this file holds — kept, not cleared.",
listed(idle),
match idle.len() {
1 => "is",
_ => "are",
}
))
.font(egui::FontId::proportional(READING))
.color(quiet),
);
asked = ui
.add(
egui::Label::new(
egui::RichText::new("Advanced")
.font(egui::FontId::proportional(READING))
.color(app::accent(ui.visuals())),
)
.sense(egui::Sense::click()),
)
.on_hover_text("every field, including the ones this face does not draw")
.clicked();
});
asked
}
fn listed(words: &[&str]) -> String {
match words {
[] => String::new(),
[one] => (*one).to_string(),
[head @ .., last] => format!("{} and {last}", head.join(", ")),
}
}
fn foot(ui: &mut egui::Ui, doc: &Doc<'_>) -> bool {
let quiet = app::caption(ui.visuals());
let mut asked = false;
if !doc.idle.is_empty() {
asked |= idle_line(ui, &doc.idle);
}
let unplaced = doc.leftovers.len();
if matches!(doc.shape, Shape::Authored { exhaustive: false }) && unplaced > 0 {
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing.x = 6.0;
ui.add_space(12.0);
icon(ui, Glyph::CircleAlert, 11.0, quiet);
ui.label(
egui::RichText::new(format!(
"{unplaced} fields the layout does not place — under Advanced, and under \
{} once the strings table names them.",
strings::Section::Other.title()
))
.font(egui::FontId::proportional(READING))
.color(quiet),
);
});
}
asked
}
fn cells(
ui: &mut egui::Ui,
ctx: &Ctx,
state: &State,
doc: &Doc<'_>,
rows: &[&Field],
piano: &mut PianoLookup,
sets: &mut Sets,
) {
let built = clustered(rows, doc);
if built.is_empty() {
return;
}
let row = egui::Layout::left_to_right(egui::Align::TOP).with_main_wrap(true);
ui.with_layout(row, |ui| {
ui.spacing_mut().item_spacing = egui::vec2(14.0, 10.0);
for cell in &built {
match cell {
Cell::One(part) => one(ui, ctx, state, part, rows, piano, sets),
Cell::Register(bars) => register(ui, ctx, state, bars, sets),
Cell::Transpose => transpose(ui, ctx, state, rows, sets),
}
}
});
}
fn clustered<'a>(rows: &[&'a Field], doc: &Doc<'a>) -> Vec<Cell<'a>> {
let mut parts: Vec<Part<'a>> = Vec::new();
let mut transposed = false;
for field in rows {
if doc.picks.contains(field.path.as_str()) {
continue;
}
if field.path == TRANSPOSE_ENABLED || field.path == TRANSPOSE {
transposed = true;
continue;
}
if field
.spec
.morph_parent()
.is_some_and(|parent| doc.morphs.contains_key(parent.as_str()))
{
continue;
}
parts.push(Part {
field,
morphs: doc
.morphs
.get(field.path.as_str())
.copied()
.unwrap_or_default(),
});
}
let mut out = merged(parts);
if transposed {
out.push(Cell::Transpose);
}
out
}
fn merged(parts: Vec<Part<'_>>) -> Vec<Cell<'_>> {
let mut out: Vec<Cell> = Vec::new();
let mut run: Vec<Part> = Vec::new();
for part in parts {
if !fitting(&run, part.field) {
out.extend(run.drain(..).map(Cell::One));
}
match fitting(&run, part.field) {
true => run.push(part),
false => out.push(Cell::One(part)),
}
if run.len() == drawbar_widget::BARS {
out.push(Cell::Register(std::mem::take(&mut run)));
}
}
out.extend(run.into_iter().map(Cell::One));
out
}
fn fitting(run: &[Part<'_>], field: &Field) -> bool {
let Some((stem, rank)) = ranked(field) else {
return false;
};
usize::from(rank) == run.len() + 1
&& run
.first()
.and_then(|first| ranked(first.field))
.is_none_or(|(opened, _)| opened == stem)
}
fn ranked(field: &Field) -> Option<(&str, u8)> {
let ControlKind::Drawbar {
bars: 1,
rank: Some(rank),
..
} = field.spec.control
else {
return None;
};
if !(1..=drawbar_widget::BARS as u8).contains(&rank) {
return None;
}
let (stem, _) = field.path.rsplit_once('_')?;
Some((stem, rank))
}
fn shown<'a>(part: &Part<'a>, lens: Option<usize>) -> Option<&'a Field> {
lens.and_then(|slot| part.morphs[slot])
}
fn one(
ui: &mut egui::Ui,
ctx: &Ctx,
state: &State,
part: &Part<'_>,
rows: &[&Field],
piano: &mut PianoLookup,
sets: &mut Sets,
) {
let lensed = shown(part, state.lens);
let drawn = lensed.unwrap_or(part.field);
let dim = state.lens.is_some() && lensed.is_none();
let legal = ctx.legal(drawn);
if lensed.is_none() && part.field.path == PIANO_MODEL && piano.model_cell(ui, part.field, sets)
{
return;
}
let named = piano.names(drawn);
let span = width(drawn, &legal);
let drawn_at = ui
.allocate_ui(egui::vec2(span, 0.0), |ui| {
if dim {
ui.set_opacity(0.45);
}
ui.vertical_centered(|ui| {
ui.spacing_mut().item_spacing.y = 3.0;
if let Some(value) = control(ui, drawn, &legal, rows, named) {
sets.push((drawn.path.clone(), value));
}
caption(ui, part.field, state.pending.contains(&drawn.path));
if state.lens.is_none() {
dots(ui, &part.morphs);
}
});
})
.response;
if let Some(slot) = lensed {
outline(ui, drawn_at.rect, is_neutral(slot));
}
}
fn outline(ui: &egui::Ui, rect: egui::Rect, neutral: bool) {
let ink = match neutral {
true => app::unlit(ui.visuals()),
false => app::accent(ui.visuals()),
};
ui.painter().rect_stroke(
rect.expand(2.0),
RADIUS,
egui::Stroke::new(1.0_f32, ink),
egui::StrokeKind::Inside,
);
}
fn caption(ui: &mut egui::Ui, field: &Field, edited: bool) {
named_caption(ui, &field.path, edited, note(field));
}
fn named_caption(ui: &mut egui::Ui, path: &str, edited: bool, note: &str) {
let known = strings::known(path);
let quiet = ui.visuals().weak_text_color();
let response = ui
.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
if edited {
app::dot(ui, app::warn(ui.visuals()), DOT);
}
if !known {
icon(ui, Glyph::Tag, 9.0, app::caption(ui.visuals()));
}
ui.add(egui::Label::new(
egui::RichText::new(strings::label(path))
.font(match known {
true => egui::FontId::proportional(LABEL),
false => egui::FontId::monospace(LABEL),
})
.color(quiet),
));
})
.response;
let mut hint = path.to_string();
if !known {
hint.push_str(" — no label yet; showing the prettified path");
}
if !note.is_empty() {
hint.push_str(" · ");
hint.push_str(note);
}
response.on_hover_text(hint);
}
fn note(field: &Field) -> &'static str {
match field.spec.control {
ControlKind::Bipolar(_) => {
"centre is the slot midpoint — accurate at the ends, approximate between"
}
ControlKind::Pattern { .. } => "step order inferred",
ControlKind::Drawbar { .. } => "rank is a position, not a pitch",
ControlKind::Morph { .. } => "a morph slot with no parameter beside it",
_ => "",
}
}
fn dots(ui: &mut egui::Ui, morphs: &[Option<&Field>; SLOTS.len()]) {
if morphs.iter().all(Option::is_none) {
return;
}
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 3.0;
for (nth, (_, word, short)) in SLOTS.iter().enumerate() {
let Some(slot) = morphs[nth] else { continue };
let lit = !is_neutral(slot);
let ink = match lit {
true => app::accent(ui.visuals()),
false => app::unlit(ui.visuals()),
};
let hint = match lit {
true => format!("{word} → {}", slot.value),
false => format!("{word} — neutral"),
};
app::dot(ui, ink, DOT).on_hover_text(hint.clone());
ui.label(
egui::RichText::new(*short)
.font(egui::FontId::proportional(COUNT_TEXT))
.color(app::caption(ui.visuals())),
)
.on_hover_text(hint);
ui.add_space(2.0);
}
});
}
fn neutral(width: u32) -> Option<u64> {
use nord_format::components::MorphOf;
Some(u64::from(match width {
1 => MorphOf::<1>::NEUTRAL,
2 => MorphOf::<2>::NEUTRAL,
3 => MorphOf::<3>::NEUTRAL,
4 => MorphOf::<4>::NEUTRAL,
5 => MorphOf::<5>::NEUTRAL,
6 => MorphOf::<6>::NEUTRAL,
7 => MorphOf::<7>::NEUTRAL,
8 => MorphOf::<8>::NEUTRAL,
_ => return None,
}))
}
fn is_neutral(slot: &Field) -> bool {
word(&slot.value) == neutral(slot.spec.width)
}
fn width(field: &Field, legal: &[String]) -> f32 {
match field.spec.control {
ControlKind::Toggle => 84.0,
ControlKind::Selector => 156.0,
ControlKind::Shift(_) => 100.0,
ControlKind::Drawbar { bars: 1, .. } => 44.0,
ControlKind::Drawbar { .. } => 220.0,
ControlKind::Pattern { steps, .. } => (f32::from(steps) * 13.0).max(110.0),
ControlKind::Reference(_) => 176.0,
_ if legal.is_empty() => 168.0,
ControlKind::Knob(_) | ControlKind::Bipolar(_) => 78.0,
ControlKind::Morph { .. } | ControlKind::Number => match legal.len() <= MENU_MAX {
true => 156.0,
false => 78.0,
},
}
}
fn control(
ui: &mut egui::Ui,
field: &Field,
legal: &[String],
rows: &[&Field],
named: Option<&str>,
) -> Option<String> {
match field.spec.control {
ControlKind::Drawbar { bars: 1, rank, .. } => bar(ui, field, rank),
ControlKind::Drawbar { order, .. } => packed(ui, field, order),
ControlKind::Pattern {
steps,
bits_per_step,
order,
} => pattern(ui, field, steps, bits_per_step, order),
ControlKind::Reference(library) => reference(ui, field, library, named),
_ if legal.is_empty() => wide(ui, field),
ControlKind::Toggle => toggle(ui, field, legal),
ControlKind::Selector => selector(ui, field, legal),
ControlKind::Knob(unit) => turned(ui, field, legal, unit, false, rows),
ControlKind::Bipolar(unit) => turned(ui, field, legal, unit, true, rows),
ControlKind::Shift(unit) => shift(ui, field, legal, unit),
ControlKind::Morph { .. } | ControlKind::Number => plain(ui, field, legal),
}
}
fn toggle(ui: &mut egui::Ui, field: &Field, legal: &[String]) -> Option<String> {
let named = match legal {
[off, on] if off != "false" || on != "true" => Some((off.as_str(), on.as_str())),
_ => None,
};
let (off, on) = named.unwrap_or(("off", "on"));
let lit = match named {
Some((_, on)) => field.value == on,
None => field.value == "true",
};
let word = match lit {
true => on,
false => off,
};
let want = led::ui(ui, lit, word)?;
Some(match named {
Some((off, on)) => match want {
true => on.to_string(),
false => off.to_string(),
},
None => want.to_string(),
})
}
fn selector(ui: &mut egui::Ui, field: &Field, legal: &[String]) -> Option<String> {
let mut picked = None;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
if strings::unrecognised(&field.value).is_some() {
icon(ui, Glyph::CircleHelp, 11.0, app::warn(ui.visuals()))
.on_hover_text("the panel cannot produce this position, and the file holds it");
}
egui::ComboBox::from_id_salt(&field.path)
.selected_text(
egui::RichText::new(strings::value_label(&field.path, &field.value))
.text_style(egui::TextStyle::Small),
)
.width(ui.available_width().min(128.0))
.show_ui(ui, |ui| {
for value in offered(&field.path, legal, &field.value) {
let word = strings::value_label(&field.path, &value);
if ui.selectable_label(value == field.value, word).clicked() {
picked = Some(value);
}
}
});
});
picked.filter(|value| *value != field.value)
}
fn offered(path: &str, legal: &[String], current: &str) -> Vec<String> {
let mut out: Vec<String> = legal
.iter()
.filter(|value| offerable(path, value))
.cloned()
.collect();
if !out.iter().any(|value| value == current) {
out.push(current.to_string());
}
out
}
fn offerable(path: &str, value: &str) -> bool {
!(value == "Unknown"
&& matches!(
path,
"effects_panel.fx1" | "effects_panel.fx2" | "effects_panel.fx3" | "effects_panel.fx4"
))
}
fn turned(
ui: &mut egui::Ui,
field: &Field,
legal: &[String],
unit: Unit,
centred: bool,
rows: &[&Field],
) -> Option<String> {
let Some((min, max)) = contiguous(legal) else {
return plain(ui, field, legal);
};
let value: i64 = field.value.trim_start_matches('+').parse().ok()?;
let mut moved = None;
let dial = ui
.scope(|ui| moved = knob::ui(ui, &field.path, value, min, max))
.response;
if centred {
detent(ui, dial.rect);
}
let shown = moved
.as_deref()
.and_then(|spelled| spelled.parse().ok())
.unwrap_or(value);
match reading(unit, centred, shown, min, max) {
Some(text) => {
ui.label(
egui::RichText::new(text)
.font(egui::FontId::monospace(READING))
.color(ui.visuals().weak_text_color()),
);
}
None => {
if let Some(word) = scale(unit) {
let hint = clocked(field, rows);
let drawn = ui.label(
egui::RichText::new(word)
.font(egui::FontId::proportional(READING))
.color(app::warn(ui.visuals())),
);
match hint {
Some(sibling) => drawn.on_hover_text(sibling),
None => drawn.on_hover_text(
"the panel's curve for this unit is not published, so the stored value \
is what is shown",
),
};
}
}
}
moved
}
fn detent(ui: &egui::Ui, dial: egui::Rect) {
let top = egui::pos2(dial.center().x, dial.top());
ui.painter().line_segment(
[top, egui::pos2(top.x, top.y + 5.0)],
egui::Stroke::new(1.0_f32, app::caption(ui.visuals())),
);
}
fn reading(unit: Unit, centred: bool, value: i64, min: i64, max: i64) -> Option<String> {
if centred {
let centre = (min + max + 1) / 2;
return Some(format!("{:+}", value - centre));
}
match (unit.describes_a_known_transform(), unit) {
(true, Unit::Panel10) if max > min => Some(format!(
"{:.1}",
(value - min) as f64 / (max - min) as f64 * 10.0
)),
_ => None,
}
}
fn scale(unit: Unit) -> Option<&'static str> {
match unit {
Unit::Hertz => Some("Hz scale"),
Unit::Milliseconds => Some("ms scale"),
Unit::Bpm => Some("BPM scale"),
Unit::ClockDivision => Some("division"),
Unit::Pan => Some("stored"),
_ => None,
}
}
fn clocked(field: &Field, rows: &[&Field]) -> Option<String> {
if !matches!(field.spec.control, ControlKind::Knob(Unit::ClockDivision)) {
return None;
}
let stem = field.path.rsplit_once('_').map(|(head, _)| head)?;
let sibling = rows
.iter()
.find(|other| other.path.starts_with(stem) && other.path.ends_with("_clock"))?;
Some(format!(
"reads as a clock division or a rate depending on {}",
sibling.path
))
}
fn shift(ui: &mut egui::Ui, field: &Field, legal: &[String], unit: Unit) -> Option<String> {
let Some((min, max)) = contiguous(legal) else {
return plain(ui, field, legal);
};
let value: i64 = field.value.trim_start_matches('+').parse().ok()?;
let word = match unit {
Unit::Semitones => "st",
Unit::Octaves => "oct",
_ => "",
};
let mut moved = None;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 3.0;
if ui
.add_enabled(value > min, egui::Button::new("−").small())
.clicked()
{
moved = Some(value - 1);
}
ui.label(
egui::RichText::new(format!("{value:+}"))
.font(egui::FontId::monospace(11.5))
.color(ui.visuals().text_color()),
);
if ui
.add_enabled(value < max, egui::Button::new("+").small())
.clicked()
{
moved = Some(value + 1);
}
if !word.is_empty() {
ui.label(
egui::RichText::new(word)
.font(egui::FontId::proportional(READING))
.color(app::caption(ui.visuals())),
);
}
});
moved.map(|moved| moved.to_string())
}
fn bar(ui: &mut egui::Ui, field: &Field, rank: Option<u8>) -> Option<String> {
let position = field.value.trim().parse().ok()?;
let rank = rank
.and_then(|rank| usize::from(rank).checked_sub(1))
.filter(|rank| *rank < drawbar_widget::BARS);
drawbar_widget::ui_one(ui, rank, position, true).map(|moved| moved.to_string())
}
fn packed(ui: &mut egui::Ui, field: &Field, order: PackedOrder) -> Option<String> {
let bits = drawbar_widget::parse(&field.value)?;
let stored = drawbar_widget::bars(bits);
let shown = match order {
PackedOrder::HighFirst => stored,
PackedOrder::LowFirst => mirrored(stored),
};
let moved = bars(ui, shown)?;
let back = match order {
PackedOrder::HighFirst => moved,
PackedOrder::LowFirst => mirrored(moved),
};
Some(drawbar_widget::spell(drawbar_widget::written(bits, back)?))
}
fn mirrored(positions: [u8; drawbar_widget::BARS]) -> [u8; drawbar_widget::BARS] {
let mut out = positions;
out.reverse();
out
}
fn bars(
ui: &mut egui::Ui,
positions: [u8; drawbar_widget::BARS],
) -> Option<[u8; drawbar_widget::BARS]> {
let mut moved = None;
ui.vertical(|ui| {
moved = drawbar_widget::ui_ranks(ui, positions, true, &drawbar_widget::ALL_RANKS);
ui.label(
egui::RichText::new(drawbar_widget::digits(&moved.unwrap_or(positions)))
.font(egui::FontId::monospace(READING))
.color(ui.visuals().weak_text_color()),
);
});
moved
}
fn register(ui: &mut egui::Ui, ctx: &Ctx, state: &State, run: &[Part<'_>], sets: &mut Sets) {
if let Some(slot) = state.lens {
for (nth, part) in run.iter().enumerate() {
let Some(target) = part.morphs[slot] else {
continue;
};
let legal = ctx.legal(target);
let at = ui
.allocate_ui(egui::vec2(64.0, 0.0), |ui| {
ui.vertical_centered(|ui| {
ui.spacing_mut().item_spacing.y = 3.0;
if let Some(value) = plain(ui, target, &legal) {
sets.push((target.path.clone(), value));
}
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 4.0;
if state.pending.contains(&target.path) {
app::dot(ui, app::warn(ui.visuals()), DOT);
}
ui.label(
egui::RichText::new(format!("bar {}", nth + 1))
.font(egui::FontId::proportional(LABEL))
.color(ui.visuals().weak_text_color()),
);
});
});
})
.response;
outline(ui, at.rect, is_neutral(target));
}
return;
}
let positions: [u8; drawbar_widget::BARS] = std::array::from_fn(|n| {
run.get(n)
.and_then(|part| part.field.value.trim().parse().ok())
.unwrap_or(0)
});
let edited = run
.iter()
.any(|part| state.pending.contains(&part.field.path));
ui.allocate_ui(egui::vec2(220.0, 0.0), |ui| {
ui.vertical_centered(|ui| {
ui.spacing_mut().item_spacing.y = 3.0;
if let Some(moved) = bars(ui, positions) {
sets.extend(bar_sets(run, &positions, &moved));
}
let stem = ranked(run[0].field).map_or(run[0].field.path.as_str(), |(stem, _)| stem);
named_caption(ui, stem, edited, "rank is a position, not a pitch");
let any: [Option<&Field>; SLOTS.len()] = std::array::from_fn(|slot| {
run.iter()
.find_map(|part| part.morphs[slot].filter(|slot| !is_neutral(slot)))
.or_else(|| run.iter().find_map(|part| part.morphs[slot]))
});
dots(ui, &any);
});
});
}
fn bar_sets(
run: &[Part<'_>],
was: &[u8; drawbar_widget::BARS],
now: &[u8; drawbar_widget::BARS],
) -> Sets {
run.iter()
.zip(was)
.zip(now)
.filter(|((_, was), now)| was != now)
.map(|((part, _), now)| (part.field.path.clone(), now.to_string()))
.collect()
}
fn step_bits(step: usize, steps: u8, bits_per_step: u8, order: PackedOrder) -> Option<(u32, u64)> {
let last = usize::from(steps).checked_sub(1)?;
if step > last {
return None;
}
let nth = match order {
PackedOrder::LowFirst => step,
PackedOrder::HighFirst => last - step,
};
let width = u32::from(bits_per_step);
let shift = width.checked_mul(u32::try_from(nth).ok()?)?;
let mask = u64::MAX.checked_shr(u64::BITS.checked_sub(width)?)?;
(shift.checked_add(width)? <= u64::BITS).then_some((shift, mask))
}
fn stepped(
stored: u64,
step: usize,
steps: u8,
bits_per_step: u8,
order: PackedOrder,
) -> Option<u64> {
let (shift, mask) = step_bits(step, steps, bits_per_step, order)?;
let next = ((stored >> shift) & mask).wrapping_add(1) & mask;
Some((stored & !(mask << shift)) | (next << shift))
}
fn pattern(
ui: &mut egui::Ui,
field: &Field,
steps: u8,
bits_per_step: u8,
order: PackedOrder,
) -> Option<String> {
let stored = word(&field.value)?;
let mut moved = None;
ui.horizontal_wrapped(|ui| {
ui.spacing_mut().item_spacing = egui::vec2(2.0, 2.0);
for step in 0..usize::from(steps) {
let Some((shift, mask)) = step_bits(step, steps, bits_per_step, order) else {
continue;
};
let held = (stored >> shift) & mask;
let (rect, response) =
ui.allocate_exact_size(egui::vec2(11.0, 14.0), egui::Sense::click());
let ink = match held {
0 => app::unlit(ui.visuals()),
_ => app::accent(ui.visuals()),
};
ui.painter().rect_filled(rect, RADIUS, ink);
if response
.on_hover_text(format!("step {} — {held} · step order inferred", step + 1))
.clicked()
{
moved = stepped(stored, step, steps, bits_per_step, order);
}
}
});
moved.map(|bits| format!("{bits:#x}"))
}
fn reference(
ui: &mut egui::Ui,
field: &Field,
library: nord_format::fields::Library,
named: Option<&str>,
) -> Option<String> {
let quiet = app::caption(ui.visuals());
ui.vertical_centered(|ui| {
ui.spacing_mut().item_spacing.y = 2.0;
let (text, ink, under) = match named {
Some(name) => (
name.to_string(),
ui.visuals().text_color(),
format!("{} {}", library.label(), field.value),
),
None => (
field.value.clone(),
app::warn(ui.visuals()),
format!("{} — not in the library", library.label()),
),
};
ui.label(
egui::RichText::new(text)
.font(egui::FontId::monospace(11.5))
.color(ink),
)
.on_hover_text("the file stores the id; only the instrument knows the name");
ui.label(
egui::RichText::new(under)
.font(egui::FontId::proportional(LABEL))
.color(quiet),
);
});
None
}
fn plain(ui: &mut egui::Ui, field: &Field, legal: &[String]) -> Option<String> {
if legal.len() <= MENU_MAX && !legal.is_empty() {
return selector(ui, field, legal);
}
let Some((min, max)) = contiguous(legal) else {
return selector(ui, field, legal);
};
let value: i64 = field.value.trim_start_matches('+').parse().ok()?;
knob::ui(ui, &field.path, value, min, max)
}
fn wide(ui: &mut egui::Ui, field: &Field) -> Option<String> {
let id = ui.id().with(("wide", field.path.as_str()));
let held: Option<String> = ui.data(|data| data.get_temp(id));
let mut text = held.clone().unwrap_or_else(|| field.value.clone());
let response = ui.add(
egui::TextEdit::singleline(&mut text)
.id(id)
.desired_width(150.0)
.font(egui::FontId::monospace(11.0)),
);
if response.ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
ui.data_mut(|data| data.remove::<String>(id));
return None;
}
let entered = response.ctx.input(|i| i.key_pressed(egui::Key::Enter));
if response.has_focus() && !entered {
ui.data_mut(|data| data.insert_temp(id, text));
return None;
}
held.as_ref()?;
ui.data_mut(|data| data.remove::<String>(id));
(text.trim() != field.value).then(|| text.trim().to_string())
}
fn transpose(ui: &mut egui::Ui, ctx: &Ctx, state: &State, rows: &[&Field], sets: &mut Sets) {
let held = |path: &str| rows.iter().find(|field| field.path == path);
let (Some(lamp), Some(amount)) = (held(TRANSPOSE_ENABLED), held(TRANSPOSE)) else {
return;
};
let Some((least, most)) = contiguous(&ctx.legal(amount)) else {
return;
};
let on = lamp.value == "true";
let Some(semitones) = amount.value.trim_start_matches('+').parse::<i64>().ok() else {
return;
};
let edited = state.pending.contains(&lamp.path) || state.pending.contains(&amount.path);
let mut switched = None;
let mut moved = None;
ui.allocate_ui(egui::vec2(120.0, 0.0), |ui| {
ui.vertical_centered(|ui| {
ui.spacing_mut().item_spacing.y = 3.0;
ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 6.0;
switched = led::ui(ui, on, "");
moved = knob::ui(ui, TRANSPOSE, semitones, least, most);
});
caption(ui, lamp, edited);
});
})
.response
.on_hover_text("two fields, one control: the lamp and the semitones move together");
let (on, semitones) = match (switched, moved) {
(_, Some(want)) => (true, want),
(Some(want_on), None) => (want_on, semitones.to_string()),
(None, None) => return,
};
sets.push((TRANSPOSE_ENABLED.to_string(), on.to_string()));
sets.push((TRANSPOSE.to_string(), semitones));
}
pub fn about(doc: &Doc<'_>, entity: &LocalEntity) -> Vec<(&'static str, String, String)> {
let (fields, slots) = doc.tally();
let (badge, format) = super::header::badge(entity);
let layout = match doc.shape() {
Shape::Authored { exhaustive: true } => (
"authored — exhaustive".to_string(),
"every field the body declares is placed".to_string(),
),
Shape::Authored { exhaustive: false } => {
let (unplaced, named) = doc.unplaced();
(
"authored".to_string(),
format!(
"{unplaced} fields no group names; {named} of them show under {}",
strings::Section::Other.title()
),
)
}
Shape::Menus => (
"menus".to_string(),
"this app's own table, in the order the instrument's menus run".to_string(),
),
Shape::Flat => (
"flat, registry order".to_string(),
"nothing knows how this panel is divided".to_string(),
),
};
vec![
("Format", badge, format),
(
"Fields",
fields.to_string(),
match slots {
0 => "no morph slots in this body".to_string(),
n => format!("{n} of them morph slots"),
},
),
("Layout", layout.0, layout.1),
("Stored at", super::header::lives(entity), String::new()),
(
"Instrument",
nord_format::accept::Family::of_tag(&entity.tag())
.map(|family| family.label().to_string())
.unwrap_or_else(|| "unknown".to_string()),
String::new(),
),
]
}
pub fn kind_word(field: &Field) -> String {
match field.spec.control {
ControlKind::Toggle => "toggle".to_string(),
ControlKind::Selector => "selector".to_string(),
ControlKind::Knob(unit) => format!("knob {}", unit_word(unit)),
ControlKind::Bipolar(unit) => format!("bipolar {}", unit_word(unit)),
ControlKind::Shift(unit) => format!("shift {}", unit_word(unit)),
ControlKind::Drawbar { bars: 1, rank, .. } => match rank {
Some(rank) => format!("drawbar {rank}"),
None => "drawbar".to_string(),
},
ControlKind::Drawbar { bars, .. } => format!("{bars} drawbars"),
ControlKind::Pattern { steps, .. } => format!("{steps} steps"),
ControlKind::Reference(library) => format!("{} id", library.label()),
ControlKind::Morph { .. } => "morph".to_string(),
ControlKind::Number => "number".to_string(),
}
}
fn unit_word(unit: Unit) -> &'static str {
match unit {
Unit::Panel10 => "0-10",
Unit::Decibels => "dB",
Unit::Milliseconds => "ms",
Unit::Hertz => "Hz",
Unit::Bpm => "BPM",
Unit::ClockDivision => "division",
Unit::Semitones => "st",
Unit::Octaves => "oct",
Unit::Pan => "pan",
Unit::None => "",
}
}
fn word(value: &str) -> Option<u64> {
let text = value.trim();
match text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
Some(hex) => u64::from_str_radix(hex, 16).ok(),
None => text.parse().ok(),
}
}
fn contiguous(legal: &[String]) -> Option<(i64, i64)> {
let mut values = Vec::with_capacity(legal.len());
for value in legal {
values.push(value.trim_start_matches('+').parse::<i64>().ok()?);
}
let min = *values.iter().min()?;
let max = *values.iter().max()?;
(max.checked_sub(min)? + 1 == values.len() as i64 && min < max).then_some((min, max))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::fields::apply;
use crate::workspace::Fresh;
use nord_format::formats::{ne5, ns4};
use nord_format::{Entity, Program};
fn electro5() -> (Vec<u8>, Vec<Field>) {
let entity = Entity::Program(Program::Electro5(ne5::program::new(
(0, 0).try_into().unwrap(),
)));
let bytes = nord_format::to_bytes(&entity).unwrap();
let fields = apply(&bytes, &[]).unwrap().0;
(bytes, fields)
}
#[test]
fn only_a_gapless_run_of_integers_is_travel() {
let full: Vec<String> = (0..128).map(|n| n.to_string()).collect();
assert_eq!(contiguous(&full), Some((0, 127)));
assert_eq!(contiguous(&["0".into(), "1".into(), "9".into()]), None);
assert_eq!(contiguous(&["Organ".into(), "Piano".into()]), None);
assert_eq!(contiguous(&["3".into()]), None);
}
#[test]
fn a_reading_appears_only_where_the_unit_supports_one() {
assert_eq!(
reading(Unit::Panel10, false, 96, 0, 127),
Some("7.6".to_string())
);
assert_eq!(reading(Unit::Hertz, false, 71, 0, 127), None);
assert_eq!(reading(Unit::Milliseconds, false, 12, 0, 127), None);
assert_eq!(
reading(Unit::Decibels, true, 70, 0, 127),
Some("+6".to_string())
);
assert_eq!(scale(Unit::Hertz), Some("Hz scale"));
assert_eq!(scale(Unit::Panel10), None);
}
#[test]
fn a_morph_slots_neutral_follows_the_width_the_library_declares() {
assert_eq!(neutral(8), Some(127));
assert_eq!(neutral(5), Some(15));
assert_eq!(neutral(3), Some(3));
assert_eq!(neutral(9), None);
}
#[test]
fn the_idle_line_names_what_is_stored_and_not_played() {
assert_eq!(listed(&["Vox"]), "Vox");
assert_eq!(listed(&["Vox", "Farfisa"]), "Vox and Farfisa");
assert_eq!(listed(&["Vox", "Farfisa", "Pipe"]), "Vox, Farfisa and Pipe");
}
#[test]
fn a_drawbar_is_a_register_or_a_bar_by_what_its_kind_counts() {
let (_, electro5) = electro5();
let packed = electro5
.iter()
.find(|field| field.path == "organ_panel.vox_preset1_drawbars")
.expect("the Vox register");
assert!(matches!(
packed.spec.control,
ControlKind::Drawbar { bars: 9, .. }
));
assert!(ranked(packed).is_none());
let (stage4, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let bar = stage4
.iter()
.find(|field| field.path == "organ_a.drawbar_1")
.expect("the first bar");
assert_eq!(ranked(bar).map(|(_, rank)| rank), Some(1));
}
#[test]
fn nine_ranked_bars_merge_into_one_register() {
let (fields, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let doc = Doc {
sections: Vec::new(),
leftovers: Vec::new(),
idle: Vec::new(),
shape: Shape::Flat,
shown: HashSet::new(),
picks: HashSet::new(),
morphs: slots_of(&fields),
fields: 0,
slots: 0,
};
let rows: Vec<&Field> = fields
.iter()
.filter(|field| ranked(field).is_some() && field.path.starts_with("organ_a."))
.take(drawbar_widget::BARS)
.collect();
assert_eq!(rows.len(), drawbar_widget::BARS);
let built = clustered(&rows, &doc);
assert_eq!(built.len(), 1);
assert!(matches!(built.first(), Some(Cell::Register(run)) if run.len() == 9));
let short = clustered(&rows[..4], &doc);
assert_eq!(short.len(), 4);
assert!(short.iter().all(|cell| matches!(cell, Cell::One(_))));
}
#[test]
fn a_morph_slot_is_drawn_on_the_parameter_it_moves() {
let (fields, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let morphs = slots_of(&fields);
let slots = morphs
.get("organ_a_volume")
.expect("the volume knob is morphed");
let named: Vec<&str> = slots
.iter()
.filter_map(|slot| slot.map(|field| field.path.as_str()))
.collect();
assert_eq!(
named,
[
"organ_a_volume_wheel",
"organ_a_volume_aftertouch",
"organ_a_volume_ctrl_pedal",
]
);
assert!(!morphs.contains_key("organ_a_volume_wheel"));
}
#[test]
fn a_slot_with_no_parameter_beside_it_still_gets_a_cell() {
let (fields, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let mut morphs = slots_of(&fields);
morphs.remove("organ_a_volume");
let doc = Doc {
sections: Vec::new(),
leftovers: Vec::new(),
idle: Vec::new(),
shape: Shape::Flat,
shown: HashSet::new(),
picks: HashSet::new(),
morphs,
fields: 0,
slots: 0,
};
let rows: Vec<&Field> = fields
.iter()
.filter(|field| field.path == "organ_a_volume_wheel")
.collect();
let built = clustered(&rows, &doc);
assert_eq!(built.len(), 1);
}
#[test]
fn a_picker_offers_every_position_but_a_second_spelling_of_off() {
let legal: Vec<String> = ["B3", "B3Bass", "Pipe", "unknown (6)"]
.iter()
.map(|value| value.to_string())
.collect();
assert_eq!(
offered("center_panel.organ_type", &legal, "B3"),
["B3", "B3Bass", "Pipe", "unknown (6)"]
);
let routing: Vec<String> = ["Off", "Unknown", "Lower", "Upper"]
.iter()
.map(|value| value.to_string())
.collect();
assert_eq!(
offered("effects_panel.fx1", &routing, "Off"),
["Off", "Lower", "Upper"]
);
assert_eq!(
offered("effects_panel.fx1", &routing, "Unknown"),
["Off", "Lower", "Upper", "Unknown"]
);
assert!(offerable("some_other_field", "Unknown"));
}
#[test]
fn the_electro5_document_is_the_librarys_layout() {
let (bytes, fields) = electro5();
let decoded =
nord_format::from_stream(&mut std::io::Cursor::new(&bytes)).expect("it decodes");
let doc = of(&decoded, &fields);
assert_eq!(doc.shape(), Shape::Authored { exhaustive: false });
let titles: Vec<&str> = doc
.sections
.iter()
.map(|section| section.title.as_str())
.collect();
assert!(titles.contains(&"Keyboard & split"), "{titles:?}");
assert!(titles.contains(&"Organ"), "{titles:?}");
assert!(!titles.contains(&"Piano"), "{titles:?}");
assert!(doc.idle.contains(&"Piano"), "{:?}", doc.idle);
let (unplaced, named) = doc.unplaced();
assert!(unplaced > 0);
assert!(named <= unplaced, "{named} named of {unplaced} unplaced");
}
#[test]
fn the_layout_line_counts_what_also_stored_will_hold() {
let (bytes, fields) = electro5();
let decoded =
nord_format::from_stream(&mut std::io::Cursor::new(&bytes)).expect("it decodes");
let doc = of(&decoded, &fields);
let (unplaced, named) = doc.unplaced();
let also = doc
.sections
.iter()
.find(|section| section.title == strings::Section::Other.title())
.expect("the named leftovers have a section");
assert_eq!(also.fields.len(), named);
assert!(named < unplaced, "{named} of {unplaced} are named");
}
#[test]
fn the_transpose_pair_stays_in_one_group() {
let (_, fields) = electro5();
let resolved = ne5::program::PANEL.resolve(&fields);
let keyboard = resolved
.sections
.iter()
.find(|section| section.group.title == "Keyboard & split")
.expect("the keyboard section");
let paths: Vec<&str> = keyboard
.fields
.iter()
.map(|field| field.path.as_str())
.collect();
assert!(paths.contains(&TRANSPOSE_ENABLED));
assert!(paths.contains(&TRANSPOSE));
}
#[test]
fn a_stage4_program_opens_every_section_it_has() {
let bytes = Fresh::Stage4Program.bytes().unwrap();
let (fields, _) = apply(&bytes, &[]).unwrap();
let decoded =
nord_format::from_stream(&mut std::io::Cursor::new(&bytes)).expect("it decodes");
let doc = of(&decoded, &fields);
assert!(!doc.sections.is_empty());
assert!(fields.len() > 800, "{} fields", fields.len());
let (all, slots) = doc.tally();
assert_eq!(all, fields.len());
assert!(slots > 300, "{slots} morph slots");
assert!(ns4::program::PANEL.resolve(&fields).sections.len() > 1);
}
fn kind_key(field: &Field) -> &'static str {
match field.spec.control {
ControlKind::Toggle => "toggle",
ControlKind::Selector => "selector",
ControlKind::Knob(_) => "knob",
ControlKind::Bipolar(_) => "bipolar",
ControlKind::Shift(_) => "shift",
ControlKind::Drawbar { bars: 1, .. } => "bar",
ControlKind::Drawbar { .. } => "register",
ControlKind::Pattern { .. } => "pattern",
ControlKind::Reference(_) => "reference",
ControlKind::Morph { .. } => "morph",
ControlKind::Number => "number",
}
}
fn headless() -> egui::RawInput {
egui::RawInput {
screen_rect: Some(egui::Rect::from_min_size(
egui::Pos2::ZERO,
egui::vec2(900.0, 540.0),
)),
..Default::default()
}
}
fn click(at: egui::Pos2) -> Vec<egui::Event> {
let button = |pressed| egui::Event::PointerButton {
pos: at,
button: egui::PointerButton::Primary,
pressed,
modifiers: egui::Modifiers::default(),
};
vec![egui::Event::PointerMoved(at), button(true), button(false)]
}
fn circles(output: &egui::FullOutput, ink: egui::Color32) -> usize {
fn count(shape: &egui::Shape, ink: egui::Color32) -> usize {
match shape {
egui::Shape::Circle(drawn) => usize::from(drawn.fill == ink),
egui::Shape::Vec(shapes) => shapes.iter().map(|shape| count(shape, ink)).sum(),
_ => 0,
}
}
output
.shapes
.iter()
.map(|clipped| count(&clipped.shape, ink))
.sum()
}
fn lookup() -> PianoLookup {
PianoLookup {
id: None,
name: None,
can_ask: false,
asked: false,
models: Vec::new(),
scan_disagrees: None,
}
}
fn drawn(field: &Field) -> usize {
fn count(shape: &egui::Shape) -> usize {
match shape {
egui::Shape::Vec(shapes) => shapes.iter().map(count).sum(),
_ => 1,
}
}
let ctx = egui::Context::default();
ctx.set_fonts(crate::app::fonts());
ctx.all_styles_mut(crate::app::metrics);
let legal = (field.spec.legal)();
let output = ctx.run(headless(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
control(ui, field, &legal, &[], None);
});
});
output
.shapes
.iter()
.map(|clipped| count(&clipped.shape))
.sum()
}
#[test]
fn every_control_kind_the_registry_declares_paints_a_control() {
let (_, electro5) = electro5();
let (stage4, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let (stage2, _) = apply(&Fresh::Stage2Program.bytes().unwrap(), &[]).unwrap();
let mut seen: Vec<&'static str> = Vec::new();
for field in stage4.iter().chain(&electro5).chain(&stage2) {
let kind = kind_key(field);
if seen.contains(&kind) {
continue;
}
seen.push(kind);
assert!(drawn(field) > 0, "{kind} ({}) painted nothing", field.path);
}
seen.sort_unstable();
assert_eq!(
seen,
[
"bar",
"bipolar",
"knob",
"morph",
"number",
"pattern",
"reference",
"register",
"selector",
"shift",
"toggle",
],
);
let wide = stage2
.iter()
.find(|field| {
(field.spec.legal)().is_empty() && matches!(field.spec.control, ControlKind::Number)
})
.expect("the Stage 2 declares a wide unclassified field");
assert!(wide.spec.width > nord_format::fields::ENUMERABLE_BITS);
assert!(wide.value.starts_with("0x"), "{}", wide.value);
assert!(drawn(wide) > 0);
}
#[test]
fn an_edit_under_the_lens_writes_the_morph_slot() {
let (fields, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let morphs = slots_of(&fields);
let part = Part {
field: fields
.iter()
.find(|field| field.path == "organ_a_volume")
.expect("the volume knob"),
morphs: morphs["organ_a_volume"],
};
assert!(
shown(&part, None).is_none(),
"the panel writes the panel value"
);
assert_eq!(
shown(&part, Some(0)).map(|field| field.path.as_str()),
Some("organ_a_volume_wheel"),
);
assert_eq!(
shown(&part, Some(2)).map(|field| field.path.as_str()),
Some("organ_a_volume_ctrl_pedal"),
);
let bare = Part {
field: fields
.iter()
.find(|field| field.path == "split_enabled")
.expect("the split switch"),
morphs: Default::default(),
};
assert!(shown(&bare, Some(0)).is_none());
}
#[test]
fn a_packed_register_is_read_from_the_end_its_field_names() {
let stored = drawbar_widget::bars(0x8_8880_0000);
assert_eq!(stored, [8, 8, 8, 8, 0, 0, 0, 0, 0]);
assert_eq!(mirrored(stored), [0, 0, 0, 0, 0, 8, 8, 8, 8]);
assert_eq!(mirrored(mirrored(stored)), stored);
let (_, electro5) = electro5();
let register = electro5
.iter()
.find(|field| field.path == "organ_panel.vox_preset1_drawbars")
.expect("the Vox register");
assert!(matches!(
register.spec.control,
ControlKind::Drawbar {
order: PackedOrder::HighFirst,
..
}
));
}
#[test]
fn a_slot_is_shown_where_the_parameter_it_moves_is_placed() {
let playing = [
("organ_section_enabled".to_string(), "true".to_string()),
("organ_a_layer_enabled".to_string(), "true".to_string()),
];
let (_, bytes) = apply(&Fresh::Stage4Program.bytes().unwrap(), &playing).unwrap();
let (fields, _) = apply(&bytes, &[]).unwrap();
let decoded =
nord_format::from_stream(&mut std::io::Cursor::new(&bytes)).expect("it decodes");
let doc = of(&decoded, &fields);
assert!(doc.shows("organ_a_volume"), "the organ layer is placed");
assert!(doc.shows("organ_a_volume_wheel"));
let mut ridden = 0;
for field in &fields {
let Some(parent) = field.spec.morph_parent() else {
continue;
};
if !doc.shows(&parent) {
continue;
}
ridden += 1;
assert!(
doc.shows(&field.path),
"{} rides on {parent}, which is drawn",
field.path,
);
}
assert!(ridden > 20, "{ridden} slots ride on a drawn parameter");
}
#[test]
fn the_edited_dot_follows_the_field_the_cell_writes() {
let (fields, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let morphs = slots_of(&fields);
let part = Part {
field: fields
.iter()
.find(|field| field.path == "organ_a_volume")
.expect("the volume knob"),
morphs: morphs["organ_a_volume"],
};
let dots = |pending: &str, lens: Option<usize>| -> usize {
let ctx = egui::Context::default();
ctx.set_fonts(crate::app::fonts());
ctx.all_styles_mut(crate::app::metrics);
let mut state = State {
pending: vec![pending.to_string()],
..Default::default()
};
if let Some(slot) = lens {
state.pretend_lens(slot);
}
let read = Ctx::default();
let mut piano = lookup();
let mut sets = Sets::new();
let ink = app::warn(&ctx.style().visuals);
let output = ctx.run(headless(), |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
one(ui, &read, &state, &part, &[], &mut piano, &mut sets);
});
});
circles(&output, ink)
};
assert_eq!(
dots("organ_a_volume_wheel", Some(0)),
1,
"the slot it writes"
);
assert_eq!(dots("organ_a_volume", Some(0)), 0, "not the panel value");
assert_eq!(
dots("organ_a_volume", None),
1,
"off the lens, the parameter"
);
}
#[test]
fn a_wide_field_commits_on_enter_or_blur_and_drops_what_escape_typed() {
const TYPED: &str = "0xfeed";
let (stage2, _) = apply(&Fresh::Stage2Program.bytes().unwrap(), &[]).unwrap();
let field = stage2
.iter()
.find(|field| {
(field.spec.legal)().is_empty() && matches!(field.spec.control, ControlKind::Number)
})
.expect("the Stage 2 declares a wide unclassified field");
assert_ne!(field.value, TYPED);
fn exit(field: &Field, focused: bool, events: Vec<egui::Event>) -> (Option<String>, bool) {
let ctx = egui::Context::default();
ctx.set_fonts(crate::app::fonts());
let mut got = None;
let mut held = false;
for events in [Vec::new(), events] {
let input = egui::RawInput {
events,
..headless()
};
let _ = ctx.run(input, |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
let id = ui.id().with(("wide", field.path.as_str()));
ui.data_mut(|data| data.insert_temp(id, TYPED.to_string()));
if focused {
ui.memory_mut(|memory| memory.request_focus(id));
}
got = wide(ui, field);
held = ui.data(|data| data.get_temp::<String>(id).is_some());
});
});
}
(got, held)
}
fn key(key: egui::Key) -> Vec<egui::Event> {
vec![egui::Event::Key {
key,
physical_key: None,
pressed: true,
repeat: false,
modifiers: egui::Modifiers::default(),
}]
}
assert_eq!(
exit(field, true, Vec::new()),
(None, true),
"half a value waits in the box",
);
assert_eq!(
exit(field, true, key(egui::Key::Enter)),
(Some(TYPED.to_string()), false),
);
assert_eq!(
exit(field, true, key(egui::Key::Escape)),
(None, false),
"escape drops what was typed",
);
assert_eq!(
exit(field, false, Vec::new()),
(Some(TYPED.to_string()), false),
"leaving the box commits it",
);
}
#[test]
fn a_lamp_writes_the_spelling_its_field_lists() {
let (stage4, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let field = stage4
.iter()
.find(|field| matches!(field.spec.control, ControlKind::Toggle))
.expect("a stage 4 program has switches");
let switched = |legal: &[String]| -> Option<String> {
let ctx = egui::Context::default();
ctx.set_fonts(crate::app::fonts());
ctx.all_styles_mut(crate::app::metrics);
let mut got = None;
let mut at = egui::Pos2::ZERO;
for pass in 0..2 {
let input = egui::RawInput {
events: match pass {
0 => Vec::new(),
_ => click(at),
},
..headless()
};
let _ = ctx.run(input, |ctx| {
egui::CentralPanel::default().show(ctx, |ui| {
let row = ui.horizontal(|ui| toggle(ui, field, legal));
got = row.inner;
at = row.response.rect.center();
});
});
}
got
};
let named: Vec<String> = ["Off", "On"].iter().map(|word| word.to_string()).collect();
assert_eq!(switched(&named).as_deref(), Some("On"));
let plain: Vec<String> = ["false", "true"]
.iter()
.map(|word| word.to_string())
.collect();
let other = (field.value != "true").to_string();
assert_eq!(switched(&plain), Some(other));
}
#[test]
fn a_click_on_a_step_moves_only_that_steps_bits() {
for step in 0..8 {
let moved = stepped(0, step, 8, 2, PackedOrder::LowFirst);
assert_eq!(moved, Some(1 << (2 * step)), "step {step}");
}
assert_eq!(stepped(0, 0, 8, 2, PackedOrder::HighFirst), Some(1 << 14));
assert_eq!(stepped(0, 7, 8, 2, PackedOrder::HighFirst), Some(1));
let three = 0b11;
assert_eq!(
stepped((three << 2) | three, 1, 8, 2, PackedOrder::LowFirst),
Some(three),
);
assert_eq!(stepped(0, 8, 8, 2, PackedOrder::LowFirst), None);
assert_eq!(stepped(0, 8, 8, 2, PackedOrder::HighFirst), None);
assert_eq!(stepped(0, 4, 8, 16, PackedOrder::LowFirst), None);
assert_eq!(stepped(0, 0, 8, 65, PackedOrder::LowFirst), None);
}
#[test]
fn a_register_writes_only_the_bars_that_moved() {
let (fields, _) = apply(&Fresh::Stage4Program.bytes().unwrap(), &[]).unwrap();
let run: Vec<Part> = fields
.iter()
.filter(|field| ranked(field).is_some() && field.path.starts_with("organ_a."))
.take(drawbar_widget::BARS)
.map(|field| Part {
field,
morphs: Default::default(),
})
.collect();
assert_eq!(run.len(), drawbar_widget::BARS);
let was = [0_u8; drawbar_widget::BARS];
let mut now = was;
now[2] = 8;
assert_eq!(
bar_sets(&run, &was, &now),
[(run[2].field.path.clone(), "8".to_string())],
);
assert!(bar_sets(&run, &was, &was).is_empty());
}
#[test]
fn a_stored_word_is_refused_unless_it_spells_a_number() {
assert_eq!(word("0x1f"), Some(31));
assert_eq!(word("0X1F"), Some(31));
assert_eq!(word(" 42 "), Some(42));
assert_eq!(word("0x"), None);
assert_eq!(word(""), None);
assert_eq!(word("ff"), None);
assert_eq!(word("-1"), None);
}
#[test]
fn the_transpose_knob_turns_as_far_as_its_field_allows() {
let (_, fields) = electro5();
let amount = fields
.iter()
.find(|field| field.path == TRANSPOSE)
.expect("the transpose amount");
assert_eq!(contiguous(&(amount.spec.legal)()), Some((-6, 6)));
}
#[test]
fn a_body_with_no_layout_falls_into_the_sections_its_paths_name() {
let titles = |bytes: Vec<u8>| -> Vec<String> {
let (fields, _) = apply(&bytes, &[]).unwrap();
let groups = prefixes(&fields);
assert_eq!(
fields.len(),
groups.iter().map(|group| group.rows.len()).sum::<usize>(),
);
groups.into_iter().map(|group| group.title).collect()
};
let stage4 = titles(Fresh::Stage4Program.bytes().unwrap());
assert_eq!(stage4.first().map(String::as_str), Some("General"));
assert!(stage4.contains(&"Organ a".to_string()), "{stage4:?}");
let stage2 = titles(Fresh::Stage2Program.bytes().unwrap());
assert!(stage2.contains(&"Slot a — organ".to_string()), "{stage2:?}");
assert_eq!(titles(Fresh::Stage3Synth.bytes().unwrap()), ["General"]);
}
}