use crate::components::{Sprite, TextInput, TextLabel};
use crate::ecs::asset_id::AssetId;
use crate::ecs::{PipelineContext, StepResult, System};
use crate::gfx::{sprite as gfx_sprite, text};
use std::time::Instant;
mod hud_layout;
mod widgets;
const HUD_OVERRIDE_LAYER_BASE: i32 = i32::MAX / 2;
const DROPDOWN_LAYER: i32 = i32::MAX - 1;
#[derive(Default)]
pub(crate) struct OverlayAssets {
pub fonts: text::FontSet,
pub(crate) sprite_texture_slots: crate::gfx::overlay_maps::TextureSlots,
pub(crate) debug_hud_chips: Vec<AssetId>,
pub(crate) stat_hud_chips: Vec<AssetId>,
pub(crate) clip_rects: crate::gfx::overlay_maps::ClipRects,
pub(crate) initial_viewport: (f32, f32),
}
#[derive(Default)]
pub(crate) struct OverlayFrame {
pub calls: Vec<crate::gfx::render_types::TextDrawCall>,
pub want_ui_cursor: bool,
pub(crate) menu_active: bool,
pub(crate) world_hidden: bool,
}
#[derive(Default)]
pub(crate) struct OverlayRecycle(pub Vec<crate::gfx::render_types::TextDrawCall>);
#[derive(Debug, Default)]
pub(crate) struct OverlaySystem {
start_time: Option<Instant>,
layers: crate::gfx::overlay_maps::OverlayLayers,
hud_scratch: hud_layout::LabelLayoutScratch,
widget_scratch: widgets::WidgetScratch,
buffer: crate::gfx::call_buffer::TextCallBuffer,
}
impl OverlaySystem {
pub(crate) fn new() -> Self {
Self::default()
}
}
impl System for OverlaySystem {
fn access(&self) -> crate::ecs::Access {
crate::ecs::Access::new()
.reads_components(crate::component_mask![
crate::components::Sprite,
crate::components::TextInput,
crate::components::LayoutContainer,
])
.writes_components(crate::component_mask![crate::components::TextLabel])
.reads_resources(crate::resource_mask![
crate::components::FrameInput,
crate::ecs::CursorState,
crate::ecs::ScreenStack,
crate::ecs::HudLayers,
crate::ecs::OpenDropdown,
crate::ecs::DesiredCursor,
crate::ecs::MenuOverride,
])
.writes_resources(crate::resource_mask![
crate::gfx::overlay::OverlayAssets,
crate::gfx::overlay::OverlayFrame,
crate::gfx::overlay::OverlayRecycle,
crate::ecs::MenuActive,
])
}
fn step(&mut self, ctx: &mut PipelineContext) -> StepResult {
let Some(assets) = ctx.take_resource::<OverlayAssets>() else {
return StepResult::Continue;
};
let elapsed = self
.start_time
.get_or_insert_with(Instant::now)
.elapsed()
.as_secs_f32();
let mut frame = self.build_frame(ctx, &assets, elapsed);
ctx.insert_resource(assets);
if let Some(forced) = ctx.resource::<crate::ecs::MenuOverride>().and_then(|m| m.0) {
frame.menu_active = forced;
}
ctx.insert_resource(crate::ecs::MenuActive(frame.menu_active));
ctx.insert_resource(frame);
StepResult::Continue
}
}
impl OverlaySystem {
fn build_frame(
&mut self,
ctx: &mut PipelineContext,
assets: &OverlayAssets,
elapsed: f32,
) -> OverlayFrame {
if let Some(spent) = ctx.take_resource::<OverlayRecycle>() {
self.buffer.recycle(spent.0);
}
let (win_w, win_h) = ctx
.resource::<crate::components::FrameInput>()
.map(|i| (i.viewport[0], i.viewport[1]))
.unwrap_or(assets.initial_viewport);
let cursor = ctx
.resource::<crate::ecs::CursorState>()
.copied()
.unwrap_or_default();
hud_layout::apply_label_layout(ctx, &assets.fonts, &mut self.hud_scratch);
hud_layout::position_debug_hud(ctx, &assets.debug_hud_chips, &assets.fonts, win_w);
hud_layout::position_stat_hud(ctx, &assets.stat_hud_chips, &assets.fonts);
let default_atlas_slot = assets.fonts.any_atlas_slot();
let sprites: &[Sprite] = ctx.query::<Sprite>().as_slice();
let labels: &[TextLabel] = ctx.query::<TextLabel>().as_slice();
let empty_layers = crate::gfx::overlay_maps::OverlayLayers::new();
let screen_layers = ctx.resource::<crate::ecs::ScreenStack>().map(|s| &s.layers);
self.layers.clear();
if let Some(screen_layers) = screen_layers.filter(|l| !l.is_empty()) {
for s in ctx.query::<Sprite>() {
if let Some(layer) = s.screen.and_then(|id| screen_layers.get(&id)) {
self.layers.insert(s.asset_id, *layer);
}
}
for l in ctx.query::<TextLabel>() {
if let Some(layer) = l.screen.and_then(|id| screen_layers.get(&id)) {
self.layers.insert(l.asset_id, *layer);
}
}
for t in ctx.query::<TextInput>() {
if let Some(layer) = t.screen.and_then(|id| screen_layers.get(&id)) {
self.layers.insert(t.asset_id, *layer);
}
}
}
if let Some(overrides) = ctx.resource::<crate::ecs::HudLayers>() {
for (id, layer) in &overrides.0 {
self.layers.insert(*id, HUD_OVERRIDE_LAYER_BASE + layer);
}
}
let hud_layers = &self.layers;
gfx_sprite::build_sprite_calls_into(
&mut self.buffer,
sprites,
default_atlas_slot,
&assets.sprite_texture_slots,
[win_w, win_h],
&assets.clip_rects,
hud_layers,
);
text::build_text_calls_into(
&mut self.buffer,
labels,
&assets.fonts,
win_w,
win_h,
&assets.clip_rects,
hud_layers,
);
if let Some(view) = ctx
.resource::<crate::ecs::OpenDropdown>()
.and_then(|d| d.0.as_ref())
{
let no_clips = crate::gfx::overlay_maps::ClipRects::new();
widgets::build_dropdown_overlay(view, &assets.fonts, &mut self.widget_scratch);
let dd_start = self.buffer.calls.len();
gfx_sprite::build_sprite_calls_into(
&mut self.buffer,
&self.widget_scratch.sprites,
default_atlas_slot,
&assets.sprite_texture_slots,
[win_w, win_h],
&no_clips,
&empty_layers,
);
text::build_text_calls_into(
&mut self.buffer,
&self.widget_scratch.labels,
&assets.fonts,
win_w,
win_h,
&no_clips,
&empty_layers,
);
for c in &mut self.buffer.calls[dd_start..] {
c.layer = DROPDOWN_LAYER;
}
}
const CARET_BLINK_PERIOD: f32 = 1.06;
let caret_visible = (elapsed % CARET_BLINK_PERIOD) < CARET_BLINK_PERIOD * 0.5;
for ti in ctx.query::<TextInput>() {
if !ti.visible {
continue;
}
widgets::build_text_input_overlay(
ti,
&assets.fonts,
caret_visible,
&mut self.widget_scratch,
);
let ti_layer = hud_layers.get(&ti.asset_id).copied().unwrap_or(0);
let ti_start = self.buffer.calls.len();
gfx_sprite::build_sprite_calls_into(
&mut self.buffer,
&self.widget_scratch.sprites,
default_atlas_slot,
&assets.sprite_texture_slots,
[win_w, win_h],
&assets.clip_rects,
&empty_layers,
);
text::build_text_calls_into(
&mut self.buffer,
&self.widget_scratch.labels,
&assets.fonts,
win_w,
win_h,
&assets.clip_rects,
&empty_layers,
);
for c in &mut self.buffer.calls[ti_start..] {
c.layer = ti_layer;
}
}
let menu_cursor = sprites
.iter()
.any(|s| s.follow_cursor && s.visible && s.tint[3] > 0.0);
let want_ui_cursor = menu_cursor && !cursor.outside_window;
if want_ui_cursor {
let cursor_shape = ctx
.resource::<crate::ecs::DesiredCursor>()
.map(|c| c.0)
.unwrap_or_default();
crate::gfx::cursor::build_cursor_calls_into(
&mut self.buffer,
sprites,
cursor.pos,
cursor_shape,
default_atlas_slot,
[win_w, win_h],
);
}
let menu_active = ctx
.resource::<crate::ecs::ScreenStack>()
.is_some_and(|s| s.pauses_world);
let world_hidden = menu_active
&& sprites.iter().any(|s| {
!s.follow_cursor && s.visible && s.tint[3] >= 1.0 && gfx_sprite::covers_canvas(s)
});
self.buffer.sort_by_layer();
OverlayFrame {
calls: self.buffer.take(),
want_ui_cursor,
menu_active,
world_hidden,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::blob::BlobData;
use crate::components::{SpriteFit, TextAlign};
use crate::ecs::{
ComponentSlot, ComponentStorage, CursorState, DropdownView, FontHandle, HudLayers,
MenuOverride, OpenDropdown, Resources, ScreenStack,
};
use crate::gfx::profile::FrameProfile;
const FONT: FontHandle = FontHandle(0);
const SCREEN: AssetId = AssetId(50);
const REF_W: f32 = 1280.0;
const REF_H: f32 = 720.0;
fn make_glyph(advance_px: f32) -> crate::gfx::font::GlyphMetrics {
crate::gfx::font::GlyphMetrics {
char_code: 0,
atlas_x: 0,
atlas_y: 0,
atlas_w: 8,
atlas_h: 12,
advance_px,
bearing_x: 0.0,
bearing_y: 12.0,
}
}
fn loaded_fonts() -> text::FontSet {
let metrics: crate::gfx::text::FontMetrics = ('a'..='z')
.chain('A'..='Z')
.map(|c| (c as u32, make_glyph(10.0)))
.collect();
let cap_px = text::derive_cap_px(&metrics, 16.0);
let mut fonts = text::FontSet::default();
fonts.insert(
FONT,
text::LoadedFont {
atlas_slot: 0,
cap_px,
metrics,
atlas_w: 128,
atlas_h: 128,
size_px: 16.0,
supersample: 1.0,
},
);
fonts
}
fn assets() -> OverlayAssets {
OverlayAssets {
fonts: loaded_fonts(),
sprite_texture_slots: crate::gfx::overlay_maps::TextureSlots::new(),
debug_hud_chips: Vec::new(),
stat_hud_chips: Vec::new(),
clip_rects: crate::gfx::overlay_maps::ClipRects::new(),
initial_viewport: (REF_W, REF_H),
}
}
fn sprite(id: AssetId) -> Sprite {
Sprite {
asset_id: id,
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
texture: None,
tint: [1.0, 1.0, 1.0, 1.0],
follow_cursor: false,
visible: true,
screen: None,
fit: SpriteFit::Fit,
corner_radius: 0.0,
border_width: 0.0,
border_color: [0.0, 0.0, 0.0, 1.0],
}
}
fn backdrop(id: AssetId) -> Sprite {
Sprite {
width: REF_W,
height: REF_H,
screen: Some(SCREEN),
..sprite(id)
}
}
fn label(id: AssetId, content: &str) -> TextLabel {
TextLabel {
asset_id: id,
font: Some(FONT),
content: content.to_string(),
x: 0.0,
y: 0.0,
color: [1.0, 1.0, 1.0],
scale: 1.0,
centered: false,
align: TextAlign::Left,
fit: SpriteFit::Fit,
background: [0.0, 0.0, 0.0, 0.0],
padding: 0.0,
visible: true,
screen: None,
wrap_width: 0.0,
max_lines: 0,
}
}
fn dropdown_view() -> DropdownView {
DropdownView {
anchor: [400.0, 100.0, 200.0, 40.0],
options: vec!["aa".to_string(), "bb".to_string()],
selected: 0,
first: 0,
hovered: None,
screen: Some(SCREEN),
font: Some(FONT),
scale: 1.0,
color: [1.0, 1.0, 1.0],
}
}
fn text_input(id: AssetId) -> TextInput {
TextInput {
asset_id: id,
font: Some(FONT),
content: "ab".to_string(),
..Default::default()
}
}
fn screen_stack(layer: i32) -> ScreenStack {
ScreenStack {
layers: std::collections::BTreeMap::from([(SCREEN, layer)]),
pauses_world: true,
captures_input: true,
}
}
struct TestWorld {
components: ComponentStorage,
blob: BlobData,
profile: FrameProfile,
resources: Resources,
scratch: crate::ecs::Arena,
}
impl TestWorld {
fn new() -> Self {
Self {
components: ComponentStorage::default(),
blob: BlobData::new(vec![Some(Vec::new())]),
profile: FrameProfile::default(),
resources: Resources::new(),
scratch: crate::ecs::Arena::with_capacity(64 * 1024),
}
}
fn push<C: ComponentSlot>(&mut self, c: C) {
self.components.push_typed(c);
}
fn ctx(&mut self) -> PipelineContext<'_> {
PipelineContext {
components: &mut self.components,
blob: &mut self.blob,
profile: &mut self.profile,
resources: &mut self.resources,
frame: crate::ecs::FrameContext::new(&self.scratch),
}
}
fn build(&mut self, elapsed: f32) -> OverlayFrame {
let a = assets();
OverlaySystem::new().build_frame(&mut self.ctx(), &a, elapsed)
}
}
fn x_span(call: &crate::gfx::render_types::TextDrawCall) -> (f32, f32) {
let xs: Vec<f32> = call.vertices.iter().map(|v| v.pos[0]).collect();
(
xs.iter().copied().fold(f32::INFINITY, f32::min),
xs.iter().copied().fold(f32::NEG_INFINITY, f32::max),
)
}
#[test]
fn step_without_overlay_assets_publishes_nothing() {
let mut w = TestWorld::new();
w.push(sprite(AssetId(1)));
let mut sys = OverlaySystem::new();
sys.step(&mut w.ctx());
assert!(w.resources.get::<OverlayFrame>().is_none());
assert!(w.resources.get::<crate::ecs::MenuActive>().is_none());
}
#[test]
fn step_publishes_the_frame_and_parks_the_assets_back() {
let mut w = TestWorld::new();
w.push(sprite(AssetId(1)));
w.resources.insert(assets());
let mut sys = OverlaySystem::new();
sys.step(&mut w.ctx());
assert_eq!(w.resources.get::<OverlayFrame>().unwrap().calls.len(), 1);
assert!(!w.resources.get::<crate::ecs::MenuActive>().unwrap().0);
assert!(
w.resources.get::<OverlayAssets>().is_some(),
"the assets go back for the next build"
);
}
#[test]
fn step_menu_override_shadows_the_menu_state_but_not_world_hidden() {
let mut w = TestWorld::new();
w.resources.insert(assets());
w.resources.insert(MenuOverride(Some(true)));
let mut sys = OverlaySystem::new();
sys.step(&mut w.ctx());
assert!(w.resources.get::<OverlayFrame>().unwrap().menu_active);
assert!(w.resources.get::<crate::ecs::MenuActive>().unwrap().0);
let mut w = TestWorld::new();
w.push(backdrop(AssetId(1)));
w.resources.insert(assets());
w.resources.insert(screen_stack(0));
w.resources.insert(MenuOverride(Some(false)));
let mut sys = OverlaySystem::new();
sys.step(&mut w.ctx());
let frame = w.resources.get::<OverlayFrame>().unwrap();
assert!(!frame.menu_active);
assert!(frame.world_hidden);
}
#[test]
fn step_menu_override_of_none_defers_to_the_world() {
let mut w = TestWorld::new();
w.resources.insert(assets());
w.resources.insert(screen_stack(0));
w.resources.insert(MenuOverride(None));
let mut sys = OverlaySystem::new();
sys.step(&mut w.ctx());
assert!(w.resources.get::<OverlayFrame>().unwrap().menu_active);
}
#[test]
fn viewport_follows_frame_input_and_falls_back_to_the_init_size() {
let mut w = TestWorld::new();
w.push(backdrop(AssetId(1)));
let frame = w.build(0.0);
assert_eq!(x_span(&frame.calls[0]), (0.0, REF_W));
w.resources.insert(crate::components::FrameInput {
viewport: [800.0, 600.0],
..Default::default()
});
let frame = w.build(0.0);
assert_eq!(x_span(&frame.calls[0]), (0.0, 800.0));
}
#[test]
fn screen_layers_spread_onto_the_elements_the_screen_owns() {
let mut w = TestWorld::new();
w.push(label(AssetId(1), "hud"));
w.push(Sprite {
screen: Some(SCREEN),
..sprite(AssetId(2))
});
w.push(TextLabel {
screen: Some(SCREEN),
..label(AssetId(3), "menu")
});
w.push(TextInput {
screen: Some(SCREEN),
..text_input(AssetId(4))
});
w.resources.insert(screen_stack(7));
let frame = w.build(0.0);
assert_eq!(frame.calls[0].layer, 0);
assert!(
frame.calls[1..].iter().all(|c| c.layer == 7),
"{:?}",
frame.calls.iter().map(|c| c.layer).collect::<Vec<_>>()
);
}
#[test]
fn elements_outside_the_active_stack_stay_at_layer_zero() {
let mut w = TestWorld::new();
w.push(sprite(AssetId(1)));
w.push(Sprite {
screen: Some(AssetId(99)),
..sprite(AssetId(2))
});
w.resources.insert(screen_stack(7));
let frame = w.build(0.0);
assert!(frame.calls.iter().all(|c| c.layer == 0));
}
#[test]
fn editor_layer_overrides_lift_elements_above_screen_layers() {
let mut w = TestWorld::new();
w.push(Sprite {
screen: Some(SCREEN),
..sprite(AssetId(1))
});
w.push(sprite(AssetId(2)));
w.resources.insert(screen_stack(7));
w.resources
.insert(HudLayers(std::collections::BTreeMap::from([(
AssetId(2),
3,
)])));
let frame = w.build(0.0);
assert_eq!(frame.calls[0].layer, 7);
assert_eq!(frame.calls[1].layer, HUD_OVERRIDE_LAYER_BASE + 3);
}
#[test]
fn a_recycled_draw_list_backs_the_next_build() {
let mut w = TestWorld::new();
w.push(sprite(AssetId(1)));
let a = assets();
let mut sys = OverlaySystem::new();
let frame = sys.build_frame(&mut w.ctx(), &a, 0.0);
assert_eq!(frame.calls.len(), 1);
let spent_ptr = frame.calls.as_ptr();
w.resources.insert(OverlayRecycle(frame.calls));
let frame = sys.build_frame(&mut w.ctx(), &a, 0.0);
assert_eq!(frame.calls.as_ptr(), spent_ptr, "list backing reused");
assert_eq!(frame.calls.len(), 1);
assert!(
w.resources
.get::<OverlayRecycle>()
.is_none_or(|r| r.0.is_empty()),
"the spent list was consumed"
);
}
#[test]
fn draw_order_is_insertion_order_without_any_layers() {
let mut w = TestWorld::new();
w.push(sprite(AssetId(1)));
w.push(label(AssetId(2), "hud"));
let frame = w.build(0.0);
assert!(frame.calls.iter().all(|c| c.layer == 0));
assert_eq!(frame.calls.len(), 2);
}
#[test]
fn open_dropdown_draws_unclipped_over_the_menu() {
let mut w = TestWorld::new();
let before = w.build(0.0).calls.len();
w.resources.insert(OpenDropdown(Some(dropdown_view())));
let frame = w.build(0.0);
assert!(frame.calls.len() > before, "the list added draw calls");
assert!(
frame.calls.iter().all(|c| c.clip_rect.is_none()),
"the list is never scissored"
);
}
#[test]
fn open_dropdown_sorts_above_the_menus_row_cards() {
let mut w = TestWorld::new();
w.push(backdrop(AssetId(1)));
w.push(Sprite {
screen: Some(SCREEN),
..sprite(AssetId(2))
});
w.push(TextLabel {
screen: Some(SCREEN),
..label(AssetId(3), "Window Mode")
});
w.resources.insert(screen_stack(7));
let menu = w.build(0.0).calls.len();
w.resources.insert(OpenDropdown(Some(dropdown_view())));
let frame = w.build(0.0);
assert!(frame.calls.len() > menu, "the list added draw calls");
let (below, list) = frame.calls.split_at(menu);
assert!(
below.iter().all(|c| c.layer == 7),
"{:?}",
below.iter().map(|c| c.layer).collect::<Vec<_>>()
);
assert!(
list.iter().all(|c| c.layer == DROPDOWN_LAYER),
"{:?}",
list.iter().map(|c| c.layer).collect::<Vec<_>>()
);
}
#[test]
fn open_dropdown_sorts_above_the_editor_layer_band() {
let mut w = TestWorld::new();
w.push(sprite(AssetId(2)));
w.resources
.insert(HudLayers(std::collections::BTreeMap::from([(
AssetId(2),
3,
)])));
let panel = w.build(0.0).calls.len();
w.resources.insert(OpenDropdown(Some(dropdown_view())));
let frame = w.build(0.0);
let (below, list) = frame.calls.split_at(panel);
assert!(below.iter().all(|c| c.layer == HUD_OVERRIDE_LAYER_BASE + 3));
assert!(list.iter().all(|c| c.layer == DROPDOWN_LAYER));
}
#[test]
fn closed_dropdown_builds_no_list() {
let mut w = TestWorld::new();
w.resources.insert(OpenDropdown(None));
assert!(w.build(0.0).calls.is_empty());
}
#[test]
fn text_input_calls_take_the_fields_own_layer() {
let mut w = TestWorld::new();
w.push(text_input(AssetId(4)));
w.push(sprite(AssetId(1)));
w.resources
.insert(HudLayers(std::collections::BTreeMap::from([(
AssetId(4),
2,
)])));
let frame = w.build(0.0);
let field_layer = HUD_OVERRIDE_LAYER_BASE + 2;
assert!(
frame.calls.iter().any(|c| c.layer == field_layer),
"the field's calls lift with it"
);
assert!(
frame
.calls
.iter()
.all(|c| c.layer == 0 || c.layer == field_layer),
"nothing else moved"
);
}
#[test]
fn the_caret_draws_only_on_the_visible_half_of_the_blink() {
let mut w = TestWorld::new();
w.push(TextInput {
focused: true,
..text_input(AssetId(4))
});
let visible = w.build(0.0).calls.len();
let dark = w.build(0.6).calls.len();
assert_eq!(visible, dark + 1, "the caret is the one call that drops");
assert_eq!(w.build(1.06).calls.len(), visible);
}
#[test]
fn hidden_text_inputs_build_no_overlay() {
let mut w = TestWorld::new();
w.push(TextInput {
visible: false,
focused: true,
..text_input(AssetId(4))
});
assert!(w.build(0.0).calls.is_empty());
}
#[test]
fn an_opaque_follow_cursor_sprite_draws_the_ui_arrow() {
let mut w = TestWorld::new();
w.push(Sprite {
follow_cursor: true,
..sprite(AssetId(1))
});
let frame = w.build(0.0);
assert!(frame.want_ui_cursor);
assert!(!frame.calls.is_empty(), "the arrow was shaped");
}
#[test]
fn the_ui_arrow_sorts_above_screen_editor_and_dropdown_layers() {
let mut w = TestWorld::new();
w.push(backdrop(AssetId(1)));
w.push(sprite(AssetId(2)));
w.push(Sprite {
follow_cursor: true,
..sprite(AssetId(3))
});
w.resources.insert(screen_stack(7));
w.resources.insert(OpenDropdown(Some(dropdown_view())));
w.resources
.insert(HudLayers(std::collections::BTreeMap::from([(
AssetId(2),
3,
)])));
let frame = w.build(0.0);
let layers: Vec<i32> = frame.calls.iter().map(|c| c.layer).collect();
let cursor = *layers.last().expect("the arrow was shaped");
assert!(
layers[..layers.len() - 1].iter().all(|l| *l < cursor),
"{layers:?}"
);
}
#[test]
fn a_transparent_or_hidden_cursor_sprite_draws_no_arrow() {
let mut w = TestWorld::new();
w.push(Sprite {
follow_cursor: true,
tint: [1.0, 1.0, 1.0, 0.0],
..sprite(AssetId(1))
});
w.push(Sprite {
follow_cursor: true,
visible: false,
..sprite(AssetId(2))
});
let frame = w.build(0.0);
assert!(!frame.want_ui_cursor);
assert!(frame.calls.is_empty());
}
#[test]
fn the_ui_arrow_hides_when_the_real_cursor_leaves_the_window() {
let mut w = TestWorld::new();
w.push(Sprite {
follow_cursor: true,
..sprite(AssetId(1))
});
w.resources.insert(CursorState {
pos: (10.0, 10.0),
outside_window: true,
});
let frame = w.build(0.0);
assert!(!frame.want_ui_cursor);
assert!(frame.calls.is_empty(), "the arrow is not shaped either");
}
#[test]
fn menu_active_follows_the_stacks_pauses_world() {
let mut w = TestWorld::new();
assert!(!w.build(0.0).menu_active, "no stack, no menu");
w.resources.insert(screen_stack(0));
assert!(w.build(0.0).menu_active);
w.resources.insert(ScreenStack {
pauses_world: false,
..screen_stack(0)
});
assert!(!w.build(0.0).menu_active);
}
#[test]
fn world_hidden_needs_a_paused_menu_behind_an_opaque_backdrop() {
let mut w = TestWorld::new();
w.push(backdrop(AssetId(1)));
w.resources.insert(screen_stack(0));
assert!(w.build(0.0).world_hidden);
}
#[test]
fn a_translucent_or_partial_backdrop_keeps_the_world_rendering() {
let mut w = TestWorld::new();
w.push(Sprite {
tint: [0.0, 0.0, 0.0, 0.5],
..backdrop(AssetId(1))
});
w.resources.insert(screen_stack(0));
assert!(!w.build(0.0).world_hidden);
let mut w = TestWorld::new();
w.push(Sprite {
width: REF_W / 2.0,
..backdrop(AssetId(1))
});
w.resources.insert(screen_stack(0));
assert!(!w.build(0.0).world_hidden);
}
#[test]
fn an_opaque_backdrop_without_a_paused_menu_keeps_the_world_rendering() {
let mut w = TestWorld::new();
w.push(backdrop(AssetId(1)));
assert!(!w.build(0.0).world_hidden);
}
}