bevyengine / bevyengine/bevy

ScrollbarThumb does not respect Overflow::clip()

Open
#25,075 0 comments 0 reactions 0 assignees View on GitHub
A-UI C-Bug S-Needs-Investigation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 16h
Merged PRs (30d)
171

Description

## Bevy version and features

0.19.0

## What you did

Wrapped the scrollbar example in an Overflow::clip() node.

```rs
//! Demonstrations of scrolling and scrollbars.

use bevy::{
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
input_focus::tab_navigation::{TabGroup, TabNavigationPlugin},
picking::hover::Hovered,
prelude::*,
ui_widgets::{ControlOrientation, Scrollbar, ScrollbarDragState, ScrollbarThumb},
};

fn main() {
App::new()
.add_plugins((DefaultPlugins, TabNavigationPlugin))
.insert_resource(UiScale(1.25))
.add_systems(Startup, setup_view_root)
.add_systems(Update, update_scrollbar_thumb)
.run();
}

fn setup_view_root(mut commands: Commands) {
let camera = commands.spawn((Camera::default(), Camera2d)).id();

commands.spawn((
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
position_type: PositionType::Absolute,
left: px(0),
top: px(0),
right: px(0),
bottom: px(0),
padding: UiRect::all(px(3)),
row_gap: px(6),
..Default::default()
},
BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
UiTargetCamera(camera),
TabGroup::default(),
Children::spawn((
Spawn(Text::new("Scrolling")),
// BUG: wrap the scroll demo in a shorter `overflow: clip` box.
Spawn((
Node {
height: px(90),
border: px(2).all(),
overflow: Overflow::clip(),
..default()
},
BorderColor::all(Color::WHITE),
children![scroll_area_demo()],
)),
)),
));
}

/// Create a scrolling area.
///
/// The "scroll area" is a container that can be scrolled. It has a nested structure which is
/// three levels deep:
/// - The outermost node is a grid that contains the scroll area and the scrollbars.
/// - The scroll area is a flex container that contains the scrollable content. This
/// is the element that has the `overflow: scroll` property.
/// - The scrollable content consists of the elements actually displayed in the scrolling area.
fn scroll_area_demo() -> impl Bundle {
(
// Frame element which contains the scroll area and scrollbars.
Node {
display: Display::Grid,
width: px(200),
height: px(150),
grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
row_gap: px(2),
column_gap: px(2),
..default()
},
Children::spawn((SpawnWith(|parent: &mut RelatedSpawner| {
// The actual scrolling area.
// Note that we're using `SpawnWith` here because we need to get the entity id of the
// scroll area in order to set the target of the scrollbars.
let scroll_area_id = parent
.spawn((
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
padding: UiRect::all(px(4)),
overflow: Overflow::scroll(),
..default()
},
BackgroundColor(colors::GRAY1.into()),
ScrollPosition(Vec2::new(0.0, 10.0)),
Children::spawn((
// The actual content of the scrolling area
Spawn(text_row("Alpha Wolf")),
Spawn(text_row("Beta Blocker")),
Spawn(text_row("Delta Sleep")),
Spawn(text_row("Gamma Ray")),
Spawn(text_row("Epsilon Eridani")),
Spawn(text_row("Zeta Function")),
Spawn(text_row("Lambda Calculus")),
Spawn(text_row("Nu Metal")),
Spawn(text_row("Pi Day")),
Spawn(text_row("Chi Pants")),
Spawn(text_row("Psi Powers")),
Spawn(text_row("Omega Fatty Acid")),
)),
))
.id();

// Vertical scrollbar
parent.spawn((
Node {
min_width: px(8),
grid_row: GridPlacement::start(1),
grid_column: GridPlacement::start(2),
..default()
},
Scrollbar {
orientation: ControlOrientation::Vertical,
target: scroll_area_id,
min_thumb_length: 8.0,
},
Children::spawn(Spawn((
Hovered::default(),
BackgroundColor(colors::GRAY2.into()),
BorderColor::all(colors::GRAY3),
ScrollbarThumb {
border_radius: BorderRadius::all(px(4)),
border: px(1).all(),
},
))),
));

// Horizontal scrollbar
parent.spawn((
Node {
min_height: px(8),
grid_row: GridPlacement::start(2),
grid_column: GridPlacement::start(1),
..default()
},
Scrollbar {
orientation: ControlOrientation::Horizontal,
target: scroll_area_id,
min_thumb_length: 8.0,
},
Children::spawn(Spawn((
Hovered::default(),
BackgroundColor(colors::GRAY2.into()),
BorderColor::all(colors::GRAY3),
ScrollbarThumb {
border_radius: BorderRadius::all(px(4)),
border: px(1).all(),
},
))),
));
}),)),
)
}

/// Create a list row
fn text_row(caption: &str) -> impl Bundle {
(
Text::new(caption),
TextFont {
font_size: FontSize::Px(14.0),
..default()
},
)
}

// Update the color of the scrollbar thumb.
fn update_scrollbar_thumb(
mut q_thumb: Query<
(&mut BackgroundColor, &Hovered, &ScrollbarDragState),
(
With,
Or<(Changed, Changed)>,
),
>,
) {
for (mut thumb_bg, Hovered(is_hovering), drag) in q_thumb.iter_mut() {
let color: Color = if *is_hovering || drag.dragging {
// If hovering, use a lighter color
colors::GRAY4
} else {
// Default color for the slider
colors::GRAY2
}
.into();

if thumb_bg.0 != color {
// Update the color of the thumb
thumb_bg.0 = color;
}
}
}

mod colors {
use bevy::color::Srgba;

pub const GRAY1: Srgba = Srgba::new(0.224, 0.224, 0.243, 1.0);
pub const GRAY2: Srgba = Srgba::new(0.486, 0.486, 0.529, 1.0);
pub const GRAY3: Srgba = Srgba::new(0.71, 0.71, 0.772, 1.0);
pub const GRAY4: Srgba = Srgba::new(1.0, 1.0, 1.0, 1.0);
}
```

This is the diff to the official example:

```diff
39c39,52
< Children::spawn((Spawn(Text::new("Scrolling")), Spawn(scroll_area_demo()))),
---
> Children::spawn((
> Spawn(Text::new("Scrolling")),
> // BUG: wrap the scroll demo in a shorter `overflow: clip` box.
> Spawn((
> Node {
> height: px(90),
> border: px(2).all(),
> overflow: Overflow::clip(),
> ..default()
> },
> BorderColor::all(Color::WHITE),
> children![scroll_area_demo()],
> )),
> )),
```

## What went wrong

The scrollbar thumbs are rendered outside of the Overflow::clip() node:

Image

Contributor guide

Open the contributing guide

Research direction

Reproduce the modified official scrolling and scrollbar example with the outer Node using Overflow::clip() and compare it with the unmodified example. Start by tracing how ScrollbarThumb is laid out and rendered relative to the clipping node. Done means the scrollbar thumbs remain clipped within the outer node without breaking scrolling or scrollbar interaction.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
62/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.