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

UI Scrolling #15291

Merged
merged 7 commits into from
Sep 23, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
10 changes: 10 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2847,7 +2847,17 @@ doc-scrape-examples = true
[package.metadata.example.grid]
name = "CSS Grid"
description = "An example for CSS Grid layout"
category = "UI (User Interface)"
wasm = true

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

[package.metadata.example.scroll]
name = "Scroll"
description = "Demonstrates scrolling UI containers"
category = "UI (User Interface)"
wasm = true

Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ui/src/layout/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,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,
}
}
}
Expand Down
67 changes: 60 additions & 7 deletions crates/bevy_ui/src/layout/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use crate::{
BorderRadius, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale,
BorderRadius, ContentSize, DefaultUiCamera, Node, Outline, OverflowAxis, ScrollPosition, Style,
TargetCamera, UiScale,
};
use bevy_ecs::{
change_detection::{DetectChanges, DetectChangesMut},
entity::{Entity, EntityHashMap, EntityHashSet},
event::EventReader,
query::{With, Without},
removal_detection::RemovedComponents,
system::{Local, Query, Res, ResMut, SystemParam},
system::{Commands, Local, Query, Res, ResMut, SystemParam},
world::Ref,
};
use bevy_hierarchy::{Children, Parent};
Expand Down Expand Up @@ -91,10 +92,10 @@ struct CameraLayoutInfo {
/// Updates the UI's layout tree, computes the new layout geometry and then updates the sizes and transforms of all the UI nodes.
#[allow(clippy::too_many_arguments)]
pub fn ui_layout_system(
mut commands: Commands,
mut buffers: Local<UiLayoutSystemBuffers>,
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
cameras: Query<(Entity, &Camera)>,
default_ui_camera: DefaultUiCamera,
camera_data: (Query<(Entity, &Camera)>, DefaultUiCamera),
ui_scale: Res<UiScale>,
mut scale_factor_events: EventReader<WindowScaleFactorChanged>,
mut resize_events: EventReader<bevy_window::WindowResized>,
Expand All @@ -115,8 +116,10 @@ pub fn ui_layout_system(
mut node_transform_query: Query<(
&mut Node,
&mut Transform,
&Style,
Option<&BorderRadius>,
Option<&Outline>,
Option<&ScrollPosition>,
)>,
#[cfg(feature = "bevy_text")] mut buffer_query: Query<&mut CosmicBuffer>,
#[cfg(feature = "bevy_text")] mut text_pipeline: ResMut<TextPipeline>,
Expand All @@ -127,6 +130,8 @@ pub fn ui_layout_system(
camera_layout_info,
} = &mut *buffers;

let (cameras, default_ui_camera) = camera_data;

let default_camera = default_ui_camera.get();
let camera_with_default = |target_camera: Option<&TargetCamera>| {
target_camera.map(TargetCamera::entity).or(default_camera)
Expand Down Expand Up @@ -266,8 +271,10 @@ pub fn ui_layout_system(
#[cfg(feature = "bevy_text")]
font_system,
);

for root in &camera.root_nodes {
update_uinode_geometry_recursive(
&mut commands,
*root,
&ui_surface,
None,
Expand All @@ -276,34 +283,47 @@ pub fn ui_layout_system(
inverse_target_scale_factor,
Vec2::ZERO,
Vec2::ZERO,
Vec2::ZERO,
);
}

camera.root_nodes.clear();
interned_root_nodes.push(camera.root_nodes);
}

// Returns the combined bounding box of the node and any of its overflowing children.
fn update_uinode_geometry_recursive(
commands: &mut Commands,
entity: Entity,
ui_surface: &UiSurface,
root_size: Option<Vec2>,
node_transform_query: &mut Query<(
&mut Node,
&mut Transform,
&Style,
Option<&BorderRadius>,
Option<&Outline>,
Option<&ScrollPosition>,
)>,
children_query: &Query<&Children>,
inverse_target_scale_factor: f32,
parent_size: Vec2,
parent_scroll_position: Vec2,
mut absolute_location: Vec2,
) {
if let Ok((mut node, mut transform, maybe_border_radius, maybe_outline)) =
node_transform_query.get_mut(entity)
if let Ok((
mut node,
mut transform,
style,
maybe_border_radius,
maybe_outline,
maybe_scroll_position,
)) = node_transform_query.get_mut(entity)
{
let Ok(layout) = ui_surface.get_layout(entity) else {
return;
};

let layout_size =
inverse_target_scale_factor * Vec2::new(layout.size.width, layout.size.height);
let layout_location =
Expand All @@ -315,7 +335,8 @@ pub fn ui_layout_system(
- approx_round_layout_coords(absolute_location);

let rounded_location =
approx_round_layout_coords(layout_location) + 0.5 * (rounded_size - parent_size);
approx_round_layout_coords(layout_location - parent_scroll_position)
+ 0.5 * (rounded_size - parent_size);

// only trigger change detection when the new values are different
if node.calculated_size != rounded_size || node.unrounded_size != layout_size {
Expand Down Expand Up @@ -351,16 +372,48 @@ pub fn ui_layout_system(
transform.translation = rounded_location.extend(0.);
}

let scroll_position: Vec2 = maybe_scroll_position
.map(|scroll_pos| {
Vec2::new(
if style.overflow.x == OverflowAxis::Scroll {
scroll_pos.offset_x
} else {
0.0
},
if style.overflow.y == OverflowAxis::Scroll {
scroll_pos.offset_y
} else {
0.0
},
)
})
.unwrap_or_default();

let round_content_size = approx_round_layout_coords(
Vec2::new(layout.content_size.width, layout.content_size.height)
* inverse_target_scale_factor,
);
let max_possible_offset = (round_content_size - rounded_size).max(Vec2::ZERO);
let clamped_scroll_position = scroll_position.clamp(Vec2::ZERO, max_possible_offset);

if clamped_scroll_position != scroll_position {
commands
.entity(entity)
.insert(ScrollPosition::from(&clamped_scroll_position));
}

if let Ok(children) = children_query.get(entity) {
for &child_uinode in children {
update_uinode_geometry_recursive(
commands,
child_uinode,
ui_surface,
Some(viewport_size),
node_transform_query,
children_query,
inverse_target_scale_factor,
rounded_size,
clamped_scroll_position,
absolute_location,
);
}
Expand Down
6 changes: 4 additions & 2 deletions crates/bevy_ui/src/node_bundles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
use crate::widget::TextFlags;
use crate::{
widget::{Button, UiImageSize},
BackgroundColor, BorderColor, BorderRadius, ContentSize, FocusPolicy, Interaction, Node, Style,
UiImage, UiMaterial, ZIndex,
BackgroundColor, BorderColor, BorderRadius, ContentSize, FocusPolicy, Interaction, Node,
ScrollPosition, Style, UiImage, UiMaterial, ZIndex,
};
use bevy_asset::Handle;
#[cfg(feature = "bevy_text")]
Expand Down Expand Up @@ -38,6 +38,8 @@ pub struct NodeBundle {
pub border_radius: BorderRadius,
/// Whether this node should block interaction with lower nodes
pub focus_policy: FocusPolicy,
/// The scroll position of the node,
pub scroll_position: ScrollPosition,
/// The transform of the node
///
/// This component is automatically managed by the UI layout system.
Expand Down
66 changes: 66 additions & 0 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,47 @@ impl Default for Node {
}
}

/// The scroll position of the node.
/// Updating the values of `ScrollPosition` will reposition the children of the node by the offset amount.
/// `ScrollPosition` may be updated by the layout system when a layout change makes a previously valid `ScrollPosition` invalid.
/// Changing this does nothing on a `Node` without a `Style` setting at least one `OverflowAxis` to `OverflowAxis::Scroll`.
#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component, Default)]
pub struct ScrollPosition {
/// How far across the node is scrolled, in pixels. (0 = not scrolled / scrolled to right)
pub offset_x: f32,
/// How far down the node is scrolled, in pixels. (0 = not scrolled / scrolled to top)
pub offset_y: f32,
}

impl ScrollPosition {
pub const DEFAULT: Self = Self {
offset_x: 0.0,
offset_y: 0.0,
};
}

impl Default for ScrollPosition {
fn default() -> Self {
Self::DEFAULT
}
}

impl From<&ScrollPosition> for Vec2 {
fn from(scroll_pos: &ScrollPosition) -> Self {
Vec2::new(scroll_pos.offset_x, scroll_pos.offset_y)
}
}

impl From<&Vec2> for ScrollPosition {
fn from(vec: &Vec2) -> Self {
ScrollPosition {
offset_x: vec.x,
offset_y: vec.y,
}
}
}

/// Describes the style of a UI container node
///
/// Nodes can be laid out using either Flexbox or CSS Grid Layout.
Expand Down Expand Up @@ -865,6 +906,29 @@ impl Overflow {
pub const fn is_visible(&self) -> bool {
self.x.is_visible() && self.y.is_visible()
}

pub const fn scroll() -> Self {
Self {
x: OverflowAxis::Scroll,
y: OverflowAxis::Scroll,
}
}

/// Scroll overflowing items on the x axis
pub const fn scroll_x() -> Self {
Self {
x: OverflowAxis::Scroll,
y: OverflowAxis::Visible,
}
}

/// Scroll overflowing items on the y axis
pub const fn scroll_y() -> Self {
Self {
x: OverflowAxis::Visible,
y: OverflowAxis::Scroll,
}
}
}

impl Default for Overflow {
Expand All @@ -888,6 +952,8 @@ pub enum OverflowAxis {
Clip,
/// Hide overflowing items by influencing layout and then clipping.
Hidden,
/// Scroll overflowing items.
Scroll,
}

impl OverflowAxis {
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,7 @@ Example | Description
[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
[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
[Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node.
[Text](../examples/ui/text.rs) | Illustrates creating and updating text
[Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout
Expand Down
Loading