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