use std::cell::Cell;
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::rc::Rc;
use bevy_ecs::hierarchy::ChildOf;
use bevy_ecs::prelude::Entity;
use windows::Win32::Foundation::HWND;
use windows::Win32::UI::Input::KeyboardAndMouse::{GetCapture, SetCapture};
use windows::Win32::UI::WindowsAndMessaging::{WM_CAPTURECHANGED, WM_LBUTTONUP, WM_MOUSEMOVE};
use super::wndproc_bridge_drag_test_support::{
CANCELMODE, CANCELS, CAPTURE_LOST, MOVED, PRESS, RealWindow, Rig, client_lparam,
spawn_drag_window,
};
use super::{WndState, make_wndproc};
use crate::ecs::drag::{
DragConfig, DragEndEvent, DragStartEvent, DragStateSnapshot, snapshot_drag_state,
start_preparing,
};
use crate::ecs::pointer::PhysicalPoint;
const AT: PhysicalPoint = PhysicalPoint { x: 400, y: 300 };
const OTHER_AT: PhysicalPoint = PhysicalPoint { x: 800, y: 300 };
const RELEASE: PhysicalPoint = PhysicalPoint { x: 150, y: 170 };
fn capture_owner() -> HWND {
unsafe { GetCapture() }
}
fn rests_released() -> bool {
matches!(
snapshot_drag_state(),
DragStateSnapshot::JustEnded {
cancelled: false,
..
}
)
}
fn release_after_threshold(borrowed: bool) -> (PhysicalPoint, Vec<DragEndEvent>, Entity) {
let rig = Rig::with_real_window(AT);
rig.start_drag();
if borrowed {
rig.send_while_borrowed(WM_LBUTTONUP, 0, client_lparam(RELEASE));
} else {
rig.send(WM_LBUTTONUP, 0, client_lparam(RELEASE));
}
(rig.screen(RELEASE), rig.dispatch_ends(), rig.target)
}
#[test]
fn t02_reentrant_release_after_threshold_ends_once_at_the_same_screen_point() {
let (screen, ordinary, target) = release_after_threshold(false);
assert_eq!(
ordinary.len(),
1,
"借用なしの道: 終了の知らせ 1 件: {ordinary:?}"
);
assert_eq!(ordinary[0].target, target, "借用なしの道: 対象");
assert!(!ordinary[0].cancelled, "借用なしの道: 印なし");
assert_eq!(
ordinary[0].position, screen,
"借用なしの道: 位置は離した点の画面の座標"
);
assert_ne!(
screen, RELEASE,
"窓の原点が画面の原点でない(窓の中の座標との取り違えを見分けられる)"
);
let (_, reentrant, target) = release_after_threshold(true);
assert_eq!(
reentrant.len(),
1,
"再入の離しでも終了の知らせは 1 件: {reentrant:?}"
);
assert_eq!(reentrant[0].target, target, "対象");
assert!(!reentrant[0].cancelled, "印なし");
assert_eq!(
reentrant[0].position, ordinary[0].position,
"位置は借用なしで渡した場合と同じ画面の座標"
);
}
fn release_while_held_rests_and_frees_capture(cross_threshold: bool) {
let rig = Rig::with_real_window(AT);
rig.press();
if cross_threshold {
rig.cross_threshold();
}
assert_eq!(capture_owner(), rig.hwnd, "押しの後は捕捉がその窓");
rig.send_while_borrowed(WM_LBUTTONUP, 0, client_lparam(PRESS));
assert!(
rests_released(),
"再入の離しで状態は印なしで休む: {:?}",
snapshot_drag_state()
);
assert_ne!(capture_owner(), rig.hwnd, "再入の離しで捕捉が解放される");
}
#[test]
fn t05_reentrant_release_before_threshold_rests_and_frees_capture() {
release_while_held_rests_and_frees_capture(false);
}
#[test]
fn t05_reentrant_release_just_after_threshold_rests_and_frees_capture() {
release_while_held_rests_and_frees_capture(true);
}
fn moving_without_press_starts_nothing(rig: &Rig, what: &str) {
rig.send(WM_MOUSEMOVE, 0, client_lparam(MOVED));
let starts = rig.dispatch::<DragStartEvent>();
assert_eq!(
starts.len(),
0,
"{what}: 押さずに動かしても開始の知らせは 0 件: {starts:?}"
);
assert!(
!snapshot_drag_state().is_button_held(),
"{what}: 状態は休んだまま: {:?}",
snapshot_drag_state()
);
}
#[test]
fn t06_no_start_without_press_after_reentrant_cancel_before_threshold() {
for kind in CANCELS {
let rig = Rig::new();
rig.press();
rig.send_while_borrowed(kind.msg, kind.wparam, 0);
moving_without_press_starts_nothing(&rig, kind.what);
}
}
#[test]
fn t06_no_start_without_press_after_reentrant_release_before_threshold() {
let rig = Rig::with_real_window(AT);
rig.press();
rig.send_while_borrowed(WM_LBUTTONUP, 0, client_lparam(PRESS));
moving_without_press_starts_nothing(&rig, "左ボタンを離す(WM_LBUTTONUP)");
}
#[derive(Clone, Copy, Debug)]
enum ReentrantEnd {
Cancel,
Release,
}
impl ReentrantEnd {
fn rig(self) -> Rig {
match self {
Self::Cancel => Rig::new(),
Self::Release => Rig::with_real_window(AT),
}
}
fn apply(self, rig: &Rig) {
match self {
Self::Cancel => rig.send_while_borrowed(CANCELMODE.msg, CANCELMODE.wparam, 0),
Self::Release => rig.send_while_borrowed(WM_LBUTTONUP, 0, client_lparam(RELEASE)),
};
rig.dispatch_ends();
}
}
fn click_without_move_after_reentrant_end_sends_no_end(end: ReentrantEnd) {
let rig = end.rig();
rig.start_drag();
end.apply(&rig);
start_preparing(rig.target, rig.screen(PRESS), rig.hwnd);
rig.send(WM_LBUTTONUP, 0, client_lparam(PRESS));
let ends = rig.dispatch_ends();
assert_eq!(
ends.len(),
0,
"{end:?} の後の動かさないクリックで終了の知らせは 0 件: {ends:?}"
);
}
#[test]
fn t09_click_without_move_after_reentrant_cancel_sends_no_end() {
click_without_move_after_reentrant_end_sends_no_end(ReentrantEnd::Cancel);
}
#[test]
fn t09_click_without_move_after_reentrant_release_sends_no_end() {
click_without_move_after_reentrant_end_sends_no_end(ReentrantEnd::Release);
}
fn next_drag_after_reentrant_end_is_reported_once_each(end: ReentrantEnd) {
let rig = end.rig();
rig.start_drag();
end.apply(&rig);
let next = rig
.world
.borrow_mut()
.world_mut()
.spawn((
DragConfig {
move_window: false,
..Default::default()
},
ChildOf(rig.target),
))
.id();
start_preparing(next, rig.screen(PRESS), rig.hwnd);
rig.send(WM_MOUSEMOVE, 0, client_lparam(MOVED));
let starts: Vec<Entity> = rig
.dispatch::<DragStartEvent>()
.iter()
.map(|s| s.target)
.collect();
assert_eq!(
starts,
vec![next],
"{end:?} の後の次のドラッグ: 開始の知らせは新しい対象について 1 件"
);
rig.send(WM_LBUTTONUP, 0, client_lparam(RELEASE));
let ends = rig.dispatch_ends();
assert_eq!(
ends.iter().map(|e| e.target).collect::<Vec<_>>(),
vec![next],
"{end:?} の後の次のドラッグ: 終了の知らせは新しい対象について 1 件: {ends:?}"
);
assert!(!ends[0].cancelled, "{end:?} の後の次のドラッグ: 印なし");
assert_eq!(
ends[0].position,
rig.screen(RELEASE),
"{end:?} の後の次のドラッグ: 位置は離した点の画面の座標"
);
}
#[test]
fn t10_next_drag_after_reentrant_cancel_starts_and_ends_once_for_the_new_target() {
next_drag_after_reentrant_end_is_reported_once_each(ReentrantEnd::Cancel);
}
#[test]
fn t10_next_drag_after_reentrant_release_starts_and_ends_once_for_the_new_target() {
next_drag_after_reentrant_end_is_reported_once_each(ReentrantEnd::Release);
}
#[test]
fn t11_release_on_another_window_does_not_end_with_or_without_borrow() {
for dragging in [false, true] {
for borrowed in [false, true] {
let case = format!(
"{}・{}",
if dragging {
"ドラッグ中"
} else {
"準備中"
},
if borrowed {
"借用中"
} else {
"借用なし"
}
);
let rig = Rig::with_real_window(AT);
let other_entity = spawn_drag_window(&rig.world, OTHER_AT);
let other = RealWindow::new(&rig.world, other_entity, OTHER_AT);
let other_state = Box::pin(WndState {
world: Rc::downgrade(&rig.world),
entity: other_entity,
});
assert_ne!(other.hwnd(), rig.hwnd, "{case}: 窓は 2 枚");
if dragging {
rig.start_drag();
} else {
rig.press();
}
let before = format!("{:?}", snapshot_drag_state());
{
let _held = borrowed.then(|| rig.world.borrow_mut());
rig.send_as(
other_state.as_ref(),
other.hwnd(),
WM_LBUTTONUP,
0,
client_lparam(RELEASE),
);
}
assert_eq!(
format!("{:?}", snapshot_drag_state()),
before,
"{case}: 違う窓の離しでは状態は変わらない"
);
assert_eq!(capture_owner(), rig.hwnd, "{case}: 捕捉は取った窓のまま");
let ends = rig.dispatch_ends();
assert_eq!(ends.len(), 0, "{case}: 終了の知らせは 0 件: {ends:?}");
}
}
}
fn lines<'a>(
events: &'a [log_capture_kit::CapturedEvent],
name: &str,
) -> Vec<&'a log_capture_kit::CapturedEvent> {
events
.iter()
.filter(|e| e.field_str("event") == Some(name))
.collect()
}
fn text<'a>(e: &'a log_capture_kit::CapturedEvent, name: &str) -> Option<&'a str> {
e.field_str(name).or_else(|| e.field(name))
}
fn assert_handled_line(
line: &log_capture_kit::CapturedEvent,
action: &str,
level: tracing::Level,
entity: Option<Entity>,
window: Entity,
) {
assert_eq!(line.field_str("action"), Some(action), "action: {line:?}");
assert_eq!(line.level, level, "{action} の行の level: {line:?}");
assert!(
text(line, "msg").is_some_and(|m| !m.is_empty()),
"{action} の行にメッセージの種類(msg)がある: {line:?}"
);
match entity {
Some(entity) => assert_eq!(
text(line, "entity"),
Some(format!("{entity:?}").as_str()),
"{action} の行のドラッグの対象(entity): {line:?}"
),
None => assert!(
text(line, "entity").is_some(),
"{action} の行に entity の欄がある: {line:?}"
),
}
assert_eq!(
text(line, "window"),
Some(format!("{window:?}").as_str()),
"{action} の行の受けた窓(window): {line:?}"
);
}
#[test]
fn t12_reentrant_handling_logs_exactly_one_line() {
let rig = Rig::new();
rig.start_drag();
let (_, events) =
log_capture_kit::capture(|| rig.send_while_borrowed(CANCELMODE.msg, CANCELMODE.wparam, 0));
let handled = lines(&events, "drag_reentry_handled");
assert_eq!(
handled.len(),
1,
"取り消したとき drag_reentry_handled は 1 行: {handled:?}"
);
assert_handled_line(
handled[0],
"cancelled",
tracing::Level::WARN,
Some(rig.target),
rig.target,
);
let rig = Rig::new();
let (_, events) = log_capture_kit::capture(|| {
rig.send_while_borrowed(CAPTURE_LOST.msg, CAPTURE_LOST.wparam, 0)
});
let handled = lines(&events, "drag_reentry_handled");
assert_eq!(
handled.len(),
1,
"何もしなかったときも drag_reentry_handled は 1 行: {handled:?}"
);
assert_handled_line(handled[0], "none", tracing::Level::DEBUG, None, rig.target);
}
#[test]
fn t12_reentrant_release_on_an_empty_window_logs_pos_unreadable() {
let rig = Rig::new();
rig.start_drag();
let (_, events) = log_capture_kit::capture(|| {
rig.send_while_borrowed(WM_LBUTTONUP, 0, client_lparam(RELEASE))
});
let unreadable = lines(&events, "drag_reentry_pos_unreadable");
assert_eq!(
unreadable.len(),
1,
"位置を読めなかった記録は 1 行: {unreadable:?}"
);
assert_eq!(
unreadable[0].level,
tracing::Level::WARN,
"読めなかった情報は warn!"
);
let handled = lines(&events, "drag_reentry_handled");
assert_eq!(handled.len(), 1, "扱いの記録も 1 行: {handled:?}");
assert_handled_line(
handled[0],
"ended",
tracing::Level::WARN,
Some(rig.target),
rig.target,
);
}
#[test]
fn t14_start_preparing_takes_capture_without_falling_over_on_capture_changed() {
let rig = Rig::with_real_window(AT);
let seen = Rc::new(Cell::new(0u32));
let fell_over = Rc::new(Cell::new(false));
let holder_entity = rig.world.borrow_mut().world_mut().spawn(()).id();
let holder = RealWindow::with_wndproc(&rig.world, holder_entity, OTHER_AT, {
let (seen, fell_over) = (seen.clone(), fell_over.clone());
let entrance = make_wndproc();
move |state, msg| {
if msg.msg == WM_CAPTURECHANGED {
seen.set(seen.get() + 1);
}
catch_unwind(AssertUnwindSafe(|| entrance(state, msg))).unwrap_or_else(|_| {
fell_over.set(true);
None
})
}
});
unsafe { SetCapture(holder.hwnd()) };
assert_eq!(
capture_owner(),
holder.hwnd(),
"先に捕捉を持つ窓が捕捉している"
);
{
let _held = rig.world.borrow_mut();
start_preparing(rig.target, rig.screen(PRESS), rig.hwnd);
}
assert_eq!(
seen.get(),
1,
"SetCapture が前の持ち主へ WM_CAPTURECHANGED を同期で送り、入口が受けた"
);
assert!(
!fell_over.get(),
"捕捉を取る間に届いた WM_CAPTURECHANGED を入口が受けても落ちない"
);
assert!(
matches!(snapshot_drag_state(), DragStateSnapshot::Preparing { .. }),
"押しの後は準備中: {:?}",
snapshot_drag_state()
);
assert_eq!(capture_owner(), rig.hwnd, "押しの後は捕捉がその窓");
}