Add UI scaling (#5814)

# Objective

- Allow users to change the scaling of the UI
- Adopted from #2808

## Solution

- This is an accessibility feature for fixed-size UI elements, allowing the developer to expose a range of UI scales for the player to set a scale that works for their needs.

> - The user can modify the UiScale struct to change the scaling at runtime. This multiplies the Px values by the scale given, while not touching any others.
> - The example showcases how this even allows for fluid transitions

> Here's how the example looks like:

https://user-images.githubusercontent.com/1631166/132979069-044161a9-8e85-45ab-9e93-fcf8e3852c2b.mp4

---

## Changelog

- Added a `UiScale` which can be used to scale all of UI


Co-authored-by: Andreas Weibye <13300393+Weibye@users.noreply.github.com>
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Andreas Weibye 2022-08-29 23:35:53 +00:00
parent f68f5cd2a5
commit 4fadd26168
6 changed files with 194 additions and 14 deletions

View file

@ -1452,6 +1452,16 @@ description = "Illustrates various features of Bevy UI"
category = "UI (User Interface)" category = "UI (User Interface)"
wasm = true 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 # Window
[[example]] [[example]]
name = "clear_color" name = "clear_color"

View file

@ -1,6 +1,6 @@
mod convert; mod convert;
use crate::{CalculatedSize, Node, Style}; use crate::{CalculatedSize, Node, Style, UiScale};
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, entity::Entity,
event::EventReader, event::EventReader,
@ -196,6 +196,7 @@ pub enum FlexError {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn flex_node_system( pub fn flex_node_system(
windows: Res<Windows>, windows: Res<Windows>,
ui_scale: Res<UiScale>,
mut scale_factor_events: EventReader<WindowScaleFactorChanged>, mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
mut flex_surface: ResMut<FlexSurface>, mut flex_surface: ResMut<FlexSurface>,
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>, root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
@ -215,15 +216,12 @@ pub fn flex_node_system(
// assume one window for time being... // assume one window for time being...
let logical_to_physical_factor = windows.scale_factor(WindowId::primary()); let logical_to_physical_factor = windows.scale_factor(WindowId::primary());
let scale_factor = logical_to_physical_factor * ui_scale.scale;
if scale_factor_events.iter().next_back().is_some() { if scale_factor_events.iter().next_back().is_some() || ui_scale.is_changed() {
update_changed( update_changed(&mut *flex_surface, scale_factor, full_node_query);
&mut *flex_surface,
logical_to_physical_factor,
full_node_query,
);
} else { } else {
update_changed(&mut *flex_surface, logical_to_physical_factor, node_query); update_changed(&mut *flex_surface, scale_factor, node_query);
} }
fn update_changed<F: WorldQuery>( fn update_changed<F: WorldQuery>(
@ -243,7 +241,7 @@ pub fn flex_node_system(
} }
for (entity, style, calculated_size) in &changed_size_query { 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, scale_factor);
} }
// TODO: handle removed nodes // TODO: handle removed nodes

View file

@ -22,11 +22,14 @@ pub use ui_node::*;
#[doc(hidden)] #[doc(hidden)]
pub mod prelude { pub mod prelude {
#[doc(hidden)] #[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_app::prelude::*;
use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel}; use bevy_ecs::{
schedule::{ParallelSystemDescriptorCoercion, SystemLabel},
system::Resource,
};
use bevy_input::InputSystem; use bevy_input::InputSystem;
use bevy_transform::TransformSystem; use bevy_transform::TransformSystem;
use bevy_window::ModifiesWindows; use bevy_window::ModifiesWindows;
@ -47,10 +50,27 @@ pub enum UiSystem {
Focus, Focus,
} }
/// The current scale of the UI.
///
/// A multiplier to fixed-sized ui values.
/// **Note:** This will only affect fixed ui values like [`Val::Px`]
#[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 { impl Plugin for UiPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default()) app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
.init_resource::<FlexSurface>() .init_resource::<FlexSurface>()
.init_resource::<UiScale>()
.register_type::<AlignContent>() .register_type::<AlignContent>()
.register_type::<AlignItems>() .register_type::<AlignItems>()
.register_type::<AlignSelf>() .register_type::<AlignSelf>()

View file

@ -1,4 +1,4 @@
use crate::{CalculatedSize, Size, Style, Val}; use crate::{CalculatedSize, Size, Style, UiScale, Val};
use bevy_asset::Assets; use bevy_asset::Assets;
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, entity::Entity,
@ -9,7 +9,7 @@ use bevy_math::Vec2;
use bevy_render::texture::Image; use bevy_render::texture::Image;
use bevy_sprite::TextureAtlas; use bevy_sprite::TextureAtlas;
use bevy_text::{DefaultTextPipeline, Font, FontAtlasSet, Text, TextError}; use bevy_text::{DefaultTextPipeline, Font, FontAtlasSet, Text, TextError};
use bevy_window::{WindowId, Windows}; use bevy_window::Windows;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct QueuedText { pub struct QueuedText {
@ -43,6 +43,7 @@ pub fn text_system(
mut textures: ResMut<Assets<Image>>, mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
windows: Res<Windows>, windows: Res<Windows>,
ui_scale: Res<UiScale>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>, mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>, mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
mut text_pipeline: ResMut<DefaultTextPipeline>, mut text_pipeline: ResMut<DefaultTextPipeline>,
@ -52,7 +53,13 @@ pub fn text_system(
Query<(&Text, &Style, &mut CalculatedSize)>, 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; let inv_scale_factor = 1. / scale_factor;

View file

@ -313,6 +313,7 @@ Example | Description
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy 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 ## Window

144
examples/ui/scaling.rs Normal file
View file

@ -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.)
}
}