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

Allow for ui scaling #2808

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,10 @@ path = "examples/ui/text_debug.rs"
name = "ui"
path = "examples/ui/ui.rs"

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

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

use crate::{CalculatedSize, Node, Style};
use crate::{CalculatedSize, Node, Style, UiScale};
use bevy_app::EventReader;
use bevy_ecs::{
entity::Entity,
Expand Down Expand Up @@ -198,6 +198,7 @@ pub enum FlexError {
#[allow(clippy::too_many_arguments, clippy::type_complexity)]
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 @@ -222,14 +223,18 @@ pub fn flex_node_system(
1.
};

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,
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 @@ -251,7 +256,12 @@ pub fn flex_node_system(
}

for (entity, style, calculated_size) in changed_size_query.iter() {
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
28 changes: 27 additions & 1 deletion crates/bevy_ui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ pub use ui_node::*;

pub mod prelude {
#[doc(hidden)]
pub use crate::{entity::*, ui_node::*, widget::Button, Anchors, Interaction, Margins};
pub use crate::{
entity::*, ui_node::*, widget::Button, Anchors, Interaction, Margins, UiScale,
};
}

use bevy_app::prelude::*;
Expand All @@ -39,9 +41,33 @@ pub enum UiSystem {
Focus,
}

#[derive(Debug)]
/// The current scale of the UI for all windows
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a conceptual reason this should be global instead of per-UI camera?

For example, I've needed a custom scale for different vscode windows because of different monitor scaling requirements - e.g. for presenting on one with notes on the other. Although I appreciate that is slightly different because those windows are different processes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a conceptual one no. So far ui-handling is not done per-window though, so I didn't go as far as understand how multiple window bevy works technically.

///
/// ## Note
/// This is purely about the logical scale, and can
/// be considered like a zoom
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm not if it's quite a zoom - in my mind it's closer to a shrinking of the logical UI window size. For me, a zoom suggests showing only part of the logical window, which this isn't intended for.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a dev I feel it more intuitive if one considers a zoom, since it effectively multiplies the size of the pixels shown just like a zoom would.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels better described to me as "setting UI scale", rather than a true zoom.

Copy link
Contributor Author

@TheNeikos TheNeikos Sep 15, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, should I remove the note?

///
/// This only affects pixel sizes, so a percent size will stay at that
pub struct UiScale {
/// The scale to be applied
///
/// # Example
///
/// A scale of `2.` will make every pixel size twice as large.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this whole 'example' section is potentially superfluous

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potentially, I prefer to make it clear though that one needs to put in 2. instead of 0.5 for twice the scale.

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.init_resource::<FlexSurface>()
.init_resource::<UiScale>()
.register_type::<AlignContent>()
.register_type::<AlignItems>()
.register_type::<AlignSelf>()
Expand Down
7 changes: 4 additions & 3 deletions crates/bevy_ui/src/widget/text.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{CalculatedSize, Node, Style, Val};
use crate::{CalculatedSize, Node, Style, UiScale, Val};
use bevy_asset::Assets;
use bevy_ecs::{
entity::Entity,
Expand Down Expand Up @@ -49,6 +49,7 @@ pub fn text_system(
mut textures: ResMut<Assets<Texture>>,
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 @@ -59,9 +60,9 @@ pub fn text_system(
)>,
) {
let scale_factor = if let Some(window) = windows.get_primary() {
window.scale_factor()
window.scale_factor() * ui_scale.scale
} else {
1.
ui_scale.scale
};

let inv_scale_factor = 1. / scale_factor;
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ Example | File | Description
`text` | [`ui/text.rs`](./ui/text.rs) | Illustrates creating and updating text
`text_debug` | [`ui/text_debug.rs`](./ui/text_debug.rs) | An example for debugging text layout
`ui` | [`ui/ui.rs`](./ui/ui.rs) | Illustrates various features of Bevy UI
`scaling` | [`ui/scaling.rs`](./ui/scaling.rs) | Illustrates how to scale the UI
TheNeikos marked this conversation as resolved.
Show resolved Hide resolved

## Window

Expand Down
148 changes: 148 additions & 0 deletions examples/ui/scaling.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
use bevy::{prelude::*, utils::Duration};

const SCALE_TIME: u64 = 400;

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

/// This example illustrates the UIScale resource from bevy_ui
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>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
commands.spawn_bundle(UiCameraBundle::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: Rect {
left: Val::Percent(25.),
top: Val::Percent(25.),
..Default::default()
},
justify_content: JustifyContent::SpaceAround,
align_items: AlignItems::Center,
..Default::default()
},
material: materials.add(Color::ANTIQUE_WHITE.into()),
..Default::default()
})
.with_children(|parent| {
parent
.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Px(40.), Val::Px(40.)),
..Default::default()
},
material: materials.add(Color::RED.into()),
..Default::default()
})
.with_children(|parent| {
parent.spawn_bundle(TextBundle {
text: Text::with_section("Size!", text_style, TextAlignment::default()),
..Default::default()
});
});
parent.spawn_bundle(NodeBundle {
style: Style {
size: Size::new(Val::Percent(15.), Val::Percent(15.)),
..Default::default()
},
material: materials.add(Color::BLUE.into()),
..Default::default()
});
parent.spawn_bundle(ImageBundle {
style: Style {
size: Size::new(Val::Px(30.0), Val::Px(30.0)),
..Default::default()
},
material: materials.add(asset_server.load("branding/icon.png").into()),
..Default::default()
});
});
}

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);
}
}

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