Skip to main content

egui_charts/ui/drawing_toolbar/utilities/
templates.rs

1//! Drawing template management UI.
2//!
3//! Provides standalone host menu rows and a name dialog for saving
4//! and loading drawing templates.
5
6use crate::localization as l10n;
7use crate::theme::ThemeExt;
8use crate::tokens::DESIGN_TOKENS;
9use crate::ui::drawing_toolbar::DrawingTemplate;
10use crate::ui::drawing_toolbar::DrawingToolbarAction;
11use crate::ui::drawing_toolbar::submenu_builder::SubmenuBuilder;
12use crate::ui_kit::ext::UiExt;
13use egui::{Rect, Ui};
14
15/// Drawing template rows and save dialog.
16pub struct TemplatesButton;
17
18impl TemplatesButton {
19    /// Render the templates dropdown rows.
20    ///
21    /// Shows save template option, followed by existing templates
22    /// that can be loaded.
23    pub fn show_submenu(
24        ui: &mut Ui,
25        templates: &[DrawingTemplate],
26    ) -> Option<DrawingToolbarAction> {
27        let mut builder = SubmenuBuilder::new(ui).add_text_item_with_action(
28            "Save as template…",
29            "Save current drawing properties as a reusable template",
30            || Some(DrawingToolbarAction::OpenTemplateMenu),
31        );
32
33        builder = builder.add_text_item_with_action(
34            "Manage templates…",
35            "Load or delete saved drawing templates",
36            || Some(DrawingToolbarAction::ManageTemplates),
37        );
38
39        // List existing templates
40        for template in templates {
41            let template_id = template.id.clone();
42            let name = if template.is_default {
43                format!("{} (default)", template.name)
44            } else {
45                template.name.clone()
46            };
47            let tooltip = format!(
48                "Load template: {} ({})",
49                template.name, template.drawing_type
50            );
51
52            builder = builder.add_text_item_with_action(&name, &tooltip, move || {
53                Some(DrawingToolbarAction::LoadTemplate {
54                    template_id: template_id.clone(),
55                })
56            });
57        }
58
59        builder.show()
60    }
61
62    /// Render a standalone save-template name input dialog.
63    ///
64    /// Returns a `SaveTemplate` action if the user confirms,
65    /// [`DrawingToolbarAction::None`] if they cancel with the Cancel button or Escape, which
66    /// also clears `name_input`, and `None` while they are still editing.
67    pub fn show_name_dialog(
68        ui: &mut Ui,
69        sidebar_rect: Rect,
70        name_input: &mut String,
71    ) -> Option<DrawingToolbarAction> {
72        let popup_width = DESIGN_TOKENS.sizing.drawing_toolbar_ext.submenu_width_lg;
73        let popup_pos = egui::Pos2::new(
74            sidebar_rect.right() + ui.active_theme().spacing.sm,
75            sidebar_rect.center().y - DESIGN_TOKENS.sizing.button_xxl,
76        );
77
78        let mut action = None;
79
80        egui::Area::new(egui::Id::unique("template_name_dialog"))
81            .fixed_pos(popup_pos)
82            .order(egui::Order::Foreground)
83            .constrain(true)
84            .show(ui.ctx(), |ui| {
85                egui::Frame::popup(ui.style())
86                    .fill(ui.style().visuals.window_fill())
87                    .stroke(ui.style().visuals.window_stroke)
88                    .corner_radius(ui.active_theme().radius.lg)
89                    .show(ui, |ui| {
90                        ui.set_min_width(popup_width);
91                        ui.space_lg();
92
93                        ui.label(
94                            egui::RichText::new(l10n::text(ui.ctx(), "Save Template"))
95                                .size(ui.active_theme().text.body)
96                                .strong(),
97                        );
98                        ui.space_md();
99
100                        ui.label(l10n::text(ui.ctx(), "Template name:"));
101                        ui.space_sm();
102
103                        let text_response = ui.text_edit_singleline(name_input);
104
105                        let focus_in_dialog = ui
106                            .memory(|m| m.focused())
107                            .and_then(|id| ui.ctx().read_response(id))
108                            .is_some_and(|response| response.layer_id == ui.layer_id());
109                        if !focus_in_dialog && !text_response.lost_focus() {
110                            text_response.request_focus();
111                        }
112
113                        ui.space_lg();
114
115                        ui.horizontal(|ui| {
116                            let can_save = !name_input.trim().is_empty();
117
118                            if ui
119                                .add_enabled(
120                                    can_save,
121                                    egui::Button::new(l10n::text(ui.ctx(), "Save")),
122                                )
123                                .clicked()
124                            {
125                                action = Some(DrawingToolbarAction::SaveTemplate {
126                                    name: name_input.trim().to_string(),
127                                });
128                            }
129
130                            if ui.button(l10n::text(ui.ctx(), "Cancel")).clicked() {
131                                name_input.clear();
132                                action = Some(DrawingToolbarAction::None);
133                            }
134                        });
135
136                        // Enter key to confirm
137                        if ui.input(|i| i.key_pressed(egui::Key::Enter))
138                            && (text_response.has_focus()
139                                || ui.memory(|m| m.had_focus_last_frame(text_response.id)))
140                            && !name_input.trim().is_empty()
141                        {
142                            action = Some(DrawingToolbarAction::SaveTemplate {
143                                name: name_input.trim().to_string(),
144                            });
145                        }
146
147                        // Escape key to cancel
148                        if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
149                            name_input.clear();
150                            action = Some(DrawingToolbarAction::None);
151                        }
152
153                        ui.space_md();
154                    });
155            });
156
157        action
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn keyboard_focus_reaches_cancel_without_saving_the_template() {
167        let ctx = egui::Context::default();
168        let mut name = "Example".to_owned();
169        let mut frame = |key: Option<egui::Key>| {
170            let mut action = None;
171            ctx.run_ui(
172                egui::RawInput {
173                    events: key
174                        .map(|key| egui::Event::Key {
175                            key,
176                            physical_key: None,
177                            pressed: true,
178                            repeat: false,
179                            modifiers: egui::Modifiers::NONE,
180                        })
181                        .into_iter()
182                        .collect(),
183                    ..Default::default()
184                },
185                |ui| {
186                    action = TemplatesButton::show_name_dialog(
187                        ui,
188                        Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(48.0, 200.0)),
189                        &mut name,
190                    );
191                },
192            )
193            .drop_without_applying_deltas();
194            (action, ctx.memory(|m| m.focused()))
195        };
196        frame(None);
197        let name_field = frame(None).1;
198        let save = frame(Some(egui::Key::Tab)).1;
199        assert_ne!(save, name_field);
200        frame(Some(egui::Key::Tab));
201        assert!(matches!(
202            frame(Some(egui::Key::Enter)).0,
203            Some(DrawingToolbarAction::None)
204        ));
205        assert!(name.is_empty());
206    }
207}