1use egui::{Align2, EventFilter, Id, Key, Modifiers, Response, Sense, Stroke, StrokeKind, Ui, pos2, vec2};
4
5use super::options::OptionList;
6use super::popover::Popover;
7use crate::automation::{self, Details, Role};
8use crate::tokens::{self, Tokens};
9
10#[derive(Clone, Copy, Debug, Default)]
11struct Memory {
12 open: bool,
13 cursor: usize,
14}
15
16pub struct Select<'a, T: AsRef<str>> {
23 id: &'a str,
24 options: &'a [T],
25 current: &'a mut usize,
26 width: Option<f32>,
27 label: &'a str,
28 placeholder: &'a str,
29}
30
31impl<'a, T: AsRef<str>> Select<'a, T> {
32 pub fn new(id: &'a str, options: &'a [T], current: &'a mut usize) -> Self {
34 Self { id, options, current, width: None, label: "", placeholder: "Choose…" }
35 }
36
37 pub fn width(mut self, width: f32) -> Self {
39 self.width = width.is_finite().then_some(width.clamp(40.0, 4000.0));
40 self
41 }
42
43 pub fn label(mut self, label: &'a str) -> Self {
45 self.label = label;
46 self
47 }
48
49 pub fn placeholder(mut self, text: &'a str) -> Self {
51 self.placeholder = text;
52 self
53 }
54
55 pub fn show(self, ui: &mut Ui) -> Response {
56 let t = tokens::current(ui.ctx());
57 let id = Id::new(("ashlar_ui::select", self.id));
58 let mem_id = id.with("memory");
59 let mut mem: Memory = ui.data(|d| d.get_temp(mem_id)).unwrap_or_default();
60 let n = self.options.len();
61 let label_of = |i: usize| self.options.get(i).map_or("", |o| o.as_ref());
62 let current = (*self.current < n).then_some(*self.current);
63 let shown = current.map_or(self.placeholder, label_of);
64
65 let mut choose = None;
66 let mut moved = false;
67 let focused = ui.memory(|m| m.has_focus(id));
68 if focused {
69 let filter = EventFilter { tab: false, horizontal_arrows: false, vertical_arrows: true, escape: mem.open };
71 ui.memory_mut(|m| m.set_focus_lock_filter(id, filter));
72 let (up, down, home, end, enter, space) = ui.input_mut(|i| {
73 (
74 i.consume_key(Modifiers::NONE, Key::ArrowUp),
75 i.consume_key(Modifiers::NONE, Key::ArrowDown),
76 mem.open && i.consume_key(Modifiers::NONE, Key::Home),
77 mem.open && i.consume_key(Modifiers::NONE, Key::End),
78 i.consume_key(Modifiers::NONE, Key::Enter),
79 i.consume_key(Modifiers::NONE, Key::Space),
80 )
81 });
82 let last = n.saturating_sub(1);
83 if !mem.open {
84 if (up || down || enter || space) && n > 0 {
85 mem.open = true;
86 mem.cursor = current.unwrap_or(0);
87 moved = true;
88 }
89 } else {
90 if up {
91 mem.cursor = mem.cursor.saturating_sub(1);
92 }
93 if down {
94 mem.cursor = mem.cursor.saturating_add(1).min(last);
95 }
96 if home {
97 mem.cursor = 0;
98 }
99 if end {
100 mem.cursor = last;
101 }
102 moved = up || down || home || end;
103 if (enter || space) && mem.cursor < n {
104 choose = Some(mem.cursor);
105 }
106 }
107 }
108
109 let width = self.width.unwrap_or(t.control_height * 12.0).min(ui.available_width().max(40.0));
110 let (rect, _) = ui.allocate_exact_size(vec2(width, t.control_height), Sense::hover());
111 let mut response = ui.interact(rect, id, Sense::click());
112 if response.clicked() {
113 response.request_focus();
115 mem.open = !mem.open && n > 0;
116 mem.cursor = current.unwrap_or(0);
117 moved = true;
118 }
119 if mem.open && response.lost_focus() && !ui.input(|i| i.pointer.any_down() || i.pointer.any_released()) {
120 mem.open = false;
122 }
123 paint_button(ui, &t, &response, shown, current.is_none(), focused);
124
125 let cursor = mem.cursor.min(n.saturating_sub(1));
126 let option_of = |row: usize| (row < n).then_some(row);
127 let id_str = self.id;
128 let clicked = Popover::attached(id.with("popup")).width(width).show(&response, &mut mem.open, |ui| {
129 OptionList {
130 id: id_str,
131 rows: n,
132 option_of: &option_of,
133 label: &label_of,
134 cursor: Some(cursor),
135 chosen: current,
136 scroll_to: moved.then_some(cursor),
137 }
138 .show(ui)
139 });
140 if let Some(Some(row)) = clicked {
141 choose = Some(row);
142 }
143 if let Some(i) = choose
144 && i < n
145 {
146 mem.open = false;
147 mem.cursor = i;
148 if *self.current != i {
149 *self.current = i;
150 response.mark_changed();
151 }
152 }
153 ui.data_mut(|d| d.insert_temp(mem_id, mem));
154
155 let chosen_label = if *self.current < n { label_of(*self.current) } else { "" };
156 response.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::ComboBox, true, self.label));
157 ui.ctx().accesskit_node_builder(id, |node| {
158 node.set_role(egui::accesskit::Role::ComboBox);
159 node.set_expanded(mem.open);
160 node.set_value(chosen_label);
161 });
162 automation::register_with(
163 ui,
164 &response,
165 self.id,
166 self.label,
167 Role::ComboBox,
168 Details { value: Some(chosen_label), selected: Some(mem.open) },
169 );
170 response
171 }
172}
173
174fn paint_button(ui: &Ui, t: &Tokens, response: &Response, text: &str, placeholder: bool, focused: bool) {
175 let rect = response.rect;
176 let fill = if response.is_pointer_button_down_on() {
177 t.surface.pressed
178 } else if response.hovered() {
179 t.surface.hover
180 } else {
181 t.surface.sunken
182 };
183 let p = ui.painter();
184 p.rect(rect, t.radius.md, fill, Stroke::new(t.stroke.hairline, t.border), StrokeKind::Inside);
185 if focused {
186 p.rect_stroke(rect.expand(1.0), t.radius.md, Stroke::new(t.stroke.focus, t.focus), StrokeKind::Outside);
187 }
188 let color = if placeholder { t.text.secondary } else { t.text.primary };
189 let text_rect = rect.shrink2(vec2(t.space.md, 0.0)).with_max_x(rect.right() - t.space.md * 2.0 - 8.0);
190 let clip = p.with_clip_rect(text_rect.intersect(p.clip_rect()));
191 clip.text(pos2(text_rect.left(), rect.center().y), Align2::LEFT_CENTER, text, Tokens::font(t.type_scale.body), color);
192 let c = pos2(rect.right() - t.space.md - 3.0, rect.center().y);
194 let s = Stroke::new(1.4, t.text.secondary);
195 p.line_segment([c + vec2(-4.0, -2.0), c + vec2(0.0, 2.0)], s);
196 p.line_segment([c + vec2(0.0, 2.0), c + vec2(4.0, -2.0)], s);
197}