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