这是indexloc提供的服务,不要输入任何密码
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 30 additions & 4 deletions crates/turborepo-ui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use super::{
};
use crate::{
tui::{
scroll::ScrollMomentum,
task::{Task, TasksByStatus},
term_output::TerminalOutput,
},
Expand Down Expand Up @@ -60,6 +61,7 @@ pub struct App<W> {
done: bool,
preferences: PreferenceLoader,
scrollback_len: u64,
scroll_momentum: ScrollMomentum,
}

impl<W> App<W> {
Expand Down Expand Up @@ -115,6 +117,7 @@ impl<W> App<W> {
is_task_selection_pinned: preferences.active_task().is_some(),
preferences,
scrollback_len,
scroll_momentum: ScrollMomentum::new(),
}
}

Expand Down Expand Up @@ -204,8 +207,21 @@ impl<W> App<W> {
}

#[tracing::instrument(skip_all)]
pub fn scroll_terminal_output(&mut self, direction: Direction) -> Result<(), Error> {
self.get_full_task_mut()?.scroll(direction)?;
pub fn scroll_terminal_output(
&mut self,
direction: Direction,
use_momentum: bool,
) -> Result<(), Error> {
let lines = if use_momentum {
self.scroll_momentum.on_scroll_event(direction)
} else {
self.scroll_momentum.reset();
1
};

if lines > 0 {
self.get_full_task_mut()?.scroll_by(direction, lines)?;
}
Ok(())
}

Expand Down Expand Up @@ -851,26 +867,36 @@ fn update(
}
Event::ScrollUp => {
app.is_task_selection_pinned = true;
app.scroll_terminal_output(Direction::Up)?;
app.scroll_momentum.reset();
app.scroll_terminal_output(Direction::Up, false)?
}
Event::ScrollDown => {
app.is_task_selection_pinned = true;
app.scroll_terminal_output(Direction::Down)?;
app.scroll_momentum.reset();
app.scroll_terminal_output(Direction::Down, false)?;
}
Event::ScrollWithMomentum(direction) => {
app.is_task_selection_pinned = true;
app.scroll_terminal_output(direction, true)?;
}
Event::PageUp => {
app.is_task_selection_pinned = true;
app.scroll_momentum.reset();
app.scroll_terminal_output_by_page(Direction::Up)?;
}
Event::PageDown => {
app.is_task_selection_pinned = true;
app.scroll_momentum.reset();
app.scroll_terminal_output_by_page(Direction::Down)?;
}
Event::JumpToLogsTop => {
app.is_task_selection_pinned = true;
app.scroll_momentum.reset();
app.jump_to_logs_top()?;
}
Event::JumpToLogsBottom => {
app.is_task_selection_pinned = true;
app.scroll_momentum.reset();
app.jump_to_logs_bottom()?;
}
Event::EnterInteractive => {
Expand Down
3 changes: 2 additions & 1 deletion crates/turborepo-ui/src/tui/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub enum Event {
Down,
ScrollUp,
ScrollDown,
ScrollWithMomentum(Direction),
PageUp,
PageDown,
JumpToLogsTop,
Expand Down Expand Up @@ -68,7 +69,7 @@ pub enum Event {
SearchBackspace,
}

#[derive(Copy, Clone)]
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Direction {
Up,
Down,
Expand Down
8 changes: 6 additions & 2 deletions crates/turborepo-ui/src/tui/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,12 @@ impl InputOptions<'_> {
match event {
crossterm::event::Event::Key(k) => translate_key_event(self, k),
crossterm::event::Event::Mouse(m) => match m.kind {
crossterm::event::MouseEventKind::ScrollDown => Some(Event::ScrollDown),
crossterm::event::MouseEventKind::ScrollUp => Some(Event::ScrollUp),
crossterm::event::MouseEventKind::ScrollDown => {
Some(Event::ScrollWithMomentum(Direction::Down))
}
crossterm::event::MouseEventKind::ScrollUp => {
Some(Event::ScrollWithMomentum(Direction::Up))
}
crossterm::event::MouseEventKind::Down(crossterm::event::MouseButton::Left)
| crossterm::event::MouseEventKind::Drag(crossterm::event::MouseButton::Left) => {
Some(Event::Mouse(m))
Expand Down
1 change: 1 addition & 0 deletions crates/turborepo-ui/src/tui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ mod input;
mod pane;
mod popup;
mod preferences;
pub mod scroll;
mod search;
mod size;
mod spinner;
Expand Down
111 changes: 111 additions & 0 deletions crates/turborepo-ui/src/tui/scroll.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use std::time::{Duration, Instant};

use crate::tui::event::Direction;

/// The maximum number of lines that can be scrolled per event.
/// Increase for a higher top speed; decrease for a lower top speed.
const MAX_VELOCITY: f32 = 12.0; // max lines per event

/// The minimum number of lines to scroll per event (when not accelerating).
/// Usually leave at 1.0 for single-line scrolls.
const MIN_VELOCITY: f32 = 1.0;

/// How much the scroll velocity increases per qualifying event.
/// Increase for faster acceleration (reaches top speed quicker, feels
/// snappier). Decrease for slower, smoother acceleration (takes longer to reach
/// top speed).
const ACCELERATION: f32 = 0.3;

/// How long (in ms) between scrolls before momentum resets.
/// Increase to allow longer pauses between scrolls while keeping momentum.
/// Decrease to require faster, more continuous scrolling to maintain momentum.
const DECAY_TIME: Duration = Duration::from_millis(350);

/// How long (in ms) between scrolls before events are ignored
/// Increase to allow longer pauses between scrolls to trigger throttling
/// Decrease to require faster, more continuous scrolling to trigger throttling
const THROTTLE_TIME: Duration = Duration::from_millis(50);

/// Only process 1 out of every N scroll events (throttling).
/// Increase to make scrolling less sensitive to high-frequency mouse wheels
/// (e.g. trackpads). Decrease to process more events (smoother, but may be
/// too fast on some input devices).
const THROTTLE_FACTOR: u8 = 3;

/// Tracks and computes momentum-based scrolling.
pub struct ScrollMomentum {
velocity: f32,
last_event: Option<Instant>,
last_direction: Option<Direction>,
throttle_counter: u8,
}

impl Default for ScrollMomentum {
fn default() -> Self {
Self::new()
}
}

impl ScrollMomentum {
/// Create a new ScrollMomentum tracker.
pub fn new() -> Self {
Self {
velocity: 0.0,
last_event: None,
last_direction: None,
throttle_counter: 0,
}
}

/// Call this on every scroll event (mouse wheel, key, etc).
/// Returns the number of lines to scroll for this event.
pub fn on_scroll_event(&mut self, direction: Direction) -> usize {
let now = Instant::now();
let last_event = self.last_event.replace(now);
let last_direction = self.last_direction.replace(direction);

let has_direction_changed = last_direction.is_some_and(|last| last != direction);
let is_scrolling_quickly =
last_event.is_some_and(|last| now.duration_since(last) < DECAY_TIME);
let is_throttling = last_event.is_some_and(|last| now.duration_since(last) < THROTTLE_TIME);

if is_throttling {
self.throttle_counter = (self.throttle_counter + 1) % THROTTLE_FACTOR;
let should_throttle = self.throttle_counter != 0;
if should_throttle {
return 0;
}
} else {
self.throttle_counter = 0;
}

if has_direction_changed {
self.velocity = MIN_VELOCITY;
} else if is_scrolling_quickly {
self.velocity = (self.velocity + ACCELERATION).min(MAX_VELOCITY);
} else {
self.velocity = MIN_VELOCITY;
}

self.velocity.round().clamp(MIN_VELOCITY, MAX_VELOCITY) as usize
}

/// Reset the momentum (e.g. on focus loss or scroll stop)
pub fn reset(&mut self) {
self.velocity = 0.0;
self.last_event = None;
self.last_direction = None;
self.throttle_counter = 0;
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn first_event_scrolls() {
let mut scroll = ScrollMomentum::new();
assert_eq!(scroll.on_scroll_event(Direction::Up), 1);
}
}
Loading