Move logic from ScrollView to ScrollCore

This commit is contained in:
Alexandre Bury 2019-03-04 16:51:22 -08:00
parent ef4448efe2
commit 6f28569dae
7 changed files with 1137 additions and 817 deletions

View File

@ -224,7 +224,7 @@ impl Cursive {
/// Show the debug console. /// Show the debug console.
/// ///
/// Currently, this will show logs if [`::logger::init()`] was called. /// Currently, this will show logs if [`logger::init()`](crate::logger::init()) was called.
pub fn show_debug_console(&mut self) { pub fn show_debug_console(&mut self) {
self.add_layer( self.add_layer(
views::Dialog::around(views::ScrollView::new(views::IdView::new( views::Dialog::around(views::ScrollView::new(views::IdView::new(

View File

@ -52,7 +52,8 @@ impl log::Log for CursiveLogger {
/// ///
/// Make sure this is the only logger your are using. /// Make sure this is the only logger your are using.
/// ///
/// Use a [`::views::DebugView`] to see the logs, or use [`::Cursive::toggle_debug_console()`]. /// Use a [`DebugView`](crate::views::DebugView) to see the logs, or use
/// [`Cursive::toggle_debug_console()`](crate::Cursive::toggle_debug_console()).
pub fn init() { pub fn init() {
// TODO: Configure the deque size? // TODO: Configure the deque size?
LOGS.lock().unwrap().reserve(1_000); LOGS.lock().unwrap().reserve(1_000);

View File

@ -51,7 +51,8 @@ mod view_trait;
// Helper bases // Helper bases
mod boxable; mod boxable;
mod identifiable; mod identifiable;
mod scroll; pub mod scroll;
mod scroll_base;
mod scrollable; mod scrollable;
mod into_boxed_view; mod into_boxed_view;
@ -63,7 +64,7 @@ pub use self::identifiable::Identifiable;
pub use self::into_boxed_view::IntoBoxedView; pub use self::into_boxed_view::IntoBoxedView;
pub use self::margins::Margins; pub use self::margins::Margins;
pub use self::position::{Offset, Position}; pub use self::position::{Offset, Position};
pub use self::scroll::{ScrollBase, ScrollStrategy}; pub use self::scroll_base::{ScrollBase, ScrollStrategy};
pub use self::scrollable::Scrollable; pub use self::scrollable::Scrollable;
pub use self::size_cache::SizeCache; pub use self::size_cache::SizeCache;
pub use self::size_constraint::SizeConstraint; pub use self::size_constraint::SizeConstraint;

File diff suppressed because it is too large Load Diff

316
src/view/scroll_base.rs Normal file
View File

@ -0,0 +1,316 @@
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>,
}
/// Defines the scrolling behaviour on content or size change
#[derive(Debug)]
pub enum ScrollStrategy {
/// Keeps the same row number
KeepRow,
/// Sticks to the top.
StickToTop,
/// Sticks to the bottom of the view.
StickToBottom,
}
impl Default for ScrollStrategy {
fn default() -> Self {
ScrollStrategy::KeepRow
}
}
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)
}
}

View File

@ -3,7 +3,7 @@ use crate::views::ScrollView;
/// Makes a view wrappable in a [`ScrollView`]. /// Makes a view wrappable in a [`ScrollView`].
/// ///
/// [`ScrollView`]: ::views::ScrollView /// [`ScrollView`]: crate::views::ScrollView
pub trait Scrollable: View + Sized { pub trait Scrollable: View + Sized {
/// Wraps `self` in a `ScrollView`. /// Wraps `self` in a `ScrollView`.
fn scrollable(self) -> ScrollView<Self> { fn scrollable(self) -> ScrollView<Self> {

View File

@ -1,55 +1,15 @@
use std::cmp::min; use crate::direction::Direction;
use crate::event::{AnyCb, Event, EventResult};
use crate::direction::{Direction, Orientation};
use crate::event::{AnyCb, Event, EventResult, Key, MouseButton, MouseEvent};
use crate::rect::Rect; use crate::rect::Rect;
use crate::theme::ColorStyle; use crate::view::{scroll, ScrollStrategy, Selector, View};
use crate::view::{ScrollStrategy, Selector, SizeCache, View}; use crate::{Printer, Vec2, With};
use crate::{Printer, Vec2, With, XY};
/// Wraps a view in a scrollable area. /// Wraps a view in a scrollable area.
pub struct ScrollView<V> { pub struct ScrollView<V> {
/// The wrapped view. /// The wrapped view.
inner: V, inner: V,
/// This is the size the child thinks we're giving him. core: scroll::ScrollCore,
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,
/// 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.
///
/// TODO: have an option to always show the scrollbar.
/// TODO: have an option to show scrollbar on top/left.
show_scrollbars: bool,
/// How much padding should be between content and scrollbar?
///
/// scrollbar_padding.x is the horizontal padding before the vertical scrollbar.
scrollbar_padding: Vec2,
/// Initial position of the cursor when dragging.
thumb_grab: Option<(Orientation, usize)>,
/// We keep the cache here so it can be busted when we change the content.
size_cache: Option<XY<SizeCache>>,
/// Defines how to update the offset when the view size changes.
scroll_strategy: ScrollStrategy,
} }
impl<V> ScrollView<V> impl<V> ScrollView<V>
@ -60,21 +20,13 @@ where
pub fn new(inner: V) -> Self { pub fn new(inner: V) -> Self {
ScrollView { ScrollView {
inner, inner,
inner_size: Vec2::zero(), core: scroll::ScrollCore::new(),
offset: Vec2::zero(),
last_size: Vec2::zero(),
enabled: XY::new(false, true),
show_scrollbars: true,
scrollbar_padding: Vec2::new(1, 0),
thumb_grab: None,
size_cache: None,
scroll_strategy: ScrollStrategy::KeepRow,
} }
} }
/// Returns the viewport in the inner content. /// Returns the viewport in the inner content.
pub fn content_viewport(&self) -> Rect { pub fn content_viewport(&self) -> Rect {
Rect::from_size(self.offset, self.available_size()) self.core.content_viewport()
} }
/// Defines the way scrolling is adjusted on content or size change. /// Defines the way scrolling is adjusted on content or size change.
@ -85,8 +37,7 @@ where
/// It is reset to `ScrollStrategy::KeepRow` whenever the user scrolls /// It is reset to `ScrollStrategy::KeepRow` whenever the user scrolls
/// manually. /// manually.
pub fn set_scroll_strategy(&mut self, strategy: ScrollStrategy) { pub fn set_scroll_strategy(&mut self, strategy: ScrollStrategy) {
self.scroll_strategy = strategy; self.core.set_scroll_strategy(strategy);
self.adjust_scroll();
} }
/// Defines the way scrolling is adjusted on content or size change. /// Defines the way scrolling is adjusted on content or size change.
@ -100,7 +51,7 @@ where
/// ///
/// Defaults to `true`. /// Defaults to `true`.
pub fn set_show_scrollbars(&mut self, show_scrollbars: bool) { pub fn set_show_scrollbars(&mut self, show_scrollbars: bool) {
self.show_scrollbars = show_scrollbars; self.core.set_show_scrollbars(show_scrollbars);
} }
/// Control whether scroll bars are visibile. /// Control whether scroll bars are visibile.
@ -115,24 +66,21 @@ where
where where
S: Into<Vec2>, S: Into<Vec2>,
{ {
let max_offset = self.inner_size.saturating_sub(self.available_size()); self.core.set_offset(offset);
self.offset = offset.into().or_min(max_offset);
} }
/// Controls whether this view can scroll vertically. /// Controls whether this view can scroll vertically.
/// ///
/// Defaults to `true`. /// Defaults to `true`.
pub fn set_scroll_y(&mut self, enabled: bool) { pub fn set_scroll_y(&mut self, enabled: bool) {
self.enabled.y = enabled; self.core.set_scroll_y(enabled);
self.invalidate_cache();
} }
/// Controls whether this view can scroll horizontally. /// Controls whether this view can scroll horizontally.
/// ///
/// Defaults to `false`. /// Defaults to `false`.
pub fn set_scroll_x(&mut self, enabled: bool) { pub fn set_scroll_x(&mut self, enabled: bool) {
self.enabled.x = enabled; self.core.set_scroll_x(enabled);
self.invalidate_cache();
} }
/// Controls whether this view can scroll vertically. /// Controls whether this view can scroll vertically.
@ -155,272 +103,22 @@ where
/// Programmatically scroll to the top of the view. /// Programmatically scroll to the top of the view.
pub fn scroll_to_top(&mut self) { pub fn scroll_to_top(&mut self) {
let curr_x = self.offset.x; self.core.scroll_to_top();
self.set_offset((curr_x, 0));
} }
/// Programmatically scroll to the bottom of the view. /// Programmatically scroll to the bottom of the view.
pub fn scroll_to_bottom(&mut self) { pub fn scroll_to_bottom(&mut self) {
let max_y = self.inner_size.saturating_sub(self.available_size()).y; self.core.scroll_to_bottom();
let curr_x = self.offset.x;
self.set_offset((curr_x, max_y));
} }
/// Programmatically scroll to the leftmost side of the view. /// Programmatically scroll to the leftmost side of the view.
pub fn scroll_to_left(&mut self) { pub fn scroll_to_left(&mut self) {
let curr_y = self.offset.y; self.core.scroll_to_left();
self.set_offset((0, curr_y));
} }
/// Programmatically scroll to the rightmost side of the view. /// Programmatically scroll to the rightmost side of the view.
pub fn scroll_to_right(&mut self) { pub fn scroll_to_right(&mut self) {
let max_x = self.inner_size.saturating_sub(self.available_size()).x; self.core.scroll_to_right();
let curr_y = self.offset.y;
self.set_offset((max_x, curr_y));
}
/// Clears the cache.
fn invalidate_cache(&mut self) {
self.size_cache = None;
}
/// 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)
}
/// Stops grabbing the scrollbar.
fn release_grab(&mut self) {
self.thumb_grab = None;
}
/// Returns the size taken by the scrollbars.
///
/// Will be zero in axis where we're not scrolling.
///
/// The scrollbar_size().x will be the horizontal space taken by the vertical scrollbar.
fn scrollbar_size(&self) -> Vec2 {
self.is_scrolling()
.swap()
.select_or(self.scrollbar_padding + (1, 1), Vec2::zero())
}
/// Returns the size available for the child view.
fn available_size(&self) -> Vec2 {
if self.show_scrollbars {
self.last_size.saturating_sub(self.scrollbar_size())
} else {
self.last_size
}
}
/// Compute the size we would need.
///
/// Given the constraints, and the axis that need scrollbars.
///
/// Returns `(inner_size, size, scrollable)`.
fn sizes_when_scrolling(
&mut self, constraint: Vec2, scrollable: XY<bool>, strict: bool,
) -> (Vec2, Vec2, XY<bool>) {
// This is the size taken by the scrollbars.
let scrollbar_size = scrollable
.swap()
.select_or(self.scrollbar_padding + (1, 1), Vec2::zero());
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.
// Where we're not, just forward inner_size.
let size = self.enabled.select_or(
Vec2::min(inner_size + scrollbar_size, constraint),
inner_size + scrollbar_size,
);
// In strict mode, there's no way our size is over constraints.
let size = if strict {
size.or_min(constraint)
} else {
size
};
// On non-scrolling axis, give inner_size the available space instead.
let inner_size = self
.enabled
.select_or(inner_size, size.saturating_sub(scrollbar_size));
let new_scrollable = inner_size.zip_map(size, |i, s| i > s);
(inner_size, size, new_scrollable)
}
/// Starts scrolling from the cursor position.
///
/// Returns `true` if the event was consumed.
fn start_drag(&mut self, position: Vec2) -> bool {
// For each scrollbar, how far it is.
let scrollbar_pos = self.last_size.saturating_sub((1, 1));
let lengths = self.scrollbar_thumb_lengths();
let offsets = self.scrollbar_thumb_offsets(lengths);
let available = self.available_size();
// This is true for Y if we grabbed the vertical scrollbar
// More specifically, we need both (for instance for the vertical bar):
// * To be in the right column: X == scrollbar_pos
// * To be in the right range: Y < available
let grabbed = position
.zip_map(scrollbar_pos, |p, s| p == s)
.swap()
.and(position.zip_map(available, |p, a| p < a));
// Iterate on axises, and keep the one we grabbed.
if let Some((orientation, pos, length, offset)) =
XY::zip4(Orientation::pair(), position, lengths, offsets)
.keep(grabbed.and(self.enabled))
.into_iter()
.filter_map(|x| x)
.next()
{
if pos >= offset && pos < offset + length {
// We grabbed the thumb! Now scroll from that position.
self.thumb_grab = Some((orientation, pos - offset));
} else {
// We hit the scrollbar, outside of the thumb.
// Let's move the middle there.
self.thumb_grab = Some((orientation, (length - 1) / 2));
self.drag(position);
}
return true;
}
false
}
/// Called when a mouse drag is detected.
fn drag(&mut self, position: Vec2) {
// Only do something if we grabbed something before.
if let Some((orientation, grab)) = self.thumb_grab {
self.scroll_to_thumb(
orientation,
position.get(orientation).saturating_sub(grab),
);
}
}
fn scroll_to_thumb(&mut self, orientation: Orientation, thumb_pos: usize) {
let lengths = self.scrollbar_thumb_lengths();
let available = self.available_size();
// We want self.scrollbar_thumb_offsets() to be thumb_pos
// steps * self.o / (self.inner + 1 - available) = thumb_pos
// self.o = thumb_pos * (self.inner + 1 - available) / (available + 1 - lengths)
// The new offset is:
// thumb_pos * (content + 1 - available) / (available + 1 - thumb size)
let extra =
(available + (1, 1)).saturating_sub(lengths).or_max((1, 1));
// We're dividing by this value, so make sure it's positive!
assert!(extra > Vec2::zero());
let new_offset =
((self.inner_size + (1, 1)).saturating_sub(available) * thumb_pos)
.div_up(extra);
let max_offset = self.inner_size.saturating_sub(self.available_size());
self.offset
.set_axis_from(orientation, &new_offset.or_min(max_offset));
}
/// Computes the size we would need given the constraints.
///
/// First be optimistic and try without scrollbars.
/// Then try with scrollbars if needed.
/// Then try again in case we now need to scroll both ways (!!!)
///
/// Returns `(inner_size, desired_size)`
fn sizes(&mut self, constraint: Vec2, strict: bool) -> (Vec2, Vec2) {
// First: try the cache
let valid_cache = !self.inner.needs_relayout()
&& self
.size_cache
.map(|cache| {
cache.zip_map(constraint, SizeCache::accept).both()
})
.unwrap_or(false);
if valid_cache {
// eprintln!("Cache: {:?}; constraint: {:?}", self.size_cache, constraint);
// The new constraint shouldn't change much,
// so we can re-use previous values
return (
self.inner_size,
self.size_cache.unwrap().map(|c| c.value),
);
}
// Attempt 1: try without scrollbars
let (inner_size, size, scrollable) = self.sizes_when_scrolling(
constraint,
XY::new(false, false),
strict,
);
// 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) =
self.sizes_when_scrolling(constraint, scrollable, strict);
if scrollable == new_scrollable {
// Yup, scrolling did it. We're good to go now.
(inner_size, size)
} else {
// Again? We're now scrolling in a new direction?
// There is no end to this!
let (inner_size, size, _) = self.sizes_when_scrolling(
constraint,
new_scrollable,
strict,
);
// That's enough. If the inner view changed again, ignore it!
// That'll teach it.
(inner_size, size)
}
} else {
// We're not showing any scrollbar, either because we don't scroll
// or because scrollbars are hidden.
(inner_size, size)
}
}
fn scrollbar_thumb_lengths(&self) -> Vec2 {
let available = self.available_size();
// The length should be (visible / total) * visible
(available * available / self.inner_size.or_max((1, 1))).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 + (1, 1)).saturating_sub(lengths);
let max_offset = self.inner_size.saturating_sub(available) + (1, 1);
steps * self.offset / max_offset
}
/// Apply the scrolling strategy to the current scroll position.
fn adjust_scroll(&mut self) {
match self.scroll_strategy {
ScrollStrategy::StickToTop => self.scroll_to_top(),
ScrollStrategy::StickToBottom => self.scroll_to_bottom(),
ScrollStrategy::KeepRow => (),
}
} }
/// Returns the wrapped view. /// Returns the wrapped view.
@ -436,259 +134,23 @@ where
V: View, V: View,
{ {
fn draw(&self, printer: &Printer<'_, '_>) { fn draw(&self, printer: &Printer<'_, '_>) {
// Draw scrollbar? self.core.draw(printer, &self.inner);
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();
// Draw the scrollbars
XY::zip5(lengths, offsets, size, line_c, Orientation::pair()).run_if(
scrolling,
|(length, offset, size, c, orientation)| {
let start = printer
.size
.saturating_sub((1, 1))
.with_axis(orientation, 0);
let offset = orientation.make_vec(offset, 0);
printer.print_line(orientation, start, size, c);
let thumb_c = if self
.thumb_grab
.map(|(o, _)| o == orientation)
.unwrap_or(false)
{
" "
} else {
""
};
printer.with_color(color, |printer| {
printer.print_line(
orientation,
start + offset,
length,
thumb_c,
);
});
},
);
// Draw the X between the two scrollbars.
if scrolling.both() {
printer.print(printer.size.saturating_sub((1, 1)), "");
}
// Draw content
let printer = printer
.cropped(size)
.content_offset(self.offset)
.inner_size(self.inner_size);
self.inner.draw(&printer);
} }
fn on_event(&mut self, event: Event) -> EventResult { fn on_event(&mut self, event: Event) -> EventResult {
// Relativize event accorging to the offset self.core.on_event(event, &mut self.inner)
let mut relative_event = event.clone();
// Should the event be treated inside, by the inner view?
let inside = if let Event::Mouse {
ref mut position,
ref offset,
..
} = relative_event
{
// For mouse events, check if it falls inside the available area
let inside = position
.checked_sub(offset)
.map(|p| p.fits_in(self.available_size()))
.unwrap_or(false);
*position = *position + self.offset;
inside
} else {
// For key events, assume it's inside by default.
true
};
let result = if inside {
// If the event is inside, give it to the child.
self.inner.on_event(relative_event)
} else {
// Otherwise, pretend it wasn't there.
EventResult::Ignored
};
match result {
EventResult::Ignored => {
// If it's an arrow, try to scroll in the given direction.
// If it's a mouse scroll, try to scroll as well.
// Also allow Ctrl+arrow to move the view,
// but not the selection.
match event {
Event::Mouse {
event: MouseEvent::WheelUp,
..
} if self.enabled.y && self.offset.y > 0 => {
self.offset.y = self.offset.y.saturating_sub(3);
}
Event::Mouse {
event: MouseEvent::WheelDown,
..
} if self.enabled.y
&& (self.offset.y + self.available_size().y
< self.inner_size.y) =>
{
self.offset.y = min(
self.inner_size
.y
.saturating_sub(self.available_size().y),
self.offset.y + 3,
);
}
Event::Mouse {
event: MouseEvent::Press(MouseButton::Left),
position,
offset,
} if self.show_scrollbars
&& position
.checked_sub(offset)
.map(|position| self.start_drag(position))
.unwrap_or(false) =>
{
// Just consume the event.
}
Event::Mouse {
event: MouseEvent::Hold(MouseButton::Left),
position,
offset,
} if self.show_scrollbars => {
let position = position.saturating_sub(offset);
self.drag(position);
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
..
} => {
self.release_grab();
}
Event::Key(Key::Home) if self.enabled.any() => {
self.offset =
self.enabled.select_or(Vec2::zero(), self.offset);
}
Event::Key(Key::End) if self.enabled.any() => {
let max_offset = self
.inner_size
.saturating_sub(self.available_size());
self.offset =
self.enabled.select_or(max_offset, self.offset);
}
Event::Ctrl(Key::Up) | Event::Key(Key::Up)
if self.enabled.y && self.offset.y > 0 =>
{
self.offset.y -= 1;
}
Event::Key(Key::PageUp)
if self.enabled.y && self.offset.y > 0 =>
{
self.offset.y = self.offset.y.saturating_sub(5);
}
Event::Key(Key::PageDown)
if self.enabled.y
&& (self.offset.y + self.available_size().y
< self.inner_size.y) =>
{
self.offset.y += 5;
}
Event::Ctrl(Key::Down) | Event::Key(Key::Down)
if self.enabled.y
&& (self.offset.y + self.available_size().y
< self.inner_size.y) =>
{
self.offset.y += 1;
}
Event::Ctrl(Key::Left) | Event::Key(Key::Left)
if self.enabled.x && self.offset.x > 0 =>
{
self.offset.x -= 1;
}
Event::Ctrl(Key::Right) | Event::Key(Key::Right)
if self.enabled.x
&& (self.offset.x + self.available_size().x
< self.inner_size.x) =>
{
self.offset.x += 1;
}
_ => return EventResult::Ignored,
};
// We just scrolled manually, so reset the scroll strategy.
self.scroll_strategy = ScrollStrategy::KeepRow;
// TODO: return callback on_scroll?
EventResult::Consumed(None)
}
other => {
// Fix offset?
let important = self.inner.important_area(self.inner_size);
// The furthest top-left we can go
let top_left = (important.bottom_right() + (1, 1))
.saturating_sub(self.available_size());
// The furthest bottom-right we can go
let bottom_right = important.top_left();
// "top_left < bottom_right" is NOT guaranteed
// if the child is larger than the view.
let offset_min = Vec2::min(top_left, bottom_right);
let offset_max = Vec2::max(top_left, bottom_right);
self.offset =
self.offset.or_max(offset_min).or_min(offset_max);
other
}
}
} }
fn layout(&mut self, size: Vec2) { fn layout(&mut self, size: Vec2) {
// Size is final now, negociations are over. self.core.layout(size, &mut self.inner);
self.last_size = size;
// This is what we'd like
let (inner_size, self_size) = self.sizes(size, true);
self.inner_size = inner_size;
self.size_cache = Some(SizeCache::build(self_size, size));
self.inner.layout(self.inner_size);
// Keep the offset in the valid range.
self.offset = self
.offset
.or_min(self.inner_size.saturating_sub(self.available_size()));
// Possibly update the offset if we're following a specific strategy.
self.adjust_scroll();
} }
fn needs_relayout(&self) -> bool { fn needs_relayout(&self) -> bool {
self.inner.needs_relayout() || self.size_cache.is_none() self.core.needs_relayout(|| self.inner.needs_relayout())
} }
fn required_size(&mut self, constraint: Vec2) -> Vec2 { fn required_size(&mut self, constraint: Vec2) -> Vec2 {
let (_, size) = self.sizes(constraint, false); self.core.required_size(constraint, &mut self.inner)
size
} }
fn call_on_any<'a>(&mut self, selector: &Selector<'_>, cb: AnyCb<'a>) { fn call_on_any<'a>(&mut self, selector: &Selector<'_>, cb: AnyCb<'a>) {
@ -700,7 +162,8 @@ where
} }
fn take_focus(&mut self, source: Direction) -> bool { fn take_focus(&mut self, source: Direction) -> bool {
let is_scrollable = self.is_scrolling().any(); let inner = &mut self.inner;
self.inner.take_focus(source) || is_scrollable self.core
.take_focus(source, |source| inner.take_focus(source))
} }
} }