egui_charts/ui_kit/modal/dialogs/
prompt.rs1use crate::localization as l10n;
6use crate::theme::ThemeExt;
7use egui::{Context, Id, TextEdit, Widget};
8
9use crate::tokens::DESIGN_TOKENS;
10use crate::ui_kit::dialog::DialogFooter;
11use crate::ui_kit::modal::Modal;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum PromptResult {
16 None,
18 Submitted(String),
20 Cancelled,
22}
23
24impl PromptResult {
25 pub fn submitted(&self) -> Option<&str> {
27 match self {
28 PromptResult::Submitted(s) => Some(s),
29 _ => None,
30 }
31 }
32
33 pub fn is_cancelled(&self) -> bool {
35 matches!(self, PromptResult::Cancelled)
36 }
37
38 pub fn is_open(&self) -> bool {
40 matches!(self, PromptResult::None)
41 }
42}
43
44pub struct PromptDialog {
46 id: Id,
47 title: String,
48 message: Option<String>,
49 placeholder: String,
50 initial_value: String,
51 submit_text: String,
52 cancel_text: String,
53 multiline: bool,
54}
55
56impl PromptDialog {
57 pub fn new(id: impl std::hash::Hash + std::fmt::Debug, title: impl Into<String>) -> Self {
59 Self {
60 id: Id::unique(id),
61 title: title.into(),
62 message: None,
63 placeholder: String::new(),
64 initial_value: String::new(),
65 submit_text: "OK".to_string(),
66 cancel_text: "Cancel".to_string(),
67 multiline: false,
68 }
69 }
70
71 pub fn message(mut self, message: impl Into<String>) -> Self {
73 self.message = Some(message.into());
74 self
75 }
76
77 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
79 self.placeholder = placeholder.into();
80 self
81 }
82
83 pub fn initial_value(mut self, value: impl Into<String>) -> Self {
85 self.initial_value = value.into();
86 self
87 }
88
89 pub fn submit_text(mut self, text: impl Into<String>) -> Self {
91 self.submit_text = text.into();
92 self
93 }
94
95 pub fn cancel_text(mut self, text: impl Into<String>) -> Self {
97 self.cancel_text = text.into();
98 self
99 }
100
101 pub fn multiline(mut self, multiline: bool) -> Self {
103 self.multiline = multiline;
104 self
105 }
106
107 pub fn show(self, ctx: &Context, open: &mut bool) -> PromptResult {
112 let value_id = self.id.with("value");
113 if !*open {
114 ctx.data_mut(|d| d.remove::<String>(value_id));
115 return PromptResult::None;
116 }
117 let mut value = ctx
118 .data_mut(|d| d.remove_temp::<String>(value_id))
119 .unwrap_or(self.initial_value);
120
121 let width = if self.multiline {
122 DESIGN_TOKENS.sizing.dialog.prompt_width_lg
123 } else {
124 DESIGN_TOKENS.sizing.dialog.prompt_width_sm
125 };
126
127 let Some(response) = Modal::new(self.id, &self.title)
128 .width(width)
129 .show(ctx, open, |ui| {
130 if let Some(msg) = &self.message {
131 ui.label(l10n::text(ui.ctx(), msg));
132 ui.add_space(ctx.active_theme().spacing.sm);
133 }
134
135 let text_edit = if self.multiline {
136 TextEdit::multiline(&mut value).desired_rows(4)
137 } else {
138 TextEdit::singleline(&mut value)
139 };
140 let input = text_edit
141 .hint_text(l10n::text(ui.ctx(), &self.placeholder))
142 .desired_width(f32::INFINITY)
143 .ui(ui);
144 let entered = !self.multiline
146 && input.lost_focus()
147 && ui.input(|i| i.key_pressed(egui::Key::Enter));
148 if !entered && !input.has_focus() {
149 input.request_focus();
150 }
151
152 let (submit, cancel) = DialogFooter::new(&self.submit_text)
153 .secondary(&self.cancel_text)
154 .show(ui);
155 (submit || entered, cancel)
156 })
157 else {
158 return PromptResult::None;
159 };
160
161 let result = match response.inner {
162 (true, _) => PromptResult::Submitted(value),
163 (_, true) => PromptResult::Cancelled,
164 _ if response.close_requested => PromptResult::Cancelled,
165 _ => {
166 ctx.data_mut(|d| d.insert_temp(value_id, value));
167 PromptResult::None
168 }
169 };
170 if result != PromptResult::None {
171 *open = false;
172 }
173 result
174 }
175}
176
177pub fn prompt(
179 ctx: &Context,
180 id: impl std::hash::Hash + std::fmt::Debug,
181 title: impl Into<String>,
182 open: &mut bool,
183) -> PromptResult {
184 PromptDialog::new(id, title).show(ctx, open)
185}
186
187pub fn prompt_with_message(
189 ctx: &Context,
190 id: impl std::hash::Hash + std::fmt::Debug,
191 title: impl Into<String>,
192 message: impl Into<String>,
193 open: &mut bool,
194) -> PromptResult {
195 PromptDialog::new(id, title)
196 .message(message)
197 .show(ctx, open)
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use egui::{Event, Key, Modifiers, RawInput};
204
205 #[test]
206 fn enter_submits_the_typed_text() {
207 let ctx = Context::default();
208 let mut open = true;
209 let mut frame = |events: Vec<Event>| {
210 let mut result = PromptResult::None;
211 let input = RawInput {
212 events,
213 ..Default::default()
214 };
215 ctx.run_ui(input, |ui| {
216 result = PromptDialog::new("p", "Name").show(ui.ctx(), &mut open)
217 })
218 .drop_without_applying_deltas();
219 result
220 };
221 frame(vec![]); frame(vec![Event::Text("x".into())]);
223 let enter = Event::Key {
224 key: Key::Enter,
225 physical_key: None,
226 pressed: true,
227 repeat: false,
228 modifiers: Modifiers::NONE,
229 };
230 assert_eq!(frame(vec![enter]), PromptResult::Submitted("x".into()));
231 assert!(!open);
232 }
233}