1use std::hash::Hash;
29
30use egui::{
31 CornerRadius, Id, Popup, PopupCloseBehavior, Pos2, Response, Sense, Ui, Vec2, Widget,
32 WidgetInfo, WidgetText, WidgetType,
33};
34
35use crate::theme::{with_alpha, Theme};
36
37#[derive(Debug, Clone)]
42#[must_use = "Call `.show_below(&trigger, |ui| ...)` to render the menu."]
43pub struct Menu {
44 id_salt: Id,
45 min_width: f32,
46}
47
48impl Menu {
49 pub fn new(id_salt: impl Hash) -> Self {
53 Self {
54 id_salt: Id::new(("elegance::menu", Id::new(id_salt))),
55 min_width: 180.0,
56 }
57 }
58
59 pub fn min_width(mut self, min_width: f32) -> Self {
61 self.min_width = min_width;
62 self
63 }
64
65 pub fn show_below<R>(
68 self,
69 trigger: &Response,
70 add_contents: impl FnOnce(&mut Ui) -> R,
71 ) -> Option<R> {
72 let popup_id = Id::new(self.id_salt);
73 Popup::menu(trigger)
74 .id(popup_id)
75 .close_behavior(PopupCloseBehavior::CloseOnClick)
76 .show(|ui| {
77 ui.set_min_width(self.min_width);
78 ui.spacing_mut().item_spacing.y = 2.0;
80 add_contents(ui)
81 })
82 .map(|r| r.inner)
83 }
84}
85
86#[must_use = "Add with `ui.add(...)`."]
91pub struct MenuItem {
92 label: WidgetText,
93 shortcut: Option<String>,
94 danger: bool,
95 enabled: bool,
96}
97
98impl std::fmt::Debug for MenuItem {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("MenuItem")
101 .field("label", &self.label.text())
102 .field("shortcut", &self.shortcut)
103 .field("danger", &self.danger)
104 .field("enabled", &self.enabled)
105 .finish()
106 }
107}
108
109impl MenuItem {
110 pub fn new(label: impl Into<WidgetText>) -> Self {
112 Self {
113 label: label.into(),
114 shortcut: None,
115 danger: false,
116 enabled: true,
117 }
118 }
119
120 pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
123 self.shortcut = Some(shortcut.into());
124 self
125 }
126
127 pub fn danger(mut self) -> Self {
130 self.danger = true;
131 self
132 }
133
134 pub fn enabled(mut self, enabled: bool) -> Self {
137 self.enabled = enabled;
138 self
139 }
140}
141
142impl Widget for MenuItem {
143 fn ui(self, ui: &mut Ui) -> Response {
144 let theme = Theme::current(ui.ctx());
145 let p = &theme.palette;
146 let t = &theme.typography;
147
148 let pad_x = 10.0;
149 let pad_y = 6.0;
150 let gap_x = 16.0;
151
152 let label_color = if !self.enabled {
153 p.text_faint
154 } else if self.danger {
155 p.danger
156 } else {
157 p.text
158 };
159
160 let label_galley =
161 crate::theme::placeholder_galley(ui, self.label.text(), t.body, false, f32::INFINITY);
162
163 let shortcut_galley = self
164 .shortcut
165 .as_deref()
166 .map(|s| crate::theme::placeholder_galley(ui, s, t.small, false, f32::INFINITY));
167
168 let content_w =
169 label_galley.size().x + shortcut_galley.as_ref().map_or(0.0, |g| g.size().x + gap_x);
170 let desired = Vec2::new(
171 ui.available_width().max(content_w + pad_x * 2.0),
172 label_galley.size().y.max(t.body) + pad_y * 2.0,
173 );
174
175 let sense = if self.enabled {
176 Sense::click()
177 } else {
178 Sense::hover()
179 };
180 let (rect, response) = ui.allocate_exact_size(desired, sense);
181
182 if ui.is_rect_visible(rect) {
183 let is_hovered = response.hovered() && self.enabled;
184 if is_hovered {
185 let bg = if self.danger {
186 with_alpha(p.red, 40)
187 } else {
188 with_alpha(p.sky, 28)
189 };
190 let radius = CornerRadius::same((theme.control_radius as u8).saturating_sub(2));
191 ui.painter().rect_filled(rect, radius, bg);
192 }
193
194 let label_pos = Pos2::new(
195 rect.min.x + pad_x,
196 rect.center().y - label_galley.size().y * 0.5,
197 );
198 ui.painter().galley(label_pos, label_galley, label_color);
199
200 if let Some(galley) = shortcut_galley {
201 let pos = Pos2::new(
202 rect.max.x - pad_x - galley.size().x,
203 rect.center().y - galley.size().y * 0.5,
204 );
205 let color = if !self.enabled {
206 p.text_faint
207 } else if self.danger {
208 with_alpha(p.danger, 200)
209 } else {
210 p.text_muted
211 };
212 ui.painter().galley(pos, galley, color);
213 }
214 }
215
216 response.widget_info(|| {
217 WidgetInfo::labeled(WidgetType::Button, self.enabled, self.label.text())
218 });
219 response
220 }
221}