use codecraft::ecs::Application as EcsApp;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use codecraft::Time;
use codecraft::scene::{Scene, SceneCommands};
use codecraft::ui::{Progress, ProgressBar, ScreenSize, UiDrawList, UiPlugin, Widget};
const WIDTH: f32 = 800.0;
const HEIGHT: f32 = 600.0;
fn ui_app() -> EcsApp {
let mut app = EcsApp::new();
app.add_plugin(UiPlugin);
app.insert_resource(ScreenSize {
width: WIDTH,
height: HEIGHT,
});
app
}
fn quads_for(value: f32) -> Vec<([f32; 2], [f32; 2])> {
let mut app = ui_app();
ProgressBar::new()
.value(value)
.spawn(&mut app.world, WIDTH, HEIGHT);
app.update();
app.world
.resource::<UiDrawList>()
.0
.iter()
.map(|quad| (quad.pos, quad.size))
.collect()
}
#[test]
fn an_empty_bar_draws_only_its_track() {
let quads = quads_for(0.0);
assert_eq!(quads.len(), 1, "track only, no fill");
let (pos, size) = quads[0];
assert_eq!(size[0], WIDTH * 0.34, "default width ratio of the screen");
assert_eq!(pos[0], WIDTH * 0.5 - size[0] * 0.5, "horizontally centered");
assert_eq!(pos[1], HEIGHT * 0.5 - size[1] * 0.5, "vertically centered");
}
#[test]
fn a_y_offset_shifts_the_bar_down_by_pixels() {
let mut app = ui_app();
ProgressBar::new()
.y_offset(70.0)
.spawn(&mut app.world, WIDTH, HEIGHT);
app.update();
let quad = app.world.resource::<UiDrawList>().0[0];
assert_eq!(quad.pos[1], HEIGHT * 0.5 + 70.0 - quad.size[1] * 0.5);
}
#[test]
fn a_half_filled_bar_fills_half_its_inner_width() {
let quads = quads_for(0.5);
assert_eq!(quads.len(), 2, "track and fill");
let (track_pos, track_size) = quads[0];
let (fill_pos, fill_size) = quads[1];
let padding = 3.0;
let inner = track_size[0] - padding * 2.0;
assert_eq!(fill_size[0], inner * 0.5);
assert_eq!(fill_size[1], track_size[1] - padding * 2.0);
assert_eq!(fill_pos[0], track_pos[0] + padding, "fill is inset");
assert_eq!(fill_pos[1], track_pos[1] + padding);
}
#[test]
fn progress_is_clamped_to_the_track() {
let full = Progress {
value: 1.0,
..progress_defaults()
};
let overfull = Progress {
value: 4.2,
..progress_defaults()
};
let negative = Progress {
value: -1.0,
..progress_defaults()
};
let track = 200.0;
assert_eq!(full.filled_width(track), overfull.filled_width(track));
assert_eq!(negative.filled_width(track), 0.0);
}
fn progress_defaults() -> Progress {
let mut app = ui_app();
let entity = ProgressBar::new().spawn(&mut app.world, WIDTH, HEIGHT);
app.world.get::<Progress>(entity).unwrap().clone()
}
#[test]
fn a_self_filling_bar_advances_with_the_frame_clock() {
let mut app = ui_app();
let bar = ProgressBar::new()
.fill_over(2.0)
.spawn(&mut app.world, WIDTH, HEIGHT);
let value = |app: &EcsApp| app.world.get::<Progress>(bar).unwrap().value;
app.insert_resource(Time {
delta: 0.5,
elapsed: 0.5,
});
app.update();
assert_eq!(value(&app), 0.25, "half a second of a two second fill");
app.update();
assert_eq!(value(&app), 0.5);
app.insert_resource(Time {
delta: 60.0,
elapsed: 61.0,
});
app.update();
assert_eq!(value(&app), 1.0);
}
#[test]
fn a_bar_without_fill_over_is_left_alone() {
let mut app = ui_app();
let bar = ProgressBar::new()
.value(0.3)
.spawn(&mut app.world, WIDTH, HEIGHT);
app.insert_resource(Time {
delta: 5.0,
elapsed: 5.0,
});
app.update();
assert_eq!(
app.world.get::<Progress>(bar).unwrap().value,
0.3,
"a caller-driven bar only moves when the caller moves it",
);
}
fn run(app: &mut EcsApp, frames: usize, delta: f32) {
for frame in 0..frames {
app.insert_resource(Time {
delta,
elapsed: delta * (frame + 1) as f32,
});
app.update();
}
}
#[test]
fn a_full_bar_runs_its_callback_exactly_once() {
let mut app = ui_app();
let fired = Arc::new(AtomicUsize::new(0));
let counter = fired.clone();
ProgressBar::new()
.fill_over(1.0)
.on_full(move || {
counter.fetch_add(1, Ordering::Relaxed);
})
.spawn(&mut app.world, WIDTH, HEIGHT);
run(&mut app, 3, 0.25);
assert_eq!(fired.load(Ordering::Relaxed), 0, "not full yet");
run(&mut app, 1, 0.25);
assert_eq!(fired.load(Ordering::Relaxed), 1, "fires on reaching full");
run(&mut app, 5, 0.25);
assert_eq!(fired.load(Ordering::Relaxed), 1);
}
#[test]
fn a_caller_driven_bar_fires_when_the_caller_fills_it() {
let mut app = ui_app();
let fired = Arc::new(AtomicUsize::new(0));
let counter = fired.clone();
let bar = ProgressBar::new()
.on_full(move || {
counter.fetch_add(1, Ordering::Relaxed);
})
.spawn(&mut app.world, WIDTH, HEIGHT);
run(&mut app, 1, 0.5);
assert_eq!(
fired.load(Ordering::Relaxed),
0,
"no fill_over, no movement"
);
app.world.get_mut::<Progress>(bar).unwrap().value = 1.0;
run(&mut app, 1, 0.5);
assert_eq!(fired.load(Ordering::Relaxed), 1);
}
struct Next;
impl Scene for Next {}
#[test]
fn then_scene_queues_the_handover_when_the_bar_fills() {
let mut app = ui_app();
let scenes = SceneCommands::new();
app.insert_resource(scenes.clone());
ProgressBar::new()
.fill_over(2.0)
.then_scene(Next)
.spawn(&mut app.world, WIDTH, HEIGHT);
run(&mut app, 3, 0.5);
assert!(!scenes.is_pending(), "still filling, still on this scene");
run(&mut app, 1, 0.5);
assert!(scenes.is_pending(), "full bar hands over");
}