rat_widget/
paragraph.rs

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