use egui::{Color32, Context, RawInput, Shape};
use egui_map::map::Map;
use egui_map::map::animation::CHEVRON_SPEED;
use egui_map::map::objects::{CometDirection, MapSegment};
use std::time::{Duration, Instant};
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
enum Kind {
FilledCircle,
Line,
Mesh,
Other,
}
fn render(map: &mut Map) -> Vec<Kind> {
let ctx = Context::default();
render_with(&ctx, map)
}
fn render_with(ctx: &Context, map: &mut Map) -> Vec<Kind> {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 300.0));
let mut out = ctx.run_ui(
RawInput {
screen_rect: Some(screen),
..RawInput::default()
},
|ui| {
ui.add(&mut *map);
},
);
let kinds = out
.shapes
.iter()
.map(|cs| match &cs.shape {
Shape::Circle(c) if c.fill.a() > 0 => Kind::FilledCircle,
Shape::LineSegment { .. } => Kind::Line,
Shape::Mesh(_) => Kind::Mesh,
_ => Kind::Other,
})
.collect();
out.textures_delta.clear();
kinds
}
fn map_with_one_segment() -> Map {
let mut map = Map::new();
map.add_lines(vec![MapSegment::new((1, 2), [0.0, 0.0], [50.0, 0.0])]);
map
}
fn extra(with: &[Kind], without: &[Kind]) -> usize {
with.len().saturating_sub(without.len())
}
#[test]
fn flash_paints_something() {
let baseline = render(&mut map_with_one_segment());
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().flash(Instant::now());
let shapes = render(&mut map);
assert!(
extra(&shapes, &baseline) > 0,
"flash painted nothing on top of the plain map: {shapes:?}"
);
}
#[test]
fn flash_stops_painting_once_finished() {
let long_ago = Instant::now() - Duration::from_secs(2);
let baseline = render(&mut map_with_one_segment()).len();
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().flash(long_ago);
let _ = render(&mut map); assert_eq!(
render(&mut map).len(),
baseline,
"flash kept painting after it should have finished"
);
}
#[test]
fn comet_once_paints_something_and_stops_on_its_own() {
let baseline = render(&mut map_with_one_segment());
let mut map = map_with_one_segment();
map.segment((1, 2))
.unwrap()
.comet_once(Instant::now(), CometDirection::Forward);
let shapes = render(&mut map);
assert!(
extra(&shapes, &baseline) > 0,
"comet_once painted nothing on top of the plain map: {shapes:?}"
);
}
#[test]
fn comet_once_stops_painting_once_finished() {
let long_ago = Instant::now() - Duration::from_secs(2);
let baseline = render(&mut map_with_one_segment()).len();
let mut map = map_with_one_segment();
map.segment((1, 2))
.unwrap()
.comet_once(long_ago, CometDirection::Forward);
let _ = render(&mut map); assert_eq!(
render(&mut map).len(),
baseline,
"comet_once kept painting after it should have finished"
);
}
#[test]
fn comet_once_direction_starts_from_the_chosen_endpoint() {
fn line_and_circle(ctx: &Context, map: &mut Map) -> ([egui::Pos2; 2], egui::Pos2) {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 300.0));
let mut out = ctx.run_ui(
RawInput {
screen_rect: Some(screen),
..RawInput::default()
},
|ui| {
ui.add(&mut *map);
},
);
let line = out
.shapes
.iter()
.find_map(|cs| match &cs.shape {
Shape::LineSegment { points, .. } => Some(*points),
_ => None,
})
.expect("expected the default segment line to be painted");
let circle = out
.shapes
.iter()
.find_map(|cs| match &cs.shape {
Shape::Circle(c) if c.fill.a() > 0 => Some(c.center),
_ => None,
})
.expect("expected a filled circle to be painted");
out.textures_delta.clear();
(line, circle)
}
let ctx = Context::default();
let mut forward = map_with_one_segment();
forward
.segment((1, 2))
.unwrap()
.comet_once(Instant::now(), CometDirection::Forward);
let ([a, b], forward_pos) = line_and_circle(&ctx, &mut forward);
let ctx = Context::default();
let mut reverse = map_with_one_segment();
reverse
.segment((1, 2))
.unwrap()
.comet_once(Instant::now(), CometDirection::Reverse);
let (_, reverse_pos) = line_and_circle(&ctx, &mut reverse);
let dist = |p: egui::Pos2, q: egui::Pos2| (p - q).length();
assert!(
dist(forward_pos, a) < dist(forward_pos, b),
"Forward must start closer to the first endpoint {a:?}, got {forward_pos:?} (other endpoint {b:?})"
);
assert!(
dist(reverse_pos, b) < dist(reverse_pos, a),
"Reverse must start closer to the second endpoint {b:?}, got {reverse_pos:?} (other endpoint {a:?})"
);
}
#[test]
fn wipe_paints_something() {
let baseline = render(&mut map_with_one_segment());
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().wipe(Instant::now());
let shapes = render(&mut map);
assert!(
extra(&shapes, &baseline) > 0,
"wipe painted nothing on top of the plain map: {shapes:?}"
);
}
#[test]
fn wipe_stops_painting_once_finished() {
let long_ago = Instant::now() - Duration::from_secs(2);
let baseline = render(&mut map_with_one_segment()).len();
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().wipe(long_ago);
let _ = render(&mut map); assert_eq!(
render(&mut map).len(),
baseline,
"wipe kept painting after it should have finished"
);
}
#[test]
fn comet_keeps_painting_and_clear_stops_it() {
let baseline = render(&mut map_with_one_segment()).len();
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().comet();
for frame in 0..5 {
assert!(
render(&mut map).len() > baseline,
"comet stopped painting at frame {frame}"
);
}
map.segment((1, 2)).unwrap().clear();
assert_eq!(
render(&mut map).len(),
baseline,
"comet kept painting after clear()"
);
}
#[test]
fn dash_paints_a_mesh_and_keeps_painting_until_cleared() {
let baseline = render(&mut map_with_one_segment());
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().dash();
let shapes = render(&mut map);
assert!(
shapes.contains(&Kind::Mesh),
"dash must paint a textured mesh, got {shapes:?}"
);
for frame in 0..5 {
assert!(
render(&mut map).len() > baseline.len(),
"dash stopped painting at frame {frame}"
);
}
map.segment((1, 2)).unwrap().clear();
assert_eq!(
render(&mut map).len(),
baseline.len(),
"dash kept painting after clear()"
);
}
#[test]
fn dash_mesh_is_painted_after_the_default_line_so_it_stays_on_top() {
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().dash();
let shapes = render(&mut map);
let line_index = shapes
.iter()
.position(|k| *k == Kind::Line)
.expect("the segment's own line must still be painted");
let mesh_index = shapes
.iter()
.position(|k| *k == Kind::Mesh)
.expect("dash must paint a textured mesh");
assert!(
mesh_index > line_index,
"the segment's own line must be painted before (under) its effect, got {shapes:?}"
);
}
#[test]
fn glow_band_paints_a_mesh_and_keeps_painting_until_cleared() {
let baseline = render(&mut map_with_one_segment());
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().glow_band();
let shapes = render(&mut map);
assert!(
shapes.contains(&Kind::Mesh),
"glow_band must paint a textured mesh, got {shapes:?}"
);
for frame in 0..5 {
assert!(
render(&mut map).len() > baseline.len(),
"glow_band stopped painting at frame {frame}"
);
}
map.segment((1, 2)).unwrap().clear();
assert_eq!(
render(&mut map).len(),
baseline.len(),
"glow_band kept painting after clear()"
);
}
#[test]
fn chevrons_paint_a_mesh_and_keep_painting_until_cleared() {
let baseline = render(&mut map_with_one_segment());
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().chevrons();
let shapes = render(&mut map);
assert!(
shapes.contains(&Kind::Mesh),
"chevrons must paint a textured mesh, got {shapes:?}"
);
for frame in 0..5 {
assert!(
render(&mut map).len() > baseline.len(),
"chevrons stopped painting at frame {frame}"
);
}
map.segment((1, 2)).unwrap().clear();
assert_eq!(
render(&mut map).len(),
baseline.len(),
"chevrons kept painting after clear()"
);
}
#[test]
fn chevrons_pattern_slides_toward_the_second_endpoint() {
fn render_and_capture(ctx: &Context, map: &mut Map) -> (f32, f32) {
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 300.0));
let mut out = ctx.run_ui(
RawInput {
screen_rect: Some(screen),
..RawInput::default()
},
|ui| {
ui.add(&mut *map);
},
);
let time = ctx.input(|i| i.time) as f32;
let u0 = out
.shapes
.iter()
.find_map(|cs| match &cs.shape {
Shape::Mesh(mesh) => mesh.vertices.first().map(|v| v.uv.x),
_ => None,
})
.expect("expected chevrons to paint a textured mesh");
out.textures_delta.clear();
(time, u0)
}
let ctx = Context::default();
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().chevrons();
let _ = render_and_capture(&ctx, &mut map);
let (time, u0) = render_and_capture(&ctx, &mut map);
let expected = (-(time * CHEVRON_SPEED)).rem_euclid(1.0);
assert!(
(u0 - expected).abs() < 1e-4,
"chevrons' texture coordinate at the first endpoint must be \
`-(time * CHEVRON_SPEED) mod 1` so the pattern travels toward the \
second endpoint, got {u0}, expected {expected} at time {time}"
);
}
#[test]
fn state_and_notification_stack_on_the_same_segment() {
let baseline = render(&mut map_with_one_segment()).len();
let mut only_state = map_with_one_segment();
only_state.segment((1, 2)).unwrap().comet();
let state_only = render(&mut only_state).len();
let mut both = map_with_one_segment();
both.segment((1, 2)).unwrap().comet();
both.segment((1, 2)).unwrap().flash(Instant::now());
let both_len = render(&mut both).len();
assert!(state_only > baseline);
assert!(
both_len > state_only,
"a notification must add to the lasting state, not replace it"
);
}
#[test]
fn color_modifier_reaches_the_painted_shape() {
let mut map = map_with_one_segment();
map.segment((1, 2))
.unwrap()
.color(Color32::RED)
.flash(Instant::now());
let ctx = Context::default();
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 300.0));
let mut out = ctx.run_ui(
RawInput {
screen_rect: Some(screen),
..RawInput::default()
},
|ui| {
ui.add(&mut map);
},
);
let reds = out
.shapes
.iter()
.filter(|cs| match &cs.shape {
Shape::LineSegment { stroke, .. } => {
stroke.color.r() > 0 && stroke.color.g() == 0 && stroke.color.b() == 0
}
_ => false,
})
.count();
out.textures_delta.clear();
assert!(
reds > 0,
"the requested colour never reached a painted shape"
);
}
#[test]
fn comet_moves_over_time() {
let ctx = Context::default();
let mut map = map_with_one_segment();
map.segment((1, 2)).unwrap().comet();
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(400.0, 300.0));
let mut frames: Vec<Vec<(i32, i32)>> = Vec::new();
for _ in 0..40 {
let mut out = ctx.run_ui(
RawInput {
screen_rect: Some(screen),
..RawInput::default()
},
|ui| {
ui.add(&mut map);
},
);
let mut centers: Vec<(i32, i32)> = out
.shapes
.iter()
.filter_map(|cs| match &cs.shape {
Shape::Circle(c) if c.fill.a() > 0 => {
Some(((c.center.x * 10.0) as i32, (c.center.y * 10.0) as i32))
}
_ => None,
})
.collect();
centers.sort_unstable();
frames.push(centers);
out.textures_delta.clear();
}
assert!(
frames.iter().any(|f| *f != frames[0]),
"the comet never moved across 40 frames: {:?}",
frames[0]
);
}