Skip to content

Commit 4fadd26

Browse files
TheNeikosWeibyecart
committed
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 <[email protected]> Co-authored-by: Carter Anderson <[email protected]>
1 parent f68f5cd commit 4fadd26

File tree

6 files changed

+194
-14
lines changed

6 files changed

+194
-14
lines changed

Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1452,6 +1452,16 @@ description = "Illustrates various features of Bevy UI"
14521452
category = "UI (User Interface)"
14531453
wasm = true
14541454

1455+
[[example]]
1456+
name = "ui_scaling"
1457+
path = "examples/ui/scaling.rs"
1458+
1459+
[package.metadata.example.ui_scaling]
1460+
name = "UI Scaling"
1461+
description = "Illustrates how to scale the UI"
1462+
category = "UI (User Interface)"
1463+
wasm = true
1464+
14551465
# Window
14561466
[[example]]
14571467
name = "clear_color"

crates/bevy_ui/src/flex/mod.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod convert;
22

3-
use crate::{CalculatedSize, Node, Style};
3+
use crate::{CalculatedSize, Node, Style, UiScale};
44
use bevy_ecs::{
55
entity::Entity,
66
event::EventReader,
@@ -196,6 +196,7 @@ pub enum FlexError {
196196
#[allow(clippy::too_many_arguments)]
197197
pub fn flex_node_system(
198198
windows: Res<Windows>,
199+
ui_scale: Res<UiScale>,
199200
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
200201
mut flex_surface: ResMut<FlexSurface>,
201202
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
@@ -215,15 +216,12 @@ pub fn flex_node_system(
215216

216217
// assume one window for time being...
217218
let logical_to_physical_factor = windows.scale_factor(WindowId::primary());
219+
let scale_factor = logical_to_physical_factor * ui_scale.scale;
218220

219-
if scale_factor_events.iter().next_back().is_some() {
220-
update_changed(
221-
&mut *flex_surface,
222-
logical_to_physical_factor,
223-
full_node_query,
224-
);
221+
if scale_factor_events.iter().next_back().is_some() || ui_scale.is_changed() {
222+
update_changed(&mut *flex_surface, scale_factor, full_node_query);
225223
} else {
226-
update_changed(&mut *flex_surface, logical_to_physical_factor, node_query);
224+
update_changed(&mut *flex_surface, scale_factor, node_query);
227225
}
228226

229227
fn update_changed<F: WorldQuery>(
@@ -243,7 +241,7 @@ pub fn flex_node_system(
243241
}
244242

245243
for (entity, style, calculated_size) in &changed_size_query {
246-
flex_surface.upsert_leaf(entity, style, *calculated_size, logical_to_physical_factor);
244+
flex_surface.upsert_leaf(entity, style, *calculated_size, scale_factor);
247245
}
248246

249247
// TODO: handle removed nodes

crates/bevy_ui/src/lib.rs

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,14 @@ pub use ui_node::*;
2222
#[doc(hidden)]
2323
pub mod prelude {
2424
#[doc(hidden)]
25-
pub use crate::{entity::*, geometry::*, ui_node::*, widget::Button, Interaction};
25+
pub use crate::{entity::*, geometry::*, ui_node::*, widget::Button, Interaction, UiScale};
2626
}
2727

2828
use bevy_app::prelude::*;
29-
use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
29+
use bevy_ecs::{
30+
schedule::{ParallelSystemDescriptorCoercion, SystemLabel},
31+
system::Resource,
32+
};
3033
use bevy_input::InputSystem;
3134
use bevy_transform::TransformSystem;
3235
use bevy_window::ModifiesWindows;
@@ -47,10 +50,27 @@ pub enum UiSystem {
4750
Focus,
4851
}
4952

53+
/// The current scale of the UI.
54+
///
55+
/// A multiplier to fixed-sized ui values.
56+
/// **Note:** This will only affect fixed ui values like [`Val::Px`]
57+
#[derive(Debug, Resource)]
58+
pub struct UiScale {
59+
/// The scale to be applied.
60+
pub scale: f64,
61+
}
62+
63+
impl Default for UiScale {
64+
fn default() -> Self {
65+
Self { scale: 1.0 }
66+
}
67+
}
68+
5069
impl Plugin for UiPlugin {
5170
fn build(&self, app: &mut App) {
5271
app.add_plugin(ExtractComponentPlugin::<UiCameraConfig>::default())
5372
.init_resource::<FlexSurface>()
73+
.init_resource::<UiScale>()
5474
.register_type::<AlignContent>()
5575
.register_type::<AlignItems>()
5676
.register_type::<AlignSelf>()

crates/bevy_ui/src/widget/text.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use crate::{CalculatedSize, Size, Style, Val};
1+
use crate::{CalculatedSize, Size, Style, UiScale, Val};
22
use bevy_asset::Assets;
33
use bevy_ecs::{
44
entity::Entity,
@@ -9,7 +9,7 @@ use bevy_math::Vec2;
99
use bevy_render::texture::Image;
1010
use bevy_sprite::TextureAtlas;
1111
use bevy_text::{DefaultTextPipeline, Font, FontAtlasSet, Text, TextError};
12-
use bevy_window::{WindowId, Windows};
12+
use bevy_window::Windows;
1313

1414
#[derive(Debug, Default)]
1515
pub struct QueuedText {
@@ -43,6 +43,7 @@ pub fn text_system(
4343
mut textures: ResMut<Assets<Image>>,
4444
fonts: Res<Assets<Font>>,
4545
windows: Res<Windows>,
46+
ui_scale: Res<UiScale>,
4647
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
4748
mut font_atlas_set_storage: ResMut<Assets<FontAtlasSet>>,
4849
mut text_pipeline: ResMut<DefaultTextPipeline>,
@@ -52,7 +53,13 @@ pub fn text_system(
5253
Query<(&Text, &Style, &mut CalculatedSize)>,
5354
)>,
5455
) {
55-
let scale_factor = windows.scale_factor(WindowId::primary());
56+
// TODO: This should support window-independent scale settings.
57+
// See https://github.com/bevyengine/bevy/issues/5621
58+
let scale_factor = if let Some(window) = windows.get_primary() {
59+
window.scale_factor() * ui_scale.scale
60+
} else {
61+
ui_scale.scale
62+
};
5663

5764
let inv_scale_factor = 1. / scale_factor;
5865

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,7 @@ Example | Description
313313
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
314314
[Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI
315315
[UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI
316+
[UI Scaling](../examples/ui/scaling.rs) | Illustrates how to scale the UI
316317

317318
## Window
318319

examples/ui/scaling.rs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
//! This example illustrates the [`UIScale`] resource from `bevy_ui`.
2+
3+
use bevy::{prelude::*, utils::Duration};
4+
5+
const SCALE_TIME: u64 = 400;
6+
7+
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, SystemLabel)]
8+
struct ApplyScaling;
9+
10+
fn main() {
11+
App::new()
12+
.add_plugins(DefaultPlugins)
13+
.insert_resource(TargetScale {
14+
start_scale: 1.0,
15+
target_scale: 1.0,
16+
target_time: Timer::new(Duration::from_millis(SCALE_TIME), false),
17+
})
18+
.add_startup_system(setup)
19+
.add_system(apply_scaling.label(ApplyScaling))
20+
.add_system(change_scaling.before(ApplyScaling))
21+
.run();
22+
}
23+
24+
fn setup(mut commands: Commands, asset_server: ResMut<AssetServer>) {
25+
commands.spawn_bundle(Camera2dBundle::default());
26+
27+
let text_style = TextStyle {
28+
font: asset_server.load("fonts/FiraMono-Medium.ttf"),
29+
font_size: 16.,
30+
color: Color::BLACK,
31+
};
32+
33+
commands
34+
.spawn_bundle(NodeBundle {
35+
style: Style {
36+
size: Size::new(Val::Percent(50.0), Val::Percent(50.0)),
37+
position_type: PositionType::Absolute,
38+
position: UiRect {
39+
left: Val::Percent(25.),
40+
top: Val::Percent(25.),
41+
..default()
42+
},
43+
justify_content: JustifyContent::SpaceAround,
44+
align_items: AlignItems::Center,
45+
..default()
46+
},
47+
color: Color::ANTIQUE_WHITE.into(),
48+
..default()
49+
})
50+
.with_children(|parent| {
51+
parent
52+
.spawn_bundle(NodeBundle {
53+
style: Style {
54+
size: Size::new(Val::Px(40.), Val::Px(40.)),
55+
..default()
56+
},
57+
color: Color::RED.into(),
58+
..default()
59+
})
60+
.with_children(|parent| {
61+
parent.spawn_bundle(TextBundle::from_section("Size!", text_style));
62+
});
63+
parent.spawn_bundle(NodeBundle {
64+
style: Style {
65+
size: Size::new(Val::Percent(15.), Val::Percent(15.)),
66+
..default()
67+
},
68+
color: Color::BLUE.into(),
69+
..default()
70+
});
71+
parent.spawn_bundle(ImageBundle {
72+
style: Style {
73+
size: Size::new(Val::Px(30.0), Val::Px(30.0)),
74+
..default()
75+
},
76+
image: asset_server.load("branding/icon.png").into(),
77+
..default()
78+
});
79+
});
80+
}
81+
82+
/// System that changes the scale of the ui when pressing up or down on the keyboard.
83+
fn change_scaling(input: Res<Input<KeyCode>>, mut ui_scale: ResMut<TargetScale>) {
84+
if input.just_pressed(KeyCode::Up) {
85+
let scale = (ui_scale.target_scale * 2.0).min(8.);
86+
ui_scale.set_scale(scale);
87+
info!("Scaling up! Scale: {}", ui_scale.target_scale);
88+
}
89+
if input.just_pressed(KeyCode::Down) {
90+
let scale = (ui_scale.target_scale / 2.0).max(1. / 8.);
91+
ui_scale.set_scale(scale);
92+
info!("Scaling down! Scale: {}", ui_scale.target_scale);
93+
}
94+
}
95+
96+
#[derive(Resource)]
97+
struct TargetScale {
98+
start_scale: f64,
99+
target_scale: f64,
100+
target_time: Timer,
101+
}
102+
103+
impl TargetScale {
104+
fn set_scale(&mut self, scale: f64) {
105+
self.start_scale = self.current_scale();
106+
self.target_scale = scale;
107+
self.target_time.reset();
108+
}
109+
110+
fn current_scale(&self) -> f64 {
111+
let completion = self.target_time.percent();
112+
let multiplier = ease_in_expo(completion as f64);
113+
self.start_scale + (self.target_scale - self.start_scale) * multiplier
114+
}
115+
116+
fn tick(&mut self, delta: Duration) -> &Self {
117+
self.target_time.tick(delta);
118+
self
119+
}
120+
121+
fn already_completed(&self) -> bool {
122+
self.target_time.finished() && !self.target_time.just_finished()
123+
}
124+
}
125+
126+
fn apply_scaling(
127+
time: Res<Time>,
128+
mut target_scale: ResMut<TargetScale>,
129+
mut ui_scale: ResMut<UiScale>,
130+
) {
131+
if target_scale.tick(time.delta()).already_completed() {
132+
return;
133+
}
134+
135+
ui_scale.scale = target_scale.current_scale();
136+
}
137+
138+
fn ease_in_expo(x: f64) -> f64 {
139+
if x == 0. {
140+
0.
141+
} else {
142+
(2.0f64).powf(5. * x - 5.)
143+
}
144+
}

0 commit comments

Comments
 (0)