use crate::event::{Point, Rect};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum Side {
Top,
#[default]
Bottom,
Left,
Right,
AtPoint,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ResolvedSide {
Top,
Bottom,
Left,
Right,
AtPoint,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Anchor {
pub rect: Rect,
pub preferred: Side,
pub margin: f32,
}
impl Anchor {
pub const fn new(rect: Rect, preferred: Side) -> Self {
Self {
rect,
preferred,
margin: 0.0,
}
}
pub const fn at_point(p: Point) -> Self {
Self {
rect: Rect::new(p.x, p.y, 0.0, 0.0),
preferred: Side::AtPoint,
margin: 0.0,
}
}
pub const fn with_margin(mut self, margin: f32) -> Self {
self.margin = margin;
self
}
pub fn resolve(&self, size: (f32, f32), viewport: Rect) -> (Point, ResolvedSide) {
let (w, h) = size;
let r = self.rect;
if self.preferred == Side::AtPoint {
let x = r.x.clamp(
viewport.x,
(viewport.x + viewport.width - w).max(viewport.x),
);
let y = r.y.clamp(
viewport.y,
(viewport.y + viewport.height - h).max(viewport.y),
);
return (Point::new(x, y), ResolvedSide::AtPoint);
}
let candidate = |side: Side| -> (f32, f32) {
match side {
Side::Top => (r.x + (r.width - w) * 0.5, r.y - self.margin - h),
Side::Bottom => (r.x + (r.width - w) * 0.5, r.y + r.height + self.margin),
Side::Left => (r.x - self.margin - w, r.y + (r.height - h) * 0.5),
Side::Right => (r.x + r.width + self.margin, r.y + (r.height - h) * 0.5),
Side::AtPoint => (r.x, r.y),
}
};
let fits = |x: f32, y: f32| -> bool {
x >= viewport.x
&& y >= viewport.y
&& x + w <= viewport.x + viewport.width
&& y + h <= viewport.y + viewport.height
};
let opposite = |side: Side| -> Side {
match side {
Side::Top => Side::Bottom,
Side::Bottom => Side::Top,
Side::Left => Side::Right,
Side::Right => Side::Left,
Side::AtPoint => Side::AtPoint,
}
};
let resolved_of = |side: Side| -> ResolvedSide {
match side {
Side::Top => ResolvedSide::Top,
Side::Bottom => ResolvedSide::Bottom,
Side::Left => ResolvedSide::Left,
Side::Right => ResolvedSide::Right,
Side::AtPoint => ResolvedSide::AtPoint,
}
};
let (px, py) = candidate(self.preferred);
if fits(px, py) {
return (Point::new(px, py), resolved_of(self.preferred));
}
let flipped = opposite(self.preferred);
let (fx, fy) = candidate(flipped);
if fits(fx, fy) {
return (Point::new(fx, fy), resolved_of(flipped));
}
let (mut x, mut y) = (px, py);
x = x.clamp(
viewport.x,
(viewport.x + viewport.width - w).max(viewport.x),
);
y = y.clamp(
viewport.y,
(viewport.y + viewport.height - h).max(viewport.y),
);
(Point::new(x, y), resolved_of(self.preferred))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Axis {
Vertical,
Horizontal,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VisibleItem {
pub idx: usize,
pub bounds: Rect,
}
pub fn visible_range_walk(
first_idx: usize,
count: usize,
origin: Point,
axis: Axis,
cross: f32,
available: f32,
mut measure: impl FnMut(usize) -> f32,
) -> Vec<VisibleItem> {
let mut out = Vec::new();
let mut pos = 0.0f32;
for idx in first_idx..count {
if pos >= available {
break;
}
let extent = measure(idx);
let remaining = available - pos;
let clipped = extent.min(remaining).max(0.0);
if clipped <= 0.0 {
break;
}
let bounds = match axis {
Axis::Vertical => Rect::new(origin.x, origin.y + pos, cross, clipped),
Axis::Horizontal => Rect::new(origin.x + pos, origin.y, clipped, cross),
};
out.push(VisibleItem { idx, bounds });
pos += extent;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
const VIEWPORT: Rect = Rect::new(0.0, 0.0, 100.0, 100.0);
#[test]
fn resolve_preferred_side_when_it_fits() {
let anchor = Anchor::new(Rect::new(40.0, 40.0, 10.0, 10.0), Side::Bottom).with_margin(2.0);
let (p, side) = anchor.resolve((20.0, 10.0), VIEWPORT);
assert_eq!(side, ResolvedSide::Bottom);
assert_eq!(p, Point::new(35.0, 52.0));
}
#[test]
fn resolve_flips_to_opposite_side_on_overflow() {
let anchor = Anchor::new(Rect::new(40.0, 95.0, 10.0, 5.0), Side::Bottom).with_margin(1.0);
let (p, side) = anchor.resolve((20.0, 10.0), VIEWPORT);
assert_eq!(side, ResolvedSide::Top);
assert_eq!(p, Point::new(35.0, 84.0));
}
#[test]
fn resolve_pins_to_edge_when_both_sides_overflow() {
let short_viewport = Rect::new(0.0, 0.0, 100.0, 12.0);
let anchor = Anchor::new(Rect::new(40.0, 4.0, 10.0, 4.0), Side::Bottom).with_margin(1.0);
let (p, side) = anchor.resolve((20.0, 10.0), short_viewport);
assert_eq!(side, ResolvedSide::Bottom);
assert!(p.y >= short_viewport.y);
assert!(p.y + 10.0 <= short_viewport.y + short_viewport.height);
}
#[test]
fn anchor_serde_round_trips() {
let anchor = Anchor::new(Rect::new(1.0, 2.0, 3.0, 4.0), Side::Right).with_margin(5.0);
let json = serde_json::to_string(&anchor).unwrap();
let back: Anchor = serde_json::from_str(&json).unwrap();
assert_eq!(anchor, back);
}
#[test]
fn resolve_at_point_never_flips_and_clamps() {
let anchor = Anchor::at_point(Point::new(98.0, 98.0));
let (p, side) = anchor.resolve((20.0, 10.0), VIEWPORT);
assert_eq!(side, ResolvedSide::AtPoint);
assert_eq!(p, Point::new(80.0, 90.0));
}
#[test]
fn vertical_walk_stops_when_viewport_is_full() {
let items = visible_range_walk(
0,
10,
Point::new(0.0, 0.0),
Axis::Vertical,
30.0,
50.0,
|_| 10.0,
);
assert_eq!(items.len(), 5);
assert_eq!(items[0].idx, 0);
assert_eq!(items[0].bounds, Rect::new(0.0, 0.0, 30.0, 10.0));
assert_eq!(items[4].idx, 4);
assert_eq!(items[4].bounds, Rect::new(0.0, 40.0, 30.0, 10.0));
}
#[test]
fn vertical_walk_starts_at_first_idx() {
let items = visible_range_walk(
3,
10,
Point::new(0.0, 0.0),
Axis::Vertical,
30.0,
25.0,
|_| 10.0,
);
let idxs: Vec<usize> = items.iter().map(|v| v.idx).collect();
assert_eq!(idxs, vec![3, 4, 5]);
assert_eq!(items.last().unwrap().bounds.height, 5.0);
}
#[test]
fn vertical_walk_stops_at_count_not_just_available_space() {
let items = visible_range_walk(
0,
3,
Point::new(0.0, 0.0),
Axis::Vertical,
30.0,
100.0,
|_| 10.0,
);
assert_eq!(items.len(), 3);
}
#[test]
fn horizontal_walk_lays_out_along_x() {
let items = visible_range_walk(
0,
5,
Point::new(10.0, 0.0),
Axis::Horizontal,
8.0,
25.0,
|_| 10.0,
);
assert_eq!(items.len(), 3);
assert_eq!(items[0].bounds, Rect::new(10.0, 0.0, 10.0, 8.0));
assert_eq!(items[1].bounds, Rect::new(20.0, 0.0, 10.0, 8.0));
assert_eq!(items[2].bounds, Rect::new(30.0, 0.0, 5.0, 8.0));
}
#[test]
fn walk_at_nonzero_origin_is_absolute_not_local() {
let items = visible_range_walk(
0,
1,
Point::new(7.0, 9.0),
Axis::Vertical,
30.0,
10.0,
|_| 10.0,
);
assert_eq!(items[0].bounds, Rect::new(7.0, 9.0, 30.0, 10.0));
}
#[test]
fn walk_skips_zero_extent_items_and_stops() {
let items = visible_range_walk(
0,
3,
Point::new(0.0, 0.0),
Axis::Vertical,
10.0,
50.0,
|_| 0.0,
);
assert!(items.is_empty());
}
#[test]
fn walk_variable_height_items_via_closure() {
let heights = [5.0, 15.0, 8.0];
let items = visible_range_walk(
0,
heights.len(),
Point::new(0.0, 0.0),
Axis::Vertical,
30.0,
100.0,
|idx| heights[idx],
);
assert_eq!(items.len(), 3);
assert_eq!(items[0].bounds.y, 0.0);
assert_eq!(items[1].bounds.y, 5.0);
assert_eq!(items[2].bounds.y, 20.0);
assert_eq!(items[2].bounds.height, 8.0);
}
}