From cd579db7592989fbf006d3d78bb47605f8494e07 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 21 Nov 2025 21:26:08 +0000 Subject: [PATCH 1/8] Added functions to get the scrollbar bounds and thumb position from `ComputedNode`. --- crates/bevy_ui/src/ui_node.rs | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index ac086d02a5556..5d434aafafba1 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -298,6 +298,50 @@ impl ComputedNode { clip_rect } + + /// Compute the bounds of the horizontal scrollbar and the thumb + /// in object-centered coordinates. + pub fn horizontal_scrollbar(&self) -> Option<(Rect, f32, f32)> { + 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; + 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), + }; + let gutter_length = gutter.size().x; + let thumb_min = gutter.min.x + gutter_length * self.scroll_position.x / self.content_size.x; + let thumb_max = thumb_min + gutter_length * gutter_length / self.content_size.x; + (gutter, thumb_min, thumb_max) + } + + /// Compute the bounds of the horizontal scrollbar and the thumb + /// in object-centered coordinates. + pub fn vertical_scrollbar(&self) -> Option { + 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; + let gutter = Rect { + min: Vec2::new(min_x, min_y), + max: Vec2::new(max_x, max_y), + }; + let gutter_length = gutter.size().y; + let thumb_min = gutter.min.y + gutter_length * self.scroll_position.y / self.content_size.y; + let thumb_max = thumb_min + gutter_length * gutter_length / self.content_size.y; + (gutter, thumb_min, thumb_max) + } } impl ComputedNode { From f8dbe315fbc209df7ee76e59681238d961fad2b2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 13:43:53 +0000 Subject: [PATCH 2/8] Added `compute_thumb` helper and some tests. --- crates/bevy_ui/src/ui_node.rs | 138 +++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 5d434aafafba1..ca643d0af1147 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -299,9 +299,25 @@ 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); + let thumb_max = thumb_min + thumb_len; + [thumb_min, thumb_max] + } + /// Compute the bounds of the horizontal scrollbar and the thumb /// in object-centered coordinates. - pub fn horizontal_scrollbar(&self) -> Option<(Rect, f32, f32)> { + pub fn horizontal_scrollbar(&self) -> Option<(Rect, [f32; 2])> { if self.scrollbar_size.y <= 0. { return None; } @@ -315,15 +331,20 @@ impl ComputedNode { min: Vec2::new(min_x, min_y), max: Vec2::new(max_x, max_y), }; - let gutter_length = gutter.size().x; - let thumb_min = gutter.min.x + gutter_length * self.scroll_position.x / self.content_size.x; - let thumb_max = thumb_min + gutter_length * gutter_length / self.content_size.x; - (gutter, thumb_min, thumb_max) + 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 /// in object-centered coordinates. - pub fn vertical_scrollbar(&self) -> Option { + pub fn vertical_scrollbar(&self) -> Option<(Rect, [f32; 2])> { if self.scrollbar_size.x <= 0. { return None; } @@ -337,10 +358,15 @@ impl ComputedNode { min: Vec2::new(min_x, min_y), max: Vec2::new(max_x, max_y), }; - let gutter_length = gutter.size().y; - let thumb_min = gutter.min.y + gutter_length * self.scroll_position.y / self.content_size.y; - let thumb_max = thumb_min + gutter_length * gutter_length / self.content_size.y; - (gutter, thumb_min, thumb_max) + Some(( + gutter, + Self::compute_thumb( + gutter.min.y, + self.content_size.y, + gutter.size().y, + self.scroll_position.y, + ), + )) } } @@ -2960,6 +2986,10 @@ impl ComputedUiRenderTargetInfo { #[cfg(test)] mod tests { + use bevy_math::Rect; + use bevy_math::Vec2; + + use crate::ComputedNode; use crate::GridPlacement; #[test] @@ -2987,4 +3017,92 @@ 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 mut node = ComputedNode::default(); + node.size = Vec2::splat(100.); + node.scrollbar_size = Vec2::splat(10.); + node.content_size = Vec2::splat(100.); + + 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::default(); + node.size = Vec2::splat(100.); + node.scrollbar_size = Vec2::new(0., 10.); + node.content_size = Vec2::new(200., 100.); + node.scroll_position = Vec2::new(0., 0.); + + 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::default(); + node.size = Vec2::splat(100.); + node.scrollbar_size = Vec2::new(10., 0.); + node.content_size = Vec2::new(100., 200.); + node.scroll_position = Vec2::new(0., 0.); + + 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.]); + } } From 71096612a396aa8bbe75e2cadb391a2f4ce99a6f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 14:42:49 +0000 Subject: [PATCH 3/8] Fix for field_reassign_with_default lint. --- crates/bevy_ui/src/ui_node.rs | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index ca643d0af1147..3e9c469b69b29 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -3020,10 +3020,12 @@ mod tests { #[test] fn computed_node_both_scrollbars() { - let mut node = ComputedNode::default(); - node.size = Vec2::splat(100.); - node.scrollbar_size = Vec2::splat(10.); - node.content_size = Vec2::splat(100.); + 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!( @@ -3048,11 +3050,13 @@ mod tests { #[test] fn computed_node_single_horizontal_scrollbar() { - let mut node = ComputedNode::default(); - node.size = Vec2::splat(100.); - node.scrollbar_size = Vec2::new(0., 10.); - node.content_size = Vec2::new(200., 100.); - node.scroll_position = Vec2::new(0., 0.); + 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() + }; let (gutter, thumb) = node.horizontal_scrollbar().unwrap(); assert_eq!( @@ -3078,11 +3082,13 @@ mod tests { #[test] fn computed_node_single_vertical_scrollbar() { - let mut node = ComputedNode::default(); - node.size = Vec2::splat(100.); - node.scrollbar_size = Vec2::new(10., 0.); - node.content_size = Vec2::new(100., 200.); - node.scroll_position = Vec2::new(0., 0.); + 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() + }; let (gutter, thumb) = node.vertical_scrollbar().unwrap(); assert_eq!( From 18b1317cb6140b212c3667a3e6e2f0747aaee5ab Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 14:46:18 +0000 Subject: [PATCH 4/8] Add check to single scrollbar tests that scrollbar other axis returns None. --- crates/bevy_ui/src/ui_node.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 3e9c469b69b29..f4f1f512bef03 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -3058,6 +3058,8 @@ mod tests { ..Default::default() }; + assert_eq!(None, node.vertical_scrollbar()); + let (gutter, thumb) = node.horizontal_scrollbar().unwrap(); assert_eq!( gutter, @@ -3090,6 +3092,8 @@ mod tests { ..Default::default() }; + assert_eq!(None, node.horizontal_scrollbar()); + let (gutter, thumb) = node.vertical_scrollbar().unwrap(); assert_eq!( gutter, From 5094654d660f6af428300848a942d95fa68e56ff Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 25 Nov 2025 14:51:30 +0000 Subject: [PATCH 5/8] clean up --- crates/bevy_ui/src/ui_node.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f4f1f512bef03..2c40dfda6ed9c 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -311,8 +311,7 @@ impl ComputedNode { let thumb_len = gutter_length * gutter_length / content_length; let thumb_min = gutter_min + scroll_position / (content_length - gutter_length) * (gutter_length - thumb_len); - let thumb_max = thumb_min + thumb_len; - [thumb_min, thumb_max] + [thumb_min, thumb_min + thumb_len] } /// Compute the bounds of the horizontal scrollbar and the thumb From b14ce563ea06dc340bc4c951e204b543def4918c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 2 Dec 2025 10:23:50 +0000 Subject: [PATCH 6/8] Update crates/bevy_ui/src/ui_node.rs Co-authored-by: Kevin Chen --- crates/bevy_ui/src/ui_node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 2c40dfda6ed9c..40d9d590d2585 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -341,7 +341,7 @@ impl ComputedNode { )) } - /// 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. { From 7872b64bfaccee6b5f5768aa6389097768992ec3 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 2 Dec 2025 11:27:56 +0000 Subject: [PATCH 7/8] Update crates/bevy_ui/src/ui_node.rs Co-authored-by: Kevin Chen --- crates/bevy_ui/src/ui_node.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 40d9d590d2585..647929b46fd5f 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -310,7 +310,7 @@ impl ComputedNode { } let thumb_len = gutter_length * gutter_length / content_length; let thumb_min = gutter_min - + scroll_position / (content_length - gutter_length) * (gutter_length - thumb_len); + + scroll_position * gutter_length / content_length; [thumb_min, thumb_min + thumb_len] } From a30219248e0f71be4165aa14a76ae3eb90a09924 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 2 Dec 2025 11:35:08 +0000 Subject: [PATCH 8/8] cargo fmt --all --- crates/bevy_ui/src/ui_node.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 647929b46fd5f..95a4391ee6ab2 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -309,8 +309,7 @@ impl ComputedNode { return [gutter_min, gutter_min + gutter_length]; } let thumb_len = gutter_length * gutter_length / content_length; - let thumb_min = gutter_min - + scroll_position * gutter_length / content_length; + let thumb_min = gutter_min + scroll_position * gutter_length / content_length; [thumb_min, thumb_min + thumb_len] }