Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4bc1166
Added `pick_ui_text` helper function, call it in `ui_picking` when po…
ickshonpe Dec 6, 2025
756929f
* Add the target camera entity to hit nodes, instead of querying for …
ickshonpe Dec 6, 2025
e5f3405
Add observers to the `TextBackgroundColors` example's text entities.…
ickshonpe Dec 6, 2025
65ddb1c
cleaned up example
ickshonpe Dec 6, 2025
d4da53a
Added draft release note
ickshonpe Dec 6, 2025
552960d
updated release note
ickshonpe Dec 6, 2025
edbd1dc
rewrite `let...else` using the `?` operator
ickshonpe Dec 6, 2025
603ecde
fixed needless borrow
ickshonpe Dec 6, 2025
5c95155
Merge branch 'main' into text_section_picking
ickshonpe Dec 6, 2025
55a5354
fixed needless borrow
ickshonpe Dec 6, 2025
ca70875
Merge branch 'text_section_picking' of https://github.com/ickshonpe/b…
ickshonpe Dec 6, 2025
ac22d20
edited release note
ickshonpe Dec 6, 2025
0e48051
Tried to fix release note.
ickshonpe Dec 6, 2025
e95f371
Use Color::BLACK in example instead of css black.
ickshonpe Dec 6, 2025
4d92f59
Merge branch 'main' into text_section_picking
ickshonpe Dec 8, 2025
d1e80b8
Merge branch 'main' into text_section_picking
ickshonpe Dec 9, 2025
d23fc71
Merge branch 'main' into text_section_picking
ickshonpe Dec 10, 2025
871647a
Added migration guide.
ickshonpe Dec 10, 2025
960bce2
Merge branch 'text_section_picking' of https://github.com/ickshonpe/b…
ickshonpe Dec 10, 2025
3ce016f
Added trailing line to migration guide
ickshonpe Dec 10, 2025
5892a6f
Made migration guide more specific.
ickshonpe Dec 10, 2025
f9f36a2
Replace UK english with US.
ickshonpe Dec 10, 2025
46f4c4d
Merge branch 'main' into text_section_picking
ickshonpe Dec 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 55 additions & 15 deletions crates/bevy_ui/src/picking_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use bevy_ecs::{prelude::*, query::QueryData};
use bevy_math::Vec2;
use bevy_platform::collections::HashMap;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_text::{ComputedTextBlock, TextLayoutInfo};
use bevy_window::PrimaryWindow;

use bevy_picking::backend::prelude::*;
Expand Down Expand Up @@ -92,6 +93,7 @@ pub struct NodeQuery {
pickable: Option<&'static Pickable>,
inherited_visibility: Option<&'static InheritedVisibility>,
target_camera: &'static ComputedUiTargetCamera,
text_node: Option<(&'static TextLayoutInfo, &'static ComputedTextBlock)>,
}

/// Computes the UI node entities under each pointer.
Expand All @@ -108,6 +110,7 @@ pub fn ui_picking(
mut output: MessageWriter<PointerHits>,
clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>,
child_of_query: Query<&ChildOf, Without<OverrideClip>>,
pickable_query: Query<&Pickable>,
) {
// Map from each camera to its active pointers and their positions in viewport space
let mut pointer_pos_by_camera = HashMap::<Entity, HashMap<PointerId, Vec2>>::default();
Expand Down Expand Up @@ -143,7 +146,7 @@ pub fn ui_picking(
}

// The list of node entities hovered for each (camera, pointer) combo
let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<(Entity, Vec2)>>::default();
let mut hit_nodes = HashMap::<(Entity, PointerId), Vec<(Entity, Entity, Vec2)>>::default();

// prepare an iterator that contains all the nodes that have the cursor in their rect,
// from the top node to the bottom one. this will also reset the interaction to `None`
Expand Down Expand Up @@ -175,7 +178,8 @@ pub fn ui_picking(
continue;
};

if settings.require_markers && node.pickable.is_none() {
// Nodes with Display::None have a (0., 0.) logical rect and can be ignored
if node.node.size() == Vec2::ZERO {
continue;
}

Expand All @@ -188,16 +192,38 @@ pub fn ui_picking(
continue;
}

// Nodes with Display::None have a (0., 0.) logical rect and can be ignored
if node.node.size() == Vec2::ZERO {
// If this is a text node, need to do this check per section.
if node.text_node.is_none() && settings.require_markers && node.pickable.is_none() {
continue;
}

// Find the normalized cursor position relative to the node.
// (±0., 0.) is the center with the corners at points (±0.5, ±0.5).
// Coordinates are relative to the entire node, not just the visible region.
for (pointer_id, cursor_position) in pointers_on_this_cam.iter() {
if node.node.contains_point(*node.transform, *cursor_position)
if let Some((text_layout_info, text_block)) = node.text_node {
if let Some(text_entity) = pick_ui_text_section(
node.node,
node.transform,
*cursor_position,
text_layout_info,
text_block,
) {
if settings.require_markers && !pickable_query.contains(text_entity) {
continue;
}

hit_nodes
.entry((camera_entity, *pointer_id))
.or_default()
.push((
text_entity,
camera_entity,
node.transform.inverse().transform_point2(*cursor_position)
/ node.node.size(),
));
}
} else if node.node.contains_point(*node.transform, *cursor_position)
&& clip_check_recursive(
*cursor_position,
node_entity,
Expand All @@ -210,6 +236,7 @@ pub fn ui_picking(
.or_default()
.push((
node_entity,
camera_entity,
node.transform.inverse().transform_point2(*cursor_position)
/ node.node.size(),
));
Expand All @@ -224,19 +251,13 @@ pub fn ui_picking(
let mut picks = Vec::new();
let mut depth = 0.0;

for (hovered_node, position) in hovered {
let node = node_query.get(*hovered_node).unwrap();

let Some(camera_entity) = node.target_camera.get() else {
continue;
};

for (hovered_node, camera_entity, position) in hovered {
picks.push((
node.entity,
HitData::new(camera_entity, depth, Some(position.extend(0.0)), None),
*hovered_node,
HitData::new(*camera_entity, depth, Some(position.extend(0.0)), None),
));

if let Some(pickable) = node.pickable {
if let Ok(pickable) = pickable_query.get(*hovered_node) {
// If an entity has a `Pickable` component, we will use that as the source of truth.
if pickable.should_block_lower {
break;
Expand All @@ -258,3 +279,22 @@ pub fn ui_picking(
output.write(PointerHits::new(*pointer, picks, order));
}
}

fn pick_ui_text_section(
uinode: &ComputedNode,
global_transform: &UiGlobalTransform,
point: Vec2,
text_layout_info: &TextLayoutInfo,
text_block: &ComputedTextBlock,
) -> Option<Entity> {
let local_point = global_transform
.try_inverse()
.map(|transform| transform.transform_point2(point) + 0.5 * uinode.size())?;

for run in text_layout_info.run_geometry.iter() {
if run.bounds.contains(local_point) {
return text_block.entities().get(run.span_index).map(|e| e.entity);
}
}
None
}
33 changes: 24 additions & 9 deletions examples/ui/text_background_colors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,30 @@ fn setup(mut commands: Commands) {
))
.with_children(|commands| {
for (i, section_str) in message_text.iter().enumerate() {
commands.spawn((
TextSpan::new(*section_str),
TextColor::BLACK,
TextFont {
font_size: 100.,
..default()
},
TextBackgroundColor(PALETTE[i % PALETTE.len()]),
));
commands
.spawn((
TextSpan::new(*section_str),
TextColor::BLACK,
TextFont {
font_size: 100.,
..default()
},
TextBackgroundColor(PALETTE[i % PALETTE.len()]),
))
.observe(
|event: On<Pointer<Over>>, mut query: Query<&mut TextColor>| {
if let Ok(mut text_color) = query.get_mut(event.entity) {
text_color.0 = Color::WHITE;
}
},
)
.observe(
|event: On<Pointer<Out>>, mut query: Query<&mut TextColor>| {
if let Ok(mut text_color) = query.get_mut(event.entity) {
text_color.0 = Color::BLACK;
}
},
);
}
});
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: "The non-text areas of UI `Text` nodes are no longer pickable"
pull_requests: [22047]
---

Only the sections of `Text` node's containing text are pickable now, the non-text areas of the node do not register pointer hits.
To replicate Bevy 0.17's picking behavior, use an intermediate parent node to intercept the pointer hits.
7 changes: 7 additions & 0 deletions release-content/release-notes/per_text_section_picking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: "UI per text section picking"
authors: ["@ickshonpe"]
pull_requests: [22047]
---

Individual text sections belonging to UI text nodes are now pickable and can be given observers.