Skip to content

Create a new OverflowAxis that allows scrolling while keeping overflown items visible #19773

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

Closed
wants to merge 7 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
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3354,6 +3354,17 @@ description = "An example to debug overflow and clipping behavior"
category = "UI (User Interface)"
wasm = true

[[example]]
name = "overflow_scroll"
path = "examples/ui/overflow_scroll.rs"
doc-scrape-examples = true

[package.metadata.example.overflow_scroll]
name = "Overflow and ScrollPosition interaction"
description = "Shows the behavior of `ScrollPosition` for nodes with [`OverflowAxis::Visible`], [`OverflowAxis::Scroll`], and [`OverflowAxis::ScrollNoClip`]"
category = "UI (User Interface)"
wasm = true

[[example]]
name = "relative_cursor_position"
path = "examples/ui/relative_cursor_position.rs"
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ui/src/layout/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,7 @@ impl From<OverflowAxis> for taffy::style::Overflow {
OverflowAxis::Visible => taffy::style::Overflow::Visible,
OverflowAxis::Clip => taffy::style::Overflow::Clip,
OverflowAxis::Hidden => taffy::style::Overflow::Hidden,
OverflowAxis::Scroll => taffy::style::Overflow::Scroll,
OverflowAxis::Scroll | OverflowAxis::ScrollNoClip => taffy::style::Overflow::Scroll,
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions crates/bevy_ui/src/layout/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -310,12 +310,18 @@ with UI components as a child of an entity without UI components, your UI layout
let scroll_position: Vec2 = maybe_scroll_position
.map(|scroll_pos| {
Vec2::new(
if style.overflow.x == OverflowAxis::Scroll {
if matches!(
style.overflow.x,
OverflowAxis::Scroll | OverflowAxis::ScrollNoClip
) {
scroll_pos.offset_x
} else {
0.0
},
if style.overflow.y == OverflowAxis::Scroll {
if matches!(
style.overflow.y,
OverflowAxis::Scroll | OverflowAxis::ScrollNoClip
) {
scroll_pos.offset_y
} else {
0.0
Expand Down
28 changes: 27 additions & 1 deletion crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1185,6 +1185,30 @@ impl Overflow {
y: OverflowAxis::Scroll,
}
}

/// Scroll overflowing items on both axis without clipping them
pub const fn scroll_no_clip() -> Self {
Self {
x: OverflowAxis::ScrollNoClip,
y: OverflowAxis::ScrollNoClip,
}
}

/// Scroll overflowing items on the x axis without clipping them
pub const fn scroll_x_no_clip() -> Self {
Self {
x: OverflowAxis::ScrollNoClip,
y: OverflowAxis::DEFAULT,
}
}

/// Scroll overflowing items on the y axis without clipping them
pub const fn scroll_y_no_clip() -> Self {
Self {
x: OverflowAxis::DEFAULT,
y: OverflowAxis::ScrollNoClip,
}
}
}

impl Default for Overflow {
Expand All @@ -1210,14 +1234,16 @@ pub enum OverflowAxis {
Hidden,
/// Scroll overflowing items.
Scroll,
/// Scroll overflowing items without clipping overflown items.
ScrollNoClip,
}

impl OverflowAxis {
pub const DEFAULT: Self = Self::Visible;

/// Overflow is visible on this axis
pub const fn is_visible(&self) -> bool {
matches!(self, Self::Visible)
matches!(self, Self::Visible | Self::ScrollNoClip)
}
}

Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,7 @@ Example | Description
[Overflow](../examples/ui/overflow.rs) | Simple example demonstrating overflow behavior
[Overflow Clip Margin](../examples/ui/overflow_clip_margin.rs) | Simple example demonstrating the OverflowClipMargin style property
[Overflow and Clipping Debug](../examples/ui/overflow_debug.rs) | An example to debug overflow and clipping behavior
[Overflow and ScrollPosition interaction](../examples/ui/overflow_scroll.rs) | Shows the behavior of `ScrollPosition` for nodes with [`OverflowAxis::Visible`], [`OverflowAxis::Scroll`], and [`OverflowAxis::ScrollNoClip`]
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
[Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world
[Scroll](../examples/ui/scroll.rs) | Demonstrates scrolling UI containers
Expand Down
152 changes: 152 additions & 0 deletions examples/ui/overflow_scroll.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
//! Shows the distinction between [`Overflow::visible`], [`Overflow::scroll`], and [`Overflow::scroll_no_clip`].
//!
//! Use [`KeyCode::ArrowLeft`] or [`KeyCode::ArrowRight`] to scroll the items

use bevy::{
app::{App, Startup, Update},
asset::{AssetServer, Handle},
color::Color,
core_pipeline::core_2d::Camera2d,
ecs::{
bundle::Bundle,
children,
component::Component,
hierarchy::{ChildOf, Children},
query::With,
spawn::{SpawnIter, SpawnRelated, SpawnableList},
system::{Commands, Query, Res},
},
image::{Image, TextureAtlas, TextureAtlasLayout},
input::{keyboard::KeyCode, ButtonInput},
math::UVec2,
time::Time,
ui::{
widget::{ImageNode, Text},
AlignItems, AlignSelf, BorderColor, FlexDirection, JustifySelf, Node, Overflow,
ScrollPosition, UiRect, Val,
},
DefaultPlugins,
};

/// Length of the sides of the texture
const TEXTURE_SIDES: u32 = 64;
/// Length of the sides of the image nodes
const NODE_SIDES: f32 = 64. * 2.;
/// Speed of scrolling in pixels per second
const SCROLL_SPEED: f32 = 320.;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, scroll)
.run();
}

#[derive(Component)]
/// Marks UI [`Node`] that can be scrolled
struct Scrollable;

/// Spawns a scene with 3 UI nodes
///
/// [`Overflow::visible`] allows items to be visible, even those out of the bounds of the node
/// [`Overflow::scroll`] clips items out of the bounds but allows scrolling using [`ScrollPosition`]
/// [`Overflow::scroll`] allows scrolling using [`ScrollPosition`] and keeps items out of bounds visible
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);

let image = asset_server.load("textures/food_kenney.png");
let layout = TextureAtlasLayout::from_grid(UVec2::splat(TEXTURE_SIDES), 7, 6, None, None);
let layout_handle = asset_server.add(layout);

commands.spawn((
Node {
width: Val::Vw(80.),
flex_direction: FlexDirection::Column,
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..Default::default()
},
Children::spawn(ExampleNodes(image, layout_handle)),
));
}

/// Scrolls the UI nodes on [`KeyCode::ArrowRight`] or [`KeyCode::ArrowLeft`] inputs
fn scroll(
mut scrollables: Query<&mut ScrollPosition, With<Scrollable>>,
key_codes: Res<ButtonInput<KeyCode>>,
time: Res<Time>,
) {
let to_scroll: f32 = [
Some(SCROLL_SPEED).filter(|_| key_codes.pressed(KeyCode::ArrowRight)),
Some(-SCROLL_SPEED).filter(|_| key_codes.pressed(KeyCode::ArrowLeft)),
]
.into_iter()
.flatten()
.sum();
let scaled_scroll = to_scroll * time.delta_secs();

for mut scrollable in scrollables.iter_mut() {
// No need to deal with going beyond the end of the scroll as it is clamped by Bevy
scrollable.offset_x += scaled_scroll;
}
}

struct ExampleNodes(Handle<Image>, Handle<TextureAtlasLayout>);

impl SpawnableList<ChildOf> for ExampleNodes {
fn spawn(self, world: &mut bevy_ecs::world::World, entity: bevy_ecs::entity::Entity) {
for (header, overflow) in [
("Overflow::visible", Overflow::visible()),
("Overflow::scroll", Overflow::scroll()),
("Overflow::scroll_no_clip", Overflow::scroll_no_clip()),
] {
world.spawn((
Node {
width: Val::Percent(100.),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
..Default::default()
},
children![
Text::new(header),
(
Scrollable,
Node {
width: Val::Px(NODE_SIDES),
height: Val::Px(NODE_SIDES),
flex_direction: FlexDirection::Row,
border: UiRect::all(Val::Px(2.)),
overflow,
..Default::default()
},
BorderColor::all(Color::WHITE),
spawn_list(&self.0, &self.1)
)
],
ChildOf(entity),
));
}
}

fn size_hint(&self) -> usize {
3
}
}

/// Spawn a list of node, each with a distinct index into a texture atlas
fn spawn_list(image: &Handle<Image>, layout: &Handle<TextureAtlasLayout>) -> impl Bundle {
Children::spawn(SpawnIter(
(0..(6 * 7))
.map(|id| ImageNode {
image: image.clone(),
texture_atlas: Some(TextureAtlas {
layout: layout.clone(),
index: id,
}),
..Default::default()
})
.collect::<Vec<_>>()
.into_iter(),
))
}
11 changes: 11 additions & 0 deletions release-content/release-notes/overflow_scroll_no_clip.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Add `OverflowAxis::ScrollNoClip
authors: ["@hukasu"]
pull_requests: [19773]
---

Create a new variant for `OverflowAxis` called `ScrollNoClip`, which allows scrolling
while keeping overflowing items visible.

This variant are also accessible through `Overflow`'s new methods `scroll_no_clip`,
`scroll_x_no_clip`, and `scroll_y_no_clip`.