Skip to content

Commit ab3c1b6

Browse files
committed
* Added the extract_scrollbars system to bevy_ui_render, this automatically renders scrollbars for scrolled UI nodes .
* Styling can be set using the new `ScrollbarStyle` component. For now, it only supports changing the scrollbar's colors. * `ComputedNode` has new methods that can be used to compute the geometry for a UI node's scrollbars: * `horizontal_scrollbar_gutter` * `vertical_scrollbar_gutter` * `horizontal_scrollbar_thumb`, * `vertical_scrollbar_thumb`
1 parent 7ad5fb7 commit ab3c1b6

File tree

4 files changed

+226
-2
lines changed

4 files changed

+226
-2
lines changed

crates/bevy_ui/src/ui_node.rs

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::{
33
FocusPolicy, UiRect, Val,
44
};
55
use bevy_camera::{visibility::Visibility, Camera, RenderTarget};
6-
use bevy_color::{Alpha, Color};
6+
use bevy_color::{palettes::css::GRAY, Alpha, Color};
77
use bevy_derive::{Deref, DerefMut};
88
use bevy_ecs::{prelude::*, system::SystemParam};
99
use bevy_math::{vec4, BVec2, Rect, UVec2, Vec2, Vec4Swizzles};
@@ -298,6 +298,52 @@ impl ComputedNode {
298298

299299
clip_rect
300300
}
301+
302+
/// Compute the size and position of the horizontal scrollbar's gutter
303+
pub fn horizontal_scrollbar_gutter(&self) -> Rect {
304+
let content_inset = self.content_inset();
305+
let min_x = content_inset.left;
306+
let max_x = self.size.x - content_inset.right - self.scrollbar_size.x;
307+
let max_y = self.size.y - content_inset.bottom;
308+
let min_y = max_y - self.scrollbar_size.y;
309+
Rect {
310+
min: (min_x, min_y).into(),
311+
max: (max_x, max_y).into(),
312+
}
313+
}
314+
315+
/// Compute the size and position of the vertical scrollbar's gutter
316+
pub fn vertical_scrollbar_gutter(&self) -> Rect {
317+
let content_inset = self.content_inset();
318+
let max_x = self.size.x - content_inset.right;
319+
let min_x = max_x - self.scrollbar_size.x;
320+
let min_y = content_inset.top;
321+
let max_y = self.size.y - content_inset.bottom - self.scrollbar_size.y;
322+
Rect {
323+
min: (min_x, min_y).into(),
324+
max: (max_x, max_y).into(),
325+
}
326+
}
327+
328+
// Compute the size and position of the horizontal scrollbar's thumb
329+
pub fn horizontal_scrollbar_thumb(&self) -> Rect {
330+
let gutter = self.horizontal_scrollbar_gutter();
331+
let width = gutter.size().x * gutter.size().x / self.content_size.x;
332+
let min_x = gutter.size().x * self.scroll_position.x / self.content_size.x;
333+
let min = (min_x, gutter.min.y).into();
334+
let max = min + Vec2::new(width, gutter.size().y);
335+
Rect { min, max }
336+
}
337+
338+
// Compute the size and position of the vertical scrollbar's thumb
339+
pub fn vertical_scrollbar_thumb(&self) -> Rect {
340+
let gutter = self.vertical_scrollbar_gutter();
341+
let height = gutter.size().y * gutter.size().y / self.content_size.y;
342+
let min_y = gutter.size().y * self.scroll_position.y / self.content_size.y;
343+
let min = (gutter.min.x, min_y).into();
344+
let max = (gutter.max.x, min_y + height).into();
345+
Rect { min, max }
346+
}
301347
}
302348

303349
impl ComputedNode {
@@ -2914,6 +2960,33 @@ impl ComputedUiRenderTargetInfo {
29142960
}
29152961
}
29162962

2963+
/// Styling for an automatic scrollbar
2964+
#[derive(Component, Clone, Copy, Debug, Reflect, PartialEq)]
2965+
#[reflect(Component, Default, PartialEq, Clone)]
2966+
#[cfg_attr(
2967+
feature = "serialize",
2968+
derive(serde::Serialize, serde::Deserialize),
2969+
reflect(Serialize, Deserialize)
2970+
)]
2971+
pub struct ScrollbarStyle {
2972+
/// Color of the scrollbar's thumb
2973+
pub thumb: Color,
2974+
/// Color of the scrollbar's gutter
2975+
pub gutter: Color,
2976+
/// Color of the scrollbar's corner section
2977+
pub corner: Color,
2978+
}
2979+
2980+
impl Default for ScrollbarStyle {
2981+
fn default() -> Self {
2982+
Self {
2983+
thumb: Color::WHITE,
2984+
gutter: GRAY.into(),
2985+
corner: Color::BLACK,
2986+
}
2987+
}
2988+
}
2989+
29172990
#[cfg(test)]
29182991
mod tests {
29192992
use crate::GridPlacement;

crates/bevy_ui_render/src/lib.rs

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ use bevy_sprite_render::SpriteAssetEvents;
2828
use bevy_ui::widget::{ImageNode, TextShadow, ViewportNode};
2929
use bevy_ui::{
3030
BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedUiTargetCamera, Display,
31-
Node, Outline, ResolvedBorderRadius, UiGlobalTransform,
31+
Node, Outline, ResolvedBorderRadius, ScrollbarStyle, UiGlobalTransform,
3232
};
3333

3434
use bevy_app::prelude::*;
@@ -122,6 +122,7 @@ pub mod stack_z_offsets {
122122
pub const MATERIAL: f32 = 0.05;
123123
pub const TEXT: f32 = 0.06;
124124
pub const TEXT_STRIKETHROUGH: f32 = 0.07;
125+
pub const SCROLLBARS: f32 = 0.1;
125126
}
126127

127128
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
@@ -138,6 +139,7 @@ pub enum RenderUiSystems {
138139
ExtractText,
139140
ExtractDebug,
140141
ExtractGradient,
142+
ExtractScrollbars,
141143
}
142144

143145
/// Marker for controlling whether UI is rendered with or without anti-aliasing
@@ -232,6 +234,7 @@ impl Plugin for UiRenderPlugin {
232234
RenderUiSystems::ExtractTextBackgrounds,
233235
RenderUiSystems::ExtractTextShadows,
234236
RenderUiSystems::ExtractText,
237+
RenderUiSystems::ExtractScrollbars,
235238
RenderUiSystems::ExtractDebug,
236239
)
237240
.chain(),
@@ -248,6 +251,7 @@ impl Plugin for UiRenderPlugin {
248251
extract_text_decorations.in_set(RenderUiSystems::ExtractTextBackgrounds),
249252
extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows),
250253
extract_text_sections.in_set(RenderUiSystems::ExtractText),
254+
extract_scrollbars.in_set(RenderUiSystems::ExtractScrollbars),
251255
#[cfg(feature = "bevy_ui_debug")]
252256
debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug),
253257
),
@@ -1304,6 +1308,132 @@ pub fn extract_text_decorations(
13041308
}
13051309
}
13061310

1311+
pub fn extract_scrollbars(
1312+
mut commands: Commands,
1313+
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
1314+
uinode_query: Extract<
1315+
Query<(
1316+
Entity,
1317+
&ComputedNode,
1318+
&UiGlobalTransform,
1319+
&InheritedVisibility,
1320+
Option<&CalculatedClip>,
1321+
&ComputedUiTargetCamera,
1322+
Option<&ScrollbarStyle>,
1323+
)>,
1324+
>,
1325+
camera_map: Extract<UiCameraMap>,
1326+
) {
1327+
let mut camera_mapper = camera_map.get_mapper();
1328+
1329+
for (entity, uinode, transform, inherited_visibility, clip, camera, colors) in &uinode_query {
1330+
// Skip invisible backgrounds
1331+
if !inherited_visibility.get() || uinode.is_empty() {
1332+
continue;
1333+
}
1334+
1335+
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
1336+
continue;
1337+
};
1338+
1339+
if uinode.scrollbar_size.cmple(Vec2::ZERO).all() {
1340+
continue;
1341+
}
1342+
1343+
let colors = colors.copied().unwrap_or_default();
1344+
1345+
let top_left = transform.affine() * Affine2::from_translation(-0.5 * uinode.size);
1346+
1347+
let h_bar = uinode.horizontal_scrollbar_gutter();
1348+
let v_bar = uinode.vertical_scrollbar_gutter();
1349+
1350+
let corner = Rect::from_corners(
1351+
Vec2::new(v_bar.min.x, h_bar.min.y),
1352+
Vec2::new(v_bar.max.x, h_bar.max.y),
1353+
);
1354+
if !corner.is_empty() {
1355+
extracted_uinodes.uinodes.push(ExtractedUiNode {
1356+
render_entity: commands.spawn(TemporaryRenderEntity).id(),
1357+
z_order: uinode.stack_index as f32 + stack_z_offsets::SCROLLBARS,
1358+
clip: clip.map(|clip| clip.clip),
1359+
image: AssetId::default(),
1360+
extracted_camera_entity,
1361+
transform: top_left * Affine2::from_translation(corner.center()),
1362+
item: ExtractedUiItem::Node {
1363+
color: colors.corner.into(),
1364+
rect: Rect {
1365+
min: Vec2::ZERO,
1366+
max: corner.size(),
1367+
},
1368+
atlas_scaling: None,
1369+
flip_x: false,
1370+
flip_y: false,
1371+
border: BorderRect::ZERO,
1372+
border_radius: ResolvedBorderRadius::ZERO,
1373+
node_type: NodeType::Rect,
1374+
},
1375+
main_entity: entity.into(),
1376+
});
1377+
}
1378+
1379+
for (gutter, thumb) in [
1380+
(h_bar, uinode.horizontal_scrollbar_thumb()),
1381+
(v_bar, uinode.vertical_scrollbar_thumb()),
1382+
] {
1383+
if gutter.is_empty() {
1384+
continue;
1385+
}
1386+
let transform = top_left * Affine2::from_translation(gutter.center());
1387+
extracted_uinodes.uinodes.push(ExtractedUiNode {
1388+
render_entity: commands.spawn(TemporaryRenderEntity).id(),
1389+
z_order: uinode.stack_index as f32 + stack_z_offsets::SCROLLBARS,
1390+
clip: clip.map(|clip| clip.clip),
1391+
image: AssetId::default(),
1392+
extracted_camera_entity,
1393+
transform,
1394+
item: ExtractedUiItem::Node {
1395+
color: colors.gutter.into(),
1396+
rect: Rect {
1397+
min: Vec2::ZERO,
1398+
max: gutter.size(),
1399+
},
1400+
atlas_scaling: None,
1401+
flip_x: false,
1402+
flip_y: false,
1403+
border: BorderRect::ZERO,
1404+
border_radius: ResolvedBorderRadius::ZERO,
1405+
node_type: NodeType::Rect,
1406+
},
1407+
main_entity: entity.into(),
1408+
});
1409+
1410+
let transform = top_left * Affine2::from_translation(thumb.center());
1411+
extracted_uinodes.uinodes.push(ExtractedUiNode {
1412+
render_entity: commands.spawn(TemporaryRenderEntity).id(),
1413+
z_order: uinode.stack_index as f32 + stack_z_offsets::SCROLLBARS,
1414+
clip: clip.map(|clip| clip.clip),
1415+
image: AssetId::default(),
1416+
extracted_camera_entity,
1417+
transform,
1418+
item: ExtractedUiItem::Node {
1419+
color: colors.thumb.into(),
1420+
rect: Rect {
1421+
min: Vec2::ZERO,
1422+
max: thumb.size(),
1423+
},
1424+
atlas_scaling: None,
1425+
flip_x: false,
1426+
flip_y: false,
1427+
border: BorderRect::ZERO,
1428+
border_radius: ResolvedBorderRadius::ZERO,
1429+
node_type: NodeType::Rect,
1430+
},
1431+
main_entity: entity.into(),
1432+
});
1433+
}
1434+
}
1435+
}
1436+
13071437
#[repr(C)]
13081438
#[derive(Copy, Clone, Pod, Zeroable)]
13091439
struct UiVertex {

examples/ui/scroll.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,7 @@ fn vertically_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
230230
align_self: AlignSelf::Stretch,
231231
height: percent(50),
232232
overflow: Overflow::scroll_y(), // n.b.
233+
scrollbar_width: 20.,
233234
..default()
234235
},
235236
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
@@ -281,6 +282,7 @@ fn bidirectional_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
281282
align_self: AlignSelf::Stretch,
282283
height: percent(50),
283284
overflow: Overflow::scroll(), // n.b.
285+
scrollbar_width: 20.,
284286
..default()
285287
},
286288
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
@@ -336,6 +338,7 @@ fn bidirectional_scrolling_list_with_sticky(font_handle: Handle<Font>) -> impl B
336338
align_self: AlignSelf::Stretch,
337339
height: percent(50),
338340
overflow: Overflow::scroll(), // n.b.
341+
scrollbar_width: 20.,
339342
grid_template_columns: RepeatedGridTrack::auto(30),
340343
..default()
341344
},
@@ -408,6 +411,7 @@ fn nested_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
408411
align_self: AlignSelf::Stretch,
409412
height: percent(50),
410413
overflow: Overflow::scroll(),
414+
scrollbar_width: 20.,
411415
..default()
412416
},
413417
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
@@ -419,6 +423,7 @@ fn nested_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
419423
align_self: AlignSelf::Stretch,
420424
height: percent(200. / 5. * (oi as f32 + 1.)),
421425
overflow: Overflow::scroll_y(),
426+
scrollbar_width: 20.,
422427
..default()
423428
},
424429
BackgroundColor(Color::srgb(0.05, 0.05, 0.05)),
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
title: Automatic scrollbar rendering
3+
authors: ["@ickshonpe"]
4+
pull_requests: []
5+
---
6+
7+
Scrollbars are now drawn automatically for nodes with scrolled content and a `scrollbar_width` greater than 0.
8+
9+
Styling can be set using the `ScrollbarStyle` component. For now, it only supports changing the scrollbar's colors.
10+
11+
`ComputedNode` has new methods that can be used to compute the geometry for a UI node's scrollbars:
12+
13+
* `horizontal_scrollbar_gutter`
14+
* `vertical_scrollbar_gutter`
15+
* `horizontal_scrollbar_thumb`,
16+
* `vertical_scrollbar_thumb`

0 commit comments

Comments
 (0)