diff --git a/yazi-widgets/src/scrollable.rs b/yazi-widgets/src/scrollable.rs index 4fde11c55..b07dbb9f6 100644 --- a/yazi-widgets/src/scrollable.rs +++ b/yazi-widgets/src/scrollable.rs @@ -8,7 +8,11 @@ pub trait Scrollable { fn offset_mut(&mut self) -> &mut usize; fn scroll(&mut self, step: impl Into) -> bool { - let new = step.into().add(*self.cursor_mut(), self.total(), self.limit()); + let step = step.into(); + let new = step + .window_position(*self.offset_mut(), self.total(), self.limit(), self.scrolloff()) + .unwrap_or_else(|| step.add(*self.cursor_mut(), self.total(), self.limit())); + if new > *self.cursor_mut() { self.next(new) } else { self.prev(new) } } diff --git a/yazi-widgets/src/step.rs b/yazi-widgets/src/step.rs index a9ec6d866..b46fe3e9d 100644 --- a/yazi-widgets/src/step.rs +++ b/yazi-widgets/src/step.rs @@ -10,6 +10,7 @@ pub enum Step { Next, Offset(isize), Percent(i8), + PercentWindow(i8), } impl Default for Step { @@ -29,6 +30,7 @@ impl FromStr for Step { "bot" => Self::Bot, "prev" => Self::Prev, "next" => Self::Next, + s if s.ends_with("%-window") => Self::PercentWindow(s[..s.len() - 8].parse()?), s if s.ends_with('%') => Self::Percent(s[..s.len() - 1].parse()?), s => Self::Offset(s.parse()?), }) @@ -76,6 +78,31 @@ impl<'de> Deserialize<'de> for Step { } impl Step { + pub fn window_position( + self, + offset: usize, + len: usize, + limit: usize, + scrolloff: usize, + ) -> Option { + let Self::PercentWindow(n) = self else { return None }; + + let window_len = len.saturating_sub(offset).min(limit); + if window_len == 0 { + return Some(0); + } + + let scrolloff = scrolloff.min(window_len.saturating_sub(1) / 2); + + // Clamp relative position in window to not reach into any scrolloff region. + // Still allow reaching the real list start and end if already visible. + let pos = Self::percent_pos(offset, window_len, n); + let min = if offset == 0 { 0 } else { offset + scrolloff }; + let max = offset + window_len - 1 - if offset + window_len >= len { 0 } else { scrolloff }; + + Some(pos.clamp(min, max)) + } + pub fn add(self, pos: usize, len: usize, limit: usize) -> usize { if len == 0 { return 0; @@ -84,6 +111,9 @@ impl Step { let off = match self { Self::Top => return 0, Self::Bot => return len - 1, + Self::PercentWindow(n) => { + return Self::percent_pos(0, if limit == 0 { len } else { len.min(limit) }, n); + } Self::Prev => -1, Self::Next => 1, Self::Offset(n) => n, @@ -100,4 +130,14 @@ impl Step { } .min(len - 1) } + + fn percent_pos(offset: usize, len: usize, n: i8) -> usize { + if len == 0 { + return 0; + } + + let span = len.saturating_sub(1); + let pos = offset.saturating_add_signed(n as isize * span as isize / 100); + pos.clamp(offset, offset + span) + } }