1use crate::_private::NonExhaustive;
6use crate::text::HasScreenCursor;
7use crate::util::revert_style;
8use rat_event::{HandleEvent, MouseOnly, Outcome, Regular, ct_event, event_flow};
9use rat_focus::{FocusBuilder, FocusFlag, HasFocus};
10use rat_reloc::{RelocatableState, relocate_area};
11use rat_scrolled::event::ScrollOutcome;
12use rat_scrolled::{Scroll, ScrollArea, ScrollAreaState, ScrollState, ScrollStyle};
13use ratatui::buffer::Buffer;
14use ratatui::layout::{Alignment, Position, Rect};
15use ratatui::style::Style;
16use ratatui::text::Text;
17use ratatui::widgets::{Block, StatefulWidget, Widget, Wrap};
18use std::cell::RefCell;
19use std::cmp::min;
20use std::mem;
21use std::ops::DerefMut;
22
23#[derive(Debug, Clone, Default)]
28pub struct Paragraph<'a> {
29 style: Style,
30 block: Option<Block<'a>>,
31 vscroll: Option<Scroll<'a>>,
32 hscroll: Option<Scroll<'a>>,
33
34 hide_focus: bool,
35 focus_style: Option<Style>,
36
37 wrap: Option<Wrap>,
38 para: RefCell<ratatui::widgets::Paragraph<'a>>,
39}
40
41#[derive(Debug, Clone)]
42pub struct ParagraphStyle {
43 pub style: Style,
44 pub block: Option<Block<'static>>,
45 pub border_style: Option<Style>,
46 pub title_style: Option<Style>,
47 pub scroll: Option<ScrollStyle>,
48
49 pub hide_focus: Option<bool>,
50 pub focus: Option<Style>,
51
52 pub non_exhaustive: NonExhaustive,
53}
54
55#[derive(Debug)]
57pub struct ParagraphState {
58 pub area: Rect,
61 pub inner: Rect,
64
65 pub lines: usize,
67
68 pub vscroll: ScrollState,
71 pub hscroll: ScrollState,
74
75 pub focus: FocusFlag,
78
79 pub non_exhaustive: NonExhaustive,
80}
81
82impl Default for ParagraphStyle {
83 fn default() -> Self {
84 Self {
85 style: Default::default(),
86 block: Default::default(),
87 border_style: Default::default(),
88 title_style: Default::default(),
89 scroll: Default::default(),
90 hide_focus: Default::default(),
91 focus: Default::default(),
92 non_exhaustive: NonExhaustive,
93 }
94 }
95}
96
97impl<'a> Paragraph<'a> {
98 pub fn new<T>(text: T) -> Self
99 where
100 T: Into<Text<'a>>,
101 {
102 Self {
103 para: RefCell::new(ratatui::widgets::Paragraph::new(text)),
104 ..Default::default()
105 }
106 }
107
108 pub fn hide_focus(mut self, show: bool) -> Self {
110 self.hide_focus = show;
111 self
112 }
113
114 pub fn text(mut self, text: impl Into<Text<'a>>) -> Self {
116 let mut para = ratatui::widgets::Paragraph::new(text);
117 if let Some(wrap) = self.wrap {
118 para = para.wrap(wrap);
119 }
120 self.para = RefCell::new(para);
121 self
122 }
123
124 pub fn block(mut self, block: Block<'a>) -> Self {
126 self.block = Some(block);
127 self.block = self.block.map(|v| v.style(self.style));
128 self
129 }
130
131 pub fn scroll(mut self, scroll: Scroll<'a>) -> Self {
133 self.hscroll = Some(scroll.clone().override_horizontal());
134 self.vscroll = Some(scroll.override_vertical());
135 self
136 }
137
138 pub fn hscroll(mut self, scroll: Scroll<'a>) -> Self {
140 self.hscroll = Some(scroll.override_horizontal());
141 self
142 }
143
144 pub fn vscroll(mut self, scroll: Scroll<'a>) -> Self {
146 self.vscroll = Some(scroll.override_vertical());
147 self
148 }
149
150 pub fn styles(mut self, styles: ParagraphStyle) -> Self {
152 self.style = styles.style;
153 if styles.block.is_some() {
154 self.block = styles.block;
155 }
156 if let Some(hide_focus) = styles.hide_focus {
157 self.hide_focus = hide_focus;
158 }
159 if let Some(border_style) = styles.border_style {
160 self.block = self.block.map(|v| v.border_style(border_style));
161 }
162 if let Some(title_style) = styles.title_style {
163 self.block = self.block.map(|v| v.title_style(title_style));
164 }
165 self.block = self.block.map(|v| v.style(self.style));
166 if let Some(styles) = styles.scroll {
167 self.hscroll = self.hscroll.map(|v| v.styles(styles.clone()));
168 self.vscroll = self.vscroll.map(|v| v.styles(styles));
169 }
170
171 if styles.focus.is_some() {
172 self.focus_style = styles.focus;
173 }
174
175 self
176 }
177
178 pub fn style(mut self, style: Style) -> Self {
180 self.style = style;
181 self.block = self.block.map(|v| v.style(self.style));
182 self
183 }
184
185 pub fn focus_style(mut self, style: Style) -> Self {
187 self.focus_style = Some(style);
188 self
189 }
190
191 pub fn wrap(mut self, wrap: Wrap) -> Self {
193 self.wrap = Some(wrap);
194
195 let mut para = mem::take(self.para.borrow_mut().deref_mut());
196 para = para.wrap(wrap);
197 self.para = RefCell::new(para);
198
199 self
200 }
201
202 pub fn alignment(mut self, alignment: Alignment) -> Self {
204 let mut para = mem::take(self.para.borrow_mut().deref_mut());
205 para = para.alignment(alignment);
206 self.para = RefCell::new(para);
207
208 self
209 }
210
211 pub fn left_aligned(self) -> Self {
213 self.alignment(Alignment::Left)
214 }
215
216 pub fn centered(self) -> Self {
218 self.alignment(Alignment::Center)
219 }
220
221 pub fn right_aligned(self) -> Self {
223 self.alignment(Alignment::Right)
224 }
225
226 pub fn line_width(&self) -> usize {
228 self.para.borrow().line_width()
229 }
230
231 pub fn line_height(&self, width: u16) -> usize {
233 let sa = ScrollArea::new()
234 .block(self.block.as_ref())
235 .h_scroll(self.hscroll.as_ref())
236 .v_scroll(self.vscroll.as_ref());
237 let padding = sa.padding();
238
239 self.para
240 .borrow()
241 .line_count(width.saturating_sub(padding.left + padding.right))
242 }
243}
244
245impl<'a> StatefulWidget for &Paragraph<'a> {
246 type State = ParagraphState;
247
248 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
249 render_paragraph(self, area, buf, state);
250 }
251}
252
253impl StatefulWidget for Paragraph<'_> {
254 type State = ParagraphState;
255
256 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
257 render_paragraph(&self, area, buf, state);
258 }
259}
260
261fn render_paragraph(
262 widget: &Paragraph<'_>,
263 area: Rect,
264 buf: &mut Buffer,
265 state: &mut ParagraphState,
266) {
267 state.area = area;
268
269 let mut para = mem::take(widget.para.borrow_mut().deref_mut());
271
272 let style = widget.style;
273 let focus_style = if let Some(focus_style) = widget.focus_style {
274 style.patch(focus_style)
275 } else {
276 revert_style(widget.style)
277 };
278
279 let sa = ScrollArea::new()
281 .block(widget.block.as_ref())
282 .h_scroll(widget.hscroll.as_ref())
283 .v_scroll(widget.vscroll.as_ref())
284 .style(style);
285 let tmp_inner = sa.inner(area, Some(&state.hscroll), Some(&state.vscroll));
287 let pad_inner = sa.padding();
288
289 state.lines = para.line_count(area.width.saturating_sub(pad_inner.left + pad_inner.right));
290
291 state
292 .vscroll
293 .set_max_offset(state.lines.saturating_sub(tmp_inner.height as usize));
294 state.vscroll.set_page_len(tmp_inner.height as usize);
295 state.hscroll.set_max_offset(if widget.wrap.is_some() {
296 0
297 } else {
298 para.line_width().saturating_sub(tmp_inner.width as usize)
299 });
300 state.hscroll.set_page_len(tmp_inner.width as usize);
301 state.inner = sa.inner(area, Some(&state.hscroll), Some(&state.vscroll));
302
303 sa.render(
304 area,
305 buf,
306 &mut ScrollAreaState::new()
307 .h_scroll(&mut state.hscroll)
308 .v_scroll(&mut state.vscroll),
309 );
310
311 para = para.scroll((state.vscroll.offset() as u16, state.hscroll.offset() as u16));
312 (¶).render(state.inner, buf);
313
314 if !widget.hide_focus && state.is_focused() {
315 let mut tag = None;
316 for x in state.inner.left()..state.inner.right() {
317 if let Some(cell) = buf.cell_mut(Position::new(x, state.inner.y)) {
318 if tag.is_none() {
319 if cell.symbol() != " " {
320 tag = Some(true);
321 }
322 } else {
323 if cell.symbol() == " " {
324 tag = Some(false);
325 }
326 }
327 if tag == Some(true) || (x - state.inner.x < 3) {
328 cell.set_style(focus_style);
329 }
330 }
331 }
332
333 let y = min(
334 state.inner.y as usize + state.vscroll.page_len() * 6 / 10,
335 (state.inner.y as usize + state.vscroll.max_offset)
336 .saturating_sub(state.vscroll.offset),
337 );
338
339 if y as u16 >= state.inner.y {
340 buf.set_style(Rect::new(state.inner.x, y as u16, 1, 1), focus_style);
341 }
342 }
343
344 *widget.para.borrow_mut().deref_mut() = para;
345}
346
347impl HasFocus for ParagraphState {
348 fn build(&self, builder: &mut FocusBuilder) {
349 builder.leaf_widget(self);
350 }
351
352 fn focus(&self) -> FocusFlag {
353 self.focus.clone()
354 }
355
356 fn area(&self) -> Rect {
357 self.area
358 }
359}
360
361impl HasScreenCursor for ParagraphState {
362 fn screen_cursor(&self) -> Option<(u16, u16)> {
363 None
364 }
365}
366
367impl RelocatableState for ParagraphState {
368 fn relocate(&mut self, offset: (i16, i16), clip: Rect) {
369 self.area = relocate_area(self.area, offset, clip);
370 self.inner = relocate_area(self.inner, offset, clip);
371 self.hscroll.relocate(offset, clip);
372 self.vscroll.relocate(offset, clip);
373 }
374}
375
376impl Clone for ParagraphState {
377 fn clone(&self) -> Self {
378 Self {
379 area: self.area,
380 inner: self.inner,
381 lines: self.lines,
382 vscroll: self.vscroll.clone(),
383 hscroll: self.hscroll.clone(),
384 focus: self.focus.new_instance(),
385 non_exhaustive: NonExhaustive,
386 }
387 }
388}
389
390impl Default for ParagraphState {
391 fn default() -> Self {
392 Self {
393 area: Default::default(),
394 inner: Default::default(),
395 focus: Default::default(),
396 vscroll: Default::default(),
397 hscroll: Default::default(),
398 non_exhaustive: NonExhaustive,
399 lines: 0,
400 }
401 }
402}
403
404impl ParagraphState {
405 pub fn new() -> Self {
406 Self::default()
407 }
408
409 pub fn named(name: &str) -> Self {
410 let mut z = Self::default();
411 z.focus = z.focus.with_name(name);
412 z
413 }
414
415 pub fn line_offset(&self) -> usize {
417 self.vscroll.offset()
418 }
419
420 pub fn set_line_offset(&mut self, offset: usize) -> bool {
422 self.vscroll.set_offset(offset)
423 }
424
425 pub fn col_offset(&self) -> usize {
427 self.hscroll.offset()
428 }
429
430 pub fn set_col_offset(&mut self, offset: usize) -> bool {
432 self.hscroll.set_offset(offset)
433 }
434
435 pub fn scroll_left(&mut self, n: usize) -> bool {
437 self.hscroll.scroll_left(n)
438 }
439
440 pub fn scroll_right(&mut self, n: usize) -> bool {
442 self.hscroll.scroll_right(n)
443 }
444
445 pub fn scroll_up(&mut self, n: usize) -> bool {
447 self.vscroll.scroll_up(n)
448 }
449
450 pub fn scroll_down(&mut self, n: usize) -> bool {
452 self.vscroll.scroll_down(n)
453 }
454}
455
456impl HandleEvent<crossterm::event::Event, Regular, Outcome> for ParagraphState {
457 fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Regular) -> Outcome {
458 event_flow!(
459 return if self.is_focused() {
460 match event {
461 ct_event!(keycode press Up) => self.scroll_up(1).into(),
462 ct_event!(keycode press Down) => self.scroll_down(1).into(),
463 ct_event!(keycode press PageUp) => {
464 self.scroll_up(self.vscroll.page_len() * 6 / 10).into()
465 }
466 ct_event!(keycode press PageDown) => {
467 self.scroll_down(self.vscroll.page_len() * 6 / 10).into()
468 }
469 ct_event!(keycode press Home) => self.set_line_offset(0).into(),
470 ct_event!(keycode press End) => {
471 self.set_line_offset(self.vscroll.max_offset()).into()
472 }
473
474 ct_event!(keycode press Left) => self.scroll_left(1).into(),
475 ct_event!(keycode press Right) => self.scroll_right(1).into(),
476 ct_event!(keycode press ALT-PageUp) => {
477 self.scroll_left(self.hscroll.page_len() * 6 / 10).into()
478 }
479 ct_event!(keycode press ALT-PageDown) => {
480 self.scroll_right(self.hscroll.page_len() * 6 / 10).into()
481 }
482 ct_event!(keycode press ALT-Home) => self.set_col_offset(0).into(),
483 ct_event!(keycode press ALT-End) => {
484 self.set_col_offset(self.hscroll.max_offset()).into()
485 }
486
487 _ => Outcome::Continue,
488 }
489 } else {
490 Outcome::Continue
491 }
492 );
493
494 self.handle(event, MouseOnly)
495 }
496}
497
498impl HandleEvent<crossterm::event::Event, MouseOnly, Outcome> for ParagraphState {
499 fn handle(&mut self, event: &crossterm::event::Event, _keymap: MouseOnly) -> Outcome {
500 let mut sas = ScrollAreaState::new()
501 .area(self.inner)
502 .h_scroll(&mut self.hscroll)
503 .v_scroll(&mut self.vscroll);
504 match sas.handle(event, MouseOnly) {
505 ScrollOutcome::Up(v) => {
506 if self.scroll_up(v) {
507 Outcome::Changed
508 } else {
509 Outcome::Continue
510 }
511 }
512 ScrollOutcome::Down(v) => {
513 if self.scroll_down(v) {
514 Outcome::Changed
515 } else {
516 Outcome::Continue
517 }
518 }
519 ScrollOutcome::Left(v) => {
520 if self.scroll_left(v) {
521 Outcome::Changed
522 } else {
523 Outcome::Continue
524 }
525 }
526 ScrollOutcome::Right(v) => {
527 if self.scroll_right(v) {
528 Outcome::Changed
529 } else {
530 Outcome::Continue
531 }
532 }
533 ScrollOutcome::VPos(v) => self.set_line_offset(v).into(),
534 ScrollOutcome::HPos(v) => self.set_col_offset(v).into(),
535 r => r.into(),
536 }
537 }
538}
539
540pub fn handle_events(
544 state: &mut ParagraphState,
545 focus: bool,
546 event: &crossterm::event::Event,
547) -> Outcome {
548 state.focus.set(focus);
549 HandleEvent::handle(state, event, Regular)
550}
551
552pub fn handle_mouse_events(state: &mut ParagraphState, event: &crossterm::event::Event) -> Outcome {
554 HandleEvent::handle(state, event, MouseOnly)
555}