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}