mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-24 01:46:31 +00:00
Add mouse scroll support to TextView
This commit is contained in:
parent
a6fb0e71cd
commit
5931ab17c8
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
Reference in New Issue
Block a user