use super::*;
use crate::{
AxisBinding, InputAction, InputActions, InputAxisAction, InputButtonAction, NoInputAxes2,
PointerDelta, WheelDelta,
};
const WATCHED: Camera = Camera::new(
View::look_at(Vec3::new(3.0, 4.0, 5.0), Vec3::X),
Projection::orthographic(8.0),
);
meshes! { enum SphereSet { Sphere } }
struct RedCube;
impl Game for RedCube {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(Camera::new(
View::look_at(Vec3::new(0.0, 0.0, 3.0), Vec3::ZERO),
Projection::perspective(60.0),
));
ctx.draw(
Cube.at(Transform::from_scale_rotation_translation(
Vec3::new(1.0, 0.4, 1.0),
Quat::IDENTITY,
Vec3::new(-0.7, 0.0, 0.0),
))
.material(Material::color(Color::rgb(1.0, 0.0, 0.0))),
);
}
}
struct Sheen {
roughness: Option<f32>,
}
impl Game for Sheen {
type Meshes = SphereSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_camera(Camera::new(
View::look_at(Vec3::Z * 2.0, Vec3::ZERO),
Projection::orthographic(1.5),
));
ctx.light(Light::directional(Vec3::NEG_Z, DIM));
let material = Material::lit(DIM);
ctx.draw(
Sphere { subdivisions: 3 }.at(Vec3::ZERO).material(
self.roughness
.map_or(material, |roughness| material.roughness(roughness)),
),
);
}
}
#[test]
fn a_surface_that_is_not_fully_rough_brightens_where_it_reflects_a_light_at_the_camera() {
let sheen = |roughness| rendered(raw("headless roughness"), Sheen { roughness });
let Some(plain) = sheen(None) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let (Some(fully_rough), Some(half_rough)) = (sheen(Some(1.0)), sheen(Some(0.5))) else {
return;
};
let turned = |pixels: &[u8]| i32::from(pixel(pixels, SIDE / 2 + 5, SIDE / 2)[0]);
let reflected = |pixels: &[u8]| i32::from(middle(pixels)[0]);
assert_eq!(
fully_rough, plain,
"a fully rough surface is what a material that names no roughness draws"
);
assert!(
reflected(&half_rough) > reflected(&plain),
"the highlight brightens the pixel the light reflects off towards the camera"
);
assert!(
reflected(&half_rough) - reflected(&plain) > turned(&half_rough) - turned(&plain),
"and brightens a pixel turned away from that angle less"
);
}
#[derive(Default)]
struct Watcher {
in_tick: Option<Camera>,
in_frame: Option<Camera>,
}
impl Game for Watcher {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
self.in_tick = Some(ctx.last_camera());
}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.in_frame = Some(ctx.last_camera());
ctx.set_camera(WATCHED);
}
}
#[test]
fn a_frame_and_a_tick_are_answered_with_the_camera_the_last_frame_drew_from() {
let Ok(mut session) = Session::new(
Config::new("headless picking"),
UVec2::splat(SIDE),
|_ctx| Ok(Watcher::default()),
) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
assert_eq!(
session.game().in_frame,
Some(Camera::default()),
"the first frame has none of its own to go on"
);
session.tick();
session.step();
assert_eq!(session.game().in_tick, Some(WATCHED));
assert_eq!(session.game().in_frame, Some(WATCHED));
}
#[derive(Clone, Copy)]
struct HighScore;
impl crate::Saves for HighScore {
fn name(&self) -> &'static str {
"Progress.HighScore"
}
}
impl crate::SaveKey for HighScore {
type Value = i64;
fn fallback(&self) -> i64 {
-1
}
}
#[derive(Default)]
struct Keeper {
at_startup: i64,
in_tick: i64,
in_frame: i64,
}
impl Game for Keeper {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
self.in_tick = ctx.saved(HighScore);
ctx.save(HighScore, self.in_tick + 1);
}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.in_frame = ctx.saved(HighScore);
}
}
#[test]
fn a_titleless_session_reads_fallbacks_and_reads_back_what_it_saved() {
let Ok(mut session) = Session::new(
Config::new("headless saves"),
UVec2::splat(SIDE),
|ctx: &mut InitContext<'_, Keeper>| {
Ok(Keeper {
at_startup: ctx.startup().saved(HighScore),
..Keeper::default()
})
},
) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.tick();
session.step();
session.tick();
assert_eq!(session.game().at_startup, -1, "no run kept anything");
assert_eq!(session.game().in_tick, 0, "the second tick reads the first");
assert_eq!(session.game().in_frame, 0);
}
fn shared_startup(startup: &mut Startup<'_>) -> (UVec2, i64) {
(startup.window_size(), startup.saved(HighScore))
}
#[derive(Default)]
struct Shared {
size: UVec2,
score: i64,
}
impl Game for Shared {
type Meshes = SphereSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _: &mut TickContext<'_, Self>) {}
fn frame(&mut self, _: &mut FrameContext<'_, Self>) {}
}
#[test]
fn one_startup_helper_starts_two_games_of_different_vocabularies() {
let started = Session::new(
Config::new("headless shared startup"),
UVec2::splat(SIDE),
|ctx: &mut InitContext<'_, Shared>| {
let (size, score) = shared_startup(ctx.startup());
Ok(Shared { size, score })
},
);
let Ok(shared) = started else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Ok(keeper) = Session::new(
Config::new("headless shared startup"),
UVec2::splat(SIDE),
|ctx: &mut InitContext<'_, Keeper>| {
Ok(Keeper {
at_startup: shared_startup(ctx.startup()).1,
..Keeper::default()
})
},
) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
assert_eq!(shared.game().size, UVec2::splat(SIDE));
assert_eq!(
keeper.game().at_startup,
shared.game().score,
"one helper read for both games"
);
}
struct Walker {
travelled: Duration,
seen_elapsed: Duration,
}
impl Game for Walker {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
self.travelled += ctx.dt();
}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.seen_elapsed = ctx.elapsed();
}
}
#[test]
fn ticks_are_fixed_and_only_ticks_advance_the_game() {
let interval = Duration::from_millis(4);
let config = Config::new("headless ticks").with_tick_interval(interval);
let start = Walker {
travelled: Duration::ZERO,
seen_elapsed: Duration::ZERO,
};
let Ok(mut session) = Session::new(config, UVec2::splat(16), |_ctx| Ok(start)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
for _ in 0..250 {
session.tick();
}
session.step();
assert_eq!(session.game().travelled, interval * 250);
assert_eq!(session.game().seen_elapsed, interval * 250);
session.step();
assert_eq!(
session.game().travelled,
interval * 250,
"rendering never ticks"
);
}
#[derive(Default)]
struct Pacer {
steps: Vec<Duration>,
}
impl Game for Pacer {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
self.steps.push(ctx.dt());
match self.steps.len() {
1 => ctx.set_tick_interval(ctx.dt() * 2),
3 => ctx.close(),
_ => {}
}
}
fn frame(&mut self, _ctx: &mut FrameContext<'_, Self>) {}
}
#[test]
fn a_tick_interval_set_in_a_tick_is_taken_from_the_next_tick_and_a_close_is_reported() {
let interval = Duration::from_millis(4);
let config = Config::new("headless pace").with_tick_interval(interval);
let Ok(mut session) = Session::new(config, UVec2::splat(16), |_ctx| Ok(Pacer::default()))
else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.tick();
session.tick();
assert_eq!(
session.game().steps,
vec![interval, interval * 2],
"the tick that set it kept its own step"
);
assert!(!session.closed());
session.tick();
assert!(session.closed(), "the third tick requested it");
session.step();
session.tick();
assert_eq!(
session.game().steps.len(),
4,
"and the caller decides when to stop"
);
assert!(session.closed());
}
#[derive(Default)]
struct Clicker {
pressed_in_ticks: u32,
released_in_ticks: u32,
pressed_in_frames: u32,
down: bool,
pointer: Vec2,
captured: Option<ButtonBinding>,
}
impl Game for Clicker {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = Key;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, ctx: &mut TickContext<'_, Self>) {
self.pressed_in_ticks += u32::from(ctx.pressed(Key::Space));
self.released_in_ticks += u32::from(ctx.released(Key::Space));
}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.pressed_in_frames += u32::from(ctx.pressed(Key::Space));
self.down = ctx.down(Key::Space);
self.pointer = ctx.pointer();
self.captured = ctx.actuated_button();
}
}
fn clicking(title: &str) -> Option<Session<Clicker>> {
Session::new(Config::new(title), UVec2::splat(SIDE), |_ctx| {
Ok(Clicker::default())
})
.ok()
}
#[test]
fn a_session_reads_the_controls_it_is_handed_and_no_others() {
let Some(mut session) = clicking("headless input") else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.tick();
session.step();
assert_eq!(session.game().pressed_in_ticks, 0, "nothing pressed one");
assert_eq!(session.game().pressed_in_frames, 0);
assert!(!session.game().down);
assert_eq!(session.game().pointer, Vec2::ZERO, "and none moved it");
session.set_pointer(Vec2::new(12.0, 20.0));
session.step();
assert_eq!(session.game().pointer, Vec2::new(12.0, 20.0));
}
#[test]
fn a_pressed_control_reaches_the_ticks_of_one_batch_and_the_frame_after_them() {
let Some(mut session) = clicking("headless press") else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.press(Key::Space);
session.tick();
session.tick();
session.step();
assert_eq!(session.game().pressed_in_ticks, 1, "the ticks of one batch");
assert_eq!(session.game().pressed_in_frames, 1, "and one frame");
assert!(session.game().down);
session.step();
assert_eq!(session.game().pressed_in_frames, 1, "the edge is spent");
assert!(session.game().down, "and the control is still held");
session.release(Key::Space);
session.tick();
session.step();
assert_eq!(session.game().released_in_ticks, 1, "as a release is");
assert_eq!(session.game().pressed_in_ticks, 1, "and is no press");
assert!(!session.game().down);
}
#[test]
fn a_pressed_mouse_button_is_the_control_a_capture_returns() {
let Some(mut session) = clicking("headless click") else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.press(MouseButton::Left);
session.step();
assert_eq!(
session.game().captured,
Some(ButtonBinding::Mouse(MouseButton::Left))
);
session.step();
assert_eq!(session.game().captured, None, "a held control is not new");
}
#[test]
fn a_step_batches_its_draws_and_places_them() {
let Ok(mut session) = Session::new(raw("headless test"), UVec2::splat(64), |_ctx| Ok(RedCube))
else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let stats = session.step();
assert_eq!((stats.draw_calls(), stats.instances()), (1, 1));
let pixels = session.pixels().expect("the target reads back");
assert_eq!(pixels.len(), 64 * 64 * 4);
let pixel = |x: usize, y: usize| {
let at = (y * 64 + x) * 4;
[pixels[at], pixels[at + 1], pixels[at + 2], pixels[at + 3]]
};
let background = pixel(0, 0);
assert_eq!(
pixel(16, 32),
[255, 0, 0, 255],
"the cube sits left of center"
);
assert_eq!(pixel(32, 32), background, "the origin is empty");
assert_eq!(pixel(16, 20), background, "the cube is squashed, not tall");
}
struct Pointing {
cursor: Option<Cursor>,
}
impl Game for Pointing {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
if let Some(cursor) = self.cursor {
ctx.set_cursor(cursor);
}
}
}
fn pointing(first: Cursor) -> Option<Session<Pointing>> {
started(Config::new("headless cursor"), UVec2::splat(SIDE), |_ctx| {
Ok(Pointing {
cursor: Some(first),
})
})
}
#[test]
fn a_frame_draws_the_pointer_as_the_cursor_it_set_and_as_the_arrow_where_it_set_none() {
let Some(mut session) = pointing(Cursor::Grab) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
assert_eq!(session.cursor(), Cursor::Arrow, "before the first step");
session.step();
assert_eq!(session.cursor(), Cursor::Grab, "what the frame set");
session.game_mut().cursor = None;
session.step();
assert_eq!(
session.cursor(),
Cursor::Arrow,
"and a frame that sets none"
);
}
#[test]
fn a_frame_that_holds_the_pointer_reads_back_held_until_a_frame_sets_another_cursor() {
let Some(mut session) = pointing(Cursor::Held) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
assert_eq!(
session.cursor(),
Cursor::Held,
"the frame holds the pointer"
);
session.game_mut().cursor = None;
session.step();
assert_eq!(
session.cursor(),
Cursor::Arrow,
"and the frame after it releases it"
);
}
#[derive(Clone, Copy)]
struct Select;
impl InputAction for Select {
type Binding = ButtonBinding;
fn defaults(&self) -> Vec<ButtonBinding> {
self.bindings()
}
fn all() -> Vec<Self> {
vec![Self]
}
fn name(&self) -> &'static str {
"Select"
}
fn from_name(name: &str) -> Option<Self> {
(name == "Select").then_some(Self)
}
}
impl InputButtonAction for Select {
fn bindings(&self) -> Vec<ButtonBinding> {
vec![ButtonBinding::Mouse(MouseButton::Left)]
}
}
#[derive(Clone, Copy)]
enum Steering {
Zoom,
Look,
}
impl InputAction for Steering {
type Binding = AxisBinding;
fn defaults(&self) -> Vec<AxisBinding> {
self.bindings()
}
fn all() -> Vec<Self> {
vec![Self::Zoom, Self::Look]
}
fn name(&self) -> &'static str {
match self {
Self::Zoom => "Zoom",
Self::Look => "Look",
}
}
fn from_name(name: &str) -> Option<Self> {
match name {
"Zoom" => Some(Self::Zoom),
"Look" => Some(Self::Look),
_ => None,
}
}
}
impl InputAxisAction for Steering {
fn bindings(&self) -> Vec<AxisBinding> {
match self {
Self::Zoom => vec![AxisBinding::from(WheelDelta::Up).scale(0.25)],
Self::Look => {
vec![AxisBinding::pointer_delta(PointerDelta::Sideways).scale(0.01)]
}
}
}
}
struct Controls;
impl InputActions for Controls {
type Button = Select;
type Axis = Steering;
type Axis2 = NoInputAxes2;
}
#[derive(Default)]
struct Zoomer {
zoom: f32,
look: f32,
selected: bool,
}
impl Game for Zoomer {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = Controls;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.zoom = ctx.axis(Steering::Zoom);
self.look = ctx.axis(Steering::Look);
self.selected = ctx.pressed(Select);
}
}
#[test]
fn a_wheel_the_session_turns_reads_back_through_an_axis_bound_to_its_lane() {
let Some(mut session) = started(Config::new("headless wheel"), UVec2::splat(SIDE), |_ctx| {
Ok(Zoomer::default())
}) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
assert_eq!(session.game().zoom, 0.0, "nothing turned it");
session.wheel_delta(WheelDelta::Up, 3.0);
session.step();
assert_eq!(session.game().zoom, 0.75, "three notches at a quarter each");
session.step();
assert_eq!(session.game().zoom, 0.0, "and a turn lasts one reading");
session.wheel_delta(WheelDelta::Sideways, 3.0);
session.step();
assert_eq!(
session.game().zoom,
0.0,
"and the other lane is another control"
);
}
#[test]
fn a_pointer_the_session_moves_reads_back_the_whole_distance_its_scale_makes_of_it() {
let Some(mut session) = started(
Config::new("headless pointer"),
UVec2::splat(SIDE),
|_ctx| Ok(Zoomer::default()),
) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
assert_eq!(session.game().look, 0.0, "nothing moved it");
session.pointer_delta(PointerDelta::Sideways, 500.0);
session.step();
assert_eq!(
session.game().look,
5.0,
"five hundred pixels at a hundredth each"
);
session.step();
assert_eq!(session.game().look, 0.0, "and a movement lasts one reading");
}
#[derive(Default)]
struct Timed {
frames: Vec<(Duration, Duration)>,
}
impl Game for Timed {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.frames.push((ctx.dt(), ctx.elapsed()));
}
}
#[test]
fn a_frame_interval_paces_the_clock_every_step_reports() {
let interval = Duration::from_micros(16_667);
let config = Config::new("headless frames").with_tick_interval(interval);
let Some(mut session) = started(config, UVec2::splat(SIDE), |_ctx| Ok(Timed::default())) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
assert_eq!(
session.game().frames,
vec![(Duration::ZERO, Duration::ZERO)],
"a session starts at no interval at all"
);
let mut session = session.with_frame_interval(interval);
session.step();
session.step();
assert_eq!(
session.game().frames[1..],
[(interval, Duration::ZERO), (interval, interval)],
"every step covers the interval, and the clock adds it up"
);
session.tick();
session.step();
assert_eq!(
session.game().frames[3],
(interval, interval * 2),
"a tick between two steps is the same span, counted once"
);
session.set_frame_interval(Duration::ZERO);
session.step();
assert_eq!(
session.game().frames[4],
(Duration::ZERO, interval * 3),
"and a step of no interval leaves the clock where it is"
);
}
#[test]
fn ticks_and_steps_over_one_span_advance_the_clock_once() {
let interval = Duration::from_micros(16_667);
let config = Config::new("headless clock").with_tick_interval(interval);
let Some(session) = started(config, UVec2::splat(SIDE), |_ctx| Ok(Timed::default())) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let mut session = session.with_frame_interval(interval);
for _ in 0..3 {
session.tick();
session.step();
}
let elapsed: Vec<Duration> = session.game().frames.iter().map(|&(_, at)| at).collect();
assert_eq!(
elapsed,
vec![interval, interval * 2, interval * 3],
"a tick and a step over one span count it once"
);
}
#[derive(Default)]
struct Counter {
clicks: Vec<u32>,
}
impl Game for Counter {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = Controls;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.clicks.push(ctx.clicks(Select));
}
}
fn clicked(session: &mut Session<Counter>) -> u32 {
session.press(MouseButton::Left);
session.step();
let clicks = *session
.game()
.clicks
.last()
.expect("a frame read the press");
session.release(MouseButton::Left);
session.step();
clicks
}
#[test]
fn presses_in_a_row_read_back_as_the_clicks_of_one_action() {
let config =
Config::new("headless clicks").with_double_click_interval(Duration::from_millis(400));
let Some(session) = started(config, UVec2::splat(SIDE), |_ctx| Ok(Counter::default())) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let mut session = session.with_frame_interval(Duration::from_millis(100));
session.step();
assert_eq!(session.game().clicks, vec![0], "a frame with no press");
assert_eq!(clicked(&mut session), 1, "the first click");
assert_eq!(clicked(&mut session), 2, "a second within the interval");
for _ in 0..5 {
session.step();
}
assert_eq!(clicked(&mut session), 1, "and one 300 milliseconds past it");
}
#[cfg(feature = "ui")]
mod ui {
use super::*;
struct Linked;
impl Game for Linked {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_cursor(Cursor::Grab);
ctx.ui(|ui| {
let _ = ui.link("controls");
});
}
}
#[test]
fn the_ui_draws_its_own_cursor_where_the_pointer_is_over_what_it_drew() {
let shown = |at| -> Option<Cursor> {
let mut session = started(
Config::new("headless ui cursor"),
UVec2::splat(64),
|_ctx| Ok(Linked),
)?;
session.step();
session.set_pointer(at);
session.step();
Some(session.cursor())
};
let Some(over) = shown(Vec2::new(12.0, 12.0)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(clear_of_it) = shown(Vec2::new(40.0, 40.0)) else {
return;
};
assert_eq!(over, Cursor::Pointer, "the link draws the hand");
assert_eq!(clear_of_it, Cursor::Grab, "and the frame's own clear of it");
}
struct HeldUnderALink;
impl Game for HeldUnderALink {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
ctx.set_cursor(Cursor::Held);
ctx.ui(|ui| {
let _ = ui.link("controls");
});
}
}
#[test]
fn a_held_pointer_stays_held_over_what_the_ui_draws_a_cursor_of_its_own_for() {
let Some(mut session) = started(
Config::new("headless ui held pointer"),
UVec2::splat(64),
|_ctx| Ok(HeldUnderALink),
) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
session.set_pointer(Vec2::new(12.0, 12.0));
session.step();
assert_eq!(session.cursor(), Cursor::Held);
}
#[derive(Default)]
struct Hud {
claimed: bool,
}
impl Game for Hud {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.claimed = ctx.ui_wants_pointer();
ctx.ui(|ui| {
ui.label("hp");
});
}
}
#[test]
fn a_pointer_the_session_places_claims_the_pointer_over_what_the_ui_drew() {
let claimed = |at| -> Option<bool> {
let mut session = started(
Config::new("headless ui pointer"),
UVec2::splat(64),
|_ctx| Ok(Hud::default()),
)?;
session.step();
session.set_pointer(at);
session.step();
session.step();
Some(session.game().claimed)
};
let Some(over) = claimed(Vec2::new(12.0, 12.0)) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some(clear_of_it) = claimed(Vec2::new(40.0, 40.0)) else {
return;
};
assert!(over, "a pointer placed over the text is claimed");
assert!(!clear_of_it, "and one placed clear of it is not");
}
#[derive(Default)]
struct Entry {
typed: String,
}
impl Game for Entry {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
let typed = &mut self.typed;
ctx.ui(|ui| {
ui.text_edit_singleline(typed).request_focus();
});
}
}
#[test]
fn text_the_session_types_reaches_the_entry_holding_the_keyboard() {
let Some(mut session) =
started(Config::new("headless ui text"), UVec2::splat(64), |_ctx| {
Ok(Entry::default())
})
else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
session.type_text("ab");
session.step();
assert_eq!(session.game().typed, "ab", "the text reaches the entry");
session.press(Key::Backspace);
session.step();
assert_eq!(session.game().typed, "a", "and a press takes a letter off");
}
#[derive(Default)]
struct Panel {
clicks: u32,
presses: u32,
claimed: bool,
}
impl Game for Panel {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = Controls;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.presses += u32::from(ctx.pressed(Select));
self.claimed = ctx.ui_wants_pointer();
let clicked = &mut self.clicks;
ctx.ui(|ui| {
*clicked += u32::from(ui.button("go").clicked());
});
}
}
#[test]
fn one_press_reaches_the_game_and_the_ui_button_under_the_pointer() {
let Some(mut session) = started(
Config::new("headless ui click"),
UVec2::new(128, 64),
|_ctx| Ok(Panel::default()),
) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
session.step();
session.set_pointer(Vec2::new(20.0, 16.0));
session.press(MouseButton::Left);
session.step();
assert_eq!(session.game().presses, 1, "the game reads the press");
session.release(MouseButton::Left);
session.step();
assert_eq!(session.game().clicks, 1, "and the button reads the click");
assert_eq!(session.game().presses, 1, "off one press");
session.step();
assert!(
session.game().claimed,
"which the UI claimed the pointer for"
);
}
struct Zoomed {
zoom: f32,
claimed: bool,
width: f32,
}
impl Game for Zoomed {
type Meshes = CubeSet;
type Sounds = NoSounds;
type InputActions = NoInputActions;
type Skyboxes = NoSkyboxes;
type SurfaceStyles = ();
type PostEffects = ();
fn tick(&mut self, _ctx: &mut TickContext<'_, Self>) {}
fn frame(&mut self, ctx: &mut FrameContext<'_, Self>) {
self.claimed = ctx.ui_wants_pointer();
let zoom = self.zoom;
let width = &mut self.width;
ctx.ui(|ui| {
ui.ctx().set_zoom_factor(zoom);
*width = ui.max_rect().width();
let _ = ui.button("go");
});
}
}
#[test]
fn a_pointer_is_placed_in_the_points_the_layer_lays_out_at() {
let laid_out = |zoom| -> Option<(bool, f32)> {
let mut session = started(
Config::new("headless ui zoom"),
UVec2::new(128, 64),
|_ctx| {
Ok(Zoomed {
zoom,
claimed: false,
width: 0.0,
})
},
)?;
session.step();
session.step();
session.set_pointer(Vec2::new(60.0, 30.0));
session.step();
session.step();
Some((session.game().claimed, session.game().width))
};
let Some((zoomed, width)) = laid_out(2.0) else {
eprintln!("skipped: this machine has no usable graphics adapter");
return;
};
let Some((plain, whole_width)) = laid_out(1.0) else {
return;
};
assert!(
zoomed,
"the pointer divides by the scale the layer lays out at"
);
assert!(
!plain,
"where at a scale of one it is the pixel it was given"
);
assert_eq!(
width, 48.0,
"and the layer covers the target, not twice it: 128 pixels at two \
a point, less the margin on either side"
);
assert_eq!(
whole_width, 112.0,
"where at a scale of one it is those 128 less the margin"
);
}
}