egui_dropdown/
lib.rs

1//! egui-dropdown
2
3#![warn(missing_docs)]
4
5use egui::{
6    Id, RectAlign, Response, ScrollArea, TextEdit, Ui, Widget, WidgetText, text::{CCursor, CCursorRange}
7};
8use std::hash::Hash;
9
10/// Dropdown widget
11pub 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    /// Creates new dropdown box.
32    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    /// Add a hint text to the Text Edit
52    pub fn hint_text(mut self, hint_text: impl Into<WidgetText>) -> Self {
53        self.hint_text = hint_text.into();
54        self
55    }
56
57    /// Determine whether to filter box items based on what is in the Text Edit already
58    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    /// Determine whether to select the text when the Text Edit gains focus
64    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    /// Passes through the desired width value to the underlying Text Edit
70    pub fn desired_width(mut self, desired_width: f32) -> Self {
71        self.desired_width = desired_width.into();
72        self
73    }
74
75    /// Set a maximum height limit for the opened popup
76    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}