use retroglyph_core::{Pos, Rect};
#[must_use]
pub fn thumb_geometry(
area: Rect,
total_len: usize,
visible_len: usize,
offset: usize,
) -> Option<(u16, u16)> {
let track = area.height();
if track == 0 || visible_len == 0 || total_len <= visible_len {
return None;
}
let track_f = f32::from(track);
#[allow(clippy::cast_precision_loss)]
let ratio = visible_len as f32 / total_len as f32;
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let len = (track_f * ratio).round().clamp(1.0, track_f) as u16;
let max_offset = total_len - visible_len; let max_start = track.saturating_sub(len); #[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let start = ((offset as f32 / max_offset as f32) * f32::from(max_start)).round() as u16;
Some((start.min(max_start), len))
}
#[must_use]
pub fn offset_for_pos(area: Rect, total_len: usize, visible_len: usize, pos: Pos) -> Option<usize> {
if !area.contains_pos(pos) || total_len <= visible_len {
return None;
}
let max_offset = total_len - visible_len;
let track = area.height().saturating_sub(1).max(1);
let rel = pos.y.saturating_sub(area.top()).min(track);
#[allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss
)]
let offset = (f32::from(rel) / f32::from(track) * max_offset as f32).round() as usize;
Some(offset.min(max_offset))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nothing_to_scroll_when_everything_fits() {
assert_eq!(thumb_geometry(Rect::new(0, 0, 1, 10), 5, 10, 0), None);
assert_eq!(thumb_geometry(Rect::new(0, 0, 1, 10), 10, 10, 0), None);
}
#[test]
fn thumb_shrinks_with_the_visible_fraction() {
let (_, len) = thumb_geometry(Rect::new(0, 0, 1, 20), 20, 10, 0).unwrap();
assert_eq!(len, 10);
let (_, len) = thumb_geometry(Rect::new(0, 0, 1, 20), 2000, 1, 0).unwrap();
assert_eq!(len, 1);
}
#[test]
fn thumb_moves_from_top_to_bottom_as_offset_increases() {
let area = Rect::new(0, 0, 1, 20);
let (start_at_zero, len) = thumb_geometry(area, 40, 10, 0).unwrap();
assert_eq!(start_at_zero, 0);
let (start_at_max, _) = thumb_geometry(area, 40, 10, 30).unwrap(); assert_eq!(start_at_max, area.height() - len);
let (start_at_mid, _) = thumb_geometry(area, 40, 10, 15).unwrap();
assert!(start_at_mid > start_at_zero && start_at_mid < start_at_max);
}
#[test]
fn offset_for_pos_round_trips_thumb_geometry_endpoints() {
let area = Rect::new(0, 0, 1, 20);
assert_eq!(
offset_for_pos(area, 40, 10, Pos::new(0, area.top())),
Some(0)
);
assert_eq!(
offset_for_pos(area, 40, 10, Pos::new(0, area.bottom() - 1)),
Some(30) );
}
#[test]
fn offset_for_pos_outside_the_area_is_none() {
let area = Rect::new(5, 5, 1, 10);
assert_eq!(offset_for_pos(area, 40, 10, Pos::new(0, 0)), None);
}
}