use bevy::ecs::system::SystemParam;
use bevy::picking::Pickable;
use bevy::prelude::*;
use bevy::text::TextColor;
use bevy::ui::widget::{ImageNode, Text};
use bevy::ui::{BackgroundColor, BorderColor, ComputedNode, UiGlobalTransform};
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
pub struct PfHitTestVisible(pub bool);
#[derive(Component, Debug)]
pub struct PfHitTestSuppressed;
#[derive(Component, Debug)]
pub struct PfBackgroundSet;
pub(crate) fn background_governs_hit_testing(kind: &str) -> bool {
matches!(
kind,
"Grid"
| "StackPanel"
| "DockPanel"
| "WrapPanel"
| "Canvas"
| "UniformGrid"
| "Window"
| "Page"
| "UserControl"
)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PfHitFilter {
Visible,
HitTestVisible,
#[default]
Painted,
}
type HitNode = (
&'static ComputedNode,
&'static UiGlobalTransform,
&'static Visibility,
&'static BackgroundColor,
&'static BorderColor,
Option<&'static Pickable>,
Option<&'static TextColor>,
Has<Text>,
Has<ImageNode>,
);
type GovernedNode = (
Option<&'static crate::components::PfElementKind>,
Has<PfBackgroundSet>,
);
#[derive(SystemParam)]
pub struct PfHitTest<'w, 's> {
nodes: Query<'w, 's, HitNode>,
children: Query<'w, 's, &'static Children>,
roots: Query<'w, 's, Entity, (With<ComputedNode>, Without<ChildOf>)>,
}
impl PfHitTest<'_, '_> {
pub fn hit(&self, point: Vec2, filter: PfHitFilter) -> Option<Entity> {
self.roots
.iter()
.find_map(|root| self.descend(root, point, filter, true))
}
pub fn hit_in(&self, root: Entity, point: Vec2, filter: PfHitFilter) -> Option<Entity> {
self.descend(root, point, filter, true)
}
pub fn contains(&self, root: Entity, point: Vec2, filter: PfHitFilter) -> bool {
self.hit_in(root, point, filter).is_some()
}
pub fn any(&self, point: Vec2, filter: PfHitFilter) -> bool {
self.hit(point, filter).is_some()
}
fn descend(
&self,
entity: Entity,
point: Vec2,
filter: PfHitFilter,
inherited: bool,
) -> Option<Entity> {
let visible = match self.nodes.get(entity) {
Ok((_, _, visibility, _, _, _, _, _, _)) => match visibility {
Visibility::Visible => true,
Visibility::Hidden => false,
Visibility::Inherited => inherited,
},
Err(_) => inherited,
};
if let Ok(children) = self.children.get(entity) {
let kids: Vec<Entity> = children.iter().collect();
for kid in kids.into_iter().rev() {
if let Some(found) = self.descend(kid, point, filter, visible) {
return Some(found);
}
}
}
if !visible {
return None;
}
let Ok((node, transform, _, background, border, pickable, text_color, has_text, has_image)) =
self.nodes.get(entity)
else {
return None;
};
let qualifies = match filter {
PfHitFilter::Visible => true,
PfHitFilter::HitTestVisible => {
pickable.is_none_or(|p| p.should_block_lower || p.is_hoverable)
}
PfHitFilter::Painted => {
let paints_background = !background.0.is_fully_transparent();
let thickness = node.border();
let paints_border = !border.is_fully_transparent()
&& (thickness.min_inset.max_element() > 0.0
|| thickness.max_inset.max_element() > 0.0);
let paints_text =
has_text && text_color.is_none_or(|c| !c.0.is_fully_transparent());
paints_background || paints_border || paints_text || has_image
}
};
if !qualifies {
return None;
}
let inverse_scale = node.inverse_scale_factor();
if inverse_scale <= 0.0 {
return None;
}
node.contains_point(*transform, point / inverse_scale)
.then_some(entity)
}
}
pub(crate) fn hit_test_visibility_is_stale(
declared: Query<(), With<PfHitTestVisible>>,
changed: Query<(), Changed<PfHitTestVisible>>,
added: Query<(), Added<Node>>,
) -> bool {
!declared.is_empty() && (!changed.is_empty() || !added.is_empty())
}
pub(crate) fn propagate_hit_test_visibility(
roots: Query<Entity, (With<Node>, Without<ChildOf>)>,
flags: Query<&PfHitTestVisible>,
children: Query<&Children>,
suppressed: Query<(), With<PfHitTestSuppressed>>,
governed: Query<GovernedNode>,
mut commands: Commands,
) {
fn restored_pickable(entity: Entity, governed: &Query<GovernedNode>) -> Pickable {
match governed.get(entity) {
Ok((Some(kind), false)) if background_governs_hit_testing(&kind.0) => Pickable::IGNORE,
_ => Pickable::default(),
}
}
fn walk(
entity: Entity,
inherited: bool,
flags: &Query<&PfHitTestVisible>,
children: &Query<&Children>,
suppressed: &Query<(), With<PfHitTestSuppressed>>,
governed: &Query<GovernedNode>,
commands: &mut Commands,
) {
let effective = inherited && flags.get(entity).map_or(true, |f| f.0);
let was_suppressed = suppressed.contains(entity);
if !effective && !was_suppressed {
commands
.entity(entity)
.insert((Pickable::IGNORE, PfHitTestSuppressed));
} else if effective && was_suppressed {
commands
.entity(entity)
.remove::<PfHitTestSuppressed>()
.insert(restored_pickable(entity, governed));
}
if let Ok(kids) = children.get(entity) {
for kid in kids.iter().collect::<Vec<_>>() {
walk(
kid, effective, flags, children, suppressed, governed, commands,
);
}
}
}
for root in roots.iter() {
walk(
root,
true,
&flags,
&children,
&suppressed,
&governed,
&mut commands,
);
}
}