diff --git a/Cargo.toml b/Cargo.toml index b0920d9ecd0fa3..9fc190a3ca2f93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_ui/src/flex/mod.rs b/crates/bevy_ui/src/flex/mod.rs index e8ee3fc2db3f0b..9e98e1f01d7f56 100644 --- a/crates/bevy_ui/src/flex/mod.rs +++ b/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, @@ -196,6 +196,7 @@ pub enum FlexError { #[allow(clippy::too_many_arguments)] pub fn flex_node_system( windows: Res, + ui_scale: Res, mut scale_factor_events: EventReader, mut flex_surface: ResMut, root_node_query: Query, Without)>, @@ -215,15 +216,12 @@ pub fn flex_node_system( // assume one window for time being... 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() { - update_changed( - &mut *flex_surface, - logical_to_physical_factor, - full_node_query, - ); + if scale_factor_events.iter().next_back().is_some() || ui_scale.is_changed() { + update_changed(&mut *flex_surface, scale_factor, full_node_query); } else { - update_changed(&mut *flex_surface, logical_to_physical_factor, node_query); + update_changed(&mut *flex_surface, scale_factor, node_query); } fn update_changed( @@ -243,7 +241,7 @@ 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, scale_factor); } // TODO: handle removed nodes diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 6ec98338b3d7d8..e5a2b21c702c93 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -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; @@ -47,10 +50,27 @@ pub enum UiSystem { 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 { fn build(&self, app: &mut App) { app.add_plugin(ExtractComponentPlugin::::default()) .init_resource::() + .init_resource::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index ff004f8ea0a0c8..6e25817b9d55fa 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/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, @@ -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 { @@ -43,6 +43,7 @@ pub fn text_system( mut textures: ResMut>, fonts: Res>, windows: Res, + ui_scale: Res, mut texture_atlases: ResMut>, mut font_atlas_set_storage: ResMut>, mut text_pipeline: ResMut, @@ -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; diff --git a/examples/README.md b/examples/README.md index b23260cdcb5e01..9f9f1ac801ae49 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/examples/ui/scaling.rs b/examples/ui/scaling.rs new file mode 100644 index 00000000000000..5274c08fa64d7b --- /dev/null +++ b/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) { + 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>, mut ui_scale: ResMut) { + 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