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