egui_dropdown/
lib.rs

1//! egui-dropdown
2
3#![warn(missing_docs)]
4
5use egui::{
6    text::{CCursor, CCursorRange},
7    Id, Response, ScrollArea, TextEdit, Ui, Widget, WidgetText,
8};
9use std::hash::Hash;
10
11/// Dropdown widget
12pub 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    /// Creates new dropdown box.
33    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    /// Add a hint text to the Text Edit
53    pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
54        self.hint_text = hint_text.into();
55        self
56    }
57
58    /// Determine whether to filter box items based on what is in the Text Edit already
59    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    /// Determine whether to select the text when the Text Edit gains focus
65    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    /// Passes through the desired width value to the underlying Text Edit
71    pub fn desired_width(mut self, desired_width: f32) -> Self {
72        self.desired_width = desired_width.into();
73        self
74    }
75
76    /// Set a maximum height limit for the opened popup
77    pub fn max_height(mut self, height: f32) -> Self {
78        self.max_height = height.into();
79        self
80    }
81}
82
83impl<'a, F: FnMut(&mut Ui, &str) -> Response, V: AsRef<str>, I: Iterator<Item = V>> Widget
84    for DropDownBox<'a, 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            ui.memory_mut(|m| m.open_popup(popup_id));
117        }
118
119        let mut changed = false;
120        egui::popup_below_widget(
121            ui,
122            popup_id,
123            &r,
124            egui::PopupCloseBehavior::CloseOnClick,
125            |ui| {
126                if let Some(max) = max_height {
127                    ui.set_max_height(max);
128                }
129
130                ScrollArea::vertical()
131                    .max_height(f32::INFINITY)
132                    .show(ui, |ui| {
133                        for var in it {
134                            let text = var.as_ref();
135                            if filter_by_input
136                                && !buf.is_empty()
137                                && !text.to_lowercase().contains(&buf.to_lowercase())
138                            {
139                                continue;
140                            }
141
142                            if display(ui, text).clicked() {
143                                *buf = text.to_owned();
144                                changed = true;
145
146                                ui.memory_mut(|m| m.close_popup());
147                            }
148                        }
149                    });
150            },
151        );
152
153        if changed {
154            r.mark_changed();
155        }
156
157        r
158    }
159}