use std::collections::HashMap;
use crate::executor::util::Window as LibWindow;
use bevy_ecs::prelude::{Entity, RemovedComponents};
use bevy_ecs::system::NonSendMut;
use crate::ecs::window::Window;
use crate::runtime::wndproc_bridge::WndState;
type ShutdownHook = Box<dyn Fn()>;
pub(crate) struct WindowRegistry<W = LibWindow<WndState>> {
map: HashMap<Entity, W>,
shutdown_hook: Option<ShutdownHook>,
}
impl<W> WindowRegistry<W> {
pub(crate) fn new() -> Self {
Self {
map: HashMap::new(),
shutdown_hook: None,
}
}
pub(crate) fn set_shutdown_hook(&mut self, hook: impl Fn() + 'static) {
self.shutdown_hook = Some(Box::new(hook));
}
pub(crate) fn insert(&mut self, entity: Entity, window: W) -> Option<W> {
self.map.insert(entity, window)
}
pub(crate) fn remove(&mut self, entity: Entity) -> Option<W> {
self.map.remove(&entity)
}
pub(crate) fn is_empty(&self) -> bool {
self.map.is_empty()
}
#[cfg(test)]
pub(crate) fn fire_shutdown_hook(&self) {
if let Some(hook) = self.shutdown_hook.as_ref() {
hook();
}
}
}
pub(crate) fn reconcile_window_registry<W: 'static>(
mut removed: RemovedComponents<Window>,
mut registry: NonSendMut<WindowRegistry<W>>,
) {
let mut removed_any = false;
for entity in removed.read() {
if registry.remove(entity).is_some() {
removed_any = true;
}
}
if removed_any && registry.is_empty() {
if let Some(hook) = registry.shutdown_hook.as_ref() {
hook();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::rc::Rc;
use bevy_ecs::world::World;
use crate::ecs::window::Window;
struct DropTracker {
dropped: Rc<Cell<bool>>,
}
impl Drop for DropTracker {
fn drop(&mut self) {
self.dropped.set(true);
}
}
#[test]
fn insert_remove_roundtrips_by_entity() {
let mut world = World::new();
let e = world.spawn_empty().id();
let mut reg: WindowRegistry<DropTracker> = WindowRegistry::new();
let flag = Rc::new(Cell::new(false));
reg.insert(
e,
DropTracker {
dropped: flag.clone(),
},
);
assert!(!reg.is_empty());
let got = reg.remove(e);
assert!(
got.is_some(),
"remove は Entity キーで挿入済み要素を返すべき"
);
assert!(reg.is_empty());
assert!(!flag.get());
drop(got);
assert!(flag.get(), "remove で取り出した値の drop が観測されるべき");
}
#[test]
fn remove_drops_held_value() {
let mut world = World::new();
let e = world.spawn_empty().id();
let mut reg: WindowRegistry<DropTracker> = WindowRegistry::new();
let flag = Rc::new(Cell::new(false));
reg.insert(
e,
DropTracker {
dropped: flag.clone(),
},
);
assert!(!flag.get());
drop(reg.remove(e));
assert!(flag.get(), "registry 内要素の remove で Drop が走るべき");
}
fn run_reconcile(world: &mut World) {
use bevy_ecs::schedule::Schedule;
let mut sched = Schedule::default();
sched.add_systems(reconcile_window_registry::<DropTracker>);
sched.run(world);
}
#[test]
fn reconcile_removes_entries_and_fires_hook_only_on_empty_transition() {
let mut world = World::new();
let e1 = world.spawn(Window::default()).id();
let e2 = world.spawn(Window::default()).id();
let mut reg: WindowRegistry<DropTracker> = WindowRegistry::new();
let d1 = Rc::new(Cell::new(false));
let d2 = Rc::new(Cell::new(false));
reg.insert(
e1,
DropTracker {
dropped: d1.clone(),
},
);
reg.insert(
e2,
DropTracker {
dropped: d2.clone(),
},
);
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
reg.set_shutdown_hook(move || f.set(f.get() + 1));
world.insert_non_send(reg);
world.entity_mut(e1).remove::<Window>();
run_reconcile(&mut world);
{
let reg = world.get_non_send::<WindowRegistry<DropTracker>>().unwrap();
assert!(!reg.is_empty(), "1 件残るので非空のまま");
}
assert!(
d1.get(),
"破棄 Entity の保持値が drop されるべき(DestroyWindow 相当)"
);
assert!(!d2.get(), "残存 Entity の保持値は drop されない");
assert_eq!(fired.get(), 0, "非空のままなら shutdown フックは発火しない");
world.entity_mut(e2).remove::<Window>();
run_reconcile(&mut world);
{
let reg = world.get_non_send::<WindowRegistry<DropTracker>>().unwrap();
assert!(reg.is_empty(), "全 Window 破棄で registry は空になる");
}
assert!(d2.get(), "最後の保持値も drop される");
assert_eq!(
fired.get(),
1,
"空への遷移ちょうどで shutdown フックが発火する"
);
run_reconcile(&mut world);
assert_eq!(
fired.get(),
1,
"既に空での空振り reconcile では再発火しない"
);
}
}