cursive_core/view/scroll_base.rs
1use crate::div::div_up;
2use crate::style::Style;
3use crate::Printer;
4use crate::Vec2;
5use std::cmp::{max, min};
6
7/// Provide scrolling functionalities to a view.
8///
9/// This is a legacy helper utility to define scrollable views.
10///
11/// The [`scroll`] module is the preferred way to achieve that.
12///
13/// [`scroll`]: ./scroll/index.html
14#[derive(Default, Debug)]
15#[deprecated(
16 since = "0.16.0",
17 note = "`ScrollBase` is being deprecated in favor of the view::scroll module."
18)]
19pub struct ScrollBase {
20 /// First line visible
21 pub start_line: usize,
22
23 /// Content height
24 pub content_height: usize,
25
26 /// Number of lines displayed
27 pub view_height: usize,
28
29 /// Padding for the scrollbar
30 ///
31 /// If present, the scrollbar will be shifted
32 /// `scrollbar_offset` columns to the left.
33 ///
34 /// (Useful when each item includes its own side borders,
35 /// to draw the scrollbar inside.)
36 pub scrollbar_offset: usize,
37
38 /// Blank between the text and the scrollbar.
39 pub right_padding: usize,
40
41 /// Initial position of the cursor when dragging.
42 pub thumb_grab: Option<usize>,
43}
44
45#[allow(deprecated)]
46impl ScrollBase {
47 /// Creates a new, uninitialized scrollbar.
48 pub fn new() -> Self {
49 ScrollBase {
50 start_line: 0,
51 content_height: 0,
52 view_height: 0,
53 scrollbar_offset: 0,
54 right_padding: 1,
55 thumb_grab: None,
56 }
57 }
58
59 /// Shifts the scrollbar toward the inside of the view.
60 ///
61 /// Used by views that draw their side borders in the children.
62 /// Pushing the scrollbar to the left allows it to stay inside
63 /// the borders.
64 #[must_use]
65 pub fn scrollbar_offset(mut self, offset: usize) -> Self {
66 self.scrollbar_offset = offset;
67 self
68 }
69
70 /// Sets the number of blank cells between the text and the scrollbar.
71 ///
72 /// Defaults to 1.
73 #[must_use]
74 pub fn right_padding(mut self, padding: usize) -> Self {
75 self.right_padding = padding;
76 self
77 }
78
79 /// Call this method when the content or the view changes.
80 pub fn set_heights(&mut self, view_height: usize, content_height: usize) {
81 self.view_height = view_height;
82 self.content_height = content_height;
83
84 // eprintln!("Setting heights: {} in {}", content_height, view_height);
85
86 if self.scrollable() {
87 self.start_line = min(self.start_line, self.content_height - self.view_height);
88 } else {
89 self.start_line = 0;
90 }
91 }
92
93 /// Returns `TRUE` if the view needs to scroll.
94 pub fn scrollable(&self) -> bool {
95 self.view_height < self.content_height
96 }
97
98 /// Returns `TRUE` unless we are at the top.
99 pub fn can_scroll_up(&self) -> bool {
100 self.start_line > 0
101 }
102
103 /// Returns `TRUE` unless we are at the bottom.
104 pub fn can_scroll_down(&self) -> bool {
105 self.start_line + self.view_height < self.content_height
106 }
107
108 /// Scroll to the top of the view.
109 pub fn scroll_top(&mut self) {
110 self.start_line = 0;
111 }
112
113 /// Makes sure that the given line is visible, scrolling if needed.
114 pub fn scroll_to(&mut self, y: usize) {
115 if y >= self.start_line + self.view_height {
116 self.start_line = 1 + y - self.view_height;
117 } else if y < self.start_line {
118 self.start_line = y;
119 }
120 }
121
122 /// Scroll to the bottom of the view.
123 pub fn scroll_bottom(&mut self) {
124 if self.scrollable() {
125 self.start_line = self.content_height - self.view_height;
126 }
127 }
128
129 /// Scroll down by the given number of line.
130 ///
131 /// Never further than the bottom of the view.
132 pub fn scroll_down(&mut self, n: usize) {
133 if self.scrollable() {
134 self.start_line = min(self.start_line + n, self.content_height - self.view_height);
135 }
136 }
137
138 /// Scrolls down until the scrollbar thumb is at the given location.
139 pub fn scroll_to_thumb(&mut self, thumb_y: usize, thumb_height: usize) {
140 // The min() is there to stop at the bottom of the content.
141 // The saturating_sub is there to stop at the bottom of the content.
142 // eprintln!("Scrolling to {}", thumb_y);
143 self.start_line = min(
144 div_up(
145 (1 + self.content_height - self.view_height) * thumb_y,
146 self.view_height - thumb_height + 1,
147 ),
148 self.content_height - self.view_height,
149 );
150 }
151
152 /// Scroll up by the given number of lines.
153 ///
154 /// Never above the top of the view.
155 pub fn scroll_up(&mut self, n: usize) {
156 if self.scrollable() {
157 self.start_line -= min(self.start_line, n);
158 }
159 }
160
161 /// Starts scrolling from the given cursor position.
162 pub fn start_drag(&mut self, position: Vec2, width: usize) -> bool {
163 // First: are we on the correct column?
164 let scrollbar_x = self.scrollbar_x(width);
165 // eprintln!("Grabbed {} for {}", position.x, scrollbar_x);
166 if position.x != scrollbar_x {
167 return false;
168 }
169
170 // Now, did we hit the thumb? Or should we direct-jump?
171 let height = self.scrollbar_thumb_height();
172 let thumb_y = self.scrollbar_thumb_y(height);
173
174 if position.y >= thumb_y && position.y < thumb_y + height {
175 // Grabbed!
176 self.thumb_grab = Some(position.y - thumb_y);
177 } else {
178 // Just jump a bit...
179 self.thumb_grab = Some((height - 1) / 2);
180 // eprintln!("Grabbed at {}", self.thumb_grab);
181 self.drag(position);
182 }
183
184 true
185 }
186
187 /// Keeps scrolling by dragging the cursor.
188 pub fn drag(&mut self, position: Vec2) {
189 // Our goal is self.scrollbar_thumb_y()+thumb_grab == position.y
190 // Which means that position.y is the middle of the scrollbar.
191 // eprintln!("Dragged: {:?}", position);
192 // eprintln!("thumb: {:?}", self.thumb_grab);
193 if let Some(grab) = self.thumb_grab {
194 let height = self.scrollbar_thumb_height();
195 self.scroll_to_thumb(position.y.saturating_sub(grab), height);
196 }
197 }
198
199 /// Returns `true` if we are in the process of dragging the scroll thumb.
200 pub fn is_dragging(&self) -> bool {
201 self.thumb_grab.is_some()
202 }
203
204 /// Stops grabbing the scrollbar.
205 pub fn release_grab(&mut self) {
206 self.thumb_grab = None;
207 }
208
209 /// Draws the scroll bar and the content using the given drawer.
210 ///
211 /// `line_drawer` will be called once for each line that needs to be drawn.
212 /// It will be given the absolute ID of the item to draw..
213 /// It will also be given a printer with the correct offset,
214 /// so it should only print on the first line.
215 ///
216 /// # Examples
217 ///
218 /// ```rust
219 /// # #[allow(deprecated)]
220 /// # fn with_printer(printer: &cursive_core::Printer) {
221 /// # let scrollbase = cursive_core::view::ScrollBase::new();
222 /// let lines = ["Line 1", "Line number 2"];
223 /// scrollbase.draw(printer, |printer, i| {
224 /// printer.print((0, 0), lines[i]);
225 /// });
226 /// # }
227 /// ```
228 pub fn draw<F>(&self, printer: &Printer, line_drawer: F)
229 where
230 F: Fn(&Printer, usize),
231 {
232 if self.view_height == 0 {
233 return;
234 }
235 // Print the content in a sub_printer
236 let max_y = min(self.view_height, self.content_height - self.start_line);
237 let w = if self.scrollable() {
238 // We have to remove the bar width and the padding.
239 printer.size.x.saturating_sub(1 + self.right_padding)
240 } else {
241 printer.size.x
242 };
243
244 for y in 0..max_y {
245 // Y is the actual coordinate of the line.
246 // The item ID is then Y + self.start_line
247 line_drawer(&printer.offset((0, y)).cropped((w, 1)), y + self.start_line);
248 }
249
250 // And draw the scrollbar if needed
251 if self.view_height < self.content_height {
252 // We directly compute the size of the scrollbar
253 // (that way we avoid using floats).
254 // (ratio) * max_height
255 // Where ratio is ({start or end} / content.height)
256 let height = self.scrollbar_thumb_height();
257 let start = self.scrollbar_thumb_y(height);
258
259 let style = if printer.focused {
260 Style::highlight()
261 } else {
262 Style::highlight_inactive()
263 };
264
265 let scrollbar_x = self.scrollbar_x(printer.size.x);
266 // eprintln!("Drawing bar at x={}", scrollbar_x);
267
268 // The background
269 printer.print_vline((scrollbar_x, 0), printer.size.y, "|");
270
271 // The scrollbar thumb
272 printer.with_style(style, |printer| {
273 printer.print_vline((scrollbar_x, start), height, "▒");
274 });
275 }
276 }
277
278 /// Returns the X position of the scrollbar, given the size available.
279 ///
280 /// Note that this does not depend whether or
281 /// not a scrollbar will actually be present.
282 pub fn scrollbar_x(&self, total_size: usize) -> usize {
283 total_size.saturating_sub(1 + self.scrollbar_offset)
284 }
285
286 /// Returns the height of the scrollbar thumb.
287 pub fn scrollbar_thumb_height(&self) -> usize {
288 max(1, self.view_height * self.view_height / self.content_height)
289 }
290
291 /// Returns the y position of the scrollbar thumb.
292 pub fn scrollbar_thumb_y(&self, scrollbar_thumb_height: usize) -> usize {
293 let steps = self.view_height - scrollbar_thumb_height + 1;
294 steps * self.start_line / (1 + self.content_height - self.view_height)
295 }
296}