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::util::{block_padding2, fill_buf_area};
12use crossterm::event::Event;
13use ratatui::buffer::Buffer;
14use ratatui::layout::{Constraint, Flex, Position, Rect, Size};
15use ratatui::style::Style;
16use ratatui::widgets::{Block, BorderType, StatefulWidget, Widget};
17
18/// Renders the frame and the Ok/Cancel buttons for a dialog window.
19///
20/// After rendering BaseDialogState::widget_area is available
21/// to render any content.
22#[derive(Debug, Default)]
23pub struct DialogFrame<'a> {
24    style: Style,
25    block: Block<'a>,
26    button_style: ButtonStyle,
27    layout: LayoutOuter,
28    ok_text: &'a str,
29    no_cancel: bool,
30    cancel_text: &'a str,
31}
32
33/// Combined styles.
34#[derive(Debug, Clone)]
35pub struct DialogFrameStyle {
36    pub style: Style,
37    pub block: Option<Block<'static>>,
38    pub button_style: Option<ButtonStyle>,
39    pub layout: Option<LayoutOuter>,
40    pub ok_text: Option<&'static str>,
41    pub no_cancel: Option<bool>,
42    pub cancel_text: Option<&'static str>,
43    pub non_exhaustive: NonExhaustive,
44}
45
46impl Default for DialogFrameStyle {
47    fn default() -> Self {
48        Self {
49            style: Default::default(),
50            block: Default::default(),
51            button_style: Default::default(),
52            layout: Default::default(),
53            ok_text: Default::default(),
54            no_cancel: Default::default(),
55            cancel_text: Default::default(),
56            non_exhaustive: NonExhaustive,
57        }
58    }
59}
60
61#[derive(Debug, Clone)]
62pub struct DialogFrameState {
63    /// Area for the dialog.
64    /// __read only__ set with each render.
65    pub area: Rect,
66    /// Area for the dialog-content.
67    /// __read only__ set with each render.
68    pub widget_area: Rect,
69
70    /// ok-button
71    pub ok: ButtonState,
72    /// no cancel button
73    pub no_cancel: bool,
74    /// cancel-button
75    pub cancel: ButtonState,
76}
77
78impl<'a> DialogFrame<'a> {
79    pub fn new() -> Self {
80        Self {
81            style: Default::default(),
82            block: Block::bordered().border_type(BorderType::Plain),
83            button_style: Default::default(),
84            layout: LayoutOuter::new()
85                .left(Constraint::Percentage(19))
86                .top(Constraint::Length(3))
87                .right(Constraint::Percentage(19))
88                .bottom(Constraint::Length(3)),
89            ok_text: "Ok",
90            no_cancel: false,
91            cancel_text: "Cancel",
92        }
93    }
94
95    pub fn styles(mut self, styles: DialogFrameStyle) -> Self {
96        self.style = styles.style;
97        if let Some(block) = styles.block {
98            self.block = block;
99        }
100        if let Some(button_style) = styles.button_style {
101            self.button_style = button_style;
102        }
103        if let Some(layout) = styles.layout {
104            self.layout = layout;
105        }
106        if let Some(ok_text) = styles.ok_text {
107            self.ok_text = ok_text;
108        }
109        if let Some(no_cancel) = styles.no_cancel {
110            self.no_cancel = no_cancel;
111        }
112        if let Some(cancel_text) = styles.cancel_text {
113            self.cancel_text = cancel_text;
114        }
115        self
116    }
117
118    /// Base style for the dialog.
119    pub fn style(mut self, style: Style) -> Self {
120        self.style = style;
121        self
122    }
123
124    /// Block for the dialog.
125    pub fn block(mut self, block: Block<'a>) -> Self {
126        self.block = block;
127        self
128    }
129
130    /// Button style.
131    pub fn button_style(mut self, style: ButtonStyle) -> Self {
132        self.button_style = style;
133        self
134    }
135
136    /// Ok text.
137    pub fn ok_text(mut self, str: &'a str) -> Self {
138        self.ok_text = str;
139        self
140    }
141
142    /// No cancel button.
143    pub fn no_cancel(mut self) -> Self {
144        self.no_cancel = true;
145        self
146    }
147
148    /// Cancel text.
149    pub fn cancel_text(mut self, str: &'a str) -> Self {
150        self.cancel_text = str;
151        self
152    }
153
154    /// Margin constraint for the left side.
155    pub fn left(mut self, left: Constraint) -> Self {
156        self.layout = self.layout.left(left);
157        self
158    }
159
160    /// Margin constraint for the top side.
161    pub fn top(mut self, top: Constraint) -> Self {
162        self.layout = self.layout.top(top);
163        self
164    }
165
166    /// Margin constraint for the right side.
167    pub fn right(mut self, right: Constraint) -> Self {
168        self.layout = self.layout.right(right);
169        self
170    }
171
172    /// Margin constraint for the bottom side.
173    pub fn bottom(mut self, bottom: Constraint) -> Self {
174        self.layout = self.layout.bottom(bottom);
175        self
176    }
177
178    /// Put at a fixed position.
179    pub fn position(mut self, pos: Position) -> Self {
180        self.layout = self.layout.position(pos);
181        self
182    }
183
184    /// Constraint for the width.
185    pub fn width(mut self, width: Constraint) -> Self {
186        self.layout = self.layout.width(width);
187        self
188    }
189
190    /// Constraint for the height.
191    pub fn height(mut self, height: Constraint) -> Self {
192        self.layout = self.layout.height(height);
193        self
194    }
195
196    /// Set at a fixed size.
197    pub fn size(mut self, size: Size) -> Self {
198        self.layout = self.layout.size(size);
199        self
200    }
201}
202
203impl<'a> StatefulWidget for DialogFrame<'a> {
204    type State = DialogFrameState;
205
206    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
207        state.area = self.layout.layout(area);
208        state.no_cancel = self.no_cancel;
209
210        let l_dlg = if self.no_cancel {
211            layout_dialog(
212                state.area,
213                block_padding2(&self.block),
214                [Constraint::Length(10)],
215                1,
216                Flex::End,
217            )
218        } else {
219            layout_dialog(
220                state.area,
221                block_padding2(&self.block),
222                [Constraint::Length(12), Constraint::Length(10)],
223                1,
224                Flex::End,
225            )
226        };
227        state.widget_area = l_dlg.widget_for(DialogItem::Content);
228
229        fill_buf_area(buf, l_dlg.area(), " ", self.style);
230        self.block.render(state.area, buf);
231
232        if self.no_cancel {
233            Button::new(self.ok_text).styles(self.button_style).render(
234                l_dlg.widget_for(DialogItem::Button(0)),
235                buf,
236                &mut state.ok,
237            );
238        } else {
239            Button::new(self.cancel_text)
240                .styles(self.button_style.clone())
241                .render(
242                    l_dlg.widget_for(DialogItem::Button(0)),
243                    buf,
244                    &mut state.cancel,
245                );
246            Button::new(self.ok_text).styles(self.button_style).render(
247                l_dlg.widget_for(DialogItem::Button(1)),
248                buf,
249                &mut state.ok,
250            );
251        }
252    }
253}
254
255impl Default for DialogFrameState {
256    fn default() -> Self {
257        let z = Self {
258            area: Default::default(),
259            widget_area: Default::default(),
260            ok: Default::default(),
261            no_cancel: Default::default(),
262            cancel: Default::default(),
263        };
264        z.ok.focus.set(true);
265        z
266    }
267}
268
269impl HasFocus for DialogFrameState {
270    fn build(&self, builder: &mut FocusBuilder) {
271        builder.widget(&self.ok);
272        if !self.no_cancel {
273            builder.widget(&self.cancel);
274        }
275    }
276
277    fn focus(&self) -> FocusFlag {
278        unimplemented!()
279    }
280
281    fn area(&self) -> Rect {
282        unimplemented!()
283    }
284}
285
286impl DialogFrameState {
287    pub fn new() -> Self {
288        Self::default()
289    }
290}
291
292/// Result type for event-handling.
293pub enum DialogOutcome {
294    /// Continue with event-handling.
295    /// In the event-loop this waits for the next event.
296    Continue,
297    /// Break event-handling without repaint.
298    /// In the event-loop this waits for the next event.
299    Unchanged,
300    /// Break event-handling and repaints/renders the application.
301    /// In the event-loop this calls `render`.
302    Changed,
303    /// Ok pressed
304    Ok,
305    /// Cancel pressed
306    Cancel,
307}
308
309impl ConsumedEvent for DialogOutcome {
310    fn is_consumed(&self) -> bool {
311        !matches!(self, DialogOutcome::Continue)
312    }
313}
314
315impl From<DialogOutcome> for Outcome {
316    fn from(value: DialogOutcome) -> Self {
317        match value {
318            DialogOutcome::Continue => Outcome::Continue,
319            DialogOutcome::Unchanged => Outcome::Unchanged,
320            DialogOutcome::Changed => Outcome::Changed,
321            DialogOutcome::Ok => Outcome::Changed,
322            DialogOutcome::Cancel => Outcome::Changed,
323        }
324    }
325}
326
327impl From<Outcome> for DialogOutcome {
328    fn from(value: Outcome) -> Self {
329        match value {
330            Outcome::Continue => DialogOutcome::Continue,
331            Outcome::Unchanged => DialogOutcome::Unchanged,
332            Outcome::Changed => DialogOutcome::Changed,
333        }
334    }
335}
336
337impl<'a> HandleEvent<Event, Dialog, DialogOutcome> for DialogFrameState {
338    fn handle(&mut self, event: &Event, _: Dialog) -> DialogOutcome {
339        flow!({
340            if !self.no_cancel {
341                match self.cancel.handle(event, Regular) {
342                    ButtonOutcome::Pressed => DialogOutcome::Cancel,
343                    r => Outcome::from(r).into(),
344                }
345            } else {
346                DialogOutcome::Continue
347            }
348        });
349        flow!(match self.ok.handle(event, Regular) {
350            ButtonOutcome::Pressed => {
351                DialogOutcome::Ok
352            }
353            r => Outcome::from(r).into(),
354        });
355
356        flow!(match event {
357            ct_event!(keycode press Esc) if !self.no_cancel => {
358                DialogOutcome::Cancel
359            }
360            ct_event!(keycode press Enter) | ct_event!(keycode press F(12)) => {
361                DialogOutcome::Ok
362            }
363            _ => DialogOutcome::Unchanged,
364        });
365
366        DialogOutcome::Unchanged
367    }
368}