use crate::components::{LoadingOverlay, ScreenCommand, Sprite, TextLabel};
use crate::ecs::asset_id::AssetId;
use crate::ecs::{PipelineContext, StepResult, System};
use crate::gfx::scene_flow::FadePhase;
use crate::gfx::scene_residency::SceneLoadState;
use std::time::Instant;
const FADE_OUT_SECS: f32 = 0.35;
#[derive(Debug)]
enum Phase {
Hidden,
Shown,
FadingOut { started: Instant },
}
#[derive(Debug)]
pub(crate) struct LoadingOverlaySystem {
screen: Option<AssetId>,
backdrop: Option<AssetId>,
track: Option<AssetId>,
fill: Option<AssetId>,
label: Option<AssetId>,
phase: Phase,
backdrop_alpha: Option<f32>,
}
impl LoadingOverlaySystem {
pub(crate) fn new(config: LoadingOverlay) -> Self {
Self {
screen: config.screen,
backdrop: config.backdrop,
track: config.track,
fill: config.fill,
label: config.label,
phase: Phase::Hidden,
backdrop_alpha: None,
}
}
fn sprite_mut<'a>(ctx: &'a mut PipelineContext, id: Option<AssetId>) -> Option<&'a mut Sprite> {
let id = id?;
ctx.query_mut::<Sprite>().find(|s| s.asset_id == id)
}
fn set_visible(ctx: &mut PipelineContext, id: Option<AssetId>, visible: bool) {
if let Some(sprite) = Self::sprite_mut(ctx, id) {
sprite.visible = visible;
}
}
fn set_backdrop_alpha(&self, ctx: &mut PipelineContext, alpha: f32) {
if let Some(sprite) = Self::sprite_mut(ctx, self.backdrop) {
sprite.tint[3] = alpha;
}
}
fn show(&mut self, ctx: &mut PipelineContext, progress: f32) {
if self.backdrop_alpha.is_none() {
self.backdrop_alpha = Self::sprite_mut(ctx, self.backdrop).map(|s| s.tint[3]);
}
if let Some(alpha) = self.backdrop_alpha {
self.set_backdrop_alpha(ctx, alpha);
}
Self::set_visible(ctx, self.track, true);
if let Some(id) = self.label
&& let Some(label) = ctx.query_mut::<TextLabel>().find(|l| l.asset_id == id)
{
label.visible = true;
}
self.update_bar(ctx, progress);
}
fn update_bar(&self, ctx: &mut PipelineContext, progress: f32) {
let progress = progress.clamp(0.0, 1.0);
let track = self
.track
.and_then(|id| ctx.query::<Sprite>().find(|s| s.asset_id == id))
.map(|s| (s.x, s.width));
if let Some(fill) = Self::sprite_mut(ctx, self.fill) {
if let Some((x, width)) = track {
fill.x = x;
fill.width = width * progress;
}
fill.visible = progress > 0.0;
}
if let Some(id) = self.label
&& let Some(label) = ctx.query_mut::<TextLabel>().find(|l| l.asset_id == id)
{
label.content = format!("Loading {:.0}%", progress * 100.0);
}
}
fn hide_bar(&self, ctx: &mut PipelineContext) {
Self::set_visible(ctx, self.track, false);
Self::set_visible(ctx, self.fill, false);
if let Some(id) = self.label
&& let Some(label) = ctx.query_mut::<TextLabel>().find(|l| l.asset_id == id)
{
label.visible = false;
}
}
fn screen_on_top(&self, ctx: &PipelineContext) -> Option<bool> {
let screen = self.screen?;
let stack = ctx.resource::<crate::ecs::ScreenStack>()?;
let mine = *stack.layers.get(&screen)?;
Some(stack.layers.values().all(|&layer| layer <= mine))
}
}
impl System for LoadingOverlaySystem {
fn access(&self) -> crate::ecs::Access {
crate::ecs::Access::new()
.writes_components(crate::component_mask![
crate::components::Sprite,
crate::components::TextLabel,
])
.reads_resources(crate::resource_mask![
crate::ecs::ActiveSceneFlow,
crate::ecs::SceneResidencyStatus,
crate::ecs::ScreenStack,
])
.writes_resources(crate::resource_mask![crate::components::ScreenCommand])
}
fn step(&mut self, ctx: &mut PipelineContext) -> StepResult {
let target = ctx
.resource::<crate::ecs::ActiveSceneFlow>()
.and_then(|slot| slot.flow.as_ref())
.map(|flow| match flow.fade {
FadePhase::ToBlack { next, .. } => next,
_ => flow.current,
});
let status = target.and_then(|t| {
ctx.resource::<crate::ecs::SceneResidencyStatus>()
.and_then(|s| s.scenes.iter().find(|(id, ..)| *id == t))
.copied()
});
let loading = matches!(status, Some((_, SceneLoadState::Loading, _)));
let progress = status.map(|(_, _, p)| p).unwrap_or(1.0);
match self.phase {
Phase::Hidden => {
let menu_up = ctx
.resource::<crate::ecs::ScreenStack>()
.is_some_and(|s| s.pauses_world);
if loading
&& !menu_up
&& let Some(screen) = self.screen
{
ctx.events_mut::<ScreenCommand>()
.send(ScreenCommand::Push(screen));
self.show(ctx, progress);
self.phase = Phase::Shown;
}
}
Phase::Shown => {
if loading {
if self.screen_on_top(ctx).is_none()
&& let Some(screen) = self.screen
{
ctx.events_mut::<ScreenCommand>()
.send(ScreenCommand::Push(screen));
}
self.update_bar(ctx, progress);
} else {
self.hide_bar(ctx);
self.phase = Phase::FadingOut {
started: Instant::now(),
};
}
}
Phase::FadingOut { started } => {
if loading {
self.show(ctx, progress);
self.phase = Phase::Shown;
return StepResult::Continue;
}
let faded = 1.0 - started.elapsed().as_secs_f32() / FADE_OUT_SECS;
if faded > 0.0 {
let base = self.backdrop_alpha.unwrap_or(1.0);
self.set_backdrop_alpha(ctx, base * faded);
return StepResult::Continue;
}
match self.screen_on_top(ctx) {
Some(true) => {
ctx.events_mut::<ScreenCommand>().send(ScreenCommand::Hide);
self.phase = Phase::Hidden;
}
Some(false) => self.set_backdrop_alpha(ctx, 0.0),
None => self.phase = Phase::Hidden,
}
}
}
StepResult::Continue
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::Screen;
use crate::ecs::SYSTEMS;
use crate::ecs::World;
use crate::gfx::scene_flow::SceneFlow;
const SCREEN: AssetId = AssetId(1);
const BACKDROP: AssetId = AssetId(2);
const TRACK: AssetId = AssetId(3);
const FILL: AssetId = AssetId(4);
const LABEL: AssetId = AssetId(5);
const SCENE: AssetId = AssetId(9);
fn overlay_world() -> World {
let mut world = World::new();
world.add_component(LoadingOverlay {
screen: Some(SCREEN),
backdrop: Some(BACKDROP),
track: Some(TRACK),
fill: Some(FILL),
label: Some(LABEL),
});
world.add_component(Screen {
asset_id: SCREEN,
..Default::default()
});
world.add_component(Sprite {
asset_id: BACKDROP,
tint: [0.0, 0.0, 0.0, 1.0],
screen: Some(SCREEN),
..Default::default()
});
world.add_component(Sprite {
asset_id: TRACK,
x: 400.0,
width: 480.0,
screen: Some(SCREEN),
..Default::default()
});
world.add_component(Sprite {
asset_id: FILL,
x: 400.0,
width: 0.0,
visible: false,
screen: Some(SCREEN),
..Default::default()
});
world.add_component(TextLabel {
asset_id: LABEL,
screen: Some(SCREEN),
..Default::default()
});
world.insert_resource(crate::ecs::ActiveSceneFlow {
flow: Some(SceneFlow {
scenes: vec![SCENE],
current: SCENE,
fade: FadePhase::None,
}),
epoch: Instant::now(),
});
world
}
fn set_status(world: &mut World, state: SceneLoadState, progress: f32) {
world.insert_resource(crate::ecs::SceneResidencyStatus {
scenes: vec![(SCENE, state, progress)],
});
}
fn shown(world: &World) -> bool {
world
.resource::<crate::ecs::ScreenStack>()
.is_some_and(|s| s.layers.contains_key(&SCREEN))
}
fn sprite(world: &World, id: AssetId) -> &Sprite {
world.query::<Sprite>().find(|s| s.asset_id == id).unwrap()
}
#[test]
fn loading_scene_shows_the_overlay_and_drives_the_bar() {
let mut world = overlay_world();
set_status(&mut world, SceneLoadState::Loading, 0.25);
world.start(SYSTEMS).unwrap();
world.step();
assert!(shown(&world), "overlay screen should be on the stack");
let fill = sprite(&world, FILL);
assert!(fill.visible);
assert!((fill.width - 480.0 * 0.25).abs() < 1e-3);
let label = world
.query::<TextLabel>()
.find(|l| l.asset_id == LABEL)
.unwrap();
assert_eq!(label.content, "Loading 25%");
}
#[test]
fn resident_scene_never_shows_the_overlay() {
let mut world = overlay_world();
set_status(&mut world, SceneLoadState::Resident, 1.0);
world.start(SYSTEMS).unwrap();
world.step();
assert!(!shown(&world));
}
#[test]
fn missing_residency_status_never_shows_the_overlay() {
let mut world = overlay_world();
world.start(SYSTEMS).unwrap();
world.step();
assert!(!shown(&world));
}
#[test]
fn an_open_pausing_screen_defers_the_overlay() {
let mut world = overlay_world();
world.add_component(Screen {
asset_id: AssetId(20),
initial: true,
..Default::default()
});
set_status(&mut world, SceneLoadState::Loading, 0.5);
world.start(SYSTEMS).unwrap();
world.step();
world.step();
assert!(!shown(&world), "overlay must not cover an open menu");
}
#[test]
fn resident_target_fades_the_overlay_out_and_hides_it() {
let mut world = overlay_world();
set_status(&mut world, SceneLoadState::Loading, 0.5);
world.start(SYSTEMS).unwrap();
world.step();
assert!(shown(&world));
set_status(&mut world, SceneLoadState::Resident, 1.0);
world.step();
assert!(!sprite(&world, FILL).visible);
assert!(!sprite(&world, TRACK).visible);
assert!(shown(&world), "backdrop still fading");
std::thread::sleep(std::time::Duration::from_secs_f32(FADE_OUT_SECS + 0.1));
world.step();
world.step();
assert!(!shown(&world));
set_status(&mut world, SceneLoadState::Loading, 0.1);
world.step();
assert!(shown(&world));
assert_eq!(sprite(&world, BACKDROP).tint[3], 1.0);
assert!(sprite(&world, TRACK).visible);
}
}