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