rat_widget/
msgdialog.rs

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