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