use super::*;
use crate::ecs::Point;
use crate::ecs::pointer::PhysicalPoint;
use std::time::Instant;
use windows::Win32::Foundation::HWND;
fn entity(id: u32) -> Entity {
Entity::from_raw_u32(id).expect("valid test entity index")
}
fn ppoint() -> PhysicalPoint {
PhysicalPoint { x: 0, y: 0 }
}
fn dragging() -> DragStateSnapshot {
DragStateSnapshot::Dragging {
entity: entity(1),
start_pos: ppoint(),
current_pos: ppoint(),
prev_pos: ppoint(),
start_time: Instant::now(),
hwnd: HWND::default(),
initial_window_pos: Point { x: 0, y: 0 },
move_window: true,
constraint: None,
}
}
fn just_ended() -> DragStateSnapshot {
DragStateSnapshot::JustEnded {
entity: entity(1),
position: ppoint(),
cancelled: false,
}
}
#[test]
fn maps_hit_some_to_opaque_when_last_was_transparent() {
let out = resolve_transition(
Some(entity(7)),
&DragStateSnapshot::Idle,
DesiredState::Transparent,
);
assert_eq!(out, Some(DesiredState::Opaque));
}
#[test]
fn maps_hit_none_to_transparent_when_last_was_opaque() {
let out = resolve_transition(None, &DragStateSnapshot::Idle, DesiredState::Opaque);
assert_eq!(out, Some(DesiredState::Transparent));
}
#[test]
fn diff_guard_hit_some_already_opaque_returns_none() {
let out = resolve_transition(
Some(entity(7)),
&DragStateSnapshot::Idle,
DesiredState::Opaque,
);
assert_eq!(out, None);
}
#[test]
fn diff_guard_hit_none_already_transparent_returns_none() {
let out = resolve_transition(None, &DragStateSnapshot::Idle, DesiredState::Transparent);
assert_eq!(out, None);
}
#[test]
fn dragging_never_goes_transparent_even_when_hit_none() {
let out = resolve_transition(None, &dragging(), DesiredState::Opaque);
assert_eq!(out, None);
}
#[test]
fn dragging_forces_opaque_when_last_was_transparent() {
let out = resolve_transition(None, &dragging(), DesiredState::Transparent);
assert_eq!(out, Some(DesiredState::Opaque));
}
#[test]
fn just_started_also_suppresses_transparent() {
let just_started = DragStateSnapshot::JustStarted {
entity: entity(1),
start_pos: ppoint(),
current_pos: ppoint(),
start_time: Instant::now(),
};
assert_eq!(
resolve_transition(None, &just_started, DesiredState::Opaque),
None
);
}
#[test]
fn just_ended_reconverges_to_transparent_when_hit_none() {
let out = resolve_transition(None, &just_ended(), DesiredState::Opaque);
assert_eq!(out, Some(DesiredState::Transparent));
}
#[test]
fn just_ended_reconverges_to_opaque_when_hit_some() {
let out = resolve_transition(Some(entity(3)), &just_ended(), DesiredState::Transparent);
assert_eq!(out, Some(DesiredState::Opaque));
}
#[test]
fn preparing_behaves_as_non_drag_mapping() {
let preparing = DragStateSnapshot::Preparing {
entity: entity(1),
start_pos: ppoint(),
start_time: Instant::now(),
};
assert_eq!(
resolve_transition(None, &preparing, DesiredState::Opaque),
Some(DesiredState::Transparent)
);
assert_eq!(
resolve_transition(Some(entity(2)), &preparing, DesiredState::Transparent),
Some(DesiredState::Opaque)
);
}
use crate::api::get_window_long_ptr;
use crate::ecs::layout::GlobalArrangement;
use crate::ecs::types::{Rect, SizeI};
use crate::ecs::window::WindowPos;
use crate::ecs::world::EcsWorld;
use bevy_ecs::world::World;
use std::cell::RefCell as StdRefCell;
use std::rc::Rc as StdRc;
use windows::Win32::UI::WindowsAndMessaging::{GWL_EXSTYLE, WS_EX_TRANSPARENT};
fn global_arrangement(left: f32, top: f32, right: f32, bottom: f32) -> GlobalArrangement {
GlobalArrangement {
transform: windows_numerics::Matrix3x2::translation(left, top),
bounds: Rect {
left,
top,
right,
bottom,
},
}
}
fn create_test_hwnd() -> HWND {
use windows::Win32::System::LibraryLoader::GetModuleHandleW;
use windows::Win32::UI::WindowsAndMessaging::{
CreateWindowExW, WINDOW_EX_STYLE, WINDOW_STYLE, WS_POPUP,
};
use windows::core::w;
unsafe {
let hinstance = GetModuleHandleW(None).expect("GetModuleHandleW");
CreateWindowExW(
WINDOW_EX_STYLE(0),
w!("Static"),
w!("wintf-clickthrough-eval-test"),
WINDOW_STYLE(WS_POPUP.0),
0,
0,
0,
0,
None,
None,
Some(hinstance.into()),
None,
)
.expect("CreateWindowExW should create a hidden test window")
}
}
fn destroy_test_hwnd(hwnd: HWND) {
use windows::Win32::UI::WindowsAndMessaging::DestroyWindow;
unsafe {
let _ = DestroyWindow(hwnd);
}
}
fn is_transparent(hwnd: HWND) -> bool {
let ex = get_window_long_ptr(hwnd, GWL_EXSTYLE).expect("read ex-style") as u32;
ex & WS_EX_TRANSPARENT.0 != 0
}
fn sim_s2c(cursor: PhysicalPoint) -> impl Fn(HWND) -> Option<PointF> {
move |_hwnd| {
Some(PointF::new(
(cursor.x - 100) as f32,
(cursor.y - 200) as f32,
))
}
}
fn world_with_hittable_window(world: &mut World) -> Entity {
let window = world
.spawn((
global_arrangement(100.0, 200.0, 500.0, 500.0),
WindowPos {
position: Some(Point { x: 100, y: 200 }),
size: Some(SizeI {
width: 400,
height: 300,
}),
..Default::default()
},
))
.id();
let widget = world
.spawn(global_arrangement(150.0, 250.0, 250.0, 300.0))
.id();
world.entity_mut(window).add_children(&[widget]);
window
}
#[test]
fn eval_hit_some_from_transparent_becomes_opaque() {
let mut world = World::new();
let window = world_with_hittable_window(&mut world);
let hwnd = create_test_hwnd();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
apply_click_through(hwnd, true).expect("seed transparent");
assert!(is_transparent(hwnd), "seed: TRANSPARENT must be set");
let mut reg = ClickThroughRegistry::new();
reg.register(window, hwnd);
reg.set_last_applied(window, DesiredState::Transparent);
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(150, 250)));
assert_eq!(
reg.last_applied(window),
Some(DesiredState::Opaque),
"hit=Some は Opaque へ収束すべき"
);
assert!(!is_transparent(hwnd), "Opaque 適用後 TRANSPARENT はクリア");
}));
destroy_test_hwnd(hwnd);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[test]
fn eval_hit_none_from_opaque_becomes_transparent() {
let mut world = World::new();
let window = world_with_hittable_window(&mut world);
let hwnd = create_test_hwnd();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
apply_click_through(hwnd, false).expect("seed opaque");
assert!(!is_transparent(hwnd), "seed: TRANSPARENT must be clear");
let mut reg = ClickThroughRegistry::new();
reg.register(window, hwnd);
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(0, 0)));
assert_eq!(
reg.last_applied(window),
Some(DesiredState::Transparent),
"hit=None は Transparent へ収束すべき"
);
assert!(
is_transparent(hwnd),
"Transparent 適用後 TRANSPARENT はセット"
);
}));
destroy_test_hwnd(hwnd);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[test]
fn eval_diff_guard_stable_on_repeat() {
let mut world = World::new();
let window = world_with_hittable_window(&mut world);
let hwnd = create_test_hwnd();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
apply_click_through(hwnd, true).expect("seed transparent");
let mut reg = ClickThroughRegistry::new();
reg.register(window, hwnd);
reg.set_last_applied(window, DesiredState::Transparent);
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(150, 250)));
assert_eq!(reg.last_applied(window), Some(DesiredState::Opaque));
assert!(!is_transparent(hwnd));
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(150, 250)));
assert_eq!(
reg.last_applied(window),
Some(DesiredState::Opaque),
"同一入力の再評価で状態は不変であるべき(差分ガード)"
);
assert!(!is_transparent(hwnd), "再評価でも ex-style は不変");
}));
destroy_test_hwnd(hwnd);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[test]
fn eval_honors_drag_snapshot_just_ended_reconverges() {
use crate::ecs::drag::DragState;
use crate::ecs::drag::{snapshot_drag_state, update_drag_state};
let mut world = World::new();
let window = world_with_hittable_window(&mut world);
let hwnd = create_test_hwnd();
update_drag_state(|state| {
*state = DragState::JustEnded {
entity: entity(1),
position: ppoint(),
cancelled: false,
};
});
assert!(matches!(
snapshot_drag_state(),
DragStateSnapshot::JustEnded { .. }
));
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
apply_click_through(hwnd, true).expect("seed transparent");
let mut reg = ClickThroughRegistry::new();
reg.register(window, hwnd);
reg.set_last_applied(window, DesiredState::Transparent);
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(150, 250)));
assert_eq!(
reg.last_applied(window),
Some(DesiredState::Opaque),
"JustEnded サイクルは現在 hit に基づき再収束すべき"
);
}));
update_drag_state(|s| *s = DragState::Idle);
destroy_test_hwnd(hwnd);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
fn is_layered(hwnd: HWND) -> bool {
use windows::Win32::UI::WindowsAndMessaging::WS_EX_LAYERED as EX_LAYERED;
let ex = get_window_long_ptr(hwnd, GWL_EXSTYLE).expect("read ex-style") as u32;
ex & EX_LAYERED.0 != 0
}
#[test]
fn eval_applies_layered_companion_on_first_pass() {
let mut world = World::new();
let window = world_with_hittable_window(&mut world);
let hwnd = create_test_hwnd();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
assert!(!is_layered(hwnd), "seed: LAYERED must start clear");
let mut reg = ClickThroughRegistry::new();
reg.register(window, hwnd);
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(150, 250)));
assert!(is_layered(hwnd), "初回評価で WS_EX_LAYERED が立つべき");
assert_eq!(
reg.iter().next().map(|t| t.layered_applied),
Some(true),
"適用成功後に layered_applied が真へ倒れるべき"
);
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(0, 0)));
assert!(is_layered(hwnd), "以後の評価でも LAYERED は保持される");
}));
destroy_test_hwnd(hwnd);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[test]
fn eval_skips_window_without_position() {
let mut world = World::new();
let window = world.spawn(global_arrangement(0.0, 0.0, 100.0, 100.0)).id();
let hwnd = create_test_hwnd();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let mut reg = ClickThroughRegistry::new();
reg.register(window, hwnd);
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(10, 10)));
assert_eq!(reg.last_applied(window), Some(DesiredState::Opaque));
}));
destroy_test_hwnd(hwnd);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
use crate::ecs::window::{Window as WinComp, WindowStyle};
use windows::Win32::UI::WindowsAndMessaging::{WS_EX_LAYERED, WS_POPUP};
fn spawn_live_window(world: &mut World) -> Entity {
world
.spawn((
WinComp {
title: "PruneTest".to_string(),
parent: None,
},
WindowStyle {
style: WS_POPUP,
ex_style: WS_EX_LAYERED,
},
))
.id()
}
#[test]
fn prune_removes_despawned_window_entity() {
let mut world = World::new();
let live = spawn_live_window(&mut world);
let dead = spawn_live_window(&mut world);
let mut reg = ClickThroughRegistry::new();
reg.register(live, HWND::default());
reg.register(dead, HWND::default());
assert_eq!(reg.len(), 2);
world.despawn(dead);
let removed = prune_dead_targets(&world, &mut reg);
assert_eq!(removed, 1, "despawn 済み Entity 1 件が除去されるべき");
assert_eq!(reg.len(), 1);
assert!(reg.last_applied(live).is_some(), "生存窓は残る");
assert!(reg.last_applied(dead).is_none(), "破棄窓は除去される");
}
#[test]
fn prune_keeps_live_entity_without_window_component() {
let mut world = World::new();
let bare = world.spawn_empty().id();
let mut reg = ClickThroughRegistry::new();
reg.register(bare, HWND::default());
assert_eq!(reg.len(), 1);
let removed = prune_dead_targets(&world, &mut reg);
assert_eq!(removed, 0, "生存 Entity は Window 有無に関わらず残す");
assert_eq!(reg.len(), 1);
}
#[test]
fn eval_skips_apply_for_destroyed_window() {
let mut world = World::new();
let window = world_with_hittable_window(&mut world);
let hwnd = create_test_hwnd();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
apply_click_through(hwnd, true).expect("seed transparent");
assert!(is_transparent(hwnd), "seed: TRANSPARENT set");
let mut reg = ClickThroughRegistry::new();
reg.register(window, hwnd);
reg.set_last_applied(window, DesiredState::Transparent);
world.despawn(window);
evaluate_targets(&world, &mut reg, sim_s2c(PhysicalPoint::new(150, 250)));
assert!(reg.is_empty(), "破棄窓は評価前 prune で除去される");
assert!(
is_transparent(hwnd),
"破棄窓へは apply が走らず ex-style は seed のまま不変であるべき"
);
}));
destroy_test_hwnd(hwnd);
if let Err(e) = result {
std::panic::resume_unwind(e);
}
}
#[test]
fn registry_handle_register_remove_reflects_shared_registry() {
let registry = StdRc::new(StdRefCell::new(ClickThroughRegistry::new()));
let handle = ClickThroughRegistryHandle::new(StdRc::clone(®istry));
let mut w = World::new();
let e = w.spawn_empty().id();
let hwnd = create_test_hwnd();
assert!(handle.is_empty());
handle.register(e, hwnd);
assert_eq!(handle.len(), 1);
assert_eq!(registry.borrow().len(), 1, "共有レジストリへ反映される");
assert!(handle.remove(e));
assert!(handle.is_empty());
assert!(registry.borrow().is_empty());
destroy_test_hwnd(hwnd);
}
#[test]
fn start_then_drop_joins_worker_without_hanging() {
let world = StdRc::new(StdRefCell::new(EcsWorld::new()));
let registry = StdRc::new(StdRefCell::new(ClickThroughRegistry::new()));
let wake = Arc::new(Event::new());
let handle = ClickThroughController::start(
StdRc::downgrade(&world),
StdRc::clone(®istry),
Arc::clone(&wake),
StdRc::new(std::cell::Cell::new(true)),
);
let mut w = World::new();
let e = w.spawn_empty().id();
let hwnd = create_test_hwnd();
handle.register(e, hwnd);
assert_eq!(
registry.borrow().len(),
1,
"start 後の register が反映される"
);
assert!(handle.remove(e), "start 後の remove が反映される");
assert!(registry.borrow().is_empty());
destroy_test_hwnd(hwnd);
drop(handle);
}
#[test]
fn shutdown_condition_world_weak_upgrade_none() {
let world = StdRc::new(StdRefCell::new(EcsWorld::new()));
let weak = StdRc::downgrade(&world);
assert!(weak.upgrade().is_some(), "生存中は upgrade できる");
drop(world);
assert!(
weak.upgrade().is_none(),
"world drop 後は Weak upgrade が None(ループはこの契機で終了する)"
);
}
#[test]
fn eval_loop_stops_without_a_check_once_the_loop_has_returned() {
let world = StdRc::new(StdRefCell::new(EcsWorld::new()));
let registry = StdRc::new(StdRefCell::new(ClickThroughRegistry::new()));
let wake = Arc::new(Event::new());
let running = StdRc::new(std::cell::Cell::new(true));
let gone = {
let mut ecs = world.borrow_mut();
let e = ecs.world_mut().spawn_empty().id();
ecs.world_mut().despawn(e);
e
};
let handle = ClickThroughController::start(
StdRc::downgrade(&world),
StdRc::clone(®istry),
Arc::clone(&wake),
StdRc::clone(&running),
);
crate::executor::block_on(async {}); let held_while_waiting = (
StdRc::strong_count(&world) - 1,
StdRc::strong_count(&handle.monitor) - 1,
);
registry.borrow_mut().register(gone, HWND::default());
wake.notify(usize::MAX);
crate::executor::block_on(async {});
assert!(
registry.borrow().is_empty(),
"ループが回っている間の起床は判定を回して居ない窓を刈るはず"
);
registry.borrow_mut().register(gone, HWND::default());
running.set(false);
wake.notify(usize::MAX);
crate::executor::block_on(async {});
let checked_after_return = registry.borrow().is_empty();
assert_eq!(
(held_while_waiting, checked_after_return),
((0, 0), false),
"((待ちの間に握っていた World の強参照, カーソル監視の強参照), ループが戻った後に判定が回ったか) \
は ((0, 0), false) のはず"
);
drop(handle);
}