118 lines
3.6 KiB
Rust
118 lines
3.6 KiB
Rust
//! History-view scroll state.
|
|
//!
|
|
//! Anchored at the top: `top_offset` counts logical lines (pre-wrap) from
|
|
//! the top of the history buffer. `follow_tail` is sticky — when set, the
|
|
//! draw step recomputes `top_offset` every frame so the latest line is
|
|
//! pinned at the bottom of the viewport. New content appended at the tail
|
|
//! only shifts the visible range when `follow_tail` is true; when the
|
|
//! user has scrolled up manually, their view stays put.
|
|
|
|
pub struct Scroll {
|
|
pub follow_tail: bool,
|
|
pub top_offset: usize,
|
|
/// Cached by the renderer so key handlers can page or jump without
|
|
/// recomputing layout.
|
|
pub area_height: u16,
|
|
pub total_lines: usize,
|
|
/// Wrap-aware: the smallest `top_offset` that still leaves the last
|
|
/// logical line pinned to the bottom row. Scrolling past this would
|
|
/// just show empty space, so it doubles as the clamp for manual
|
|
/// scroll actions. Updated every frame by the renderer.
|
|
pub tail_top_offset: usize,
|
|
/// Line indices where each turn starts. Used for Ctrl-[/] navigation.
|
|
pub turn_starts: Vec<usize>,
|
|
}
|
|
|
|
impl Default for Scroll {
|
|
fn default() -> Self {
|
|
Self {
|
|
follow_tail: true,
|
|
top_offset: 0,
|
|
area_height: 0,
|
|
total_lines: 0,
|
|
tail_top_offset: 0,
|
|
turn_starts: Vec::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Scroll {
|
|
/// Maximum valid `top_offset` given the cached totals. Beyond this
|
|
/// the viewport would show only blank space past the tail. Uses the
|
|
/// wrap-aware offset so long CJK / wrapped lines stay visible.
|
|
pub fn max_top_offset(&self) -> usize {
|
|
self.tail_top_offset
|
|
}
|
|
|
|
pub fn scroll_up(&mut self, n: usize) {
|
|
self.follow_tail = false;
|
|
self.top_offset = self.top_offset.saturating_sub(n);
|
|
}
|
|
|
|
pub fn scroll_down(&mut self, n: usize) {
|
|
let max = self.max_top_offset();
|
|
let next = self.top_offset.saturating_add(n).min(max);
|
|
self.top_offset = next;
|
|
if next >= max {
|
|
self.follow_tail = true;
|
|
}
|
|
}
|
|
|
|
pub fn page_up(&mut self) {
|
|
let step = self.area_height.saturating_sub(1).max(1) as usize;
|
|
self.scroll_up(step);
|
|
}
|
|
|
|
pub fn page_down(&mut self) {
|
|
let step = self.area_height.saturating_sub(1).max(1) as usize;
|
|
self.scroll_down(step);
|
|
}
|
|
|
|
pub fn to_top(&mut self) {
|
|
self.follow_tail = false;
|
|
self.top_offset = 0;
|
|
}
|
|
|
|
pub fn to_bottom(&mut self) {
|
|
self.follow_tail = true;
|
|
}
|
|
|
|
pub fn jump_prev_turn(&mut self) {
|
|
// Find the last turn start strictly above the current top.
|
|
let current = self.top_offset;
|
|
let target = self
|
|
.turn_starts
|
|
.iter()
|
|
.rev()
|
|
.copied()
|
|
.find(|&start| start < current);
|
|
if let Some(t) = target {
|
|
self.follow_tail = false;
|
|
self.top_offset = t;
|
|
} else if !self.turn_starts.is_empty() {
|
|
// Already at or above the first turn; pin to top.
|
|
self.follow_tail = false;
|
|
self.top_offset = 0;
|
|
}
|
|
}
|
|
|
|
pub fn jump_next_turn(&mut self) {
|
|
let current = self.top_offset;
|
|
let target = self
|
|
.turn_starts
|
|
.iter()
|
|
.copied()
|
|
.find(|&start| start > current);
|
|
if let Some(t) = target {
|
|
self.follow_tail = false;
|
|
let max = self.max_top_offset();
|
|
self.top_offset = t.min(max);
|
|
if self.top_offset >= max {
|
|
self.follow_tail = true;
|
|
}
|
|
} else {
|
|
self.to_bottom();
|
|
}
|
|
}
|
|
}
|