Skip to main content

egui_charts/ui/drawing_toolbar/
glyph_picker.rs

1//! Searchable bundled icons, emoji and stickers with session-local recents.
2
3use super::DrawingToolbarAction;
4use crate::drawings::glyphs::{self, GLYPHS, Glyph};
5use crate::drawings::{DrawingToolRegistry, DrawingToolType};
6use crate::localization as l10n;
7use egui::{Rect, Ui, Vec2};
8
9/// Session state for the drawing glyph picker.
10#[derive(Clone, Debug)]
11pub struct GlyphPicker {
12    /// Search by glyph name across icons, emoji and stickers.
13    pub query: String,
14    /// Size in logical pixels, independent of line width.
15    pub size: f32,
16    group: &'static str,
17    recent: Vec<&'static str>,
18}
19
20impl Default for GlyphPicker {
21    fn default() -> Self {
22        Self {
23            query: String::new(),
24            size: 40.0,
25            group: "All",
26            recent: Vec::new(),
27        }
28    }
29}
30
31impl GlyphPicker {
32    pub(crate) fn show(
33        &mut self,
34        ui: &mut Ui,
35        registry: &DrawingToolRegistry,
36    ) -> Option<DrawingToolbarAction> {
37        ui.spacing_mut().item_spacing = egui::vec2(6.0, 6.0);
38        ui.set_width(312.0_f32.min(ui.ctx().content_rect().width() - 32.0));
39        ui.add(
40            egui::TextEdit::singleline(&mut self.query)
41                .hint_text(l10n::text(ui.ctx(), "Search icons, emoji and stickers"))
42                .desired_width(f32::INFINITY),
43        );
44        ui.horizontal_wrapped(|ui| {
45            for group in ["All", "Icons", "Emoji", "Stickers"] {
46                ui.selectable_value(&mut self.group, group, l10n::text(ui.ctx(), group));
47            }
48        });
49        ui.horizontal(|ui| {
50            ui.label(l10n::text(ui.ctx(), "Size"));
51            ui.add(
52                egui::DragValue::new(&mut self.size)
53                    .range(8.0..=128.0)
54                    .suffix(l10n::text(ui.ctx(), " px")),
55            );
56        });
57        ui.separator();
58        let query = self.query.trim().to_lowercase();
59        let ctx = ui.ctx().clone();
60        let matches = |glyph: &&Glyph| {
61            registry.availability(tool_type(glyph.id)).visible
62                && ((query.is_empty()
63                    && (self.group == "All" || glyph.category.starts_with(self.group)))
64                    || (!query.is_empty()
65                        && (glyph.name.to_lowercase().contains(&query)
66                            || l10n::text(&ctx, glyph.name).to_lowercase().contains(&query))))
67        };
68        let mut picked = None;
69        egui::ScrollArea::vertical()
70            .max_height(400.0)
71            .show(ui, |ui| {
72                let recent: Vec<_> = self
73                    .recent
74                    .iter()
75                    .filter_map(|id| GLYPHS.iter().find(|g| g.id == *id))
76                    .filter(matches)
77                    .collect();
78                if !recent.is_empty() {
79                    ui.label(l10n::text(ui.ctx(), "Recent"));
80                    picked = cells(ui, &recent, registry);
81                }
82                let filtered: Vec<_> = GLYPHS.iter().filter(matches).collect();
83                if filtered.is_empty() {
84                    ui.label(l10n::text(ui.ctx(), "No matching glyphs"));
85                }
86                let mut start = 0;
87                while start < filtered.len() {
88                    let category = filtered[start].category;
89                    let end = start
90                        + filtered[start..]
91                            .iter()
92                            .take_while(|g| g.category == category)
93                            .count();
94                    ui.label(l10n::text(ui.ctx(), category));
95                    picked = picked.or(cells(ui, &filtered[start..end], registry));
96                    start = end;
97                }
98            });
99        let glyph = picked?;
100        self.recent.retain(|id| *id != glyph);
101        self.recent.insert(0, glyph);
102        self.recent.truncate(12);
103        Some(DrawingToolbarAction::InsertGlyph {
104            icon_name: glyph,
105            size: self.size,
106        })
107    }
108}
109
110fn cells(ui: &mut Ui, glyphs: &[&Glyph], registry: &DrawingToolRegistry) -> Option<&'static str> {
111    let columns = ((ui.available_width() / 48.0) as usize).max(1);
112    let mut picked = None;
113    for row in glyphs.chunks(columns) {
114        ui.horizontal(|ui| {
115            for glyph in row {
116                let response = ui
117                    .add_enabled(
118                        registry.availability(tool_type(glyph.id)).enabled,
119                        egui::Button::new("").min_size(Vec2::splat(40.0)),
120                    )
121                    .on_hover_text(l10n::text(ui.ctx(), glyph.name));
122                let translated_label = l10n::text(ui.ctx(), glyph.name);
123                response.widget_info(|| {
124                    egui::WidgetInfo::labeled(egui::Role::Button, true, &translated_label)
125                });
126                let rect = Rect::from_center_size(response.rect.center(), Vec2::splat(26.0));
127                let color = ui.visuals().text_color();
128                if !glyphs::paint(ui.painter(), glyph.id, rect, color) {
129                    ui.painter().text(
130                        rect.center(),
131                        egui::Align2::CENTER_CENTER,
132                        l10n::text(ui.ctx(), glyph.id),
133                        egui::FontId::proportional(24.0),
134                        color,
135                    );
136                }
137                if response.clicked() {
138                    picked = Some(glyph.id);
139                }
140            }
141        });
142    }
143    picked
144}
145
146fn tool_type(id: &str) -> DrawingToolType {
147    if id.starts_with("sticker:") {
148        DrawingToolType::Sticker
149    } else {
150        DrawingToolType::FontIcon
151    }
152}