Move event handling outside of scroll::Core

This commit is contained in:
Alexandre Bury 2020-12-05 23:05:47 -08:00
parent 31b8e1f515
commit 0a66978d7f
6 changed files with 266 additions and 176 deletions

View File

@ -17,6 +17,11 @@ use crate::XY;
/// [`XY`]: crate::XY
pub type Vec2 = XY<usize>;
/// A signed 2D quantity, in cells.
///
/// Usually represents an offset.
pub type Vec2i = XY<isize>;
impl<T: PartialOrd> PartialOrd for XY<T> {
/// `a < b` <=> `a.x < b.x && a.y < b.y`
fn partial_cmp(&self, other: &XY<T>) -> Option<Ordering> {

View File

@ -1,14 +1,15 @@
use std::cmp::min;
use crate::direction::Orientation;
use crate::event::{AnyCb, Event, EventResult, Key, MouseButton, MouseEvent};
use crate::printer::Printer;
use crate::rect::Rect;
use crate::theme::ColorStyle;
use crate::view::{ScrollStrategy, Selector, SizeCache};
use crate::with::With;
use crate::Vec2;
use crate::XY;
use crate::{
direction::Orientation,
event::{AnyCb, Event, EventResult, Key, MouseButton, MouseEvent},
printer::Printer,
rect::Rect,
theme::ColorStyle,
view::{ScrollStrategy, Selector, SizeCache},
with::With,
Vec2, XY,
};
/// Describes an item with a scroll core.
///
@ -217,137 +218,6 @@ impl Core {
}
}
/// Handle an event after processing by the content.
pub fn on_inner_event(
&mut self,
event: Event,
inner_result: EventResult,
important_area: Rect,
) -> EventResult {
match inner_result {
EventResult::Ignored => {
// The view ignored the event, so we're free to use it.
// 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,
// without affecting 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.last_available_size().y
< self.inner_size.y) =>
{
self.offset.y = min(
self.inner_size
.y
.saturating_sub(self.last_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.last_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.last_available_size().y
< self.inner_size.y) =>
{
// No `min` check here - we allow going over the edge.
self.offset.y += 5;
}
Event::Ctrl(Key::Down) | Event::Key(Key::Down)
if self.enabled.y
&& (self.offset.y
+ self.last_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.last_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 => {
// The view consumed the event. Maybe something changed?
self.scroll_to_rect(important_area);
other
}
}
}
/// Specifies the size given in a layout phase.
pub(crate) fn set_last_size(
&mut self,
@ -507,6 +377,7 @@ impl Core {
{
let max_offset =
self.inner_size.saturating_sub(self.last_available_size());
self.offset = offset.into().or_min(max_offset);
}
@ -599,32 +470,56 @@ impl Core {
}
}
/// Scroll by `n` cells to the left.
pub fn scroll_left(&mut self, n: usize) {
// Goal: never repeat .x (to prevent typo/confusion)
// TODO: further reduce code duplication.
let offset = self.offset.as_ref_mut().x;
*offset = offset.saturating_sub(n);
}
/// Scroll by `n` cells to the top.
pub fn scroll_up(&mut self, n: usize) {
let offset = self.offset.as_ref_mut().y;
*offset = offset.saturating_sub(n);
}
/// Scroll by `n` cells to the bottom.
pub fn scroll_down(&mut self, n: usize) {
let max_offset = self.max_offset();
let (offset, max) = XY::zip(self.offset.as_ref_mut(), max_offset).y;
*offset = min(max, *offset + n);
}
/// Scroll by `n` cells to the right.
pub fn scroll_right(&mut self, n: usize) {
let max_offset = self.max_offset();
let (offset, max) = XY::zip(self.offset.as_ref_mut(), max_offset).x;
*offset = min(max, *offset + n);
}
/// Programmatically scroll to the top of the view.
pub fn scroll_to_top(&mut self) {
let curr_x = self.offset.x;
self.set_offset((curr_x, 0));
self.offset.y = 0;
}
/// Programmatically scroll to the bottom of the view.
pub fn scroll_to_bottom(&mut self) {
let max_y =
self.inner_size.saturating_sub(self.last_available_size()).y;
let curr_x = self.offset.x;
self.set_offset((curr_x, max_y));
self.offset.y = self.max_offset().y;
}
/// Programmatically scroll to the leftmost side of the view.
pub fn scroll_to_left(&mut self) {
let curr_y = self.offset.y;
self.set_offset((0, curr_y));
self.offset.x = 0;
}
/// Programmatically scroll to the rightmost side of the view.
pub fn scroll_to_right(&mut self) {
let max_x =
self.inner_size.saturating_sub(self.last_available_size()).x;
let curr_y = self.offset.y;
self.set_offset((max_x, curr_y));
self.offset.x = self.max_offset().x;
}
fn max_offset(&self) -> Vec2 {
self.inner_size.saturating_sub(self.last_available_size())
}
/// Clears the cache.
@ -639,7 +534,7 @@ impl Core {
}
/// Stops grabbing the scrollbar.
fn release_grab(&mut self) {
pub fn release_grab(&mut self) {
self.thumb_grab = None;
}
@ -664,10 +559,45 @@ impl Core {
self.last_available_size() + self.scrollbar_size()
}
/// Checks if we can scroll up.
///
/// Returns `true` if vertical scrolling is enabled, and if we are not at
/// the top already.
pub fn can_scroll_up(&self) -> bool {
self.enabled.y && self.offset.y > 0
}
/// Checks if we can scroll to the left.
///
/// Returns `true` if horizontal scrolling is enabled, and if we are not at
/// the left edge already.
pub fn can_scroll_left(&self) -> bool {
self.enabled.x && self.offset.x > 0
}
/// Checks if we can scroll down.
///
/// Returns `true` if vertical scrolling is enabled, and if we are not at
/// the bottom already.
pub fn can_scroll_down(&self) -> bool {
self.enabled.y
&& (self.offset.y + self.last_available_size().y
< self.inner_size.y)
}
/// Checks if we can scroll to the right.
///
/// Returns `true` if horizontal scrolling is enabled, and if we are not at
/// the right edge already.
pub fn can_scroll_right(&self) -> bool {
self.enabled.x
&& (self.offset.x + self.last_available_size().x
< self.inner_size.x)
}
/// Starts scrolling from the cursor position.
///
/// Returns `true` if the event was consumed.
fn start_drag(&mut self, position: Vec2) -> bool {
pub fn start_drag(&mut self, position: Vec2) -> bool {
// For each scrollbar, how far it is.
let scrollbar_pos = self.last_outer_size().saturating_sub((1, 1));
let lengths = self.scrollbar_thumb_lengths();
@ -708,7 +638,7 @@ impl Core {
}
/// Called when a mouse drag is detected.
fn drag(&mut self, position: Vec2) {
pub 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(

View File

@ -2,12 +2,14 @@
//!
//! Most functions take a generic `Model` class, and various closures to get
//! the required things from this model.
use crate::event::{Event, EventResult};
use crate::rect::Rect;
use crate::view::scroll;
use crate::xy::XY;
use crate::Printer;
use crate::Vec2;
//!
use crate::{
event::{Event, EventResult, Key, MouseButton, MouseEvent},
rect::Rect,
view::scroll,
xy::XY,
Printer, Vec2,
};
/// Implements `View::draw` over the `model`.
pub fn draw<Model, GetScroller, Draw>(
@ -224,7 +226,124 @@ pub fn on_event<Model: ?Sized>(
} else {
EventResult::Ignored
};
match result {
EventResult::Ignored => {
// The view ignored the event, so we're free to use it.
// 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,
// without affecting the selection.
match event {
Event::Mouse {
event: MouseEvent::WheelUp,
..
} if get_scroller(model).can_scroll_up() => {
get_scroller(model).scroll_up(3);
}
Event::Mouse {
event: MouseEvent::WheelDown,
..
} if get_scroller(model).can_scroll_down() => {
get_scroller(model).scroll_down(3);
}
Event::Mouse {
event: MouseEvent::Press(MouseButton::Left),
position,
offset,
} if get_scroller(model).get_show_scrollbars()
&& position
.checked_sub(offset)
.map(|position| {
get_scroller(model).start_drag(position)
})
.unwrap_or(false) =>
{
// Just consume the event.
}
Event::Mouse {
event: MouseEvent::Hold(MouseButton::Left),
position,
offset,
} if get_scroller(model).get_show_scrollbars() => {
let position = position.saturating_sub(offset);
get_scroller(model).drag(position);
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
..
} => {
get_scroller(model).release_grab();
}
Event::Key(Key::Home)
if get_scroller(model).is_enabled().any() =>
{
let actions: XY<fn(&mut scroll::Core)> = XY::new(
scroll::Core::scroll_to_left,
scroll::Core::scroll_to_top,
);
let scroller = get_scroller(model);
actions.run_if(scroller.is_enabled(), |a| a(scroller));
}
Event::Key(Key::End)
if get_scroller(model).is_enabled().any() =>
{
let actions: XY<fn(&mut scroll::Core)> = XY::new(
scroll::Core::scroll_to_right,
scroll::Core::scroll_to_bottom,
);
let scroller = get_scroller(model);
actions.run_if(scroller.is_enabled(), |a| a(scroller));
}
Event::Ctrl(Key::Up) | Event::Key(Key::Up)
if get_scroller(model).can_scroll_up() =>
{
get_scroller(model).scroll_up(1);
}
Event::Key(Key::PageUp)
if get_scroller(model).can_scroll_up() =>
{
get_scroller(model).scroll_up(5);
}
Event::Key(Key::PageDown)
if get_scroller(model).can_scroll_down() =>
{
// No `min` check here - we allow going over the edge.
get_scroller(model).scroll_down(5);
}
Event::Ctrl(Key::Down) | Event::Key(Key::Down)
if get_scroller(model).can_scroll_down() =>
{
get_scroller(model).scroll_down(1);
}
Event::Ctrl(Key::Left) | Event::Key(Key::Left)
if get_scroller(model).can_scroll_left() =>
{
get_scroller(model).scroll_left(1);
}
Event::Ctrl(Key::Right) | Event::Key(Key::Right)
if get_scroller(model).can_scroll_right() =>
{
get_scroller(model).scroll_right(1);
}
_ => return EventResult::Ignored,
};
// We just scrolled manually, so reset the scroll strategy.
get_scroller(model)
.set_scroll_strategy(scroll::ScrollStrategy::KeepRow);
// TODO: return callback on_scroll?
EventResult::Consumed(None)
}
other => {
// The view consumed the event. Maybe something changed?
let inner_size = get_scroller(model).inner_size();
let important = important_area(model, inner_size);
get_scroller(model).on_inner_event(event, result, important)
get_scroller(model).scroll_to_rect(important);
other
}
}
}

View File

@ -1,5 +1,12 @@
/// Generic trait to enable chainable API
pub trait With: Sized {
/// Calls the given closure and return the result.
///
/// Used to chainify wrapper constructors.
fn wrap_with<U, F: FnOnce(Self) -> U>(self, f: F) -> U {
f(self)
}
/// Calls the given closure on `self`.
fn with<F: FnOnce(&mut Self)>(mut self, f: F) -> Self {
f(&mut self);

View File

@ -110,9 +110,9 @@ impl<T> XY<T> {
///
/// assert_eq!(xy.run_if(cond, |v| v * 3), XY::new(Some(3), None));
/// ```
pub fn run_if<F, U>(self, condition: XY<bool>, f: F) -> XY<Option<U>>
pub fn run_if<F, U>(self, condition: XY<bool>, mut f: F) -> XY<Option<U>>
where
F: Fn(T) -> U,
F: FnMut(T) -> U,
{
self.zip_map(condition, |v, c| if c { Some(f(v)) } else { None })
}
@ -163,7 +163,7 @@ impl<T> XY<T> {
(self.x, self.y)
}
/// Return a `XY` with references to this one's values.
/// Returns a `XY` with references to this one's values.
///
/// # Examples
///
@ -184,6 +184,11 @@ impl<T> XY<T> {
XY::new(&self.x, &self.y)
}
/// Returns a `XY` with mutable references to this one's values.
pub fn as_ref_mut(&mut self) -> XY<&mut T> {
XY::new(&mut self.x, &mut self.y)
}
/// Creates an iterator that returns references to `x`, then `y`.
///
/// # Examples
@ -331,9 +336,9 @@ impl<T> XY<T> {
/// let xy = a.zip_map(b, |(a1, a2), b| if b { a1 } else { a2 });
/// assert_eq!(xy, XY::new(1, 20));
/// ```
pub fn zip_map<U, V, F>(self, other: XY<U>, f: F) -> XY<V>
pub fn zip_map<U, V, F>(self, other: XY<U>, mut f: F) -> XY<V>
where
F: Fn(T, U) -> V,
F: FnMut(T, U) -> V,
{
XY::new(f(self.x, other.x), f(self.y, other.y))
}

View File

@ -1,6 +1,8 @@
use cursive::align::HAlign;
use cursive::view::Scrollable;
use cursive::event::{EventResult, Key};
use cursive::traits::With;
use cursive::view::{scroll::Scroller, Scrollable};
use cursive::views::{Dialog, Panel, TextView};
use cursive::{align::HAlign, views::OnEventView};
fn main() {
// Read some long text from a file.
@ -14,7 +16,29 @@ fn main() {
// The text is too long to fit on a line, so the view will wrap lines,
// and will adapt to the terminal size.
siv.add_fullscreen_layer(
Dialog::around(Panel::new(TextView::new(content).scrollable()))
Dialog::around(Panel::new(
TextView::new(content)
.scrollable()
.wrap_with(OnEventView::new)
.on_pre_event_inner(Key::PageUp, |v, _| {
let scroller = v.get_scroller_mut();
if scroller.can_scroll_up() {
scroller.scroll_up(
scroller.last_outer_size().y.saturating_sub(1),
);
}
Some(EventResult::Consumed(None))
})
.on_pre_event_inner(Key::PageDown, |v, _| {
let scroller = v.get_scroller_mut();
if scroller.can_scroll_down() {
scroller.scroll_down(
scroller.last_outer_size().y.saturating_sub(1),
);
}
Some(EventResult::Consumed(None))
}),
))
.title("Unicode and wide-character support")
// This is the alignment for the button
.h_align(HAlign::Center)