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::{Align, Button, Id, Layout, Response, Ui, Vec2, Widget};
8use std::hash::Hash;
9
10/// MultiSelect widget
11
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        let mut r = if answers.is_empty() {
63            ui.add(
64                Button::new(format!("Choose max {} options", max_opt))
65                    .min_size(Vec2 { x: 200.0, y: 22.0 }),
66            )
67        } else {
68            ui.horizontal(|ui| {
69                ui.set_width(320.0);
70                ui.horizontal_wrapped(|ui| {
71                    ui.set_max_width(220.0);
72                    for (i, item) in answers.clone().iter().enumerate() {
73                        if ui.selectable_label(true, format!("{item} x")).clicked() {
74                            answers.remove(i);
75                            items.push(item.clone());
76                            ui.memory_mut(|m| m.open_popup(popup_id))
77                        };
78                    }
79                });
80                ui.with_layout(Layout::right_to_left(Align::TOP), |ui| {
81                    let icon_trash = egui_phosphor::regular::TRASH.to_owned();
82                    if ui.button(icon_trash).clicked() {
83                        answers.clear();
84                        items.clear();
85                        for item in options.clone() {
86                            items.push(item)
87                        }
88                        ui.memory_mut(|m| m.open_popup(popup_id))
89                    };
90                    let icon_open = egui_phosphor::regular::FOLDER_OPEN.to_owned();
91                    if ui.button(icon_open).clicked() && !items.is_empty() {
92                        ui.memory_mut(|m| m.open_popup(popup_id))
93                    }
94                });
95            })
96            .response
97        };
98        if r.clicked() {
99            ui.memory_mut(|m| m.open_popup(popup_id));
100        }
101        let mut changed = false;
102        eframe::egui::popup_below_widget(
103            ui,
104            popup_id,
105            &r,
106            eframe::egui::PopupCloseBehavior::CloseOnClickOutside,
107            |ui| {
108                eframe::egui::ScrollArea::vertical().show(ui, |ui| {
109                    for (i, var) in items.clone().iter().enumerate() {
110                        let text = var.clone();
111
112                        if display(ui, &text).clicked() {
113                            if answers.clone().len() != *max_opt as usize {
114                                answers.push(text.clone());
115                                items.remove(i);
116                                changed = true;
117                            } else {
118                                *toasted = true;
119                            }
120                        }
121                    }
122                });
123            },
124        );
125        if changed {
126            r.mark_changed();
127            if !items.is_empty() {
128                ui.memory_mut(|m| m.open_popup(popup_id))
129            }
130            if answers.len() == *max_opt as usize {
131                ui.memory_mut(|m| m.close_popup())
132            }
133        }
134        r
135    }
136}