Skip to content

Robust Scrolling Example #19586

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
169 changes: 98 additions & 71 deletions examples/ui/scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,99 @@ fn main() {
app.add_plugins(DefaultPlugins)
.insert_resource(WinitSettings::desktop_app())
.add_systems(Startup, setup)
.add_systems(Update, update_scroll_position);
.add_systems(Update, send_scroll_events)
.add_observer(on_scroll_handler);

app.run();
}

const FONT_SIZE: f32 = 20.;
const LINE_HEIGHT: f32 = 21.;

/// Injects scroll events into the UI hierarchy.
fn send_scroll_events(
mut mouse_wheel_events: EventReader<MouseWheel>,
hover_map: Res<HoverMap>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut commands: Commands,
) {
for event in mouse_wheel_events.read() {
let mut delta = -Vec2::new(event.x, event.y);

if event.unit == MouseScrollUnit::Line {
delta *= LINE_HEIGHT;
}

if keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]) {
std::mem::swap(&mut delta.x, &mut delta.y);
}

for pointer_map in hover_map.values() {
for entity in pointer_map.keys() {
commands.trigger_targets(Scroll { delta }, *entity);
}
}
}
}

/// UI scrolling event.
#[derive(Event, EntityEvent, Debug)]
#[entity_event(auto_propagate, traversal = &'static ChildOf)]
struct Scroll {
/// Scroll delta in logical coordinates.
delta: Vec2,
}

fn on_scroll_handler(
mut trigger: On<Scroll>,
mut query: Query<(&mut ScrollPosition, &Node, &ComputedNode)>,
) {
let target = trigger.target();
let delta = &mut trigger.event_mut().delta;

let Ok((mut scroll_position, node, computed)) = query.get_mut(target) else {
return;
};

let max_offset = (computed.content_size() - computed.size()) * computed.inverse_scale_factor();

if node.overflow.x == OverflowAxis::Scroll && delta.x != 0. {
// Is this node already scrolled all the way in the direction of the scroll?
let max = if delta.x > 0. {
scroll_position.x >= max_offset.x
} else {
scroll_position.x <= 0.
};

if !max {
scroll_position.x += delta.x;
// Consume the X portion of the scroll delta.
delta.x = 0.;
}
}

if node.overflow.y == OverflowAxis::Scroll && delta.y != 0. {
// Is this node already scrolled all the way in the direction of the scroll?
let max = if delta.y > 0. {
scroll_position.y >= max_offset.y
} else {
scroll_position.y <= 0.
};

if !max {
scroll_position.y += delta.y;
// Consume the Y portion of the scroll delta.
delta.y = 0.;
}
}

// Stop propagating when the delta is fully consumed.
if *delta == Vec2::ZERO {
trigger.propagate(false);
}
}

const FONT_SIZE: f32 = 20.;

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// Camera
commands.spawn((Camera2d, IsDefaultUiCamera));
Expand All @@ -39,7 +124,6 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
flex_direction: FlexDirection::Column,
..default()
})
.insert(Pickable::IGNORE)
.with_children(|parent| {
// horizontal scroll example
parent
Expand Down Expand Up @@ -83,16 +167,12 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
},
Label,
AccessibilityNode(Accessible::new(Role::ListItem)),
Node {
min_width: Val::Px(200.),
align_content: AlignContent::Center,
..default()
},
))
.insert(Node {
min_width: Val::Px(200.),
align_content: AlignContent::Center,
..default()
})
.insert(Pickable {
should_block_lower: false,
..default()
})
.observe(
|trigger: On<Pointer<Press>>, mut commands: Commands| {
if trigger.event().button == PointerButton::Primary {
Expand Down Expand Up @@ -159,10 +239,6 @@ fn vertically_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
max_height: Val::Px(LINE_HEIGHT),
..default()
},
Pickable {
should_block_lower: false,
..default()
},
children![(
Text(format!("Item {i}")),
TextFont {
Expand All @@ -171,10 +247,6 @@ fn vertically_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
},
Label,
AccessibilityNode(Accessible::new(Role::ListItem)),
Pickable {
should_block_lower: false,
..default()
}
)],
)
})))
Expand Down Expand Up @@ -217,7 +289,6 @@ fn bidirectional_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
flex_direction: FlexDirection::Row,
..default()
},
Pickable::IGNORE,
Children::spawn(SpawnIter((0..10).map({
let value = font_handle.clone();
move |i| {
Expand All @@ -229,10 +300,6 @@ fn bidirectional_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
},
Label,
AccessibilityNode(Accessible::new(Role::ListItem)),
Pickable {
should_block_lower: false,
..default()
},
)
}
}))),
Expand Down Expand Up @@ -264,45 +331,38 @@ fn nested_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
Label,
),
(
// Outer, horizontal scrolling container
// Outer, bi-directional scrolling container
Node {
column_gap: Val::Px(20.),
flex_direction: FlexDirection::Row,
align_self: AlignSelf::Stretch,
height: Val::Percent(50.),
overflow: Overflow::scroll_x(), // n.b.
overflow: Overflow::scroll(),
..default()
},
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
// Inner, scrolling columns
Children::spawn(SpawnIter((0..30).map(move |oi| {
Children::spawn(SpawnIter((0..5).map(move |oi| {
(
Node {
flex_direction: FlexDirection::Column,
align_self: AlignSelf::Stretch,
height: Val::Percent(200. / 5. * (oi as f32 + 1.)),
overflow: Overflow::scroll_y(),
..default()
},
BackgroundColor(Color::srgb(0.05, 0.05, 0.05)),
Pickable {
should_block_lower: false,
..default()
},
Children::spawn(SpawnIter((0..30).map({
Children::spawn(SpawnIter((0..20).map({
let value = font_handle.clone();
move |i| {
(
Text(format!("Item {}", (oi * 25) + i)),
Text(format!("Item {}", (oi * 20) + i)),
TextFont {
font: value.clone(),
..default()
},
Label,
AccessibilityNode(Accessible::new(Role::ListItem)),
Pickable {
should_block_lower: false,
..default()
},
)
}
}))),
Expand All @@ -312,36 +372,3 @@ fn nested_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
],
)
}

/// Updates the scroll position of scrollable nodes in response to mouse input
pub fn update_scroll_position(
mut mouse_wheel_events: EventReader<MouseWheel>,
hover_map: Res<HoverMap>,
mut scrolled_node_query: Query<&mut ScrollPosition>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
for mouse_wheel_event in mouse_wheel_events.read() {
let (mut dx, mut dy) = match mouse_wheel_event.unit {
MouseScrollUnit::Line => (
mouse_wheel_event.x * LINE_HEIGHT,
mouse_wheel_event.y * LINE_HEIGHT,
),
MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y),
};

if keyboard_input.pressed(KeyCode::ControlLeft)
|| keyboard_input.pressed(KeyCode::ControlRight)
{
std::mem::swap(&mut dx, &mut dy);
}

for (_pointer, pointer_map) in hover_map.iter() {
for (entity, _hit) in pointer_map.iter() {
if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) {
scroll_position.x -= dx;
scroll_position.y -= dy;
}
}
}
}
}