wintf 0.0.3

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
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
//! # Hit Test System - ヒットテストAPI
//!
//! このモジュールは、画面座標からエンティティを特定するヒットテストシステムを提供します。
//!
//! ## 主要機能
//!
//! ### HitTestMode / HitTest コンポーネント
//! エンティティのヒットテスト動作を設定します。
//!
//! ### hit_test_entity
//! 単一エンティティのヒットテストを実行します。
//!
//! ### hit_test
//! ルート配下を走査してスクリーン座標でヒットテストを実行します。
//!
//! ### hit_test_in_window
//! ウィンドウクライアント座標でヒットテストを実行します。
//!
//! # 例
//!
//! ```rust,ignore
//! use wintf::ecs::layout::{hit_test, HitTest, HitTestMode};
//!
//! // ヒットテスト対象外として設定
//! commands.spawn((Arrangement::default(), HitTest::none()));
//!
//! // ルートからヒットテスト実行
//! if let Some(entity) = hit_test(world, root, PhysicalPoint::new(100.0, 200.0)) {
//!     println!("Hit: {:?}", entity);
//! }
//! ```

use std::sync::Arc;

use bevy_ecs::prelude::*;
use tracing::warn;

use super::hit_region::HitRegionMap;
use super::{Arrangement, D2DRect, D2DRectExt, GlobalArrangement};
use crate::ecs::WindowPos;
use crate::ecs::common::DepthFirstReversePostOrder;
use crate::ecs::types::PointF;
use crate::ecs::widget::bitmap_source::AlphaMask;

// ============================================================================
// PhysicalPoint — PointF への後方互換エイリアス
// ============================================================================

/// 後方互換性エイリアス(PhysicalPoint → PointF)
pub type PhysicalPoint = PointF;

// ============================================================================
// HitTestMode - ヒットテストモード
// ============================================================================

/// ヒットテストの動作モード
///
/// エンティティがマウスイベントにどう反応するかを定義します。
///
/// # 例
/// ```rust,ignore
/// use wintf::ecs::layout::{HitTest, HitTestMode};
///
/// // ヒットテスト対象外
/// let hit_test = HitTest { mode: HitTestMode::None };
///
/// // 矩形領域でヒットテスト(デフォルト)
/// let hit_test = HitTest::bounds();
///
/// // αマスクによるピクセル単位ヒットテスト
/// let hit_test = HitTest::alpha_mask();
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HitTestMode {
    /// ヒットテスト対象外(マウスイベントを透過)
    None,
    /// バウンディングボックス(GlobalArrangement.bounds)でヒットテスト
    #[default]
    Bounds,
    /// αマスクによるピクセル単位ヒットテスト
    AlphaMask,
    /// 名前付きヒット領域(矩形・多角形・カラーマップ)による部位判定
    NamedRegions,
}

// ============================================================================
// HitTest コンポーネント
// ============================================================================

/// ヒットテスト設定コンポーネント
///
/// エンティティのヒットテスト動作を設定します。
/// このコンポーネントが付与されていない場合、デフォルトで `HitTestMode::Bounds` として扱われます。
///
/// # 例
/// ```rust,ignore
/// use wintf::ecs::layout::HitTest;
///
/// // ヒットテスト対象外として作成
/// commands.spawn((Arrangement::default(), HitTest::none()));
///
/// // 矩形領域でヒットテスト(デフォルト)
/// commands.spawn((Arrangement::default(), HitTest::bounds()));
/// ```
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct HitTest {
    pub mode: HitTestMode,
}

impl HitTest {
    /// ヒットテスト対象外として作成
    pub fn none() -> Self {
        Self {
            mode: HitTestMode::None,
        }
    }

    /// バウンディングボックスでヒットテスト(デフォルト)
    pub fn bounds() -> Self {
        Self {
            mode: HitTestMode::Bounds,
        }
    }

    /// αマスクによるピクセル単位ヒットテスト
    pub fn alpha_mask() -> Self {
        Self {
            mode: HitTestMode::AlphaMask,
        }
    }

    /// 名前付きヒット領域によるヒットテスト
    pub fn named_regions() -> Self {
        Self {
            mode: HitTestMode::NamedRegions,
        }
    }
}

// ============================================================================
// AlphaMaskResource - 汎用 αマスク CPU リソース
// ============================================================================

/// hit-test 用 αマスク CPU リソース(汎用供給口)
///
/// `BitmapSourceResource` 以外の供給者(例: emo-present の合成表示)が
/// `HitTestMode::AlphaMask` のヒットテストへ [`AlphaMask`] を渡すための
/// `Component`。`hit_test_entity` / `hit_test_entity_ex` の `AlphaMask` 分岐で
/// **最優先**に読まれ、不在時は従来どおり `BitmapSourceResource` → 矩形
/// フォールバックへ委ねる(既存挙動は完全後方互換)。
///
/// # 座標契約
/// マスクは合成の**原寸**(native px)で、`GlobalArrangement.bounds` は表示の
/// **物理寸**(原寸 × 拡大率 k)である。bounds 相対の比例写像がその ÷k を 1 度だけ与え、
/// 任意 DPI でクリック座標が一致する(design DPI 表示契約・R2.5)。
///
/// # 命名
/// wintf の CPU リソース命名規約(`XxxResource`・`BitmapSourceResource` 等)に準拠。
///
/// # 内部表現
/// マスクは `Arc<AlphaMask>` で保持する。供給側(合成結果の所有者)が同一マスクを
/// 参照カウント増だけで渡せるようにするためで([`set_shared`](Self::set_shared))、
/// 公開シグネチャと観測可能な挙動は値保持だった頃と同一である([`set`](Self::set) は
/// 値を受け取り `Arc::new` で包み、[`mask`](Self::mask) は `Option<&AlphaMask>` を返す)。
/// 読み手は `hit_test` 内の `alpha_mask_hit`(`World::get` の同期 pull)のみで
/// `Changed`/`Added` 依存はリポジトリ全体に存在しないため、内部表現の変更は
/// 変更検知の下流へも影響しない。
#[derive(Component, Debug, Clone, Default)]
pub struct AlphaMaskResource {
    mask: Option<Arc<AlphaMask>>,
}

impl AlphaMaskResource {
    /// 空(マスク未設定)の `AlphaMaskResource` を作成
    pub fn new() -> Self {
        Self { mask: None }
    }

    /// αマスクを設定(表示所有者が合成結果から更新)
    pub fn set(&mut self, mask: AlphaMask) {
        self.mask = Some(Arc::new(mask));
    }

    /// 共有αマスクを設定(供給は参照カウント増のみ・複製なし)
    ///
    /// [`set`](Self::set) と観測結果は同一だが、呼び出し側が既に `Arc` で持っている
    /// マスクをそのまま渡せる。表示バッファと当たり判定マスクを同一の確保で共有し、
    /// 毎コマ適用でのマスク複製を無くすための口(要件 3.1)。
    pub fn set_shared(&mut self, mask: Arc<AlphaMask>) {
        self.mask = Some(mask);
    }

    /// αマスクへの参照を取得(未設定なら `None`)
    pub fn mask(&self) -> Option<&AlphaMask> {
        self.mask.as_deref()
    }
}

// ============================================================================
// alpha_mask_hit - AlphaMask 読み取り + 座標変換 + 判定の共有ヘルパ
// ============================================================================

/// `HitTestMode::AlphaMask` 分岐の αマスク読み取り・座標変換・判定を
/// `hit_test_entity` と `hit_test_entity_ex` で共有する純粋ヘルパ。
///
/// # 読み取り優先順位
/// 1. [`AlphaMaskResource`](最優先・汎用供給口)
/// 2. `BitmapSourceResource::alpha_mask()`(既存経路・後方互換)
/// 3. どちらも供給しない/マスク未生成 → 矩形フォールバック(bounds 内は常にヒット)
///
/// # 前提
/// 呼び出し側で bounds 内判定(早期リターン)済みであること。本ヘルパは bounds 相対
/// 比例→マスク座標変換のみを行い、変換ロジックは従来と同一(emo-present では
/// bounds=物理寸・マスク=原寸ゆえ、この比例写像が ÷k を 1 度だけ与える=R2.5 の成立根拠)。
///
/// # Returns
/// - `true`: 当該座標のマスクピクセルがヒット(α ≧ 128)/フォールバック(供給者不在)
/// - `false`: 当該座標のマスクピクセルが透明(α < 128)
fn alpha_mask_hit(world: &World, entity: Entity, bounds: &D2DRect, point: PhysicalPoint) -> bool {
    use crate::ecs::widget::bitmap_source::BitmapSourceResource;

    // AlphaMaskResource を最優先で読み、無ければ BitmapSourceResource の αマスクへ委ねる。
    // どちらも供給しない場合は矩形フォールバック(従来挙動)。
    let mask = world
        .get::<AlphaMaskResource>(entity)
        .and_then(|r| r.mask())
        .or_else(|| {
            world
                .get::<BitmapSourceResource>(entity)
                .and_then(|r| r.alpha_mask())
        });

    let Some(alpha_mask) = mask else {
        // 供給者不在/マスク未生成 → 矩形判定にフォールバック
        return true;
    };

    // スクリーン座標 → マスク座標への変換(従来ロジックと同一)
    let bounds_width = bounds.right - bounds.left;
    let bounds_height = bounds.bottom - bounds.top;

    if bounds_width <= 0.0 || bounds_height <= 0.0 {
        return true; // サイズが0以下の場合はフォールバック
    }

    // 相対座標を計算(0.0〜1.0)
    let rel_x = (point.x - bounds.left) / bounds_width;
    let rel_y = (point.y - bounds.top) / bounds_height;

    // マスク座標に変換(切り捨て、範囲チェックは is_hit 内で行う)
    let mask_x = (rel_x * alpha_mask.width() as f32) as u32;
    let mask_y = (rel_y * alpha_mask.height() as f32) as u32;

    // αマスクで判定
    alpha_mask.is_hit(mask_x, mask_y)
}

// ============================================================================
// hit_test_entity - 単一エンティティヒットテスト
// ============================================================================

/// 単一エンティティのヒットテスト
///
/// 指定エンティティのみを判定対象とします(子孫は走査しません)。
///
/// # Arguments
/// - `world`: ECS World 参照
/// - `entity`: 判定対象エンティティ
/// - `point`: スクリーン座標(物理ピクセル)
///
/// # Returns
/// - `true`: ヒット
/// - `false`: ヒットしない、または GlobalArrangement がない
///
/// # Note
/// `HitTest` コンポーネントがない場合は `HitTestMode::Bounds` として扱います。
///
/// # AlphaMask判定
/// `HitTestMode::AlphaMask` の場合:
/// 1. まず矩形判定(早期リターン)
/// 2. BitmapSourceResource.alpha_mask を取得
/// 3. 座標変換(スクリーン → マスク座標)
/// 4. AlphaMask.is_hit() 呼び出し
/// 5. αマスク未生成時は矩形判定にフォールバック
pub fn hit_test_entity(world: &World, entity: Entity, point: PhysicalPoint) -> bool {
    // HitTest コンポーネントを取得(なければデフォルト = Bounds)
    let mode = world
        .get::<HitTest>(entity)
        .map(|h| h.mode)
        .unwrap_or(HitTestMode::Bounds);

    // HitTestMode::None の場合はヒットしない
    if mode == HitTestMode::None {
        return false;
    }

    // GlobalArrangement を取得(なければヒットしない)
    let Some(global) = world.get::<GlobalArrangement>(entity) else {
        return false;
    };

    // まず矩形判定(全モード共通の早期リターン)
    if !global.bounds.contains(point.x, point.y) {
        return false;
    }

    // HitTestMode::Bounds の場合は矩形判定 + α値判定
    if mode == HitTestMode::Bounds {
        // Opacity * Brushes.foreground.a の合成α値が ALPHA_THRESHOLD 未満なら透明と判定
        // AlphaMask の ALPHA_THRESHOLD (128/255 ≈ 0.502) と同一基準
        const ALPHA_THRESHOLD: f32 = 128.0 / 255.0;

        let opacity = world
            .get::<crate::ecs::graphics::Visual>(entity)
            .map(|v| v.clamped_opacity())
            .unwrap_or(1.0);

        let foreground_a = world
            .get::<crate::ecs::widget::brushes::Brushes>(entity)
            .and_then(|b| b.foreground.as_color())
            .map(|c| c.a)
            .unwrap_or_else(|| {
                crate::ecs::widget::brushes::DEFAULT_FOREGROUND
                    .as_color()
                    .map(|c| c.a)
                    .unwrap_or(1.0)
            });

        let composite_alpha = opacity * foreground_a;
        if composite_alpha < ALPHA_THRESHOLD {
            return false;
        }

        return true;
    }

    // HitTestMode::AlphaMask の場合
    // 共有ヘルパで αマスクを読み取り(AlphaMaskResource 最優先 → BitmapSourceResource → 矩形)
    alpha_mask_hit(world, entity, &global.bounds, point)
}

// ============================================================================
// HitTestResult - 拡張ヒットテスト結果
// ============================================================================

/// 拡張ヒットテスト結果
///
/// ヒットしたエンティティと、名前付きリージョン(NamedRegions モード時)を含む。
#[derive(Debug, Clone)]
pub struct HitTestResult {
    /// ヒットしたエンティティ
    pub entity: Entity,
    /// ヒットした領域名(`NamedRegions` モード時のみ、`None` は無名ヒット)
    pub region: Option<String>,
}

// ============================================================================
// RegionHit - 内部判定結果
// ============================================================================

/// hit_test_entity_ex の内部返値
pub(crate) enum RegionHit {
    /// エンティティにヒットしなかった
    Miss,
    /// エンティティにヒットした(リージョン名は Option)
    Hit(Option<String>),
}

// ============================================================================
// hit_test_entity_ex - 単一エンティティ拡張ヒットテスト
// ============================================================================

/// 単一エンティティの拡張ヒットテスト
///
/// 既存 `hit_test_entity` と同等だが、`NamedRegions` モード時にリージョン名も返す。
///
/// # Arguments
/// - `world`: ECS World 参照
/// - `entity`: 判定対象エンティティ
/// - `point`: スクリーン座標(物理ピクセル)
///
/// # Returns
/// - `RegionHit::Hit(Some(name))`: 名前付き領域にヒット
/// - `RegionHit::Hit(None)`: エンティティにヒット(無名)
/// - `RegionHit::Miss`: ヒットしない
pub(crate) fn hit_test_entity_ex(world: &World, entity: Entity, point: PhysicalPoint) -> RegionHit {
    // HitTest コンポーネントを取得(なければデフォルト = Bounds)
    let mode = world
        .get::<HitTest>(entity)
        .map(|h| h.mode)
        .unwrap_or(HitTestMode::Bounds);

    // HitTestMode::None の場合はヒットしない
    if mode == HitTestMode::None {
        return RegionHit::Miss;
    }

    // GlobalArrangement を取得(なければヒットしない)
    let Some(global) = world.get::<GlobalArrangement>(entity) else {
        return RegionHit::Miss;
    };

    // まず矩形判定(全モード共通の早期リターン)
    if !global.bounds.contains(point.x, point.y) {
        return RegionHit::Miss;
    }

    // モード別判定
    match mode {
        HitTestMode::None => unreachable!(),
        HitTestMode::Bounds => {
            // Opacity * Brushes.foreground.a の合成α値が ALPHA_THRESHOLD 未満なら透明と判定
            const ALPHA_THRESHOLD: f32 = 128.0 / 255.0;

            let opacity = world
                .get::<crate::ecs::graphics::Visual>(entity)
                .map(|v| v.clamped_opacity())
                .unwrap_or(1.0);

            let foreground_a = world
                .get::<crate::ecs::widget::brushes::Brushes>(entity)
                .and_then(|b| b.foreground.as_color())
                .map(|c| c.a)
                .unwrap_or_else(|| {
                    crate::ecs::widget::brushes::DEFAULT_FOREGROUND
                        .as_color()
                        .map(|c| c.a)
                        .unwrap_or(1.0)
                });

            let composite_alpha = opacity * foreground_a;
            if composite_alpha < ALPHA_THRESHOLD {
                RegionHit::Miss
            } else {
                RegionHit::Hit(None)
            }
        }
        HitTestMode::AlphaMask => {
            // 共有ヘルパで αマスクを読み取り(AlphaMaskResource 最優先 → BitmapSourceResource → 矩形)。
            // hit_test_entity と同一挙動を保証(両エントリポイントの一致)。
            if alpha_mask_hit(world, entity, &global.bounds, point) {
                RegionHit::Hit(None)
            } else {
                RegionHit::Miss
            }
        }
        HitTestMode::NamedRegions => {
            // HitRegionMap を取得
            let Some(region_map) = world.get::<HitRegionMap>(entity) else {
                // HitRegionMap 不在時は Bounds フォールバック(1.3)
                warn!(
                    entity = ?entity,
                    "NamedRegions モードですが HitRegionMap がありません。Bounds にフォールバックします"
                );
                return RegionHit::Hit(None);
            };

            // 正規化座標を計算
            let bounds = &global.bounds;
            let bounds_width = bounds.right - bounds.left;
            let bounds_height = bounds.bottom - bounds.top;

            if bounds_width <= 0.0 || bounds_height <= 0.0 {
                return RegionHit::Hit(None);
            }

            let rel_x = (point.x - bounds.left) / bounds_width;
            let rel_y = (point.y - bounds.top) / bounds_height;

            // Arrangement.size を取得(DIPサイズ)
            // Arrangement がない場合は bounds サイズをフォールバック値として使用
            let entity_size =
                world
                    .get::<Arrangement>(entity)
                    .map(|a| a.size)
                    .unwrap_or(super::Size {
                        width: bounds_width,
                        height: bounds_height,
                    });

            // HitRegionMap で判定
            let region_name = region_map.hit_test_region(rel_x, rel_y, &entity_size);

            RegionHit::Hit(region_name.map(|s| s.to_string()))
        }
    }
}

// ============================================================================
// hit_test - ツリー走査ヒットテスト
// ============================================================================

/// 指定ルートエンティティ配下でスクリーン座標によるヒットテストを実行
///
/// # Arguments
/// - `world`: ECS World 参照
/// - `root`: 検索ルートエンティティ(LayoutRoot, Window, 任意のサブツリー)
/// - `screen_point`: スクリーン座標(物理ピクセル)
///
/// # Returns
/// - `Some(Entity)`: ヒットした最前面エンティティ
/// - `None`: ヒットなし
///
/// # Algorithm
/// 深さ優先・逆順・後順走査で最前面エンティティを優先
pub fn hit_test(world: &World, root: Entity, screen_point: PhysicalPoint) -> Option<Entity> {
    let mut traversal = DepthFirstReversePostOrder::new(root);

    while let Some(entity) = traversal.next(world) {
        let hit = hit_test_entity(world, entity, screen_point);
        if hit {
            return Some(entity);
        }
    }

    None
}

// ============================================================================
// hit_test_in_window - ウィンドウ座標ヒットテスト
// ============================================================================

/// 指定Window内でクライアント座標によるヒットテストを実行
///
/// # Arguments
/// - `world`: ECS World 参照
/// - `window`: Window エンティティ(WindowPos を持つ)
/// - `client_point`: ウィンドウクライアント座標(物理ピクセル)
///
/// # Returns
/// - `Some(Entity)`: ヒットした最前面エンティティ
/// - `None`: ヒットなしまたは WindowPos が存在しない
pub fn hit_test_in_window(
    world: &World,
    window: Entity,
    client_point: PhysicalPoint,
) -> Option<Entity> {
    // WindowPos を取得(なければ None)
    let window_pos = world.get::<WindowPos>(window)?;

    // position が None の場合も None を返す
    let position = window_pos.position?;

    // クライアント座標 + ウィンドウ位置 = スクリーン座標
    let screen_point = PhysicalPoint::new(
        client_point.x + position.x as f32,
        client_point.y + position.y as f32,
    );

    // hit_test に委譲
    hit_test(world, window, screen_point)
}

// ============================================================================
// hit_test_ex - ツリー走査拡張ヒットテスト
// ============================================================================

/// 指定ルートエンティティ配下でスクリーン座標による拡張ヒットテストを実行
///
/// 既存 `hit_test` と同じ走査だが、`HitTestResult`(エンティティ+リージョン名)を返す。
///
/// # Arguments
/// - `world`: ECS World 参照
/// - `root`: 検索ルートエンティティ
/// - `screen_point`: スクリーン座標(物理ピクセル)
///
/// # Returns
/// - `Some(HitTestResult)`: ヒットした最前面エンティティと領域名
/// - `None`: ヒットなし
pub fn hit_test_ex(
    world: &World,
    root: Entity,
    screen_point: PhysicalPoint,
) -> Option<HitTestResult> {
    let mut traversal = DepthFirstReversePostOrder::new(root);

    while let Some(entity) = traversal.next(world) {
        match hit_test_entity_ex(world, entity, screen_point) {
            RegionHit::Hit(region) => {
                return Some(HitTestResult { entity, region });
            }
            RegionHit::Miss => continue,
        }
    }

    None
}

// ============================================================================
// hit_test_in_window_ex - ウィンドウ座標拡張ヒットテスト
// ============================================================================

/// 指定Window内でクライアント座標による拡張ヒットテストを実行
///
/// # Arguments
/// - `world`: ECS World 参照
/// - `window`: Window エンティティ(WindowPos を持つ)
/// - `client_point`: ウィンドウクライアント座標(物理ピクセル)
///
/// # Returns
/// - `Some(HitTestResult)`: ヒットした最前面エンティティと領域名
/// - `None`: ヒットなしまたは WindowPos が存在しない
pub fn hit_test_in_window_ex(
    world: &World,
    window: Entity,
    client_point: PhysicalPoint,
) -> Option<HitTestResult> {
    let window_pos = world.get::<WindowPos>(window)?;
    let position = window_pos.position?;

    let screen_point = PhysicalPoint::new(
        client_point.x + position.x as f32,
        client_point.y + position.y as f32,
    );

    hit_test_ex(world, window, screen_point)
}

// ============================================================================
// テスト
// ============================================================================

#[cfg(test)]
mod tests;

#[cfg(test)]
mod tests_ex;

/// `AlphaMaskResource` の内部 `Arc` 化と `set_shared` の檻(structure.md のテスト分離規約)
#[cfg(test)]
#[path = "hit_test_shared_mask_tests.rs"]
mod shared_mask_tests;