CoreScrollbar widget. (#19803)
# Objective Part of #19236 ## Demo  https://discord.com/channels/691052431525675048/743663673393938453/1387110701386039317 --------- Co-authored-by: ickshonpe <david.curthoys@googlemail.com>
This commit is contained in:
parent
70902413b2
commit
9be1c36391
11
Cargo.toml
11
Cargo.toml
@ -4547,6 +4547,17 @@ description = "Demonstrates use of core (headless) widgets in Bevy UI, with Obse
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "scrollbars"
|
||||
path = "examples/ui/scrollbars.rs"
|
||||
doc-scrape-examples = true
|
||||
|
||||
[package.metadata.example.scrollbars]
|
||||
name = "Scrollbars"
|
||||
description = "Demonstrates use of core scrollbar in Bevy UI"
|
||||
category = "UI (User Interface)"
|
||||
wasm = true
|
||||
|
||||
[[example]]
|
||||
name = "feathers"
|
||||
path = "examples/ui/feathers.rs"
|
||||
|
339
crates/bevy_core_widgets/src/core_scrollbar.rs
Normal file
339
crates/bevy_core_widgets/src/core_scrollbar.rs
Normal file
@ -0,0 +1,339 @@
|
||||
use bevy_app::{App, Plugin, PostUpdate};
|
||||
use bevy_ecs::{
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
hierarchy::{ChildOf, Children},
|
||||
observer::On,
|
||||
query::{With, Without},
|
||||
system::{Query, Res},
|
||||
};
|
||||
use bevy_math::Vec2;
|
||||
use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press};
|
||||
use bevy_ui::{
|
||||
ComputedNode, ComputedNodeTarget, Node, ScrollPosition, UiGlobalTransform, UiScale, Val,
|
||||
};
|
||||
|
||||
/// Used to select the orientation of a scrollbar, slider, or other oriented control.
|
||||
// TODO: Move this to a more central place.
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq)]
|
||||
pub enum ControlOrientation {
|
||||
/// Horizontal orientation (stretching from left to right)
|
||||
Horizontal,
|
||||
/// Vertical orientation (stretching from top to bottom)
|
||||
#[default]
|
||||
Vertical,
|
||||
}
|
||||
|
||||
/// A headless scrollbar widget, which can be used to build custom scrollbars.
|
||||
///
|
||||
/// Scrollbars operate differently than the other core widgets in a number of respects.
|
||||
///
|
||||
/// Unlike sliders, scrollbars don't have an [`AccessibilityNode`](bevy_a11y::AccessibilityNode)
|
||||
/// component, nor can they have keyboard focus. This is because scrollbars are usually used in
|
||||
/// conjunction with a scrollable container, which is itself accessible and focusable. This also
|
||||
/// means that scrollbars don't accept keyboard events, which is also the responsibility of the
|
||||
/// scrollable container.
|
||||
///
|
||||
/// Scrollbars don't emit notification events; instead they modify the scroll position of the target
|
||||
/// entity directly.
|
||||
///
|
||||
/// A scrollbar can have any number of child entities, but one entity must be the scrollbar thumb,
|
||||
/// which is marked with the [`CoreScrollbarThumb`] component. Other children are ignored. The core
|
||||
/// scrollbar will directly update the position and size of this entity; the application is free to
|
||||
/// set any other style properties as desired.
|
||||
///
|
||||
/// The application is free to position the scrollbars relative to the scrolling container however
|
||||
/// it wants: it can overlay them on top of the scrolling content, or use a grid layout to displace
|
||||
/// the content to make room for the scrollbars.
|
||||
#[derive(Component, Debug)]
|
||||
pub struct CoreScrollbar {
|
||||
/// Entity being scrolled.
|
||||
pub target: Entity,
|
||||
/// Whether the scrollbar is vertical or horizontal.
|
||||
pub orientation: ControlOrientation,
|
||||
/// Minimum length of the scrollbar thumb, in pixel units, in the direction parallel to the main
|
||||
/// scrollbar axis. The scrollbar will resize the thumb entity based on the proportion of
|
||||
/// visible size to content size, but no smaller than this. This prevents the thumb from
|
||||
/// disappearing in cases where the ratio of content size to visible size is large.
|
||||
pub min_thumb_length: f32,
|
||||
}
|
||||
|
||||
/// Marker component to indicate that the entity is a scrollbar thumb (the moving, draggable part of
|
||||
/// the scrollbar). This should be a child of the scrollbar entity.
|
||||
#[derive(Component, Debug)]
|
||||
#[require(CoreScrollbarDragState)]
|
||||
pub struct CoreScrollbarThumb;
|
||||
|
||||
impl CoreScrollbar {
|
||||
/// Construct a new scrollbar.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `target` - The scrollable entity that this scrollbar will control.
|
||||
/// * `orientation` - The orientation of the scrollbar (horizontal or vertical).
|
||||
/// * `min_thumb_length` - The minimum size of the scrollbar's thumb, in pixels.
|
||||
pub fn new(target: Entity, orientation: ControlOrientation, min_thumb_length: f32) -> Self {
|
||||
Self {
|
||||
target,
|
||||
orientation,
|
||||
min_thumb_length,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Component used to manage the state of a scrollbar during dragging. This component is
|
||||
/// inserted on the thumb entity.
|
||||
#[derive(Component, Default)]
|
||||
pub struct CoreScrollbarDragState {
|
||||
/// Whether the scrollbar is currently being dragged.
|
||||
pub dragging: bool,
|
||||
/// The value of the scrollbar when dragging started.
|
||||
drag_origin: f32,
|
||||
}
|
||||
|
||||
fn scrollbar_on_pointer_down(
|
||||
mut ev: On<Pointer<Press>>,
|
||||
q_thumb: Query<&ChildOf, With<CoreScrollbarThumb>>,
|
||||
mut q_scrollbar: Query<(
|
||||
&CoreScrollbar,
|
||||
&ComputedNode,
|
||||
&ComputedNodeTarget,
|
||||
&UiGlobalTransform,
|
||||
)>,
|
||||
mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<CoreScrollbar>>,
|
||||
ui_scale: Res<UiScale>,
|
||||
) {
|
||||
if q_thumb.contains(ev.target()) {
|
||||
// If they click on the thumb, do nothing. This will be handled by the drag event.
|
||||
ev.propagate(false);
|
||||
} else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.target()) {
|
||||
// If they click on the scrollbar track, page up or down.
|
||||
ev.propagate(false);
|
||||
|
||||
// Convert to widget-local coordinates.
|
||||
let local_pos = transform.try_inverse().unwrap().transform_point2(
|
||||
ev.event().pointer_location.position * node_target.scale_factor() / ui_scale.0,
|
||||
) + node.size() * 0.5;
|
||||
|
||||
// Bail if we don't find the target entity.
|
||||
let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Convert the click coordinates into a scroll position. If it's greater than the
|
||||
// current scroll position, scroll forward by one step (visible size) otherwise scroll
|
||||
// back.
|
||||
let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor;
|
||||
let content_size = scroll_content.content_size() * scroll_content.inverse_scale_factor;
|
||||
let max_range = (content_size - visible_size).max(Vec2::ZERO);
|
||||
|
||||
fn adjust_scroll_pos(scroll_pos: &mut f32, click_pos: f32, step: f32, range: f32) {
|
||||
*scroll_pos =
|
||||
(*scroll_pos + if click_pos > *scroll_pos { step } else { -step }).clamp(0., range);
|
||||
}
|
||||
|
||||
match scrollbar.orientation {
|
||||
ControlOrientation::Horizontal => {
|
||||
if node.size().x > 0. {
|
||||
let click_pos = local_pos.x * content_size.x / node.size().x;
|
||||
adjust_scroll_pos(
|
||||
&mut scroll_pos.offset_x,
|
||||
click_pos,
|
||||
visible_size.x,
|
||||
max_range.x,
|
||||
);
|
||||
}
|
||||
}
|
||||
ControlOrientation::Vertical => {
|
||||
if node.size().y > 0. {
|
||||
let click_pos = local_pos.y * content_size.y / node.size().y;
|
||||
adjust_scroll_pos(
|
||||
&mut scroll_pos.offset_y,
|
||||
click_pos,
|
||||
visible_size.y,
|
||||
max_range.y,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollbar_on_drag_start(
|
||||
mut ev: On<Pointer<DragStart>>,
|
||||
mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>,
|
||||
q_scrollbar: Query<&CoreScrollbar>,
|
||||
q_scroll_area: Query<&ScrollPosition>,
|
||||
) {
|
||||
if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.target()) {
|
||||
ev.propagate(false);
|
||||
if let Ok(scrollbar) = q_scrollbar.get(*thumb_parent) {
|
||||
if let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) {
|
||||
drag.dragging = true;
|
||||
drag.drag_origin = match scrollbar.orientation {
|
||||
ControlOrientation::Horizontal => scroll_area.offset_x,
|
||||
ControlOrientation::Vertical => scroll_area.offset_y,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollbar_on_drag(
|
||||
mut ev: On<Pointer<Drag>>,
|
||||
mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>,
|
||||
mut q_scrollbar: Query<(&ComputedNode, &CoreScrollbar)>,
|
||||
mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<CoreScrollbar>>,
|
||||
ui_scale: Res<UiScale>,
|
||||
) {
|
||||
if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.target()) {
|
||||
if let Ok((node, scrollbar)) = q_scrollbar.get_mut(*thumb_parent) {
|
||||
ev.propagate(false);
|
||||
let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if drag.dragging {
|
||||
let distance = ev.event().distance / ui_scale.0;
|
||||
let visible_size = scroll_content.size() * scroll_content.inverse_scale_factor;
|
||||
let content_size =
|
||||
scroll_content.content_size() * scroll_content.inverse_scale_factor;
|
||||
let scrollbar_size = (node.size() * node.inverse_scale_factor).max(Vec2::ONE);
|
||||
|
||||
match scrollbar.orientation {
|
||||
ControlOrientation::Horizontal => {
|
||||
let range = (content_size.x - visible_size.x).max(0.);
|
||||
scroll_pos.offset_x = (drag.drag_origin
|
||||
+ (distance.x * content_size.x) / scrollbar_size.x)
|
||||
.clamp(0., range);
|
||||
}
|
||||
ControlOrientation::Vertical => {
|
||||
let range = (content_size.y - visible_size.y).max(0.);
|
||||
scroll_pos.offset_y = (drag.drag_origin
|
||||
+ (distance.y * content_size.y) / scrollbar_size.y)
|
||||
.clamp(0., range);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollbar_on_drag_end(
|
||||
mut ev: On<Pointer<DragEnd>>,
|
||||
mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>,
|
||||
) {
|
||||
if let Ok(mut drag) = q_thumb.get_mut(ev.target()) {
|
||||
ev.propagate(false);
|
||||
if drag.dragging {
|
||||
drag.dragging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scrollbar_on_drag_cancel(
|
||||
mut ev: On<Pointer<Cancel>>,
|
||||
mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>,
|
||||
) {
|
||||
if let Ok(mut drag) = q_thumb.get_mut(ev.target()) {
|
||||
ev.propagate(false);
|
||||
if drag.dragging {
|
||||
drag.dragging = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_scrollbar_thumb(
|
||||
q_scroll_area: Query<(&ScrollPosition, &ComputedNode)>,
|
||||
q_scrollbar: Query<(&CoreScrollbar, &ComputedNode, &Children)>,
|
||||
mut q_thumb: Query<&mut Node, With<CoreScrollbarThumb>>,
|
||||
) {
|
||||
for (scrollbar, scrollbar_node, children) in q_scrollbar.iter() {
|
||||
let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Size of the visible scrolling area.
|
||||
let visible_size = scroll_area.1.size() * scroll_area.1.inverse_scale_factor;
|
||||
|
||||
// Size of the scrolling content.
|
||||
let content_size = scroll_area.1.content_size() * scroll_area.1.inverse_scale_factor;
|
||||
|
||||
// Length of the scrollbar track.
|
||||
let track_length = scrollbar_node.size() * scrollbar_node.inverse_scale_factor;
|
||||
|
||||
fn size_and_pos(
|
||||
content_size: f32,
|
||||
visible_size: f32,
|
||||
track_length: f32,
|
||||
min_size: f32,
|
||||
offset: f32,
|
||||
) -> (f32, f32) {
|
||||
let thumb_size = if content_size > visible_size {
|
||||
(track_length * visible_size / content_size)
|
||||
.max(min_size)
|
||||
.min(track_length)
|
||||
} else {
|
||||
track_length
|
||||
};
|
||||
|
||||
let thumb_pos = if content_size > visible_size {
|
||||
offset * (track_length - thumb_size) / (content_size - visible_size)
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
(thumb_size, thumb_pos)
|
||||
}
|
||||
|
||||
for child in children {
|
||||
if let Ok(mut thumb) = q_thumb.get_mut(*child) {
|
||||
match scrollbar.orientation {
|
||||
ControlOrientation::Horizontal => {
|
||||
let (thumb_size, thumb_pos) = size_and_pos(
|
||||
content_size.x,
|
||||
visible_size.x,
|
||||
track_length.x,
|
||||
scrollbar.min_thumb_length,
|
||||
scroll_area.0.offset_x,
|
||||
);
|
||||
|
||||
thumb.top = Val::Px(0.);
|
||||
thumb.bottom = Val::Px(0.);
|
||||
thumb.left = Val::Px(thumb_pos);
|
||||
thumb.width = Val::Px(thumb_size);
|
||||
}
|
||||
ControlOrientation::Vertical => {
|
||||
let (thumb_size, thumb_pos) = size_and_pos(
|
||||
content_size.y,
|
||||
visible_size.y,
|
||||
track_length.y,
|
||||
scrollbar.min_thumb_length,
|
||||
scroll_area.0.offset_y,
|
||||
);
|
||||
|
||||
thumb.left = Val::Px(0.);
|
||||
thumb.right = Val::Px(0.);
|
||||
thumb.top = Val::Px(thumb_pos);
|
||||
thumb.height = Val::Px(thumb_size);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin that adds the observers for the [`CoreScrollbar`] widget.
|
||||
pub struct CoreScrollbarPlugin;
|
||||
|
||||
impl Plugin for CoreScrollbarPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_observer(scrollbar_on_pointer_down)
|
||||
.add_observer(scrollbar_on_drag_start)
|
||||
.add_observer(scrollbar_on_drag_end)
|
||||
.add_observer(scrollbar_on_drag_cancel)
|
||||
.add_observer(scrollbar_on_drag)
|
||||
.add_systems(PostUpdate, update_scrollbar_thumb);
|
||||
}
|
||||
}
|
@ -17,6 +17,7 @@
|
||||
mod core_button;
|
||||
mod core_checkbox;
|
||||
mod core_radio;
|
||||
mod core_scrollbar;
|
||||
mod core_slider;
|
||||
|
||||
use bevy_app::{App, Plugin};
|
||||
@ -24,6 +25,10 @@ use bevy_app::{App, Plugin};
|
||||
pub use core_button::{CoreButton, CoreButtonPlugin};
|
||||
pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked};
|
||||
pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin};
|
||||
pub use core_scrollbar::{
|
||||
ControlOrientation, CoreScrollbar, CoreScrollbarDragState, CoreScrollbarPlugin,
|
||||
CoreScrollbarThumb,
|
||||
};
|
||||
pub use core_slider::{
|
||||
CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue,
|
||||
SliderRange, SliderStep, SliderValue, TrackClick,
|
||||
@ -39,6 +44,7 @@ impl Plugin for CoreWidgetsPlugin {
|
||||
CoreButtonPlugin,
|
||||
CoreCheckboxPlugin,
|
||||
CoreRadioGroupPlugin,
|
||||
CoreScrollbarPlugin,
|
||||
CoreSliderPlugin,
|
||||
));
|
||||
}
|
||||
|
@ -563,6 +563,7 @@ Example | Description
|
||||
[Relative Cursor Position](../examples/ui/relative_cursor_position.rs) | Showcases the RelativeCursorPosition component
|
||||
[Render UI to Texture](../examples/ui/render_ui_to_texture.rs) | An example of rendering UI as a part of a 3D world
|
||||
[Scroll](../examples/ui/scroll.rs) | Demonstrates scrolling UI containers
|
||||
[Scrollbars](../examples/ui/scrollbars.rs) | Demonstrates use of core scrollbar in Bevy UI
|
||||
[Size Constraints](../examples/ui/size_constraints.rs) | Demonstrates how the to use the size constraints to control the size of a UI node.
|
||||
[Stacked Gradients](../examples/ui/stacked_gradients.rs) | An example demonstrating stacked gradients
|
||||
[Tab Navigation](../examples/ui/tab_navigation.rs) | Demonstration of Tab Navigation between UI elements
|
||||
|
208
examples/ui/scrollbars.rs
Normal file
208
examples/ui/scrollbars.rs
Normal file
@ -0,0 +1,208 @@
|
||||
//! Demonstrations of scrolling and scrollbars.
|
||||
|
||||
use bevy::{
|
||||
core_widgets::{
|
||||
ControlOrientation, CoreScrollbar, CoreScrollbarDragState, CoreScrollbarPlugin,
|
||||
CoreScrollbarThumb,
|
||||
},
|
||||
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
|
||||
input_focus::{
|
||||
tab_navigation::{TabGroup, TabNavigationPlugin},
|
||||
InputDispatchPlugin,
|
||||
},
|
||||
picking::hover::Hovered,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins((
|
||||
DefaultPlugins,
|
||||
CoreScrollbarPlugin,
|
||||
InputDispatchPlugin,
|
||||
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: Val::Px(0.),
|
||||
top: Val::Px(0.),
|
||||
right: Val::Px(0.),
|
||||
bottom: Val::Px(0.),
|
||||
padding: UiRect::all(Val::Px(3.)),
|
||||
row_gap: Val::Px(6.),
|
||||
..Default::default()
|
||||
},
|
||||
BackgroundColor(Color::srgb(0.1, 0.1, 0.1)),
|
||||
UiTargetCamera(camera),
|
||||
TabGroup::default(),
|
||||
Children::spawn((Spawn(Text::new("Scrolling")), Spawn(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: Val::Px(200.0),
|
||||
height: Val::Px(150.0),
|
||||
grid_template_columns: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
|
||||
grid_template_rows: vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)],
|
||||
row_gap: Val::Px(2.0),
|
||||
column_gap: Val::Px(2.0),
|
||||
..default()
|
||||
},
|
||||
Children::spawn((SpawnWith(|parent: &mut RelatedSpawner<ChildOf>| {
|
||||
// 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(Val::Px(4.0)),
|
||||
overflow: Overflow::scroll(),
|
||||
..default()
|
||||
},
|
||||
BackgroundColor(colors::GRAY1.into()),
|
||||
ScrollPosition {
|
||||
offset_x: 0.0,
|
||||
offset_y: 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: Val::Px(8.0),
|
||||
grid_row: GridPlacement::start(1),
|
||||
grid_column: GridPlacement::start(2),
|
||||
..default()
|
||||
},
|
||||
CoreScrollbar {
|
||||
orientation: ControlOrientation::Vertical,
|
||||
target: scroll_area_id,
|
||||
min_thumb_length: 8.0,
|
||||
},
|
||||
Children::spawn(Spawn((
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
Hovered::default(),
|
||||
BackgroundColor(colors::GRAY2.into()),
|
||||
BorderRadius::all(Val::Px(4.0)),
|
||||
CoreScrollbarThumb,
|
||||
))),
|
||||
));
|
||||
|
||||
// Horizontal scrollbar
|
||||
parent.spawn((
|
||||
Node {
|
||||
min_height: Val::Px(8.0),
|
||||
grid_row: GridPlacement::start(2),
|
||||
grid_column: GridPlacement::start(1),
|
||||
..default()
|
||||
},
|
||||
CoreScrollbar {
|
||||
orientation: ControlOrientation::Horizontal,
|
||||
target: scroll_area_id,
|
||||
min_thumb_length: 8.0,
|
||||
},
|
||||
Children::spawn(Spawn((
|
||||
Node {
|
||||
position_type: PositionType::Absolute,
|
||||
..default()
|
||||
},
|
||||
Hovered::default(),
|
||||
BackgroundColor(colors::GRAY2.into()),
|
||||
BorderRadius::all(Val::Px(4.0)),
|
||||
CoreScrollbarThumb,
|
||||
))),
|
||||
));
|
||||
}),)),
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a list row
|
||||
fn text_row(caption: &str) -> impl Bundle {
|
||||
(
|
||||
Text::new(caption),
|
||||
TextFont {
|
||||
font_size: 14.0,
|
||||
..default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Update the color of the scrollbar thumb.
|
||||
fn update_scrollbar_thumb(
|
||||
mut q_thumb: Query<
|
||||
(&mut BackgroundColor, &Hovered, &CoreScrollbarDragState),
|
||||
(
|
||||
With<CoreScrollbarThumb>,
|
||||
Or<(Changed<Hovered>, Changed<CoreScrollbarDragState>)>,
|
||||
),
|
||||
>,
|
||||
) {
|
||||
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::GRAY3
|
||||
} 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(1.0, 1.0, 1.0, 1.0);
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Headless Widgets
|
||||
authors: ["@viridia"]
|
||||
pull_requests: [19366, 19584, 19665, 19778]
|
||||
pull_requests: [19366, 19584, 19665, 19778, 19803]
|
||||
---
|
||||
|
||||
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately
|
||||
@ -34,7 +34,9 @@ sliders, checkboxes and radio buttons.
|
||||
|
||||
- `CoreButton` is a push button. It emits an activation event when clicked.
|
||||
- `CoreSlider` is a standard slider, which lets you edit an `f32` value in a given range.
|
||||
- `CoreScrollbar` can be used to implement scrollbars.
|
||||
- `CoreCheckbox` can be used for checkboxes and toggle switches.
|
||||
- `CoreRadio` and `CoreRadioGroup` can be used for radio buttons.
|
||||
|
||||
## Widget Interaction States
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user