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, LayoutOuter, layout_dialog};
47use crate::paragraph::{Paragraph, ParagraphState};
48use crate::text::HasScreenCursor;
49use crate::util::{block_padding2, reset_buf_area};
50use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
51use rat_event::{ConsumedEvent, Dialog, HandleEvent, Outcome, Regular, ct_event};
52use rat_focus::{Focus, FocusBuilder, FocusFlag, HasFocus};
53use rat_reloc::RelocatableState;
54use rat_scrolled::{Scroll, ScrollStyle};
55use ratatui::buffer::Buffer;
56use ratatui::layout::{Alignment, Constraint, Flex, Position, Rect, Size};
57use ratatui::style::Style;
58use ratatui::text::{Line, Text};
59use ratatui::widgets::{Block, Padding, StatefulWidget, Widget};
60use std::cell::{Cell, RefCell};
61use std::cmp::max;
62use std::fmt::Debug;
63
64/// Basic status dialog for longer messages.
65#[derive(Debug, Default, Clone)]
66pub struct MsgDialog<'a> {
67    style: Style,
68    block: Option<Block<'a>>,
69    scroll_style: Option<ScrollStyle>,
70    button_style: Option<ButtonStyle>,
71    markdown: bool,
72    markdown_header_1: Option<Style>,
73    markdown_header_n: Option<Style>,
74    hide_paragraph_focus: bool,
75
76    layout: LayoutOuter,
77}
78
79/// Combined style.
80#[derive(Debug, Clone)]
81pub struct MsgDialogStyle {
82    pub style: Style,
83    pub block: Option<Block<'static>>,
84    pub border_style: Option<Style>,
85    pub title_style: Option<Style>,
86    pub markdown: Option<bool>,
87    pub markdown_header_1: Option<Style>,
88    pub markdown_header_n: Option<Style>,
89    pub hide_paragraph_focus: Option<bool>,
90    pub scroll: Option<ScrollStyle>,
91
92    pub button: Option<ButtonStyle>,
93
94    pub non_exhaustive: NonExhaustive,
95}
96
97/// State & event handling.
98#[derive(Debug, Clone)]
99pub struct MsgDialogState {
100    /// Full area.
101    /// __readonly__. renewed for each render.
102    pub area: Rect,
103    /// Area inside the borders.
104    /// __readonly__. renewed for each render.
105    pub inner: Rect,
106
107    /// Dialog is active.
108    /// __read+write__
109    pub active: Cell<bool>,
110    /// Dialog title
111    /// __read+write__
112    pub message_title: RefCell<String>,
113    /// Dialog text.
114    /// __read+write__
115    pub message: RefCell<String>,
116
117    /// Ok button
118    button: RefCell<ButtonState>,
119    /// message-text
120    paragraph: RefCell<ParagraphState>,
121}
122
123impl<'a> MsgDialog<'a> {
124    /// New widget
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    /// Enable some markdown formatting.
130    pub fn markdown(mut self, md: bool) -> Self {
131        self.markdown = md;
132        self
133    }
134
135    /// Header 1 style
136    pub fn markdown_header_1(mut self, style: impl Into<Style>) -> Self {
137        self.markdown_header_1 = Some(style.into());
138        self
139    }
140
141    /// Other headers style
142    pub fn markdown_header_n(mut self, style: impl Into<Style>) -> Self {
143        self.markdown_header_n = Some(style.into());
144        self
145    }
146
147    /// Show the focus markers for the paragraph.
148    pub fn hide_paragraph_focus(mut self, hide: bool) -> Self {
149        self.hide_paragraph_focus = hide;
150        self
151    }
152
153    /// Block
154    pub fn block(mut self, block: Block<'a>) -> Self {
155        self.block = Some(block);
156        self.block = self.block.map(|v| v.style(self.style));
157        self
158    }
159
160    /// Margin constraint for the left side.
161    pub fn left(mut self, left: Constraint) -> Self {
162        self.layout = self.layout.left(left);
163        self
164    }
165
166    /// Margin constraint for the top side.
167    pub fn top(mut self, top: Constraint) -> Self {
168        self.layout = self.layout.top(top);
169        self
170    }
171
172    /// Margin constraint for the right side.
173    pub fn right(mut self, right: Constraint) -> Self {
174        self.layout = self.layout.right(right);
175        self
176    }
177
178    /// Margin constraint for the bottom side.
179    pub fn bottom(mut self, bottom: Constraint) -> Self {
180        self.layout = self.layout.bottom(bottom);
181        self
182    }
183
184    /// Put at a fixed position.
185    pub fn position(mut self, pos: Position) -> Self {
186        self.layout = self.layout.position(pos);
187        self
188    }
189
190    /// Constraint for the width.
191    pub fn width(mut self, width: Constraint) -> Self {
192        self.layout = self.layout.width(width);
193        self
194    }
195
196    /// Constraint for the height.
197    pub fn height(mut self, height: Constraint) -> Self {
198        self.layout = self.layout.height(height);
199        self
200    }
201
202    /// Set at a fixed size.
203    pub fn size(mut self, size: Size) -> Self {
204        self.layout = self.layout.size(size);
205        self
206    }
207
208    /// Combined style
209    pub fn styles(mut self, styles: MsgDialogStyle) -> Self {
210        self.style = styles.style;
211        if let Some(markdown) = styles.markdown {
212            self.markdown = markdown;
213        }
214        if let Some(markdown_header_1) = styles.markdown_header_1 {
215            self.markdown_header_1 = Some(markdown_header_1);
216        }
217        if let Some(markdown_header_n) = styles.markdown_header_n {
218            self.markdown_header_n = Some(markdown_header_n);
219        }
220        if let Some(hide_paragraph_focus) = styles.hide_paragraph_focus {
221            self.hide_paragraph_focus = hide_paragraph_focus;
222        }
223        if styles.block.is_some() {
224            self.block = styles.block;
225        }
226        if let Some(border_style) = styles.border_style {
227            self.block = self.block.map(|v| v.border_style(border_style));
228        }
229        if let Some(title_style) = styles.title_style {
230            self.block = self.block.map(|v| v.title_style(title_style));
231        }
232        self.block = self.block.map(|v| v.style(self.style));
233
234        if styles.scroll.is_some() {
235            self.scroll_style = styles.scroll;
236        }
237
238        if styles.button.is_some() {
239            self.button_style = styles.button;
240        }
241
242        self
243    }
244
245    /// Base style
246    pub fn style(mut self, style: impl Into<Style>) -> Self {
247        self.style = style.into();
248        self.block = self.block.map(|v| v.style(self.style));
249        self
250    }
251
252    /// Scroll style.
253    pub fn scroll_style(mut self, style: ScrollStyle) -> Self {
254        self.scroll_style = Some(style);
255        self
256    }
257
258    /// Button style.
259    pub fn button_style(mut self, style: ButtonStyle) -> Self {
260        self.button_style = Some(style);
261        self
262    }
263}
264
265impl Default for MsgDialogStyle {
266    fn default() -> Self {
267        Self {
268            style: Default::default(),
269            block: Default::default(),
270            border_style: Default::default(),
271            title_style: Default::default(),
272            markdown: Default::default(),
273            markdown_header_1: Default::default(),
274            markdown_header_n: Default::default(),
275            hide_paragraph_focus: Default::default(),
276            scroll: Default::default(),
277            button: Default::default(),
278            non_exhaustive: NonExhaustive,
279        }
280    }
281}
282
283impl HasFocus for MsgDialogState {
284    fn build(&self, _builder: &mut FocusBuilder) {
285        // don't expose inner workings.
286    }
287
288    fn focus(&self) -> FocusFlag {
289        unimplemented!("not available")
290    }
291
292    fn area(&self) -> Rect {
293        unimplemented!("not available")
294    }
295}
296
297impl RelocatableState for MsgDialogState {
298    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
299        self.area.relocate(shift, clip);
300        self.inner.relocate(shift, clip);
301        self.button.borrow_mut().relocate(shift, clip);
302        self.paragraph.borrow_mut().relocate(shift, clip);
303    }
304}
305
306impl HasScreenCursor for MsgDialogState {
307    fn screen_cursor(&self) -> Option<(u16, u16)> {
308        None
309    }
310}
311
312impl MsgDialogState {
313    pub fn new() -> Self {
314        Self::default()
315    }
316
317    /// New dialog with active-flag set.
318    pub fn new_active(title: impl Into<String>, msg: impl AsRef<str>) -> Self {
319        let zelf = Self::default();
320        zelf.set_active(true);
321        zelf.title(title);
322        zelf.append(msg.as_ref());
323        zelf
324    }
325
326    /// Show the dialog.
327    pub fn set_active(&self, active: bool) {
328        self.active.set(active);
329        self.build_focus().focus(&*self.paragraph.borrow());
330        self.paragraph.borrow_mut().set_line_offset(0);
331        self.paragraph.borrow_mut().set_col_offset(0);
332    }
333
334    /// Dialog is active.
335    pub fn active(&self) -> bool {
336        self.active.get()
337    }
338
339    /// Clear message text, set active to false.
340    pub fn clear(&self) {
341        self.active.set(false);
342        *self.message.borrow_mut() = Default::default();
343    }
344
345    /// Set the title for the message.
346    pub fn title(&self, title: impl Into<String>) {
347        *self.message_title.borrow_mut() = title.into();
348    }
349
350    /// *Append* to the message.
351    pub fn append(&self, msg: &str) {
352        self.set_active(true);
353        let mut message = self.message.borrow_mut();
354        if !message.is_empty() {
355            message.push('\n');
356        }
357        message.push_str(msg);
358    }
359}
360
361impl Default for MsgDialogState {
362    fn default() -> Self {
363        let s = Self {
364            active: Default::default(),
365            area: Default::default(),
366            inner: Default::default(),
367            message: Default::default(),
368            button: Default::default(),
369            paragraph: Default::default(),
370            message_title: Default::default(),
371        };
372        s.paragraph.borrow().focus.set(true);
373        s
374    }
375}
376
377impl MsgDialogState {
378    fn build_focus(&self) -> Focus {
379        let mut fb = FocusBuilder::default();
380        fb.widget(&*self.paragraph.borrow())
381            .widget(&*self.button.borrow());
382        fb.build()
383    }
384}
385
386impl<'a> StatefulWidget for &MsgDialog<'a> {
387    type State = MsgDialogState;
388
389    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
390        render_ref(self, area, buf, state);
391    }
392}
393
394impl StatefulWidget for MsgDialog<'_> {
395    type State = MsgDialogState;
396
397    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
398        render_ref(&self, area, buf, state);
399    }
400}
401
402fn render_ref(widget: &MsgDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut MsgDialogState) {
403    state.area = area;
404
405    if !state.active.get() {
406        return;
407    }
408
409    let mut block;
410    let title = state.message_title.borrow();
411    let block = if let Some(b) = &widget.block {
412        if !title.is_empty() {
413            block = b.clone().title(title.as_str());
414            &block
415        } else {
416            b
417        }
418    } else {
419        block = Block::bordered()
420            .style(widget.style)
421            .padding(Padding::new(1, 1, 1, 1));
422        if !title.is_empty() {
423            block = block.title(title.as_str());
424        }
425        &block
426    };
427
428    let l_dlg = layout_dialog(
429        area, //
430        block_padding2(block),
431        [Constraint::Length(10)],
432        0,
433        Flex::End,
434    );
435    state.area = l_dlg.area();
436    state.inner = l_dlg.widget_for(DialogItem::Inner);
437
438    let header_1 = widget.markdown_header_1.unwrap_or_default();
439    let header_n = widget.markdown_header_n.unwrap_or_default();
440
441    reset_buf_area(state.area, buf);
442    block.render(state.area, buf);
443
444    {
445        let scroll = if let Some(style) = &widget.scroll_style {
446            Scroll::new().styles(style.clone())
447        } else {
448            Scroll::new().style(widget.style)
449        };
450
451        let message = state.message.borrow();
452        let mut lines = Vec::new();
453        if widget.markdown {
454            for t in message.lines() {
455                if t.starts_with("##") {
456                    lines.push(Line::from(t).style(header_n));
457                } else if t.starts_with("#") {
458                    lines.push(Line::from(t).style(header_1));
459                } else {
460                    lines.push(Line::from(t));
461                }
462            }
463        } else {
464            for t in message.lines() {
465                lines.push(Line::from(t));
466            }
467        }
468
469        let text = Text::from(lines).alignment(Alignment::Center);
470
471        Paragraph::new(text)
472            .scroll(scroll)
473            .hide_focus(widget.hide_paragraph_focus)
474            .render(
475                l_dlg.widget_for(DialogItem::Content),
476                buf,
477                &mut state.paragraph.borrow_mut(),
478            );
479    }
480
481    Button::new("Ok")
482        .styles_opt(widget.button_style.clone())
483        .render(
484            l_dlg.widget_for(DialogItem::Button(0)),
485            buf,
486            &mut state.button.borrow_mut(),
487        );
488}
489
490impl HandleEvent<crossterm::event::Event, Dialog, Outcome> for MsgDialogState {
491    fn handle(&mut self, event: &crossterm::event::Event, _: Dialog) -> Outcome {
492        if self.active.get() {
493            let mut focus = self.build_focus();
494            let f = focus.handle(event, Regular);
495
496            let mut r = match self
497                .button
498                .borrow_mut()
499                .handle(event, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
500            {
501                ButtonOutcome::Pressed => {
502                    self.clear();
503                    self.active.set(false);
504                    Outcome::Changed
505                }
506                v => v.into(),
507            };
508            r = r.or_else(|| self.paragraph.borrow_mut().handle(event, Regular));
509            r = r.or_else(|| match event {
510                ct_event!(keycode press Esc) => {
511                    self.clear();
512                    self.active.set(false);
513                    Outcome::Changed
514                }
515                _ => Outcome::Continue,
516            });
517            // mandatory consume everything else.
518            max(max(Outcome::Unchanged, f), r)
519        } else {
520            Outcome::Continue
521        }
522    }
523}
524
525/// Handle events for the MsgDialog.
526pub fn handle_dialog_events(
527    state: &mut MsgDialogState,
528    event: &crossterm::event::Event,
529) -> Outcome {
530    state.handle(event, Dialog)
531}