use std::cmp::min;
use crate::direction::Orientation;
use crate::event::{AnyCb, Event, EventResult, Key, MouseButton, MouseEvent};
use crate::printer::Printer;
use crate::rect::Rect;
use crate::theme::ColorStyle;
use crate::vec::Vec2;
use crate::view::{ScrollStrategy, Selector, SizeCache};
use crate::with::With;
use crate::XY;
pub trait Scroller {
fn get_scroller_mut(&mut self) -> &mut Core;
fn get_scroller(&self) -> &Core;
}
#[derive(Debug)]
pub struct Core {
inner_size: Vec2,
offset: Vec2,
last_size: Vec2,
enabled: XY<bool>,
show_scrollbars: bool,
scrollbar_padding: Vec2,
thumb_grab: Option<(Orientation, usize)>,
size_cache: Option<XY<SizeCache>>,
scroll_strategy: ScrollStrategy,
}
impl Default for Core {
fn default() -> Self {
Self::new()
}
}
impl Core {
pub fn new() -> Self {
Core {
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,
}
}
pub fn sub_printer<'a, 'b>(
&self,
printer: &Printer<'a, 'b>,
) -> Printer<'a, 'b> {
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();
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,
);
});
},
);
if scrolling.both() {
printer.print(printer.size.saturating_sub((1, 1)), "╳");
}
printer
.cropped(size)
.content_offset(self.offset)
.inner_size(self.inner_size)
}
pub fn is_event_inside(&self, event: &mut Event) -> bool {
if let Event::Mouse {
ref mut position,
ref offset,
..
} = event
{
let inside = position
.checked_sub(offset)
.map(|p| p.fits_in(self.available_size()))
.unwrap_or(false);
*position = *position + self.offset;
inside
} else {
true
}
}
pub fn on_inner_event(
&mut self,
event: Event,
inner_result: EventResult,
important_area: Rect,
) -> EventResult {
match inner_result {
EventResult::Ignored => {
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) =>
{
}
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,
};
self.scroll_strategy = ScrollStrategy::KeepRow;
EventResult::Consumed(None)
}
other => {
let important = important_area;
let top_left = (important.bottom_right() + (1, 1))
.saturating_sub(self.available_size());
let bottom_right = important.top_left();
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
}
}
}
pub(crate) fn set_last_size(&mut self, last_size: Vec2) {
self.last_size = last_size;
}
pub fn last_size(&self) -> Vec2 {
self.last_size
}
pub(crate) fn set_inner_size(&mut self, inner_size: Vec2) {
self.inner_size = inner_size;
}
pub(crate) fn build_cache(&mut self, self_size: Vec2, last_size: Vec2) {
self.size_cache = Some(SizeCache::build(self_size, last_size));
}
pub(crate) fn update_offset(&mut self) {
self.offset = self
.offset
.or_min(self.inner_size.saturating_sub(self.available_size()));
self.adjust_scroll();
}
pub fn needs_relayout(&self) -> bool {
self.size_cache.is_none()
}
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)
}
pub fn focus_view<F>(
&mut self,
selector: &Selector<'_>,
inner_focus_view: F,
) -> Result<(), ()>
where
F: FnOnce(&Selector) -> Result<(), ()>,
{
inner_focus_view(selector)
}
pub fn content_viewport(&self) -> Rect {
Rect::from_size(self.offset, self.available_size())
}
pub fn set_scroll_strategy(&mut self, strategy: ScrollStrategy) {
self.scroll_strategy = strategy;
self.adjust_scroll();
}
pub fn scroll_strategy(self, strategy: ScrollStrategy) -> Self {
self.with(|s| s.set_scroll_strategy(strategy))
}
pub fn set_scrollbar_padding<V: Into<Vec2>>(
&mut self,
scrollbar_padding: V,
) {
self.scrollbar_padding = scrollbar_padding.into();
}
pub fn scrollbar_padding<V: Into<Vec2>>(
self,
scrollbar_padding: V,
) -> Self {
self.with(|s| s.set_scrollbar_padding(scrollbar_padding))
}
pub fn get_scrollbar_padding(&self) -> Vec2 {
self.scrollbar_padding
}
pub fn is_enabled(&self) -> XY<bool> {
self.enabled
}
pub fn set_show_scrollbars(&mut self, show_scrollbars: bool) {
self.show_scrollbars = show_scrollbars;
}
pub fn show_scrollbars(self, show_scrollbars: bool) -> Self {
self.with(|s| s.set_show_scrollbars(show_scrollbars))
}
pub fn get_show_scrollbars(&self) -> bool {
self.show_scrollbars
}
pub fn inner_size(&self) -> Vec2 {
self.inner_size
}
pub fn set_offset<S>(&mut self, offset: S)
where
S: Into<Vec2>,
{
let max_offset = self.inner_size.saturating_sub(self.available_size());
self.offset = offset.into().or_min(max_offset);
}
pub fn set_scroll_y(&mut self, enabled: bool) {
self.enabled.y = enabled;
self.invalidate_cache();
}
pub fn set_scroll_x(&mut self, enabled: bool) {
self.enabled.x = enabled;
self.invalidate_cache();
}
pub fn scroll_y(self, enabled: bool) -> Self {
self.with(|s| s.set_scroll_y(enabled))
}
pub fn scroll_x(self, enabled: bool) -> Self {
self.with(|s| s.set_scroll_x(enabled))
}
pub fn keep_in_view(&mut self, rect: Rect) {
let min = rect.bottom_right().saturating_sub(self.last_size);
let max = rect.top_left();
let (min, max) = (Vec2::min(min, max), Vec2::max(min, max));
self.offset = self.offset.or_min(max).or_max(min);
}
pub fn scroll_to(&mut self, pos: Vec2) {
let min = pos.saturating_sub(self.last_size);
let max = pos;
self.offset = self.offset.or_min(max).or_max(min);
}
pub fn scroll_to_x(&mut self, x: usize) {
if x >= self.offset.x + self.last_size.x {
self.offset.x = 1 + x - self.last_size.x;
} else if x < self.offset.x {
self.offset.x = x;
}
}
pub fn scroll_to_y(&mut self, y: usize) {
if y >= self.offset.y + self.last_size.y {
self.offset.y = 1 + y - self.last_size.y;
} else if y < self.offset.y {
self.offset.y = y;
}
}
pub fn scroll_to_top(&mut self) {
let curr_x = self.offset.x;
self.set_offset((curr_x, 0));
}
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));
}
pub fn scroll_to_left(&mut self) {
let curr_y = self.offset.y;
self.set_offset((0, curr_y));
}
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));
}
fn invalidate_cache(&mut self) {
self.size_cache = None;
}
pub fn is_scrolling(&self) -> XY<bool> {
self.inner_size.zip_map(self.last_size, |i, s| i > s)
}
fn release_grab(&mut self) {
self.thumb_grab = None;
}
pub fn scrollbar_size(&self) -> Vec2 {
self.is_scrolling()
.swap()
.select_or(self.scrollbar_padding + (1, 1), Vec2::zero())
}
fn available_size(&self) -> Vec2 {
if self.show_scrollbars {
self.last_size.saturating_sub(self.scrollbar_size())
} else {
self.last_size
}
}
fn start_drag(&mut self, position: Vec2) -> bool {
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();
let grabbed = position
.zip_map(scrollbar_pos, |p, s| p == s)
.swap()
.and(position.zip_map(available, |p, a| p < a));
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 {
self.thumb_grab = Some((orientation, pos - offset));
} else {
self.thumb_grab = Some((orientation, (length - 1) / 2));
self.drag(position);
}
return true;
}
false
}
fn drag(&mut self, position: Vec2) {
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();
let extra =
(available + (1, 1)).saturating_sub(lengths).or_max((1, 1));
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));
}
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 {
None
}
})
}
fn scrollbar_thumb_lengths(&self) -> Vec2 {
let available = self.available_size();
(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();
let steps = (available + (1, 1)).saturating_sub(lengths);
let max_offset = self.inner_size.saturating_sub(available) + (1, 1);
steps * self.offset / max_offset
}
fn adjust_scroll(&mut self) {
match self.scroll_strategy {
ScrollStrategy::StickToTop => self.scroll_to_top(),
ScrollStrategy::StickToBottom => self.scroll_to_bottom(),
ScrollStrategy::KeepRow => (),
}
}
}