egui_charts/ui/dialogs/templates/
dialog.rs1use super::TemplateStore;
4use crate::localization as l10n;
5use crate::services::StoreKind;
6use crate::ui_kit::modal::Modal;
7use serde::{Serialize, de::DeserializeOwned};
8
9pub struct TemplateDialog<T> {
12 pub templates: TemplateStore<T>,
14 pub is_open: bool,
16 current: Option<T>,
17 name: String,
18}
19
20impl<T: Clone + Serialize + DeserializeOwned> TemplateDialog<T> {
21 pub fn new(kind: StoreKind) -> Self {
23 Self {
24 templates: TemplateStore::new(kind),
25 is_open: false,
26 current: None,
27 name: String::new(),
28 }
29 }
30
31 pub fn open(&mut self, current: T) {
33 self.current = Some(current);
34 self.name.clear();
35 self.is_open = true;
36 }
37
38 pub fn open_catalog(&mut self) {
40 self.current = None;
41 self.name.clear();
42 self.is_open = true;
43 }
44
45 pub fn show(&mut self, ctx: &egui::Context, title: &str) -> Option<T> {
48 self.templates.poll(ctx);
49 if !self.is_open {
50 return None;
51 }
52 let mut loaded = None;
53 let mut open = self.is_open;
54 Modal::new(title, title)
55 .width(440.0)
56 .show(ctx, &mut open, |ui| {
57 egui::ScrollArea::vertical()
58 .max_height(ui.available_height())
59 .show(ui, |ui| {
60 let ready = self.templates.ready();
61 if self.current.is_some() {
62 ui.label(l10n::text(ui.ctx(), "Template name"));
63 ui.add(
64 egui::TextEdit::singleline(&mut self.name)
65 .desired_width(ui.available_width()),
66 );
67 let label = if self.templates.entries().contains_key(self.name.trim()) {
68 "Replace"
69 } else {
70 "Save"
71 };
72 if ui
73 .add_enabled(
74 ready && !self.name.trim().is_empty(),
75 egui::Button::new(l10n::text(ui.ctx(), label)),
76 )
77 .clicked()
78 && let Some(current) = &self.current
79 && let Err(error) = self.templates.save(&self.name, current.clone())
80 {
81 self.templates.error = Some(error);
82 }
83 }
84 ui.separator();
85 let names: Vec<_> = self.templates.entries().keys().cloned().collect();
86 if names.is_empty() && ready {
87 ui.label(l10n::text(ui.ctx(), "No saved templates"));
88 }
89 for name in names {
90 ui.add(egui::Label::new(l10n::text(ui.ctx(), &name)).truncate())
91 .on_hover_text(l10n::text(ui.ctx(), &name));
92 ui.horizontal(|ui| {
93 if ui
94 .add_enabled(
95 ready,
96 egui::Button::new(l10n::text(ui.ctx(), "Load")),
97 )
98 .clicked()
99 {
100 loaded = self.templates.entries().get(&name).cloned();
101 }
102 if ui
103 .add_enabled(
104 ready,
105 egui::Button::new(l10n::text(ui.ctx(), "Delete")),
106 )
107 .clicked()
108 && let Err(error) = self.templates.delete(&name)
109 {
110 self.templates.error = Some(error);
111 }
112 });
113 }
114 if let Some(error) = &self.templates.error {
115 ui.colored_label(
116 ui.visuals().error_fg_color,
117 l10n::text(ui.ctx(), error),
118 );
119 if !self.templates.ready()
120 && ui
121 .button(l10n::text(ui.ctx(), "Retry loading templates"))
122 .clicked()
123 {
124 self.templates.reload();
125 }
126 } else if !ready {
127 ui.label(l10n::text(ui.ctx(), "Loading templates…"));
128 }
129 });
130 });
131 self.is_open = open && loaded.is_none();
132 loaded
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn catalog_actions_remain_visible_with_long_names_in_short_and_narrow_windows() {
142 for theme in [crate::theme::Theme::light(), crate::theme::Theme::dark()] {
143 for size in [egui::vec2(1440.0, 900.0), egui::vec2(420.0, 360.0)] {
144 let ctx = egui::Context::default();
145 crate::theme::apply_to_egui(&ctx, &theme);
146 let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, size);
147 let mut dialog = TemplateDialog::new(StoreKind::StudyTemplates);
148 for index in 0..20 {
149 dialog
150 .templates
151 .save(&format!("{index:02} {}", "Long name ".repeat(20)), index)
152 .unwrap();
153 }
154 dialog.open(42);
155 for frame in 0..3 {
156 let output = ctx.run_ui(
157 egui::RawInput {
158 screen_rect: Some(screen),
159 ..Default::default()
160 },
161 |ui| {
162 dialog.show(ui.ctx(), "Study templates");
163 },
164 );
165 let shapes = output.shapes.clone();
166 output.drop_without_applying_deltas();
167 if frame > 0 {
168 let area = ctx
169 .memory(|memory| memory.area_rect(egui::Id::unique("Study templates")))
170 .unwrap();
171 assert!(
172 screen.shrink(23.0).contains_rect(area),
173 "{size:?}: {area:?}"
174 );
175 for label in ["Save", "Load", "Delete"] {
176 let (clip, rect) = shapes
177 .iter()
178 .find_map(|shape| match &shape.shape {
179 egui::Shape::Text(text) if text.galley.text() == label => {
180 Some((
181 shape.clip_rect,
182 egui::Rect::from_min_size(text.pos, text.galley.size()),
183 ))
184 }
185 _ => None,
186 })
187 .expect("catalog action is painted");
188 assert!(
189 screen.contains_rect(rect) && clip.contains_rect(rect),
190 "{label}: {rect:?}"
191 );
192 }
193 }
194 }
195 }
196 }
197 }
198}