use crate::core::event::{KeyCode, KeyEvent, MouseKind};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum ScrollAction {
LineUp(usize),
LineDown(usize),
LineLeft(usize),
LineRight(usize),
Home,
End,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ScrollMetrics {
pub len: usize,
pub visible: usize,
pub max_offset: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ScrollRequest {
Delta(isize),
ViewportDelta {
numerator: isize,
denominator: usize,
},
Top,
Bottom,
}
impl ScrollRequest {
pub const fn lines(delta: isize) -> Self {
Self::Delta(delta)
}
pub const fn viewport_fraction(numerator: isize, denominator: usize) -> Self {
Self::ViewportDelta {
numerator,
denominator,
}
}
pub const fn page_up() -> Self {
Self::viewport_fraction(-1, 1)
}
pub const fn page_down() -> Self {
Self::viewport_fraction(1, 1)
}
pub const fn half_page_up() -> Self {
Self::viewport_fraction(-1, 2)
}
pub const fn half_page_down() -> Self {
Self::viewport_fraction(1, 2)
}
pub const fn top() -> Self {
Self::Top
}
pub const fn bottom() -> Self {
Self::Bottom
}
pub const fn has_effect(self, offset: usize, max_offset: usize) -> bool {
match self {
Self::Delta(delta) => (delta < 0 && offset > 0) || (delta > 0 && offset < max_offset),
Self::ViewportDelta { numerator, .. } => {
(numerator < 0 && offset > 0) || (numerator > 0 && offset < max_offset)
}
Self::Top => offset > 0,
Self::Bottom => offset < max_offset,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ScrollKeymap(u8);
impl ScrollKeymap {
pub const NONE: Self = Self(0);
pub const ARROWS: Self = Self(1 << 0);
pub const VIM_VERTICAL: Self = Self(1 << 1);
pub const VIM_HORIZONTAL: Self = Self(1 << 2);
pub const HOME_END: Self = Self(1 << 3);
pub const VIM: Self = Self(Self::VIM_VERTICAL.0 | Self::VIM_HORIZONTAL.0);
pub const DEFAULT: Self = Self(Self::ARROWS.0 | Self::VIM.0 | Self::HOME_END.0);
pub fn contains(self, other: Self) -> bool {
(self.0 & other.0) == other.0
}
pub fn with_vim_vertical(self) -> Self {
Self(self.0 | Self::VIM_VERTICAL.0)
}
pub fn with_vim_horizontal(self) -> Self {
Self(self.0 | Self::VIM_HORIZONTAL.0)
}
pub fn without_vim_vertical(self) -> Self {
Self(self.0 & !Self::VIM_VERTICAL.0)
}
pub fn without_vim_horizontal(self) -> Self {
Self(self.0 & !Self::VIM_HORIZONTAL.0)
}
}
impl std::ops::BitOr for ScrollKeymap {
type Output = Self;
fn bitor(self, rhs: Self) -> Self {
Self(self.0 | rhs.0)
}
}
impl std::ops::BitOrAssign for ScrollKeymap {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl std::ops::BitAnd for ScrollKeymap {
type Output = Self;
fn bitand(self, rhs: Self) -> Self {
Self(self.0 & rhs.0)
}
}
impl std::ops::BitAndAssign for ScrollKeymap {
fn bitand_assign(&mut self, rhs: Self) {
self.0 &= rhs.0;
}
}
impl std::ops::Not for ScrollKeymap {
type Output = Self;
fn not(self) -> Self {
Self(!self.0)
}
}
impl Default for ScrollKeymap {
fn default() -> Self {
Self::DEFAULT
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum ScrollClip {
#[default]
Partial,
}
pub(crate) fn apply_scroll_request(
offset: usize,
metrics: ScrollMetrics,
request: ScrollRequest,
) -> usize {
if metrics.visible == 0 || metrics.len == 0 {
return 0;
}
match request {
ScrollRequest::Delta(delta) => {
if delta < 0 {
apply_scroll_action(offset, metrics, ScrollAction::LineUp(delta.unsigned_abs()))
} else if delta > 0 {
apply_scroll_action(offset, metrics, ScrollAction::LineDown(delta as usize))
} else {
offset.min(metrics.max_offset)
}
}
ScrollRequest::ViewportDelta {
numerator,
denominator,
} => {
let denominator = denominator.max(1);
let magnitude = ((metrics.visible.saturating_mul(numerator.unsigned_abs()))
+ denominator.saturating_sub(1))
/ denominator;
let lines = magnitude.max(1);
if numerator < 0 {
apply_scroll_action(offset, metrics, ScrollAction::LineUp(lines))
} else if numerator > 0 {
apply_scroll_action(offset, metrics, ScrollAction::LineDown(lines))
} else {
offset.min(metrics.max_offset)
}
}
ScrollRequest::Top => 0,
ScrollRequest::Bottom => metrics.max_offset,
}
}
pub(crate) fn scroll_metrics(len: usize, visible: usize, _offset: usize) -> ScrollMetrics {
let visible = visible.min(len);
let max_offset = if visible == 0 {
0
} else {
len.saturating_sub(visible)
};
ScrollMetrics {
len,
visible,
max_offset,
}
}
pub(crate) fn scroll_action_from_key(key: &KeyEvent, keymap: ScrollKeymap) -> Option<ScrollAction> {
match key.code {
KeyCode::Up if keymap.contains(ScrollKeymap::ARROWS) => Some(ScrollAction::LineUp(1)),
KeyCode::Down if keymap.contains(ScrollKeymap::ARROWS) => Some(ScrollAction::LineDown(1)),
KeyCode::Left if keymap.contains(ScrollKeymap::ARROWS) => Some(ScrollAction::LineLeft(1)),
KeyCode::Right if keymap.contains(ScrollKeymap::ARROWS) => Some(ScrollAction::LineRight(1)),
KeyCode::Char('k') if keymap.contains(ScrollKeymap::VIM_VERTICAL) => {
Some(ScrollAction::LineUp(1))
}
KeyCode::Char('j') if keymap.contains(ScrollKeymap::VIM_VERTICAL) => {
Some(ScrollAction::LineDown(1))
}
KeyCode::Char('h') if keymap.contains(ScrollKeymap::VIM_HORIZONTAL) => {
Some(ScrollAction::LineLeft(1))
}
KeyCode::Char('l') if keymap.contains(ScrollKeymap::VIM_HORIZONTAL) => {
Some(ScrollAction::LineRight(1))
}
KeyCode::Home if keymap.contains(ScrollKeymap::HOME_END) => Some(ScrollAction::Home),
KeyCode::End if keymap.contains(ScrollKeymap::HOME_END) => Some(ScrollAction::End),
_ => None,
}
}
pub(crate) fn scroll_action_from_mouse_n(
event: crate::core::event::MouseEvent,
lines: usize,
) -> Option<ScrollAction> {
if event.mods.shift {
match event.kind {
MouseKind::ScrollUp => Some(ScrollAction::LineLeft(lines)),
MouseKind::ScrollDown => Some(ScrollAction::LineRight(lines)),
_ => None,
}
} else {
match event.kind {
MouseKind::ScrollUp => Some(ScrollAction::LineUp(lines)),
MouseKind::ScrollDown => Some(ScrollAction::LineDown(lines)),
_ => None,
}
}
}
pub(crate) fn apply_scroll_action(
offset: usize,
metrics: ScrollMetrics,
action: ScrollAction,
) -> usize {
if metrics.visible == 0 || metrics.len == 0 {
return 0;
}
let max_offset = metrics.max_offset;
match action {
ScrollAction::LineUp(lines) | ScrollAction::LineLeft(lines) => offset.saturating_sub(lines),
ScrollAction::LineDown(lines) | ScrollAction::LineRight(lines) => {
offset.saturating_add(lines).min(max_offset)
}
ScrollAction::Home => 0,
ScrollAction::End => max_offset,
}
}