Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Add UI scaling #5814

Closed
wants to merge 12 commits into from
10 changes: 10 additions & 0 deletions Cargo.toml
Expand Up @@ -1452,6 +1452,16 @@ description = "Illustrates various features of Bevy UI"
category = "UI (User Interface)"
wasm = true

[[example]]
name = "ui_scaling"
path = "examples/ui/scaling.rs"

[package.metadata.example.ui_scaling]
name = "UI Scaling"
description = "Illustrates how to scale the UI"
category = "UI (User Interface)"
wasm = true

# Window
[[example]]
name = "clear_color"
Expand Down
20 changes: 15 additions & 5 deletions crates/bevy_ui/src/flex/mod.rs
@@ -1,6 +1,6 @@
mod convert;

use crate::{CalculatedSize, Node, Style};
use crate::{CalculatedSize, Node, Style, UiScale};
use bevy_ecs::{
entity::Entity,
event::EventReader,
Expand Down Expand Up @@ -196,6 +196,7 @@ pub enum FlexError {
#[allow(clippy::too_many_arguments)]
pub fn flex_node_system(
windows: Res<Windows>,
ui_scale: Res<UiScale>,
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
mut flex_surface: ResMut<FlexSurface>,
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
Expand All @@ -216,14 +217,18 @@ pub fn flex_node_system(
// assume one window for time being...
let logical_to_physical_factor = windows.scale_factor(WindowId::primary());

if scale_factor_events.iter().next_back().is_some() {
if scale_factor_events.iter().next_back().is_some() || ui_scale.is_changed() {
update_changed(
&mut *flex_surface,
logical_to_physical_factor,
logical_to_physical_factor * ui_scale.scale,
cart marked this conversation as resolved.
Show resolved Hide resolved
full_node_query,
);
} else {
update_changed(&mut *flex_surface, logical_to_physical_factor, node_query);
update_changed(
&mut *flex_surface,
logical_to_physical_factor * ui_scale.scale,
node_query,
);
}

fn update_changed<F: WorldQuery>(
Expand All @@ -243,7 +248,12 @@ pub fn flex_node_system(
}

for (entity, style, calculated_size) in &changed_size_query {
flex_surface.upsert_leaf(entity, style, *calculated_size, logical_to_physical_factor);
flex_surface.upsert_leaf(
entity,
style,
*calculated_size,
logical_to_physical_factor * ui_scale.scale,
);
}

// TODO: handle removed nodes
Expand Down
24 changes: 22 additions & 2 deletions crates/bevy_ui/src/lib.rs
Expand Up @@ -22,11 +22,14 @@ pub use ui_node::*;
#[doc(hidden)]
pub mod prelude {
#[doc(hidden)]
pub use crate::{entity::*, geometry::*, ui_node::*, widget::Button, Interaction};
pub use crate::{entity::*, geometry::*, ui_node::*, widget::Button, Interaction, UiScale};
}

use bevy_app::prelude::*;
use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
use bevy_ecs::{
schedule::{ParallelSystemDescriptorCoercion, SystemLabel},
system::Resource,
};
use bevy_input::InputSystem;
use bevy_transform::TransformSystem;
use bevy_window::ModifiesWindows;
Expand All @@ -47,10 +50,27 @@ pub enum UiSystem {
Focus,
}

/// The current scale of the UI.
///
/// A multiplier to fixed-sized ui values.
/// **Note:** This will not affect non-fixed ui values like [`Val::Percent`]
Weibye marked this conversation as resolved.
Show resolved Hide resolved
#[derive(Debug, Resource)]
pub struct UiScale {
/// The scale to be applied.
pub scale: f64,
}

impl Default for UiScale {
fn default() -> Self {
Self { scale: 1.0 }
}
}

impl Plugin for UiPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
.init_resource::<FlexSurface>()
.init_resource::<UiScale>()
.register_type::<AlignContent>()
.register_type::<AlignItems>()
.register_type::<AlignSelf>()
Expand Down
13 changes: 10 additions & 3 deletions crates/bevy_ui/src/widget/text.rs
@@ -1,4 +1,4 @@
use crate::{CalculatedSize, Size, Style, Val};
use crate::{CalculatedSize, Size, Style, UiScale, Val};
use bevy_asset::Assets;
use bevy_ecs::{
entity::Entity,
Expand All @@ -9,7 +9,7 @@ use bevy_math::Vec2;
use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas;
use bevy_text::{DefaultTextPipeline, Font, FontAtlasSet, Text, TextError};
use bevy_window::{WindowId, Windows};
use bevy_window::Windows;

#[derive(Debug, Default)]
pub struct QueuedText {
Expand Down Expand Up @@ -43,6 +43,7 @@ pub fn text_system(
mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>,
windows: Res<Windows>,
ui_scale: Res<UiScale>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<DefaultTextPipeline>,
Expand All @@ -52,7 +53,13 @@ pub fn text_system(
Query<(&Text, &Style, &mut CalculatedSize)>,
)>,
) {
let scale_factor = windows.scale_factor(WindowId::primary());
// TODO: This should support window-independent scale settings.
// See https://github.com/bevyengine/bevy/issues/5621
let scale_factor = if let Some(window) = windows.get_primary() {
window.scale_factor() * ui_scale.scale
} else {
ui_scale.scale
};

let inv_scale_factor = 1. / scale_factor;

Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Expand Up @@ -313,6 +313,7 @@ Example | Description
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI
[UI Scaling](../examples/ui/scaling.rs) | Illustrates how to scale the UI

## Window

Expand Down
144 changes: 144 additions & 0 deletions examples/ui/scaling.rs
@@ -0,0 +1,144 @@
//! This example illustrates the [`UIScale`] resource from `bevy_ui`.

use bevy::{prelude::*, utils::Duration};

const SCALE_TIME: u64 = 400;

#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, SystemLabel)]
struct ApplyScaling;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(TargetScale {
start_scale: 1.0,
target_scale: 1.0,
target_time: Timer::new(Duration::from_millis(SCALE_TIME), false),
})
.add_startup_system(setup)
.add_system(apply_scaling.label(ApplyScaling))
.add_system(change_scaling.before(ApplyScaling))
.run();
}

fn setup(mut commands: Commands, asset_server: ResMut<AssetServer>) {
commands.spawn_bundle(Camera2dBundle::default());

let text_style = TextStyle {
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
font_size: 16.,
color: Color::BLACK,
};

commands
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Percent(50.0), Val::Percent(50.0)),
position_type: PositionType::Absolute,
position: UiRect {
left: Val::Percent(25.),
top: Val::Percent(25.),
..default()
},
justify_content: JustifyContent::SpaceAround,
align_items: AlignItems::Center,
..default()
},
color: Color::ANTIQUE_WHITE.into(),
..default()
})
.with_children(|parent| {
parent
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(40.), Val::Px(40.)),
..default()
},
color: Color::RED.into(),
..default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle::from_section("Size!", text_style));
});
parent.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Percent(15.), Val::Percent(15.)),
..default()
},
color: Color::BLUE.into(),
..default()
});
parent.spawn_bundle(ImageBundle {
style: Style {
size: Size::new(Val::Px(30.0), Val::Px(30.0)),
..default()
},
image: asset_server.load("branding/icon.png").into(),
..default()
});
});
}

/// System that changes the scale of the ui when pressing up or down on the keyboard.
fn change_scaling(input: Res<Input<KeyCode>>, mut ui_scale: ResMut<TargetScale>) {
if input.just_pressed(KeyCode::Up) {
let scale = (ui_scale.target_scale * 2.0).min(8.);
ui_scale.set_scale(scale);
info!("Scaling up! Scale: {}", ui_scale.target_scale);
}
if input.just_pressed(KeyCode::Down) {
let scale = (ui_scale.target_scale / 2.0).max(1. / 8.);
ui_scale.set_scale(scale);
info!("Scaling down! Scale: {}", ui_scale.target_scale);
}
}

#[derive(Resource)]
struct TargetScale {
start_scale: f64,
target_scale: f64,
target_time: Timer,
}

impl TargetScale {
fn set_scale(&mut self, scale: f64) {
self.start_scale = self.current_scale();
self.target_scale = scale;
self.target_time.reset();
}

fn current_scale(&self) -> f64 {
let completion = self.target_time.percent();
let multiplier = ease_in_expo(completion as f64);
self.start_scale + (self.target_scale - self.start_scale) * multiplier
}

fn tick(&mut self, delta: Duration) -> &Self {
self.target_time.tick(delta);
self
}

fn already_completed(&self) -> bool {
self.target_time.finished() && !self.target_time.just_finished()
}
}

fn apply_scaling(
time: Res<Time>,
mut target_scale: ResMut<TargetScale>,
mut ui_scale: ResMut<UiScale>,
) {
if target_scale.tick(time.delta()).already_completed() {
return;
}

ui_scale.scale = target_scale.current_scale();
}

fn ease_in_expo(x: f64) -> f64 {
if x == 0. {
0.
} else {
(2.0f64).powf(5. * x - 5.)
}
}