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;
pub type PhysicalPoint = PointF;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HitTestMode {
None,
#[default]
Bounds,
AlphaMask,
NamedRegions,
}
#[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,
}
}
}
#[derive(Component, Debug, Clone, Default)]
pub struct AlphaMaskResource {
mask: Option<Arc<AlphaMask>>,
}
impl AlphaMaskResource {
pub fn new() -> Self {
Self { mask: None }
}
pub fn set(&mut self, mask: AlphaMask) {
self.mask = Some(Arc::new(mask));
}
pub fn set_shared(&mut self, mask: Arc<AlphaMask>) {
self.mask = Some(mask);
}
pub fn mask(&self) -> Option<&AlphaMask> {
self.mask.as_deref()
}
}
fn alpha_mask_hit(world: &World, entity: Entity, bounds: &D2DRect, point: PhysicalPoint) -> bool {
use crate::ecs::widget::bitmap_source::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; }
let rel_x = (point.x - bounds.left) / bounds_width;
let rel_y = (point.y - bounds.top) / bounds_height;
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)
}
pub fn hit_test_entity(world: &World, entity: Entity, point: PhysicalPoint) -> bool {
let mode = world
.get::<HitTest>(entity)
.map(|h| h.mode)
.unwrap_or(HitTestMode::Bounds);
if mode == HitTestMode::None {
return false;
}
let Some(global) = world.get::<GlobalArrangement>(entity) else {
return false;
};
if !global.bounds.contains(point.x, point.y) {
return false;
}
if mode == HitTestMode::Bounds {
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;
}
alpha_mask_hit(world, entity, &global.bounds, point)
}
#[derive(Debug, Clone)]
pub struct HitTestResult {
pub entity: Entity,
pub region: Option<String>,
}
pub(crate) enum RegionHit {
Miss,
Hit(Option<String>),
}
pub(crate) fn hit_test_entity_ex(world: &World, entity: Entity, point: PhysicalPoint) -> RegionHit {
let mode = world
.get::<HitTest>(entity)
.map(|h| h.mode)
.unwrap_or(HitTestMode::Bounds);
if mode == HitTestMode::None {
return RegionHit::Miss;
}
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 => {
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 => {
if alpha_mask_hit(world, entity, &global.bounds, point) {
RegionHit::Hit(None)
} else {
RegionHit::Miss
}
}
HitTestMode::NamedRegions => {
let Some(region_map) = world.get::<HitRegionMap>(entity) else {
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;
let entity_size =
world
.get::<Arrangement>(entity)
.map(|a| a.size)
.unwrap_or(super::Size {
width: bounds_width,
height: bounds_height,
});
let region_name = region_map.hit_test_region(rel_x, rel_y, &entity_size);
RegionHit::Hit(region_name.map(|s| s.to_string()))
}
}
}
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
}
pub fn hit_test_in_window(
world: &World,
window: Entity,
client_point: PhysicalPoint,
) -> Option<Entity> {
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(world, window, screen_point)
}
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
}
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;
#[cfg(test)]
#[path = "hit_test_shared_mask_tests.rs"]
mod shared_mask_tests;