mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-23 17:35:00 +00:00
Refactor scroll::Core mechanism.
Moved most View-trait-performing-functions outside of `scroll::Core` into `scroll::raw`. Added a `Scroller` trait and convenience methods like `scroll::layout` to weave the borrows appropriately.
This commit is contained in:
parent
e5ef01c90f
commit
4b5a7867e3
13
src/lib.rs
13
src/lib.rs
@ -87,7 +87,6 @@ pub mod align;
|
||||
pub mod direction;
|
||||
pub mod logger;
|
||||
pub mod menu;
|
||||
pub mod rect;
|
||||
pub mod theme;
|
||||
pub mod vec;
|
||||
pub mod views;
|
||||
@ -95,6 +94,7 @@ pub mod views;
|
||||
// This probably doesn't need to be public?
|
||||
mod cursive;
|
||||
mod printer;
|
||||
mod rect;
|
||||
mod with;
|
||||
mod xy;
|
||||
|
||||
@ -103,8 +103,9 @@ mod utf8;
|
||||
|
||||
pub mod backend;
|
||||
|
||||
pub use crate::cursive::{CbFunc, CbSink, Cursive, ScreenId};
|
||||
pub use crate::printer::Printer;
|
||||
pub use crate::vec::Vec2;
|
||||
pub use crate::with::With;
|
||||
pub use crate::xy::XY;
|
||||
pub use self::cursive::{CbFunc, CbSink, Cursive, ScreenId};
|
||||
pub use self::printer::Printer;
|
||||
pub use self::rect::Rect;
|
||||
pub use self::vec::Vec2;
|
||||
pub use self::with::With;
|
||||
pub use self::xy::XY;
|
||||
|
@ -1,131 +0,0 @@
|
||||
use crate::vec::Vec2;
|
||||
use crate::Printer;
|
||||
|
||||
use crate::direction::Direction;
|
||||
use crate::event::{Event, EventResult};
|
||||
use crate::view::scroll::ScrollCore;
|
||||
use crate::view::scroll::{InnerLayout, InnerOnEvent, InnerRequiredSize};
|
||||
|
||||
/// 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 {
|
||||
core: ScrollCore,
|
||||
}
|
||||
|
||||
struct RequiredSize<F>(F);
|
||||
|
||||
impl<F> InnerRequiredSize for RequiredSize<F>
|
||||
where
|
||||
F: FnMut(Vec2) -> Vec2,
|
||||
{
|
||||
fn needs_relayout(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||
self.0(constraint)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> InnerLayout for RequiredSize<F>
|
||||
where
|
||||
F: FnMut(Vec2) -> Vec2,
|
||||
{
|
||||
fn layout(&mut self, _size: Vec2) {}
|
||||
|
||||
fn needs_relayout(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||
self.0(constraint)
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollBase {
|
||||
/// Creates a new, uninitialized scrollbar.
|
||||
pub fn new() -> Self {
|
||||
ScrollBase {
|
||||
core: ScrollCore::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs `View::layout()`.
|
||||
pub fn layout<F>(&mut self, size: Vec2, required_size: F)
|
||||
where
|
||||
F: FnMut(Vec2) -> Vec2,
|
||||
{
|
||||
self.core.layout(size, RequiredSize(required_size));
|
||||
}
|
||||
|
||||
/// Performs `View::required_size()`.
|
||||
pub fn required_size<F>(
|
||||
&mut self, constraint: Vec2, required_size: F,
|
||||
) -> Vec2
|
||||
where
|
||||
F: FnMut(Vec2) -> Vec2,
|
||||
{
|
||||
self.core
|
||||
.required_size(constraint, RequiredSize(required_size))
|
||||
}
|
||||
|
||||
/// 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<'_, '_>, mut line_drawer: F)
|
||||
where
|
||||
F: FnMut(&Printer<'_, '_>, usize),
|
||||
{
|
||||
self.core.draw(printer, |printer| {
|
||||
let start = printer.content_offset.y;
|
||||
let end = start + printer.output_size.y;
|
||||
for y in start..end {
|
||||
let printer =
|
||||
printer.offset((0, y)).cropped((printer.size.x, 1));
|
||||
line_drawer(&printer, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Performs `View::take_focus()`.
|
||||
pub fn take_focus<F>(
|
||||
&mut self, source: Direction, inner_take_focus: F,
|
||||
) -> bool
|
||||
where
|
||||
F: FnOnce(Direction) -> bool,
|
||||
{
|
||||
self.core.take_focus(source, inner_take_focus)
|
||||
}
|
||||
|
||||
/// Performs `View::on_event()`.
|
||||
pub fn on_event<I>(&mut self, event: Event, inner: I) -> EventResult
|
||||
where
|
||||
I: InnerOnEvent,
|
||||
{
|
||||
self.core.on_event(event, inner)
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
use std::cmp::min;
|
||||
|
||||
use crate::direction::{Direction, Orientation};
|
||||
use crate::direction::Orientation;
|
||||
use crate::event::{AnyCb, Event, EventResult, Key, MouseButton, MouseEvent};
|
||||
use crate::printer::Printer;
|
||||
use crate::rect::Rect;
|
||||
@ -10,13 +10,25 @@ use crate::view::{ScrollStrategy, Selector, SizeCache};
|
||||
use crate::with::With;
|
||||
use crate::XY;
|
||||
|
||||
use crate::view::scroll::{InnerLayout, InnerOnEvent, InnerRequiredSize};
|
||||
/// Describes an item with a scroll core.
|
||||
///
|
||||
/// This trait is used to represent "something that can scroll".
|
||||
/// All it needs is an accessible core.
|
||||
///
|
||||
/// See the various methods in the [`scroll`](crate::view::scroll) module.
|
||||
pub trait Scroller {
|
||||
/// Returns a mutable access to the scroll core.
|
||||
fn get_scroller_mut(&mut self) -> &mut Core;
|
||||
|
||||
/// Returns an immutable access to the scroll core.
|
||||
fn get_scroller(&self) -> &Core;
|
||||
}
|
||||
|
||||
/// Core system for scrolling views.
|
||||
///
|
||||
/// See also [`ScrollView`](crate::views::ScrollView).
|
||||
#[derive(Debug)]
|
||||
pub struct ScrollCore {
|
||||
pub struct Core {
|
||||
/// This is the size the child thinks we're giving him.
|
||||
inner_size: Vec2,
|
||||
|
||||
@ -57,16 +69,16 @@ pub struct ScrollCore {
|
||||
scroll_strategy: ScrollStrategy,
|
||||
}
|
||||
|
||||
impl Default for ScrollCore {
|
||||
impl Default for Core {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ScrollCore {
|
||||
/// Creates a new `ScrollCore`.
|
||||
impl Core {
|
||||
/// Creates a new `Core`.
|
||||
pub fn new() -> Self {
|
||||
ScrollCore {
|
||||
Core {
|
||||
inner_size: Vec2::zero(),
|
||||
offset: Vec2::zero(),
|
||||
last_size: Vec2::zero(),
|
||||
@ -79,11 +91,10 @@ impl ScrollCore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs the `View::draw()` operation.
|
||||
pub fn draw<F>(&self, printer: &Printer<'_, '_>, inner_draw: F)
|
||||
where
|
||||
F: FnOnce(&Printer<'_, '_>),
|
||||
{
|
||||
/// Returns a sub-printer ready to draw the content.
|
||||
pub fn sub_printer<'a, 'b>(
|
||||
&self, printer: &Printer<'a, 'b>,
|
||||
) -> Printer<'a, 'b> {
|
||||
// Draw scrollbar?
|
||||
let scrolling = self.is_scrolling();
|
||||
|
||||
@ -138,27 +149,21 @@ impl ScrollCore {
|
||||
}
|
||||
|
||||
// Draw content
|
||||
let printer = printer
|
||||
printer
|
||||
.cropped(size)
|
||||
.content_offset(self.offset)
|
||||
.inner_size(self.inner_size);
|
||||
|
||||
inner_draw(&printer);
|
||||
.inner_size(self.inner_size)
|
||||
}
|
||||
|
||||
/// Performs `View::on_event()`
|
||||
pub fn on_event<I: InnerOnEvent>(
|
||||
&mut self, event: Event, mut inner: I,
|
||||
) -> EventResult {
|
||||
// Relativize event accorging to the offset
|
||||
let mut relative_event = event.clone();
|
||||
|
||||
// Should the event be treated inside, by the inner view?
|
||||
let inside = if let Event::Mouse {
|
||||
/// Returns `true` if `event` should be processed by the content.
|
||||
///
|
||||
/// This also updates `event` so that it is relative to the content.
|
||||
pub fn is_event_inside(&self, event: &mut Event) -> bool {
|
||||
if let Event::Mouse {
|
||||
ref mut position,
|
||||
ref offset,
|
||||
..
|
||||
} = relative_event
|
||||
} = event
|
||||
{
|
||||
// For mouse events, check if it falls inside the available area
|
||||
let inside = position
|
||||
@ -170,17 +175,15 @@ impl ScrollCore {
|
||||
} 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.
|
||||
inner.on_event(relative_event)
|
||||
} else {
|
||||
// Otherwise, pretend it wasn't there.
|
||||
EventResult::Ignored
|
||||
};
|
||||
|
||||
match result {
|
||||
/// 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.
|
||||
|
||||
@ -294,7 +297,7 @@ impl ScrollCore {
|
||||
// The view consumed the event. Maybe something changed?
|
||||
|
||||
// Fix offset?
|
||||
let important = inner.important_area(self.inner_size);
|
||||
let important = important_area;
|
||||
|
||||
// The furthest top-left we can go
|
||||
let top_left = (important.bottom_right() + (1, 1))
|
||||
@ -315,21 +318,28 @@ impl ScrollCore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs `View::layout()`
|
||||
pub fn layout<I: InnerLayout>(&mut self, size: Vec2, mut inner: I) {
|
||||
// Size is final now, negociations are over.
|
||||
self.last_size = size;
|
||||
/// Specifies the size given in a layout phase.
|
||||
pub(crate) fn set_last_size(&mut self, last_size: Vec2) {
|
||||
self.last_size = last_size;
|
||||
}
|
||||
|
||||
// This is what we'd like
|
||||
let (inner_size, self_size) =
|
||||
self.sizes(size, true, Layout2Sizes { inner: &mut inner });
|
||||
/// Returns the size last given in `set_last_size()`.
|
||||
pub fn last_size(&self) -> Vec2 {
|
||||
self.last_size
|
||||
}
|
||||
|
||||
/// Specifies the size allocated to the content.
|
||||
pub(crate) fn set_inner_size(&mut self, inner_size: Vec2) {
|
||||
self.inner_size = inner_size;
|
||||
}
|
||||
|
||||
self.size_cache = Some(SizeCache::build(self_size, size));
|
||||
|
||||
inner.layout(self.inner_size);
|
||||
/// Rebuild the cache with the given parameters.
|
||||
pub(crate) fn build_cache(&mut self, self_size: Vec2, last_size: Vec2) {
|
||||
self.size_cache = Some(SizeCache::build(self_size, last_size));
|
||||
}
|
||||
|
||||
/// Makes sure the viewport is within the content.
|
||||
pub(crate) fn update_offset(&mut self) {
|
||||
// Keep the offset in the valid range.
|
||||
self.offset = self
|
||||
.offset
|
||||
@ -339,25 +349,11 @@ impl ScrollCore {
|
||||
self.adjust_scroll();
|
||||
}
|
||||
|
||||
/// Performs `View::needs_relayout()`
|
||||
pub fn needs_relayout<F>(&self, inner_needs_relayout: F) -> bool
|
||||
where
|
||||
F: FnOnce() -> bool,
|
||||
{
|
||||
self.size_cache.is_none() || inner_needs_relayout()
|
||||
}
|
||||
|
||||
/// Performs `View::required_size()`
|
||||
pub fn required_size<I: InnerRequiredSize>(
|
||||
&mut self, constraint: Vec2, mut inner: I,
|
||||
) -> Vec2 {
|
||||
let (_, size) = self.sizes(
|
||||
constraint,
|
||||
false,
|
||||
Required2Sizes { inner: &mut inner },
|
||||
);
|
||||
|
||||
size
|
||||
/// Returns `true` if we should relayout, no matter the content.
|
||||
///
|
||||
/// Even if this returns `false`, the content itself might still needs to relayout.
|
||||
pub fn needs_relayout(&self) -> bool {
|
||||
self.size_cache.is_none()
|
||||
}
|
||||
|
||||
/// Performs `View::call_on_any()`
|
||||
@ -380,17 +376,6 @@ impl ScrollCore {
|
||||
inner_focus_view(selector)
|
||||
}
|
||||
|
||||
/// Performs `View::take_focus()`
|
||||
pub fn take_focus<F>(
|
||||
&mut self, source: Direction, inner_take_focus: F,
|
||||
) -> bool
|
||||
where
|
||||
F: FnOnce(Direction) -> bool,
|
||||
{
|
||||
let is_scrollable = self.is_scrolling().any();
|
||||
inner_take_focus(source) || is_scrollable
|
||||
}
|
||||
|
||||
/// Returns the viewport in the inner content.
|
||||
pub fn content_viewport(&self) -> Rect {
|
||||
Rect::from_size(self.offset, self.available_size())
|
||||
@ -431,6 +416,19 @@ impl ScrollCore {
|
||||
self.with(|s| s.set_scrollbar_padding(scrollbar_padding))
|
||||
}
|
||||
|
||||
/// Returns the padding between content and scrollbar.
|
||||
pub fn get_scrollbar_padding(&self) -> Vec2 {
|
||||
self.scrollbar_padding
|
||||
}
|
||||
|
||||
/// For each axis, returns `true` if this view can scroll.
|
||||
///
|
||||
/// For example, a vertically-scrolling view will return
|
||||
/// `XY { x: false, y: true }`.
|
||||
pub fn is_enabled(&self) -> XY<bool> {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
/// Control whether scroll bars are visibile.
|
||||
///
|
||||
/// Defaults to `true`.
|
||||
@ -445,6 +443,18 @@ impl ScrollCore {
|
||||
self.with(|s| s.set_show_scrollbars(show_scrollbars))
|
||||
}
|
||||
|
||||
/// Returns `true` if we will show scrollbars when needed.
|
||||
///
|
||||
/// Scrollbars are always hidden when not needed.
|
||||
pub fn get_show_scrollbars(&self) -> bool {
|
||||
self.show_scrollbars
|
||||
}
|
||||
|
||||
/// Returns the size given to the content on the last layout phase.
|
||||
pub fn inner_size(&self) -> Vec2 {
|
||||
self.inner_size
|
||||
}
|
||||
|
||||
/// Sets the scroll offset to the given value
|
||||
pub fn set_offset<S>(&mut self, offset: S)
|
||||
where
|
||||
@ -529,7 +539,7 @@ impl ScrollCore {
|
||||
}
|
||||
|
||||
/// Returns for each axis if we are scrolling.
|
||||
fn is_scrolling(&self) -> XY<bool> {
|
||||
pub fn is_scrolling(&self) -> XY<bool> {
|
||||
self.inner_size.zip_map(self.last_size, |i, s| i > s)
|
||||
}
|
||||
|
||||
@ -543,7 +553,7 @@ impl ScrollCore {
|
||||
/// 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 {
|
||||
pub fn scrollbar_size(&self) -> Vec2 {
|
||||
self.is_scrolling()
|
||||
.swap()
|
||||
.select_or(self.scrollbar_padding + (1, 1), Vec2::zero())
|
||||
@ -558,49 +568,6 @@ impl ScrollCore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute the size we would need.
|
||||
///
|
||||
/// Given the constraints, and the axis that need scrollbars.
|
||||
///
|
||||
/// Returns `(inner_size, size, scrollable)`.
|
||||
fn sizes_when_scrolling<I: InnerSizes>(
|
||||
&mut self, constraint: Vec2, scrollable: XY<bool>, strict: bool,
|
||||
inner: &mut I,
|
||||
) -> (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 = 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.
|
||||
@ -679,74 +646,17 @@ impl ScrollCore {
|
||||
.set_axis_from(orientation, &new_offset.or_min(max_offset));
|
||||
}
|
||||
|
||||
/// Computes the size we would need given the constraints.
|
||||
/// Tries to apply the cache to the current constraint.
|
||||
///
|
||||
/// 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<I: InnerSizes>(
|
||||
&mut self, constraint: Vec2, strict: bool, mut inner: I,
|
||||
) -> (Vec2, Vec2) {
|
||||
// First: try the cache
|
||||
let valid_cache = !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,
|
||||
&mut inner,
|
||||
);
|
||||
|
||||
// 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, &mut inner,
|
||||
);
|
||||
if scrollable == new_scrollable {
|
||||
// Yup, scrolling did it. We're good to go now.
|
||||
(inner_size, size)
|
||||
/// Returns the cached value if it works, or `None`.
|
||||
pub(crate) fn try_cache(&self, constraint: Vec2) -> Option<(Vec2, Vec2)> {
|
||||
self.size_cache.and_then(|cache| {
|
||||
if cache.zip_map(constraint, SizeCache::accept).both() {
|
||||
Some((self.inner_size, cache.map(|c| c.value)))
|
||||
} 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,
|
||||
&mut inner,
|
||||
);
|
||||
|
||||
// That's enough. If the inner view changed again, ignore it!
|
||||
// That'll teach it.
|
||||
(inner_size, size)
|
||||
None
|
||||
}
|
||||
} 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 {
|
||||
@ -774,34 +684,3 @@ impl ScrollCore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Layout2Sizes<'a, I> {
|
||||
inner: &'a mut I,
|
||||
}
|
||||
|
||||
trait InnerSizes {
|
||||
fn needs_relayout(&self) -> bool;
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2;
|
||||
}
|
||||
|
||||
impl<'a, I: InnerLayout> InnerSizes for Layout2Sizes<'a, I> {
|
||||
fn needs_relayout(&self) -> bool {
|
||||
self.inner.needs_relayout()
|
||||
}
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||
self.inner.required_size(constraint)
|
||||
}
|
||||
}
|
||||
|
||||
struct Required2Sizes<'a, I> {
|
||||
inner: &'a mut I,
|
||||
}
|
||||
|
||||
impl<'a, I: InnerRequiredSize> InnerSizes for Required2Sizes<'a, I> {
|
||||
fn needs_relayout(&self) -> bool {
|
||||
self.inner.needs_relayout()
|
||||
}
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||
self.inner.required_size(constraint)
|
||||
}
|
||||
}
|
||||
|
@ -2,17 +2,17 @@
|
||||
//!
|
||||
//! *This module is still unstable and may go through breaking changes.*
|
||||
//!
|
||||
//! This module defines [`ScrollCore`](crate::view::scroll::ScrollCore) and related traits.
|
||||
//! This module defines [`Core`](crate::view::scroll::Core) and related traits.
|
||||
//!
|
||||
//! [`ScrollView`](crate::views::ScrollView) may be an easier way to add scrolling to an existing view.
|
||||
|
||||
mod base;
|
||||
mod core;
|
||||
mod traits;
|
||||
mod raw;
|
||||
|
||||
pub use self::base::ScrollBase;
|
||||
pub use self::core::ScrollCore;
|
||||
pub use self::traits::{InnerLayout, InnerOnEvent, InnerRequiredSize};
|
||||
pub use self::core::{Core, Scroller};
|
||||
|
||||
use crate::event::{Event, EventResult};
|
||||
use crate::{Printer, Rect, Vec2};
|
||||
|
||||
/// Defines the scrolling behaviour on content or size change
|
||||
#[derive(Debug)]
|
||||
@ -30,3 +30,210 @@ impl Default for ScrollStrategy {
|
||||
ScrollStrategy::KeepRow
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs `View::on_event` on a `scroll::Scroller`.
|
||||
///
|
||||
/// Example:
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// fn on_event(&mut self, event: Event) -> EventResult {
|
||||
/// scroll::on_event(self, event, Self::inner_on_event, Self::inner_important_area)
|
||||
/// }
|
||||
/// ```
|
||||
pub fn on_event<T, OnEvent, ImportantArea>(
|
||||
scroller: &mut T, event: Event, on_event: OnEvent,
|
||||
important_area: ImportantArea,
|
||||
) -> EventResult
|
||||
where
|
||||
T: Scroller,
|
||||
OnEvent: FnMut(&mut T, Event) -> EventResult,
|
||||
ImportantArea: FnMut(&T, Vec2) -> Rect,
|
||||
{
|
||||
raw::on_event(
|
||||
event,
|
||||
scroller,
|
||||
Scroller::get_scroller_mut,
|
||||
on_event,
|
||||
important_area,
|
||||
)
|
||||
}
|
||||
|
||||
/// Performs `View::important_area` on a `scroll::Scroller`.
|
||||
pub fn important_area<T, ImportantArea>(
|
||||
scroller: &T, size: Vec2, mut important_area: ImportantArea,
|
||||
) -> Rect
|
||||
where
|
||||
T: Scroller,
|
||||
ImportantArea: FnMut(&T, Vec2) -> Rect,
|
||||
{
|
||||
let viewport = scroller.get_scroller().content_viewport();
|
||||
let area = important_area(scroller, size);
|
||||
let top_left = area.top_left().saturating_sub(viewport.top_left());
|
||||
let bot_right = area
|
||||
.bottom_right()
|
||||
.saturating_sub(viewport.top_left())
|
||||
.or_min(viewport.bottom_right());
|
||||
|
||||
Rect::from_corners(top_left, bot_right)
|
||||
}
|
||||
|
||||
/// Performs `View::layout` on a `scroll::Scroller`.
|
||||
pub fn layout<T, Layout, RequiredSize>(
|
||||
scroller: &mut T, size: Vec2, needs_relayout: bool, layout: Layout,
|
||||
required_size: RequiredSize,
|
||||
) where
|
||||
T: Scroller,
|
||||
Layout: FnMut(&mut T, Vec2),
|
||||
RequiredSize: FnMut(&mut T, Vec2) -> Vec2,
|
||||
{
|
||||
raw::layout(
|
||||
size,
|
||||
needs_relayout,
|
||||
scroller,
|
||||
Scroller::get_scroller_mut,
|
||||
required_size,
|
||||
layout,
|
||||
);
|
||||
}
|
||||
|
||||
/// Performs `View::required_size` on a `scroll::Scroller`.
|
||||
pub fn required_size<T, RequiredSize>(
|
||||
scroller: &mut T, size: Vec2, needs_relayout: bool,
|
||||
required_size: RequiredSize,
|
||||
) -> Vec2
|
||||
where
|
||||
T: Scroller,
|
||||
RequiredSize: FnMut(&mut T, Vec2) -> Vec2,
|
||||
{
|
||||
raw::required_size(
|
||||
size,
|
||||
needs_relayout,
|
||||
scroller,
|
||||
Scroller::get_scroller_mut,
|
||||
required_size,
|
||||
)
|
||||
}
|
||||
|
||||
/// Performs `View::draw` on a `scroll::Scroller`.
|
||||
pub fn draw<T, Draw>(scroller: &T, printer: &Printer, draw: Draw)
|
||||
where
|
||||
T: Scroller,
|
||||
Draw: FnOnce(&T, &Printer),
|
||||
{
|
||||
raw::draw(printer, scroller, Scroller::get_scroller, draw);
|
||||
}
|
||||
|
||||
/// Performs a line-based `View::draw` on a `scroll::Scroller`.
|
||||
///
|
||||
/// This is an alternative to `scroll::draw()` when you just need to print individual lines.
|
||||
pub fn draw_lines<T, LineDrawer>(
|
||||
scroller: &T, printer: &Printer, mut line_drawer: LineDrawer,
|
||||
) where
|
||||
T: Scroller,
|
||||
LineDrawer: FnMut(&T, &Printer, usize),
|
||||
{
|
||||
draw(scroller, printer, |s, printer| {
|
||||
let start = printer.content_offset.y;
|
||||
let end = start + printer.output_size.y;
|
||||
for y in start..end {
|
||||
let printer = printer.offset((0, y)).cropped((printer.size.x, 1));
|
||||
line_drawer(s, &printer, y);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draws a frame around the scrollable content.
|
||||
///
|
||||
/// `left_border` will be called for each row to draw the left border for the given line number.
|
||||
pub fn draw_frame<T, LeftBorder, TopBorder, RightBorder, BottomBorder>(
|
||||
scroller: &T, printer: &Printer, mut left_border: LeftBorder,
|
||||
mut top_border: TopBorder, mut right_border: RightBorder,
|
||||
mut bottom_border: BottomBorder,
|
||||
) where
|
||||
T: Scroller,
|
||||
LeftBorder: FnMut(&T, &Printer, usize),
|
||||
TopBorder: FnMut(&T, &Printer, usize),
|
||||
RightBorder: FnMut(&T, &Printer, usize),
|
||||
BottomBorder: FnMut(&T, &Printer, usize),
|
||||
{
|
||||
let viewport = scroller.get_scroller().content_viewport();
|
||||
let size = printer.size.saturating_sub((1, 1));
|
||||
|
||||
for (i, x) in (viewport.left()..=viewport.right()).enumerate() {
|
||||
top_border(scroller, &printer.offset((i + 1, 0)), x);
|
||||
bottom_border(scroller, &printer.offset((i + 1, size.y)), x);
|
||||
}
|
||||
|
||||
// Also draw padding
|
||||
let scrollbar_size = scroller.get_scroller().scrollbar_size();
|
||||
printer.print_hline((viewport.right() + 2, 0), scrollbar_size.x, "─");
|
||||
printer.print_hline(
|
||||
(viewport.right() + 2, size.y),
|
||||
scrollbar_size.x,
|
||||
"─",
|
||||
);
|
||||
printer.print_vline((0, viewport.bottom() + 2), scrollbar_size.y, "│");
|
||||
printer.print_vline(
|
||||
(size.x, viewport.bottom() + 2),
|
||||
scrollbar_size.y,
|
||||
"│",
|
||||
);
|
||||
|
||||
for (i, y) in (viewport.top()..=viewport.bottom()).enumerate() {
|
||||
left_border(scroller, &printer.offset((0, i + 1)), y);
|
||||
right_border(scroller, &printer.offset((size.x, i + 1)), y);
|
||||
}
|
||||
|
||||
printer.print((0, 0), "┌");
|
||||
printer.print(size.keep_y(), "└");
|
||||
printer.print(size.keep_x(), "┐");
|
||||
printer.print(size, "┘");
|
||||
}
|
||||
|
||||
/// Draws a box-style frame around a scrollable content.
|
||||
///
|
||||
/// Assumes horizontal lines are present in the content whenever `is_h_delim`
|
||||
/// returns `true` (and vertical lines when `is_v_delim` returns `true`).
|
||||
///
|
||||
/// It will print a box with the appropriate `├`, `┤` and so on.
|
||||
pub fn draw_box_frame<T, IsHDelim, IsVDelim>(
|
||||
scroller: &T, printer: &Printer, is_h_delim: IsHDelim,
|
||||
is_v_delim: IsVDelim,
|
||||
) where
|
||||
T: Scroller,
|
||||
IsHDelim: Fn(&T, usize) -> bool,
|
||||
IsVDelim: Fn(&T, usize) -> bool,
|
||||
{
|
||||
draw_frame(
|
||||
scroller,
|
||||
printer,
|
||||
|s, printer, y| {
|
||||
if is_h_delim(s, y) {
|
||||
printer.print((0, 0), "├");
|
||||
} else {
|
||||
printer.print((0, 0), "│");
|
||||
}
|
||||
},
|
||||
|s, printer, x| {
|
||||
if is_v_delim(s, x) {
|
||||
printer.print((0, 0), "┬");
|
||||
} else {
|
||||
printer.print((0, 0), "─");
|
||||
}
|
||||
},
|
||||
|s, printer, y| {
|
||||
if is_h_delim(s, y) {
|
||||
printer.print((0, 0), "┤");
|
||||
} else {
|
||||
printer.print((0, 0), "│");
|
||||
}
|
||||
},
|
||||
|s, printer, x| {
|
||||
if is_v_delim(s, x) {
|
||||
printer.print((0, 0), "┴");
|
||||
} else {
|
||||
printer.print((0, 0), "─");
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
198
src/view/scroll/raw.rs
Normal file
198
src/view/scroll/raw.rs
Normal file
@ -0,0 +1,198 @@
|
||||
use crate::event::{Event, EventResult};
|
||||
use crate::rect::Rect;
|
||||
use crate::view::scroll;
|
||||
use crate::xy::XY;
|
||||
use crate::Printer;
|
||||
use crate::Vec2;
|
||||
|
||||
pub fn draw<Model, GetScroller, Draw>(
|
||||
printer: &Printer, model: &Model, mut get_scroller: GetScroller,
|
||||
inner_draw: Draw,
|
||||
) where
|
||||
Model: ?Sized,
|
||||
GetScroller: FnMut(&Model) -> &scroll::Core,
|
||||
Draw: FnOnce(&Model, &Printer),
|
||||
{
|
||||
let printer = get_scroller(model).sub_printer(printer);
|
||||
inner_draw(model, &printer);
|
||||
}
|
||||
|
||||
fn sizes_when_scrolling<Model, GetScroller, RequiredSize>(
|
||||
constraint: Vec2, scrollable: XY<bool>, strict: bool, model: &mut Model,
|
||||
get_scroller: &mut GetScroller, required_size: &mut RequiredSize,
|
||||
) -> (Vec2, Vec2, XY<bool>)
|
||||
where
|
||||
Model: ?Sized,
|
||||
GetScroller: FnMut(&mut Model) -> &mut scroll::Core,
|
||||
RequiredSize: FnMut(&mut Model, Vec2) -> Vec2,
|
||||
{
|
||||
// This is the size taken by the scrollbars.
|
||||
let scrollbar_size = scrollable.swap().select_or(
|
||||
get_scroller(model).get_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 = required_size(model, available);
|
||||
|
||||
// Where we're "enabled", accept the constraints.
|
||||
// Where we're not, just forward inner_size.
|
||||
let size = get_scroller(model).is_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 = get_scroller(model)
|
||||
.is_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)
|
||||
}
|
||||
|
||||
fn sizes<Model, GetScroller, RequiredSize>(
|
||||
constraint: Vec2, strict: bool, needs_relayout: bool, model: &mut Model,
|
||||
get_scroller: &mut GetScroller, required_size: &mut RequiredSize,
|
||||
) -> (Vec2, Vec2)
|
||||
where
|
||||
Model: ?Sized,
|
||||
GetScroller: FnMut(&mut Model) -> &mut scroll::Core,
|
||||
RequiredSize: FnMut(&mut Model, Vec2) -> Vec2,
|
||||
{
|
||||
if !needs_relayout {
|
||||
if let Some(cached) = get_scroller(model).try_cache(constraint) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt 1: try without scrollbars
|
||||
let (inner_size, size, scrollable) = sizes_when_scrolling(
|
||||
constraint,
|
||||
XY::new(false, false),
|
||||
strict,
|
||||
model,
|
||||
get_scroller,
|
||||
required_size,
|
||||
);
|
||||
|
||||
// If we need to add scrollbars, the available size will change.
|
||||
if scrollable.any() && get_scroller(model).get_show_scrollbars() {
|
||||
// Attempt 2: he wants to scroll? Sure!
|
||||
// Try again with some space for the scrollbar.
|
||||
let (inner_size, size, new_scrollable) = sizes_when_scrolling(
|
||||
constraint,
|
||||
scrollable,
|
||||
strict,
|
||||
model,
|
||||
get_scroller,
|
||||
required_size,
|
||||
);
|
||||
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, _) = sizes_when_scrolling(
|
||||
constraint,
|
||||
new_scrollable,
|
||||
strict,
|
||||
model,
|
||||
get_scroller,
|
||||
required_size,
|
||||
);
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn layout<Model, GetScroller, RequiredSize, Layout>(
|
||||
size: Vec2, needs_relayout: bool, model: &mut Model,
|
||||
mut get_scroller: GetScroller, mut required_size: RequiredSize,
|
||||
mut layout: Layout,
|
||||
) where
|
||||
Model: ?Sized,
|
||||
GetScroller: FnMut(&mut Model) -> &mut scroll::Core,
|
||||
RequiredSize: FnMut(&mut Model, Vec2) -> Vec2,
|
||||
Layout: FnMut(&mut Model, Vec2),
|
||||
{
|
||||
get_scroller(model).set_last_size(size);
|
||||
|
||||
// This is what we'd like
|
||||
let (inner_size, self_size) = sizes(
|
||||
size,
|
||||
true,
|
||||
needs_relayout,
|
||||
model,
|
||||
&mut get_scroller,
|
||||
&mut required_size,
|
||||
);
|
||||
|
||||
get_scroller(model).set_inner_size(inner_size);
|
||||
get_scroller(model).build_cache(self_size, size);
|
||||
|
||||
layout(model, inner_size);
|
||||
|
||||
get_scroller(model).update_offset();
|
||||
}
|
||||
|
||||
pub fn required_size<Model, GetScroller, RequiredSize>(
|
||||
constraint: Vec2, needs_relayout: bool, model: &mut Model,
|
||||
mut get_scroller: GetScroller, mut required_size: RequiredSize,
|
||||
) -> Vec2
|
||||
where
|
||||
Model: ?Sized,
|
||||
GetScroller: FnMut(&mut Model) -> &mut scroll::Core,
|
||||
RequiredSize: FnMut(&mut Model, Vec2) -> Vec2,
|
||||
{
|
||||
let (_, size) = sizes(
|
||||
constraint,
|
||||
false,
|
||||
needs_relayout,
|
||||
model,
|
||||
&mut get_scroller,
|
||||
&mut required_size,
|
||||
);
|
||||
|
||||
size
|
||||
}
|
||||
|
||||
pub fn on_event<Model, GetScroller, OnEvent, ImportantArea>(
|
||||
event: Event, model: &mut Model, mut get_scroller: GetScroller,
|
||||
mut on_event: OnEvent, mut important_area: ImportantArea,
|
||||
) -> EventResult
|
||||
where
|
||||
Model: ?Sized,
|
||||
GetScroller: FnMut(&mut Model) -> &mut scroll::Core,
|
||||
OnEvent: FnMut(&mut Model, Event) -> EventResult,
|
||||
ImportantArea: FnMut(&Model, Vec2) -> Rect,
|
||||
{
|
||||
let mut relative_event = event.clone();
|
||||
let inside = get_scroller(model).is_event_inside(&mut relative_event);
|
||||
let result = if inside {
|
||||
on_event(model, relative_event)
|
||||
} else {
|
||||
EventResult::Ignored
|
||||
};
|
||||
let inner_size = get_scroller(model).inner_size();
|
||||
let important = important_area(model, inner_size);
|
||||
get_scroller(model).on_inner_event(event, result, important)
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
use crate::event::{Event, EventResult};
|
||||
use crate::rect::Rect;
|
||||
|
||||
use crate::vec::Vec2;
|
||||
use crate::view::View;
|
||||
|
||||
/// Inner implementation for `ScrollCore::on_event`
|
||||
pub trait InnerOnEvent {
|
||||
/// Performs `View::on_event()`
|
||||
fn on_event(&mut self, event: Event) -> EventResult;
|
||||
|
||||
/// Performs `View::important_area()`
|
||||
fn important_area(&self, size: Vec2) -> Rect;
|
||||
}
|
||||
|
||||
impl<'a, V: View> InnerOnEvent for &'a mut V {
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
<V as View>::on_event(self, event)
|
||||
}
|
||||
fn important_area(&self, size: Vec2) -> Rect {
|
||||
<V as View>::important_area(self, size)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner implementation for `ScrollCore::draw()`
|
||||
/// Inner implementation for `ScrollCore::InnerLayout()`
|
||||
pub trait InnerLayout {
|
||||
/// Performs `View::layout()`
|
||||
fn layout(&mut self, size: Vec2);
|
||||
/// Performs `View::needs_relayout()`
|
||||
fn needs_relayout(&self) -> bool;
|
||||
/// Performs `View::required_size()`
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2;
|
||||
}
|
||||
|
||||
impl<'a, V: View> InnerLayout for &'a mut V {
|
||||
fn layout(&mut self, size: Vec2) {
|
||||
<V as View>::layout(self, size);
|
||||
}
|
||||
fn needs_relayout(&self) -> bool {
|
||||
<V as View>::needs_relayout(self)
|
||||
}
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||
<V as View>::required_size(self, constraint)
|
||||
}
|
||||
}
|
||||
|
||||
/// Inner implementation for `ScrollCore::required_size()`
|
||||
pub trait InnerRequiredSize {
|
||||
/// Performs `View::needs_relayout()`
|
||||
fn needs_relayout(&self) -> bool;
|
||||
/// Performs `View::required_size()`
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2;
|
||||
}
|
||||
|
||||
impl<V: View> InnerRequiredSize for &mut V {
|
||||
fn needs_relayout(&self) -> bool {
|
||||
<V as View>::needs_relayout(self)
|
||||
}
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||
<V as View>::required_size(self, constraint)
|
||||
}
|
||||
}
|
@ -5,7 +5,7 @@ use crate::event::{
|
||||
use crate::menu::{MenuItem, MenuTree};
|
||||
use crate::rect::Rect;
|
||||
use crate::vec::Vec2;
|
||||
use crate::view::scroll::{InnerOnEvent, ScrollBase};
|
||||
use crate::view::scroll;
|
||||
use crate::view::{Position, View};
|
||||
use crate::views::OnEventView;
|
||||
use crate::Cursive;
|
||||
@ -19,11 +19,23 @@ use unicode_width::UnicodeWidthStr;
|
||||
pub struct MenuPopup {
|
||||
menu: Rc<MenuTree>,
|
||||
focus: usize,
|
||||
scrollbase: ScrollBase,
|
||||
scroll_core: scroll::Core,
|
||||
align: Align,
|
||||
on_dismiss: Option<Callback>,
|
||||
on_action: Option<Callback>,
|
||||
last_size: Vec2,
|
||||
}
|
||||
|
||||
// The `scroll::Scroller` trait is used to weave the borrow phases.
|
||||
//
|
||||
// TODO: use some macro to auto-generate this.
|
||||
impl scroll::Scroller for MenuPopup {
|
||||
fn get_scroller(&self) -> &scroll::Core {
|
||||
&self.scroll_core
|
||||
}
|
||||
|
||||
fn get_scroller_mut(&mut self) -> &mut scroll::Core {
|
||||
&mut self.scroll_core
|
||||
}
|
||||
}
|
||||
|
||||
impl MenuPopup {
|
||||
@ -32,11 +44,10 @@ impl MenuPopup {
|
||||
MenuPopup {
|
||||
menu,
|
||||
focus: 0,
|
||||
scrollbase: ScrollBase::new(),
|
||||
scroll_core: scroll::Core::new(),
|
||||
align: Align::top_left(),
|
||||
on_dismiss: None,
|
||||
on_action: None,
|
||||
last_size: Vec2::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,6 +63,11 @@ impl MenuPopup {
|
||||
self.with(|s| s.set_focus(focus))
|
||||
}
|
||||
|
||||
/// Returns the position of the currently focused child.
|
||||
pub fn get_focus(&self) -> usize {
|
||||
self.focus
|
||||
}
|
||||
|
||||
fn item_width(item: &MenuItem) -> usize {
|
||||
match *item {
|
||||
MenuItem::Delimiter => 1,
|
||||
@ -107,6 +123,193 @@ impl MenuPopup {
|
||||
pub fn set_on_action<F: 'static + Fn(&mut Cursive)>(&mut self, f: F) {
|
||||
self.on_action = Some(Callback::from_fn(f));
|
||||
}
|
||||
|
||||
fn scroll_up(&mut self, mut n: usize, cycle: bool) {
|
||||
while n > 0 {
|
||||
if self.focus > 0 {
|
||||
self.focus -= 1;
|
||||
} else if cycle {
|
||||
self.focus = self.menu.children.len() - 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if !self.menu.children[self.focus].is_delimiter() {
|
||||
n -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_down(&mut self, mut n: usize, cycle: bool) {
|
||||
while n > 0 {
|
||||
if self.focus + 1 < self.menu.children.len() {
|
||||
self.focus += 1;
|
||||
} else if cycle {
|
||||
self.focus = 0;
|
||||
} else {
|
||||
// Stop if we're at the bottom.
|
||||
break;
|
||||
}
|
||||
|
||||
if !self.menu.children[self.focus].is_delimiter() {
|
||||
n -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn submit(&mut self) -> EventResult {
|
||||
match self.menu.children[self.focus] {
|
||||
MenuItem::Leaf(_, ref cb) => {
|
||||
let cb = cb.clone();
|
||||
let action_cb = self.on_action.clone();
|
||||
EventResult::with_cb(move |s| {
|
||||
// Remove ourselves from the face of the earth
|
||||
s.pop_layer();
|
||||
// If we had prior orders, do it now.
|
||||
if let Some(ref action_cb) = action_cb {
|
||||
action_cb.clone()(s);
|
||||
}
|
||||
// And transmit his last words.
|
||||
cb.clone()(s);
|
||||
})
|
||||
}
|
||||
MenuItem::Subtree(_, ref tree) => self.make_subtree_cb(tree),
|
||||
_ => unreachable!("Delimiters cannot be submitted."),
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self) -> EventResult {
|
||||
let dismiss_cb = self.on_dismiss.clone();
|
||||
EventResult::with_cb(move |s| {
|
||||
if let Some(ref cb) = dismiss_cb {
|
||||
cb.clone()(s);
|
||||
}
|
||||
s.pop_layer();
|
||||
})
|
||||
}
|
||||
|
||||
fn make_subtree_cb(&self, tree: &Rc<MenuTree>) -> EventResult {
|
||||
let tree = Rc::clone(tree);
|
||||
let max_width = 4 + self
|
||||
.menu
|
||||
.children
|
||||
.iter()
|
||||
.map(MenuPopup::item_width)
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
let offset = Vec2::new(max_width, self.focus);
|
||||
let action_cb = self.on_action.clone();
|
||||
|
||||
EventResult::with_cb(move |s| {
|
||||
let action_cb = action_cb.clone();
|
||||
s.screen_mut().add_layer_at(
|
||||
Position::parent(offset),
|
||||
OnEventView::new(MenuPopup::new(Rc::clone(&tree)).on_action(
|
||||
move |s| {
|
||||
// This will happen when the subtree popup
|
||||
// activates something;
|
||||
// First, remove ourselve.
|
||||
s.pop_layer();
|
||||
if let Some(ref action_cb) = action_cb {
|
||||
action_cb.clone()(s);
|
||||
}
|
||||
},
|
||||
))
|
||||
.on_event(Key::Left, |s| {
|
||||
s.pop_layer();
|
||||
}),
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
/// Handle an event for the content.
|
||||
///
|
||||
/// Here the event has already been relativized. This means `y=0` points to the first item.
|
||||
fn inner_on_event(&mut self, event: Event) -> EventResult {
|
||||
match event {
|
||||
Event::Key(Key::Up) => self.scroll_up(1, true),
|
||||
Event::Key(Key::PageUp) => self.scroll_up(5, false),
|
||||
Event::Key(Key::Down) => self.scroll_down(1, true),
|
||||
Event::Key(Key::PageDown) => self.scroll_down(5, false),
|
||||
|
||||
Event::Key(Key::Home) => self.focus = 0,
|
||||
Event::Key(Key::End) => {
|
||||
self.focus = self.menu.children.len().saturating_sub(1)
|
||||
}
|
||||
|
||||
Event::Key(Key::Right)
|
||||
if self.menu.children[self.focus].is_subtree() =>
|
||||
{
|
||||
return match self.menu.children[self.focus] {
|
||||
MenuItem::Subtree(_, ref tree) => {
|
||||
self.make_subtree_cb(tree)
|
||||
}
|
||||
_ => unreachable!("Child is a subtree"),
|
||||
};
|
||||
}
|
||||
Event::Key(Key::Enter)
|
||||
if !self.menu.children[self.focus].is_delimiter() =>
|
||||
{
|
||||
return self.submit();
|
||||
}
|
||||
Event::Mouse {
|
||||
event: MouseEvent::Press(_),
|
||||
position,
|
||||
offset,
|
||||
} => {
|
||||
// eprintln!("Position: {:?} / {:?}", position, offset);
|
||||
if let Some(position) = position.checked_sub(offset) {
|
||||
// Now `position` is relative to the top-left of the content.
|
||||
let focus = position.y;
|
||||
if !self.menu.children[focus].is_delimiter() {
|
||||
self.focus = focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse {
|
||||
event: MouseEvent::Release(MouseButton::Left),
|
||||
position,
|
||||
offset,
|
||||
} if !self.menu.children[self.focus].is_delimiter()
|
||||
&& position
|
||||
.checked_sub(offset)
|
||||
.map(|position| position.y == self.focus)
|
||||
.unwrap_or(false) =>
|
||||
{
|
||||
return self.submit();
|
||||
}
|
||||
Event::Key(Key::Esc) => {
|
||||
return self.dismiss();
|
||||
}
|
||||
|
||||
_ => return EventResult::Ignored,
|
||||
}
|
||||
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
/// Compute the required size for the content.
|
||||
fn inner_required_size(&mut self, _req: Vec2) -> Vec2 {
|
||||
let w = 2 + self
|
||||
.menu
|
||||
.children
|
||||
.iter()
|
||||
.map(Self::item_width)
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
|
||||
let h = self.menu.children.len();
|
||||
|
||||
Vec2::new(w, h)
|
||||
}
|
||||
|
||||
fn inner_important_area(&self, size: Vec2) -> Rect {
|
||||
if self.menu.is_empty() {
|
||||
return Rect::from((0, 0));
|
||||
}
|
||||
|
||||
Rect::from_size((0, self.focus), (size.x, 1))
|
||||
}
|
||||
}
|
||||
|
||||
impl View for MenuPopup {
|
||||
@ -121,14 +324,19 @@ impl View for MenuPopup {
|
||||
let printer = &printer.offset((0, offset));
|
||||
|
||||
// Start with a box
|
||||
printer.print_box(Vec2::new(0, 0), printer.size, false);
|
||||
scroll::draw_box_frame(
|
||||
self,
|
||||
&printer,
|
||||
|s, y| s.menu.children[y].is_delimiter(),
|
||||
|_s, _x| false,
|
||||
);
|
||||
|
||||
// We're giving it a reduced size because of borders.
|
||||
let printer = printer.shrinked_centered((2, 2));
|
||||
|
||||
self.scrollbase.draw(&printer, |printer, i| {
|
||||
printer.with_selection(i == self.focus, |printer| {
|
||||
let item = &self.menu.children[i];
|
||||
scroll::draw_lines(self, &printer, |s, printer, i| {
|
||||
printer.with_selection(i == s.focus, |printer| {
|
||||
let item = &s.menu.children[i];
|
||||
match *item {
|
||||
MenuItem::Delimiter => {
|
||||
// printer.print_hdelim((0, 0), printer.size.x)
|
||||
@ -159,43 +367,38 @@ impl View for MenuPopup {
|
||||
// We can't really shrink our items here, so it's not flexible.
|
||||
|
||||
// 2 is the padding
|
||||
let w = 2 + self
|
||||
.menu
|
||||
.children
|
||||
.iter()
|
||||
.map(Self::item_width)
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
|
||||
let h = self.menu.children.len();
|
||||
|
||||
let res = self
|
||||
.scrollbase
|
||||
.required_size(req.saturating_sub((2, 2)), |_| Vec2::new(w, h))
|
||||
+ (2, 2);
|
||||
|
||||
res
|
||||
scroll::required_size(
|
||||
self,
|
||||
req.saturating_sub((2, 2)),
|
||||
true,
|
||||
Self::inner_required_size,
|
||||
) + (2, 2)
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
match self.scrollbase.on_event(
|
||||
match scroll::on_event(
|
||||
self,
|
||||
event.relativized((1, 1)),
|
||||
OnEvent {
|
||||
focus: &mut self.focus,
|
||||
menu: &self.menu,
|
||||
on_dismiss: &self.on_dismiss,
|
||||
on_action: &self.on_action,
|
||||
last_size: &self.last_size,
|
||||
},
|
||||
Self::inner_on_event,
|
||||
Self::inner_important_area,
|
||||
) {
|
||||
EventResult::Ignored => {
|
||||
// Check back the non-relativized event now
|
||||
if let Event::Mouse {
|
||||
event: MouseEvent::Press(_),
|
||||
position,
|
||||
offset,
|
||||
} = event
|
||||
{
|
||||
if !position.fits_in_rect(offset, self.last_size) {
|
||||
// Mouse press will be ignored if they are outside of the content.
|
||||
// They can be on the border, or entirely outside of the popup.
|
||||
|
||||
// Mouse clicks outside of the popup should dismiss it.
|
||||
if !position.fits_in_rect(
|
||||
offset,
|
||||
self.scroll_core.last_size() + (2, 2),
|
||||
) {
|
||||
let dismiss_cb = self.on_dismiss.clone();
|
||||
return EventResult::with_cb(move |s| {
|
||||
if let Some(ref cb) = dismiss_cb {
|
||||
@ -213,210 +416,21 @@ impl View for MenuPopup {
|
||||
}
|
||||
|
||||
fn layout(&mut self, size: Vec2) {
|
||||
self.last_size = size;
|
||||
|
||||
let children = &self.menu.children;
|
||||
|
||||
self.scrollbase.layout(size.saturating_sub((2, 2)), |size| {
|
||||
Vec2::new(size.x, children.len())
|
||||
});
|
||||
scroll::layout(
|
||||
self,
|
||||
size.saturating_sub((2, 2)),
|
||||
true,
|
||||
|_s, _size| (),
|
||||
Self::inner_required_size,
|
||||
);
|
||||
}
|
||||
|
||||
fn important_area(&self, size: Vec2) -> Rect {
|
||||
if self.menu.is_empty() {
|
||||
return Rect::from((0, 0));
|
||||
}
|
||||
|
||||
Rect::from_size((0, self.focus), (size.x, 1))
|
||||
}
|
||||
}
|
||||
|
||||
struct OnEvent<'a> {
|
||||
focus: &'a mut usize,
|
||||
menu: &'a Rc<MenuTree>,
|
||||
on_dismiss: &'a Option<Callback>,
|
||||
on_action: &'a Option<Callback>,
|
||||
last_size: &'a Vec2,
|
||||
}
|
||||
|
||||
impl<'a> OnEvent<'a> {
|
||||
fn scroll_up(&mut self, mut n: usize, cycle: bool) {
|
||||
while n > 0 {
|
||||
if *self.focus > 0 {
|
||||
*self.focus -= 1;
|
||||
} else if cycle {
|
||||
*self.focus = self.menu.children.len() - 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
if !self.menu.children[*self.focus].is_delimiter() {
|
||||
n -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_down(&mut self, mut n: usize, cycle: bool) {
|
||||
while n > 0 {
|
||||
if *self.focus + 1 < self.menu.children.len() {
|
||||
*self.focus += 1;
|
||||
} else if cycle {
|
||||
*self.focus = 0;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if !self.menu.children[*self.focus].is_delimiter() {
|
||||
n -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn submit(&mut self) -> EventResult {
|
||||
match self.menu.children[*self.focus] {
|
||||
MenuItem::Leaf(_, ref cb) => {
|
||||
let cb = cb.clone();
|
||||
let action_cb = self.on_action.clone();
|
||||
EventResult::with_cb(move |s| {
|
||||
// Remove ourselves from the face of the earth
|
||||
s.pop_layer();
|
||||
// If we had prior orders, do it now.
|
||||
if let Some(ref action_cb) = action_cb {
|
||||
action_cb.clone()(s);
|
||||
}
|
||||
// And transmit his last words.
|
||||
cb.clone()(s);
|
||||
})
|
||||
}
|
||||
MenuItem::Subtree(_, ref tree) => self.make_subtree_cb(tree),
|
||||
_ => panic!("No delimiter here"),
|
||||
}
|
||||
}
|
||||
|
||||
fn dismiss(&mut self) -> EventResult {
|
||||
let dismiss_cb = self.on_dismiss.clone();
|
||||
EventResult::with_cb(move |s| {
|
||||
if let Some(ref cb) = dismiss_cb {
|
||||
cb.clone()(s);
|
||||
}
|
||||
s.pop_layer();
|
||||
})
|
||||
}
|
||||
|
||||
fn make_subtree_cb(&self, tree: &Rc<MenuTree>) -> EventResult {
|
||||
let tree = Rc::clone(tree);
|
||||
let max_width = 4 + self
|
||||
.menu
|
||||
.children
|
||||
.iter()
|
||||
.map(MenuPopup::item_width)
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
let offset = Vec2::new(max_width, *self.focus);
|
||||
let action_cb = self.on_action.clone();
|
||||
|
||||
EventResult::with_cb(move |s| {
|
||||
let action_cb = action_cb.clone();
|
||||
s.screen_mut().add_layer_at(
|
||||
Position::parent(offset),
|
||||
OnEventView::new(MenuPopup::new(Rc::clone(&tree)).on_action(
|
||||
move |s| {
|
||||
// This will happen when the subtree popup
|
||||
// activates something;
|
||||
// First, remove ourselve.
|
||||
s.pop_layer();
|
||||
if let Some(ref action_cb) = action_cb {
|
||||
action_cb.clone()(s);
|
||||
}
|
||||
},
|
||||
))
|
||||
.on_event(Key::Left, |s| {
|
||||
s.pop_layer();
|
||||
}),
|
||||
);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> InnerOnEvent for OnEvent<'a> {
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
match event {
|
||||
Event::Key(Key::Up) => self.scroll_up(1, true),
|
||||
Event::Key(Key::PageUp) => self.scroll_up(5, false),
|
||||
Event::Key(Key::Down) => self.scroll_down(1, true),
|
||||
Event::Key(Key::PageDown) => self.scroll_down(5, false),
|
||||
|
||||
Event::Key(Key::Home) => *self.focus = 0,
|
||||
Event::Key(Key::End) => {
|
||||
*self.focus = self.menu.children.len().saturating_sub(1)
|
||||
}
|
||||
|
||||
Event::Key(Key::Right)
|
||||
if self.menu.children[*self.focus].is_subtree() =>
|
||||
{
|
||||
return match self.menu.children[*self.focus] {
|
||||
MenuItem::Subtree(_, ref tree) => {
|
||||
self.make_subtree_cb(tree)
|
||||
}
|
||||
_ => panic!("Not a subtree???"),
|
||||
};
|
||||
}
|
||||
Event::Key(Key::Enter)
|
||||
if !self.menu.children[*self.focus].is_delimiter() =>
|
||||
{
|
||||
return self.submit();
|
||||
}
|
||||
Event::Mouse {
|
||||
event: MouseEvent::Press(_),
|
||||
position,
|
||||
offset,
|
||||
} if position.fits_in_rect(
|
||||
offset,
|
||||
self.last_size.saturating_sub((2, 2)),
|
||||
) =>
|
||||
{
|
||||
// eprintln!("Position: {:?} / {:?}", position, offset);
|
||||
// eprintln!("Last size: {:?}", self.last_size);
|
||||
let inner_size = self.last_size.saturating_sub((2, 2));
|
||||
if let Some(position) = position.checked_sub(offset) {
|
||||
// `position` is not relative to the content
|
||||
// (It's inside the border)
|
||||
if position < inner_size {
|
||||
let focus = position.y;
|
||||
if !self.menu.children[focus].is_delimiter() {
|
||||
*self.focus = focus;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::Mouse {
|
||||
event: MouseEvent::Release(MouseButton::Left),
|
||||
position,
|
||||
offset,
|
||||
} if !self.menu.children[*self.focus].is_delimiter()
|
||||
&& position
|
||||
.checked_sub(offset)
|
||||
.map(|position| {
|
||||
position < self.last_size.saturating_sub((2, 2))
|
||||
&& (position.y == *self.focus)
|
||||
})
|
||||
.unwrap_or(false) =>
|
||||
{
|
||||
return self.submit();
|
||||
}
|
||||
Event::Key(Key::Esc)
|
||||
| Event::Mouse {
|
||||
event: MouseEvent::Press(_),
|
||||
..
|
||||
} => {
|
||||
return self.dismiss();
|
||||
}
|
||||
|
||||
_ => return EventResult::Ignored,
|
||||
}
|
||||
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
fn important_area(&self, size: Vec2) -> Rect {
|
||||
Rect::from_size((0, *self.focus), (size.x, 1))
|
||||
scroll::important_area(
|
||||
self,
|
||||
size.saturating_sub((2, 2)),
|
||||
Self::inner_important_area,
|
||||
)
|
||||
.with(|area| area.offset((1, 1)))
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,26 @@
|
||||
use crate::direction::Direction;
|
||||
use crate::event::{AnyCb, Event, EventResult};
|
||||
use crate::rect::Rect;
|
||||
use crate::view::{scroll, ScrollStrategy, Selector, View};
|
||||
use crate::{Printer, Vec2, With};
|
||||
use crate::{Printer, Rect, Vec2, With};
|
||||
|
||||
/// Wraps a view in a scrollable area.
|
||||
pub struct ScrollView<V> {
|
||||
/// The wrapped view.
|
||||
inner: V,
|
||||
|
||||
core: scroll::ScrollCore,
|
||||
core: scroll::Core,
|
||||
}
|
||||
|
||||
impl<V> scroll::Scroller for ScrollView<V>
|
||||
where
|
||||
V: View,
|
||||
{
|
||||
fn get_scroller(&self) -> &scroll::Core {
|
||||
&self.core
|
||||
}
|
||||
fn get_scroller_mut(&mut self) -> &mut scroll::Core {
|
||||
&mut self.core
|
||||
}
|
||||
}
|
||||
|
||||
impl<V> ScrollView<V>
|
||||
@ -20,7 +31,7 @@ where
|
||||
pub fn new(inner: V) -> Self {
|
||||
ScrollView {
|
||||
inner,
|
||||
core: scroll::ScrollCore::new(),
|
||||
core: scroll::Core::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,23 +145,39 @@ where
|
||||
V: View,
|
||||
{
|
||||
fn draw(&self, printer: &Printer<'_, '_>) {
|
||||
self.core.draw(printer, |printer| self.inner.draw(printer));
|
||||
scroll::draw(self, printer, |s, p| s.inner.draw(p));
|
||||
}
|
||||
|
||||
fn on_event(&mut self, event: Event) -> EventResult {
|
||||
self.core.on_event(event, &mut self.inner)
|
||||
scroll::on_event(
|
||||
self,
|
||||
event,
|
||||
|s, e| s.inner.on_event(e),
|
||||
|s, si| s.inner.important_area(si),
|
||||
)
|
||||
}
|
||||
|
||||
fn layout(&mut self, size: Vec2) {
|
||||
self.core.layout(size, &mut self.inner);
|
||||
scroll::layout(
|
||||
self,
|
||||
size,
|
||||
self.inner.needs_relayout(),
|
||||
|s, si| s.inner.layout(si),
|
||||
|s, c| s.inner.required_size(c),
|
||||
);
|
||||
}
|
||||
|
||||
fn needs_relayout(&self) -> bool {
|
||||
self.core.needs_relayout(|| self.inner.needs_relayout())
|
||||
self.core.needs_relayout() || self.inner.needs_relayout()
|
||||
}
|
||||
|
||||
fn required_size(&mut self, constraint: Vec2) -> Vec2 {
|
||||
self.core.required_size(constraint, &mut self.inner)
|
||||
scroll::required_size(
|
||||
self,
|
||||
constraint,
|
||||
self.inner.needs_relayout(),
|
||||
|s, c| s.inner.required_size(c),
|
||||
)
|
||||
}
|
||||
|
||||
fn call_on_any<'a>(&mut self, selector: &Selector<'_>, cb: AnyCb<'a>) {
|
||||
@ -162,8 +189,10 @@ where
|
||||
}
|
||||
|
||||
fn take_focus(&mut self, source: Direction) -> bool {
|
||||
let inner = &mut self.inner;
|
||||
self.core
|
||||
.take_focus(source, |source| inner.take_focus(source))
|
||||
self.inner.take_focus(source) || self.core.is_scrolling().any()
|
||||
}
|
||||
|
||||
fn important_area(&self, size: Vec2) -> Rect {
|
||||
scroll::important_area(self, size, |s, si| s.inner.important_area(si))
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user