egui_charts/ui/drawing_toolbar/utilities/
templates.rs1use 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
15pub struct TemplatesButton;
17
18impl TemplatesButton {
19 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 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 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 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 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}