rat_widget/
dialog_frame.rs

1//!
2//! A simple dialog frame and buttons.
3//!
4use crate::_private::NonExhaustive;
5use crate::button::{Button, ButtonState, ButtonStyle};
6use crate::event::{
7    ButtonOutcome, ConsumedEvent, Dialog, HandleEvent, Outcome, Regular, ct_event, flow,
8};
9use crate::focus::{FocusBuilder, FocusFlag, HasFocus};
10use crate::layout::{DialogItem, LayoutOuter};
11use crate::text::HasScreenCursor;
12use crate::util::{block_padding2, reset_buf_area};
13use crossterm::event::Event;
14use rat_event::MouseOnly;
15use rat_reloc::RelocatableState;
16use ratatui::buffer::Buffer;
17use ratatui::layout::{Constraint, Flex, Position, Rect, Size};
18use ratatui::style::Style;
19use ratatui::widgets::{Block, BorderType, StatefulWidget, Widget};
20
21/// Renders the frame and the Ok/Cancel buttons for a dialog window.
22///
23/// After rendering BaseDialogState::widget_area is available
24/// to render any content.
25#[derive(Debug, Clone, Default)]
26pub struct DialogFrame<'a> {
27    block: Block<'a>,
28    button_style: ButtonStyle,
29    layout: LayoutOuter,
30    ok_text: &'a str,
31    no_cancel: bool,
32    cancel_text: &'a str,
33}
34
35/// Combined styles.
36#[derive(Debug, Clone)]
37pub struct DialogFrameStyle {
38    pub style: Style,
39    pub block: Option<Block<'static>>,
40    pub border_style: Option<Style>,
41    pub title_style: Option<Style>,
42    pub button_style: Option<ButtonStyle>,
43    pub layout: Option<LayoutOuter>,
44    pub ok_text: Option<&'static str>,
45    pub no_cancel: Option<bool>,
46    pub cancel_text: Option<&'static str>,
47    pub non_exhaustive: NonExhaustive,
48}
49
50impl Default for DialogFrameStyle {
51    fn default() -> Self {
52        Self {
53            style: Default::default(),
54            block: Default::default(),
55            border_style: Default::default(),
56            title_style: Default::default(),
57            button_style: Default::default(),
58            layout: Default::default(),
59            ok_text: Default::default(),
60            no_cancel: Default::default(),
61            cancel_text: Default::default(),
62            non_exhaustive: NonExhaustive,
63        }
64    }
65}
66
67#[derive(Debug, Clone)]
68pub struct DialogFrameState {
69    /// Area for the dialog.
70    /// __read only__ set with each render.
71    pub area: Rect,
72    /// Area for the dialog-content.
73    /// __read only__ set with each render.
74    pub widget_area: Rect,
75
76    /// ok-button
77    pub ok: ButtonState,
78    /// no cancel button
79    pub no_cancel: bool,
80    /// cancel-button
81    pub cancel: ButtonState,
82
83    pub non_exhaustive: NonExhaustive,
84}
85
86impl<'a> DialogFrame<'a> {
87    pub fn new() -> Self {
88        Self {
89            block: Block::bordered().border_type(BorderType::Plain),
90            button_style: Default::default(),
91            layout: LayoutOuter::new()
92                .left(Constraint::Percentage(19))
93                .top(Constraint::Length(3))
94                .right(Constraint::Percentage(19))
95                .bottom(Constraint::Length(3)),
96            ok_text: "Ok",
97            no_cancel: false,
98            cancel_text: "Cancel",
99        }
100    }
101
102    pub fn styles(mut self, styles: DialogFrameStyle) -> Self {
103        if let Some(block) = styles.block {
104            self.block = block;
105        }
106        self.block = self.block.style(styles.style);
107        if let Some(border_style) = styles.border_style {
108            self.block = self.block.border_style(border_style);
109        }
110        if let Some(title_style) = styles.title_style {
111            self.block = self.block.title_style(title_style);
112        }
113        if let Some(button_style) = styles.button_style {
114            self.button_style = button_style;
115        }
116        if let Some(layout) = styles.layout {
117            self.layout = layout;
118        }
119        if let Some(ok_text) = styles.ok_text {
120            self.ok_text = ok_text;
121        }
122        if let Some(no_cancel) = styles.no_cancel {
123            self.no_cancel = no_cancel;
124        }
125        if let Some(cancel_text) = styles.cancel_text {
126            self.cancel_text = cancel_text;
127        }
128        self
129    }
130
131    /// Base style for the dialog.
132    pub fn style(mut self, style: Style) -> Self {
133        self.block = self.block.style(style);
134        self
135    }
136
137    /// Block for the dialog.
138    pub fn block(mut self, block: Block<'a>) -> Self {
139        self.block = block;
140        self
141    }
142
143    /// Sets the border-style for the Block, if any.
144    pub fn border_style(mut self, style: Style) -> Self {
145        self.block = self.block.border_style(style);
146        self
147    }
148
149    /// Sets the title-style for the Block, if any.
150    pub fn title_style(mut self, style: Style) -> Self {
151        self.block = self.block.title_style(style);
152        self
153    }
154
155    /// Button style.
156    pub fn button_style(mut self, style: ButtonStyle) -> Self {
157        self.button_style = style;
158        self
159    }
160
161    /// Ok text.
162    pub fn ok_text(mut self, str: &'a str) -> Self {
163        self.ok_text = str;
164        self
165    }
166
167    /// No cancel button.
168    pub fn no_cancel(mut self) -> Self {
169        self.no_cancel = true;
170        self
171    }
172
173    /// Cancel text.
174    pub fn cancel_text(mut self, str: &'a str) -> Self {
175        self.cancel_text = str;
176        self
177    }
178
179    /// Margin constraint for the left side.
180    pub fn left(mut self, left: Constraint) -> Self {
181        self.layout = self.layout.left(left);
182        self
183    }
184
185    /// Margin constraint for the top side.
186    pub fn top(mut self, top: Constraint) -> Self {
187        self.layout = self.layout.top(top);
188        self
189    }
190
191    /// Margin constraint for the right side.
192    pub fn right(mut self, right: Constraint) -> Self {
193        self.layout = self.layout.right(right);
194        self
195    }
196
197    /// Margin constraint for the bottom side.
198    pub fn bottom(mut self, bottom: Constraint) -> Self {
199        self.layout = self.layout.bottom(bottom);
200        self
201    }
202
203    /// Put at a fixed position.
204    pub fn position(mut self, pos: Position) -> Self {
205        self.layout = self.layout.position(pos);
206        self
207    }
208
209    /// Constraint for the width.
210    pub fn width(mut self, width: Constraint) -> Self {
211        self.layout = self.layout.width(width);
212        self
213    }
214
215    /// Constraint for the height.
216    pub fn height(mut self, height: Constraint) -> Self {
217        self.layout = self.layout.height(height);
218        self
219    }
220
221    /// Set at a fixed size.
222    pub fn size(mut self, size: Size) -> Self {
223        self.layout = self.layout.size(size);
224        self
225    }
226
227    /// Calculate the resulting dialog area.
228    /// Returns the inner area that is available for drawing widgets.
229    pub fn layout_size(&self, area: Rect) -> Rect {
230        let l_dlg = if self.no_cancel {
231            self.layout.layout_dialog(
232                area,
233                block_padding2(&self.block),
234                [Constraint::Length(10)],
235                1,
236                Flex::End,
237            )
238        } else {
239            self.layout.layout_dialog(
240                area,
241                block_padding2(&self.block),
242                [Constraint::Length(12), Constraint::Length(10)],
243                1,
244                Flex::End,
245            )
246        };
247        l_dlg.widget_for(DialogItem::Content)
248    }
249}
250
251impl<'a> StatefulWidget for DialogFrame<'a> {
252    type State = DialogFrameState;
253
254    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
255        let l_dlg = if self.no_cancel {
256            self.layout.layout_dialog(
257                area,
258                block_padding2(&self.block),
259                [Constraint::Length(10)],
260                1,
261                Flex::End,
262            )
263        } else {
264            self.layout.layout_dialog(
265                area,
266                block_padding2(&self.block),
267                [Constraint::Length(12), Constraint::Length(10)],
268                1,
269                Flex::End,
270            )
271        };
272        state.area = l_dlg.area();
273        state.widget_area = l_dlg.widget_for(DialogItem::Content);
274        state.no_cancel = self.no_cancel;
275
276        reset_buf_area(l_dlg.area(), buf);
277        self.block.render(state.area, buf);
278
279        if self.no_cancel {
280            Button::new(self.ok_text).styles(self.button_style).render(
281                l_dlg.widget_for(DialogItem::Button(0)),
282                buf,
283                &mut state.ok,
284            );
285        } else {
286            Button::new(self.cancel_text)
287                .styles(self.button_style.clone())
288                .render(
289                    l_dlg.widget_for(DialogItem::Button(0)),
290                    buf,
291                    &mut state.cancel,
292                );
293            Button::new(self.ok_text).styles(self.button_style).render(
294                l_dlg.widget_for(DialogItem::Button(1)),
295                buf,
296                &mut state.ok,
297            );
298        }
299    }
300}
301
302impl Default for DialogFrameState {
303    fn default() -> Self {
304        let z = Self {
305            area: Default::default(),
306            widget_area: Default::default(),
307            ok: Default::default(),
308            no_cancel: Default::default(),
309            cancel: Default::default(),
310            non_exhaustive: NonExhaustive,
311        };
312        z.ok.focus.set(true);
313        z
314    }
315}
316
317impl HasFocus for DialogFrameState {
318    fn build(&self, builder: &mut FocusBuilder) {
319        builder.widget(&self.ok);
320        if !self.no_cancel {
321            builder.widget(&self.cancel);
322        }
323    }
324
325    fn focus(&self) -> FocusFlag {
326        unimplemented!()
327    }
328
329    fn area(&self) -> Rect {
330        unimplemented!()
331    }
332}
333
334impl HasScreenCursor for DialogFrameState {
335    fn screen_cursor(&self) -> Option<(u16, u16)> {
336        None
337    }
338}
339
340impl RelocatableState for DialogFrameState {
341    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
342        self.area.relocate(shift, clip);
343        self.widget_area.relocate(shift, clip);
344        self.ok.relocate(shift, clip);
345        self.cancel.relocate(shift, clip);
346    }
347}
348
349impl DialogFrameState {
350    pub fn new() -> Self {
351        Self::default()
352    }
353
354    pub fn named(_name: &str) -> Self {
355        Self::default()
356    }
357}
358
359/// Result type for event-handling.
360pub enum DialogOutcome {
361    /// Continue with event-handling.
362    /// In the event-loop this waits for the next event.
363    Continue,
364    /// Break event-handling without repaint.
365    /// In the event-loop this waits for the next event.
366    Unchanged,
367    /// Break event-handling and repaints/renders the application.
368    /// In the event-loop this calls `render`.
369    Changed,
370    /// Ok pressed
371    Ok,
372    /// Cancel pressed
373    Cancel,
374}
375
376impl ConsumedEvent for DialogOutcome {
377    fn is_consumed(&self) -> bool {
378        !matches!(self, DialogOutcome::Continue)
379    }
380}
381
382impl From<DialogOutcome> for Outcome {
383    fn from(value: DialogOutcome) -> Self {
384        match value {
385            DialogOutcome::Continue => Outcome::Continue,
386            DialogOutcome::Unchanged => Outcome::Unchanged,
387            DialogOutcome::Changed => Outcome::Changed,
388            DialogOutcome::Ok => Outcome::Changed,
389            DialogOutcome::Cancel => Outcome::Changed,
390        }
391    }
392}
393
394impl From<Outcome> for DialogOutcome {
395    fn from(value: Outcome) -> Self {
396        match value {
397            Outcome::Continue => DialogOutcome::Continue,
398            Outcome::Unchanged => DialogOutcome::Unchanged,
399            Outcome::Changed => DialogOutcome::Changed,
400        }
401    }
402}
403
404impl<'a> HandleEvent<Event, Dialog, DialogOutcome> for DialogFrameState {
405    fn handle(&mut self, event: &Event, _: Dialog) -> DialogOutcome {
406        flow!({
407            if !self.no_cancel {
408                match self.cancel.handle(event, Regular) {
409                    ButtonOutcome::Pressed => DialogOutcome::Cancel,
410                    r => Outcome::from(r).into(),
411                }
412            } else {
413                DialogOutcome::Continue
414            }
415        });
416        flow!(match self.ok.handle(event, Regular) {
417            ButtonOutcome::Pressed => {
418                DialogOutcome::Ok
419            }
420            r => Outcome::from(r).into(),
421        });
422
423        flow!(match event {
424            ct_event!(keycode press Esc) if !self.no_cancel => {
425                DialogOutcome::Cancel
426            }
427            ct_event!(keycode press Enter) | ct_event!(keycode press F(12)) => {
428                DialogOutcome::Ok
429            }
430            _ => DialogOutcome::Unchanged,
431        });
432
433        DialogOutcome::Unchanged
434    }
435}
436
437impl<'a> HandleEvent<Event, MouseOnly, DialogOutcome> for DialogFrameState {
438    fn handle(&mut self, event: &Event, _: MouseOnly) -> DialogOutcome {
439        flow!({
440            if !self.no_cancel {
441                match self.cancel.handle(event, MouseOnly) {
442                    ButtonOutcome::Pressed => DialogOutcome::Cancel,
443                    r => Outcome::from(r).into(),
444                }
445            } else {
446                DialogOutcome::Continue
447            }
448        });
449        flow!(match self.ok.handle(event, MouseOnly) {
450            ButtonOutcome::Pressed => {
451                DialogOutcome::Ok
452            }
453            r => Outcome::from(r).into(),
454        });
455
456        DialogOutcome::Unchanged
457    }
458}
459
460/// Handle events for the popup.
461/// Call before other handlers to deal with intersections
462/// with other widgets.
463pub fn handle_events(state: &mut DialogFrameState, _focus: bool, event: &Event) -> DialogOutcome {
464    HandleEvent::handle(state, event, Dialog)
465}
466
467/// Handle only mouse-events.
468pub fn handle_mouse_events(state: &mut DialogFrameState, event: &Event) -> DialogOutcome {
469    HandleEvent::handle(state, event, MouseOnly)
470}