1use eframe::egui::style::StyleModifier;
8use eframe::egui::{Align, Button, Id, Layout, Popup, Response, Ui, Vec2, Widget};
9use std::hash::Hash;
10
11pub 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 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 if answers.len() == *max_opt as usize {
138 Popup::close_id(ui.ctx(), popup_id);
139 }
140 }
141 r
142 }
143}