use concinnity_core::components::FrameInput;
use concinnity_core::components::Identity;
use concinnity_core::components::LayoutContainer;
use concinnity_core::components::Sprite;
use concinnity_core::components::TextInput;
use concinnity_core::components::TextLabel;
use concinnity_core::ecs::asset_id::AssetId;
use concinnity_core::ecs::{
Access, CursorState, DesiredCursor, FrameTime, HudLayers, MenuActive, MenuOverride,
OpenDropdown, PipelineContext, ScreenStack, StepResult, System,
};
use concinnity_core::gfx::render_types;
use concinnity_core::render::call_buffer;
use concinnity_core::render::cursor;
use concinnity_core::render::overlay_maps;
use concinnity_core::render::overlay_maps::Placement;
use concinnity_core::render::sprite as gfx_sprite;
use concinnity_core::render::text;
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: overlay_maps::TextureSlots,
pub(crate) debug_hud_chips: Vec<AssetId>,
pub(crate) stat_hud_chips: Vec<AssetId>,
pub(crate) clip_rects: overlay_maps::ClipRects,
pub(crate) initial_viewport: (f32, f32),
}
#[derive(Default)]
pub(crate) struct OverlayFrame {
pub calls: Vec<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<render_types::TextDrawCall>);
#[derive(Debug, Default)]
pub(crate) struct OverlaySystem {
elapsed: f32,
layers: overlay_maps::OverlayLayers,
hud_scratch: hud_layout::LabelLayoutScratch,
widget_scratch: widgets::WidgetScratch,
buffer: call_buffer::TextCallBuffer,
}
impl OverlaySystem {
pub(crate) fn new() -> Self {
Self::default()
}
}
impl System for OverlaySystem {
fn access(&self) -> Access {
Access::new()
.reads_components(crate::component_mask![
Sprite,
TextInput,
LayoutContainer,
Identity
])
.writes_components(crate::component_mask![TextLabel])
.reads_resources(crate::resource_mask![
concinnity_core::ecs::EntityById,
FrameInput,
CursorState,
ScreenStack,
HudLayers,
OpenDropdown,
DesiredCursor,
MenuOverride,
FrameTime,
])
.writes_resources(crate::resource_mask![
crate::gfx::overlay::OverlayAssets,
crate::gfx::overlay::OverlayFrame,
crate::gfx::overlay::OverlayRecycle,
MenuActive,
])
}
fn step(&mut self, ctx: &mut PipelineContext) -> StepResult {
let Some(assets) = ctx.take_resource::<OverlayAssets>() else {
return StepResult::Continue;
};
self.elapsed += ctx
.resource::<FrameTime>()
.copied()
.unwrap_or_default()
.dt
.max(0.0);
let mut frame = self.build_frame(ctx, &assets, self.elapsed);
ctx.insert_resource(assets);
if let Some(forced) = ctx.resource::<MenuOverride>().and_then(|m| m.0) {
frame.menu_active = forced;
}
ctx.insert_resource(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::<FrameInput>()
.map(|i| (i.viewport[0], i.viewport[1]))
.unwrap_or(assets.initial_viewport);
let cursor = ctx.resource::<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 screen_layers = ctx.resource::<ScreenStack>().map(|s| &s.layers);
self.layers.clear();
if let Some(screen_layers) = screen_layers.filter(|l| !l.is_empty()) {
for (_, s, identity) in ctx.join2::<Sprite, Identity>() {
if let Some(layer) = s.screen.and_then(|id| screen_layers.get(&id.id())) {
self.layers.insert(identity.id(), *layer);
}
}
for (_, l, identity) in ctx.join2::<TextLabel, Identity>() {
if let Some(layer) = l.screen.and_then(|id| screen_layers.get(&id.id())) {
self.layers.insert(identity.id(), *layer);
}
}
for (_, t, identity) in ctx.join2::<TextInput, Identity>() {
if let Some(layer) = t.screen.and_then(|id| screen_layers.get(&id.id())) {
self.layers.insert(identity.id(), *layer);
}
}
}
if let Some(overrides) = ctx.resource::<HudLayers>() {
for (id, layer) in &overrides.0 {
self.layers.insert(*id, HUD_OVERRIDE_LAYER_BASE + layer);
}
}
let hud_layers = &self.layers;
let placement = |entity| {
let id = ctx.get::<Identity>(entity).map(|i| i.id());
Placement::of(id, &assets.clip_rects, hud_layers)
};
gfx_sprite::build_sprite_calls_into(
&mut self.buffer,
ctx.query_with_entity::<Sprite>()
.map(|(entity, s)| (s, placement(entity))),
default_atlas_slot,
&assets.sprite_texture_slots,
[win_w, win_h],
);
text::build_text_calls_into(
&mut self.buffer,
ctx.query_with_entity::<TextLabel>()
.map(|(entity, l)| (l, placement(entity))),
&assets.fonts,
[win_w, win_h],
);
if let Some(view) = ctx.resource::<OpenDropdown>().and_then(|d| d.0.as_ref()) {
widgets::build_dropdown_overlay(view, &assets.fonts, &mut self.widget_scratch);
let on_top = Placement {
clip: None,
layer: DROPDOWN_LAYER,
};
gfx_sprite::build_sprite_calls_into(
&mut self.buffer,
self.widget_scratch.sprites.iter().map(|s| (s, on_top)),
default_atlas_slot,
&assets.sprite_texture_slots,
[win_w, win_h],
);
text::build_text_calls_into(
&mut self.buffer,
self.widget_scratch.labels.iter().map(|l| (l, on_top)),
&assets.fonts,
[win_w, win_h],
);
}
const CARET_BLINK_PERIOD: f32 = 1.06;
let caret_visible = (elapsed % CARET_BLINK_PERIOD) < CARET_BLINK_PERIOD * 0.5;
for (entity, ti) in ctx.query_with_entity::<TextInput>() {
if !ti.visible {
continue;
}
widgets::build_text_input_overlay(
ti,
&assets.fonts,
caret_visible,
&mut self.widget_scratch,
);
let field = placement(entity);
gfx_sprite::build_sprite_calls_into(
&mut self.buffer,
self.widget_scratch.sprites.iter().map(|s| (s, field)),
default_atlas_slot,
&assets.sprite_texture_slots,
[win_w, win_h],
);
text::build_text_calls_into(
&mut self.buffer,
self.widget_scratch.labels.iter().map(|l| (l, field)),
&assets.fonts,
[win_w, win_h],
);
}
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::<DesiredCursor>()
.map(|c| c.0)
.unwrap_or_default();
cursor::build_cursor_calls_into(
&mut self.buffer,
sprites,
cursor.pos.into(),
cursor_shape,
default_atlas_slot,
[win_w, win_h],
);
}
let menu_active = ctx
.resource::<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 concinnity_core::components::{SpriteFit, TextAlign};
use concinnity_core::ecs::Arena;
use concinnity_core::ecs::FrameContext;
use concinnity_core::ecs::Ref;
use concinnity_core::ecs::{
ComponentSlot, ComponentStorage, CursorState, DropdownView, FontHandle, HudLayers,
MenuOverride, OpenDropdown, Resources, ScreenStack,
};
use concinnity_core::gfx::font;
use concinnity_core::profile::FrameProfile;
use concinnity_host::store::blob::BlobData;
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) -> font::GlyphMetrics {
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: 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: overlay_maps::TextureSlots::new(),
debug_hud_chips: Vec::new(),
stat_hud_chips: Vec::new(),
clip_rects: overlay_maps::ClipRects::new(),
initial_viewport: (REF_W, REF_H),
}
}
fn sprite() -> Sprite {
Sprite {
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() -> Sprite {
Sprite {
width: REF_W,
height: REF_H,
screen: Some(Ref::new(SCREEN)),
..sprite()
}
}
fn label(content: &str) -> TextLabel {
TextLabel {
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() -> TextInput {
TextInput {
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: Arena,
}
impl TestWorld {
fn new() -> Self {
Self {
components: ComponentStorage::default(),
blob: BlobData::new(vec![Some(Vec::new())]),
profile: FrameProfile::default(),
resources: Resources::new(),
scratch: Arena::with_capacity(64 * 1024),
}
}
fn push_as<C: ComponentSlot>(&mut self, id: AssetId, c: C) {
self.ctx().push_identified(id, c);
}
fn ctx(&mut self) -> PipelineContext<'_> {
PipelineContext {
components: &mut self.components,
blob: &mut self.blob,
profile: &mut self.profile,
resources: &mut self.resources,
frame: 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: &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_as(AssetId(1), sprite());
let mut sys = OverlaySystem::new();
sys.step(&mut w.ctx());
assert!(w.resources.get::<OverlayFrame>().is_none());
assert!(w.resources.get::<MenuActive>().is_none());
}
#[test]
fn step_publishes_the_frame_and_parks_the_assets_back() {
let mut w = TestWorld::new();
w.push_as(AssetId(1), sprite());
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::<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::<MenuActive>().unwrap().0);
let mut w = TestWorld::new();
w.push_as(AssetId(1), backdrop());
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_as(AssetId(1), backdrop());
let frame = w.build(0.0);
assert_eq!(x_span(&frame.calls[0]), (0.0, REF_W));
w.resources.insert(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_as(AssetId(1), label("hud"));
w.push_as(
AssetId(2),
Sprite {
screen: Some(Ref::new(SCREEN)),
..sprite()
},
);
w.push_as(
AssetId(3),
TextLabel {
screen: Some(Ref::new(SCREEN)),
..label("menu")
},
);
w.push_as(
AssetId(4),
TextInput {
screen: Some(Ref::new(SCREEN)),
..text_input()
},
);
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_as(AssetId(1), sprite());
w.push_as(
AssetId(2),
Sprite {
screen: Some(Ref::new(AssetId(99))),
..sprite()
},
);
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_as(
AssetId(1),
Sprite {
screen: Some(Ref::new(SCREEN)),
..sprite()
},
);
w.push_as(AssetId(2), sprite());
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_as(AssetId(1), sprite());
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_as(AssetId(1), sprite());
w.push_as(AssetId(2), label("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_as(AssetId(1), backdrop());
w.push_as(
AssetId(2),
Sprite {
screen: Some(Ref::new(SCREEN)),
..sprite()
},
);
w.push_as(
AssetId(3),
TextLabel {
screen: Some(Ref::new(SCREEN)),
..label("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_as(AssetId(2), sprite());
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_as(AssetId(4), text_input());
w.push_as(AssetId(1), sprite());
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_as(
AssetId(4),
TextInput {
focused: true,
..text_input()
},
);
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 the_step_blinks_the_caret_by_frame_time() {
let mut w = TestWorld::new();
w.push_as(
AssetId(4),
TextInput {
focused: true,
..text_input()
},
);
w.resources.insert(assets());
let mut sys = OverlaySystem::new();
let mut step = |w: &mut TestWorld, dt: f32| {
w.resources.insert(FrameTime { dt, elapsed: 0.0 });
sys.step(&mut w.ctx());
w.resources.get::<OverlayFrame>().unwrap().calls.len()
};
let visible = step(&mut w, 0.0);
assert_eq!(step(&mut w, 0.6), visible - 1, "the dark half");
assert_eq!(step(&mut w, 0.5), visible, "the next period");
}
#[test]
fn hidden_text_inputs_build_no_overlay() {
let mut w = TestWorld::new();
w.push_as(
AssetId(4),
TextInput {
visible: false,
focused: true,
..text_input()
},
);
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_as(
AssetId(1),
Sprite {
follow_cursor: true,
..sprite()
},
);
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_as(AssetId(1), backdrop());
w.push_as(AssetId(2), sprite());
w.push_as(
AssetId(3),
Sprite {
follow_cursor: true,
..sprite()
},
);
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_as(
AssetId(1),
Sprite {
follow_cursor: true,
tint: [1.0, 1.0, 1.0, 0.0],
..sprite()
},
);
w.push_as(
AssetId(2),
Sprite {
follow_cursor: true,
visible: false,
..sprite()
},
);
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_as(
AssetId(1),
Sprite {
follow_cursor: true,
..sprite()
},
);
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_as(AssetId(1), backdrop());
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_as(
AssetId(1),
Sprite {
tint: [0.0, 0.0, 0.0, 0.5],
..backdrop()
},
);
w.resources.insert(screen_stack(0));
assert!(!w.build(0.0).world_hidden);
let mut w = TestWorld::new();
w.push_as(
AssetId(1),
Sprite {
width: REF_W / 2.0,
..backdrop()
},
);
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_as(AssetId(1), backdrop());
assert!(!w.build(0.0).world_hidden);
}
}