1#![warn(missing_docs)]
4
5use egui::{
6 Id, RectAlign, Response, ScrollArea, TextEdit, Ui, Widget, WidgetText, text::{CCursor, CCursorRange}
7};
8use std::hash::Hash;
9
10pub struct DropDownBox<
12 'a,
13 F: FnMut(&mut Ui, &str) -> Response,
14 V: AsRef<str>,
15 I: Iterator<Item = V>,
16> {
17 buf: &'a mut String,
18 popup_id: Id,
19 display: F,
20 it: I,
21 hint_text: WidgetText,
22 filter_by_input: bool,
23 select_on_focus: bool,
24 desired_width: Option<f32>,
25 max_height: Option<f32>,
26}
27
28impl<'a, F: FnMut(&mut Ui, &str) -> Response, V: AsRef<str>, I: Iterator<Item = V>>
29 DropDownBox<'a, F, V, I>
30{
31 pub fn from_iter(
33 it: impl IntoIterator<IntoIter = I>,
34 id_source: impl Hash,
35 buf: &'a mut String,
36 display: F,
37 ) -> Self {
38 Self {
39 popup_id: Id::new(id_source),
40 it: it.into_iter(),
41 display,
42 buf,
43 hint_text: WidgetText::default(),
44 filter_by_input: true,
45 select_on_focus: false,
46 desired_width: None,
47 max_height: None,
48 }
49 }
50
51 pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
53 self.hint_text = hint_text.into();
54 self
55 }
56
57 pub fn filter_by_input(mut self, filter_by_input: bool) -> Self {
59 self.filter_by_input = filter_by_input;
60 self
61 }
62
63 pub fn select_on_focus(mut self, select_on_focus: bool) -> Self {
65 self.select_on_focus = select_on_focus;
66 self
67 }
68
69 pub fn desired_width(mut self, desired_width: f32) -> Self {
71 self.desired_width = desired_width.into();
72 self
73 }
74
75 pub fn max_height(mut self, height: f32) -> Self {
77 self.max_height = height.into();
78 self
79 }
80}
81
82impl<F: FnMut(&mut Ui, &str) -> Response, V: AsRef<str>, I: Iterator<Item = V>> Widget
83 for DropDownBox<'_, F, V, I>
84{
85 fn ui(self, ui: &mut Ui) -> Response {
86 let Self {
87 popup_id,
88 buf,
89 it,
90 mut display,
91 hint_text,
92 filter_by_input,
93 select_on_focus,
94 desired_width,
95 max_height,
96 } = self;
97
98 let mut edit = TextEdit::singleline(buf).hint_text(hint_text);
99 if let Some(dw) = desired_width {
100 edit = edit.desired_width(dw);
101 }
102 let mut edit_output = edit.show(ui);
103 let mut r = edit_output.response;
104 if r.gained_focus() {
105 if select_on_focus {
106 edit_output
107 .state
108 .cursor
109 .set_char_range(Some(CCursorRange::two(
110 CCursor::new(0),
111 CCursor::new(buf.len()),
112 )));
113 edit_output.state.store(ui.ctx(), r.id);
114 }
115
116 egui::Popup::open_id(ui.ctx(), popup_id);
117 }
118
119 let mut changed = false;
120 egui::Popup::menu(&r)
121 .align(RectAlign::BOTTOM_START)
122 .close_behavior(egui::PopupCloseBehavior::CloseOnClick)
123 .show(|ui| {
124 if let Some(max) = max_height {
125 ui.set_max_height(max);
126 }
127
128 ScrollArea::vertical()
129 .max_height(f32::INFINITY)
130 .show(ui, |ui| {
131 for var in it {
132 let text = var.as_ref();
133 if filter_by_input
134 && !buf.is_empty()
135 && !text.to_lowercase().contains(&buf.to_lowercase())
136 {
137 continue;
138 }
139
140 if display(ui, text).clicked() {
141 *buf = text.to_owned();
142 changed = true;
143
144 egui::Popup::close_id(ui.ctx(), popup_id);
145 }
146 }
147 });
148 });
149
150 if changed {
151 r.mark_changed();
152 }
153
154 r
155 }
156}