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::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, Rect};
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    scroll_style: Option<ScrollStyle>,
69    button_style: Option<ButtonStyle>,
70    block: Option<Block<'a>>,
71}
72
73/// Combined style.
74#[derive(Debug, Clone)]
75pub struct MsgDialogStyle {
76    pub style: Style,
77    pub scroll: Option<ScrollStyle>,
78    pub block: Option<Block<'static>>,
79    pub button: Option<ButtonStyle>,
80
81    pub non_exhaustive: NonExhaustive,
82}
83
84/// State & event handling.
85#[derive(Debug, Clone)]
86pub struct MsgDialogState {
87    /// Full area.
88    /// __readonly__. renewed for each render.
89    pub area: Rect,
90    /// Area inside the borders.
91    /// __readonly__. renewed for each render.
92    pub inner: Rect,
93
94    /// Dialog is active.
95    /// __read+write__
96    pub active: Cell<bool>,
97    /// Dialog title
98    /// __read+write__
99    pub message_title: RefCell<String>,
100    /// Dialog text.
101    /// __read+write__
102    pub message: RefCell<String>,
103
104    /// Ok button
105    button: RefCell<ButtonState>,
106    /// message-text
107    paragraph: RefCell<ParagraphState>,
108}
109
110impl<'a> MsgDialog<'a> {
111    /// New widget
112    pub fn new() -> Self {
113        Self {
114            block: None,
115            style: Default::default(),
116            scroll_style: Default::default(),
117            button_style: Default::default(),
118        }
119    }
120
121    /// Block
122    pub fn block(mut self, block: Block<'a>) -> Self {
123        self.block = Some(block);
124        self.block = self.block.map(|v| v.style(self.style));
125        self
126    }
127
128    /// Combined style
129    pub fn styles(mut self, styles: MsgDialogStyle) -> Self {
130        self.style = styles.style;
131        if styles.scroll.is_some() {
132            self.scroll_style = styles.scroll;
133        }
134        if styles.block.is_some() {
135            self.block = styles.block;
136        }
137        if styles.button.is_some() {
138            self.button_style = styles.button;
139        }
140        self.block = self.block.map(|v| v.style(self.style));
141        self
142    }
143
144    /// Base style
145    pub fn style(mut self, style: impl Into<Style>) -> Self {
146        self.style = style.into();
147        self.block = self.block.map(|v| v.style(self.style));
148        self
149    }
150
151    /// Scroll style.
152    pub fn scroll_style(mut self, style: ScrollStyle) -> Self {
153        self.scroll_style = Some(style);
154        self
155    }
156
157    /// Button style.
158    pub fn button_style(mut self, style: ButtonStyle) -> Self {
159        self.button_style = Some(style);
160        self
161    }
162}
163
164impl Default for MsgDialogStyle {
165    fn default() -> Self {
166        Self {
167            style: Default::default(),
168            scroll: None,
169            block: None,
170            button: Default::default(),
171            non_exhaustive: NonExhaustive,
172        }
173    }
174}
175
176impl HasFocus for MsgDialogState {
177    fn build(&self, _builder: &mut FocusBuilder) {
178        // don't expose inner workings.
179    }
180
181    fn focus(&self) -> FocusFlag {
182        unimplemented!("not available")
183    }
184
185    fn area(&self) -> Rect {
186        unimplemented!("not available")
187    }
188}
189
190impl RelocatableState for MsgDialogState {
191    fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
192        self.area.relocate(shift, clip);
193        self.inner.relocate(shift, clip);
194        self.button.borrow_mut().relocate(shift, clip);
195        self.paragraph.borrow_mut().relocate(shift, clip);
196    }
197}
198
199impl HasScreenCursor for MsgDialogState {
200    fn screen_cursor(&self) -> Option<(u16, u16)> {
201        None
202    }
203}
204
205impl MsgDialogState {
206    pub fn new() -> Self {
207        Self::default()
208    }
209
210    /// New dialog with active-flag set.
211    pub fn new_active(title: impl Into<String>, msg: impl AsRef<str>) -> Self {
212        let zelf = Self::default();
213        zelf.set_active(true);
214        zelf.title(title);
215        zelf.append(msg.as_ref());
216        zelf
217    }
218
219    /// Show the dialog.
220    pub fn set_active(&self, active: bool) {
221        self.active.set(active);
222        self.build_focus().focus(&*self.paragraph.borrow());
223        self.paragraph.borrow_mut().set_line_offset(0);
224        self.paragraph.borrow_mut().set_col_offset(0);
225    }
226
227    /// Dialog is active.
228    pub fn active(&self) -> bool {
229        self.active.get()
230    }
231
232    /// Clear message text, set active to false.
233    pub fn clear(&self) {
234        self.active.set(false);
235        *self.message.borrow_mut() = Default::default();
236    }
237
238    /// Set the title for the message.
239    pub fn title(&self, title: impl Into<String>) {
240        *self.message_title.borrow_mut() = title.into();
241    }
242
243    /// *Append* to the message.
244    pub fn append(&self, msg: &str) {
245        self.set_active(true);
246        let mut message = self.message.borrow_mut();
247        if !message.is_empty() {
248            message.push('\n');
249        }
250        message.push_str(msg);
251    }
252}
253
254impl Default for MsgDialogState {
255    fn default() -> Self {
256        let s = Self {
257            active: Default::default(),
258            area: Default::default(),
259            inner: Default::default(),
260            message: Default::default(),
261            button: Default::default(),
262            paragraph: Default::default(),
263            message_title: Default::default(),
264        };
265        s.paragraph.borrow().focus.set(true);
266        s
267    }
268}
269
270impl MsgDialogState {
271    fn build_focus(&self) -> Focus {
272        let mut fb = FocusBuilder::default();
273        fb.widget(&*self.paragraph.borrow())
274            .widget(&*self.button.borrow());
275        fb.build()
276    }
277}
278
279impl<'a> StatefulWidget for &MsgDialog<'a> {
280    type State = MsgDialogState;
281
282    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
283        render_ref(self, area, buf, state);
284    }
285}
286
287impl StatefulWidget for MsgDialog<'_> {
288    type State = MsgDialogState;
289
290    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
291        render_ref(&self, area, buf, state);
292    }
293}
294
295fn render_ref(widget: &MsgDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut MsgDialogState) {
296    state.area = area;
297
298    if !state.active.get() {
299        return;
300    }
301
302    let mut block;
303    let title = state.message_title.borrow();
304    let block = if let Some(b) = &widget.block {
305        if !title.is_empty() {
306            block = b.clone().title(title.as_str());
307            &block
308        } else {
309            b
310        }
311    } else {
312        block = Block::bordered()
313            .style(widget.style)
314            .padding(Padding::new(1, 1, 1, 1));
315        if !title.is_empty() {
316            block = block.title(title.as_str());
317        }
318        &block
319    };
320
321    let l_dlg = layout_dialog(
322        area, //
323        block_padding2(block),
324        [Constraint::Length(10)],
325        0,
326        Flex::End,
327    );
328    state.area = l_dlg.area();
329    state.inner = l_dlg.widget_for(DialogItem::Inner);
330
331    reset_buf_area(state.area, buf);
332    block.render(state.area, buf);
333
334    {
335        let scroll = if let Some(style) = &widget.scroll_style {
336            Scroll::new().styles(style.clone())
337        } else {
338            Scroll::new().style(widget.style)
339        };
340
341        let message = state.message.borrow();
342        let mut lines = Vec::new();
343        for t in message.split('\n') {
344            lines.push(Line::from(t));
345        }
346        let text = Text::from(lines).alignment(Alignment::Center);
347        Paragraph::new(text).scroll(scroll).render(
348            l_dlg.widget_for(DialogItem::Content),
349            buf,
350            &mut state.paragraph.borrow_mut(),
351        );
352    }
353
354    Button::new("Ok")
355        .styles_opt(widget.button_style.clone())
356        .render(
357            l_dlg.widget_for(DialogItem::Button(0)),
358            buf,
359            &mut state.button.borrow_mut(),
360        );
361}
362
363impl HandleEvent<crossterm::event::Event, Dialog, Outcome> for MsgDialogState {
364    fn handle(&mut self, event: &crossterm::event::Event, _: Dialog) -> Outcome {
365        if self.active.get() {
366            let mut focus = self.build_focus();
367            let f = focus.handle(event, Regular);
368
369            let mut r = match self
370                .button
371                .borrow_mut()
372                .handle(event, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
373            {
374                ButtonOutcome::Pressed => {
375                    self.clear();
376                    self.active.set(false);
377                    Outcome::Changed
378                }
379                v => v.into(),
380            };
381            r = r.or_else(|| self.paragraph.borrow_mut().handle(event, Regular));
382            r = r.or_else(|| match event {
383                ct_event!(keycode press Esc) => {
384                    self.clear();
385                    self.active.set(false);
386                    Outcome::Changed
387                }
388                _ => Outcome::Continue,
389            });
390            // mandatory consume everything else.
391            max(max(Outcome::Unchanged, f), r)
392        } else {
393            Outcome::Continue
394        }
395    }
396}
397
398/// Handle events for the MsgDialog.
399pub fn handle_dialog_events(
400    state: &mut MsgDialogState,
401    event: &crossterm::event::Event,
402) -> Outcome {
403    state.handle(event, Dialog)
404}