1use crate::theme::get_global_color;
2use egui::{
3 ecolor::Color32, pos2, FontId, Rect, Response,
4 Sense, Ui, Vec2, Widget,
5};
6
7#[derive(Clone, Copy, PartialEq, Debug)]
9pub enum BadgeColor {
10 Primary,
12 Error,
14 Success,
16 Warning,
18 Neutral,
20 Custom(Color32, Color32), }
23
24#[derive(Clone, Copy, PartialEq, Debug)]
26pub enum BadgeSize {
27 Small,
29 Regular,
31 Large,
33}
34
35#[derive(Clone, Copy, PartialEq, Debug)]
37pub enum BadgePosition {
38 TopRight,
40 TopLeft,
42 BottomRight,
44 BottomLeft,
46 Custom(Vec2),
48}
49
50#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
75pub struct MaterialBadge {
76 content: String,
78 color: BadgeColor,
80 size: BadgeSize,
82 dot: bool,
84 position_offset: Vec2,
86}
87
88impl MaterialBadge {
89 pub fn new(content: impl Into<String>) -> Self {
94 Self {
95 content: content.into(),
96 color: BadgeColor::Error,
97 size: BadgeSize::Regular,
98 dot: false,
99 position_offset: Vec2::new(0.0, 0.0),
100 }
101 }
102
103 pub fn dot() -> Self {
105 Self {
106 content: String::new(),
107 color: BadgeColor::Error,
108 size: BadgeSize::Small,
109 dot: true,
110 position_offset: Vec2::new(0.0, 0.0),
111 }
112 }
113
114 pub fn color(mut self, color: BadgeColor) -> Self {
116 self.color = color;
117 self
118 }
119
120 pub fn size(mut self, size: BadgeSize) -> Self {
122 self.size = size;
123 self
124 }
125
126 pub fn as_dot(mut self, dot: bool) -> Self {
128 self.dot = dot;
129 self
130 }
131
132 pub fn position_offset(mut self, offset: Vec2) -> Self {
134 self.position_offset = offset;
135 self
136 }
137
138 pub fn draw_on(
147 &self,
148 ui: &mut Ui,
149 target_rect: Rect,
150 position: BadgePosition,
151 ) -> Response {
152 let (bg_color, text_color) = self.get_colors();
153 let (min_width, min_height, font_size) = self.get_dimensions();
154
155 let painter = ui.painter();
156
157 let text_galley = if !self.dot && !self.content.is_empty() {
159 Some(painter.layout_no_wrap(
160 self.content.clone(),
161 FontId::proportional(font_size),
162 text_color,
163 ))
164 } else {
165 None
166 };
167
168 let badge_width = if let Some(ref galley) = text_galley {
169 (galley.size().x + min_width).max(min_height)
170 } else {
171 min_height
172 };
173 let badge_height = min_height;
174
175 let overlap_factor = 0.95;
178 let badge_pos = match position {
179 BadgePosition::TopRight => {
180 pos2(
181 target_rect.max.x - badge_width * (1.0 - overlap_factor),
182 target_rect.min.y - badge_height * (1.0 - overlap_factor),
183 )
184 }
185 BadgePosition::TopLeft => {
186 pos2(
187 target_rect.min.x - badge_width * overlap_factor,
188 target_rect.min.y - badge_height * (1.0 - overlap_factor),
189 )
190 }
191 BadgePosition::BottomRight => {
192 pos2(
193 target_rect.max.x - badge_width * (1.0 - overlap_factor),
194 target_rect.max.y - badge_height * (1.0 - overlap_factor),
195 )
196 }
197 BadgePosition::BottomLeft => {
198 pos2(
199 target_rect.min.x - badge_width * overlap_factor,
200 target_rect.max.y - badge_height * (1.0 - overlap_factor),
201 )
202 }
203 BadgePosition::Custom(offset) => {
204 pos2(target_rect.center().x + offset.x, target_rect.center().y + offset.y)
205 }
206 };
207
208 let badge_pos = pos2(
209 badge_pos.x + self.position_offset.x,
210 badge_pos.y + self.position_offset.y,
211 );
212
213 let badge_rect = Rect::from_center_size(badge_pos, Vec2::new(badge_width, badge_height));
214
215 painter.rect_filled(badge_rect, badge_height / 2.0, bg_color);
217
218 if let Some(galley) = text_galley {
220 painter.galley(
221 pos2(
222 badge_rect.center().x - galley.size().x / 2.0,
223 badge_rect.center().y - galley.size().y / 2.0,
224 ),
225 galley,
226 text_color,
227 );
228 }
229
230 ui.interact(badge_rect, ui.id().with("badge"), Sense::hover())
231 }
232
233 fn get_colors(&self) -> (Color32, Color32) {
234 match self.color {
235 BadgeColor::Primary => {
236 let bg = get_global_color("primary");
237 let text = get_global_color("onPrimary");
238 (bg, text)
239 }
240 BadgeColor::Error => (
241 Color32::from_rgb(239, 68, 68), Color32::WHITE,
243 ),
244 BadgeColor::Success => (
245 Color32::from_rgb(34, 197, 94), Color32::WHITE,
247 ),
248 BadgeColor::Warning => (
249 Color32::from_rgb(234, 179, 8), Color32::WHITE,
251 ),
252 BadgeColor::Neutral => (
253 Color32::from_rgb(107, 114, 128), Color32::WHITE,
255 ),
256 BadgeColor::Custom(bg, text) => (bg, text),
257 }
258 }
259
260 fn get_dimensions(&self) -> (f32, f32, f32) {
261 match self.size {
263 BadgeSize::Small => {
264 if self.dot {
265 (0.0, 8.0, 0.0) } else {
267 (4.0, 16.0, 10.0) }
269 }
270 BadgeSize::Regular => {
271 if self.dot {
272 (0.0, 10.0, 0.0) } else {
274 (6.0, 20.0, 12.0) }
276 }
277 BadgeSize::Large => {
278 if self.dot {
279 (0.0, 12.0, 0.0) } else {
281 (8.0, 24.0, 14.0) }
283 }
284 }
285 }
286}
287
288impl Widget for MaterialBadge {
289 fn ui(self, ui: &mut Ui) -> Response {
290 let (bg_color, text_color) = self.get_colors();
291 let (min_width, min_height, font_size) = self.get_dimensions();
292
293 let text_galley = if !self.dot && !self.content.is_empty() {
295 Some(ui.painter().layout_no_wrap(
296 self.content.clone(),
297 FontId::proportional(font_size),
298 text_color,
299 ))
300 } else {
301 None
302 };
303
304 let badge_width = if let Some(ref galley) = text_galley {
305 (galley.size().x + min_width).max(min_height)
306 } else {
307 min_height
308 };
309 let badge_height = min_height;
310
311 let desired_size = Vec2::new(badge_width, badge_height);
312 let (rect, response) = ui.allocate_exact_size(desired_size, Sense::hover());
313
314 ui.painter()
316 .rect_filled(rect, badge_height / 2.0, bg_color);
317
318 if let Some(galley) = text_galley {
320 ui.painter().galley(
321 pos2(
322 rect.center().x - galley.size().x / 2.0,
323 rect.center().y - galley.size().y / 2.0,
324 ),
325 galley,
326 text_color,
327 );
328 }
329
330 response
331 }
332}
333
334pub fn badge(content: impl Into<String>) -> MaterialBadge {
343 MaterialBadge::new(content)
344}
345
346pub fn badge_dot() -> MaterialBadge {
355 MaterialBadge::dot()
356}