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, Default, 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 HasFocus for BaseDialogState {
211    fn build(&self, builder: &mut FocusBuilder) {
212        builder.widget(&self.ok);
213        builder.widget(&self.cancel);
214    }
215
216    fn focus(&self) -> FocusFlag {
217        unimplemented!()
218    }
219
220    fn area(&self) -> Rect {
221        unimplemented!()
222    }
223}
224
225impl BaseDialogState {
226    pub fn new() -> Self {
227        Self::default()
228    }
229}
230
231/// Result type for event-handling.
232pub enum DialogOutcome {
233    /// Continue with event-handling.
234    /// In the event-loop this waits for the next event.
235    Continue,
236    /// Break event-handling without repaint.
237    /// In the event-loop this waits for the next event.
238    Unchanged,
239    /// Break event-handling and repaints/renders the application.
240    /// In the event-loop this calls `render`.
241    Changed,
242    /// Ok pressed
243    Ok,
244    /// Cancel pressed
245    Cancel,
246}
247
248impl ConsumedEvent for DialogOutcome {
249    fn is_consumed(&self) -> bool {
250        !matches!(self, DialogOutcome::Continue)
251    }
252}
253
254impl From<DialogOutcome> for Outcome {
255    fn from(value: DialogOutcome) -> Self {
256        match value {
257            DialogOutcome::Continue => Outcome::Continue,
258            DialogOutcome::Unchanged => Outcome::Unchanged,
259            DialogOutcome::Changed => Outcome::Changed,
260            DialogOutcome::Ok => Outcome::Changed,
261            DialogOutcome::Cancel => Outcome::Changed,
262        }
263    }
264}
265
266impl From<Outcome> for DialogOutcome {
267    fn from(value: Outcome) -> Self {
268        match value {
269            Outcome::Continue => DialogOutcome::Continue,
270            Outcome::Unchanged => DialogOutcome::Unchanged,
271            Outcome::Changed => DialogOutcome::Changed,
272        }
273    }
274}
275
276impl HandleEvent<Event, Dialog, DialogOutcome> for BaseDialogState {
277    fn handle(&mut self, event: &Event, _: Dialog) -> DialogOutcome {
278        flow!(match self.cancel.handle(event, Regular) {
279            ButtonOutcome::Pressed => {
280                DialogOutcome::Cancel
281            }
282            r => Outcome::from(r).into(),
283        });
284        flow!(match self.ok.handle(event, Regular) {
285            ButtonOutcome::Pressed => {
286                DialogOutcome::Ok
287            }
288            r => Outcome::from(r).into(),
289        });
290
291        flow!(match event {
292            ct_event!(keycode press Esc) => {
293                DialogOutcome::Cancel
294            }
295            ct_event!(keycode press Enter) | ct_event!(keycode press F(12)) => {
296                DialogOutcome::Ok
297            }
298            _ => DialogOutcome::Unchanged,
299        });
300
301        DialogOutcome::Unchanged
302    }
303}