Skip to content

UI rounding is applied before UI scaling #14714

@eero-lehtinen

Description

@eero-lehtinen

Bevy version

main 7f658ca

What you did

Apply any UiScale or window scale factor override over 1, then move a UI element smoothly over the screen.

What went wrong

Here is an example of UiScale being 20 and then moving the white square smoothly across the screen. It jumps by 20 pixel increments.
The blue square is there as a comparison to show what smooth movement looks like. GlobalTransform was modified directly to achieve that.

ui_scale.mp4

I would expect the white square position to be rounded after applying UiScale. E.g. Px(0.47) would first be scaled to Px(0.47*20) = Px(9.4), then rounded to Px(9). This would effectively round logical UI pixels to physical screen pixels, which sounds more correct to me than the current behavior.

The jittery movement is especially bad when trying to position scrolling lists or virtual cursors that need to look smooth.

Additional information

Example code used in the video.

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, update)
        .run();
}

#[derive(Component)]
struct Node1;
#[derive(Component)]
struct Node2;

#[derive(Resource)]
struct NodeAnimation((f32, f32));

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());
    commands.spawn((
        NodeBundle {
            background_color: Color::WHITE.into(),
            style: Style {
                width: Val::Px(6.0),
                height: Val::Px(6.0),
                position_type: PositionType::Absolute,
                ..default()
            },
            ..default()
        },
        Node1,
    ));

    commands.spawn((
        NodeBundle {
            background_color: Srgba::BLUE.into(),
            style: Style {
                width: Val::Px(6.0),
                height: Val::Px(6.0),
                top: Val::Px(6.0),
                position_type: PositionType::Absolute,
                ..default()
            },
            ..default()
        },
        Node2,
    ));

    commands.insert_resource(UiScale(20.0));
    commands.insert_resource(NodeAnimation((0.0, 1.0)));
}

fn cubic_ease_in_out(factor: f32) -> f32 {
    if factor < 0.5 {
        4. * factor * factor * factor
    } else {
        1. - (-2. * factor + 2.).powf(3.) / 2.
    }
}

fn position(factor: f32) -> f32 {
    cubic_ease_in_out(factor) * 30.
}

fn update(
    mut node1_q: Query<&mut Style, With<Node1>>,
    mut node2_q: Query<&mut GlobalTransform, With<Node2>>,
    mut node_animation: ResMut<NodeAnimation>,
    time: Res<Time>,
) {
    let (factor, direction) = &mut node_animation.0;
    *factor += time.delta_seconds() * *direction * 0.3;
    if *factor > 1. {
        *direction = -1.;
    } else if *factor < 0. {
        *direction = 1.;
    }

    for mut style in node1_q.iter_mut() {
        style.left = Val::Px(position(*factor));
    }

    for mut t in node2_q.iter_mut() {
        let mut transform = t.compute_transform();
        transform.translation.x = position(*factor) + 3.;
        *t = transform.into();
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-RenderingDrawing game state to the screenA-UIGraphical user interfaces, styles, layouts, and widgetsC-BugAn unexpected or incorrect behaviorD-ComplexQuite challenging from either a design or technical perspective. Ask for help!S-Needs-DesignThis issue requires design work to think about how it would best be accomplished

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions