rat_widget/
msgdialog.rs

1//!
2//! A message dialog.
3//!
4//! ```
5//! use ratatui::buffer::Buffer;
6//! use ratatui::prelude::Rect;
7//! use ratatui::widgets::{Block, StatefulWidget};
8//! use rat_event::{Dialog, HandleEvent, Outcome};
9//! use rat_widget::msgdialog::{MsgDialog, MsgDialogState};
10//!
11//! let mut state = MsgDialogState::new_active(
12//!     "Warning",
13//!     "This is some warning etc etc");
14//!
15//! # let area = Rect::new(5,5,60,15);
16//! # let mut buf = Buffer::empty(area);
17//! # let buf = &mut buf;
18//!
19//! MsgDialog::new()
20//!     .block(Block::bordered())
21//!     .render(area, buf, &mut state);
22//!
23//!
24//! // ...
25//!
26//! # let event = crossterm::event::Event::FocusGained;//dummy
27//! # let event = &event;
28//! match state.handle(event, Dialog) {
29//!     Outcome::Continue => {}
30//!     Outcome::Unchanged | Outcome::Changed => { return; }
31//! };
32//!
33//! ```
34//!
35//! The trick to make this work like a dialog is to render it
36//! as the last thing during your rendering and to let it
37//! handle events before any other widgets.
38//!
39//! Then it will be rendered on top of everything else and will
40//! react to events first if it is `active`.
41//!
42
43use crate::_private::NonExhaustive;
44use crate::button::{Button, ButtonState, ButtonStyle};
45use crate::event::ButtonOutcome;
46use crate::layout::{DialogItem, layout_dialog};
47use crate::paragraph::{Paragraph, ParagraphState};
48use crate::util::{block_padding2, reset_buf_area};
49use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
50use rat_event::{ConsumedEvent, Dialog, HandleEvent, Outcome, Regular, ct_event};
51use rat_focus::{Focus, FocusBuilder};
52use rat_scrolled::{Scroll, ScrollStyle};
53use ratatui::buffer::Buffer;
54use ratatui::layout::{Alignment, Constraint, Flex, Rect};
55use ratatui::style::Style;
56use ratatui::text::{Line, Text};
57use ratatui::widgets::{Block, Padding, StatefulWidget, Widget};
58use std::cell::{Cell, RefCell};
59use std::cmp::max;
60use std::fmt::Debug;
61
62/// Basic status dialog for longer messages.
63#[derive(Debug, Default, Clone)]
64pub struct MsgDialog<'a> {
65    style: Style,
66    scroll_style: Option<ScrollStyle>,
67    button_style: Option<ButtonStyle>,
68    block: Option<Block<'a>>,
69}
70
71/// Combined style.
72#[derive(Debug, Clone)]
73pub struct MsgDialogStyle {
74    pub style: Style,
75    pub scroll: Option<ScrollStyle>,
76    pub block: Option<Block<'static>>,
77    pub button: Option<ButtonStyle>,
78
79    pub non_exhaustive: NonExhaustive,
80}
81
82/// State & event handling.
83#[derive(Debug, Clone)]
84pub struct MsgDialogState {
85    /// Full area.
86    /// __readonly__. renewed for each render.
87    pub area: Rect,
88    /// Area inside the borders.
89    /// __readonly__. renewed for each render.
90    pub inner: Rect,
91
92    /// Dialog is active.
93    /// __read+write__
94    pub active: Cell<bool>,
95    /// Dialog title
96    /// __read+write__
97    pub message_title: RefCell<String>,
98    /// Dialog text.
99    /// __read+write__
100    pub message: RefCell<String>,
101
102    /// Ok button
103    button: RefCell<ButtonState>,
104    /// message-text
105    paragraph: RefCell<ParagraphState>,
106}
107
108impl<'a> MsgDialog<'a> {
109    /// New widget
110    pub fn new() -> Self {
111        Self {
112            block: None,
113            style: Default::default(),
114            scroll_style: Default::default(),
115            button_style: Default::default(),
116        }
117    }
118
119    /// Block
120    pub fn block(mut self, block: Block<'a>) -> Self {
121        self.block = Some(block);
122        self.block = self.block.map(|v| v.style(self.style));
123        self
124    }
125
126    /// Combined style
127    pub fn styles(mut self, styles: MsgDialogStyle) -> Self {
128        self.style = styles.style;
129        if styles.scroll.is_some() {
130            self.scroll_style = styles.scroll;
131        }
132        if styles.block.is_some() {
133            self.block = styles.block;
134        }
135        if styles.button.is_some() {
136            self.button_style = styles.button;
137        }
138        self.block = self.block.map(|v| v.style(self.style));
139        self
140    }
141
142    /// Base style
143    pub fn style(mut self, style: impl Into<Style>) -> Self {
144        self.style = style.into();
145        self.block = self.block.map(|v| v.style(self.style));
146        self
147    }
148
149    /// Scroll style.
150    pub fn scroll_style(mut self, style: ScrollStyle) -> Self {
151        self.scroll_style = Some(style);
152        self
153    }
154
155    /// Button style.
156    pub fn button_style(mut self, style: ButtonStyle) -> Self {
157        self.button_style = Some(style);
158        self
159    }
160}
161
162impl Default for MsgDialogStyle {
163    fn default() -> Self {
164        Self {
165            style: Default::default(),
166            scroll: None,
167            block: None,
168            button: Default::default(),
169            non_exhaustive: NonExhaustive,
170        }
171    }
172}
173
174impl MsgDialogState {
175    pub fn new() -> Self {
176        Self::default()
177    }
178
179    /// New dialog with active-flag set.
180    pub fn new_active(title: impl Into<String>, msg: impl AsRef<str>) -> Self {
181        let zelf = Self::default();
182        zelf.set_active(true);
183        zelf.title(title);
184        zelf.append(msg.as_ref());
185        zelf
186    }
187
188    /// Show the dialog.
189    pub fn set_active(&self, active: bool) {
190        self.active.set(active);
191        self.build_focus().focus(&*self.paragraph.borrow());
192        self.paragraph.borrow_mut().set_line_offset(0);
193        self.paragraph.borrow_mut().set_col_offset(0);
194    }
195
196    /// Dialog is active.
197    pub fn active(&self) -> bool {
198        self.active.get()
199    }
200
201    /// Clear message text, set active to false.
202    pub fn clear(&self) {
203        self.active.set(false);
204        *self.message.borrow_mut() = Default::default();
205    }
206
207    /// Set the title for the message.
208    pub fn title(&self, title: impl Into<String>) {
209        *self.message_title.borrow_mut() = title.into();
210    }
211
212    /// *Append* to the message.
213    pub fn append(&self, msg: &str) {
214        self.set_active(true);
215        let mut message = self.message.borrow_mut();
216        if !message.is_empty() {
217            message.push('\n');
218        }
219        message.push_str(msg);
220    }
221}
222
223impl Default for MsgDialogState {
224    fn default() -> Self {
225        let s = Self {
226            active: Default::default(),
227            area: Default::default(),
228            inner: Default::default(),
229            message: Default::default(),
230            button: Default::default(),
231            paragraph: Default::default(),
232            message_title: Default::default(),
233        };
234        s.paragraph.borrow().focus.set(true);
235        s
236    }
237}
238
239impl MsgDialogState {
240    fn build_focus(&self) -> Focus {
241        let mut fb = FocusBuilder::default();
242        fb.widget(&*self.paragraph.borrow())
243            .widget(&*self.button.borrow());
244        fb.build()
245    }
246}
247
248impl<'a> StatefulWidget for &MsgDialog<'a> {
249    type State = MsgDialogState;
250
251    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
252        render_ref(self, area, buf, state);
253    }
254}
255
256impl StatefulWidget for MsgDialog<'_> {
257    type State = MsgDialogState;
258
259    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
260        render_ref(&self, area, buf, state);
261    }
262}
263
264fn render_ref(widget: &MsgDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut MsgDialogState) {
265    if state.active.get() {
266        let mut block;
267        let title = state.message_title.borrow();
268        let block = if let Some(b) = &widget.block {
269            if !title.is_empty() {
270                block = b.clone().title(title.as_str());
271                &block
272            } else {
273                b
274            }
275        } else {
276            block = Block::bordered()
277                .style(widget.style)
278                .padding(Padding::new(1, 1, 1, 1));
279            if !title.is_empty() {
280                block = block.title(title.as_str());
281            }
282            &block
283        };
284
285        let l_dlg = layout_dialog(
286            area, //
287            block_padding2(block),
288            [Constraint::Length(10)],
289            0,
290            Flex::End,
291        );
292        state.area = l_dlg.area();
293        state.inner = l_dlg.widget_for(DialogItem::Inner);
294
295        reset_buf_area(state.area, buf);
296        block.render(state.area, buf);
297
298        {
299            let scroll = if let Some(style) = &widget.scroll_style {
300                Scroll::new().styles(style.clone())
301            } else {
302                Scroll::new().style(widget.style)
303            };
304
305            let message = state.message.borrow();
306            let mut lines = Vec::new();
307            for t in message.split('\n') {
308                lines.push(Line::from(t));
309            }
310            let text = Text::from(lines).alignment(Alignment::Center);
311            Paragraph::new(text).scroll(scroll).render(
312                l_dlg.widget_for(DialogItem::Content),
313                buf,
314                &mut state.paragraph.borrow_mut(),
315            );
316        }
317
318        Button::new("Ok")
319            .styles_opt(widget.button_style.clone())
320            .render(
321                l_dlg.widget_for(DialogItem::Button(0)),
322                buf,
323                &mut state.button.borrow_mut(),
324            );
325    }
326}
327
328impl HandleEvent<crossterm::event::Event, Dialog, Outcome> for MsgDialogState {
329    fn handle(&mut self, event: &crossterm::event::Event, _: Dialog) -> Outcome {
330        if self.active.get() {
331            let mut focus = self.build_focus();
332            let f = focus.handle(event, Regular);
333
334            let mut r = match self
335                .button
336                .borrow_mut()
337                .handle(event, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
338            {
339                ButtonOutcome::Pressed => {
340                    self.clear();
341                    self.active.set(false);
342                    Outcome::Changed
343                }
344                v => v.into(),
345            };
346            r = r.or_else(|| self.paragraph.borrow_mut().handle(event, Regular));
347            r = r.or_else(|| match event {
348                ct_event!(keycode press Esc) => {
349                    self.clear();
350                    self.active.set(false);
351                    Outcome::Changed
352                }
353                _ => Outcome::Continue,
354            });
355            // mandatory consume everything else.
356            max(max(Outcome::Unchanged, f), r)
357        } else {
358            Outcome::Continue
359        }
360    }
361}
362
363/// Handle events for the MsgDialog.
364pub fn handle_dialog_events(
365    state: &mut MsgDialogState,
366    event: &crossterm::event::Event,
367) -> Outcome {
368    state.handle(event, Dialog)
369}