Skip to main content

egui_components/
icon.rs

1//! `Icon` — a small set of vector icons drawn with the painter.
2//!
3//! egui bundles no icon font, and gpui-component's Lucide set is far too large
4//! to vendor, so this provides the handful of glyphs the other components
5//! need (chevrons, check, close, search, …) as stroke-drawn shapes. Each is
6//! laid out inside a unit box and scaled to the requested size, so they stay
7//! crisp at any scale.
8//!
9//! ```ignore
10//! ui.add(sc::Icon::new(sc::IconKind::Search).size(18.0));
11//! ```
12
13use egui::{pos2, vec2, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Widget};
14use egui_components_theme::Theme;
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum IconKind {
18    Check,
19    Close,
20    ChevronRight,
21    ChevronDown,
22    ChevronLeft,
23    ChevronUp,
24    Search,
25    Menu,
26    Plus,
27    Minus,
28    Bell,
29    Info,
30    Warning,
31    Error,
32    Home,
33    Settings,
34    User,
35    File,
36    Folder,
37    Trash,
38    Star,
39    Heart,
40}
41
42pub struct Icon {
43    kind: IconKind,
44    size: f32,
45    color: Option<Color32>,
46    stroke_width: f32,
47}
48
49impl Icon {
50    pub fn new(kind: IconKind) -> Self {
51        Self {
52            kind,
53            size: 16.0,
54            color: None,
55            stroke_width: 1.6,
56        }
57    }
58    pub fn size(mut self, s: f32) -> Self {
59        self.size = s;
60        self
61    }
62    pub fn color(mut self, c: Color32) -> Self {
63        self.color = Some(c);
64        self
65    }
66    pub fn stroke_width(mut self, w: f32) -> Self {
67        self.stroke_width = w;
68        self
69    }
70}
71
72impl Widget for Icon {
73    fn ui(self, ui: &mut Ui) -> Response {
74        let (rect, response) = ui.allocate_exact_size(vec2(self.size, self.size), Sense::hover());
75        if ui.is_rect_visible(rect) {
76            let color = self
77                .color
78                .unwrap_or_else(|| Theme::get(ui.ctx()).colors.foreground);
79            paint_icon(ui.painter(), self.kind, rect, color, self.stroke_width);
80        }
81        response
82    }
83}
84
85/// Paint `kind` inside `rect` — reusable by other components that already own
86/// a painter and rect (menus, sidebar, alerts, …).
87pub fn paint_icon(painter: &egui::Painter, kind: IconKind, rect: Rect, color: Color32, sw: f32) {
88    let stroke = Stroke::new(sw, color);
89    // Map unit coordinates (0..1) inside a slightly inset box to screen space.
90    let inset = rect.size().min_elem() * 0.12;
91    let b = rect.shrink(inset);
92    let p = |x: f32, y: f32| -> Pos2 { pos2(b.left() + x * b.width(), b.top() + y * b.height()) };
93    let line = |a: Pos2, c: Pos2| {
94        painter.line_segment([a, c], stroke);
95    };
96    let poly = |pts: &[Pos2]| {
97        for w in pts.windows(2) {
98            painter.line_segment([w[0], w[1]], stroke);
99        }
100    };
101
102    match kind {
103        IconKind::Check => poly(&[p(0.15, 0.55), p(0.42, 0.8), p(0.85, 0.22)]),
104        IconKind::Close => {
105            line(p(0.2, 0.2), p(0.8, 0.8));
106            line(p(0.8, 0.2), p(0.2, 0.8));
107        }
108        IconKind::ChevronRight => poly(&[p(0.4, 0.2), p(0.7, 0.5), p(0.4, 0.8)]),
109        IconKind::ChevronLeft => poly(&[p(0.6, 0.2), p(0.3, 0.5), p(0.6, 0.8)]),
110        IconKind::ChevronDown => poly(&[p(0.2, 0.4), p(0.5, 0.7), p(0.8, 0.4)]),
111        IconKind::ChevronUp => poly(&[p(0.2, 0.6), p(0.5, 0.3), p(0.8, 0.6)]),
112        IconKind::Search => {
113            painter.circle_stroke(p(0.42, 0.42), b.width() * 0.26, stroke);
114            line(p(0.62, 0.62), p(0.85, 0.85));
115        }
116        IconKind::Menu => {
117            line(p(0.15, 0.28), p(0.85, 0.28));
118            line(p(0.15, 0.5), p(0.85, 0.5));
119            line(p(0.15, 0.72), p(0.85, 0.72));
120        }
121        IconKind::Plus => {
122            line(p(0.5, 0.18), p(0.5, 0.82));
123            line(p(0.18, 0.5), p(0.82, 0.5));
124        }
125        IconKind::Minus => line(p(0.18, 0.5), p(0.82, 0.5)),
126        IconKind::Bell => {
127            // Dome + clapper.
128            poly(&[
129                p(0.25, 0.68),
130                p(0.25, 0.45),
131                p(0.32, 0.28),
132                p(0.5, 0.22),
133                p(0.68, 0.28),
134                p(0.75, 0.45),
135                p(0.75, 0.68),
136            ]);
137            line(p(0.18, 0.68), p(0.82, 0.68));
138            poly(&[p(0.43, 0.78), p(0.5, 0.84), p(0.57, 0.78)]);
139        }
140        IconKind::Info => {
141            painter.circle_stroke(b.center(), b.width() * 0.42, stroke);
142            painter.circle_filled(p(0.5, 0.32), sw * 0.8, color);
143            line(p(0.5, 0.46), p(0.5, 0.72));
144        }
145        IconKind::Warning => {
146            poly(&[p(0.5, 0.18), p(0.9, 0.82), p(0.1, 0.82), p(0.5, 0.18)]);
147            line(p(0.5, 0.4), p(0.5, 0.62));
148            painter.circle_filled(p(0.5, 0.72), sw * 0.7, color);
149        }
150        IconKind::Error => {
151            painter.circle_stroke(b.center(), b.width() * 0.42, stroke);
152            line(p(0.35, 0.35), p(0.65, 0.65));
153            line(p(0.65, 0.35), p(0.35, 0.65));
154        }
155        IconKind::Home => {
156            poly(&[p(0.15, 0.5), p(0.5, 0.18), p(0.85, 0.5)]);
157            poly(&[
158                p(0.25, 0.45),
159                p(0.25, 0.82),
160                p(0.75, 0.82),
161                p(0.75, 0.45),
162            ]);
163        }
164        IconKind::Settings => {
165            painter.circle_stroke(b.center(), b.width() * 0.18, stroke);
166            painter.circle_stroke(b.center(), b.width() * 0.4, stroke);
167            for k in 0..8 {
168                let a = std::f32::consts::TAU * (k as f32) / 8.0;
169                let (s, c) = a.sin_cos();
170                let inner = b.center() + vec2(c, s) * b.width() * 0.4;
171                let outer = b.center() + vec2(c, s) * b.width() * 0.5;
172                painter.line_segment([inner, outer], stroke);
173            }
174        }
175        IconKind::User => {
176            painter.circle_stroke(p(0.5, 0.35), b.width() * 0.18, stroke);
177            poly(&[p(0.22, 0.82), p(0.3, 0.6), p(0.7, 0.6), p(0.78, 0.82)]);
178        }
179        IconKind::File => {
180            poly(&[
181                p(0.28, 0.15),
182                p(0.6, 0.15),
183                p(0.75, 0.3),
184                p(0.75, 0.85),
185                p(0.28, 0.85),
186                p(0.28, 0.15),
187            ]);
188            poly(&[p(0.6, 0.15), p(0.6, 0.3), p(0.75, 0.3)]);
189        }
190        IconKind::Folder => {
191            poly(&[
192                p(0.15, 0.78),
193                p(0.15, 0.3),
194                p(0.42, 0.3),
195                p(0.5, 0.4),
196                p(0.85, 0.4),
197                p(0.85, 0.78),
198                p(0.15, 0.78),
199            ]);
200        }
201        IconKind::Trash => {
202            line(p(0.2, 0.28), p(0.8, 0.28));
203            poly(&[p(0.4, 0.28), p(0.42, 0.18), p(0.58, 0.18), p(0.6, 0.28)]);
204            poly(&[p(0.28, 0.28), p(0.32, 0.85), p(0.68, 0.85), p(0.72, 0.28)]);
205        }
206        IconKind::Star => {
207            let pts = star_points(b.center(), b.width() * 0.45, b.width() * 0.18, 5);
208            poly(&pts);
209        }
210        IconKind::Heart => {
211            painter.circle_stroke(p(0.33, 0.38), b.width() * 0.16, stroke);
212            painter.circle_stroke(p(0.67, 0.38), b.width() * 0.16, stroke);
213            poly(&[p(0.19, 0.45), p(0.5, 0.82), p(0.81, 0.45)]);
214        }
215    }
216}
217
218fn star_points(center: Pos2, outer: f32, inner: f32, points: usize) -> Vec<Pos2> {
219    let mut out = Vec::with_capacity(points * 2 + 1);
220    let start = -std::f32::consts::FRAC_PI_2;
221    for k in 0..points * 2 {
222        let r = if k % 2 == 0 { outer } else { inner };
223        let a = start + std::f32::consts::PI * (k as f32) / points as f32;
224        let (s, c) = a.sin_cos();
225        out.push(center + vec2(c, s) * r);
226    }
227    out.push(out[0]);
228    out
229}