use concinnity_core::gfx::overlay::UI_REFERENCE_SIZE;
pub(crate) const MAX_VISIBLE: usize = 8;
pub(crate) fn visible_count(count: usize) -> usize {
count.min(MAX_VISIBLE)
}
pub(crate) fn max_first(count: usize) -> usize {
count.saturating_sub(MAX_VISIBLE)
}
pub(crate) fn first_for_selected(selected: usize, count: usize) -> usize {
selected
.saturating_sub(MAX_VISIBLE / 2)
.min(max_first(count))
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct DropdownLayout {
pub list: [f32; 4],
pub items: Vec<[f32; 4]>,
}
pub(crate) fn layout(anchor: [f32; 4], count: usize) -> DropdownLayout {
let [ax, ay, aw, ah] = anchor;
let count = visible_count(count);
let item_h = ah;
let list_h = item_h * count as f32;
let ref_h = UI_REFERENCE_SIZE[1];
let below_top = ay + ah;
let above_top = ay - list_h;
let mut top = if below_top + list_h <= ref_h || above_top < 0.0 {
below_top
} else {
above_top
};
let max_top = (ref_h - list_h).max(0.0);
top = top.clamp(0.0, max_top);
let items = (0..count)
.map(|i| [ax, top + i as f32 * item_h, aw, item_h])
.collect();
DropdownLayout {
list: [ax, top, aw, list_h],
items,
}
}
pub(crate) fn item_at(layout: &DropdownLayout, px: f32, py: f32) -> Option<usize> {
layout
.items
.iter()
.position(|&[x, y, w, h]| px >= x && px < x + w && py >= y && py < y + h)
}
const THUMB_W: f32 = 14.0;
const THUMB_PAD: f32 = 2.0;
const THUMB_MIN_H: f32 = 16.0;
const THUMB_MAX_H: f32 = 28.0;
fn thumb_height(list_h: f32, count: usize) -> f32 {
let frac = visible_count(count) as f32 / count.max(1) as f32;
(list_h * frac).clamp(THUMB_MIN_H, THUMB_MAX_H)
}
pub(crate) fn thumb_rect(layout: &DropdownLayout, first: usize, count: usize) -> Option<[f32; 4]> {
let visible = visible_count(count);
if count <= visible || visible == 0 {
return None;
}
let [lx, ly, lw, lh] = layout.list;
let thumb_h = thumb_height(lh, count);
let travel = lh - thumb_h;
let pos = first as f32 / max_first(count) as f32;
Some([
lx + lw - THUMB_W - THUMB_PAD,
ly + travel * pos.clamp(0.0, 1.0),
THUMB_W,
thumb_h,
])
}
pub(crate) fn track_rect(layout: &DropdownLayout, count: usize) -> Option<[f32; 4]> {
if count <= visible_count(count) {
return None;
}
let [lx, ly, lw, lh] = layout.list;
let w = THUMB_W + 2.0 * THUMB_PAD;
Some([lx + lw - w, ly, w, lh])
}
pub(crate) fn first_for_thumb_top(layout: &DropdownLayout, count: usize, top: f32) -> f32 {
let [_, ly, _, lh] = layout.list;
let travel = lh - thumb_height(lh, count);
if travel <= 0.0 {
return 0.0;
}
((top - ly) / travel).clamp(0.0, 1.0) * max_first(count) as f32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opens_downward_with_room_below() {
let anchor = [400.0, 100.0, 200.0, 40.0];
let l = layout(anchor, 3);
assert_eq!(l.list, [400.0, 140.0, 200.0, 120.0]);
assert_eq!(l.items.len(), 3);
assert_eq!(l.items[0], [400.0, 140.0, 200.0, 40.0]);
assert_eq!(l.items[1], [400.0, 180.0, 200.0, 40.0]);
assert_eq!(l.items[2], [400.0, 220.0, 200.0, 40.0]);
}
#[test]
fn flips_upward_when_it_would_overflow_the_bottom() {
let anchor = [400.0, 680.0, 200.0, 40.0];
let l = layout(anchor, 4); assert_eq!(l.list, [400.0, 520.0, 200.0, 160.0]);
assert_eq!(l.items[0], [400.0, 520.0, 200.0, 40.0]);
assert_eq!(l.items[3], [400.0, 640.0, 200.0, 40.0]);
}
#[test]
fn long_lists_window_to_max_visible() {
let anchor = [0.0, 300.0, 100.0, 40.0];
let l = layout(anchor, 40);
assert_eq!(l.items.len(), MAX_VISIBLE);
assert_eq!(l.list[3], 40.0 * MAX_VISIBLE as f32);
assert_eq!(l.list[1], 340.0);
assert_eq!(layout(anchor, 3).items.len(), 3);
}
#[test]
fn scroll_window_helpers_clamp() {
assert_eq!(visible_count(3), 3);
assert_eq!(visible_count(40), MAX_VISIBLE);
assert_eq!(max_first(MAX_VISIBLE), 0);
assert_eq!(max_first(40), 40 - MAX_VISIBLE);
assert_eq!(first_for_selected(0, 40), 0);
assert_eq!(first_for_selected(20, 40), 20 - MAX_VISIBLE / 2);
assert_eq!(first_for_selected(39, 40), 40 - MAX_VISIBLE);
assert_eq!(first_for_selected(2, 3), 0);
}
#[test]
fn thumb_tracks_the_scroll_position() {
let l = layout([0.0, 100.0, 100.0, 40.0], 16); let top = thumb_rect(&l, 0, 16).unwrap();
assert_eq!(top[1], l.list[1]);
let bottom = thumb_rect(&l, 8, 16).unwrap();
assert_eq!(bottom[1] + bottom[3], l.list[1] + l.list[3]);
assert!(thumb_rect(&layout([0.0, 100.0, 100.0, 40.0], 4), 0, 4).is_none());
}
#[test]
fn thumb_height_clamps_to_a_grabbable_handle() {
let l = layout([0.0, 100.0, 100.0, 40.0], 1000);
assert_eq!(thumb_rect(&l, 0, 1000).unwrap()[3], THUMB_MIN_H);
let l = layout([0.0, 100.0, 100.0, 40.0], 9);
assert_eq!(thumb_rect(&l, 0, 9).unwrap()[3], THUMB_MAX_H);
}
#[test]
fn track_strip_spans_the_list_and_contains_the_thumb() {
let l = layout([0.0, 100.0, 200.0, 40.0], 16);
let track = track_rect(&l, 16).unwrap();
assert_eq!(track[1], l.list[1]);
assert_eq!(track[3], l.list[3]);
assert_eq!(track[0] + track[2], l.list[0] + l.list[2]);
let thumb = thumb_rect(&l, 4, 16).unwrap();
assert!(thumb[0] >= track[0]);
assert!(thumb[0] + thumb[2] <= track[0] + track[2]);
assert!(track_rect(&layout([0.0, 100.0, 200.0, 40.0], 4), 4).is_none());
}
#[test]
fn thumb_top_maps_back_to_the_scroll_range() {
let l = layout([0.0, 100.0, 100.0, 40.0], 16); let [_, ly, _, lh] = l.list;
let th = thumb_rect(&l, 0, 16).unwrap()[3];
assert_eq!(first_for_thumb_top(&l, 16, ly), 0.0);
assert_eq!(first_for_thumb_top(&l, 16, ly + lh - th), 8.0);
assert_eq!(first_for_thumb_top(&l, 16, ly + (lh - th) / 2.0), 4.0);
assert_eq!(first_for_thumb_top(&l, 16, ly - 100.0), 0.0);
assert_eq!(first_for_thumb_top(&l, 16, ly + lh), 8.0);
}
#[test]
fn item_at_finds_the_row_under_a_point() {
let l = layout([400.0, 100.0, 200.0, 40.0], 3);
assert_eq!(item_at(&l, 500.0, 150.0), Some(0));
assert_eq!(item_at(&l, 500.0, 190.0), Some(1));
assert_eq!(item_at(&l, 500.0, 230.0), Some(2));
assert_eq!(item_at(&l, 700.0, 150.0), None);
assert_eq!(item_at(&l, 500.0, 300.0), None);
}
#[test]
fn zero_options_is_empty() {
let l = layout([0.0, 0.0, 100.0, 40.0], 0);
assert!(l.items.is_empty());
assert_eq!(l.list[3], 0.0);
}
}