From 15132d44af5729d89d951c709f23068fccea75a3 Mon Sep 17 00:00:00 2001 From: alice pellerin Date: Wed, 29 Apr 2026 17:41:05 -0500 Subject: [PATCH] fix scrolling and clamping --- src/buffer.rs | 8 +- src/buffer/actions.rs | 191 ++++++++++++-------------- src/cursor.rs | 9 +- src/cursor/actions.rs | 26 ++-- src/main.rs | 4 +- src/utilities.rs | 4 + src/utilities/floor_to_the_nearest.rs | 16 +++ src/utilities/saturating_subtract.rs | 9 ++ 8 files changed, 136 insertions(+), 131 deletions(-) create mode 100644 src/utilities/floor_to_the_nearest.rs create mode 100644 src/utilities/saturating_subtract.rs diff --git a/src/buffer.rs b/src/buffer.rs index 6553a11..7f0cbe3 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -144,9 +144,11 @@ impl Buffer { }; assert!(self.scroll_position.is_multiple_of(BYTES_PER_LINE)); - assert!(self.scroll_position < self.contents.len()); - assert!(self.primary_cursor.head < self.contents.len()); - assert!(self.primary_cursor.tail < self.contents.len()); + if !self.contents.is_empty() { + assert!(self.scroll_position < self.contents.len()); + assert!(self.primary_cursor.head < self.contents.len()); + assert!(self.primary_cursor.tail < self.contents.len()); + } assert!(self.scroll_position <= self.primary_cursor.head); assert!(self.primary_cursor.head < self.scroll_position + window_size.visible_byte_count()); diff --git a/src/buffer/actions.rs b/src/buffer/actions.rs index 0e71703..3439d59 100644 --- a/src/buffer/actions.rs +++ b/src/buffer/actions.rs @@ -1,7 +1,7 @@ use std::{cmp::min, collections::hash_set::Entry, convert::identity, fs::File, io::Write, iter, mem::{replace, swap}}; use itertools::Itertools; use ratatui::{style::{Color, Stylize}, text::Span}; -use crate::{BYTES_OF_PADDING, BYTES_PER_LINE, LINES_OF_PADDING, action::BufferAction, buffer::{Buffer, InspectionStatus, Mode, PartialAction}, cursor::Cursor, edit_action::EditAction, popup::Popup, window_size::WindowSize}; +use crate::{BYTES_OF_PADDING, BYTES_PER_LINE, LINES_OF_PADDING, action::BufferAction, buffer::{Buffer, InspectionStatus, Mode, PartialAction}, cursor::Cursor, edit_action::EditAction, popup::Popup, utilities::{Floorable, SaturatingSubtract}, window_size::WindowSize}; impl Buffer { pub fn execute(&mut self, action: BufferAction, window_size: WindowSize) { @@ -60,7 +60,7 @@ impl Buffer { BufferAction::AlignViewCenter => self.align_view_center(window_size), BufferAction::AlignViewBottom => self.align_view_bottom(window_size), - BufferAction::AlignViewTop => self.align_view_top(), + BufferAction::AlignViewTop => self.align_view_top(window_size), BufferAction::FindTillMark => self.till_mark(false, window_size), // extend: false BufferAction::FindTillNull => self.till_null(false, window_size), // extend: false @@ -110,131 +110,75 @@ impl Buffer { } pub fn scroll_down(&mut self, window_size: WindowSize) { - if self.contents.len() <= BYTES_OF_PADDING { return; } - - self.scroll_position = min( - self.scroll_position + BYTES_PER_LINE, - self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE - ); - - if window_size.hex_rows() > LINES_OF_PADDING * 2 { - self.primary_cursor.clamp( - self.scroll_position + BYTES_OF_PADDING, - window_size.visible_byte_count() - BYTES_OF_PADDING * 2 - ); - } else { - self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); - } - + self.scroll_position += BYTES_PER_LINE; + self.clamp_screen_to_contents(window_size); + self.clamp_primary_cursor_to_screen(window_size); self.combine_cursors_if_overlapping(); } pub fn scroll_up(&mut self, window_size: WindowSize) { - self.scroll_position = self.scroll_position.saturating_sub(BYTES_PER_LINE); - - if window_size.hex_rows() > LINES_OF_PADDING * 2 { - self.primary_cursor.clamp( - self.scroll_position + BYTES_OF_PADDING, - window_size.visible_byte_count() - BYTES_OF_PADDING * 2 - ); - } else { - self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); - } - + self.scroll_position.saturating_subtract(BYTES_PER_LINE); + self.clamp_primary_cursor_to_screen(window_size); self.combine_cursors_if_overlapping(); } fn page_cursor_half_down(&mut self, window_size: WindowSize) { - if self.contents.len() <= BYTES_OF_PADDING { return; } + let scroll_amount = (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE); - let old_scroll_position = self.scroll_position; + self.scroll_position += scroll_amount; + self.clamp_screen_to_contents(window_size); - self.scroll_position = min( - self.scroll_position + (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE), - self.contents.len() - BYTES_OF_PADDING - self.contents.len() % BYTES_PER_LINE - ); + self.primary_cursor.head += scroll_amount; + if self.mode != Mode::Select { + self.primary_cursor.tail += scroll_amount; + } + self.primary_cursor.clamp(0, self.max_contents_index()); + self.clamp_screen_to_primary_cursor(window_size); - let scroll_position_change = self.scroll_position - old_scroll_position; let max_contents_index = self.max_contents_index(); - self.primary_cursor.head = min( - self.primary_cursor.head + scroll_position_change, - max_contents_index - ); - self.primary_cursor.tail = min( - self.primary_cursor.tail + scroll_position_change, - max_contents_index - ); - for cursor in &mut self.cursors { - cursor.head = (cursor.head + scroll_position_change).min(max_contents_index); - cursor.tail = (cursor.tail + scroll_position_change).min(max_contents_index); + cursor.head += scroll_amount; + if self.mode != Mode::Select { + cursor.tail += scroll_amount; + } + cursor.clamp(0, max_contents_index); } self.combine_cursors_if_overlapping(); } fn page_cursor_half_up(&mut self, window_size: WindowSize) { - let old_scroll_position = self.scroll_position; + let scroll_amount = (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE); - self.scroll_position = self.scroll_position.saturating_sub( - (window_size.visible_byte_count() / 2).next_multiple_of(BYTES_PER_LINE) - ); + self.scroll_position.saturating_subtract(scroll_amount); - let scroll_position_change = old_scroll_position - self.scroll_position; - let max_contents_index = self.max_contents_index(); - - self.primary_cursor.head = min( - self.primary_cursor.head - scroll_position_change, - max_contents_index - ); - self.primary_cursor.tail = min( - self.primary_cursor.tail - scroll_position_change, - max_contents_index - ); + self.primary_cursor.head.saturating_subtract(scroll_amount); + if self.mode != Mode::Select { + self.primary_cursor.tail.saturating_subtract(scroll_amount); + } for cursor in &mut self.cursors { - cursor.head = (cursor.head - scroll_position_change).min(max_contents_index); - cursor.tail = (cursor.tail - scroll_position_change).min(max_contents_index); + cursor.head.saturating_subtract(scroll_amount); + if self.mode != Mode::Select { + cursor.tail.saturating_subtract(scroll_amount); + } } self.combine_cursors_if_overlapping(); } fn page_down(&mut self, window_size: WindowSize) { - if self.contents.len() <= BYTES_OF_PADDING { return; } - - self.scroll_position = min( - self.scroll_position + window_size.visible_byte_count(), - self.max_contents_index() - BYTES_OF_PADDING - self.max_contents_index() % BYTES_PER_LINE - ); - - if window_size.hex_rows() > LINES_OF_PADDING * 2 { - self.primary_cursor.clamp( - self.scroll_position + BYTES_OF_PADDING, - window_size.visible_byte_count() - BYTES_OF_PADDING * 2 - ); - } else { - self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); - } - + self.scroll_position += window_size.visible_byte_count(); + self.clamp_screen_to_contents(window_size); + self.clamp_primary_cursor_to_screen(window_size); self.combine_cursors_if_overlapping(); } fn page_up(&mut self, window_size: WindowSize) { - self.scroll_position = self.scroll_position.saturating_sub( - window_size.visible_byte_count() - ); - - if window_size.hex_rows() > LINES_OF_PADDING * 2 { - self.primary_cursor.clamp( - self.scroll_position + BYTES_OF_PADDING, - window_size.visible_byte_count() - BYTES_OF_PADDING * 2 - ); - } else { - self.primary_cursor.clamp(self.scroll_position, window_size.visible_byte_count()); - } - + self.scroll_position.saturating_subtract(window_size.visible_byte_count()); + self.clamp_screen_to_contents(window_size); + self.clamp_primary_cursor_to_screen(window_size); self.combine_cursors_if_overlapping(); } @@ -540,25 +484,25 @@ impl Buffer { let half_a_screen = window_size.visible_byte_count() / 2; self.scroll_position = self.primary_cursor.head - .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) - .saturating_sub(half_a_screen - (half_a_screen % BYTES_PER_LINE)); + .floored_to_the_nearest(BYTES_PER_LINE) + .saturating_sub(half_a_screen.floored_to_the_nearest(BYTES_PER_LINE)); } fn align_view_bottom(&mut self, window_size: WindowSize) { self.scroll_position = self.primary_cursor.head - .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) + .floored_to_the_nearest(BYTES_PER_LINE) .saturating_sub( window_size .visible_byte_count() - .saturating_sub(BYTES_PER_LINE + BYTES_OF_PADDING) + .saturating_sub(BYTES_PER_LINE + Self::bottom_padding(window_size)) ) - .min(self.max_contents_index() - self.max_contents_index() % BYTES_PER_LINE); + .min(self.max_contents_index().floored_to_the_nearest(BYTES_PER_LINE)); } - const fn align_view_top(&mut self) { + const fn align_view_top(&mut self, window_size: WindowSize) { self.scroll_position = self.primary_cursor.head - .saturating_sub(self.primary_cursor.head % BYTES_PER_LINE) - .saturating_sub(BYTES_OF_PADDING); + .floored_to_the_nearest(BYTES_PER_LINE) + .saturating_sub(self.top_padding(window_size)); } fn till_mark(&mut self, extend: bool, window_size: WindowSize) { @@ -870,13 +814,52 @@ fn inspect_color(selection: &[u8]) -> Vec> { // MARK: helpers impl Buffer { + const fn bottom_padding(window_size: WindowSize) -> usize { + if window_size.hex_rows() <= LINES_OF_PADDING * 2 { + 0 + } else { + BYTES_OF_PADDING + } + } + + const fn top_padding(&self, window_size: WindowSize) -> usize { + if window_size.hex_rows() <= LINES_OF_PADDING * 2 || self.scroll_position == 0 { + 0 + } else { + BYTES_OF_PADDING + } + } + + pub const fn clamp_screen_to_contents(&mut self, window_size: WindowSize) { + let max_scroll_position = self.max_contents_index() + .floored_to_the_nearest(BYTES_PER_LINE) + .saturating_sub(Self::bottom_padding(window_size)); + + if self.scroll_position > max_scroll_position { + self.scroll_position = max_scroll_position; + } + } + pub fn clamp_screen_to_primary_cursor(&mut self, window_size: WindowSize) { - if self.primary_cursor.head < self.scroll_position + BYTES_OF_PADDING { - self.align_view_top(); - } else if self.primary_cursor.head > self.scroll_position + (window_size.visible_byte_count() - 1).saturating_sub(BYTES_OF_PADDING) { + if self.primary_cursor.head < self.scroll_position + self.top_padding(window_size) { + self.align_view_top(window_size); + } else if self.primary_cursor.head > self.scroll_position + (window_size.visible_byte_count() - 1).saturating_sub(Self::bottom_padding(window_size)) { self.align_view_bottom(window_size); } } + + fn clamp_primary_cursor_to_screen(&mut self, window_size: WindowSize) { + let min = self.scroll_position + self.top_padding(window_size); + let max = self.scroll_position + window_size.visible_byte_count() + .saturating_sub(Self::bottom_padding(window_size)) + .saturating_sub(BYTES_PER_LINE); + + if self.mode == Mode::Select { + self.primary_cursor.head = self.primary_cursor.head.clamp(min, max); + } else { + self.primary_cursor.clamp(min, max); + } + } } pub fn bytes_to_nat(bytes: &[u8]) -> Option { diff --git a/src/cursor.rs b/src/cursor.rs index 9e81d13..cd77a0c 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -59,12 +59,9 @@ impl Cursor { swap(&mut self.head, &mut self.tail); } - // TODO: in visual mode, should only clamp head - pub fn clamp(&mut self, scroll_position: usize, screen_size: usize) { - let max_row = scroll_position + screen_size - 1; - - self.head = self.head.clamp(scroll_position, max_row); - self.tail = self.tail.clamp(scroll_position, max_row); + pub fn clamp(&mut self, min: usize, max: usize) { + self.head = self.head.clamp(min, max); + self.tail = self.tail.clamp(min, max); } pub fn combine_with(&mut self, other: Self) { diff --git a/src/cursor/actions.rs b/src/cursor/actions.rs index be7adf8..4d13c92 100644 --- a/src/cursor/actions.rs +++ b/src/cursor/actions.rs @@ -1,5 +1,5 @@ use std::{cmp::min, mem::swap}; -use crate::{BYTES_PER_LINE, action::CursorAction, cursor::Cursor}; +use crate::{BYTES_PER_LINE, action::CursorAction, cursor::Cursor, utilities::{Floorable, SaturatingSubtract}}; impl Cursor { pub fn execute( @@ -114,12 +114,12 @@ impl Cursor { } pub const fn extend_line_start(&mut self) { - self.head -= self.head % BYTES_PER_LINE; + self.head.floor_to_the_nearest(BYTES_PER_LINE); } pub fn extend_line_end(&mut self, max: usize) { self.head = min( - self.head + BYTES_PER_LINE - 1 - (self.head % BYTES_PER_LINE), + self.head.floored_to_the_nearest(BYTES_PER_LINE) + BYTES_PER_LINE - 1, max ); } @@ -164,7 +164,7 @@ impl Cursor { self.tail = self.head - 1; self.head -= 4; } else { - self.head -= self.head % 4; + self.head.floor_to_the_nearest(4); } } @@ -194,7 +194,7 @@ impl Cursor { if self.head.is_multiple_of(4) { // at the beginning of a word self.head -= 4; } else { - self.head -= self.head % 4; + self.head.floor_to_the_nearest(4); } } @@ -208,9 +208,9 @@ impl Cursor { { self.head = min(self.head + BYTES_PER_LINE, max); } else { - self.tail -= self.tail % BYTES_PER_LINE; + self.tail.floor_to_the_nearest(BYTES_PER_LINE); self.head = min( - self.head + BYTES_PER_LINE - 1 - (self.head % BYTES_PER_LINE), + self.head.floored_to_the_nearest(BYTES_PER_LINE) + BYTES_PER_LINE - 1, max ); } @@ -225,11 +225,11 @@ impl Cursor { (self.tail % BYTES_PER_LINE == BYTES_PER_LINE - 1 || self.tail == max) { - self.head = self.head.saturating_sub(BYTES_PER_LINE); + self.head.saturating_subtract(BYTES_PER_LINE); } else { - self.head -= self.head % BYTES_PER_LINE; + self.head.floor_to_the_nearest(BYTES_PER_LINE); self.tail = min( - self.tail + BYTES_PER_LINE - 1 - (self.tail % BYTES_PER_LINE), + self.tail.floored_to_the_nearest(BYTES_PER_LINE) + BYTES_PER_LINE - 1, max ); } @@ -237,11 +237,7 @@ impl Cursor { } const fn previous_multiple_of(multiple: usize, number: usize) -> usize { - if number == 0 { - 0 - } else { - (number - 1) - ((number - 1) % multiple) - } + number.saturating_sub(1).floored_to_the_nearest(multiple) } mod tests { diff --git a/src/main.rs b/src/main.rs index 4920915..43fefc9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ #![feature(exact_bitshifts)] #![feature(hash_set_entry)] #![feature(trim_prefix_suffix)] +#![feature(const_trait_impl)] use arguments::Arguments; use clap::Parser; @@ -33,9 +34,6 @@ const LINES_OF_PADDING: usize = 5; const BYTES_OF_PADDING: usize = LINES_OF_PADDING * BYTES_PER_LINE; // TODO: -// - fix scroll clamping -// - fix scrolling in select mode -// - shouldn't clamp tail // - `go` goto entered offset // - search // - `/` hex, `A-/` ascii diff --git a/src/utilities.rs b/src/utilities.rs index 97190b6..ec8d1bc 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -1,7 +1,11 @@ mod cardinality; mod empty_span; mod custom_greys; +mod floor_to_the_nearest; +mod saturating_subtract; pub use cardinality::HasCardinality; pub use empty_span::empty_span; pub use custom_greys::CustomGreys; +pub use floor_to_the_nearest::Floorable; +pub use saturating_subtract::SaturatingSubtract; diff --git a/src/utilities/floor_to_the_nearest.rs b/src/utilities/floor_to_the_nearest.rs new file mode 100644 index 0000000..8ebbde5 --- /dev/null +++ b/src/utilities/floor_to_the_nearest.rs @@ -0,0 +1,16 @@ + +pub const trait Floorable { + fn floor_to_the_nearest(&mut self, step: Self); + fn floored_to_the_nearest(self, step: Self) -> Self; +} + +const impl Floorable for usize { + fn floor_to_the_nearest(&mut self, step: Self) { + *self -= *self % step; + } + + fn floored_to_the_nearest(self, step: Self) -> Self { + self - (self % step) + } +} + diff --git a/src/utilities/saturating_subtract.rs b/src/utilities/saturating_subtract.rs new file mode 100644 index 0000000..bb698b3 --- /dev/null +++ b/src/utilities/saturating_subtract.rs @@ -0,0 +1,9 @@ +pub trait SaturatingSubtract { + fn saturating_subtract(&mut self, other: Self); +} + +impl SaturatingSubtract for usize { + fn saturating_subtract(&mut self, other: Self) { + *self = self.saturating_sub(other); + } +}