1use 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#[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 markdown: bool,
72 markdown_header_1: Option<Style>,
73 markdown_header_n: Option<Style>,
74 hide_paragraph_focus: bool,
75
76 layout: LayoutOuter,
77}
78
79#[derive(Debug, Clone)]
81pub struct MsgDialogStyle {
82 pub style: Style,
83 pub block: Option<Block<'static>>,
84 pub border_style: Option<Style>,
85 pub title_style: Option<Style>,
86 pub markdown: Option<bool>,
87 pub markdown_header_1: Option<Style>,
88 pub markdown_header_n: Option<Style>,
89 pub hide_paragraph_focus: Option<bool>,
90 pub scroll: Option<ScrollStyle>,
91
92 pub button: Option<ButtonStyle>,
93
94 pub non_exhaustive: NonExhaustive,
95}
96
97#[derive(Debug, Clone)]
99pub struct MsgDialogState {
100 pub area: Rect,
103 pub inner: Rect,
106
107 pub active: Cell<bool>,
110 pub message_title: RefCell<String>,
113 pub message: RefCell<String>,
116
117 button: RefCell<ButtonState>,
119 paragraph: RefCell<ParagraphState>,
121}
122
123impl<'a> MsgDialog<'a> {
124 pub fn new() -> Self {
126 Self::default()
127 }
128
129 pub fn markdown(mut self, md: bool) -> Self {
131 self.markdown = md;
132 self
133 }
134
135 pub fn markdown_header_1(mut self, style: impl Into<Style>) -> Self {
137 self.markdown_header_1 = Some(style.into());
138 self
139 }
140
141 pub fn markdown_header_n(mut self, style: impl Into<Style>) -> Self {
143 self.markdown_header_n = Some(style.into());
144 self
145 }
146
147 pub fn hide_paragraph_focus(mut self, hide: bool) -> Self {
149 self.hide_paragraph_focus = hide;
150 self
151 }
152
153 pub fn block(mut self, block: Block<'a>) -> Self {
155 self.block = Some(block);
156 self.block = self.block.map(|v| v.style(self.style));
157 self
158 }
159
160 pub fn left(mut self, left: Constraint) -> Self {
162 self.layout = self.layout.left(left);
163 self
164 }
165
166 pub fn top(mut self, top: Constraint) -> Self {
168 self.layout = self.layout.top(top);
169 self
170 }
171
172 pub fn right(mut self, right: Constraint) -> Self {
174 self.layout = self.layout.right(right);
175 self
176 }
177
178 pub fn bottom(mut self, bottom: Constraint) -> Self {
180 self.layout = self.layout.bottom(bottom);
181 self
182 }
183
184 pub fn position(mut self, pos: Position) -> Self {
186 self.layout = self.layout.position(pos);
187 self
188 }
189
190 pub fn width(mut self, width: Constraint) -> Self {
192 self.layout = self.layout.width(width);
193 self
194 }
195
196 pub fn height(mut self, height: Constraint) -> Self {
198 self.layout = self.layout.height(height);
199 self
200 }
201
202 pub fn size(mut self, size: Size) -> Self {
204 self.layout = self.layout.size(size);
205 self
206 }
207
208 pub fn styles(mut self, styles: MsgDialogStyle) -> Self {
210 self.style = styles.style;
211 if let Some(markdown) = styles.markdown {
212 self.markdown = markdown;
213 }
214 if let Some(markdown_header_1) = styles.markdown_header_1 {
215 self.markdown_header_1 = Some(markdown_header_1);
216 }
217 if let Some(markdown_header_n) = styles.markdown_header_n {
218 self.markdown_header_n = Some(markdown_header_n);
219 }
220 if let Some(hide_paragraph_focus) = styles.hide_paragraph_focus {
221 self.hide_paragraph_focus = hide_paragraph_focus;
222 }
223 if styles.block.is_some() {
224 self.block = styles.block;
225 }
226 if let Some(border_style) = styles.border_style {
227 self.block = self.block.map(|v| v.border_style(border_style));
228 }
229 if let Some(title_style) = styles.title_style {
230 self.block = self.block.map(|v| v.title_style(title_style));
231 }
232 self.block = self.block.map(|v| v.style(self.style));
233
234 if styles.scroll.is_some() {
235 self.scroll_style = styles.scroll;
236 }
237
238 if styles.button.is_some() {
239 self.button_style = styles.button;
240 }
241
242 self
243 }
244
245 pub fn style(mut self, style: impl Into<Style>) -> Self {
247 self.style = style.into();
248 self.block = self.block.map(|v| v.style(self.style));
249 self
250 }
251
252 pub fn scroll_style(mut self, style: ScrollStyle) -> Self {
254 self.scroll_style = Some(style);
255 self
256 }
257
258 pub fn button_style(mut self, style: ButtonStyle) -> Self {
260 self.button_style = Some(style);
261 self
262 }
263}
264
265impl Default for MsgDialogStyle {
266 fn default() -> Self {
267 Self {
268 style: Default::default(),
269 block: Default::default(),
270 border_style: Default::default(),
271 title_style: Default::default(),
272 markdown: Default::default(),
273 markdown_header_1: Default::default(),
274 markdown_header_n: Default::default(),
275 hide_paragraph_focus: Default::default(),
276 scroll: Default::default(),
277 button: Default::default(),
278 non_exhaustive: NonExhaustive,
279 }
280 }
281}
282
283impl HasFocus for MsgDialogState {
284 fn build(&self, _builder: &mut FocusBuilder) {
285 }
287
288 fn focus(&self) -> FocusFlag {
289 unimplemented!("not available")
290 }
291
292 fn area(&self) -> Rect {
293 unimplemented!("not available")
294 }
295}
296
297impl RelocatableState for MsgDialogState {
298 fn relocate(&mut self, shift: (i16, i16), clip: Rect) {
299 self.area.relocate(shift, clip);
300 self.inner.relocate(shift, clip);
301 self.button.borrow_mut().relocate(shift, clip);
302 self.paragraph.borrow_mut().relocate(shift, clip);
303 }
304}
305
306impl HasScreenCursor for MsgDialogState {
307 fn screen_cursor(&self) -> Option<(u16, u16)> {
308 None
309 }
310}
311
312impl MsgDialogState {
313 pub fn new() -> Self {
314 Self::default()
315 }
316
317 pub fn new_active(title: impl Into<String>, msg: impl AsRef<str>) -> Self {
319 let zelf = Self::default();
320 zelf.set_active(true);
321 zelf.title(title);
322 zelf.append(msg.as_ref());
323 zelf
324 }
325
326 pub fn set_active(&self, active: bool) {
328 self.active.set(active);
329 self.build_focus().focus(&*self.paragraph.borrow());
330 self.paragraph.borrow_mut().set_line_offset(0);
331 self.paragraph.borrow_mut().set_col_offset(0);
332 }
333
334 pub fn active(&self) -> bool {
336 self.active.get()
337 }
338
339 pub fn clear(&self) {
341 self.active.set(false);
342 *self.message.borrow_mut() = Default::default();
343 }
344
345 pub fn title(&self, title: impl Into<String>) {
347 *self.message_title.borrow_mut() = title.into();
348 }
349
350 pub fn append(&self, msg: &str) {
352 self.set_active(true);
353 let mut message = self.message.borrow_mut();
354 if !message.is_empty() {
355 message.push('\n');
356 }
357 message.push_str(msg);
358 }
359}
360
361impl Default for MsgDialogState {
362 fn default() -> Self {
363 let s = Self {
364 active: Default::default(),
365 area: Default::default(),
366 inner: Default::default(),
367 message: Default::default(),
368 button: Default::default(),
369 paragraph: Default::default(),
370 message_title: Default::default(),
371 };
372 s.paragraph.borrow().focus.set(true);
373 s
374 }
375}
376
377impl MsgDialogState {
378 fn build_focus(&self) -> Focus {
379 let mut fb = FocusBuilder::default();
380 fb.widget(&*self.paragraph.borrow())
381 .widget(&*self.button.borrow());
382 fb.build()
383 }
384}
385
386impl<'a> StatefulWidget for &MsgDialog<'a> {
387 type State = MsgDialogState;
388
389 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
390 render_ref(self, area, buf, state);
391 }
392}
393
394impl StatefulWidget for MsgDialog<'_> {
395 type State = MsgDialogState;
396
397 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
398 render_ref(&self, area, buf, state);
399 }
400}
401
402fn render_ref(widget: &MsgDialog<'_>, area: Rect, buf: &mut Buffer, state: &mut MsgDialogState) {
403 state.area = area;
404
405 if !state.active.get() {
406 return;
407 }
408
409 let mut block;
410 let title = state.message_title.borrow();
411 let block = if let Some(b) = &widget.block {
412 if !title.is_empty() {
413 block = b.clone().title(title.as_str());
414 &block
415 } else {
416 b
417 }
418 } else {
419 block = Block::bordered()
420 .style(widget.style)
421 .padding(Padding::new(1, 1, 1, 1));
422 if !title.is_empty() {
423 block = block.title(title.as_str());
424 }
425 &block
426 };
427
428 let l_dlg = layout_dialog(
429 area, block_padding2(block),
431 [Constraint::Length(10)],
432 0,
433 Flex::End,
434 );
435 state.area = l_dlg.area();
436 state.inner = l_dlg.widget_for(DialogItem::Inner);
437
438 let header_1 = widget.markdown_header_1.unwrap_or_default();
439 let header_n = widget.markdown_header_n.unwrap_or_default();
440
441 reset_buf_area(state.area, buf);
442 block.render(state.area, buf);
443
444 {
445 let scroll = if let Some(style) = &widget.scroll_style {
446 Scroll::new().styles(style.clone())
447 } else {
448 Scroll::new().style(widget.style)
449 };
450
451 let message = state.message.borrow();
452 let mut lines = Vec::new();
453 if widget.markdown {
454 for t in message.lines() {
455 if t.starts_with("##") {
456 lines.push(Line::from(t).style(header_n));
457 } else if t.starts_with("#") {
458 lines.push(Line::from(t).style(header_1));
459 } else {
460 lines.push(Line::from(t));
461 }
462 }
463 } else {
464 for t in message.lines() {
465 lines.push(Line::from(t));
466 }
467 }
468
469 let text = Text::from(lines).alignment(Alignment::Center);
470
471 Paragraph::new(text)
472 .scroll(scroll)
473 .hide_focus(widget.hide_paragraph_focus)
474 .render(
475 l_dlg.widget_for(DialogItem::Content),
476 buf,
477 &mut state.paragraph.borrow_mut(),
478 );
479 }
480
481 Button::new("Ok")
482 .styles_opt(widget.button_style.clone())
483 .render(
484 l_dlg.widget_for(DialogItem::Button(0)),
485 buf,
486 &mut state.button.borrow_mut(),
487 );
488}
489
490impl HandleEvent<crossterm::event::Event, Dialog, Outcome> for MsgDialogState {
491 fn handle(&mut self, event: &crossterm::event::Event, _: Dialog) -> Outcome {
492 if self.active.get() {
493 let mut focus = self.build_focus();
494 let f = focus.handle(event, Regular);
495
496 let mut r = match self
497 .button
498 .borrow_mut()
499 .handle(event, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE))
500 {
501 ButtonOutcome::Pressed => {
502 self.clear();
503 self.active.set(false);
504 Outcome::Changed
505 }
506 v => v.into(),
507 };
508 r = r.or_else(|| self.paragraph.borrow_mut().handle(event, Regular));
509 r = r.or_else(|| match event {
510 ct_event!(keycode press Esc) => {
511 self.clear();
512 self.active.set(false);
513 Outcome::Changed
514 }
515 _ => Outcome::Continue,
516 });
517 max(max(Outcome::Unchanged, f), r)
519 } else {
520 Outcome::Continue
521 }
522 }
523}
524
525pub fn handle_dialog_events(
527 state: &mut MsgDialogState,
528 event: &crossterm::event::Event,
529) -> Outcome {
530 state.handle(event, Dialog)
531}