From 9b84bdf60ae2feb5ac97a1955a45b0d6fc89ab90 Mon Sep 17 00:00:00 2001 From: Sean Ryan Date: Mon, 16 Sep 2024 12:50:36 -0500 Subject: [PATCH] port #8104 to main --- crates/bevy_ui/src/layout/convert.rs | 1 + crates/bevy_ui/src/layout/mod.rs | 17 +++++++-- crates/bevy_ui/src/lib.rs | 11 +++++- crates/bevy_ui/src/node_bundles.rs | 6 +-- crates/bevy_ui/src/ui_node.rs | 55 ++++++++++++++++++++++++++++ crates/bevy_ui/src/update.rs | 55 +++++++++++++++++++++++++--- examples/ui/ui.rs | 44 ++++------------------ 7 files changed, 139 insertions(+), 50 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 240f899656f03..f936855c11bab 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -258,6 +258,7 @@ impl From 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, } } } diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 269af520b1e8f..b805f25c9c42a 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,5 +1,5 @@ use crate::{ - BorderRadius, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiScale, + BorderRadius, ContentSize, DefaultUiCamera, Node, Outline, ScrollPosition, Style, TargetCamera, UiScale }; use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, @@ -117,6 +117,7 @@ pub fn ui_layout_system( &mut Transform, Option<&BorderRadius>, Option<&Outline>, + Option<&ScrollPosition>, )>, #[cfg(feature = "bevy_text")] mut buffer_query: Query<&mut CosmicBuffer>, #[cfg(feature = "bevy_text")] mut text_pipeline: ResMut, @@ -276,6 +277,7 @@ pub fn ui_layout_system( inverse_target_scale_factor, Vec2::ZERO, Vec2::ZERO, + Vec2::ZERO, ); } @@ -292,13 +294,15 @@ pub fn ui_layout_system( &mut Transform, 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)) = + if let Ok((mut node, mut transform, maybe_border_radius, maybe_outline, maybe_scroll_position)) = node_transform_query.get_mut(entity) { let Ok(layout) = ui_surface.get_layout(entity) else { @@ -310,12 +314,12 @@ pub fn ui_layout_system( inverse_target_scale_factor * Vec2::new(layout.location.x, layout.location.y); absolute_location += layout_location; - + let rounded_size = approx_round_layout_coords(absolute_location + layout_size) - 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 { @@ -351,6 +355,10 @@ pub fn ui_layout_system( transform.translation = rounded_location.extend(0.); } + let scroll_position: Vec2 = maybe_scroll_position + .map(|scroll_pos| scroll_pos.into()) + .unwrap_or_else(Vec2::default); + if let Ok(children) = children_query.get(entity) { for &child_uinode in children { update_uinode_geometry_recursive( @@ -361,6 +369,7 @@ pub fn ui_layout_system( children_query, inverse_target_scale_factor, rounded_size, + scroll_position, absolute_location, ); } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index f55e875de22c4..31ab92ce95e62 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -67,7 +67,7 @@ use bevy_transform::TransformSystem; use layout::ui_surface::UiSurface; use stack::ui_stack_system; pub use stack::UiStack; -use update::{update_clipping_system, update_target_camera_system}; +use update::{update_clipping_system, update_scroll_position, update_target_camera_system}; /// The basic plugin for Bevy UI #[derive(Default)] @@ -80,6 +80,10 @@ pub enum UiSystem { /// /// Runs in [`PreUpdate`]. Focus, + /// After this label, scroll positions will have been updated for UI entities. + /// + /// Runs in [`PreUpdate`]. + Scroll, /// All UI systems in [`PostUpdate`] will run in or after this label. Prepare, /// After this label, the ui layout state has been updated. @@ -158,7 +162,10 @@ impl Plugin for UiPlugin { ) .add_systems( PreUpdate, - ui_focus_system.in_set(UiSystem::Focus).after(InputSystem), + ( + ui_focus_system.in_set(UiSystem::Focus).after(InputSystem), + update_scroll_position.in_set(UiSystem::Scroll).after(UiSystem::Focus), + ) ); app.add_systems( diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index a093cf13cf81a..eb6359a561eac 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -3,9 +3,7 @@ #[cfg(feature = "bevy_text")] use crate::widget::TextFlags; use crate::{ - widget::{Button, UiImageSize}, - BackgroundColor, BorderColor, BorderRadius, ContentSize, FocusPolicy, Interaction, Node, Style, - UiImage, UiMaterial, ZIndex, + widget::{Button, UiImageSize}, BackgroundColor, BorderColor, BorderRadius, ContentSize, FocusPolicy, Interaction, Node, ScrollPosition, Style, UiImage, UiMaterial, ZIndex }; use bevy_asset::Handle; #[cfg(feature = "bevy_text")] @@ -38,6 +36,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. diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 9fbe8c4437c38..59b341e726c14 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -150,6 +150,36 @@ impl Default for Node { } } + +/// The scroll position on the node +#[derive(Component, Debug, Clone, Reflect)] +#[reflect(Component, Default)] +pub struct ScrollPosition { + /// How far accross the node is scrolled (0 = not scrolled / scrolled to right) + pub offset_x: f32, + /// How far down the node is scrolled (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) + } +} + /// Describes the style of a UI container node /// /// Nodes can be laid out using either Flexbox or CSS Grid Layout. @@ -865,6 +895,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 { @@ -888,6 +941,8 @@ pub enum OverflowAxis { Clip, /// Hide overflowing items by influencing layout and then clipping. Hidden, + /// Scroll overflowing items. + Scroll, } impl OverflowAxis { diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index e93e8214b97c8..7cc36370b563a 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -1,15 +1,15 @@ //! This module contains systems that update the UI when something changes -use crate::{CalculatedClip, Display, OverflowAxis, Style, TargetCamera}; +use crate::{CalculatedClip, Display, OverflowAxis, ScrollPosition, Style, TargetCamera}; use super::Node; use bevy_ecs::{ - entity::Entity, - query::{Changed, With, Without}, - system::{Commands, Query}, + entity::Entity, event::EventReader, query::{Changed, With, Without}, system::{Commands, Query, Res} }; use bevy_hierarchy::{Children, Parent}; -use bevy_math::Rect; +use bevy_input::mouse::{MouseScrollUnit, MouseWheel}; +use bevy_math::{Rect, Vec2}; +use bevy_picking::focus::HoverMap; use bevy_transform::components::GlobalTransform; use bevy_utils::HashSet; @@ -181,3 +181,48 @@ fn update_children_target_camera( ); } } + +pub fn update_scroll_position( + mut mouse_wheel_events: EventReader, + hover_map: Res, + mut scrolled_node_query: Query<(&mut ScrollPosition, &Style, &Children, &Node)>, + just_node_query: Query<&Node>, +) { + for mouse_wheel_event in mouse_wheel_events.read() { + // TODO: 90% sure this should be user-configurable, bevy shouldn't own scroll speed + let (dx, dy) = match mouse_wheel_event.unit { + MouseScrollUnit::Line => (mouse_wheel_event.x * 20., mouse_wheel_event.y * 20.), + MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y), + }; + + for (_pointer, pointer_map) in hover_map.iter() { + for (entity, _hit) in pointer_map.iter() { + if let Ok((mut scroll_position, style, children, scrolled_node)) = scrolled_node_query.get_mut(*entity) { + let Vec2 { + x: container_width, + y: container_height, + } = scrolled_node.size(); + + let (items_width, items_height): (f32, f32) = + children.iter().fold((0.0, 0.0), |sum, child| { + let size = just_node_query.get(*child).unwrap().size(); + (sum.0 + size.x, sum.1 + size.y) + }); + + if style.overflow.x == OverflowAxis::Scroll { + let max_scroll_x = (items_width - container_width).max(0.); + scroll_position.offset_x = + (scroll_position.offset_x + dx).clamp(-max_scroll_x, 0.); + } + if style.overflow.y == OverflowAxis::Scroll { + let max_scroll_y = (items_height - container_height).max(0.); + scroll_position.offset_y = + (scroll_position.offset_y + dy).clamp(-max_scroll_y, 0.); + } + + println!("new scroll possy: {:?} | scroll node size {:?} | items_w {:?} items_h {:?}", scroll_position, scrolled_node.size(), items_width, items_height); + } + } + } + } +} diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index fec5bb825f9d1..7564f7836bac7 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -16,8 +16,7 @@ fn main() { app.add_plugins(DefaultPlugins) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) - .add_systems(Startup, setup) - .add_systems(Update, mouse_scroll); + .add_systems(Startup, setup); #[cfg(feature = "bevy_dev_tools")] { @@ -43,6 +42,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }) + .insert(Pickable::IGNORE) .with_children(|parent| { // left vertical fill (border) parent @@ -122,7 +122,6 @@ fn setup(mut commands: Commands, asset_server: Res) { width: Val::Px(200.), ..default() }, - background_color: Color::srgb(0.15, 0.15, 0.15).into(), ..default() }) .with_children(|parent| { @@ -138,14 +137,14 @@ fn setup(mut commands: Commands, asset_server: Res) { ), Label, )); - // List with hidden overflow + // Scrolling list parent .spawn(NodeBundle { style: Style { flex_direction: FlexDirection::Column, align_self: AlignSelf::Stretch, height: Val::Percent(50.), - overflow: Overflow::clip_y(), + overflow: Overflow::scroll_y(), ..default() }, background_color: Color::srgb(0.10, 0.10, 0.10).into(), @@ -163,12 +162,12 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }, - ScrollingList::default(), AccessibilityNode(NodeBuilder::new(Role::List)), )) + .insert(Pickable::IGNORE) .with_children(|parent| { // List items - for i in 0..30 { + for i in 0..100 { parent.spawn(( TextBundle::from_section( format!("Item {i}"), @@ -185,6 +184,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }); }); }); + parent .spawn(NodeBundle { style: Style { @@ -224,6 +224,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }) + .insert(Pickable::IGNORE) .with_children(|parent| { parent .spawn(NodeBundle { @@ -336,35 +337,6 @@ fn setup(mut commands: Commands, asset_server: Res) { }); } -#[derive(Component, Default)] -struct ScrollingList { - position: f32, -} - -fn mouse_scroll( - mut mouse_wheel_events: EventReader, - mut query_list: Query<(&mut ScrollingList, &mut Style, &Parent, &Node)>, - query_node: Query<&Node>, -) { - for mouse_wheel_event in mouse_wheel_events.read() { - for (mut scrolling_list, mut style, parent, list_node) in &mut query_list { - let items_height = list_node.size().y; - let container_height = query_node.get(parent.get()).unwrap().size().y; - - let max_scroll = (items_height - container_height).max(0.); - - let dy = match mouse_wheel_event.unit { - MouseScrollUnit::Line => mouse_wheel_event.y * 20., - MouseScrollUnit::Pixel => mouse_wheel_event.y, - }; - - scrolling_list.position += dy; - scrolling_list.position = scrolling_list.position.clamp(-max_scroll, 0.); - style.top = Val::Px(scrolling_list.position); - } - } -} - #[cfg(feature = "bevy_dev_tools")] // The system that will enable/disable the debug outlines around the nodes fn toggle_overlay(