Add mouse scroll support to TextView

This commit is contained in:
Alexandre Bury 2017-10-11 18:06:58 -07:00
parent a6fb0e71cd
commit 5931ab17c8
2 changed files with 144 additions and 34 deletions

View File

@ -1,6 +1,5 @@
use Printer; use Printer;
use std::cmp::{max, min}; use std::cmp::{max, min};
use theme::ColorStyle; use theme::ColorStyle;
use vec::Vec2; use vec::Vec2;
@ -12,10 +11,13 @@ use vec::Vec2;
pub struct ScrollBase { pub struct ScrollBase {
/// First line visible /// First line visible
pub start_line: usize, pub start_line: usize,
/// Content height /// Content height
pub content_height: usize, pub content_height: usize,
/// Number of lines displayed /// Number of lines displayed
pub view_height: usize, pub view_height: usize,
/// Padding for the scrollbar /// Padding for the scrollbar
/// ///
/// If present, the scrollbar will be shifted /// If present, the scrollbar will be shifted
@ -27,6 +29,9 @@ pub struct ScrollBase {
/// Blank between the text and the scrollbar. /// Blank between the text and the scrollbar.
pub right_padding: usize, pub right_padding: usize,
/// Initial position of the cursor when dragging.
pub thumb_grab: usize,
} }
/// Defines the scrolling behaviour on content or size change /// Defines the scrolling behaviour on content or size change
@ -54,6 +59,7 @@ impl ScrollBase {
view_height: 0, view_height: 0,
scrollbar_offset: 0, scrollbar_offset: 0,
right_padding: 1, right_padding: 1,
thumb_grab: 0,
} }
} }
@ -81,8 +87,8 @@ impl ScrollBase {
self.content_height = content_height; self.content_height = content_height;
if self.scrollable() { if self.scrollable() {
self.start_line = min(self.start_line, self.start_line =
self.content_height - self.view_height); min(self.start_line, self.content_height - self.view_height);
} else { } else {
self.start_line = 0; self.start_line = 0;
} }
@ -129,11 +135,24 @@ impl ScrollBase {
/// Never further than the bottom of the view. /// Never further than the bottom of the view.
pub fn scroll_down(&mut self, n: usize) { pub fn scroll_down(&mut self, n: usize) {
if self.scrollable() { if self.scrollable() {
self.start_line = min(self.start_line + n, self.start_line = min(
self.content_height - self.view_height); 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.
self.start_line = min(
(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. /// Scroll up by the given number of lines.
/// ///
/// Never above the top of the view. /// Never above the top of the view.
@ -143,6 +162,42 @@ impl ScrollBase {
} }
} }
/// 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?
if position.x != self.scrollbar_x(width) {
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 = position.y - thumb_y;
} else {
// Just jump a bit...
self.thumb_grab = height / 2;
}
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.
let height = self.scrollbar_thumb_height();
let grab = self.thumb_grab;
self.scroll_to_thumb(position.y.saturating_sub(grab), height);
}
/// Draws the scroll bar and the content using the given drawer. /// 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. /// `line_drawer` will be called once for each line that needs to be drawn.
@ -168,14 +223,15 @@ impl ScrollBase {
/// }); /// });
/// ``` /// ```
pub fn draw<F>(&self, printer: &Printer, line_drawer: F) pub fn draw<F>(&self, printer: &Printer, line_drawer: F)
where F: Fn(&Printer, usize) where
F: Fn(&Printer, usize),
{ {
if self.view_height == 0 { if self.view_height == 0 {
return; return;
} }
// Print the content in a sub_printer // Print the content in a sub_printer
let max_y = min(self.view_height, let max_y =
self.content_height - self.start_line); min(self.view_height, self.content_height - self.start_line);
let w = if self.scrollable() { let w = if self.scrollable() {
// We have to remove the bar width and the padding. // We have to remove the bar width and the padding.
printer.size.x.saturating_sub(1 + self.right_padding) printer.size.x.saturating_sub(1 + self.right_padding)
@ -186,10 +242,10 @@ impl ScrollBase {
for y in 0..max_y { for y in 0..max_y {
// Y is the actual coordinate of the line. // Y is the actual coordinate of the line.
// The item ID is then Y + self.start_line // The item ID is then Y + self.start_line
line_drawer(&printer.sub_printer(Vec2::new(0, y), line_drawer(
Vec2::new(w, 1), &printer.sub_printer(Vec2::new(0, y), Vec2::new(w, 1), true),
true), y + self.start_line,
y + self.start_line); );
} }
@ -199,15 +255,8 @@ impl ScrollBase {
// (that way we avoid using floats). // (that way we avoid using floats).
// (ratio) * max_height // (ratio) * max_height
// Where ratio is ({start or end} / content.height) // Where ratio is ({start or end} / content.height)
let height = max(1, let height = self.scrollbar_thumb_height();
self.view_height * self.view_height / let start = self.scrollbar_thumb_y(height);
self.content_height);
// Number of different possible positions
let steps = self.view_height - height + 1;
// Now
let start = steps * self.start_line /
(1 + self.content_height - self.view_height);
let color = if printer.focused { let color = if printer.focused {
ColorStyle::Highlight ColorStyle::Highlight
@ -215,12 +264,34 @@ impl ScrollBase {
ColorStyle::HighlightInactive ColorStyle::HighlightInactive
}; };
// TODO: use 1 instead of 2 let scrollbar_x = self.scrollbar_x(printer.size.x);
let scrollbar_x = printer.size.x.saturating_sub(1 + self.scrollbar_offset);
// The background
printer.print_vline((scrollbar_x, 0), printer.size.y, "|"); printer.print_vline((scrollbar_x, 0), printer.size.y, "|");
// The scrollbar thumb
printer.with_color(color, |printer| { printer.with_color(color, |printer| {
printer.print_vline((scrollbar_x, start), height, ""); 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)
}
} }

View File

@ -4,12 +4,10 @@ use XY;
use align::*; use align::*;
use direction::Direction; use direction::Direction;
use event::*; use event::*;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use utils::{LinesIterator, Row}; use utils::{LinesIterator, Row};
use vec::Vec2; use vec::Vec2;
use view::{SizeCache, View, ScrollBase, ScrollStrategy}; use view::{ScrollBase, ScrollStrategy, SizeCache, View};
/// A simple view showing a fixed text /// A simple view showing a fixed text
pub struct TextView { pub struct TextView {
@ -181,8 +179,8 @@ impl TextView {
// First attempt: naively hope that we won't need a scrollbar_width // First attempt: naively hope that we won't need a scrollbar_width
// (This means we try to use the entire available width for text). // (This means we try to use the entire available width for text).
self.rows = LinesIterator::new(strip_last_newline(&self.content), self.rows =
size.x) LinesIterator::new(strip_last_newline(&self.content), size.x)
.collect(); .collect();
// Width taken by the scrollbar. Without a scrollbar, it's 0. // Width taken by the scrollbar. Without a scrollbar, it's 0.
@ -241,11 +239,10 @@ impl TextView {
impl View for TextView { impl View for TextView {
fn draw(&self, printer: &Printer) { fn draw(&self, printer: &Printer) {
let h = self.rows.len(); let h = self.rows.len();
// If the content is smaller than the view, align it somewhere.
let offset = self.align.v.get_offset(h, printer.size.y); let offset = self.align.v.get_offset(h, printer.size.y);
let printer = let printer = &printer.offset((0, offset), true);
&printer.sub_printer(Vec2::new(0, offset), printer.size, true);
self.scrollbase.draw(printer, |printer, i| { self.scrollbase.draw(printer, |printer, i| {
let row = &self.rows[i]; let row = &self.rows[i];
@ -261,14 +258,56 @@ impl View for TextView {
return EventResult::Ignored; return EventResult::Ignored;
} }
// We know we are scrollable, otherwise the event would just be ignored.
match event { match event {
Event::Key(Key::Home) => self.scrollbase.scroll_top(), Event::Key(Key::Home) => self.scrollbase.scroll_top(),
Event::Key(Key::End) => self.scrollbase.scroll_bottom(), Event::Key(Key::End) => self.scrollbase.scroll_bottom(),
Event::Key(Key::Up) if self.scrollbase.can_scroll_up() => { Event::Key(Key::Up) if self.scrollbase.can_scroll_up() => {
self.scrollbase.scroll_up(1) self.scrollbase.scroll_up(1)
} }
Event::Key(Key::Down) if self.scrollbase Event::Key(Key::Down) if self.scrollbase.can_scroll_down() => {
.can_scroll_down() => self.scrollbase.scroll_down(1), self.scrollbase.scroll_down(1)
}
Event::Mouse {
event: MouseEvent::WheelDown,
position: _,
offset: _,
} if self.scrollbase.can_scroll_down() =>
{
self.scrollbase.scroll_down(5)
}
Event::Mouse {
event: MouseEvent::WheelUp,
position: _,
offset: _,
} if self.scrollbase.can_scroll_up() =>
{
self.scrollbase.scroll_up(5)
}
Event::Mouse {
event: MouseEvent::Press(MouseButton::Left),
position,
offset,
} if position
.checked_sub(offset)
.and_then(|position| {
self.width.map(
|width| self.scrollbase.start_drag(position, width),
)
})
.unwrap_or(false) =>
{
// Start scroll drag at the given position
}
Event::Mouse {
event: MouseEvent::Hold(MouseButton::Left),
position,
offset,
} => {
position
.checked_sub(offset)
.map(|position| self.scrollbase.drag(position));
}
Event::Key(Key::PageDown) => self.scrollbase.scroll_down(10), Event::Key(Key::PageDown) => self.scrollbase.scroll_down(10),
Event::Key(Key::PageUp) => self.scrollbase.scroll_up(10), Event::Key(Key::PageUp) => self.scrollbase.scroll_up(10),
_ => return EventResult::Ignored, _ => return EventResult::Ignored,