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