1use 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#[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#[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#[derive(Debug, Clone)]
102pub struct MsgDialogState {
103 pub area: Rect,
106 pub inner: Rect,
109
110 pub active: Cell<bool>,
113 pub message_title: RefCell<String>,
116 pub message: RefCell<String>,
119
120 button: RefCell<ButtonState>,
122 paragraph: RefCell<ParagraphState>,
124}
125
126impl<'a> MsgDialog<'a> {
127 pub fn new() -> Self {
129 Self::default()
130 }
131
132 pub fn markdown(mut self, md: bool) -> Self {
134 self.markdown = md;
135 self
136 }
137
138 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 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 pub fn hide_paragraph_focus(mut self, hide: bool) -> Self {
152 self.hide_paragraph_focus = hide;
153 self
154 }
155
156 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 pub fn left(mut self, left: Constraint) -> Self {
165 self.layout = self.layout.left(left);
166 self
167 }
168
169 pub fn top(mut self, top: Constraint) -> Self {
171 self.layout = self.layout.top(top);
172 self
173 }
174
175 pub fn right(mut self, right: Constraint) -> Self {
177 self.layout = self.layout.right(right);
178 self
179 }
180
181 pub fn bottom(mut self, bottom: Constraint) -> Self {
183 self.layout = self.layout.bottom(bottom);
184 self
185 }
186
187 pub fn position(mut self, pos: Position) -> Self {
189 self.layout = self.layout.position(pos);
190 self
191 }
192
193 pub fn width(mut self, width: Constraint) -> Self {
195 self.layout = self.layout.width(width);
196 self
197 }
198
199 pub fn height(mut self, height: Constraint) -> Self {
201 self.layout = self.layout.height(height);
202 self
203 }
204
205 pub fn size(mut self, size: Size) -> Self {
207 self.layout = self.layout.size(size);
208 self
209 }
210
211 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 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 pub fn scroll_style(mut self, style: ScrollStyle) -> Self {
257 self.scroll_style = Some(style);
258 self
259 }
260
261 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 }
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 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 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 pub fn active(&self) -> bool {
339 self.active.get()
340 }
341
342 pub fn clear(&self) {
344 self.active.set(false);
345 *self.message.borrow_mut() = Default::default();
346 }
347
348 pub fn title(&self, title: impl Into<String>) {
350 *self.message_title.borrow_mut() = title.into();
351 }
352
353 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, 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 max(max(Outcome::Unchanged, f), r)
522 } else {
523 Outcome::Continue
524 }
525 }
526}
527
528pub fn handle_dialog_events(state: &mut MsgDialogState, event: &Event) -> Outcome {
530 state.handle(event, Dialog)
531}