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