use egui::{Align2, EventFilter, Id, Key, Modifiers, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
use super::options::OptionList;
use super::popover::Popover;
use crate::automation::{self, Details, Role};
use crate::tokens::{self, Tokens};
#[derive(Clone, Copy, Debug, Default)]
struct Memory {
open: bool,
cursor: usize,
}
pub struct Select<'a, T: AsRef<str>> {
id: &'a str,
options: &'a [T],
current: &'a mut usize,
width: Option<f32>,
label: &'a str,
placeholder: &'a str,
}
impl<'a, T: AsRef<str>> Select<'a, T> {
pub fn new(id: &'a str, options: &'a [T], current: &'a mut usize) -> Self {
Self { id, options, current, width: None, label: "", placeholder: "Choose…" }
}
pub fn width(mut self, width: f32) -> Self {
self.width = width.is_finite().then_some(width.clamp(40.0, 4000.0));
self
}
pub fn label(mut self, label: &'a str) -> Self {
self.label = label;
self
}
pub fn placeholder(mut self, text: &'a str) -> Self {
self.placeholder = text;
self
}
pub fn show(self, ui: &mut Ui) -> Response {
let t = tokens::current(ui.ctx());
let id = Id::new(("ashlar_ui::select", self.id));
let mem_id = id.with("memory");
let mut mem: Memory = ui.data(|d| d.get_temp(mem_id)).unwrap_or_default();
let n = self.options.len();
let label_of = |i: usize| self.options.get(i).map_or("", |o| o.as_ref());
let current = (*self.current < n).then_some(*self.current);
let shown = current.map_or(self.placeholder, label_of);
let mut choose = None;
let mut moved = false;
let focused = ui.memory(|m| m.has_focus(id));
if focused {
let filter = EventFilter { tab: false, horizontal_arrows: false, vertical_arrows: true, escape: mem.open };
ui.memory_mut(|m| m.set_focus_lock_filter(id, filter));
let (up, down, home, end, enter, space) = ui.input_mut(|i| {
(
i.consume_key(Modifiers::NONE, Key::ArrowUp),
i.consume_key(Modifiers::NONE, Key::ArrowDown),
mem.open && i.consume_key(Modifiers::NONE, Key::Home),
mem.open && i.consume_key(Modifiers::NONE, Key::End),
i.consume_key(Modifiers::NONE, Key::Enter),
i.consume_key(Modifiers::NONE, Key::Space),
)
});
let last = n.saturating_sub(1);
if !mem.open {
if (up || down || enter || space) && n > 0 {
mem.open = true;
mem.cursor = current.unwrap_or(0);
moved = true;
}
} else {
if up {
mem.cursor = mem.cursor.saturating_sub(1);
}
if down {
mem.cursor = mem.cursor.saturating_add(1).min(last);
}
if home {
mem.cursor = 0;
}
if end {
mem.cursor = last;
}
moved = up || down || home || end;
if (enter || space) && mem.cursor < n {
choose = Some(mem.cursor);
}
}
}
let width = self.width.unwrap_or(t.control_height * 12.0).min(ui.available_width().max(40.0));
let (rect, _) = ui.allocate_exact_size(vec2(width, t.control_height), Sense::hover());
let mut response = ui.interact(rect, id, Sense::click());
if response.clicked() {
response.request_focus();
mem.open = !mem.open && n > 0;
mem.cursor = current.unwrap_or(0);
moved = true;
}
if mem.open && response.lost_focus() && !ui.input(|i| i.pointer.any_down() || i.pointer.any_released()) {
mem.open = false;
}
paint_button(ui, &t, &response, shown, current.is_none(), focused);
let cursor = mem.cursor.min(n.saturating_sub(1));
let option_of = |row: usize| (row < n).then_some(row);
let id_str = self.id;
let clicked = Popover::attached(id.with("popup")).width(width).show(&response, &mut mem.open, |ui| {
OptionList {
id: id_str,
rows: n,
option_of: &option_of,
label: &label_of,
cursor: Some(cursor),
chosen: current,
scroll_to: moved.then_some(cursor),
}
.show(ui)
});
if let Some(Some(row)) = clicked {
choose = Some(row);
}
if let Some(i) = choose
&& i < n
{
mem.open = false;
mem.cursor = i;
if *self.current != i {
*self.current = i;
response.mark_changed();
}
}
ui.data_mut(|d| d.insert_temp(mem_id, mem));
let chosen_label = if *self.current < n { label_of(*self.current) } else { "" };
response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, true, self.label));
ui.ctx().accesskit_node_builder(id, |node| {
node.set_role(egui::accesskit::Role::ComboBox);
node.set_expanded(mem.open);
node.set_value(chosen_label);
});
automation::register_with(
ui,
&response,
self.id,
self.label,
Role::ComboBox,
Details { value: Some(chosen_label), selected: Some(mem.open) },
);
response
}
}
fn paint_button(ui: &Ui, t: &Tokens, response: &Response, text: &str, placeholder: bool, focused: bool) {
let rect = response.rect;
let fill = if response.is_pointer_button_down_on() {
t.surface.pressed
} else if response.hovered() {
t.surface.hover
} else {
t.surface.sunken
};
let p = ui.painter();
p.rect(rect, t.radius.md, fill, Stroke::new(t.stroke.hairline, t.border), StrokeKind::Inside);
if focused {
p.rect_stroke(rect.expand(1.0), t.radius.md, Stroke::new(t.stroke.focus, t.focus), StrokeKind::Outside);
}
let color = if placeholder { t.text.secondary } else { t.text.primary };
let text_rect = rect.shrink2(vec2(t.space.md, 0.0)).with_max_x(rect.right() - t.space.md * 2.0 - 8.0);
let clip = p.with_clip_rect(text_rect.intersect(p.clip_rect()));
clip.text(pos2(text_rect.left(), rect.center().y), Align2::LEFT_CENTER, text, Tokens::font(t.type_scale.body), color);
let c = pos2(rect.right() - t.space.md - 3.0, rect.center().y);
let s = Stroke::new(1.4, t.text.secondary);
p.line_segment([c + vec2(-4.0, -2.0), c + vec2(0.0, 2.0)], s);
p.line_segment([c + vec2(0.0, 2.0), c + vec2(4.0, -2.0)], s);
}