bevyengine / bevyengine/bevy

`UiTransform.scale` breaks text rendering, image scaling and border radius

Open
#22,880 0 comments 0 reactions 0 assignees View on GitHub
A-UI C-Bug S-Needs-Investigation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy 0.18

## What you did

Add a `Node` with overflow set to `scroll` and a (larger) child `Node` which is then scaled using `UiTransform`.

## What went wrong

- A `ImageNode` with `ImageNodeMode::Stretch` on the child does not scale correctly. Setting scale to 3 for example, appears to scale down the node.
- `Text` or `TextSpan` components on the child (or its descendants) stretch the text when close to the border of the parent.
- The borders of the descendants of the child scale incorrectly and seem to grow disproportionally fast when the descendant is (partially) outside the bounds of the parent.

https://github.com/user-attachments/assets/6fa38aee-a68d-4d71-a128-5c9c9adfac9d

## Additional information

The code to produce the above app

```rust

use bevy::{
asset::RenderAssetUsages, ecs::relationship::{RelatedSpawnerCommands, Relationship}, prelude::*, render::render_resource::{Extent3d, TextureDimension, TextureFormat}
};
fn main() {
App::new()
.add_plugins((DefaultPlugins,))
.add_systems(Startup, setup)
.add_systems(Update, update_scale)
.run();
}

#[derive(Component)]
struct ScaleText;
#[derive(Component)]
struct Scale;

fn update_scale(
mut scaled_background: Single<&mut UiTransform, With>,
mut text: Single<&mut TextSpan, With>,
time: Res

fn setup(mut commands: Commands, mut images: ResMut>) {
commands.spawn(Camera2d);

commands
.spawn(Node {
display: Display::Grid,
width: vw(100),
height: vh(100),
padding: UiRect::all(px(20)),
column_gap: px(20),
row_gap: px(20),
grid_template_columns: vec![RepeatedGridTrack::fr(2, 1.0)],
grid_template_rows: vec![RepeatedGridTrack::auto(1), RepeatedGridTrack::fr(1, 1.0)],
..Default::default()
})
.with_children(|parent| {
parent.spawn((
Node {
grid_column: GridPlacement::span(2),
..Default::default()
},
Text("The left and right panel contain the same content. The left panels content is scaled with a scale factor of ".into()),
)).with_children(|text| {
text.spawn((TextSpan("-/-".into()), TextColor(Srgba::RED.into()), ScaleText));
text.spawn(TextSpan(" whereas the right panel is not scaled (scale = 1).".into()));
});

let background = images.add(uv_debug_texture());
spawn_content(parent, background.clone(), true);
spawn_content(parent, background, false);
});
}

fn spawn_content(commands: &mut RelatedSpawnerCommands, background: Handle, with_scale: bool) {
const BACKGROUND_SIZE: f32 = 40960.0;
commands.spawn((
Node {
border: UiRect::all(px(2)),
overflow: Overflow::scroll(),
..Default::default()
},
ScrollPosition(Vec2::splat(BACKGROUND_SIZE / 2.0)),
BorderColor::all(Color::WHITE),
)).with_children(|parent| {
parent.spawn((
Node {
min_width: px(BACKGROUND_SIZE),
min_height: px(BACKGROUND_SIZE),
..Default::default()
},
BackgroundColor(Srgba::BLUE.into()),
ImageNode::new(background).with_mode(NodeImageMode::Stretch),
)).insert_if(Scale, || with_scale)
.with_child((
Node {
position_type: PositionType::Absolute,
width: px(300),
height: px(300),
border: UiRect::all(px(2)),
border_radius: BorderRadius::all(px(30)),
..Default::default()
},
UiTransform::from_translation(Val2::px(BACKGROUND_SIZE / 2.0, BACKGROUND_SIZE / 2.0)),
BackgroundColor(Color::BLACK.with_alpha(0.75)),
BorderColor::all(Color::WHITE),
children![(
Text("Hello World!".into()),
Node {
margin: UiRect::all(Val::Auto),
..Default::default()
}
)]
));
});
}

/// Creates a colorful test pattern
fn uv_debug_texture() -> Image {
const TEXTURE_SIZE: usize = 256;

let mut texture_data = [0; TEXTURE_SIZE * TEXTURE_SIZE * 4];
for y in 0..TEXTURE_SIZE {
for x in 0.. TEXTURE_SIZE {
let index = 4 * (y * TEXTURE_SIZE + x);
let color = if (x + y) % 2 == 0 {
[0, 0, 0, 255]
} else {
[255 * (x % 2) as u8, 255 * (y % 2) as u8, 0, 255]
};
texture_data[index..index + 4].copy_from_slice(&color);
}
}

let mut img = Image::new_fill(
Extent3d {
width: TEXTURE_SIZE as u32,
height: TEXTURE_SIZE as u32,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&texture_data,
TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD,
);
img.sampler = bevy::image::ImageSampler::nearest();
img
}

```


Contributor guide

Open the contributing guide

Research direction

Start by running the supplied Rust reproduction and observe the scaled and unscaled panels side by side. Trace the interaction between UiTransform, overflow scrolling, ImageNodeMode::Stretch, Text/TextSpan, borders, and border_radius; done means image scaling, text layout, and descendant borders remain correct at varying scale values.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
computer-graphics, game-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.