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