use egui::{Id, Key, Modifiers, RichText, ScrollArea, TextEdit, Ui, UiKind};
pub(crate) struct PaletteEntry<T> {
pub(crate) group: &'static str,
pub(crate) label: String,
pub(crate) description: &'static str,
pub(crate) suggested: bool,
pub(crate) current: bool,
pub(crate) found: Box<dyn Fn(&str) -> bool>,
pub(crate) value: T,
}
#[derive(Clone, Default)]
struct PaletteState {
query: String,
highlighted: usize,
focused: bool,
}
pub(crate) fn palette<T: Clone>(ui: &mut Ui, id: Id, entries: &[PaletteEntry<T>]) -> Option<T> {
let mut state: PaletteState = ui.data(|data| data.get_temp(id)).unwrap_or_default();
ui.set_min_width(320.0);
ui.set_max_width(420.0);
let (up, down, enter, escape) = ui.input_mut(|input| {
(
input.consume_key(Modifiers::NONE, Key::ArrowUp),
input.consume_key(Modifiers::NONE, Key::ArrowDown),
input.consume_key(Modifiers::NONE, Key::Enter),
input.consume_key(Modifiers::NONE, Key::Escape),
)
});
let search = ui.add(
TextEdit::singleline(&mut state.query)
.hint_text("Search…")
.desired_width(f32::INFINITY),
);
if !state.focused {
search.request_focus();
state.focused = true;
}
if search.changed() {
state.highlighted = 0;
}
let query = state.query.trim().to_lowercase();
let mut order: Vec<(Option<&'static str>, usize)> = Vec::new();
if query.is_empty() {
order.extend(
entries
.iter()
.enumerate()
.filter(|(_, entry)| entry.suggested)
.map(|(index, _)| (Some("Suggested"), index)),
);
order.extend(
entries
.iter()
.enumerate()
.map(|(index, entry)| (Some(entry.group), index)),
);
} else {
order.extend(
entries
.iter()
.enumerate()
.filter(|(_, entry)| (entry.found)(&query))
.map(|(index, entry)| (Some(entry.group), index)),
);
}
if down && state.highlighted + 1 < order.len() {
state.highlighted += 1;
}
if up {
state.highlighted = state.highlighted.saturating_sub(1);
}
let mut picked = None;
if enter && let Some((_, index)) = order.get(state.highlighted) {
picked = Some(entries[*index].value.clone());
}
ScrollArea::vertical().max_height(380.0).show(ui, |ui| {
if order.is_empty() {
ui.weak("Nothing found");
}
let mut heading = None;
for (position, (group, index)) in order.iter().enumerate() {
if *group != heading {
heading = *group;
ui.add_space(4.0);
ui.label(
RichText::new(group.unwrap_or_default().to_uppercase())
.small()
.weak(),
);
}
let entry = &entries[*index];
let text = if entry.current {
egui::RichText::new(format!("{} (now)", entry.label)).strong()
} else {
egui::RichText::new(&entry.label)
};
let mut response = ui.selectable_label(position == state.highlighted, text);
if !entry.description.is_empty() {
response = response.on_hover_text(entry.description);
}
if position == state.highlighted && (up || down) {
response.scroll_to_me(None);
}
if response.clicked() {
picked = Some(entry.value.clone());
}
}
});
if picked.is_some() || escape {
ui.data_mut(|data| data.remove::<PaletteState>(id));
ui.close_kind(UiKind::Menu);
} else {
ui.data_mut(|data| data.insert_temp(id, state));
}
picked
}