diff --git a/Cargo.toml b/Cargo.toml index 7c5687121e804..a35c8ee752b86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1745,6 +1745,16 @@ description = "Illustrates how FontAtlases are populated (used to optimize text category = "UI (User Interface)" wasm = true +[[example]] +name = "overflow_debug" +path = "examples/ui/overflow_debug.rs" + +[package.metadata.example.overflow_debug] +name = "Overflow and Clipping Debug" +description = "An example to debug overflow and clipping behavior" +category = "UI (User Interface)" +wasm = true + [[example]] name = "relative_cursor_position" path = "examples/ui/relative_cursor_position.rs" diff --git a/examples/README.md b/examples/README.md index 92f6e07d0640b..f42f6620bd0b8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -331,6 +331,7 @@ Example | Description [Button](../examples/ui/button.rs) | Illustrates creating and updating a button [Flex Layout](../examples/ui/flex_layout.rs) | Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text [Font Atlas Debug](../examples/ui/font_atlas_debug.rs) | Illustrates how FontAtlases are populated (used to optimize text rendering internally) +[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior [Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component [Text](../examples/ui/text.rs) | Illustrates creating and updating text [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout diff --git a/examples/ui/overflow_debug.rs b/examples/ui/overflow_debug.rs new file mode 100644 index 0000000000000..14e3e0d1f305c --- /dev/null +++ b/examples/ui/overflow_debug.rs @@ -0,0 +1,303 @@ +//! Tests how different transforms behave when clipped with `Overflow::Hidden` +use bevy::prelude::*; +use std::f32::consts::{FRAC_PI_2, PI, TAU}; + +const CONTAINER_SIZE: f32 = 150.0; +const HALF_CONTAINER_SIZE: f32 = CONTAINER_SIZE / 2.0; +const LOOP_LENGTH: f32 = 4.0; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // TODO: Remove once #8144 is fixed + .insert_resource(GizmoConfig { + enabled: false, + ..default() + }) + .insert_resource(AnimationState { + playing: false, + paused_at: 0.0, + paused_total: 0.0, + t: 0.0, + }) + .add_systems(Startup, setup) + .add_systems( + Update, + ( + toggle_overflow, + next_container_size, + update_transform::, + update_transform::, + update_transform::, + update_animation, + ), + ) + .run(); +} + +#[derive(Resource)] +struct AnimationState { + playing: bool, + paused_at: f32, + paused_total: f32, + t: f32, +} + +#[derive(Component)] +struct Container(u8); + +trait UpdateTransform { + fn update(&self, t: f32, transform: &mut Transform); +} + +#[derive(Component)] +struct Move; + +impl UpdateTransform for Move { + fn update(&self, t: f32, transform: &mut Transform) { + transform.translation.x = (t * TAU - FRAC_PI_2).sin() * HALF_CONTAINER_SIZE; + transform.translation.y = -(t * TAU - FRAC_PI_2).cos() * HALF_CONTAINER_SIZE; + } +} + +#[derive(Component)] +struct Scale; + +impl UpdateTransform for Scale { + fn update(&self, t: f32, transform: &mut Transform) { + transform.scale.x = 1.0 + 0.5 * (t * TAU).cos().max(0.0); + transform.scale.y = 1.0 + 0.5 * (t * TAU + PI).cos().max(0.0); + } +} + +#[derive(Component)] +struct Rotate; + +impl UpdateTransform for Rotate { + fn update(&self, t: f32, transform: &mut Transform) { + transform.rotation = Quat::from_axis_angle(Vec3::Z, ((t * TAU).cos() * 45.0).to_radians()); + } +} + +fn setup(mut commands: Commands, asset_server: Res) { + // Camera + commands.spawn(Camera2dBundle::default()); + + commands + .spawn(NodeBundle { + style: Style { + size: Size::width(Val::Percent(100.)), + flex_direction: FlexDirection::Column, + ..default() + }, + ..default() + }) + .with_children(|parent| { + parent + .spawn(NodeBundle { + style: Style { + size: Size::height(Val::Px(32.)), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + ..default() + }, + background_color: Color::DARK_GRAY.into(), + ..default() + }) + .with_children(|parent| { + parent.spawn(TextBundle::from_section( + vec![ + "Toggle Overflow (O)", + "Next Container Size (S)", + "Toggle Animation (space)", + ] + .join(" ยท "), + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 18.0, + color: Color::WHITE, + }, + )); + }); + + parent + .spawn(NodeBundle { + style: Style { + flex_grow: 1., + flex_direction: FlexDirection::Column, + ..default() + }, + ..default() + }) + .with_children(|parent| { + spawn_row(parent, |parent| { + spawn_image(parent, &asset_server, Move); + spawn_image(parent, &asset_server, Scale); + spawn_image(parent, &asset_server, Rotate); + }); + + spawn_row(parent, |parent| { + spawn_text(parent, &asset_server, Move); + spawn_text(parent, &asset_server, Scale); + spawn_text(parent, &asset_server, Rotate); + }); + }); + }); +} + +fn spawn_row(parent: &mut ChildBuilder, spawn_children: impl FnOnce(&mut ChildBuilder)) { + parent + .spawn(NodeBundle { + style: Style { + size: Size::width(Val::Percent(50.)), + align_items: AlignItems::Center, + justify_content: JustifyContent::SpaceEvenly, + ..default() + }, + ..default() + }) + .with_children(spawn_children); +} + +fn spawn_image( + parent: &mut ChildBuilder, + asset_server: &Res, + update_transform: impl UpdateTransform + Component, +) { + spawn_container(parent, update_transform, |parent| { + parent.spawn(ImageBundle { + image: UiImage::new(asset_server.load("branding/bevy_logo_dark_big.png")), + style: Style { + size: Size::height(Val::Px(100.)), + position_type: PositionType::Absolute, + top: Val::Px(-50.), + left: Val::Px(-200.), + ..default() + }, + ..default() + }); + }); +} + +fn spawn_text( + parent: &mut ChildBuilder, + asset_server: &Res, + update_transform: impl UpdateTransform + Component, +) { + spawn_container(parent, update_transform, |parent| { + parent.spawn(TextBundle::from_section( + "Bevy", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 120.0, + color: Color::WHITE, + }, + )); + }); +} + +fn spawn_container( + parent: &mut ChildBuilder, + update_transform: impl UpdateTransform + Component, + spawn_children: impl FnOnce(&mut ChildBuilder), +) { + let mut transform = Transform::default(); + + update_transform.update(0.0, &mut transform); + + parent + .spawn(( + NodeBundle { + style: Style { + size: Size::new(Val::Px(CONTAINER_SIZE), Val::Px(CONTAINER_SIZE)), + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + overflow: Overflow::Hidden, + ..default() + }, + background_color: Color::DARK_GRAY.into(), + ..default() + }, + Container(0), + )) + .with_children(|parent| { + parent + .spawn(( + NodeBundle { + style: Style { + align_items: AlignItems::Center, + justify_content: JustifyContent::Center, + top: Val::Px(transform.translation.x), + left: Val::Px(transform.translation.y), + ..default() + }, + transform, + ..default() + }, + update_transform, + )) + .with_children(spawn_children); + }); +} + +fn update_animation( + mut animation: ResMut, + time: Res