Skip to content

Commit 6231ed0

Browse files
ickshonpealice-i-cecilemweatherley
authored andcommitted
Ignore clicks on uinodes outside of rounded corners (#14957)
Fixes #14941 1. Add a `resolved_border_radius` field to `Node` to hold the resolved border radius values. 2. Remove the border radius calculations from the UI's extraction functions. 4. Compute the border radius during UI relayouts in `ui_layout_system` and store them in `Node`. 5. New `pick_rounded_rect` function based on the border radius SDF from `ui.wgsl`. 6. Use `pick_rounded_rect` in `focus` and `picking_backend` to check if the pointer is hovering UI nodes with rounded corners. --- ``` cargo run --example button ``` https://github.com/user-attachments/assets/ea951a64-17ef-455e-b5c9-a2e6f6360648 Modified button example with buttons with different corner radius: ``` use bevy::{color::palettes::basic::*, prelude::*, winit::WinitSettings}; fn main() { App::new() .add_plugins(DefaultPlugins) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) .add_systems(Startup, setup) .add_systems(Update, button_system) .run(); } const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15); const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25); const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35); fn button_system( mut interaction_query: Query< ( &Interaction, &mut BackgroundColor, &mut BorderColor, &Children, ), (Changed<Interaction>, With<Button>), >, mut text_query: Query<&mut Text>, ) { for (interaction, mut color, mut border_color, children) in &mut interaction_query { let mut text = text_query.get_mut(children[0]).unwrap(); match *interaction { Interaction::Pressed => { text.sections[0].value = "Press".to_string(); *color = PRESSED_BUTTON.into(); border_color.0 = RED.into(); } Interaction::Hovered => { text.sections[0].value = "Hover".to_string(); *color = HOVERED_BUTTON.into(); border_color.0 = Color::WHITE; } Interaction::None => { text.sections[0].value = "Button".to_string(); *color = NORMAL_BUTTON.into(); border_color.0 = Color::BLACK; } } } } fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { // ui camera commands.spawn(Camera2dBundle::default()); commands .spawn(NodeBundle { style: Style { width: Val::Percent(100.0), height: Val::Percent(100.0), align_items: AlignItems::Center, justify_content: JustifyContent::Center, row_gap: Val::Px(10.), ..default() }, ..default() }) .with_children(|parent| { for border_radius in [ BorderRadius { top_left: Val::ZERO, ..BorderRadius::MAX }, BorderRadius { top_right: Val::ZERO, ..BorderRadius::MAX }, BorderRadius { bottom_right: Val::ZERO, ..BorderRadius::MAX }, BorderRadius { bottom_left: Val::ZERO, ..BorderRadius::MAX }, ] { parent .spawn(ButtonBundle { style: Style { width: Val::Px(150.0), height: Val::Px(65.0), border: UiRect::all(Val::Px(5.0)), // horizontally center child text justify_content: JustifyContent::Center, // vertically center child text align_items: AlignItems::Center, ..default() }, border_color: BorderColor(Color::BLACK), border_radius, background_color: NORMAL_BUTTON.into(), ..default() }) .with_child(TextBundle::from_section( "Button", TextStyle { font: asset_server.load("fonts/FiraSans-Bold.ttf"), font_size: 40.0, color: Color::srgb(0.9, 0.9, 0.9), }, )); } }); } ``` --------- Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Matty <[email protected]>
1 parent 9afe666 commit 6231ed0

File tree

4 files changed

+144
-104
lines changed

4 files changed

+144
-104
lines changed

crates/bevy_ui/src/focus.rs

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use crate::{CalculatedClip, DefaultUiCamera, Node, TargetCamera, UiScale, UiStack};
1+
use crate::{
2+
CalculatedClip, DefaultUiCamera, Node, ResolvedBorderRadius, TargetCamera, UiScale, UiStack,
3+
};
24
use bevy_ecs::{
35
change_detection::DetectChangesMut,
46
entity::Entity,
@@ -249,19 +251,18 @@ pub fn ui_focus_system(
249251
.map(|clip| node_rect.intersect(clip.clip))
250252
.unwrap_or(node_rect);
251253

254+
let cursor_position = camera_cursor_positions.get(&camera_entity);
255+
252256
// The mouse position relative to the node
253257
// (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner
254258
// Coordinates are relative to the entire node, not just the visible region.
255-
let relative_cursor_position =
256-
camera_cursor_positions
257-
.get(&camera_entity)
258-
.and_then(|cursor_position| {
259-
// ensure node size is non-zero in all dimensions, otherwise relative position will be
260-
// +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to
261-
// false positives for mouse_over (#12395)
262-
(node_rect.size().cmpgt(Vec2::ZERO).all())
263-
.then_some((*cursor_position - node_rect.min) / node_rect.size())
264-
});
259+
let relative_cursor_position = cursor_position.and_then(|cursor_position| {
260+
// ensure node size is non-zero in all dimensions, otherwise relative position will be
261+
// +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to
262+
// false positives for mouse_over (#12395)
263+
(node_rect.size().cmpgt(Vec2::ZERO).all())
264+
.then_some((*cursor_position - node_rect.min) / node_rect.size())
265+
});
265266

266267
// If the current cursor position is within the bounds of the node's visible area, consider it for
267268
// clicking
@@ -270,7 +271,16 @@ pub fn ui_focus_system(
270271
normalized: relative_cursor_position,
271272
};
272273

273-
let contains_cursor = relative_cursor_position_component.mouse_over();
274+
let contains_cursor = relative_cursor_position_component.mouse_over()
275+
&& cursor_position
276+
.map(|point| {
277+
pick_rounded_rect(
278+
*point - node_rect.center(),
279+
node_rect.size(),
280+
node.node.border_radius,
281+
)
282+
})
283+
.unwrap_or(false);
274284

275285
// Save the relative cursor position to the correct component
276286
if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position
@@ -332,3 +342,18 @@ pub fn ui_focus_system(
332342
}
333343
}
334344
}
345+
346+
fn pick_rounded_rect(point: Vec2, size: Vec2, border_radius: ResolvedBorderRadius) -> bool {
347+
let s = point.signum();
348+
let r = (border_radius.top_left * (1. - s.x) * (1. - s.y)
349+
+ border_radius.top_right * (1. + s.x) * (1. - s.y)
350+
+ border_radius.bottom_right * (1. + s.x) * (1. + s.y)
351+
+ border_radius.bottom_left * (1. - s.x) * (1. + s.y))
352+
/ 4.;
353+
354+
let corner_to_point = point.abs() - 0.5 * size;
355+
let q = corner_to_point + r;
356+
let l = q.max(Vec2::ZERO).length();
357+
let m = q.max_element().min(0.);
358+
l + m - r < 0.
359+
}

crates/bevy_ui/src/layout/mod.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use thiserror::Error;
22

3-
use crate::{ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale};
3+
use crate::{
4+
BorderRadius, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale,
5+
};
46
use bevy_ecs::{
57
change_detection::{DetectChanges, DetectChangesMut},
68
entity::Entity,
@@ -93,7 +95,12 @@ pub fn ui_layout_system(
9395
children_query: Query<(Entity, Ref<Children>), With<Node>>,
9496
just_children_query: Query<&Children>,
9597
mut removed_components: UiLayoutSystemRemovedComponentParam,
96-
mut node_transform_query: Query<(&mut Node, &mut Transform, Option<&Outline>)>,
98+
mut node_transform_query: Query<(
99+
&mut Node,
100+
&mut Transform,
101+
Option<&BorderRadius>,
102+
Option<&Outline>,
103+
)>,
97104
) {
98105
struct CameraLayoutInfo {
99106
size: UVec2,
@@ -246,13 +253,20 @@ pub fn ui_layout_system(
246253
entity: Entity,
247254
ui_surface: &UiSurface,
248255
root_size: Option<Vec2>,
249-
node_transform_query: &mut Query<(&mut Node, &mut Transform, Option<&Outline>)>,
256+
node_transform_query: &mut Query<(
257+
&mut Node,
258+
&mut Transform,
259+
Option<&BorderRadius>,
260+
Option<&Outline>,
261+
)>,
250262
children_query: &Query<&Children>,
251263
inverse_target_scale_factor: f32,
252264
parent_size: Vec2,
253265
mut absolute_location: Vec2,
254266
) {
255-
if let Ok((mut node, mut transform, outline)) = node_transform_query.get_mut(entity) {
267+
if let Ok((mut node, mut transform, maybe_border_radius, maybe_outline)) =
268+
node_transform_query.get_mut(entity)
269+
{
256270
let Ok(layout) = ui_surface.get_layout(entity) else {
257271
return;
258272
};
@@ -277,7 +291,13 @@ pub fn ui_layout_system(
277291

278292
let viewport_size = root_size.unwrap_or(node.calculated_size);
279293

280-
if let Some(outline) = outline {
294+
if let Some(border_radius) = maybe_border_radius {
295+
// We don't trigger change detection for changes to border radius
296+
node.bypass_change_detection().border_radius =
297+
border_radius.resolve(node.calculated_size, viewport_size);
298+
}
299+
300+
if let Some(outline) = maybe_outline {
281301
// don't trigger change detection when only outlines are changed
282302
let node = node.bypass_change_detection();
283303
node.outline_width = outline

crates/bevy_ui/src/render/mod.rs

Lines changed: 33 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ use ui_texture_slice_pipeline::UiTextureSlicerPlugin;
2323

2424
use crate::graph::{NodeUi, SubGraphUi};
2525
use crate::{
26-
BackgroundColor, BorderColor, BorderRadius, CalculatedClip, ContentSize, DefaultUiCamera, Node,
27-
Outline, Style, TargetCamera, UiImage, UiScale, Val,
26+
BackgroundColor, BorderColor, CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline,
27+
Style, TargetCamera, UiImage, UiScale, Val,
2828
};
2929

3030
use bevy_app::prelude::*;
@@ -201,7 +201,6 @@ pub fn extract_uinode_background_colors(
201201
Option<&CalculatedClip>,
202202
Option<&TargetCamera>,
203203
&BackgroundColor,
204-
Option<&BorderRadius>,
205204
&Style,
206205
Option<&Parent>,
207206
)>,
@@ -216,7 +215,6 @@ pub fn extract_uinode_background_colors(
216215
clip,
217216
camera,
218217
background_color,
219-
border_radius,
220218
style,
221219
parent,
222220
) in &uinode_query
@@ -257,16 +255,13 @@ pub fn extract_uinode_background_colors(
257255

258256
let border = [left, top, right, bottom];
259257

260-
let border_radius = if let Some(border_radius) = border_radius {
261-
resolve_border_radius(
262-
border_radius,
263-
uinode.size(),
264-
ui_logical_viewport_size,
265-
ui_scale.0,
266-
)
267-
} else {
268-
[0.; 4]
269-
};
258+
let border_radius = [
259+
uinode.border_radius.top_left,
260+
uinode.border_radius.top_right,
261+
uinode.border_radius.bottom_right,
262+
uinode.border_radius.bottom_left,
263+
]
264+
.map(|r| r * ui_scale.0);
270265

271266
extracted_uinodes.uinodes.insert(
272267
entity,
@@ -310,7 +305,6 @@ pub fn extract_uinode_images(
310305
Option<&TargetCamera>,
311306
&UiImage,
312307
Option<&TextureAtlas>,
313-
Option<&BorderRadius>,
314308
Option<&Parent>,
315309
&Style,
316310
),
@@ -319,18 +313,8 @@ pub fn extract_uinode_images(
319313
>,
320314
node_query: Extract<Query<&Node>>,
321315
) {
322-
for (
323-
uinode,
324-
transform,
325-
view_visibility,
326-
clip,
327-
camera,
328-
image,
329-
atlas,
330-
border_radius,
331-
parent,
332-
style,
333-
) in &uinode_query
316+
for (uinode, transform, view_visibility, clip, camera, image, atlas, parent, style) in
317+
&uinode_query
334318
{
335319
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
336320
else {
@@ -391,16 +375,13 @@ pub fn extract_uinode_images(
391375

392376
let border = [left, top, right, bottom];
393377

394-
let border_radius = if let Some(border_radius) = border_radius {
395-
resolve_border_radius(
396-
border_radius,
397-
uinode.size(),
398-
ui_logical_viewport_size,
399-
ui_scale.0,
400-
)
401-
} else {
402-
[0.; 4]
403-
};
378+
let border_radius = [
379+
uinode.border_radius.top_left,
380+
uinode.border_radius.top_right,
381+
uinode.border_radius.bottom_right,
382+
uinode.border_radius.bottom_left,
383+
]
384+
.map(|r| r * ui_scale.0);
404385

405386
extracted_uinodes.uinodes.insert(
406387
commands.spawn_empty().id(),
@@ -435,33 +416,6 @@ pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_s
435416
}
436417
}
437418

438-
pub(crate) fn resolve_border_radius(
439-
&values: &BorderRadius,
440-
node_size: Vec2,
441-
viewport_size: Vec2,
442-
ui_scale: f32,
443-
) -> [f32; 4] {
444-
let max_radius = 0.5 * node_size.min_element() * ui_scale;
445-
[
446-
values.top_left,
447-
values.top_right,
448-
values.bottom_right,
449-
values.bottom_left,
450-
]
451-
.map(|value| {
452-
match value {
453-
Val::Auto => 0.,
454-
Val::Px(px) => ui_scale * px,
455-
Val::Percent(percent) => node_size.min_element() * percent / 100.,
456-
Val::Vw(percent) => viewport_size.x * percent / 100.,
457-
Val::Vh(percent) => viewport_size.y * percent / 100.,
458-
Val::VMin(percent) => viewport_size.min_element() * percent / 100.,
459-
Val::VMax(percent) => viewport_size.max_element() * percent / 100.,
460-
}
461-
.clamp(0., max_radius)
462-
})
463-
}
464-
465419
#[inline]
466420
fn clamp_corner(r: f32, size: Vec2, offset: Vec2) -> f32 {
467421
let s = 0.5 * size + offset;
@@ -501,7 +455,6 @@ pub fn extract_uinode_borders(
501455
Option<&Parent>,
502456
&Style,
503457
&BorderColor,
504-
&BorderRadius,
505458
),
506459
Without<ContentSize>,
507460
>,
@@ -510,17 +463,8 @@ pub fn extract_uinode_borders(
510463
) {
511464
let image = AssetId::<Image>::default();
512465

513-
for (
514-
node,
515-
global_transform,
516-
view_visibility,
517-
clip,
518-
camera,
519-
parent,
520-
style,
521-
border_color,
522-
border_radius,
523-
) in &uinode_query
466+
for (uinode, global_transform, view_visibility, clip, camera, parent, style, border_color) in
467+
&uinode_query
524468
{
525469
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
526470
else {
@@ -530,8 +474,8 @@ pub fn extract_uinode_borders(
530474
// Skip invisible borders
531475
if !view_visibility.get()
532476
|| border_color.0.is_fully_transparent()
533-
|| node.size().x <= 0.
534-
|| node.size().y <= 0.
477+
|| uinode.size().x <= 0.
478+
|| uinode.size().y <= 0.
535479
{
536480
continue;
537481
}
@@ -567,25 +511,27 @@ pub fn extract_uinode_borders(
567511
continue;
568512
}
569513

570-
let border_radius = resolve_border_radius(
571-
border_radius,
572-
node.size(),
573-
ui_logical_viewport_size,
574-
ui_scale.0,
575-
);
514+
let border_radius = [
515+
uinode.border_radius.top_left,
516+
uinode.border_radius.top_right,
517+
uinode.border_radius.bottom_right,
518+
uinode.border_radius.bottom_left,
519+
]
520+
.map(|r| r * ui_scale.0);
521+
522+
let border_radius = clamp_radius(border_radius, uinode.size(), border.into());
576523

577-
let border_radius = clamp_radius(border_radius, node.size(), border.into());
578524
let transform = global_transform.compute_matrix();
579525

580526
extracted_uinodes.uinodes.insert(
581527
commands.spawn_empty().id(),
582528
ExtractedUiNode {
583-
stack_index: node.stack_index,
529+
stack_index: uinode.stack_index,
584530
// This translates the uinode's transform to the center of the current border rectangle
585531
transform,
586532
color: border_color.0.into(),
587533
rect: Rect {
588-
max: node.size(),
534+
max: uinode.size(),
589535
..Default::default()
590536
},
591537
image,

0 commit comments

Comments
 (0)