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