Skip to main content

egui_dropdown/
lib.rs

1//! egui-dropdown
2
3#![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
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<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}