1use crate::{
25 gui::{state::ElementId, Direction},
26 ops::clamp_size,
27 prelude::*,
28};
29use std::cmp;
30
31pub const MAX_DISPLAYED: usize = 100;
33const SELECT_POP_LABEL: &str = "##select_pop";
34
35impl PixState {
36 pub fn select_box<S, I>(
62 &mut self,
63 label: S,
64 selected: &mut usize,
65 items: &[I],
66 mut displayed_count: usize,
67 ) -> PixResult<bool>
68 where
69 S: AsRef<str>,
70 I: AsRef<str>,
71 {
72 let label = label.as_ref();
73
74 if displayed_count > MAX_DISPLAYED {
75 displayed_count = MAX_DISPLAYED;
76 }
77 if *selected >= items.len() {
78 *selected = items.len() - 1;
79 }
80
81 let s = self;
82 let id = s.ui.get_id(&label);
83 let label = s.ui.get_label(label);
84 let pos = s.cursor_pos();
85 let font_size = clamp_size(s.theme.font_size);
86 let spacing = s.theme.spacing;
87 let fpad = spacing.frame_pad;
88 let ipad = spacing.item_pad;
89
90 let (item_width, item_height) = s.text_size(items.get(0).map_or("", AsRef::as_ref))?;
92 let width = s.ui.next_width.take().unwrap_or(item_width);
93 let (label_width, label_height) = s.text_size(label)?;
94 let [mut x, y] = pos.coords();
95 if !label.is_empty() {
96 x += label_width + ipad.x();
97 }
98 let select_box = rect![x, y, width, item_height].offset_size(2 * fpad);
99
100 let hovered = s.focused() && s.ui.try_hover(id, &select_box);
102 let focused = s.focused() && s.ui.try_focus(id);
103
104 s.push();
105 s.ui.push_cursor();
106
107 if !label.is_empty() {
109 s.set_cursor_pos([
110 pos.x(),
111 pos.y() + select_box.height() / 2 - label_height / 2,
112 ]);
113 s.text(label)?;
114 }
115
116 s.rect_mode(RectMode::Corner);
118 if hovered {
119 s.frame_cursor(&Cursor::hand())?;
120 }
121 let [stroke, bg, fg] = s.widget_colors(id, ColorType::Background);
122 s.stroke(stroke);
123 s.fill(bg);
124 s.rect(select_box)?;
125
126 let arrow_width = font_size + 2 * fpad.y();
128 let arrow_x = cmp::max(select_box.left(), select_box.right() - arrow_width);
129
130 let [_, select_y, _, select_height] = select_box.coords();
131 let arrow_box = rect![arrow_x, select_y, arrow_width, select_height];
132 s.rect(arrow_box)?;
133
134 if arrow_x + arrow_width - fpad.x() <= select_box.right() {
135 s.stroke(None);
136 s.fill(fg);
137 s.clip(arrow_box)?;
138 s.arrow(
139 [
140 arrow_x + fpad.y(),
141 select_y + arrow_box.height() / 2 - arrow_width / 4,
142 ],
143 Direction::Down,
144 f64::from(fpad.y()) / 8.0,
145 )?;
146 }
147
148 s.clip(rect![
150 select_box.top_left(),
151 select_box.width() - arrow_box.width(),
152 select_box.height()
153 ])?;
154
155 s.wrap(None);
156 s.set_cursor_pos(select_box.top_left() + fpad);
157 s.stroke(None);
158 s.fill(fg);
159 s.text(&items[*selected])?;
160
161 s.clip(None)?;
162 s.ui.pop_cursor();
163 s.pop();
164 s.advance_cursor([select_box.right() - pos.x(), select_box.height()]);
165
166 let line_height = font_size + 2 * ipad.y();
167 let expanded_list = rect![
168 select_box.left(),
169 select_box.bottom() + 1,
170 select_box.width(),
171 displayed_count as i32 * line_height + 2 * fpad.y(),
172 ];
173 let original_selected = *selected;
174 s.select_list_popup(id, selected, items, displayed_count, expanded_list)?;
175
176 s.push_id(id);
178 let list_id = s.ui.get_id(&SELECT_POP_LABEL);
179 let scroll = s.ui.scroll(list_id);
180 s.pop_id();
181 let expanded = s.ui.expanded(id);
182 if focused {
183 s.ui.set_expanded(id, true);
184 if let Some(key) = s.ui.key_entered() {
185 if let Key::Escape | Key::Return = key {
186 s.ui.set_expanded(id, !expanded);
187 s.ui.clear_entered();
188 } else {
189 let new_selected = match key {
190 Key::Up => Some(selected.saturating_sub(1)),
191 Key::Down => Some(cmp::min(items.len() - 1, selected.saturating_add(1))),
192 _ => None,
193 };
194 if let Some(selection) = new_selected {
195 *selected = selection;
196 s.ui.clear_entered();
197 let sel_y = *selected as i32 * line_height;
198 let mut new_scroll = scroll;
199 let height = expanded_list.height();
200 if sel_y < scroll.y() {
201 new_scroll.set_y(sel_y);
203 } else if sel_y + line_height > scroll.y() + height {
204 new_scroll
206 .set_y((sel_y + line_height) - (height - font_size - ipad.y()));
207 }
208 if new_scroll != scroll {
209 s.ui.set_scroll(list_id, new_scroll);
210 }
211 }
212 }
213 }
214 }
215 let clicked_outside = s.mouse_down(Mouse::Left)
216 && !select_box.contains(s.mouse_pos())
217 && !expanded_list.contains(s.mouse_pos());
218 if (expanded && clicked_outside) || (!focused && !s.mouse_down(Mouse::Left)) {
219 s.ui.set_expanded(id, false);
220 }
221
222 s.ui.handle_focus(id);
223
224 Ok(original_selected != *selected)
225 }
226
227 pub fn select_list<S, I>(
252 &mut self,
253 label: S,
254 selected: &mut usize,
255 items: &[I],
256 mut displayed_count: usize,
257 ) -> PixResult<bool>
258 where
259 S: AsRef<str>,
260 I: AsRef<str>,
261 {
262 let label = label.as_ref();
263
264 if displayed_count > MAX_DISPLAYED {
265 displayed_count = MAX_DISPLAYED;
266 }
267 if *selected >= items.len() {
268 *selected = items.len() - 1;
269 }
270
271 let s = self;
272 let id = s.ui.get_id(&label);
273 let label = s.ui.get_label(label);
274 let pos = s.cursor_pos();
275 let font_size = clamp_size(s.theme.font_size);
276 let spacing = s.theme.spacing;
277 let fpad = spacing.frame_pad;
278 let ipad = spacing.item_pad;
279
280 let (label_width, label_height) = s.text_size(label)?;
282 let width = s.ui.next_width.take().unwrap_or(label_width);
283 let [x, mut y] = pos.coords();
284 if !label.is_empty() {
285 y += label_height + ipad.y();
286 }
287 let line_height = font_size + 2 * ipad.y();
288 let select_list = rect![
289 x,
290 y,
291 width,
292 displayed_count as i32 * line_height + 2 * fpad.y() + 2
293 ];
294
295 let focused = s.focused() && s.ui.try_focus(id);
297
298 s.push();
299 s.ui.push_cursor();
300
301 s.rect_mode(RectMode::Corner);
303 s.text(label)?;
304
305 let original_selected = *selected;
306 s.select_list_items(id, selected, items, displayed_count, select_list)?;
307
308 s.ui.pop_cursor();
309 s.pop();
310
311 let scroll = s.ui.scroll(id);
313 let line_height = font_size + 2 * ipad.y();
314 if focused {
315 if let Some(key) = s.ui.key_entered() {
316 let new_selected = match key {
317 Key::Up => Some(selected.saturating_sub(1)),
318 Key::Down => Some(cmp::min(items.len() - 1, selected.saturating_add(1))),
319 _ => None,
320 };
321 if let Some(selection) = new_selected {
322 *selected = selection;
323 s.ui.clear_entered();
324 let sel_y = *selected as i32 * line_height;
325 let mut new_scroll = scroll;
326 let height = select_list.height();
327 if sel_y < scroll.y() {
328 new_scroll.set_y(sel_y);
330 } else if sel_y + line_height > scroll.y() + height {
331 new_scroll.set_y((sel_y + line_height) - (height - font_size - ipad.y()));
333 }
334 if new_scroll != scroll {
335 s.ui.set_scroll(id, new_scroll);
336 }
337 }
338 }
339 }
340 s.ui.handle_focus(id);
341
342 let total_height = items.len() as i32 * line_height + 2;
344 let total_width = items.iter().fold(0, |max_width, item| {
345 let (w, _) = s.text_size(item.as_ref()).unwrap_or((0, 0));
346 cmp::max(w, max_width)
347 });
348
349 let rect = s.scroll(
350 id,
351 select_list,
352 total_width + 2 * fpad.x(),
353 total_height + 2 * fpad.y(),
354 )?;
355 s.advance_cursor([rect.width().max(label_width), rect.bottom() - pos.y()]);
356
357 Ok(original_selected != *selected)
358 }
359}
360
361impl PixState {
362 #[inline]
363 fn select_list_popup<I>(
364 &mut self,
365 id: ElementId,
366 selected: &mut usize,
367 items: &[I],
368 displayed_count: usize,
369 size: Rect<i32>,
370 ) -> PixResult<bool>
371 where
372 I: AsRef<str>,
373 {
374 let s = self;
375 let font_size = clamp_size(s.theme.font_size);
376 let spacing = s.theme.spacing;
377 let fpad = spacing.frame_pad;
378 let ipad = spacing.item_pad;
379
380 let line_height = font_size + 2 * ipad.y();
381 let height = displayed_count as i32 * line_height + 2 * fpad.y();
382
383 let expanded = s.ui.expanded(id);
384 if expanded {
385 let total_height = items.len() as i32 * line_height + 2 * fpad.y();
387 let texture_id = s.get_or_create_texture(id, None, size)?;
388
389 s.ui.offset_mouse(size.top_left());
390
391 s.set_texture_target(texture_id)?;
392 s.clear()?;
393 s.set_cursor_pos([0, 0]);
394 if total_height > height {
395 s.next_width((size.width() - spacing.scroll_size) as u32);
396 } else {
397 s.next_width(size.width() as u32);
398 }
399 s.ui.disable_focus();
400 s.push_id(id);
401 let changed = s.select_list(SELECT_POP_LABEL, selected, items, displayed_count)?;
402 s.pop_id();
403 s.ui.enable_focus();
404 s.clear_texture_target();
405
406 s.ui.clear_mouse_offset();
407 if changed {
408 s.ui.set_expanded(id, false);
409 }
410 Ok(changed)
411 } else {
412 Ok(false)
413 }
414 }
415
416 #[inline]
417 fn select_list_items<I>(
418 &mut self,
419 id: ElementId,
420 selected: &mut usize,
421 items: &[I],
422 displayed_count: usize,
423 select_list: Rect<i32>,
424 ) -> PixResult<()>
425 where
426 I: AsRef<str>,
427 {
428 let s = self;
429 let font_size = clamp_size(s.theme.font_size);
430 let spacing = s.theme.spacing;
431 let colors = s.theme.colors;
432 let fpad = spacing.frame_pad;
433 let ipad = spacing.item_pad;
434
435 let hovered = s.focused() && s.ui.try_hover(id, &select_list);
437 let active = s.ui.is_active(id);
438 let disabled = s.ui.disabled;
439
440 let [stroke, bg, fg] = s.widget_colors(id, ColorType::Background);
441 s.stroke(stroke);
442 s.fill(colors.background);
443 s.rect(select_list)?;
444
445 let mpos = s.mouse_pos();
447
448 let border_clip = select_list.shrink([1, 1]);
449 s.clip(border_clip)?;
450 let content_clip = border_clip.shrink(fpad);
451 let item_clip = rect![
452 select_list.x() + 1,
453 content_clip.y(),
454 select_list.width() - 2,
455 content_clip.height(),
456 ];
457
458 let scroll = s.ui.scroll(id);
459 let line_height = font_size + ipad.y() * 2;
460 let skip_count = (scroll.y() / line_height) as usize;
461 let displayed_items = items
462 .iter()
463 .enumerate()
464 .skip(skip_count)
465 .take(displayed_count + 1); let x = select_list.x() + fpad.x() - scroll.x();
468 let mut y = content_clip.y() - scroll.y() + (skip_count as i32 * line_height);
469 for (i, item) in displayed_items {
470 let item_rect = rect!(select_list.x(), y, select_list.width(), line_height);
471 let clickable =
472 item_rect.bottom() > content_clip.y() || item_rect.top() < select_list.height();
473 s.push();
474 s.clip(item_clip)?;
475 if hovered && clickable && item_rect.contains(mpos) {
476 s.frame_cursor(&Cursor::hand())?;
477 s.stroke(None);
478 s.fill(bg);
479 s.rect([item_clip.x(), y, item_clip.width(), line_height])?;
480 if active && s.mouse_clicked(Mouse::Left) {
481 *selected = i;
482 }
483 }
484 if *selected == i {
485 s.stroke(None);
486 if disabled {
487 s.fill(colors.primary.blended(colors.background, 0.38));
488 } else {
489 s.fill(colors.primary);
490 }
491 s.rect([item_clip.x(), y, item_clip.width(), line_height])?;
492 }
493 s.pop();
494 s.clip(content_clip)?;
495 s.set_cursor_pos([x, y + ipad.y()]);
496 s.stroke(None);
497 if *selected == i {
498 s.fill(colors.on_primary);
499 } else {
500 s.fill(fg);
501 }
502 s.text(item)?;
503 s.clip(border_clip)?;
504 y += line_height;
505 }
506
507 s.clip(None)?;
508
509 Ok(())
510 }
511}