diff --git a/src/cursive.rs b/src/cursive.rs index b4570bf..347b573 100644 --- a/src/cursive.rs +++ b/src/cursive.rs @@ -224,7 +224,7 @@ impl Cursive { /// 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) { self.add_layer( views::Dialog::around(views::ScrollView::new(views::IdView::new( diff --git a/src/logger.rs b/src/logger.rs index ed3be5a..6e04813 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -52,7 +52,8 @@ impl log::Log for CursiveLogger { /// /// 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() { // TODO: Configure the deque size? LOGS.lock().unwrap().reserve(1_000); diff --git a/src/view/mod.rs b/src/view/mod.rs index a744ae4..380fcde 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -51,7 +51,8 @@ mod view_trait; // Helper bases mod boxable; mod identifiable; -mod scroll; +pub mod scroll; +mod scroll_base; mod scrollable; mod into_boxed_view; @@ -63,7 +64,7 @@ pub use self::identifiable::Identifiable; pub use self::into_boxed_view::IntoBoxedView; pub use self::margins::Margins; 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::size_cache::SizeCache; pub use self::size_constraint::SizeConstraint; diff --git a/src/view/scroll.rs b/src/view/scroll.rs index d183db4..eada83c 100644 --- a/src/view/scroll.rs +++ b/src/view/scroll.rs @@ -1,316 +1,855 @@ -use crate::div::div_up; +//! Core mechanisms to implement scrolling. +//! +//! This module defines [`ScrollCore`](crate::view::scroll::ScrollCore) and related traits. +//! +//! [`ScrollView`](crate::views::ScrollView) may be an easier way to add scrolling to an existing view. +use std::cmp::min; + +use crate::direction::{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::vec::Vec2; -use crate::Printer; -use std::cmp::{max, min}; +use crate::view::{ScrollStrategy, Selector, SizeCache, View}; +use crate::with::With; +use crate::XY; -/// Provide scrolling functionalities to a 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 { + ::on_event(self, event) + } + fn important_area(&self, size: Vec2) -> Rect { + ::important_area(self, size) + } +} + +/// Inner implementation for `ScrollCore::draw()` +pub trait InnerDraw { + /// Performs `View::draw()` + fn draw(&self, printer: &Printer<'_, '_>); +} + +impl<'a, V: View> InnerDraw for &'a V { + fn draw(&self, printer: &Printer<'_, '_>) { + ::draw(self, printer); + } +} + +/// 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; +} + +struct Layout2Sizes<'a, I> { + inner: &'a mut I, +} + +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) + } +} + +impl<'a, V: View> InnerLayout for &'a mut V { + fn layout(&mut self, size: Vec2) { + ::layout(self, size); + } + fn needs_relayout(&self) -> bool { + ::needs_relayout(self) + } + fn required_size(&mut self, constraint: Vec2) -> Vec2 { + ::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 InnerRequiredSize for &mut V { + fn needs_relayout(&self) -> bool { + ::needs_relayout(self) + } + fn required_size(&mut self, constraint: Vec2) -> Vec2 { + ::required_size(self, 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) + } +} + +trait InnerSizes { + fn needs_relayout(&self) -> bool; + fn required_size(&mut self, constraint: Vec2) -> Vec2; +} + +impl InnerSizes for &mut I { + fn needs_relayout(&self) -> bool { + ::needs_relayout(self) + } + fn required_size(&mut self, constraint: Vec2) -> Vec2 { + ::required_size(self, constraint) + } +} + +/// Core system for scrolling views. /// -/// 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, +/// See also [`ScrollView`](crate::views::ScrollView). +pub struct ScrollCore { + /// This is the size the child thinks we're giving him. + inner_size: Vec2, - /// Content height - pub content_height: usize, - - /// Number of lines displayed - pub view_height: usize, - - /// Padding for the scrollbar + /// Offset into the inner view. /// - /// 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, + /// Our `(0,0)` will be inner's `offset` + offset: Vec2, - /// Blank between the text and the scrollbar. - pub right_padding: usize, + /// 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, + + /// 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. - pub thumb_grab: Option, + thumb_grab: Option<(Orientation, usize)>, + + /// We keep the cache here so it can be busted when we change the content. + size_cache: Option>, + + /// Defines how to update the offset when the view size changes. + scroll_strategy: ScrollStrategy, } -/// 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 { +impl Default for ScrollCore { fn default() -> Self { - ScrollStrategy::KeepRow + Self::new() } } -impl ScrollBase { - /// Creates a new, uninitialized scrollbar. +impl ScrollCore { + /// Creates a new `ScrollCore`. pub fn new() -> Self { - ScrollBase { - start_line: 0, - content_height: 0, - view_height: 0, - scrollbar_offset: 0, - right_padding: 1, + ScrollCore { + inner_size: Vec2::zero(), + 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, } } - /// 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 - } + /// Performs the `View::draw()` operation. + pub fn draw(&self, printer: &Printer<'_, '_>, inner: I) { + // Draw scrollbar? + let scrolling = self.is_scrolling(); - /// 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 - } + let lengths = self.scrollbar_thumb_lengths(); + let offsets = self.scrollbar_thumb_offsets(lengths); - /// 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; + let line_c = XY::new("-", "|"); - // eprintln!("Setting heights: {} in {}", content_height, view_height); - - if self.scrollable() { - self.start_line = - min(self.start_line, self.content_height - self.view_height); + let color = if printer.focused { + ColorStyle::highlight() } else { - self.start_line = 0; - } - } + ColorStyle::highlight_inactive() + }; - /// Returns `TRUE` if the view needs to scroll. - pub fn scrollable(&self) -> bool { - self.view_height < self.content_height - } + let size = self.available_size(); - /// Returns `TRUE` unless we are at the top. - pub fn can_scroll_up(&self) -> bool { - self.start_line > 0 - } + // 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); - /// Returns `TRUE` unless we are at the bottom. - pub fn can_scroll_down(&self) -> bool { - self.start_line + self.view_height < self.content_height - } + printer.print_line(orientation, start, size, c); - /// 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, + 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, + ); + }); + }, ); - } - /// 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; + // Draw the X between the two scrollbars. + if scrolling.both() { + printer.print(printer.size.saturating_sub((1, 1)), "╳"); } - // 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); + // Draw content + let printer = printer + .cropped(size) + .content_offset(self.offset) + .inner_size(self.inner_size); - if position.y >= thumb_y && position.y < thumb_y + height { - // Grabbed! - self.thumb_grab = Some(position.y - thumb_y); + inner.draw(&printer); + } + + /// Performs `View::on_event()` + pub fn on_event( + &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 { + 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 { - // Just jump a bit... - self.thumb_grab = Some((height - 1) / 2); - // eprintln!("Grabbed at {}", self.thumb_grab); - self.drag(position); - } + // For key events, assume it's inside by default. + true + }; - 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 + }; - /// 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); + 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 = 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 + } } } - /// Returns `true` if we are in the process of dragging the scroll thumb. - pub fn is_dragging(&self) -> bool { - self.thumb_grab.is_some() + /// Performs `View::layout()` + pub fn layout(&mut self, size: Vec2, mut inner: I) { + // Size is final now, negociations are over. + self.last_size = size; + + // This is what we'd like + let (inner_size, self_size) = + self.sizes(size, true, Layout2Sizes { inner: &mut inner }); + + self.inner_size = inner_size; + + self.size_cache = Some(SizeCache::build(self_size, size)); + + 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(); + } + + /// Performs `View::needs_relayout()` + pub fn needs_relayout(&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( + &mut self, constraint: Vec2, mut inner: I, + ) -> Vec2 { + let (_, size) = self.sizes( + constraint, + false, + Required2Sizes { inner: &mut inner }, + ); + + size + } + + /// Performs `View::call_on_any()` + pub fn call_on_any<'a, F>( + &mut self, selector: &Selector<'_>, cb: AnyCb<'a>, + inner_call_on_any: F, + ) where + F: FnOnce(&Selector, AnyCb), + { + inner_call_on_any(selector, cb) + } + + /// Performs `View::focus_view()` + pub fn focus_view( + &mut self, selector: &Selector<'_>, inner_focus_view: F, + ) -> Result<(), ()> + where + F: FnOnce(&Selector) -> Result<(), ()>, + { + inner_focus_view(selector) + } + + /// Performs `View::take_focus()` + pub fn take_focus( + &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()) + } + + /// Defines the way scrolling is adjusted on content or size change. + /// + /// The scroll strategy defines how the scrolling position is adjusted + /// when the size of the view or the content change. + /// + /// It is reset to `ScrollStrategy::KeepRow` whenever the user scrolls + /// manually. + pub fn set_scroll_strategy(&mut self, strategy: ScrollStrategy) { + self.scroll_strategy = strategy; + self.adjust_scroll(); + } + + /// Defines the way scrolling is adjusted on content or size change. + /// + /// Chainable variant. + pub fn scroll_strategy(self, strategy: ScrollStrategy) -> Self { + self.with(|s| s.set_scroll_strategy(strategy)) + } + + /// Control whether scroll bars are visibile. + /// + /// Defaults to `true`. + pub fn set_show_scrollbars(&mut self, show_scrollbars: bool) { + self.show_scrollbars = show_scrollbars; + } + + /// Control whether scroll bars are visibile. + /// + /// Chainable variant + pub fn show_scrollbars(self, show_scrollbars: bool) -> Self { + self.with(|s| s.set_show_scrollbars(show_scrollbars)) + } + + /// Sets the scroll offset to the given value + pub fn set_offset(&mut self, offset: S) + where + S: Into, + { + let max_offset = self.inner_size.saturating_sub(self.available_size()); + self.offset = offset.into().or_min(max_offset); + } + + /// Controls whether this view can scroll vertically. + /// + /// Defaults to `true`. + pub fn set_scroll_y(&mut self, enabled: bool) { + self.enabled.y = enabled; + self.invalidate_cache(); + } + + /// Controls whether this view can scroll horizontally. + /// + /// Defaults to `false`. + pub fn set_scroll_x(&mut self, enabled: bool) { + self.enabled.x = enabled; + self.invalidate_cache(); + } + + /// Controls whether this view can scroll vertically. + /// + /// Defaults to `true`. + /// + /// Chainable variant. + pub fn scroll_y(self, enabled: bool) -> Self { + self.with(|s| s.set_scroll_y(enabled)) + } + + /// Controls whether this view can scroll horizontally. + /// + /// Defaults to `false`. + /// + /// Chainable variant. + pub fn scroll_x(self, enabled: bool) -> Self { + self.with(|s| s.set_scroll_x(enabled)) + } + + /// 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)); + } + + /// Programmatically scroll to the bottom of the view. + pub fn scroll_to_bottom(&mut self) { + let max_y = self.inner_size.saturating_sub(self.available_size()).y; + let curr_x = self.offset.x; + self.set_offset((curr_x, max_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)); + } + + /// 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.available_size()).x; + 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 { + self.inner_size.zip_map(self.last_size, |i, s| i > s) } /// Stops grabbing the scrollbar. - pub fn release_grab(&mut self) { + fn release_grab(&mut self) { self.thumb_grab = None; } - /// Draws the scroll bar and the content using the given drawer. + /// Returns the size taken by the scrollbars. /// - /// `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. + /// Will be zero in axis where we're not scrolling. /// - /// # 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(&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) + /// 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 { - printer.size.x + 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, strict: bool, + inner: &mut I, + ) -> (Vec2, Vec2, XY) { + // 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 }; - 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, + // 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, 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), ); } - // 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); + // Attempt 1: try without scrollbars + let (inner_size, size, scrollable) = self.sizes_when_scrolling( + constraint, + XY::new(false, false), + strict, + &mut inner, + ); - let color = if printer.focused { - ColorStyle::highlight() + // 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) } else { - ColorStyle::highlight_inactive() - }; + // 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, + ); - 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, "▒"); - }); + // 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) } } - /// 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) + 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)) } - /// 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) + 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 } - /// 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) + /// 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 => (), + } } } diff --git a/src/view/scroll_base.rs b/src/view/scroll_base.rs new file mode 100644 index 0000000..d183db4 --- /dev/null +++ b/src/view/scroll_base.rs @@ -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, +} + +/// 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(&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) + } +} diff --git a/src/view/scrollable.rs b/src/view/scrollable.rs index de70d61..0dcc586 100644 --- a/src/view/scrollable.rs +++ b/src/view/scrollable.rs @@ -3,7 +3,7 @@ use crate::views::ScrollView; /// Makes a view wrappable in a [`ScrollView`]. /// -/// [`ScrollView`]: ::views::ScrollView +/// [`ScrollView`]: crate::views::ScrollView pub trait Scrollable: View + Sized { /// Wraps `self` in a `ScrollView`. fn scrollable(self) -> ScrollView { diff --git a/src/views/scroll_view.rs b/src/views/scroll_view.rs index dc47e42..b19f7ce 100644 --- a/src/views/scroll_view.rs +++ b/src/views/scroll_view.rs @@ -1,55 +1,15 @@ -use std::cmp::min; - -use crate::direction::{Direction, Orientation}; -use crate::event::{AnyCb, Event, EventResult, Key, MouseButton, MouseEvent}; +use crate::direction::Direction; +use crate::event::{AnyCb, Event, EventResult}; use crate::rect::Rect; -use crate::theme::ColorStyle; -use crate::view::{ScrollStrategy, Selector, SizeCache, View}; -use crate::{Printer, Vec2, With, XY}; +use crate::view::{scroll, ScrollStrategy, Selector, View}; +use crate::{Printer, Vec2, With}; /// Wraps a view in a scrollable area. pub struct ScrollView { /// The wrapped view. inner: V, - /// This is the size the child thinks we're giving him. - inner_size: Vec2, - - /// Offset into the inner view. - /// - /// Our `(0,0)` will be inner's `offset` - offset: Vec2, - - /// What was our own size last time we checked. - /// - /// This includes scrollbars, if any. - last_size: Vec2, - - /// Are we scrollable in each direction? - enabled: XY, - - /// 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>, - - /// Defines how to update the offset when the view size changes. - scroll_strategy: ScrollStrategy, + core: scroll::ScrollCore, } impl ScrollView @@ -60,21 +20,13 @@ where pub fn new(inner: V) -> Self { ScrollView { inner, - inner_size: Vec2::zero(), - 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, + core: scroll::ScrollCore::new(), } } /// Returns the viewport in the inner content. 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. @@ -85,8 +37,7 @@ where /// It is reset to `ScrollStrategy::KeepRow` whenever the user scrolls /// manually. pub fn set_scroll_strategy(&mut self, strategy: ScrollStrategy) { - self.scroll_strategy = strategy; - self.adjust_scroll(); + self.core.set_scroll_strategy(strategy); } /// Defines the way scrolling is adjusted on content or size change. @@ -100,7 +51,7 @@ where /// /// Defaults to `true`. 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. @@ -115,24 +66,21 @@ where where S: Into, { - let max_offset = self.inner_size.saturating_sub(self.available_size()); - self.offset = offset.into().or_min(max_offset); + self.core.set_offset(offset); } /// Controls whether this view can scroll vertically. /// /// Defaults to `true`. pub fn set_scroll_y(&mut self, enabled: bool) { - self.enabled.y = enabled; - self.invalidate_cache(); + self.core.set_scroll_y(enabled); } /// Controls whether this view can scroll horizontally. /// /// Defaults to `false`. pub fn set_scroll_x(&mut self, enabled: bool) { - self.enabled.x = enabled; - self.invalidate_cache(); + self.core.set_scroll_x(enabled); } /// Controls whether this view can scroll vertically. @@ -155,272 +103,22 @@ where /// 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.core.scroll_to_top(); } /// Programmatically scroll to the bottom of the view. pub fn scroll_to_bottom(&mut self) { - let max_y = self.inner_size.saturating_sub(self.available_size()).y; - let curr_x = self.offset.x; - self.set_offset((curr_x, max_y)); + self.core.scroll_to_bottom(); } /// 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.core.scroll_to_left(); } /// 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.available_size()).x; - 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 { - 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, strict: bool, - ) -> (Vec2, Vec2, XY) { - // 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 => (), - } + self.core.scroll_to_right(); } /// Returns the wrapped view. @@ -436,259 +134,23 @@ where V: View, { fn draw(&self, printer: &Printer<'_, '_>) { - // Draw scrollbar? - let scrolling = self.is_scrolling(); - - let lengths = self.scrollbar_thumb_lengths(); - let offsets = self.scrollbar_thumb_offsets(lengths); - - let line_c = XY::new("-", "|"); - - let color = if printer.focused { - ColorStyle::highlight() - } else { - ColorStyle::highlight_inactive() - }; - - let size = self.available_size(); - - // 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); + self.core.draw(printer, &self.inner); } fn on_event(&mut self, event: Event) -> 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 { - 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 - } - } + self.core.on_event(event, &mut self.inner) } fn layout(&mut self, size: Vec2) { - // Size is final now, negociations are over. - 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(); + self.core.layout(size, &mut self.inner); } 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 { - let (_, size) = self.sizes(constraint, false); - - size + self.core.required_size(constraint, &mut self.inner) } 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 { - let is_scrollable = self.is_scrolling().any(); - self.inner.take_focus(source) || is_scrollable + let inner = &mut self.inner; + self.core + .take_focus(source, |source| inner.take_focus(source)) } }