wintf 0.0.2

Windows Tategaki Framework - Rust UI library with Japanese vertical text support
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
//! マウス移動・ヒットテスト・離脱ハンドラ
//!
//! WM_NCHITTEST, WM_MOUSEMOVE, WM_MOUSELEAVE

#![allow(non_snake_case)]

use std::cell::RefCell;
use std::rc::Rc;

use bevy_ecs::prelude::Entity;
use tracing::{debug, trace};
use windows::Win32::Foundation::*;
use windows::Win32::UI::WindowsAndMessaging::*;

use crate::ecs::world::EcsWorld;

/// メッセージハンドラの戻り値型
type HandlerResult = Option<LRESULT>;

/// WM_SETCURSOR: 診断用の十字カーソル(環境変数 `WINTF_CROSSHAIR=1` のときのみ有効)。
///
/// クライアント領域(`HTCLIENT`)上でカーソルを `IDC_CROSS` に差し替え、`Some(LRESULT(1))` を
/// 返して既定処理を止める。クリック透過中(`WS_EX_TRANSPARENT`)の窓は WM_SETCURSOR を受け取ら
/// ない(入力は背面プロセスへ抜ける)ため、**十字が出る領域=この窓が反応する(不透過)領域**=
/// クリック透過の当たり判定域の可視化になる(表示と判定のズレ確認に使う)。
///
/// 環境変数未設定時・非クライアント時は `None`(`DefWindowProcW` へ委譲)を返し、通常の挙動を
/// **一切変えない**(既定 OFF の純粋な診断アドオン)。フラグはプロセス起動時に一度だけ評価する。
#[inline]
pub(super) fn WM_SETCURSOR(
    _world: &Rc<RefCell<EcsWorld>>,
    _entity: Entity,
    _hwnd: HWND,
    _wparam: WPARAM,
    lparam: LPARAM,
) -> HandlerResult {
    use std::sync::OnceLock;
    static ENABLED: OnceLock<bool> = OnceLock::new();
    if !*ENABLED.get_or_init(|| std::env::var_os("WINTF_CROSSHAIR").is_some()) {
        return None;
    }
    // LOWORD(lparam) = ヒットテスト結果。HTCLIENT 上のみ十字化(枠・スクロールバー等は既定)。
    let hit_test = (lparam.0 & 0xFFFF) as i32;
    if hit_test != HTCLIENT as i32 {
        return None;
    }
    // SAFETY: Win32 境界。LoadCursorW(IDC_CROSS)→SetCursor。所有権・寿命に影響しない。
    unsafe {
        if let Ok(cursor) = LoadCursorW(None, IDC_CROSS) {
            let _ = SetCursor(Some(cursor));
        }
    }
    Some(LRESULT(1))
}

/// WM_NCHITTEST: 非クライアント領域ヒットテスト
///
/// クライアント領域判定を実装し、キャッシュ付きhit_testを呼び出す。
/// HTCLIENT / HTTRANSPARENT を返す(クリックスルー対応)。
#[inline]
pub(super) fn WM_NCHITTEST(
    world: &Rc<RefCell<EcsWorld>>,
    entity: Entity,
    hwnd: HWND,
    _wparam: WPARAM,
    lparam: LPARAM,
) -> HandlerResult {
    use windows::Win32::UI::WindowsAndMessaging::GetClientRect;

    // スクリーン座標を取得(lparam から)
    let x = (lparam.0 & 0xFFFF) as i16 as i32;
    let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32;

    // クライアント領域判定用に座標変換(クライアント領域外はDefWindowProcWに委譲)
    unsafe {
        use windows::Win32::Graphics::Gdi::ScreenToClient;
        let mut pt = POINT { x, y };
        if !ScreenToClient(hwnd, &mut pt).as_bool() {
            return None; // DefWindowProcW に委譲
        }

        // クライアント領域外の場合は DefWindowProcW に委譲
        let mut rect = RECT::default();
        if GetClientRect(hwnd, &mut rect).is_err() {
            return None;
        }
        if pt.x < rect.left || pt.x >= rect.right || pt.y < rect.top || pt.y >= rect.bottom {
            return None; // DefWindowProcW に委譲(非クライアント領域処理)
        }
    }

    // キャッシュ付きヒットテスト実行
    crate::ecs::pointer::nchittest_cache::cached_nchittest(hwnd, (x, y), entity, world)
}

/// 当該ウィンドウに属し、exclude と異なるエンティティの PointerState 保持者を収集する。
///
/// WM_MOUSEMOVE の leave 処理で使用するヘルパー関数。
/// `find_owner_window` でウィンドウスコーピングを行い、他ウィンドウの PointerState を保護する。
pub(super) fn collect_entities_to_leave(
    world: &mut bevy_ecs::world::World,
    window_entity: bevy_ecs::prelude::Entity,
    exclude: bevy_ecs::prelude::Entity,
) -> Vec<bevy_ecs::prelude::Entity> {
    use crate::ecs::pointer::PointerState;
    use crate::ecs::window::find_owner_window;

    let mut result = Vec::new();
    let mut query = world.query::<(bevy_ecs::prelude::Entity, &PointerState)>();
    for (e, _) in query.iter(world) {
        if e != exclude && find_owner_window(world, e) == Some(window_entity) {
            result.push(e);
        }
    }
    result
}

/// WM_MOUSEMOVE: マウス移動メッセージ
///
/// 位置をPointerBufferに蓄積し、hit_testでヒットしたエンティティにPointerStateを付与。
/// 初回移動時にTrackMouseEventを設定。
#[inline]
pub(super) fn WM_MOUSEMOVE(
    world: &Rc<RefCell<EcsWorld>>,
    window_entity: Entity,
    hwnd: HWND,
    wparam: WPARAM,
    lparam: LPARAM,
) -> HandlerResult {
    use crate::ecs::layout::hit_test::{PhysicalPoint as HitTestPoint, hit_test_in_window};
    use crate::ecs::pointer::{
        PointerLeave, PointerState, WindowPointerTracking, push_pointer_sample, set_modifier_state,
    };
    use std::time::Instant;
    use windows::Win32::UI::Input::KeyboardAndMouse::{
        TME_LEAVE, TRACKMOUSEEVENT, TrackMouseEvent,
    };

    // 位置取得(物理ピクセル、クライアント座標)
    let x = (lparam.0 & 0xFFFF) as i16 as i32;
    let y = ((lparam.0 >> 16) & 0xFFFF) as i16 as i32;

    tracing::trace!("[WM_MOUSEMOVE] Received: x={}, y={}", x, y);

    // 修飾キー状態を抽出
    let wparam_val = wparam.0 as u32;
    let shift = (wparam_val & 0x04) != 0; // MK_SHIFT
    let ctrl = (wparam_val & 0x08) != 0; // MK_CONTROL

    // World借用してhit_testとPointerState管理
    // ドラッグ中のSetWindowPosはWorld借用外で実行する(WM_WINDOWPOSCHANGED同期発火対策)
    let mut deferred_set_window_pos: Option<(HWND, i32, i32)> = None;

    {
        if let Ok(mut world_borrow) = world.try_borrow_mut() {
            // TrackMouseEvent 設定(ウィンドウに対して)
            if let Ok(mut entity_ref) = world_borrow.world_mut().get_entity_mut(window_entity) {
                let needs_tracking = entity_ref
                    .get::<WindowPointerTracking>()
                    .is_none_or(|t| !t.0);

                if needs_tracking {
                    let mut tme = TRACKMOUSEEVENT {
                        cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
                        dwFlags: TME_LEAVE,
                        hwndTrack: hwnd,
                        dwHoverTime: 0,
                    };
                    let _ = unsafe { TrackMouseEvent(&mut tme) };

                    if entity_ref.get::<WindowPointerTracking>().is_some() {
                        if let Some(mut tracking) = entity_ref.get_mut::<WindowPointerTracking>() {
                            tracking.0 = true;
                        }
                    } else {
                        drop(entity_ref);
                        world_borrow
                            .world_mut()
                            .entity_mut(window_entity)
                            .insert(WindowPointerTracking(true));
                    }

                    trace!(
                        entity = ?window_entity,
                        hwnd = ?hwnd,
                        "TrackMouseEvent(TME_LEAVE) set"
                    );
                }
            }

            // hit_test でヒットしたエンティティを取得
            let hit_entity = hit_test_in_window(
                world_borrow.world(),
                window_entity,
                HitTestPoint::new(x as f32, y as f32),
            );

            // WindowPosを取得してスクリーン座標を計算
            let window_pos = world_borrow
                .world()
                .get::<crate::ecs::window::WindowPos>(window_entity);
            let (screen_x, screen_y) = if let Some(wp) = window_pos {
                if let Some(pos) = wp.position {
                    (x + pos.x, y + pos.y)
                } else {
                    (x, y)
                }
            } else {
                (x, y)
            };

            // ドラッグ処理(thread_local DragState + DragAccumulatorResource)
            // ヒットテスト結果に関わらず、Preparing/Dragging状態なら処理を続ける
            let state_snapshot = crate::ecs::drag::snapshot_drag_state();

            match state_snapshot {
                crate::ecs::drag::DragStateSnapshot::Preparing {
                    entity,
                    start_pos,
                    start_time,
                } => {
                    if let Some(drag_config) = world_borrow
                        .world()
                        .get::<crate::ecs::drag::DragConfig>(entity)
                    {
                        if drag_config.enabled {
                            let current_pos =
                                crate::ecs::pointer::PhysicalPoint::new(screen_x, screen_y);

                            // 閾値チェック
                            let dx = current_pos.x - start_pos.x;
                            let dy = current_pos.y - start_pos.y;
                            let distance_sq = dx * dx + dy * dy;
                            let threshold_sq = drag_config.threshold * drag_config.threshold;

                            if distance_sq >= threshold_sq {
                                // 閾値到達:Dragging状態に遷移
                                crate::ecs::drag::start_dragging(current_pos);

                                // DragAccumulatorResourceにStarted遷移を記録
                                if let Some(accumulator) = world_borrow
                                    .world()
                                    .get_resource::<crate::ecs::drag::DragAccumulatorResource>(
                                ) {
                                    accumulator.set_transition(
                                        crate::ecs::drag::DragTransition::Started {
                                            entity,
                                            start_pos,
                                            timestamp: start_time,
                                        },
                                    );
                                    accumulator.update_position(current_pos);
                                }
                            }
                        }
                    }
                }
                crate::ecs::drag::DragStateSnapshot::Dragging {
                    prev_pos,
                    start_pos,
                    hwnd: drag_hwnd,
                    initial_window_pos,
                    move_window,
                    constraint,
                    ..
                } => {
                    let current_pos = crate::ecs::pointer::PhysicalPoint::new(screen_x, screen_y);

                    // ドラッグ中:デルタを累積
                    let delta = crate::ecs::pointer::PhysicalPoint::new(
                        current_pos.x - prev_pos.x,
                        current_pos.y - prev_pos.y,
                    );

                    tracing::trace!(
                        "[WM_MOUSEMOVE] Dragging: delta=({}, {}), current=({}, {})",
                        delta.x,
                        delta.y,
                        current_pos.x,
                        current_pos.y
                    );

                    // DragAccumulatorResourceにデルタを累積
                    if let Some(accumulator) = world_borrow
                        .world()
                        .get_resource::<crate::ecs::drag::DragAccumulatorResource>(
                    ) {
                        accumulator.accumulate_delta(delta);
                        accumulator.update_position(current_pos);
                    }

                    // thread_local DragStateのprev_posを更新
                    let ctx_res = world_borrow
                        .world()
                        .get_resource::<crate::ecs::drag::WindowDragContextResource>();
                    crate::ecs::drag::update_dragging(current_pos, ctx_res);

                    // WndProcレベル直接SetWindowPos(ECSパイプラインバイパス)
                    // NOTE: SetWindowPosはWM_WINDOWPOSCHANGEDを同期発火し、そこで
                    // World borrowが必要になるため、借用スコープ外に遅延実行する
                    if move_window {
                        let mut new_x = initial_window_pos.x + (current_pos.x - start_pos.x);
                        let mut new_y = initial_window_pos.y + (current_pos.y - start_pos.y);

                        // DragConstraint 適用
                        if let Some(ref c) = constraint {
                            let (cx, cy) = c.apply(new_x, new_y);
                            new_x = cx;
                            new_y = cy;
                        }

                        deferred_set_window_pos = Some((drag_hwnd, new_x, new_y));
                    }
                }
                _ => {}
            }

            // ヒットしたエンティティが存在する場合
            if let Some(target_entity) = hit_entity {
                // 当該ウィンドウに属し、target_entity と異なるエンティティを Leave 対象として収集
                let entities_to_leave = collect_entities_to_leave(
                    world_borrow.world_mut(),
                    window_entity,
                    target_entity,
                );

                // 古いエンティティからPointerStateを削除し、PointerLeaveを付与
                for old_entity in entities_to_leave {
                    if let Ok(mut entity_ref) = world_borrow.world_mut().get_entity_mut(old_entity)
                    {
                        entity_ref.remove::<PointerState>();
                        entity_ref.insert(PointerLeave);
                        debug!(
                            old_entity = ?old_entity,
                            new_entity = ?target_entity,
                            "PointerState moved, Leave marker inserted"
                        );
                    }
                }

                // 新しいエンティティにPointerStateを挿入または維持
                if let Ok(entity_ref) = world_borrow.world_mut().get_entity_mut(target_entity) {
                    let needs_insert = entity_ref.get::<PointerState>().is_none();
                    tracing::trace!(
                        entity = ?target_entity,
                        needs_insert,
                        "[WM_MOUSEMOVE] PointerState check"
                    );
                    drop(entity_ref);

                    if needs_insert {
                        world_borrow
                            .world_mut()
                            .entity_mut(target_entity)
                            .insert(PointerState {
                                client_point: crate::ecs::pointer::PhysicalPoint::new(x, y),
                                local_point: crate::ecs::pointer::PhysicalPoint::new(x, y),
                                shift_down: shift,
                                ctrl_down: ctrl,
                                ..Default::default()
                            });
                        debug!(
                            entity = ?target_entity,
                            x, y,
                            "PointerState inserted (Enter)"
                        );
                    }
                } else {
                    tracing::warn!(
                        entity = ?target_entity,
                        "[WM_MOUSEMOVE] Failed to get entity_mut"
                    );
                }

                // PointerStateが存在することを保証した上で、バッファに蓄積
                push_pointer_sample(target_entity, x as f32, y as f32, Instant::now());
                set_modifier_state(target_entity, shift, ctrl);
            } else {
                // hit_test失敗(Windowの空白部分)
                // Windowエンティティに対して処理
                let target_entity = window_entity;

                // 当該ウィンドウに属し、target_entity と異なるエンティティを Leave 対象として収集
                let entities_to_leave = collect_entities_to_leave(
                    world_borrow.world_mut(),
                    window_entity,
                    target_entity,
                );

                // 古いエンティティからPointerStateを削除し、PointerLeaveを付与
                for old_entity in entities_to_leave {
                    if let Ok(mut entity_ref) = world_borrow.world_mut().get_entity_mut(old_entity)
                    {
                        entity_ref.remove::<PointerState>();
                        entity_ref.insert(PointerLeave);
                        debug!(
                            old_entity = ?old_entity,
                            new_entity = ?target_entity,
                            "PointerState moved to Window, Leave marker inserted"
                        );
                    }
                }

                // PointerStateを挿入または維持
                if let Ok(entity_ref) = world_borrow.world_mut().get_entity_mut(target_entity) {
                    let needs_insert = entity_ref.get::<PointerState>().is_none();
                    drop(entity_ref);

                    if needs_insert {
                        world_borrow
                            .world_mut()
                            .entity_mut(target_entity)
                            .insert(PointerState {
                                client_point: crate::ecs::pointer::PhysicalPoint::new(x, y),
                                local_point: crate::ecs::pointer::PhysicalPoint::new(x, y),
                                shift_down: shift,
                                ctrl_down: ctrl,
                                ..Default::default()
                            });
                        debug!(
                            entity = ?target_entity,
                            x, y,
                            "PointerState inserted on Window (no hit)"
                        );
                    }
                }

                // PointerStateが存在することを保証した上で、バッファに蓄積
                push_pointer_sample(target_entity, x as f32, y as f32, Instant::now());
                set_modifier_state(target_entity, shift, ctrl);
            }
        }
    }

    // World借用解放後にSetWindowPosを実行
    // これにより同期発火するWM_WINDOWPOSCHANGEDがWorldを借用でき、
    // WindowPos.positionが正しく更新される
    if let Some((drag_hwnd, new_x, new_y)) = deferred_set_window_pos {
        let _ = unsafe {
            crate::ecs::window::guarded_set_window_pos(
                drag_hwnd,
                None,
                new_x,
                new_y,
                0,
                0,
                SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE,
            )
        };
    }

    Some(LRESULT(0))
}

/// WM_MOUSELEAVE: マウス離脱メッセージ
///
/// 当該ウィンドウに属するエンティティのPointerStateのみを削除し、
/// PointerLeaveマーカーを付与する(他ウィンドウのPointerStateは保持)。
#[inline]
pub(super) fn WM_MOUSELEAVE(
    world: &Rc<RefCell<EcsWorld>>,
    window_entity: Entity,
    hwnd: HWND,
    _wparam: WPARAM,
    _lparam: LPARAM,
) -> HandlerResult {
    use crate::ecs::pointer::{PointerLeave, PointerState, WindowPointerTracking};
    use crate::ecs::window::find_owner_window;

    {
        if let Ok(mut world_borrow) = world.try_borrow_mut() {
            // PointerStateを持つ全エンティティを収集(当該ウィンドウに属するもののみ)
            let mut entities_with_pointer_state = Vec::new();
            {
                let world_mut = world_borrow.world_mut();
                let mut query = world_mut.query::<(bevy_ecs::prelude::Entity, &PointerState)>();
                for (e, _) in query.iter(world_mut) {
                    if find_owner_window(world_mut, e) == Some(window_entity) {
                        entities_with_pointer_state.push(e);
                    }
                }
            }

            // 当該ウィンドウのエンティティからPointerStateを削除し、PointerLeaveを付与
            for entity in entities_with_pointer_state {
                if let Ok(mut entity_ref) = world_borrow.world_mut().get_entity_mut(entity) {
                    entity_ref.remove::<PointerState>();
                    entity_ref.insert(PointerLeave);
                    debug!(
                        entity = ?entity,
                        hwnd = ?hwnd,
                        "PointerLeave marker inserted (scoped to window)"
                    );
                }
            }

            // WindowPointerTrackingを無効化
            if let Ok(mut entity_ref) = world_borrow.world_mut().get_entity_mut(window_entity) {
                if let Some(mut tracking) = entity_ref.get_mut::<WindowPointerTracking>() {
                    tracking.0 = false;
                }
            }
        }
    }

    Some(LRESULT(0))
}

#[cfg(test)]
mod tests {
    use super::collect_entities_to_leave;
    use crate::ecs::pointer::PointerState;
    use crate::ecs::window::Window;
    use bevy_ecs::hierarchy::ChildOf;
    use bevy_ecs::prelude::*;

    // collect_entities_to_leave は World ベース(query + find_owner_window)でデバイス非依存。
    // 当該ウィンドウ所属 / exclude 除外 / 他ウィンドウ保護 / PointerState 不在スキップ を特性化する。

    fn spawn_window(world: &mut World, title: &str) -> Entity {
        world
            .spawn(Window {
                title: title.to_string(),
                ..Default::default()
            })
            .id()
    }

    #[test]
    fn test_collect_excludes_target_and_collects_siblings() {
        // 同一ウィンドウ配下の PointerState 保持者のうち、exclude 以外を収集する
        let mut world = World::new();
        let window = spawn_window(&mut world, "W");
        let target = world.spawn((ChildOf(window), PointerState::default())).id();
        let other = world.spawn((ChildOf(window), PointerState::default())).id();

        let result = collect_entities_to_leave(&mut world, window, target);

        // other は収集される。target(exclude)は含まれない。
        assert!(result.contains(&other), "exclude 以外の保持者を収集");
        assert!(!result.contains(&target), "exclude 対象は除外");
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn test_collect_does_not_include_entities_without_pointer_state() {
        // PointerState を持たないエンティティは収集対象外
        let mut world = World::new();
        let window = spawn_window(&mut world, "W");
        let target = world.spawn((ChildOf(window), PointerState::default())).id();
        // PointerState なしの子(収集されないはず)
        let _no_state = world.spawn(ChildOf(window)).id();

        let result = collect_entities_to_leave(&mut world, window, target);
        assert!(
            result.is_empty(),
            "PointerState 不在エンティティは収集されない"
        );
    }

    #[test]
    fn test_collect_protects_other_windows_pointer_state() {
        // 別ウィンドウ配下の PointerState 保持者は収集しない(ウィンドウスコーピング)
        let mut world = World::new();
        let window_a = spawn_window(&mut world, "A");
        let window_b = spawn_window(&mut world, "B");

        let target_a = world
            .spawn((ChildOf(window_a), PointerState::default()))
            .id();
        let widget_a = world
            .spawn((ChildOf(window_a), PointerState::default()))
            .id();
        // 別ウィンドウ B 配下の保持者
        let widget_b = world
            .spawn((ChildOf(window_b), PointerState::default()))
            .id();

        let result = collect_entities_to_leave(&mut world, window_a, target_a);

        assert!(
            result.contains(&widget_a),
            "同一ウィンドウ A の保持者は収集"
        );
        assert!(
            !result.contains(&widget_b),
            "別ウィンドウ B の PointerState は保護(非収集)"
        );
        assert_eq!(result.len(), 1);
    }

    #[test]
    fn test_collect_returns_empty_when_only_excluded_holder_exists() {
        // PointerState 保持者が exclude 1件のみ → 空
        let mut world = World::new();
        let window = spawn_window(&mut world, "W");
        let target = world.spawn((ChildOf(window), PointerState::default())).id();

        let result = collect_entities_to_leave(&mut world, window, target);
        assert!(result.is_empty());
    }
}