rat_widget/
paragraph.rs

1//!
2//! Extensions for ratatui Paragraph.
3//!
4
5use 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/// List widget.
23///
24/// Fully compatible with ratatui Paragraph.
25/// Add Scroll and event-handling.
26#[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/// State & event handling.
51#[derive(Debug)]
52pub struct ParagraphState {
53    /// Full area of the widget.
54    /// __readonly__. renewed for each render.
55    pub area: Rect,
56    /// Inner area of the widget.
57    /// __readonly__. renewed for each render.
58    pub inner: Rect,
59
60    /// Text lines
61    pub lines: usize,
62
63    /// Vertical scroll.
64    /// __read+write__
65    pub vscroll: ScrollState,
66    /// Horizontal scroll.
67    /// __read+write__
68    pub hscroll: ScrollState,
69
70    /// Focus.
71    /// __read+write__
72    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    /// Text
101    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    /// Block.
111    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    /// Set both hscroll and vscroll.
118    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    /// Set horizontal scroll.
125    pub fn hscroll(mut self, scroll: Scroll<'a>) -> Self {
126        self.hscroll = Some(scroll.override_horizontal());
127        self
128    }
129
130    /// Set vertical scroll.
131    pub fn vscroll(mut self, scroll: Scroll<'a>) -> Self {
132        self.vscroll = Some(scroll.override_vertical());
133        self
134    }
135
136    /// Styles.
137    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    /// Base style.
154    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    /// Base style.
161    pub fn focus_style(mut self, style: Style) -> Self {
162        self.focus_style = Some(style);
163        self
164    }
165
166    /// Word wrap.
167    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    /// Text alignment.
178    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    /// Text alignment.
187    pub fn left_aligned(self) -> Self {
188        self.alignment(Alignment::Left)
189    }
190
191    /// Text alignment.
192    pub fn centered(self) -> Self {
193        self.alignment(Alignment::Center)
194    }
195
196    /// Text alignment.
197    pub fn right_aligned(self) -> Self {
198        self.alignment(Alignment::Right)
199    }
200
201    /// Line width when not wrapped.
202    pub fn line_width(&self) -> usize {
203        self.para.borrow().line_width()
204    }
205
206    /// Line height for the supposed width.
207    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    // take paragraph
245    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    // update scroll
255    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    // not the final inner, showing the scrollbar might change this.
261    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    (&para).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    /// Current offset.
386    pub fn line_offset(&self) -> usize {
387        self.vscroll.offset()
388    }
389
390    /// Set limited offset.
391    pub fn set_line_offset(&mut self, offset: usize) -> bool {
392        self.vscroll.set_offset(offset)
393    }
394
395    /// Current offset.
396    pub fn col_offset(&self) -> usize {
397        self.hscroll.offset()
398    }
399
400    /// Set limited offset.
401    pub fn set_col_offset(&mut self, offset: usize) -> bool {
402        self.hscroll.set_offset(offset)
403    }
404
405    /// Scroll left by n.
406    pub fn scroll_left(&mut self, n: usize) -> bool {
407        self.hscroll.scroll_left(n)
408    }
409
410    /// Scroll right by n.
411    pub fn scroll_right(&mut self, n: usize) -> bool {
412        self.hscroll.scroll_right(n)
413    }
414
415    /// Scroll up by n.
416    pub fn scroll_up(&mut self, n: usize) -> bool {
417        self.vscroll.scroll_up(n)
418    }
419
420    /// Scroll down by n.
421    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}