1use crate::ratatui::buffer::Buffer;
2use crate::ratatui::layout::Rect;
3use crate::ratatui::text::{Span, Text};
4use crate::ratatui::widgets::{Paragraph, Widget};
5use crate::textarea::TextArea;
6use crate::util::num_digits;
7#[cfg(feature = "ratatui")]
8use ratatui::text::Line;
9use std::cmp;
10use std::sync::atomic::{AtomicU64, Ordering};
11#[cfg(feature = "tuirs")]
12use tui::text::Spans as Line;
13
14#[derive(Default, Debug)]
23pub struct Viewport(AtomicU64);
24
25impl Clone for Viewport {
26 fn clone(&self) -> Self {
27 let u = self.0.load(Ordering::Relaxed);
28 Viewport(AtomicU64::new(u))
29 }
30}
31
32impl Viewport {
33 pub fn scroll_top(&self) -> (u16, u16) {
34 let u = self.0.load(Ordering::Relaxed);
35 ((u >> 16) as u16, u as u16)
36 }
37
38 pub fn rect(&self) -> (u16, u16, u16, u16) {
39 let u = self.0.load(Ordering::Relaxed);
40 let width = (u >> 48) as u16;
41 let height = (u >> 32) as u16;
42 let row = (u >> 16) as u16;
43 let col = u as u16;
44 (row, col, width, height)
45 }
46
47 pub fn position(&self) -> (u16, u16, u16, u16) {
48 let (row_top, col_top, width, height) = self.rect();
49 let row_bottom = row_top.saturating_add(height).saturating_sub(1);
50 let col_bottom = col_top.saturating_add(width).saturating_sub(1);
51
52 (
53 row_top,
54 col_top,
55 cmp::max(row_top, row_bottom),
56 cmp::max(col_top, col_bottom),
57 )
58 }
59
60 fn store(&self, row: u16, col: u16, width: u16, height: u16) {
61 let u =
63 ((width as u64) << 48) | ((height as u64) << 32) | ((row as u64) << 16) | col as u64;
64 self.0.store(u, Ordering::Relaxed);
65 }
66
67 pub fn scroll(&mut self, rows: i16, cols: i16) {
68 fn apply_scroll(pos: u16, delta: i16) -> u16 {
69 if delta >= 0 {
70 pos.saturating_add(delta as u16)
71 } else {
72 pos.saturating_sub(-delta as u16)
73 }
74 }
75
76 let u = self.0.get_mut();
77 let row = apply_scroll((*u >> 16) as u16, rows);
78 let col = apply_scroll(*u as u16, cols);
79 *u = (*u & 0xffff_ffff_0000_0000) | ((row as u64) << 16) | (col as u64);
80 }
81}
82
83#[inline]
84fn next_scroll_top(prev_top: u16, cursor: u16, len: u16) -> u16 {
85 if cursor < prev_top {
86 cursor
87 } else if prev_top + len <= cursor {
88 cursor + 1 - len
89 } else {
90 prev_top
91 }
92}
93
94impl<'a> TextArea<'a> {
95 fn text_widget(&'a self, top_row: usize, height: usize) -> Text<'a> {
96 let lines_len = self.lines().len();
97 let lnum_len = num_digits(lines_len);
98 let bottom_row = cmp::min(top_row + height, lines_len);
99 let mut lines = Vec::with_capacity(bottom_row - top_row);
100 for (i, line) in self.lines()[top_row..bottom_row].iter().enumerate() {
101 lines.push(self.line_spans(line.as_str(), top_row + i, lnum_len));
102 }
103 Text::from(lines)
104 }
105
106 fn placeholder_widget(&'a self) -> Text<'a> {
107 let cursor = Span::styled(" ", self.cursor_style);
108 let text = Span::raw(self.placeholder.as_str());
109 Text::from(Line::from(vec![cursor, text]))
110 }
111
112 fn scroll_top_row(&self, prev_top: u16, height: u16) -> u16 {
113 next_scroll_top(prev_top, self.cursor().0 as u16, height)
114 }
115
116 fn scroll_top_col(&self, prev_top: u16, width: u16) -> u16 {
117 let mut cursor = self.cursor().1 as u16;
118 if self.line_number_style().is_some() {
120 let lnum = num_digits(self.lines().len()) as u16 + 2; if cursor <= lnum {
122 cursor *= 2; } else {
124 cursor += lnum; };
126 }
127 next_scroll_top(prev_top, cursor, width)
128 }
129}
130
131impl Widget for &TextArea<'_> {
132 fn render(self, area: Rect, buf: &mut Buffer) {
133 let Rect { width, height, .. } = if let Some(b) = self.block() {
134 b.inner(area)
135 } else {
136 area
137 };
138
139 let (top_row, top_col) = self.viewport.scroll_top();
140 let top_row = self.scroll_top_row(top_row, height);
141 let top_col = self.scroll_top_col(top_col, width);
142
143 let (text, style) = if !self.placeholder.is_empty() && self.is_empty() {
144 (self.placeholder_widget(), self.placeholder_style)
145 } else {
146 (self.text_widget(top_row as _, height as _), self.style())
147 };
148
149 let mut text_area = area;
152 let mut inner = Paragraph::new(text)
153 .style(style)
154 .alignment(self.alignment());
155 if let Some(b) = self.block() {
156 text_area = b.inner(area);
157 #[cfg(feature = "tuirs")]
160 let b = b.clone();
161 b.render(area, buf)
162 }
163 if top_col != 0 {
164 inner = inner.scroll((0, top_col));
165 }
166
167 self.viewport.store(top_row, top_col, width, height);
169
170 inner.render(text_area, buf);
171 }
172}