use super::*;
pub(crate) struct PaletteEntry {
pub(crate) tab: Tab,
pub(crate) index_in_tab: usize,
pub(crate) spec: &'static ControlSpec,
}
impl PaletteEntry {
pub(crate) fn haystack(&self) -> String {
format!(
"{} \u{b7} {} \u{b7} {}",
self.spec.id,
self.tab.name(),
self.spec.label
)
}
}
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)
&& !entries.iter().any(|e| e.spec.id == spec.id)
{
entries.push(PaletteEntry {
tab,
index_in_tab,
spec,
});
}
}
}
entries
}
pub(crate) struct PaletteMatch {
pub(crate) entry: usize,
pub(crate) score: i32,
pub(crate) hits: Vec<usize>,
}
#[derive(Clone)]
pub(crate) struct StagedEdit {
pub(crate) id: &'static str,
pub(crate) value: f32,
}
pub(crate) enum PaletteAction {
None,
Jump(usize),
CommitNow(Vec<StagedEdit>),
CommitAtBar(Vec<StagedEdit>),
}
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]) -> Self {
let mut state = Self {
entries: palette_entries(),
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 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();
}
}
pub(crate) fn backspace(&mut self) {
if !self.value_buf.is_empty() {
self.value_buf.pop();
} else if self.locked.is_some() {
self.locked = None;
} else {
self.query.pop();
self.recompute();
}
}
pub(crate) fn move_selection(&mut self, dir: isize) {
if self.matches.is_empty() || self.locked.is_some() {
return;
}
let len = self.matches.len() as isize;
self.selected = (self.selected as isize + dir).rem_euclid(len) as usize;
}
pub(crate) fn autocomplete(&mut self) {
if self.locked.is_none()
&& let Some(m) = self.matches.get(self.selected)
{
self.locked = Some(m.entry);
}
}
pub(crate) fn enter(&mut self) -> PaletteAction {
if let Some(entry) = self.locked {
if let Ok(value) = self.value_buf.parse::<f32>() {
self.staged.push(StagedEdit {
id: self.entries[entry].spec.id,
value,
});
self.locked = None;
self.value_buf.clear();
self.query.clear();
self.recompute();
return PaletteAction::None;
}
return PaletteAction::Jump(entry);
}
if !self.staged.is_empty() && self.query.is_empty() {
return PaletteAction::CommitNow(std::mem::take(&mut self.staged));
}
match self.matches.get(self.selected) {
Some(m) => PaletteAction::Jump(m.entry),
None => PaletteAction::None,
}
}
pub(crate) fn commit_at_bar(&mut self) -> PaletteAction {
if let (Some(entry), Ok(value)) = (self.locked, self.value_buf.parse::<f32>()) {
self.staged.push(StagedEdit {
id: self.entries[entry].spec.id,
value,
});
self.locked = None;
self.value_buf.clear();
}
if self.staged.is_empty() {
return PaletteAction::None;
}
PaletteAction::CommitAtBar(std::mem::take(&mut self.staged))
}
fn recompute(&mut self) {
self.matches.clear();
if self.query.is_empty() {
for entry in 0..self.entries.len() {
self.matches.push(PaletteMatch {
entry,
score: 0,
hits: Vec::new(),
});
}
self.sort_by_context();
} else {
for (entry, e) in self.entries.iter().enumerate() {
if let Some((score, hits)) = fuzzy_score(&self.query, &e.haystack()) {
self.matches.push(PaletteMatch { entry, score, hits });
}
}
let entries = &self.entries;
let recent = &self.recent;
let current_tab = self.current_tab;
self.matches.sort_by(|left, right| {
right.score.cmp(&left.score).then_with(|| {
context_rank(entries, left.entry, current_tab, recent).cmp(&context_rank(
entries,
right.entry,
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, current_tab, recent));
}
}
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| id == palette_entry.spec.id);
let page_index = tab_specs(current_tab)
.iter()
.position(|spec| spec.id == palette_entry.spec.id);
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(|c| c.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 idx = found?;
if prev_hit == Some(idx.wrapping_sub(1)) {
score += 8;
}
let at_word_start =
idx == 0 || matches!(hay.get(idx - 1), Some(' ') | Some('\u{b7}') | Some('.'));
if at_word_start {
score += 6;
}
if hits.is_empty() {
score -= idx as i32;
}
hits.push(idx);
prev_hit = Some(idx);
hay_i = idx + 1;
}
score += (needle.len() as i32) * 2;
Some((score, hits))
}