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