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