use std::time::Duration;
use super::input_history::InputHistory;
use super::scroll_animator::ScrollAnimator;
use crate::frame::VisualLine;
const HISTORY_WINDOW: Duration = Duration::from_millis(5000);
const HISTORY_CAP: usize = 128;
#[derive(Clone, Copy)]
pub(super) struct Layout {
pub sidebar_cols: u16,
pub image_col: u16, pub image_cols: u16, pub image_rows: u16, pub status_row: u16, pub cell_w: u16, pub cell_h: u16, }
pub(super) struct ScrollState {
pub y_offset: u32,
pub anchor: f64,
pub img_h: u32, pub vp_w: u32, pub vp_h: u32, pub animator: ScrollAnimator,
pub input_history: InputHistory,
}
impl ScrollState {
pub fn new(
initial_y: u32,
img_h: u32,
vp_w: u32,
vp_h: u32,
animation: crate::config::ScrollAnimation,
) -> Self {
Self {
y_offset: initial_y,
anchor: initial_y as f64,
img_h,
vp_w,
vp_h,
animator: ScrollAnimator::from_config(initial_y as f64, animation),
input_history: InputHistory::new(HISTORY_WINDOW, HISTORY_CAP),
}
}
pub fn derived_target(&self, max_scroll: u32) -> u32 {
let sum: i64 = self.input_history.iter().map(|r| r.delta_px as i64).sum();
let raw = (self.anchor.round() as i64 + sum).max(0);
(raw as u32).min(max_scroll)
}
pub fn current_position(&self, now: std::time::Instant) -> f64 {
self.animator
.current_position(self.anchor, &self.input_history, now)
}
pub fn is_animating(&self) -> bool {
self.animator
.is_animating(self.anchor, &self.input_history, std::time::Instant::now())
}
pub fn tick(&mut self, dt: Duration) -> bool {
let prev = self.y_offset;
let now = std::time::Instant::now();
let current =
self.animator
.tick(self.anchor, &self.input_history, self.vp_h as f64, now, dt);
self.y_offset = current.round().max(0.0) as u32;
self.y_offset != prev
}
}
impl Layout {
pub(super) fn viewport_width_pt(&self, ppi: f64) -> f64 {
self.image_cols as f64 * self.cell_w as f64 * 72.0 / ppi
}
pub(super) fn viewport_height_pt(&self, ppi: f64) -> f64 {
self.image_rows as f64 * self.cell_h as f64 * 72.0 / ppi
}
pub(super) fn sidebar_width_pt(&self, ppi: f64) -> f64 {
self.sidebar_cols as f64 * self.cell_w as f64 * 72.0 / ppi
}
pub(super) fn align_tile_height_pt(&self, tile_height_pt: f64, ppi: f64) -> f64 {
let raw_px = (tile_height_pt * ppi / 72.0).round() as u32;
let cell_h = self.cell_h as u32;
let aligned_px = raw_px.div_ceil(cell_h) * cell_h;
aligned_px as f64 * 72.0 / ppi
}
}
pub(super) fn compute_layout(
term_cols: u16,
term_rows: u16,
pixel_w: u16,
pixel_h: u16,
sidebar_cols: u16,
) -> Layout {
let image_col = sidebar_cols;
let image_cols = term_cols.saturating_sub(sidebar_cols);
let image_rows = term_rows.saturating_sub(1);
let status_row = term_rows.saturating_sub(1);
let cell_w = pixel_w.checked_div(term_cols).unwrap_or(1);
let cell_h = pixel_h.checked_div(term_rows).unwrap_or(1);
Layout {
sidebar_cols,
image_col,
image_cols,
image_rows,
status_row,
cell_w,
cell_h,
}
}
pub(super) fn vp_dims(layout: &Layout, img_w: u32, img_h: u32) -> (u32, u32) {
let vp_w = (layout.image_cols as u32 * layout.cell_w as u32).min(img_w);
let vp_h = (layout.image_rows as u32 * layout.cell_h as u32).min(img_h);
(vp_w, vp_h)
}
pub(super) fn visual_line_offset(
visual_lines: &[VisualLine],
max_scroll: u32,
line_num: u32,
) -> u32 {
let idx = (line_num as usize).saturating_sub(1); if idx < visual_lines.len() {
visual_lines[idx.saturating_sub(1)].y_px.min(max_scroll)
} else {
0
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compute_layout_basic() {
let l = compute_layout(80, 24, 1280, 576, 6);
assert_eq!(l.sidebar_cols, 6);
assert_eq!(l.image_col, 6);
assert_eq!(l.image_cols, 74);
assert_eq!(l.image_rows, 23);
assert_eq!(l.status_row, 23);
assert_eq!(l.cell_w, 16); assert_eq!(l.cell_h, 24); }
#[test]
fn compute_layout_zero_cols_no_panic() {
let l = compute_layout(0, 0, 0, 0, 0);
assert_eq!(l.image_cols, 0);
assert_eq!(l.cell_w, 1); assert_eq!(l.cell_h, 1);
}
#[test]
fn visual_line_offset_first_line() {
let vls = vec![
VisualLine {
y_pt: 0.0,
y_px: 0,
md_block_range: None,
md_offset: None,
diff_status: None,
},
VisualLine {
y_pt: 0.0,
y_px: 100,
md_block_range: None,
md_offset: None,
diff_status: None,
},
];
assert_eq!(visual_line_offset(&vls, 1000, 1), 0);
}
#[test]
fn visual_line_offset_middle_line() {
let vls = vec![
VisualLine {
y_pt: 0.0,
y_px: 0,
md_block_range: None,
md_offset: None,
diff_status: None,
},
VisualLine {
y_pt: 0.0,
y_px: 100,
md_block_range: None,
md_offset: None,
diff_status: None,
},
VisualLine {
y_pt: 0.0,
y_px: 200,
md_block_range: None,
md_offset: None,
diff_status: None,
},
];
assert_eq!(visual_line_offset(&vls, 1000, 3), 100);
}
#[test]
fn visual_line_offset_out_of_range() {
let vls = vec![VisualLine {
y_pt: 0.0,
y_px: 0,
md_block_range: None,
md_offset: None,
diff_status: None,
}];
assert_eq!(visual_line_offset(&vls, 1000, 99), 0);
}
#[test]
fn visual_line_offset_clamps_to_max() {
let vls = vec![
VisualLine {
y_pt: 0.0,
y_px: 0,
md_block_range: None,
md_offset: None,
diff_status: None,
},
VisualLine {
y_pt: 0.0,
y_px: 500,
md_block_range: None,
md_offset: None,
diff_status: None,
},
];
assert_eq!(visual_line_offset(&vls, 100, 2), 0);
let vls2 = vec![
VisualLine {
y_pt: 0.0,
y_px: 0,
md_block_range: None,
md_offset: None,
diff_status: None,
},
VisualLine {
y_pt: 0.0,
y_px: 9999,
md_block_range: None,
md_offset: None,
diff_status: None,
},
VisualLine {
y_pt: 0.0,
y_px: 10000,
md_block_range: None,
md_offset: None,
diff_status: None,
},
];
assert_eq!(visual_line_offset(&vls2, 500, 3), 500);
}
#[test]
fn vp_dims_viewport_smaller_than_image() {
let l = compute_layout(80, 24, 1280, 576, 6);
let (vp_w, vp_h) = vp_dims(&l, 2000, 5000);
assert_eq!(vp_w, 1184);
assert_eq!(vp_h, 552);
}
#[test]
fn vp_dims_viewport_larger_than_image() {
let l = compute_layout(80, 24, 1280, 576, 6);
let (vp_w, vp_h) = vp_dims(&l, 100, 200);
assert_eq!(vp_w, 100);
assert_eq!(vp_h, 200);
}
#[test]
fn layout_pt_conversions() {
let l = compute_layout(80, 24, 1280, 576, 6);
let ppi = 144.0;
assert_eq!(l.viewport_width_pt(ppi), 592.0);
assert_eq!(l.viewport_height_pt(ppi), 276.0);
assert_eq!(l.sidebar_width_pt(ppi), 48.0);
}
#[test]
fn align_tile_height_rounds_up_to_cell_boundary() {
let l = compute_layout(80, 24, 1280, 576, 6);
let ppi = 144.0;
assert_eq!(l.align_tile_height_pt(276.0, ppi), 276.0);
assert_eq!(l.align_tile_height_pt(277.0, ppi), 288.0);
}
}