1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
use crate::div::div_up;
use crate::theme::ColorStyle;
use crate::vec::Vec2;
use crate::Printer;
use std::cmp::{max, min};

/// Provide scrolling functionalities to a view.
///
/// You're not supposed to use this directly,
/// but it can be helpful if you create your own Views.
#[derive(Default, Debug)]
pub struct ScrollBase {
    /// First line visible
    pub start_line: usize,

    /// Content height
    pub content_height: usize,

    /// Number of lines displayed
    pub view_height: usize,

    /// Padding for the scrollbar
    ///
    /// If present, the scrollbar will be shifted
    /// `scrollbar_offset` columns to the left.
    ///
    /// (Useful when each item includes its own side borders,
    /// to draw the scrollbar inside.)
    pub scrollbar_offset: usize,

    /// Blank between the text and the scrollbar.
    pub right_padding: usize,

    /// Initial position of the cursor when dragging.
    pub thumb_grab: Option<usize>,
}

impl ScrollBase {
    /// Creates a new, uninitialized scrollbar.
    pub fn new() -> Self {
        ScrollBase {
            start_line: 0,
            content_height: 0,
            view_height: 0,
            scrollbar_offset: 0,
            right_padding: 1,
            thumb_grab: None,
        }
    }

    /// Shifts the scrollbar toward the inside of the view.
    ///
    /// Used by views that draw their side borders in the children.
    /// Pushing the scrollbar to the left allows it to stay inside
    /// the borders.
    pub fn scrollbar_offset(mut self, offset: usize) -> Self {
        self.scrollbar_offset = offset;
        self
    }

    /// Sets the number of blank cells between the text and the scrollbar.
    ///
    /// Defaults to 1.
    pub fn right_padding(mut self, padding: usize) -> Self {
        self.right_padding = padding;
        self
    }

    /// Call this method whem the content or the view changes.
    pub fn set_heights(&mut self, view_height: usize, content_height: usize) {
        self.view_height = view_height;
        self.content_height = content_height;

        // eprintln!("Setting heights: {} in {}", content_height, view_height);

        if self.scrollable() {
            self.start_line =
                min(self.start_line, self.content_height - self.view_height);
        } else {
            self.start_line = 0;
        }
    }

    /// Returns `TRUE` if the view needs to scroll.
    pub fn scrollable(&self) -> bool {
        self.view_height < self.content_height
    }

    /// Returns `TRUE` unless we are at the top.
    pub fn can_scroll_up(&self) -> bool {
        self.start_line > 0
    }

    /// Returns `TRUE` unless we are at the bottom.
    pub fn can_scroll_down(&self) -> bool {
        self.start_line + self.view_height < self.content_height
    }

    /// Scroll to the top of the view.
    pub fn scroll_top(&mut self) {
        self.start_line = 0;
    }

    /// Makes sure that the given line is visible, scrolling if needed.
    pub fn scroll_to(&mut self, y: usize) {
        if y >= self.start_line + self.view_height {
            self.start_line = 1 + y - self.view_height;
        } else if y < self.start_line {
            self.start_line = y;
        }
    }

    /// Scroll to the bottom of the view.
    pub fn scroll_bottom(&mut self) {
        if self.scrollable() {
            self.start_line = self.content_height - self.view_height;
        }
    }

    /// Scroll down by the given number of line.
    ///
    /// Never further than the bottom of the view.
    pub fn scroll_down(&mut self, n: usize) {
        if self.scrollable() {
            self.start_line = min(
                self.start_line + n,
                self.content_height - self.view_height,
            );
        }
    }

    /// Scrolls down until the scrollbar thumb is at the given location.
    pub fn scroll_to_thumb(&mut self, thumb_y: usize, thumb_height: usize) {
        // The min() is there to stop at the bottom of the content.
        // The saturating_sub is there to stop at the bottom of the content.
        // eprintln!("Scrolling to {}", thumb_y);
        self.start_line = min(
            div_up(
                (1 + self.content_height - self.view_height) * thumb_y,
                self.view_height - thumb_height + 1,
            ),
            self.content_height - self.view_height,
        );
    }

    /// Scroll up by the given number of lines.
    ///
    /// Never above the top of the view.
    pub fn scroll_up(&mut self, n: usize) {
        if self.scrollable() {
            self.start_line -= min(self.start_line, n);
        }
    }

    /// Starts scrolling from the given cursor position.
    pub fn start_drag(&mut self, position: Vec2, width: usize) -> bool {
        // First: are we on the correct column?
        let scrollbar_x = self.scrollbar_x(width);
        // eprintln!("Grabbed {} for {}", position.x, scrollbar_x);
        if position.x != scrollbar_x {
            return false;
        }

        // Now, did we hit the thumb? Or should we direct-jump?
        let height = self.scrollbar_thumb_height();
        let thumb_y = self.scrollbar_thumb_y(height);

        if position.y >= thumb_y && position.y < thumb_y + height {
            // Grabbed!
            self.thumb_grab = Some(position.y - thumb_y);
        } else {
            // Just jump a bit...
            self.thumb_grab = Some((height - 1) / 2);
            // eprintln!("Grabbed at {}", self.thumb_grab);
            self.drag(position);
        }

        true
    }

    /// Keeps scrolling by dragging the cursor.
    pub fn drag(&mut self, position: Vec2) {
        // Our goal is self.scrollbar_thumb_y()+thumb_grab == position.y
        // Which means that position.y is the middle of the scrollbar.
        // eprintln!("Dragged: {:?}", position);
        // eprintln!("thumb: {:?}", self.thumb_grab);
        if let Some(grab) = self.thumb_grab {
            let height = self.scrollbar_thumb_height();
            self.scroll_to_thumb(position.y.saturating_sub(grab), height);
        }
    }

    /// Returns `true` if we are in the process of dragging the scroll thumb.
    pub fn is_dragging(&self) -> bool {
        self.thumb_grab.is_some()
    }

    /// Stops grabbing the scrollbar.
    pub fn release_grab(&mut self) {
        self.thumb_grab = None;
    }

    /// Draws the scroll bar and the content using the given drawer.
    ///
    /// `line_drawer` will be called once for each line that needs to be drawn.
    /// It will be given the absolute ID of the item to draw..
    /// It will also be given a printer with the correct offset,
    /// so it should only print on the first line.
    ///
    /// # Examples
    ///
    /// ```rust
    /// # use cursive::view::ScrollBase;
    /// # use cursive::Printer;
    /// # use cursive::theme;
    /// # use cursive::backend;
    /// # let scrollbase = ScrollBase::new();
    /// # let b = backend::dummy::Backend::init();
    /// # let t = theme::load_default();
    /// # let printer = Printer::new((5,1), &t, &*b);
    /// # let printer = &printer;
    /// let lines = ["Line 1", "Line number 2"];
    /// scrollbase.draw(printer, |printer, i| {
    ///     printer.print((0,0), lines[i]);
    /// });
    /// ```
    pub fn draw<F>(&self, printer: &Printer<'_, '_>, line_drawer: F)
    where
        F: Fn(&Printer<'_, '_>, usize),
    {
        if self.view_height == 0 {
            return;
        }
        // Print the content in a sub_printer
        let max_y =
            min(self.view_height, self.content_height - self.start_line);
        let w = if self.scrollable() {
            // We have to remove the bar width and the padding.
            printer.size.x.saturating_sub(1 + self.right_padding)
        } else {
            printer.size.x
        };

        for y in 0..max_y {
            // Y is the actual coordinate of the line.
            // The item ID is then Y + self.start_line
            line_drawer(
                &printer.offset((0, y)).cropped((w, 1)),
                y + self.start_line,
            );
        }

        // And draw the scrollbar if needed
        if self.view_height < self.content_height {
            // We directly compute the size of the scrollbar
            // (that way we avoid using floats).
            // (ratio) * max_height
            // Where ratio is ({start or end} / content.height)
            let height = self.scrollbar_thumb_height();
            let start = self.scrollbar_thumb_y(height);

            let color = if printer.focused {
                ColorStyle::highlight()
            } else {
                ColorStyle::highlight_inactive()
            };

            let scrollbar_x = self.scrollbar_x(printer.size.x);
            // eprintln!("Drawing bar at x={}", scrollbar_x);

            // The background
            printer.print_vline((scrollbar_x, 0), printer.size.y, "|");

            // The scrollbar thumb
            printer.with_color(color, |printer| {
                printer.print_vline((scrollbar_x, start), height, "▒");
            });
        }
    }

    /// Returns the X position of the scrollbar, given the size available.
    ///
    /// Note that this does not depend whether or
    /// not a scrollbar will actually be present.
    pub fn scrollbar_x(&self, total_size: usize) -> usize {
        total_size.saturating_sub(1 + self.scrollbar_offset)
    }

    /// Returns the height of the scrollbar thumb.
    pub fn scrollbar_thumb_height(&self) -> usize {
        max(1, self.view_height * self.view_height / self.content_height)
    }

    /// Returns the y position of the scrollbar thumb.
    pub fn scrollbar_thumb_y(&self, scrollbar_thumb_height: usize) -> usize {
        let steps = self.view_height - scrollbar_thumb_height + 1;
        steps * self.start_line / (1 + self.content_height - self.view_height)
    }
}