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