Skip to main content

egui_charts/ui_kit/modal/dialogs/
prompt.rs

1//! Prompt Dialog
2//!
3//! A dialog with a text input field.
4
5use 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/// Result of showing a prompt dialog
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum PromptResult {
16    /// Dialog is still open, no action taken
17    None,
18    /// User submitted the input
19    Submitted(String),
20    /// User cancelled the dialog
21    Cancelled,
22}
23
24impl PromptResult {
25    /// Get the submitted value if any
26    pub fn submitted(&self) -> Option<&str> {
27        match self {
28            PromptResult::Submitted(s) => Some(s),
29            _ => None,
30        }
31    }
32
33    /// Check if cancelled
34    pub fn is_cancelled(&self) -> bool {
35        matches!(self, PromptResult::Cancelled)
36    }
37
38    /// Check if the dialog is still open
39    pub fn is_open(&self) -> bool {
40        matches!(self, PromptResult::None)
41    }
42}
43
44/// A prompt dialog with text input
45pub 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    /// Create a new prompt dialog
58    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    /// Set a message to display above the input
72    pub fn message(mut self, message: impl Into<String>) -> Self {
73        self.message = Some(message.into());
74        self
75    }
76
77    /// Set the placeholder text
78    pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
79        self.placeholder = placeholder.into();
80        self
81    }
82
83    /// Set the initial value
84    pub fn initial_value(mut self, value: impl Into<String>) -> Self {
85        self.initial_value = value.into();
86        self
87    }
88
89    /// Set the submit button text
90    pub fn submit_text(mut self, text: impl Into<String>) -> Self {
91        self.submit_text = text.into();
92        self
93    }
94
95    /// Set the cancel button text
96    pub fn cancel_text(mut self, text: impl Into<String>) -> Self {
97        self.cancel_text = text.into();
98        self
99    }
100
101    /// Make the input multiline
102    pub fn multiline(mut self, multiline: bool) -> Self {
103        self.multiline = multiline;
104        self
105    }
106
107    /// Show the prompt dialog
108    ///
109    /// The typed text is kept between frames while the dialog is open. Enter in a single-line
110    /// input submits; Escape, the close button and a click on the backdrop cancel.
111    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                // Enter in a single-line input surrenders focus; read that before refocusing.
145                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
177/// Show a simple prompt dialog
178pub 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
187/// Show a prompt dialog with a message
188pub 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![]); // the input takes focus
222        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}