Skip to main content

egui_components/
tag.rs

1//! `Tag` widget. Like [`Badge`](crate::badge::Badge) but with an
2//! optional close (×) button and click semantics.
3
4use crate::common::Variant;
5use egui::{
6    pos2, vec2, Color32, FontId, Rect, Response, Sense, Stroke, Ui, WidgetText,
7};
8use egui_components_theme::{Theme, ThemeColor};
9
10pub struct Tag {
11    label: WidgetText,
12    variant: Variant,
13    closable: bool,
14}
15
16pub struct TagResponse {
17    pub response: Response,
18    pub close_clicked: bool,
19}
20
21impl Tag {
22    pub fn new(label: impl Into<WidgetText>) -> Self {
23        Self {
24            label: label.into(),
25            variant: Variant::Secondary,
26            closable: false,
27        }
28    }
29    pub fn variant(mut self, v: Variant) -> Self {
30        self.variant = v;
31        self
32    }
33    pub fn closable(mut self) -> Self {
34        self.closable = true;
35        self
36    }
37
38    /// Show the tag, returning a [`TagResponse`] with click + close info.
39    pub fn show(self, ui: &mut Ui) -> TagResponse {
40        let theme = Theme::get(ui.ctx());
41        let m = theme.metrics;
42        let pad_x = 8.0;
43        let pad_y = 3.0;
44        let close_w = if self.closable { 16.0 } else { 0.0 };
45        let font = FontId::proportional(m.font_size_xs);
46        let galley = self.label.into_galley(
47            ui,
48            Some(egui::TextWrapMode::Extend),
49            f32::INFINITY,
50            font,
51        );
52        let desired = vec2(
53            galley.size().x + pad_x * 2.0 + close_w,
54            galley.size().y + pad_y * 2.0,
55        );
56        let (rect, response) = ui.allocate_exact_size(desired, Sense::click());
57
58        let (bg, fg) = tag_colors(&theme.colors, self.variant);
59        let radius = theme.corner_sm();
60
61        let mut close_clicked = false;
62        if ui.is_rect_visible(rect) {
63            let painter = ui.painter();
64            let bg_eff = if response.hovered() {
65                egui_components_theme::mix(bg, Color32::WHITE, 0.05)
66            } else {
67                bg
68            };
69            painter.rect(
70                rect,
71                radius,
72                bg_eff,
73                Stroke::new(1.0, egui_components_theme::mix(bg, Color32::BLACK, 0.1)),
74                egui::StrokeKind::Inside,
75            );
76
77            let text_pos = pos2(rect.left() + pad_x, rect.center().y - galley.size().y * 0.5);
78            painter.galley_with_override_text_color(text_pos, galley.clone(), fg);
79
80            if self.closable {
81                let cx = rect.right() - pad_x - 4.0;
82                let cy = rect.center().y;
83                let close_size = 10.0;
84                let close_rect = Rect::from_center_size(pos2(cx, cy), vec2(close_size, close_size));
85                let close_response = ui.interact(
86                    close_rect.expand(2.0),
87                    response.id.with("close"),
88                    Sense::click(),
89                );
90                let stroke_color = if close_response.hovered() {
91                    Color32::WHITE
92                } else {
93                    fg
94                };
95                painter.line_segment(
96                    [close_rect.left_top(), close_rect.right_bottom()],
97                    Stroke::new(1.2, stroke_color),
98                );
99                painter.line_segment(
100                    [close_rect.right_top(), close_rect.left_bottom()],
101                    Stroke::new(1.2, stroke_color),
102                );
103                if close_response.clicked() {
104                    close_clicked = true;
105                }
106                if close_response.hovered() {
107                    ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
108                }
109            }
110        }
111
112        TagResponse { response, close_clicked }
113    }
114}
115
116fn tag_colors(c: &ThemeColor, v: Variant) -> (Color32, Color32) {
117    match v {
118        Variant::Primary => (c.primary_background, c.primary_foreground),
119        Variant::Secondary => (c.secondary_background, c.secondary_foreground),
120        Variant::Ghost => (c.muted_background, c.muted_foreground),
121        Variant::Outline => (c.background, c.foreground),
122        Variant::Link => (c.link_foreground, c.background),
123        Variant::Danger => (c.danger_background, c.danger_foreground),
124        Variant::Success => (c.success_background, c.success_foreground),
125        Variant::Warning => (c.warning_background, c.warning_foreground),
126        Variant::Info => (c.info_background, c.info_foreground),
127    }
128}