rat_widget/
msgdialog.rs

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