Skip to content
Open
Show file tree
Hide file tree
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
75 changes: 74 additions & 1 deletion crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use crate::{
FocusPolicy, UiRect, Val,
};
use bevy_camera::{visibility::Visibility, Camera, RenderTarget};
use bevy_color::{Alpha, Color};
use bevy_color::{palettes::css::GRAY, Alpha, Color};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{prelude::*, system::SystemParam};
use bevy_math::{vec4, BVec2, Rect, UVec2, Vec2, Vec4Swizzles};
Expand Down Expand Up @@ -298,6 +298,52 @@ impl ComputedNode {

clip_rect
}

/// Compute the size and position of the horizontal scrollbar's gutter
pub fn horizontal_scrollbar_gutter(&self) -> Rect {
let content_inset = self.content_inset();
let min_x = content_inset.left;
let max_x = self.size.x - content_inset.right - self.scrollbar_size.x;
let max_y = self.size.y - content_inset.bottom;
let min_y = max_y - self.scrollbar_size.y;
Rect {
min: (min_x, min_y).into(),
max: (max_x, max_y).into(),
}
}

/// Compute the size and position of the vertical scrollbar's gutter
pub fn vertical_scrollbar_gutter(&self) -> Rect {
let content_inset = self.content_inset();
let max_x = self.size.x - content_inset.right;
let min_x = max_x - self.scrollbar_size.x;
let min_y = content_inset.top;
let max_y = self.size.y - content_inset.bottom - self.scrollbar_size.y;
Rect {
min: (min_x, min_y).into(),
max: (max_x, max_y).into(),
}
}

// Compute the size and position of the horizontal scrollbar's thumb
pub fn horizontal_scrollbar_thumb(&self) -> Rect {
let gutter = self.horizontal_scrollbar_gutter();
let width = gutter.size().x * gutter.size().x / self.content_size.x;
let min_x = gutter.size().x * self.scroll_position.x / self.content_size.x;
let min = (min_x, gutter.min.y).into();
let max = min + Vec2::new(width, gutter.size().y);
Rect { min, max }
}

// Compute the size and position of the vertical scrollbar's thumb
pub fn vertical_scrollbar_thumb(&self) -> Rect {
let gutter = self.vertical_scrollbar_gutter();
let height = gutter.size().y * gutter.size().y / self.content_size.y;
let min_y = gutter.size().y * self.scroll_position.y / self.content_size.y;
let min = (gutter.min.x, min_y).into();
let max = (gutter.max.x, min_y + height).into();
Rect { min, max }
}
}

impl ComputedNode {
Expand Down Expand Up @@ -2914,6 +2960,33 @@ impl ComputedUiRenderTargetInfo {
}
}

/// Styling for an automatic scrollbar
#[derive(Component, Clone, Copy, Debug, Reflect, PartialEq)]
#[reflect(Component, Default, PartialEq, Clone)]
#[cfg_attr(
feature = "serialize",
derive(serde::Serialize, serde::Deserialize),
reflect(Serialize, Deserialize)
)]
pub struct ScrollbarStyle {
/// Color of the scrollbar's thumb
pub thumb: Color,
/// Color of the scrollbar's gutter
pub gutter: Color,
/// Color of the scrollbar's corner section
pub corner: Color,
}

impl Default for ScrollbarStyle {
fn default() -> Self {
Self {
thumb: Color::WHITE,
gutter: GRAY.into(),
corner: Color::BLACK,
}
}
}

#[cfg(test)]
mod tests {
use crate::GridPlacement;
Expand Down
132 changes: 131 additions & 1 deletion crates/bevy_ui_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use bevy_sprite_render::SpriteAssetEvents;
use bevy_ui::widget::{ImageNode, TextShadow, ViewportNode};
use bevy_ui::{
BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedUiTargetCamera, Display,
Node, Outline, ResolvedBorderRadius, UiGlobalTransform,
Node, Outline, ResolvedBorderRadius, ScrollbarStyle, UiGlobalTransform,
};

use bevy_app::prelude::*;
Expand Down Expand Up @@ -122,6 +122,7 @@ pub mod stack_z_offsets {
pub const MATERIAL: f32 = 0.05;
pub const TEXT: f32 = 0.06;
pub const TEXT_STRIKETHROUGH: f32 = 0.07;
pub const SCROLLBARS: f32 = 0.1;
}

#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
Expand All @@ -138,6 +139,7 @@ pub enum RenderUiSystems {
ExtractText,
ExtractDebug,
ExtractGradient,
ExtractScrollbars,
}

/// Marker for controlling whether UI is rendered with or without anti-aliasing
Expand Down Expand Up @@ -232,6 +234,7 @@ impl Plugin for UiRenderPlugin {
RenderUiSystems::ExtractTextBackgrounds,
RenderUiSystems::ExtractTextShadows,
RenderUiSystems::ExtractText,
RenderUiSystems::ExtractScrollbars,
RenderUiSystems::ExtractDebug,
)
.chain(),
Expand All @@ -248,6 +251,7 @@ impl Plugin for UiRenderPlugin {
extract_text_decorations.in_set(RenderUiSystems::ExtractTextBackgrounds),
extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows),
extract_text_sections.in_set(RenderUiSystems::ExtractText),
extract_scrollbars.in_set(RenderUiSystems::ExtractScrollbars),
#[cfg(feature = "bevy_ui_debug")]
debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug),
),
Expand Down Expand Up @@ -1304,6 +1308,132 @@ pub fn extract_text_decorations(
}
}

pub fn extract_scrollbars(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
uinode_query: Extract<
Query<(
Entity,
&ComputedNode,
&UiGlobalTransform,
&InheritedVisibility,
Option<&CalculatedClip>,
&ComputedUiTargetCamera,
Option<&ScrollbarStyle>,
)>,
>,
camera_map: Extract<UiCameraMap>,
) {
let mut camera_mapper = camera_map.get_mapper();

for (entity, uinode, transform, inherited_visibility, clip, camera, colors) in &uinode_query {
// Skip invisible backgrounds
if !inherited_visibility.get() || uinode.is_empty() {
continue;
}

let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
continue;
};

if uinode.scrollbar_size.cmple(Vec2::ZERO).all() {
continue;
}

let colors = colors.copied().unwrap_or_default();

let top_left = transform.affine() * Affine2::from_translation(-0.5 * uinode.size);

let h_bar = uinode.horizontal_scrollbar_gutter();
let v_bar = uinode.vertical_scrollbar_gutter();

let corner = Rect::from_corners(
Vec2::new(v_bar.min.x, h_bar.min.y),
Vec2::new(v_bar.max.x, h_bar.max.y),
);
if !corner.is_empty() {
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
z_order: uinode.stack_index as f32 + stack_z_offsets::SCROLLBARS,
clip: clip.map(|clip| clip.clip),
image: AssetId::default(),
extracted_camera_entity,
transform: top_left * Affine2::from_translation(corner.center()),
item: ExtractedUiItem::Node {
color: colors.corner.into(),
rect: Rect {
min: Vec2::ZERO,
max: corner.size(),
},
atlas_scaling: None,
flip_x: false,
flip_y: false,
border: BorderRect::ZERO,
border_radius: ResolvedBorderRadius::ZERO,
node_type: NodeType::Rect,
},
main_entity: entity.into(),
});
}

for (gutter, thumb) in [
(h_bar, uinode.horizontal_scrollbar_thumb()),
(v_bar, uinode.vertical_scrollbar_thumb()),
] {
if gutter.is_empty() {
continue;
}
let transform = top_left * Affine2::from_translation(gutter.center());
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
z_order: uinode.stack_index as f32 + stack_z_offsets::SCROLLBARS,
clip: clip.map(|clip| clip.clip),
image: AssetId::default(),
extracted_camera_entity,
transform,
item: ExtractedUiItem::Node {
color: colors.gutter.into(),
rect: Rect {
min: Vec2::ZERO,
max: gutter.size(),
},
atlas_scaling: None,
flip_x: false,
flip_y: false,
border: BorderRect::ZERO,
border_radius: ResolvedBorderRadius::ZERO,
node_type: NodeType::Rect,
},
main_entity: entity.into(),
});

let transform = top_left * Affine2::from_translation(thumb.center());
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
z_order: uinode.stack_index as f32 + stack_z_offsets::SCROLLBARS,
clip: clip.map(|clip| clip.clip),
image: AssetId::default(),
extracted_camera_entity,
transform,
item: ExtractedUiItem::Node {
color: colors.thumb.into(),
rect: Rect {
min: Vec2::ZERO,
max: thumb.size(),
},
atlas_scaling: None,
flip_x: false,
flip_y: false,
border: BorderRect::ZERO,
border_radius: ResolvedBorderRadius::ZERO,
node_type: NodeType::Rect,
},
main_entity: entity.into(),
});
}
}
}

#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
struct UiVertex {
Expand Down
5 changes: 5 additions & 0 deletions examples/ui/scroll.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ fn vertically_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
align_self: AlignSelf::Stretch,
height: percent(50),
overflow: Overflow::scroll_y(), // n.b.
scrollbar_width: 20.,
..default()
},
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
Expand Down Expand Up @@ -281,6 +282,7 @@ fn bidirectional_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
align_self: AlignSelf::Stretch,
height: percent(50),
overflow: Overflow::scroll(), // n.b.
scrollbar_width: 20.,
..default()
},
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
Expand Down Expand Up @@ -336,6 +338,7 @@ fn bidirectional_scrolling_list_with_sticky(font_handle: Handle<Font>) -> impl B
align_self: AlignSelf::Stretch,
height: percent(50),
overflow: Overflow::scroll(), // n.b.
scrollbar_width: 20.,
grid_template_columns: RepeatedGridTrack::auto(30),
..default()
},
Expand Down Expand Up @@ -408,6 +411,7 @@ fn nested_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
align_self: AlignSelf::Stretch,
height: percent(50),
overflow: Overflow::scroll(),
scrollbar_width: 20.,
..default()
},
BackgroundColor(Color::srgb(0.10, 0.10, 0.10)),
Expand All @@ -419,6 +423,7 @@ fn nested_scrolling_list(font_handle: Handle<Font>) -> impl Bundle {
align_self: AlignSelf::Stretch,
height: percent(200. / 5. * (oi as f32 + 1.)),
overflow: Overflow::scroll_y(),
scrollbar_width: 20.,
..default()
},
BackgroundColor(Color::srgb(0.05, 0.05, 0.05)),
Expand Down
16 changes: 16 additions & 0 deletions release-content/release-notes/automatic_scrollbar_rendering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
title: Automatic scrollbar rendering
authors: ["@ickshonpe"]
pull_requests: [21897]
---

Scrollbars are now drawn automatically for nodes with scrolled content and a `scrollbar_width` greater than 0.

Styling can be set using the `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`