use crate::event::{KeyEvent, MouseButton, MouseEvent, MouseKind};
use crate::geometry::{Rect, clamp_u16};
use crate::keymap::Key;
use crate::style::{CellStyle, WidgetStyle};
use crate::theme::State;
use crate::widget::{EventCx, PaintCx};
use super::scrollbar::{self, ScrollMetrics, ScrollbarStyle};
pub(crate) const WHEEL_ROWS: u16 = 3;
#[derive(Debug, Default)]
pub(crate) struct RowScroll {
pub(crate) offset: usize,
pub(crate) followed: Option<usize>,
pub(crate) dragging: bool,
pub(crate) flashed: Option<usize>,
}
impl RowScroll {
pub(crate) fn follow(&mut self, selected: Option<usize>, total: usize, visible: usize) -> usize {
if self.followed != selected {
if let Some(selected) = selected {
if selected < self.offset {
self.offset = selected;
} else if visible > 0 && selected >= self.offset + visible {
self.offset = selected + 1 - visible;
}
}
self.followed = selected;
}
self.offset = self.offset.min(total.saturating_sub(visible));
self.offset
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Step {
Up,
Down,
PageUp,
PageDown,
Home,
End,
}
impl Step {
pub(crate) fn from_key(key: &KeyEvent) -> Option<Self> {
let pairs = [
(Key::Up, Self::Up),
(Key::Char('k'), Self::Up),
(Key::Down, Self::Down),
(Key::Char('j'), Self::Down),
(Key::PageUp, Self::PageUp),
(Key::PageDown, Self::PageDown),
(Key::Home, Self::Home),
(Key::End, Self::End),
];
pairs.into_iter().find(|(k, _)| key.is_plain(*k)).map(|(_, step)| step)
}
pub(crate) fn apply(self, current: Option<usize>, len: usize, page: usize) -> Option<usize> {
let last = len.checked_sub(1)?;
let page = page.max(1);
Some(match (self, current) {
(Self::Home, _) | (Self::Down | Self::PageDown, None) => 0,
(Self::End, _) | (Self::Up | Self::PageUp, None) => last,
(Self::Up, Some(i)) => i.saturating_sub(1),
(Self::Down, Some(i)) => (i + 1).min(last),
(Self::PageUp, Some(i)) => i.saturating_sub(page),
(Self::PageDown, Some(i)) => (i + page).min(last),
})
}
}
pub(crate) fn row_states(hovered: bool, selected: bool, focused: bool, pressed: bool) -> Vec<State> {
let mut states = Vec::new();
if hovered {
states.push(State::Hover);
}
if selected {
states.push(State::Selected);
if focused {
states.push(State::Focus);
}
}
if pressed {
states.push(State::Pressed);
}
states
}
pub(crate) fn paint_row(cx: &mut PaintCx<'_>, rect: Rect, style: &WidgetStyle) -> CellStyle {
let mut text = style.text();
if let Some(bg) = text.bg {
cx.fill(rect, bg);
}
if let Some(pillar) = style.color("pillar") {
cx.pillar(rect.x, rect.y, pillar);
}
text.bg = None;
text
}
pub(crate) fn slide(cx: &PaintCx<'_>, states: &[State]) -> u16 {
let raised = states.contains(&State::Hover) || states.contains(&State::Selected);
u16::from(raised && cx.env().slide())
}
pub(crate) fn paint_scrollbar(
cx: &mut PaintCx<'_>,
body: Rect,
total: usize,
offset: usize,
pinned: Option<ScrollbarStyle>,
) {
let metrics = ScrollMetrics { total, visible: usize::from(body.height), offset };
if !metrics.overflows() {
return;
}
let bar = Rect::new(body.right() - 1, body.y, 1, body.height);
let dragging = cx.memory::<RowScroll>().dragging;
let active = dragging || cx.pointer().is_some_and(|(x, y)| x == bar.x && y >= bar.y && y < bar.bottom());
scrollbar::paint(cx, bar, metrics, active, pinned);
}
pub(crate) fn scroll_mouse<Msg>(cx: &mut EventCx<'_, Msg>, mouse: &MouseEvent, body: Rect, total: usize) -> bool {
let offset = cx.memory::<RowScroll>().offset;
let metrics = ScrollMetrics { total, visible: usize::from(body.height), offset };
let on_bar = metrics.overflows() && mouse.x == body.right() - 1 && mouse.y >= body.y && mouse.y < body.bottom();
let memory = cx.memory::<RowScroll>();
match mouse.kind {
MouseKind::ScrollUp => {
memory.offset = memory.offset.saturating_sub(usize::from(WHEEL_ROWS));
true
}
MouseKind::ScrollDown => {
memory.offset = (memory.offset + usize::from(WHEEL_ROWS)).min(metrics.max_offset());
true
}
MouseKind::Down(MouseButton::Left) if on_bar => {
memory.dragging = true;
memory.offset = metrics.offset_at(clamp_u16(mouse.y - body.y), body.height);
cx.capture_pointer();
true
}
MouseKind::Drag(MouseButton::Left) if memory.dragging => {
memory.offset = metrics.offset_at(clamp_u16(mouse.y - body.y), body.height);
true
}
MouseKind::Up(MouseButton::Left) if memory.dragging => {
memory.dragging = false;
true
}
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn steps_stay_inside_the_rows() {
assert_eq!(Step::Down.apply(None, 5, 3), Some(0));
assert_eq!(Step::Up.apply(None, 5, 3), Some(4));
assert_eq!(Step::Down.apply(Some(4), 5, 3), Some(4));
assert_eq!(Step::PageDown.apply(Some(1), 5, 3), Some(4));
assert_eq!(Step::PageUp.apply(Some(4), 5, 3), Some(1));
assert_eq!(Step::Home.apply(Some(3), 5, 3), Some(0));
assert_eq!(Step::End.apply(None, 0, 3), None);
}
#[test]
fn follow_scrolls_a_new_selection_into_view_once() {
let mut scroll = RowScroll::default();
assert_eq!(scroll.follow(Some(9), 20, 4), 6);
scroll.offset = 0;
assert_eq!(scroll.follow(Some(9), 20, 4), 0, "an unchanged selection does not pull the view back");
assert_eq!(scroll.follow(Some(2), 20, 4), 0);
scroll.offset = 50;
assert_eq!(scroll.follow(Some(2), 20, 4), 16);
}
}