pub(crate) const PRECISION_TOLERANCE: f64 = 1e-10;
pub(crate) const HERO_START_WEIGHTS: &[u16] = &[8, 2];
pub(crate) const HERO_CENTER_WEIGHTS: &[u16] = &[2, 6, 2];
pub(crate) const HERO_END_WEIGHTS: &[u16] = &[2, 8];
pub(crate) const CONTAINED_WEIGHTS: &[u16] = &[5, 4, 1];
pub(crate) const CONTAINED_EXTENDED_WEIGHTS: &[u16] = &[4, 3, 2, 1];
fn finite(value: f64) -> f64 {
if value.is_finite() { value } else { 0.0 }
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Slot {
pub(crate) index: usize,
pub(crate) offset: f64,
pub(crate) extent: f64,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct WeightedLayout<'w> {
weights: &'w [u16],
viewport: f64,
shrink_extent: f64,
scroll_offset: f64,
consume_max_weight: bool,
}
impl<'w> WeightedLayout<'w> {
pub(crate) fn new(
weights: &'w [u16],
viewport: f64,
shrink_extent: f64,
scroll_offset: f64,
consume_max_weight: bool,
) -> Self {
Self {
weights,
viewport: finite(viewport).max(0.0),
shrink_extent: finite(shrink_extent).max(0.0),
scroll_offset: finite(scroll_offset).max(0.0),
consume_max_weight,
}
}
fn is_degenerate(&self) -> bool {
self.viewport <= 0.0 || self.weights.is_empty() || self.total_weight() <= 0.0
}
fn total_weight(&self) -> f64 {
self.weights.iter().map(|w| f64::from(*w)).sum()
}
fn max_weight(&self) -> f64 {
self.weights
.iter()
.map(|w| f64::from(*w))
.fold(0.0, f64::max)
}
fn min_weight(&self) -> f64 {
self.weights
.iter()
.map(|w| f64::from(*w))
.fold(f64::INFINITY, f64::min)
}
pub(crate) fn extent_unit(&self) -> f64 {
if self.is_degenerate() {
return 0.0;
}
self.viewport / self.total_weight()
}
pub(crate) fn first_child_extent(&self) -> f64 {
if self.is_degenerate() {
return 0.0;
}
f64::from(self.weights[0]) * self.extent_unit()
}
pub(crate) fn max_child_extent(&self) -> f64 {
self.max_weight() * self.extent_unit()
}
pub(crate) fn min_child_extent(&self) -> f64 {
if self.is_degenerate() {
return 0.0;
}
self.min_weight() * self.extent_unit()
}
fn effective_shrink_extent(&self) -> f64 {
self.shrink_extent.max(0.0).min(self.min_child_extent())
}
fn smaller_leading_weights(&self) -> i64 {
let max = self.max_weight();
self.weights
.iter()
.take_while(|w| f64::from(**w) < max)
.count() as i64
}
fn raw_leading_index(&self) -> i64 {
let first = self.first_child_extent();
if first <= 0.0 {
return 0;
}
let actual = self.scroll_offset / first;
if !actual.is_finite() {
return 0;
}
let round = actual.round();
if (actual - round).abs() < PRECISION_TOLERANCE {
round as i64
} else {
actual.floor() as i64
}
}
pub(crate) fn first_visible_index(&self) -> i64 {
if self.is_degenerate() {
return 0;
}
if self.consume_max_weight {
self.raw_leading_index() - self.smaller_leading_weights()
} else {
self.raw_leading_index()
}
}
fn offscreen_extent(&self) -> f64 {
if self.is_degenerate() {
return 0.0;
}
self.scroll_offset - self.raw_leading_index() as f64 * self.first_child_extent()
}
fn distance_to_leading_edge(&self) -> f64 {
self.first_child_extent() - self.offscreen_extent()
}
fn extent_within_weights(&self, slot: usize) -> f64 {
let first = self.first_child_extent();
if first <= 0.0 || slot == 0 || slot >= self.weights.len() {
return 0.0;
}
let curr = f64::from(self.weights[slot]);
let prev = f64::from(self.weights[slot - 1]);
let progress = self.offscreen_extent() / first;
let max_weight = self.max_weight();
let final_increase = if max_weight > 0.0 {
(prev - curr) / max_weight
} else {
0.0
};
self.extent_unit() * curr + final_increase * progress * self.max_child_extent()
}
fn total_before(&self, index: i64) -> f64 {
let first = self.first_visible_index();
let mut total = self.distance_to_leading_edge();
let shrink = self.effective_shrink_extent();
let mut i = first + 1;
while i < index {
let slot = (i - first) as usize;
total += if slot < self.weights.len() {
self.extent_within_weights(slot)
} else {
(self.viewport - total).max(shrink)
};
i += 1;
}
total
}
pub(crate) fn item_extent(&self, index: i64) -> f64 {
if self.is_degenerate() {
return 0.0;
}
let first = self.first_visible_index();
let shrink = self.effective_shrink_extent();
if index == first {
return self.distance_to_leading_edge().max(shrink);
}
if index > first {
let slot = (index - first) as usize;
if slot < self.weights.len() {
return self.extent_within_weights(slot);
}
return (self.viewport - self.total_before(index)).max(shrink);
}
self.min_child_extent().max(shrink)
}
pub(crate) fn layout_offset(&self, index: i64) -> f64 {
if self.is_degenerate() {
return 0.0;
}
let first = self.first_visible_index();
if index == first {
let distance = self.distance_to_leading_edge();
let shrink = self.effective_shrink_extent();
if distance <= shrink {
return self.scroll_offset - shrink + distance;
}
return self.scroll_offset;
}
self.scroll_offset + self.total_before(index)
}
pub(crate) fn slots(&self, item_count: usize) -> Vec<Slot> {
let mut out = Vec::new();
if self.is_degenerate() || item_count == 0 {
return out;
}
let first = self.first_visible_index();
let count = item_count as i64;
if first >= count {
return out;
}
if first >= 0 {
out.push(Slot {
index: first as usize,
offset: self.layout_offset(first) - self.scroll_offset,
extent: self.item_extent(first),
});
}
let shrink = self.effective_shrink_extent();
let mut total = self.distance_to_leading_edge();
let mut i = first + 1;
while i < count && total < self.viewport {
let slot = (i - first) as usize;
let extent = if slot < self.weights.len() {
self.extent_within_weights(slot)
} else {
(self.viewport - total).max(shrink)
};
if i >= 0 {
out.push(Slot {
index: i as usize,
offset: total,
extent,
});
}
total += extent;
i += 1;
}
out
}
pub(crate) fn max_scroll_offset(&self, item_count: usize) -> f64 {
let last_window = (item_count as f64 - self.weights.len() as f64).max(0.0);
last_window * self.first_child_extent()
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FixedLayout {
item_extent: f64,
shrink_extent: f64,
viewport: f64,
scroll_offset: f64,
}
impl FixedLayout {
pub(crate) fn new(
item_extent: f64,
shrink_extent: f64,
viewport: f64,
scroll_offset: f64,
) -> Self {
let viewport = finite(viewport).max(0.0);
Self {
item_extent: finite(item_extent).max(0.0).min(viewport),
shrink_extent: finite(shrink_extent).max(0.0),
viewport,
scroll_offset: finite(scroll_offset).max(0.0),
}
}
pub(crate) fn stride(&self) -> f64 {
self.item_extent
}
fn effective_min_extent(&self) -> f64 {
if self.item_extent <= 0.0 {
return self.shrink_extent;
}
(self.viewport % self.item_extent).max(self.shrink_extent)
}
pub(crate) fn first_visible_index(&self) -> i64 {
if self.item_extent <= 0.0 {
return 0;
}
let actual = self.scroll_offset / self.item_extent;
if !actual.is_finite() {
return 0;
}
actual.floor() as i64
}
fn max_child_index_for_scroll_offset(&self, scroll_offset: f64) -> i64 {
if self.item_extent <= 0.0 {
return 0;
}
let actual = scroll_offset / self.item_extent - 1.0;
if !actual.is_finite() {
return 0;
}
let round = actual.round();
if ((actual - round) * self.item_extent).abs() < PRECISION_TOLERANCE {
(round as i64).max(0)
} else {
(actual.ceil() as i64).max(0)
}
}
pub(crate) fn item_extent_at(&self, index: i64) -> f64 {
if self.item_extent <= 0.0 {
return 0.0;
}
let min = self.effective_min_extent();
let offscreen = self.scroll_offset - self.first_visible_index() as f64 * self.item_extent;
if index == self.first_visible_index() {
return (self.item_extent - offscreen).max(min);
}
let trailing_offset = self.scroll_offset + self.viewport;
if index == self.max_child_index_for_scroll_offset(trailing_offset) {
let raw = trailing_offset - self.item_extent * index as f64;
return raw.max(min.min(self.item_extent)).min(self.item_extent);
}
self.item_extent
}
pub(crate) fn layout_offset(&self, index: i64) -> f64 {
if self.item_extent <= 0.0 {
return 0.0;
}
let first = self.first_visible_index();
if index == first {
let min = self.effective_min_extent();
if self.item_extent_at(index) <= min {
return self.item_extent * index as f64 - min + self.item_extent;
}
return self.scroll_offset;
}
self.item_extent * index as f64
}
pub(crate) fn slots(&self, item_count: usize) -> Vec<Slot> {
let mut out = Vec::new();
if self.item_extent <= 0.0 || self.viewport <= 0.0 || item_count == 0 {
return out;
}
let count = item_count as i64;
let first = self.first_visible_index().max(0);
let mut i = first;
while i < count {
let offset = self.layout_offset(i) - self.scroll_offset;
if offset >= self.viewport {
break;
}
out.push(Slot {
index: i as usize,
offset,
extent: self.item_extent_at(i),
});
i += 1;
}
out
}
pub(crate) fn max_scroll_offset(&self, item_count: usize) -> f64 {
(item_count as f64 * self.item_extent - self.viewport).max(0.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn covered(slots: &[Slot]) -> f64 {
slots.iter().map(|s| s.extent).sum()
}
#[test]
fn contained_resting_extents_are_the_weight_shares_of_the_viewport() {
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, 0.0, false);
assert_eq!(l.extent_unit(), 40.0);
assert_eq!(l.first_child_extent(), 200.0);
assert_eq!(l.max_child_extent(), 200.0);
assert_eq!(l.min_child_extent(), 40.0);
let slots = l.slots(6);
assert_eq!(slots.len(), 3, "three weighted slots fill the viewport");
assert_eq!(
slots[0],
Slot {
index: 0,
offset: 0.0,
extent: 200.0
}
);
assert_eq!(
slots[1],
Slot {
index: 1,
offset: 200.0,
extent: 160.0
}
);
assert_eq!(
slots[2],
Slot {
index: 2,
offset: 360.0,
extent: 40.0
}
);
assert_eq!(covered(&slots), 400.0);
}
#[test]
fn hero_center_resting_extents_put_the_big_slot_in_the_middle() {
let l = WeightedLayout::new(HERO_CENTER_WEIGHTS, 500.0, 0.0, 0.0, false);
let slots = l.slots(5);
assert_eq!(
slots
.iter()
.map(|s| (s.offset, s.extent))
.collect::<Vec<_>>(),
vec![(0.0, 100.0), (100.0, 300.0), (400.0, 100.0)]
);
}
#[test]
fn hero_start_and_end_mirror_each_other() {
let start = WeightedLayout::new(HERO_START_WEIGHTS, 500.0, 0.0, 0.0, false);
let end = WeightedLayout::new(HERO_END_WEIGHTS, 500.0, 0.0, 0.0, false);
assert_eq!(
start.slots(4).iter().map(|s| s.extent).collect::<Vec<_>>(),
vec![400.0, 100.0]
);
assert_eq!(
end.slots(4).iter().map(|s| s.extent).collect::<Vec<_>>(),
vec![100.0, 400.0]
);
}
#[test]
fn extended_contained_shows_four_descending_slots() {
let l = WeightedLayout::new(CONTAINED_EXTENDED_WEIGHTS, 500.0, 0.0, 0.0, false);
assert_eq!(
l.slots(8).iter().map(|s| s.extent).collect::<Vec<_>>(),
vec![200.0, 150.0, 100.0, 50.0]
);
}
#[test]
fn a_second_viewport_width_rescales_every_extent_proportionally() {
let narrow = WeightedLayout::new(CONTAINED_WEIGHTS, 200.0, 0.0, 0.0, false);
let wide = WeightedLayout::new(CONTAINED_WEIGHTS, 800.0, 0.0, 0.0, false);
for (n, w) in narrow.slots(6).iter().zip(wide.slots(6).iter()) {
assert_eq!(n.extent * 4.0, w.extent);
assert_eq!(n.offset * 4.0, w.offset);
}
}
#[test]
fn a_half_item_scroll_morphs_every_slot_between_its_weights() {
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, 100.0, false);
let slots = l.slots(6);
assert_eq!(
slots[0],
Slot {
index: 0,
offset: 0.0,
extent: 100.0
}
);
assert_eq!(slots[1].extent, 180.0);
assert_eq!(slots[2].extent, 100.0);
assert_eq!(slots[3].extent, 20.0);
assert_eq!(covered(&slots), 400.0);
}
#[test]
fn scrolling_one_whole_item_reproduces_the_resting_layout_one_index_along() {
let rest = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, 0.0, false);
let stepped = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, 200.0, false);
let rest_slots = rest.slots(6);
let stepped_slots = stepped.slots(6);
assert_eq!(stepped_slots[0].index, 1, "item 1 now leads");
for (r, s) in rest_slots.iter().zip(stepped_slots.iter()) {
assert_eq!(r.offset, s.offset);
assert!((r.extent - s.extent).abs() < 1e-9);
}
}
#[test]
fn slot_extents_stay_monotone_across_a_continuous_scroll() {
let mut prev_leading = f64::INFINITY;
let mut prev_second = 0.0;
for step in 0..=20 {
let offset = f64::from(step) * 10.0;
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, offset, false);
let slots = l.slots(6);
if offset >= 200.0 {
break;
}
assert!(slots[0].extent <= prev_leading + 1e-9);
assert!(slots[1].extent >= prev_second - 1e-9);
assert!(
(covered(&slots) - 400.0).abs() < 1e-9,
"the visible slots always tile the viewport exactly"
);
prev_leading = slots[0].extent;
prev_second = slots[1].extent;
}
}
#[test]
fn the_shrink_extent_floors_the_leading_item_and_slides_it_off() {
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 30.0, 180.0, false);
let slots = l.slots(6);
assert_eq!(slots[0].extent, 30.0);
assert_eq!(
slots[0].offset, -10.0,
"it slides off rather than shrinking"
);
}
#[test]
fn the_shrink_extent_can_never_exceed_the_smallest_slot() {
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 1000.0, 199.0, false);
assert_eq!(l.item_extent(0), 40.0);
}
#[test]
fn consume_max_weight_leaves_a_phantom_slot_before_item_zero() {
let l = WeightedLayout::new(HERO_CENTER_WEIGHTS, 500.0, 0.0, 0.0, true);
assert_eq!(l.first_visible_index(), -1, "one smaller leading weight");
let slots = l.slots(5);
assert_eq!(slots[0].index, 0);
assert_eq!(slots[0].offset, 100.0, "the phantom leading slot's width");
assert_eq!(slots[0].extent, 300.0, "item 0 occupies the max slot");
}
#[test]
fn without_consume_max_weight_item_zero_leads_the_smallest_slot() {
let l = WeightedLayout::new(HERO_CENTER_WEIGHTS, 500.0, 0.0, 0.0, false);
assert_eq!(l.first_visible_index(), 0);
assert_eq!(l.slots(5)[0].offset, 0.0);
}
#[test]
fn max_scroll_offset_stops_with_a_full_window_showing() {
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, 0.0, false);
assert_eq!(l.max_scroll_offset(6), 600.0);
assert_eq!(l.max_scroll_offset(2), 0.0);
}
#[test]
fn a_zero_viewport_degrades_to_zero_everywhere_instead_of_dividing_by_it() {
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 0.0, 0.0, 50.0, false);
assert_eq!(l.extent_unit(), 0.0);
assert_eq!(l.first_child_extent(), 0.0);
assert_eq!(l.item_extent(0), 0.0);
assert_eq!(l.layout_offset(0), 0.0);
assert!(l.slots(5).is_empty());
}
#[test]
fn non_finite_inputs_degrade_instead_of_poisoning_every_extent() {
let l = WeightedLayout::new(CONTAINED_WEIGHTS, f64::NAN, f64::NAN, f64::INFINITY, false);
assert_eq!(l.extent_unit(), 0.0);
assert!(l.slots(5).is_empty());
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, f64::NAN, false);
assert!(l.slots(5).iter().all(|s| s.extent.is_finite()));
}
#[test]
fn an_empty_weight_list_is_degenerate_rather_than_a_panic() {
let l = WeightedLayout::new(&[], 400.0, 0.0, 100.0, false);
assert_eq!(l.first_child_extent(), 0.0);
assert!(l.slots(5).is_empty());
assert_eq!(l.max_scroll_offset(5), 0.0);
}
#[test]
fn an_all_zero_weight_list_is_degenerate_rather_than_a_division_by_zero() {
let l = WeightedLayout::new(&[0, 0], 400.0, 0.0, 100.0, false);
assert_eq!(l.extent_unit(), 0.0);
assert!(l.slots(5).is_empty());
}
#[test]
fn a_scroll_offset_within_precision_tolerance_pins_to_the_boundary() {
let exact = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, 200.0, false);
let fuzzy = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, 200.0 - 1e-12, false);
assert_eq!(exact.first_visible_index(), fuzzy.first_visible_index());
}
#[test]
fn fewer_items_than_slots_produces_only_the_items_that_exist() {
let l = WeightedLayout::new(CONTAINED_WEIGHTS, 400.0, 0.0, 0.0, false);
let slots = l.slots(2);
assert_eq!(slots.len(), 2);
assert_eq!(slots[0].extent, 200.0);
assert_eq!(slots[1].extent, 160.0);
}
#[test]
fn uncontained_items_all_rest_at_the_item_extent() {
let l = FixedLayout::new(100.0, 0.0, 350.0, 0.0);
let slots = l.slots(10);
assert_eq!(
slots[0],
Slot {
index: 0,
offset: 0.0,
extent: 100.0
}
);
assert_eq!(
slots[1],
Slot {
index: 1,
offset: 100.0,
extent: 100.0
}
);
assert_eq!(slots[2].extent, 100.0);
assert_eq!(slots[3].offset, 300.0);
assert_eq!(slots[3].extent, 50.0);
assert_eq!(slots.len(), 4);
}
#[test]
fn the_uncontained_leading_item_shrinks_as_it_scrolls_off() {
let l = FixedLayout::new(100.0, 0.0, 300.0, 40.0);
let slots = l.slots(10);
assert_eq!(slots[0].index, 0);
assert_eq!(slots[0].offset, 0.0);
assert_eq!(slots[0].extent, 60.0, "40px of it scrolled off");
assert_eq!(slots[1].offset, 60.0);
}
#[test]
fn the_uncontained_item_extent_is_capped_at_the_viewport() {
let l = FixedLayout::new(900.0, 0.0, 300.0, 0.0);
assert_eq!(l.stride(), 300.0);
assert_eq!(l.slots(3).len(), 1);
}
#[test]
fn uncontained_max_scroll_offset_is_the_ordinary_list_bound() {
let l = FixedLayout::new(100.0, 0.0, 350.0, 0.0);
assert_eq!(l.max_scroll_offset(10), 650.0);
assert_eq!(l.max_scroll_offset(2), 0.0);
}
#[test]
fn a_zero_extent_uncontained_layout_degrades_instead_of_dividing_by_zero() {
let l = FixedLayout::new(0.0, 0.0, 300.0, 20.0);
assert_eq!(l.item_extent_at(0), 0.0);
assert_eq!(l.layout_offset(0), 0.0);
assert!(l.slots(5).is_empty());
}
#[test]
fn non_finite_uncontained_inputs_degrade_to_zero() {
let l = FixedLayout::new(f64::NAN, 0.0, f64::NAN, f64::NAN);
assert_eq!(l.stride(), 0.0);
assert!(l.slots(5).is_empty());
}
}