Skip to content
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
171 changes: 171 additions & 0 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,75 @@ impl ComputedNode {

clip_rect
}

const fn compute_thumb(
gutter_min: f32,
content_length: f32,
gutter_length: f32,
scroll_position: f32,
) -> [f32; 2] {
if content_length <= gutter_length {
return [gutter_min, gutter_min + gutter_length];
}
let thumb_len = gutter_length * gutter_length / content_length;
let thumb_min = gutter_min
+ scroll_position / (content_length - gutter_length) * (gutter_length - thumb_len);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have the option to simplify this to:

Suggested change
+ scroll_position / (content_length - gutter_length) * (gutter_length - thumb_len);
+ scroll_position * gutter_length / content_length;

If you find it too verbose
Screenshot 2025-11-28 at 6 25 32 PM
I understand if you want to keep it written the way you have it since they’re equivalent / you may find it clearer the way you wrote it.

At the very least, I think it’s easier to read the line was changed to have the multiplicative term first i.e.

Suggested change
+ scroll_position / (content_length - gutter_length) * (gutter_length - thumb_len);
+ scroll_position * (gutter_length - thumb_len) / (content_length - gutter_length);

so it can be seen as multiplying by the ratio of “scrollable length in scrollbar” to “original hidden content length"

The new tests you wrote pass for me when I replace with the first suggested line.

[thumb_min, thumb_min + thumb_len]
}

/// Compute the bounds of the horizontal scrollbar and the thumb
/// in object-centered coordinates.
pub fn horizontal_scrollbar(&self) -> Option<(Rect, [f32; 2])> {
if self.scrollbar_size.y <= 0. {
return None;
}
let content_inset = self.content_inset();
let half_size = 0.5 * self.size;
let min_x = -half_size.x + content_inset.left;
let max_x = half_size.x - content_inset.right - self.scrollbar_size.x;
Copy link
Contributor

@kfc35 kfc35 Nov 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These will probably have to be changed dependent on the timing of your PR titled "UI scrollbars clipping fix" being merged #21910 , so that these subtractions of scrollbar_size can be removed when applicable

let max_y = half_size.y - content_inset.bottom;
let min_y = max_y - self.scrollbar_size.y;
let gutter = Rect {
min: Vec2::new(min_x, min_y),
max: Vec2::new(max_x, max_y),
};
Some((
gutter,
Self::compute_thumb(
gutter.min.x,
self.content_size.x,
gutter.size().x,
self.scroll_position.x,
),
))
}

/// Compute the bounds of the horizontal scrollbar and the thumb
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Compute the bounds of the horizontal scrollbar and the thumb
/// Compute the bounds of the vertical scrollbar and the thumb

/// in object-centered coordinates.
pub fn vertical_scrollbar(&self) -> Option<(Rect, [f32; 2])> {
if self.scrollbar_size.x <= 0. {
return None;
}
let content_inset = self.content_inset();
let half_size = 0.5 * self.size;
let max_x = half_size.x - content_inset.right;
let min_x = max_x - self.scrollbar_size.x;
let min_y = -half_size.y + content_inset.top;
let max_y = half_size.y - content_inset.bottom - self.scrollbar_size.y;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here about your other PR and the timing of merges for the removal of - self.scrollbar_size.y

let gutter = Rect {
min: Vec2::new(min_x, min_y),
max: Vec2::new(max_x, max_y),
};
Some((
gutter,
Self::compute_thumb(
gutter.min.y,
self.content_size.y,
gutter.size().y,
self.scroll_position.y,
),
))
}
}

impl ComputedNode {
Expand Down Expand Up @@ -2916,6 +2985,10 @@ impl ComputedUiRenderTargetInfo {

#[cfg(test)]
mod tests {
use bevy_math::Rect;
use bevy_math::Vec2;

use crate::ComputedNode;
use crate::GridPlacement;

#[test]
Expand Down Expand Up @@ -2943,4 +3016,102 @@ mod tests {
assert_eq!(GridPlacement::start_span(3, 5).get_end(), None);
assert_eq!(GridPlacement::end_span(-4, 12).get_start(), None);
}

#[test]
fn computed_node_both_scrollbars() {
let node = ComputedNode {
size: Vec2::splat(100.),
scrollbar_size: Vec2::splat(10.),
content_size: Vec2::splat(100.),
..Default::default()
};

let (gutter, thumb) = node.horizontal_scrollbar().unwrap();
assert_eq!(
gutter,
Rect {
min: Vec2::new(-50., 40.),
max: Vec2::new(40., 50.)
}
);
assert_eq!(thumb, [-50., 31.]);

let (gutter, thumb) = node.vertical_scrollbar().unwrap();
assert_eq!(
gutter,
Rect {
min: Vec2::new(40., -50.),
max: Vec2::new(50., 40.)
}
);
assert_eq!(thumb, [-50., 31.]);
}

#[test]
fn computed_node_single_horizontal_scrollbar() {
let mut node = ComputedNode {
size: Vec2::splat(100.),
scrollbar_size: Vec2::new(0., 10.),
content_size: Vec2::new(200., 100.),
scroll_position: Vec2::new(0., 0.),
..Default::default()
};

assert_eq!(None, node.vertical_scrollbar());

let (gutter, thumb) = node.horizontal_scrollbar().unwrap();
assert_eq!(
gutter,
Rect {
min: Vec2::new(-50., 40.),
max: Vec2::new(50., 50.)
}
);
assert_eq!(thumb, [-50., 0.]);

node.scroll_position.x += 100.;
let (gutter, thumb) = node.horizontal_scrollbar().unwrap();
assert_eq!(
gutter,
Rect {
min: Vec2::new(-50., 40.),
max: Vec2::new(50., 50.)
}
);
assert_eq!(thumb, [0., 50.]);
}

#[test]
fn computed_node_single_vertical_scrollbar() {
let mut node = ComputedNode {
size: Vec2::splat(100.),
scrollbar_size: Vec2::new(10., 0.),
content_size: Vec2::new(100., 200.),
scroll_position: Vec2::new(0., 0.),
..Default::default()
};

assert_eq!(None, node.horizontal_scrollbar());

let (gutter, thumb) = node.vertical_scrollbar().unwrap();
assert_eq!(
gutter,
Rect {
min: Vec2::new(40., -50.),
max: Vec2::new(50., 50.)
}
);
assert_eq!(thumb, [-50., 0.]);

node.scroll_position.y += 100.;
let (gutter, thumb) = node.vertical_scrollbar().unwrap();
assert_eq!(
gutter,
Rect {
min: Vec2::new(40., -50.),
max: Vec2::new(50., 50.)
}
);
assert_eq!(thumb, [0., 50.]);
}
}