rat_dialog/decorations/
base_dialog.rs

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