use super::module::{MODULE_CATALOG, chain_amount_slot, module_available_on, tab_has_module_chain};
use super::*;
pub(crate) enum PaletteEntry {
Control {
tab: Tab,
index_in_tab: usize,
spec: &'static ControlSpec,
},
Module { tab: Tab, catalog_index: usize },
ModuleControl {
tab: Tab,
spec: &'static ControlSpec,
module_name: &'static str,
parameter: &'static str,
},
}
impl PaletteEntry {
pub(crate) fn haystack(&self) -> String {
match self {
Self::Control { tab, spec, .. } => {
format!("{} · {} · {}", spec.id, tab.name(), spec.label)
}
Self::Module { tab, catalog_index } => {
let kind = MODULE_CATALOG[*catalog_index];
format!(
"{} · {} · {} · module",
kind.id,
kind.display_name,
tab.name()
)
}
Self::ModuleControl {
tab,
spec,
module_name,
parameter,
} => {
format!(
"{}.{}.{} · {}",
tab.name().to_lowercase(),
module_name.to_lowercase(),
parameter.to_lowercase().replace(' ', "_"),
spec.label
)
}
}
}
pub(crate) fn spec(&self) -> Option<&'static ControlSpec> {
match self {
Self::Control { spec, .. } | Self::ModuleControl { spec, .. } => Some(spec),
Self::Module { .. } => None,
}
}
pub(crate) fn id(&self) -> Option<&'static str> {
self.spec().map(|spec| spec.id)
}
pub(crate) fn value(&self, c: &FluidControls) -> String {
match self {
Self::Control { spec, .. } | Self::ModuleControl { spec, .. } => (spec.display)(c),
Self::Module { tab, catalog_index } => {
let kind = MODULE_CATALOG[*catalog_index];
c.modules
.for_tab(*tab)
.and_then(|slots| chain_amount_slot(slots, kind.id))
.map_or_else(
|| "add".to_string(),
|slot| {
format!(
"{:.0}%",
c.modules.for_tab(*tab).expect("layer exists")[slot].amount * 100.0
)
},
)
}
}
}
}
pub(crate) fn palette_entries() -> Vec<PaletteEntry> {
let mut entries: Vec<PaletteEntry> = Vec::new();
for tab in Tab::all() {
for (index_in_tab, spec) in tab_specs(tab).iter().enumerate() {
if tab_owning_control(spec.id) == Some(tab)
&& parse_module_slot_id(spec.id).is_none()
&& !entries.iter().any(|e| e.id() == Some(spec.id))
{
entries.push(PaletteEntry::Control {
tab,
index_in_tab,
spec,
});
}
}
}
for tab in Tab::all() {
if !tab_has_module_chain(tab) {
continue;
}
for (catalog_index, kind) in MODULE_CATALOG.iter().copied().enumerate() {
if module_available_on(kind, tab) {
entries.push(PaletteEntry::Module { tab, catalog_index });
}
}
}
entries
}
pub(crate) struct PaletteMatch {
pub(crate) entry_index: usize,
pub(crate) score: i32,
pub(crate) hits: Vec<usize>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ModuleScope {
pub(crate) tab: Tab,
pub(crate) slot: usize,
pub(crate) catalog_index: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct StagedEdit {
pub(crate) id: &'static str,
pub(crate) value: f32,
}
pub(crate) struct PaletteState {
entries: Vec<PaletteEntry>,
current_tab: Tab,
recent: Vec<&'static str>,
pub(crate) query: String,
pub(crate) matches: Vec<PaletteMatch>,
pub(crate) selected: usize,
pub(crate) locked: Option<usize>,
pub(crate) value_buf: String,
pub(crate) staged: Vec<StagedEdit>,
}
impl PaletteState {
pub(crate) fn new(
current_tab: Tab,
recent: &[&'static str],
module_scope: Option<ModuleScope>,
) -> Self {
let mut state = Self {
entries: module_scope.map_or_else(palette_entries, |scope| {
module_palette_entries(scope.tab, scope.slot, scope.catalog_index)
}),
current_tab,
recent: recent.to_vec(),
query: String::new(),
matches: Vec::new(),
selected: 0,
locked: None,
value_buf: String::new(),
staged: Vec::new(),
};
state.recompute();
state
}
pub(crate) fn entry(&self, index: usize) -> &PaletteEntry {
&self.entries[index]
}
pub(crate) fn contains_entry(&self, index: usize) -> bool {
index < self.entries.len()
}
pub(crate) fn push_char(&mut self, c: char) {
if self.locked.is_some() {
if c.is_ascii_digit() || c == '.' || c == '-' {
self.value_buf.push(c);
}
} else {
self.query.push(c);
self.recompute();
}
}
fn recompute(&mut self) {
self.matches.clear();
if self.query.is_empty() {
for entry_index in 0..self.entries.len() {
self.matches.push(PaletteMatch {
entry_index,
score: 0,
hits: Vec::new(),
});
}
self.sort_by_context();
} else {
for (entry_index, entry) in self.entries.iter().enumerate() {
if let Some((score, hits)) = fuzzy_score(&self.query, &entry.haystack()) {
self.matches.push(PaletteMatch {
entry_index,
score,
hits,
});
}
}
let entries = &self.entries;
let recent = &self.recent;
let current_tab = self.current_tab;
let query = self.query.as_str();
let primary_key = |entry: usize| {
entries[entry]
.id()
.and_then(|id| layer_primary_rank(id, query))
.unwrap_or(usize::MAX)
};
self.matches.sort_by(|left, right| {
primary_key(left.entry_index)
.cmp(&primary_key(right.entry_index))
.then_with(|| right.score.cmp(&left.score))
.then_with(|| {
context_rank(entries, left.entry_index, current_tab, recent).cmp(
&context_rank(entries, right.entry_index, current_tab, recent),
)
})
});
}
self.selected = 0;
}
fn sort_by_context(&mut self) {
let entries = &self.entries;
let recent = &self.recent;
let current_tab = self.current_tab;
self.matches
.sort_by_key(|m| context_rank(entries, m.entry_index, current_tab, recent));
}
}
fn module_palette_entries(tab: Tab, slot: usize, catalog_index: usize) -> Vec<PaletteEntry> {
MODULE_CATALOG[catalog_index]
.parameters()
.iter()
.filter_map(|parameter| {
module_slot_spec(tab, slot, parameter.field).map(|spec| PaletteEntry::ModuleControl {
tab,
spec,
module_name: MODULE_CATALOG[catalog_index].display_name,
parameter: parameter.label,
})
})
.collect()
}
fn layer_primary_rank(id: &str, query: &str) -> Option<usize> {
if query.is_empty() {
return None;
}
Tab::all().iter().position(|&tab| {
tab.level_id() == Some(id) && {
let namespace = id.split('.').next().unwrap_or(id);
starts_with_ignore_case(namespace, query) || starts_with_ignore_case(tab.name(), query)
}
})
}
fn starts_with_ignore_case(haystack: &str, prefix: &str) -> bool {
haystack.len() >= prefix.len()
&& haystack
.chars()
.zip(prefix.chars())
.all(|(h, p)| h.eq_ignore_ascii_case(&p))
}
fn context_rank(
entries: &[PaletteEntry],
entry: usize,
current_tab: Tab,
recent: &[&'static str],
) -> (u8, usize, usize) {
let palette_entry = &entries[entry];
let recent_rank = recent.iter().position(|&id| Some(id) == palette_entry.id());
let page_index = match palette_entry {
PaletteEntry::Module { tab, catalog_index } => {
(*tab == current_tab).then_some(tab_specs(current_tab).len() + catalog_index)
}
PaletteEntry::Control { spec, .. } => tab_specs(current_tab)
.iter()
.position(|other| other.id == spec.id),
PaletteEntry::ModuleControl { tab, spec, .. } => (*tab == current_tab)
.then(|| {
tab_specs(current_tab)
.iter()
.position(|other| other.id == spec.id)
})
.flatten(),
};
let group = match (recent_rank, page_index) {
(Some(_), _) => 0,
(None, Some(_)) => 1,
(None, None) => 2,
};
let order = match group {
0 => recent_rank.unwrap_or(usize::MAX),
1 => page_index.unwrap_or(usize::MAX),
_ => entry,
};
(group, order, entry)
}
pub(crate) fn next_bar_beat(beat: f64) -> f64 {
const BEATS_PER_BAR: f64 = 4.0;
(beat / BEATS_PER_BAR).floor() * BEATS_PER_BAR + BEATS_PER_BAR
}
pub(crate) fn fuzzy_score(query: &str, haystack: &str) -> Option<(i32, Vec<usize>)> {
let needle: Vec<char> = query.chars().flat_map(char::to_lowercase).collect();
if needle.is_empty() {
return Some((0, Vec::new()));
}
let hay: Vec<char> = haystack.chars().collect();
let lower_at = |i: usize| -> char {
hay.get(i)
.and_then(|c| c.to_lowercase().next())
.unwrap_or(' ')
};
let mut hits = Vec::with_capacity(needle.len());
let mut score = 0i32;
let mut hay_i = 0usize;
let mut prev_hit: Option<usize> = None;
for &nc in &needle {
let mut found = None;
while hay_i < hay.len() {
if lower_at(hay_i) == nc {
found = Some(hay_i);
break;
}
hay_i += 1;
}
let hit_index = found?;
if prev_hit == Some(hit_index.wrapping_sub(1)) {
score += 8;
}
let at_word_start =
hit_index == 0 || matches!(hay.get(hit_index - 1), Some(' ') | Some('·') | Some('.'));
if at_word_start {
score += 6;
}
if hits.is_empty() {
score -= hit_index as i32;
}
hits.push(hit_index);
prev_hit = Some(hit_index);
hay_i = hit_index + 1;
}
score += (needle.len() as i32) * 2;
Some((score, hits))
}