Skip to main content

egui_multiselect/
lib.rs

1//! egui-multiselect
2
3// With thanks to ItsEthra for inspiration https://github.com/ItsEthra/egui-dropdown
4
5//#![warn(missing_docs)]
6
7use eframe::egui::style::StyleModifier;
8use eframe::egui::{Align, Button, Id, Layout, Popup, Response, Ui, Vec2, Widget};
9use std::hash::Hash;
10
11/// MultiSelect widget
12pub struct MultiSelect<'a, F: FnMut(&mut Ui, &str) -> Response> {
13    popup_id: Id,
14    items: &'a mut Vec<String>,
15    answers: &'a mut Vec<String>,
16    options: &'a Vec<String>,
17    display: F,
18    max_opt: &'a u8,
19    toasted: &'a mut bool,
20}
21
22impl<'a, F: FnMut(&mut Ui, &str) -> Response> MultiSelect<'a, F> {
23    /// Creates new MultiSelect box.
24    pub fn new(
25        id_source: impl Hash,
26        items: &'a mut Vec<String>,
27        answers: &'a mut Vec<String>,
28        options: &'a Vec<String>,
29        display: F,
30        max_opt: &'a u8,
31        toasted: &'a mut bool,
32    ) -> Self {
33        Self {
34            popup_id: Id::new(id_source),
35            items,
36            answers,
37            options,
38            display,
39            max_opt,
40            toasted,
41        }
42    }
43}
44
45impl<'a, F: FnMut(&mut Ui, &str) -> Response> Widget for MultiSelect<'a, F> {
46    fn ui(self, ui: &mut Ui) -> Response {
47        let Self {
48            popup_id,
49            items,
50            answers,
51            options,
52            mut display,
53            max_opt,
54            toasted,
55        } = self;
56
57        if items.is_empty() && answers.is_empty() {
58            for item in options.clone() {
59                items.push(item)
60            }
61        }
62
63        let mut r = if answers.is_empty() {
64            ui.add(
65                Button::new(format!("Choose max {} options", max_opt))
66                    .min_size(Vec2 { x: 200.0, y: 22.0 }),
67            )
68        } else {
69            ui.horizontal(|ui| {
70                ui.set_width(320.0);
71                ui.horizontal_wrapped(|ui| {
72                    ui.set_max_width(220.0);
73                    for (i, item) in answers.clone().iter().enumerate() {
74                        if ui.selectable_label(true, format!("{item} x")).clicked() {
75                            answers.remove(i);
76                            answers.sort_by_key(|s| {
77                                options.iter().position(|x| x == s).unwrap_or_default()
78                            });
79                            items.push(item.clone());
80                            items.sort_by_key(|s| {
81                                options.iter().position(|x| x == s).unwrap_or_default()
82                            });
83
84                            Popup::open_id(ui.ctx(), popup_id);
85                        };
86                    }
87                });
88                ui.with_layout(Layout::right_to_left(Align::TOP), |ui| {
89                    let icon_trash = egui_phosphor::regular::TRASH.to_owned();
90                    if ui.button(icon_trash).clicked() {
91                        answers.clear();
92                        items.clear();
93                        for item in options.clone() {
94                            items.push(item)
95                        }
96                        Popup::open_id(ui.ctx(), popup_id);
97                    };
98                    let icon_open = egui_phosphor::regular::FOLDER_OPEN.to_owned();
99                    if ui.button(icon_open).clicked() && !items.is_empty() {
100                        Popup::open_id(ui.ctx(), popup_id);
101                    }
102                });
103            })
104            .response
105        };
106        let mut changed = false;
107        Popup::menu(&r)
108            .id(popup_id)
109            .style(StyleModifier::default())
110            .close_behavior(eframe::egui::PopupCloseBehavior::CloseOnClickOutside)
111            .show(|ui| {
112                eframe::egui::ScrollArea::vertical().show(ui, |ui| {
113                    for (i, var) in items.clone().iter().enumerate() {
114                        let text = var.clone();
115                        if display(ui, &text).clicked() {
116                            if answers.len() < *max_opt as usize {
117                                answers.push(text.clone());
118                                answers.sort_by_key(|s| {
119                                    options.iter().position(|x| x == s).unwrap_or_default()
120                                });
121                                items.remove(i);
122                                items.sort_by_key(|s| {
123                                    options.iter().position(|x| x == s).unwrap_or_default()
124                                });
125                                changed = true;
126                            } else {
127                                *toasted = true;
128                            }
129                        }
130                    }
131                });
132            });
133
134        if changed {
135            r.mark_changed();
136            // close when max reached:
137            if answers.len() == *max_opt as usize {
138                Popup::close_id(ui.ctx(), popup_id);
139            }
140        }
141        r
142    }
143}