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#[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 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 if self.line_number_style().is_some() {
116 let lnum = num_digits(self.lines().len()) as u16 + 2; if cursor <= lnum {
118 cursor *= 2; } else {
120 cursor += lnum; };
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 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 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}