Add scrollbars to ScrollView

This commit is contained in:
Alexandre Bury 2018-06-15 16:41:20 -07:00
parent 8edc0e20c9
commit 0c318b7194
5 changed files with 172 additions and 15 deletions

View File

@ -28,6 +28,11 @@ pub enum Orientation {
}
impl Orientation {
/// Returns a `XY(Horizontal, Vertical)`.
pub fn pair() -> XY<Orientation> {
XY::new(Orientation::Horizontal, Orientation::Vertical)
}
/// Returns the component of `v` corresponding to this orientation.
///
/// (`Horizontal` will return the x value,

View File

@ -1,6 +1,7 @@
//! Provide higher-level abstraction to draw things on backends.
use backend::Backend;
use direction::Orientation;
use enumset::EnumSet;
use std::cmp::min;
use theme::{BorderStyle, ColorStyle, Effect, PaletteColor, Style, Theme};
@ -196,6 +197,16 @@ impl<'a> Printer<'a> {
}
}
/// Prints a line using the given character.
pub fn print_line<T: Into<Vec2>>(
&self, orientation: Orientation, start: T, length: usize, c: &str,
) {
match orientation {
Orientation::Vertical => self.print_vline(start, length, c),
Orientation::Horizontal => self.print_hline(start, length, c),
}
}
/// Prints a horizontal line using the given character.
pub fn print_hline<T: Into<Vec2>>(&self, start: T, width: usize, c: &str) {
let start = start.into();

View File

@ -240,6 +240,27 @@ impl Mul<usize> for XY<usize> {
}
}
impl<T> Mul<XY<T>> for XY<T>
where
T: Mul<T>,
{
type Output = XY<T::Output>;
fn mul(self, other: XY<T>) -> Self::Output {
self.zip_map(other, |s, o| s * o)
}
}
impl<T> Div<XY<T>> for XY<T>
where
T: Div<T>,
{
type Output = XY<T::Output>;
fn div(self, other: XY<T>) -> Self::Output {
self.zip_map(other, |s, o| s / o)
}
}
#[cfg(test)]
mod tests {
use super::Vec2;

View File

@ -1,6 +1,7 @@
use direction::Direction;
use direction::{Direction, Orientation};
use event::{AnyCb, Event, EventResult, Key, MouseEvent};
use rect::Rect;
use theme::ColorStyle;
use vec::Vec2;
use view::{Selector, View};
use xy::XY;
@ -11,19 +12,30 @@ use std::cmp::min;
/// Wraps a view in a scrollable area.
pub struct ScrollView<V> {
inner_size: Vec2,
// The wrapped view.
inner: V,
// This is the size the child thinks we're giving him.
inner_size: Vec2,
// Offset into the inner view.
//
// Our `(0,0)` will be inner's `offset`
offset: Vec2,
// What was our own size last time we checked.
//
// This includes scrollbars, if any.
last_size: Vec2,
// Can we scroll horizontally?
// Are we scrollable in each direction?
enabled: XY<bool>,
// Should we show scrollbars?
//
// Even if this is true, no scrollbar will be printed if we don't need to scroll.
//
// Could be an enum {Never, Auto, Always}
show_scrollbars: bool,
// How much padding should be between content and scrollbar?
@ -46,7 +58,7 @@ impl<V> ScrollView<V> {
/// Returns the viewport in the inner content.
pub fn content_viewport(&self) -> Rect {
Rect::from_size(self.offset, self.last_size)
Rect::from_size(self.offset, self.available_size())
}
/// Sets the scroll offset to the given value
@ -54,7 +66,7 @@ impl<V> ScrollView<V> {
where
S: Into<Vec2>,
{
let max_offset = self.inner_size.saturating_sub(self.last_size);
let max_offset = self.inner_size.saturating_sub(self.available_size());
self.offset = offset.into().or_min(max_offset);
}
@ -89,6 +101,24 @@ impl<V> ScrollView<V> {
pub fn scroll_x(self, enabled: bool) -> Self {
self.with(|s| s.set_scroll_x(enabled))
}
/// Returns for each axis if we are scrolling.
fn is_scrolling(&self) -> XY<bool> {
self.inner_size.zip_map(self.last_size, |i, s| i > s)
}
/// Returns the size taken by the scrollbars.
///
/// Will be zero in axis where we're not scrolling.
fn scrollbar_size(&self) -> Vec2 {
self.is_scrolling()
.select_or(self.scrollbar_padding + (1, 1), Vec2::zero())
}
/// Returns the size available for the child view.
fn available_size(&self) -> Vec2 {
self.last_size - self.scrollbar_size()
}
}
impl<V> ScrollView<V>
@ -109,6 +139,7 @@ where
let available = constraint.saturating_sub(scrollbar_size);
// This the ideal size for the child. May not be what he gets.
let inner_size = self.inner.required_size(available);
// Where we're "enabled", accept the constraints.
@ -118,6 +149,10 @@ where
inner_size + scrollbar_size,
);
// On non-scrolling axis, give inner_size the available space instead.
let inner_size =
self.enabled.select_or(inner_size, size - scrollbar_size);
let new_scrollable = inner_size.zip_map(size, |i, s| i > s);
(inner_size, size, new_scrollable)
@ -134,7 +169,7 @@ where
let (inner_size, size, scrollable) =
self.sizes_when_scrolling(constraint, XY::new(false, false));
// Did it work?
// If we need to add scrollbars, the available size will change.
if scrollable.any() && self.show_scrollbars {
// Attempt 2: he wants to scroll? Sure! Try again with some space for the scrollbar.
let (inner_size, size, new_scrollable) =
@ -158,6 +193,18 @@ where
(inner_size, size)
}
}
fn scrollbar_thumb_lengths(&self) -> Vec2 {
let available = self.available_size();
(available * available / self.inner_size).or_max((1, 1))
}
fn scrollbar_thumb_offsets(&self, lengths: Vec2) -> Vec2 {
let available = self.available_size();
// The number of steps is 1 + the "extra space"
let steps = available - lengths + (1, 1);
steps * self.offset / (self.inner_size + (1, 1) - available)
}
}
impl<V> View for ScrollView<V>
@ -165,13 +212,51 @@ where
V: View,
{
fn draw(&self, printer: &Printer) {
// Draw scrollbar?
let scrolling = self.is_scrolling();
let lengths = self.scrollbar_thumb_lengths();
let offsets = self.scrollbar_thumb_offsets(lengths);
let line_c = XY::new("-", "|");
let color = if printer.focused {
ColorStyle::highlight()
} else {
ColorStyle::highlight_inactive()
};
let size = self.available_size();
// TODO: use a more generic zip_all or something?
XY::zip5(lengths, offsets, size, line_c, Orientation::pair()).run_if(
scrolling,
|(length, offset, size, c, orientation)| {
let start = (printer.size - (1, 1)).with_axis(orientation, 0);
let offset = orientation.make_vec(offset, 0);
printer.print_line(orientation, start, size, c);
printer.with_color(color, |printer| {
printer.print_line(
orientation,
start + offset,
length,
"",
);
});
},
);
if scrolling.both() {
printer.print((printer.size.x - 1, printer.size.y - 1), "");
}
// Draw content
let printer = printer
.cropped(size)
.content_offset(self.offset)
.inner_size(self.inner_size);
self.inner.draw(&printer);
// Draw scrollbar?
}
fn on_event(&mut self, event: Event) -> EventResult {
@ -199,11 +284,13 @@ where
event: MouseEvent::WheelDown,
..
} if self.enabled.y
&& (self.offset.y + self.last_size.y
&& (self.offset.y + self.available_size().y
< self.inner_size.y) =>
{
self.offset.y = min(
self.inner_size.y.saturating_sub(self.last_size.y),
self.inner_size
.y
.saturating_sub(self.available_size().y),
self.offset.y + 3,
);
EventResult::Consumed(None)
@ -216,7 +303,7 @@ where
}
Event::Ctrl(Key::Down) | Event::Key(Key::Down)
if self.enabled.y
&& (self.offset.y + self.last_size.y
&& (self.offset.y + self.available_size().y
< self.inner_size.y) =>
{
self.offset.y += 1;
@ -230,7 +317,7 @@ where
}
Event::Ctrl(Key::Right) | Event::Key(Key::Right)
if self.enabled.x
&& (self.offset.x + self.last_size.x
&& (self.offset.x + self.available_size().x
< self.inner_size.x) =>
{
self.offset.x += 1;
@ -245,7 +332,7 @@ where
// The furthest top-left we can go
let top_left = (important.bottom_right() + (1, 1))
.saturating_sub(self.last_size);
.saturating_sub(self.available_size());
// The furthest bottom-right we can go
let bottom_right = important.top_left();
@ -296,8 +383,7 @@ where
}
fn take_focus(&mut self, source: Direction) -> bool {
let is_scrollable =
self.enabled.any() && (self.inner_size != self.last_size);
let is_scrollable = self.is_scrolling().any();
self.inner.take_focus(source) || is_scrollable
}
}

View File

@ -16,6 +16,11 @@ impl<T> XY<T> {
XY { x, y }
}
/// Swaps the x and y values.
pub fn swap(self) -> Self {
XY::new(self.y, self.x)
}
/// Returns `f(self.x, self.y)`
pub fn fold<U, F>(self, f: F) -> U
where
@ -42,6 +47,16 @@ impl<T> XY<T> {
self.zip_map(condition, |v, c| if c { f(v) } else { v })
}
/// Applies `f` on axis where `condition` is true.
///
/// Returns `None` otherwise.
pub fn run_if<F, U>(self, condition: XY<bool>, f: F) -> XY<Option<U>>
where
F: Fn(T) -> U,
{
self.zip_map(condition, |v, c| if c { Some(f(v)) } else { None })
}
/// Creates a new `XY` by applying `f` to `x`, and carrying `y` over.
pub fn map_x<F>(self, f: F) -> Self
where
@ -94,6 +109,25 @@ impl<T> XY<T> {
XY::new((self.x, other.x), (self.y, other.y))
}
/// Returns a new `XY` of tuples made by zipping `self`, `a` and `b`.
pub fn zip3<U, V>(self, a: XY<U>, b: XY<V>) -> XY<(T, U, V)> {
XY::new((self.x, a.x, b.x), (self.y, a.y, b.y))
}
/// Returns a new `XY` of tuples made by zipping `self`, `a`, `b` and `c`.
pub fn zip4<U, V, W>(
self, a: XY<U>, b: XY<V>, c: XY<W>,
) -> XY<(T, U, V, W)> {
XY::new((self.x, a.x, b.x, c.x), (self.y, a.y, b.y, c.y))
}
/// Returns a new `XY` of tuples made by zipping `self`, `a`, `b`, `c` and `d`.
pub fn zip5<U, V, W, Z>(
self, a: XY<U>, b: XY<V>, c: XY<W>, d: XY<Z>,
) -> XY<(T, U, V, W, Z)> {
XY::new((self.x, a.x, b.x, c.x, d.x), (self.y, a.y, b.y, c.y, d.y))
}
/// Returns a new `XY` by calling `f` on `self` and `other` for each axis.
pub fn zip_map<U, V, F>(self, other: XY<U>, f: F) -> XY<V>
where