#[allow(clippy::module_name_repetitions)]
#[derive(Debug, Clone)]
pub struct Viewport {
pub parent: Option<Box<Self>>,
pub pos: Pos,
pub viewport: Pos,
}
impl Viewport {
fn is_visible(&self) -> (bool, f32) {
if let Some((visible, dist)) = self.parent.as_ref().map(|x| x.is_visible()) {
if visible {
let pos = self.pos;
let vp = self.viewport;
super::is_visible(vp.x, vp.y, vp.w, vp.h, pos.x, pos.y, pos.w, pos.h)
} else {
(false, dist)
}
} else {
(true, 0.0)
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Pos {
pub x: f32,
pub y: f32,
pub w: f32,
pub h: f32,
}
#[allow(clippy::module_name_repetitions)]
#[derive(Debug)]
pub struct ViewportListener {
pub viewport: Option<Viewport>,
visible: bool,
prev_visible: Option<bool>,
initialized: bool,
dist: f32,
prev_dist: Option<f32>,
pub pos: Pos,
}
impl ViewportListener {
#[must_use]
pub const fn new(viewport: Option<Viewport>, x: f32, y: f32, w: f32, h: f32) -> Self {
Self {
viewport,
visible: false,
prev_visible: None,
initialized: false,
dist: 0.0,
prev_dist: None,
pos: Pos { x, y, w, h },
}
}
fn is_visible(&self) -> (bool, f32) {
if let Some(((visible, dist), vp, pos)) = self
.viewport
.as_ref()
.map(|x| (x.is_visible(), x.viewport, x.pos))
{
if visible {
super::is_visible(
vp.x + pos.x,
vp.y + pos.y,
vp.w,
vp.h,
self.pos.x,
self.pos.y,
self.pos.w,
self.pos.h,
)
} else {
(false, dist)
}
} else {
(true, 0.0)
}
}
pub fn check(&mut self) -> ((bool, Option<bool>), (f32, Option<f32>)) {
let (visible, dist) = self.is_visible();
log::trace!("check: pos={:?} visible={visible} dist={dist}", self.pos);
if self.initialized {
let prev_visible = self.visible;
let prev_dist = self.dist;
self.prev_visible = if prev_visible == visible {
None
} else {
self.visible = visible;
Some(prev_visible)
};
self.prev_dist = if (prev_dist - dist) < 0.01 {
None
} else {
self.dist = dist;
Some(prev_dist)
};
((visible, self.prev_visible), (dist, self.prev_dist))
} else {
self.initialized = true;
self.visible = visible;
self.dist = dist;
((visible, None), (dist, None))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test_log::test]
fn test_viewport_listener_initial_check_visible() {
let viewport = Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 800.0,
h: 600.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 800.0,
h: 600.0,
},
};
let mut listener = ViewportListener::new(Some(viewport), 100.0, 100.0, 50.0, 50.0);
let ((visible, prev_visible), (dist, prev_dist)) = listener.check();
assert!(visible);
assert!(prev_visible.is_none()); assert!(dist < 0.001);
assert!(prev_dist.is_none());
}
#[test_log::test]
fn test_viewport_listener_initial_check_not_visible() {
let viewport = Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 800.0,
h: 600.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 800.0,
h: 600.0,
},
};
let mut listener = ViewportListener::new(Some(viewport), 1000.0, 1000.0, 50.0, 50.0);
let ((visible, prev_visible), (_dist, prev_dist)) = listener.check();
assert!(!visible);
assert!(prev_visible.is_none());
assert!(prev_dist.is_none());
}
#[test_log::test]
fn test_viewport_listener_visibility_change() {
let viewport = Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 800.0,
h: 600.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 800.0,
h: 600.0,
},
};
let mut listener = ViewportListener::new(Some(viewport), 100.0, 100.0, 50.0, 50.0);
let ((visible, _), _) = listener.check();
assert!(visible);
listener.viewport = Some(Viewport {
parent: None,
pos: Pos {
x: 1000.0,
y: 1000.0,
w: 800.0,
h: 600.0,
},
viewport: Pos {
x: 1000.0,
y: 1000.0,
w: 800.0,
h: 600.0,
},
});
let ((visible, prev_visible), _) = listener.check();
assert!(!visible);
assert_eq!(prev_visible, Some(true)); }
#[test_log::test]
fn test_viewport_listener_no_viewport() {
let mut listener = ViewportListener::new(None, 100.0, 100.0, 50.0, 50.0);
let ((visible, _), (dist, _)) = listener.check();
assert!(visible);
assert!(dist < 0.001);
}
#[test_log::test]
fn test_viewport_listener_no_change() {
let viewport = Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 800.0,
h: 600.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 800.0,
h: 600.0,
},
};
let mut listener = ViewportListener::new(Some(viewport), 100.0, 100.0, 50.0, 50.0);
listener.check();
let ((visible, prev_visible), (_, prev_dist)) = listener.check();
assert!(visible);
assert!(prev_visible.is_none()); assert!(prev_dist.is_none()); }
#[test_log::test]
fn test_viewport_with_parent_both_visible() {
let parent = Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 1000.0,
h: 1000.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 1000.0,
h: 1000.0,
},
};
let child = Viewport {
parent: Some(Box::new(parent)),
pos: Pos {
x: 100.0,
y: 100.0,
w: 600.0,
h: 400.0,
},
viewport: Pos {
x: 100.0,
y: 100.0,
w: 600.0,
h: 400.0,
},
};
let mut listener = ViewportListener::new(Some(child), 200.0, 200.0, 50.0, 50.0);
let ((visible, _), _) = listener.check();
assert!(visible);
}
#[test_log::test]
fn test_viewport_with_parent_child_not_visible() {
let parent = Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 1000.0,
h: 1000.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 1000.0,
h: 1000.0,
},
};
let child = Viewport {
parent: Some(Box::new(parent)),
pos: Pos {
x: 100.0,
y: 100.0,
w: 600.0,
h: 400.0,
},
viewport: Pos {
x: 100.0,
y: 100.0,
w: 600.0,
h: 400.0,
},
};
let mut listener = ViewportListener::new(Some(child), 1000.0, 1000.0, 50.0, 50.0);
let ((visible, _), _) = listener.check();
assert!(!visible);
}
#[test_log::test]
fn test_viewport_with_parent_not_visible_propagates_invisibility() {
let grandparent = Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
};
let parent = Viewport {
parent: Some(Box::new(grandparent)),
pos: Pos {
x: 500.0,
y: 500.0,
w: 200.0,
h: 200.0,
},
viewport: Pos {
x: 500.0,
y: 500.0,
w: 200.0,
h: 200.0,
},
};
let child = Viewport {
parent: Some(Box::new(parent)),
pos: Pos {
x: 550.0,
y: 550.0,
w: 100.0,
h: 100.0,
},
viewport: Pos {
x: 550.0,
y: 550.0,
w: 100.0,
h: 100.0,
},
};
let mut listener = ViewportListener::new(Some(child), 560.0, 560.0, 20.0, 20.0);
let ((visible, _), (dist, _)) = listener.check();
assert!(!visible);
assert!(dist > 0.0);
}
#[test_log::test]
fn test_viewport_listener_distance_change_threshold() {
let viewport = Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
};
let mut listener = ViewportListener::new(Some(viewport), 200.0, 200.0, 50.0, 50.0);
let ((visible, _), (initial_dist, _)) = listener.check();
assert!(!visible);
assert!(initial_dist > 0.0);
let ((_, _), (_, prev_dist)) = listener.check();
assert!(
prev_dist.is_none(),
"Distance change below threshold should not report previous distance"
);
}
#[test_log::test]
fn test_viewport_listener_distance_change_above_threshold() {
let mut listener = ViewportListener::new(
Some(Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
}),
200.0,
200.0,
50.0,
50.0,
);
let ((_, _), (initial_dist, _)) = listener.check();
assert!(initial_dist > 0.0);
listener.viewport = Some(Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
viewport: Pos {
x: 150.0,
y: 150.0,
w: 100.0,
h: 100.0,
},
});
let ((_, _), (new_dist, prev_dist)) = listener.check();
assert!(
prev_dist.is_some(),
"Significant distance change should report previous distance"
);
assert!(
(new_dist - initial_dist).abs() > 0.01,
"Distance should have changed significantly"
);
}
#[test_log::test]
fn test_viewport_listener_visibility_toggle_back_and_forth() {
let mut listener = ViewportListener::new(
Some(Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
}),
10.0,
10.0,
50.0,
50.0,
);
let ((visible, _), _) = listener.check();
assert!(visible);
listener.viewport = Some(Viewport {
parent: None,
pos: Pos {
x: 500.0,
y: 500.0,
w: 100.0,
h: 100.0,
},
viewport: Pos {
x: 500.0,
y: 500.0,
w: 100.0,
h: 100.0,
},
});
let ((visible, prev_visible), _) = listener.check();
assert!(!visible);
assert_eq!(prev_visible, Some(true));
listener.viewport = Some(Viewport {
parent: None,
pos: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
viewport: Pos {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
});
let ((visible, prev_visible), _) = listener.check();
assert!(visible);
assert_eq!(prev_visible, Some(false));
}
}