Skip to main content

kas_widgets/
dialog.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Dialog boxes
7//!
8//! KAS dialog boxes are pre-configured windows, usually allowing some
9//! customisation.
10//!
11//! # Design status
12//!
13//! At the current time, only a minimal selection of dialog boxes are provided
14//! and their design is likely to change.
15
16use crate::{
17    AccessLabel, Button, EditBox, Filler, ScrollLabel, SelectableLabel, adapt::AdaptWidgetAny,
18};
19use kas::prelude::*;
20use kas::runner::AppData;
21use kas::text::format::FormattableText;
22use std::error::Error;
23use std::fmt::Write;
24
25#[derive(Copy, Clone, Debug)]
26struct MessageBoxOk;
27
28#[impl_self]
29mod MessageBox {
30    /// A simple message box.
31    #[widget]
32    #[layout(column! [self.label, self.button])]
33    pub struct MessageBox<T: FormattableText + 'static> {
34        core: widget_core!(),
35        #[widget]
36        label: SelectableLabel<T>,
37        #[widget]
38        button: Button<AccessLabel>,
39    }
40
41    impl Self {
42        /// Construct
43        pub fn new(message: T) -> Self {
44            MessageBox {
45                core: Default::default(),
46                label: SelectableLabel::new(message),
47                button: Button::label_msg("&Ok", MessageBoxOk),
48            }
49        }
50
51        /// Build a [`Window`]
52        pub fn into_window<A: AppData>(self, title: impl ToString) -> Window<A> {
53            Window::new(self.map_any(), title).with_restrictions(true, true)
54        }
55
56        /// Display as a modal window with the given `title`
57        pub fn display(self, cx: &mut EventCx, title: impl ToString) {
58            cx.add_dataless_window(self.into_window(title), true);
59        }
60    }
61
62    impl Events for Self {
63        type Data = ();
64
65        fn configure(&mut self, cx: &mut ConfigCx) {
66            cx.register_nav_fallback(self.id());
67        }
68
69        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
70            match event {
71                Event::Command(Command::Escape, _) | Event::Command(Command::Enter, _) => {
72                    cx.close_own_window();
73                    Used
74                }
75                _ => Unused,
76            }
77        }
78
79        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
80            if let Some(MessageBoxOk) = cx.try_pop() {
81                cx.close_own_window();
82            }
83        }
84    }
85}
86
87#[derive(Copy, Clone, Debug)]
88pub struct ErrorResult;
89
90#[impl_self]
91mod AlertError {
92    /// Alert user about an error
93    #[widget]
94    #[layout(column! [
95        self.label,
96        self.details,
97        self.ok,
98    ])]
99    pub struct AlertError<T: FormattableText + 'static> {
100        core: widget_core!(),
101        parent: Id,
102        title: String,
103        #[widget]
104        label: SelectableLabel<T>,
105        #[widget]
106        details: ScrollLabel<String>,
107        #[widget]
108        ok: Button<AccessLabel>,
109    }
110
111    impl Self {
112        /// Construct
113        pub fn new(message: T, error: &dyn Error) -> Self {
114            let mut details = format!("{error}");
115            let mut source = error.source();
116            while let Some(error) = source {
117                write!(&mut details, "\nCause: {error}").expect("write! to String failed");
118                source = error.source();
119            }
120
121            AlertError {
122                core: Default::default(),
123                parent: Id::default(),
124                title: "Error".to_string(),
125                label: SelectableLabel::new(message),
126                details: ScrollLabel::new(details),
127                ok: Button::label_msg("&Ok", ErrorResult),
128            }
129        }
130
131        /// Set a custom window title
132        pub fn with_title(mut self, title: impl ToString) -> Self {
133            self.title = title.to_string();
134            self
135        }
136
137        /// Display as a modal window
138        ///
139        /// On closure, an [`ErrorResult`] message will be sent to `parent`.
140        pub fn display_for(mut self, cx: &mut EventCx, parent: Id) {
141            self.parent = parent;
142            let title = std::mem::take(&mut self.title);
143            let window = Window::new(self, title).with_restrictions(true, true);
144            cx.add_dataless_window(window, true);
145        }
146    }
147
148    impl Events for Self {
149        type Data = ();
150
151        fn configure(&mut self, cx: &mut ConfigCx) {
152            cx.register_nav_fallback(self.id());
153        }
154
155        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
156            let result = match event {
157                Event::Command(Command::Escape, _) | Event::Command(Command::Enter, _) => {
158                    ErrorResult
159                }
160                _ => return Unused,
161            };
162
163            cx.send(self.parent.clone(), result);
164            cx.close_own_window();
165            Used
166        }
167
168        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
169            if let Some(result) = cx.try_pop::<ErrorResult>() {
170                cx.send(self.parent.clone(), result);
171                cx.close_own_window();
172            }
173        }
174    }
175}
176
177#[derive(Copy, Clone, Debug)]
178pub enum UnsavedResult {
179    Save,
180    Discard,
181    Cancel,
182}
183
184#[impl_self]
185mod AlertUnsaved {
186    /// Alert user that they have unsaved changes
187    #[widget]
188    #[layout(column! [
189        self.label,
190        row![self.save, self.discard, self.cancel],
191    ])]
192    pub struct AlertUnsaved<T: FormattableText + 'static> {
193        core: widget_core!(),
194        parent: Id,
195        title: String,
196        #[widget]
197        label: SelectableLabel<T>,
198        #[widget]
199        save: Button<AccessLabel>,
200        #[widget]
201        discard: Button<AccessLabel>,
202        #[widget]
203        cancel: Button<AccessLabel>,
204    }
205
206    impl Self {
207        /// Construct
208        pub fn new(message: T) -> Self {
209            AlertUnsaved {
210                core: Default::default(),
211                parent: Id::default(),
212                title: "Unsaved changes".to_string(),
213                label: SelectableLabel::new(message),
214                save: Button::label_msg("&Save", UnsavedResult::Save),
215                discard: Button::label_msg("&Discard", UnsavedResult::Discard),
216                cancel: Button::label_msg("&Cancel", UnsavedResult::Cancel),
217            }
218        }
219
220        /// Set a custom window title
221        pub fn with_title(mut self, title: impl ToString) -> Self {
222            self.title = title.to_string();
223            self
224        }
225
226        /// Display as a modal window
227        ///
228        /// On closure, an [`UnsavedResult`] message will be sent to `parent`.
229        pub fn display_for(mut self, cx: &mut EventCx, parent: Id) {
230            self.parent = parent;
231            let title = std::mem::take(&mut self.title);
232            let window = Window::new(self, title).with_restrictions(true, true);
233            cx.add_dataless_window(window, true);
234        }
235    }
236
237    impl Events for Self {
238        type Data = ();
239
240        fn configure(&mut self, cx: &mut ConfigCx) {
241            cx.register_nav_fallback(self.id());
242        }
243
244        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
245            let result = match event {
246                Event::Command(Command::Escape, _) => UnsavedResult::Cancel,
247                Event::Command(Command::Enter, _) => {
248                    if let Some(focus) = cx.nav_focus() {
249                        if self.save.is_ancestor_of(focus) {
250                            UnsavedResult::Save
251                        } else if self.discard.is_ancestor_of(focus) {
252                            UnsavedResult::Discard
253                        } else if self.cancel.is_ancestor_of(focus) {
254                            UnsavedResult::Cancel
255                        } else {
256                            return Unused;
257                        }
258                    } else {
259                        return Unused;
260                    }
261                }
262                _ => return Unused,
263            };
264
265            cx.send(self.parent.clone(), result);
266            cx.close_own_window();
267            Used
268        }
269
270        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
271            if let Some(result) = cx.try_pop::<UnsavedResult>() {
272                cx.send(self.parent.clone(), result);
273                cx.close_own_window();
274            }
275        }
276    }
277}
278
279/// Message sent by [`TextEdit`] on closure.
280#[derive(Debug)]
281pub enum TextEditResult {
282    Cancel,
283    Ok(String),
284}
285
286#[derive(Clone, Debug)]
287struct MsgClose(bool);
288
289#[impl_self]
290mod TextEdit {
291    #[widget]
292    #[layout(grid! {
293        (0..=2, 0) => self.edit,
294        (0, 1) => Filler::maximize(),
295        (1, 1) => Button::label_msg("&Cancel", MsgClose(false)),
296        (2, 1) => Button::label_msg("&Save", MsgClose(true)),
297    })]
298    /// An editor over a `String`
299    ///
300    /// Emits a [`TextEditResult`] message when the "Ok" or "Cancel" button is
301    /// pressed. When used as a pop-up, it is up to the caller to close on this
302    /// message.
303    pub struct TextEdit {
304        core: widget_core!(),
305        #[widget]
306        edit: EditBox,
307    }
308
309    impl Self {
310        /// Construct
311        pub fn new(text: impl ToString, multi_line: bool) -> Self {
312            TextEdit {
313                core: Default::default(),
314                edit: EditBox::text(text).with_multi_line(multi_line),
315            }
316        }
317
318        /// Set text, clearing undo history
319        pub fn set_text(&mut self, cx: &mut EventState, text: impl ToString) {
320            self.edit.clear(cx);
321            self.edit.set_string(cx, text.to_string());
322        }
323
324        /// Build a [`Window`]
325        pub fn into_window<A: AppData>(self, title: impl ToString) -> Window<A> {
326            Window::new(self.map_any(), title)
327        }
328
329        fn close(&mut self, cx: &mut EventCx, commit: bool) -> IsUsed {
330            cx.push(if commit {
331                TextEditResult::Ok(self.edit.clone_string())
332            } else {
333                TextEditResult::Cancel
334            });
335            Used
336        }
337    }
338
339    impl Events for Self {
340        type Data = ();
341
342        /* NOTE: this makes sense for a window but not an embedded editor.
343        fn configure(&mut self, cx: &mut ConfigCx) {
344            cx.register_nav_fallback(self.id());
345
346            // Focus first item initially:
347            if cx.nav_focus().is_none() {
348                cx.next_nav_focus(self.id(), false, FocusSource::Synthetic);
349            }
350        }*/
351
352        fn handle_event(&mut self, cx: &mut EventCx, _: &Self::Data, event: Event) -> IsUsed {
353            match event {
354                Event::Command(Command::Escape, _) => self.close(cx, false),
355                Event::Command(Command::Enter, _) => self.close(cx, true),
356                _ => Unused,
357            }
358        }
359
360        fn handle_messages(&mut self, cx: &mut EventCx, _: &Self::Data) {
361            if let Some(MsgClose(commit)) = cx.try_pop() {
362                let _ = self.close(cx, commit);
363            }
364        }
365    }
366}