Skip to main content

ratatui_textarea/
widget.rs

1use crate::textarea::TextArea;
2use crate::util::num_digits;
3use crate::wrap::WrapMode;
4#[cfg(feature = "portable-atomic")]
5use portable_atomic::{AtomicU64, Ordering};
6use ratatui_core::buffer::Buffer;
7use ratatui_core::layout::Rect;
8use ratatui_core::text::{Line, Span, Text};
9use ratatui_core::widgets::Widget;
10use ratatui_widgets::paragraph::Paragraph;
11use std::cmp;
12#[cfg(not(feature = "portable-atomic"))]
13use std::sync::atomic::{AtomicU64, Ordering};
14
15// &mut 'a (u16, u16, u16, u16) is not available since `render` method takes immutable reference of TextArea
16// instance. In the case, the TextArea instance cannot be accessed from any other objects since it is mutablly
17// borrowed.
18//
19// `ratatui::Frame::render_stateful_widget` would be an assumed way to render a stateful widget. But at this
20// point we stick with using `ratatui::Frame::render_widget` because it is simpler API. Users don't need to
21// manage states of textarea instances separately.
22// https://docs.rs/ratatui/latest/ratatui/terminal/struct.Frame.html#method.render_stateful_widget
23#[derive(Default, Debug)]
24pub struct Viewport(AtomicU64);
25
26impl Clone for Viewport {
27    fn clone(&self) -> Self {
28        let u = self.0.load(Ordering::Relaxed);
29        Viewport(AtomicU64::new(u))
30    }
31}
32
33impl Viewport {
34    pub fn scroll_top(&self) -> (u16, u16) {
35        let u = self.0.load(Ordering::Relaxed);
36        ((u >> 16) as u16, u as u16)
37    }
38
39    pub fn rect(&self) -> (u16, u16, u16, u16) {
40        let u = self.0.load(Ordering::Relaxed);
41        let width = (u >> 48) as u16;
42        let height = (u >> 32) as u16;
43        let row = (u >> 16) as u16;
44        let col = u as u16;
45        (row, col, width, height)
46    }
47
48    pub fn position(&self) -> (u16, u16, u16, u16) {
49        let (row_top, col_top, width, height) = self.rect();
50        let row_bottom = row_top.saturating_add(height).saturating_sub(1);
51        let col_bottom = col_top.saturating_add(width).saturating_sub(1);
52
53        (
54            row_top,
55            col_top,
56            cmp::max(row_top, row_bottom),
57            cmp::max(col_top, col_bottom),
58        )
59    }
60
61    fn store(&self, row: u16, col: u16, width: u16, height: u16) {
62        // Pack four u16 values into one u64 value
63        let u =
64            ((width as u64) << 48) | ((height as u64) << 32) | ((row as u64) << 16) | col as u64;
65        self.0.store(u, Ordering::Relaxed);
66    }
67
68    pub fn scroll(&mut self, rows: i16, cols: i16) {
69        fn apply_scroll(pos: u16, delta: i16) -> u16 {
70            if delta >= 0 {
71                pos.saturating_add(delta as u16)
72            } else {
73                pos.saturating_sub(-delta as u16)
74            }
75        }
76
77        let u = self.0.get_mut();
78        let row = apply_scroll((*u >> 16) as u16, rows);
79        let col = apply_scroll(*u as u16, cols);
80        *u = (*u & 0xffff_ffff_0000_0000) | ((row as u64) << 16) | (col as u64);
81    }
82}
83
84#[inline]
85fn next_scroll_top(prev_top: u16, cursor: u16, len: u16) -> u16 {
86    if cursor < prev_top {
87        cursor
88    } else if prev_top + len <= cursor {
89        cursor + 1 - len
90    } else {
91        prev_top
92    }
93}
94
95impl<'a> TextArea<'a> {
96    fn text_widget(&'a self, top_row: usize, height: usize) -> Text<'a> {
97        let lnum_len = num_digits(self.lines().len());
98        let screen_lines = self.screen_lines.borrow();
99        let bottom_row = cmp::min(top_row + height, screen_lines.len());
100        let mut lines = Vec::with_capacity(bottom_row - top_row);
101        for row in &screen_lines[top_row..bottom_row] {
102            let line = &self.lines()[row.wrapped.row];
103            lines.push(self.line_spans_segment(line, &row.wrapped, lnum_len));
104        }
105        Text::from(lines)
106    }
107
108    fn scroll_top_row(&self, prev_top: u16, height: u16) -> u16 {
109        next_scroll_top(prev_top, self.screen_cursor().row as u16, height)
110    }
111
112    fn scroll_top_col(&self, prev_top: u16, width: u16) -> u16 {
113        let mut cursor = self.screen_cursor().col as u16;
114        // Adjust the cursor position due to the width of line number.
115        if self.line_number_style().is_some() {
116            let lnum = num_digits(self.lines().len()) as u16 + 2; // `+ 2` for margins
117            if cursor <= lnum {
118                cursor *= 2; // Smoothly slide the line number into the screen on scrolling left
119            } else {
120                cursor += lnum; // The cursor position is shifted by the line number part
121            };
122        }
123        next_scroll_top(prev_top, cursor, width)
124    }
125}
126
127impl Widget for &TextArea<'_> {
128    fn render(self, area: Rect, buf: &mut Buffer) {
129        let inner_area = if let Some(b) = self.block() {
130            b.inner(area)
131        } else {
132            area
133        };
134        let Rect { width, height, .. } = inner_area;
135
136        if self.area.get() != inner_area {
137            self.area.set(inner_area);
138            self.screen_map_load();
139        }
140
141        let (prev_top_row, prev_top_col) = self.viewport.scroll_top();
142        let (text, top_row, top_col) = if self.is_empty() && !self.placeholder.lines.is_empty() {
143            let mut placeholder = self.placeholder.clone();
144            let cursor = Span::styled(" ", self.cursor_style);
145            if let Some(first_line) = placeholder.lines.first_mut() {
146                first_line.spans.insert(0, cursor);
147            } else {
148                placeholder.lines.push(Line::from(vec![cursor]));
149            }
150            (placeholder, 0u16, 0u16)
151        } else {
152            let top_row = self.scroll_top_row(prev_top_row, height);
153            let top_col = if self.wrap_mode() == WrapMode::None {
154                self.scroll_top_col(prev_top_col, width)
155            } else {
156                0
157            };
158            (
159                self.text_widget(top_row as _, height as _)
160                    .style(self.style()),
161                top_row,
162                top_col,
163            )
164        };
165
166        // To get fine control over the text color and the surrrounding block they have to be rendered separately
167        // see https://github.com/ratatui/ratatui/issues/144
168        let mut text_area = area;
169        let mut inner = Paragraph::new(text).alignment(self.alignment());
170        if let Some(b) = self.block() {
171            text_area = b.inner(area);
172            b.render(area, buf)
173        }
174        if top_col != 0 {
175            inner = inner.scroll((0, top_col));
176        }
177
178        // Store scroll top position for rendering on the next tick
179        self.viewport.store(top_row, top_col, width, height);
180
181        inner.render(text_area, buf);
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn viewport_store_and_load() {
191        let vp = Viewport::default();
192        vp.store(3, 7, 80, 24);
193        assert_eq!(vp.scroll_top(), (3, 7));
194        let (row, col, width, height) = vp.rect();
195        assert_eq!((row, col, width, height), (3, 7, 80, 24));
196    }
197
198    #[test]
199    fn viewport_clone() {
200        let vp = Viewport::default();
201        vp.store(5, 2, 40, 10);
202        let vp2 = vp.clone();
203        assert_eq!(vp2.scroll_top(), (5, 2));
204    }
205}