use bevy::color::Color;
use bevy::ecs::{
component::Component,
hierarchy::Children,
lifecycle,
observer::On,
query::{Changed, Or, With},
spawn::{Spawn, SpawnRelated},
system::{Commands, Query, Res, SystemParam},
};
use bevy::input::{ButtonInput, keyboard::KeyCode};
use bevy::input_focus::InputDispatchPlugin;
use bevy::picking::{
events::{Drag, DragStart, Pointer, Scroll},
hover::Hovered,
};
use bevy::ui::{
BackgroundColor, BorderRadius, ComputedNode, Display, FlexDirection, FlexWrap, GridPlacement,
Node, Overflow, OverflowAxis, PositionType, RepeatedGridTrack, ScrollPosition, UiRect, UiScale,
px,
};
use bevy::ui_widgets::{
ControlOrientation, Scrollbar, ScrollbarDragState, ScrollbarPlugin, ScrollbarThumb,
};
use bevy::{math::Vec2, text::TextLayout, utils::default};
use bevy_immediate::{
CapSet, Imm, ImmEntity,
attach::{BevyImmediateAttachPlugin, ImmediateAttach},
ui::text::ImmUiText,
};
use bevy_immediate_ui::{CapsUi, ImplCapsUi};
use crate::{bevy_scrollarea::colors::GRAY1, styles::title_text_style};
pub struct BevyScrollareaExamplePlugin;
impl bevy::app::Plugin for BevyScrollareaExamplePlugin {
fn build(&self, app: &mut bevy::app::App) {
if !app.is_plugin_added::<ScrollbarPlugin>() {
app.add_plugins(ScrollbarPlugin);
}
if !app.is_plugin_added::<InputDispatchPlugin>() {
app.add_plugins(InputDispatchPlugin);
}
app.add_plugins(BevyImmediateAttachPlugin::<CapsUi, BevyScrollareaExampleRoot>::new());
app.add_systems(bevy::app::Update, update_scrollbar_style_on_drag);
app.add_observer(
|event: On<lifecycle::Add, MyScrollableNode>, mut commands: Commands| {
commands
.entity(event.event().entity)
.insert(ScrollState::default())
.observe(scroll_on_mouse)
.observe(scroll_on_drag_start)
.observe(scroll_on_drag);
},
);
}
}
#[derive(Component)]
pub struct BevyScrollareaExampleRoot;
#[derive(SystemParam)]
pub struct Params {}
impl ImmediateAttach<CapsUi> for BevyScrollareaExampleRoot {
type Params = Params;
fn construct(ui: &mut Imm<CapsUi>, _params: &mut Params) {
ui.ch()
.on_spawn_insert(title_text_style)
.on_spawn_text("Bevy scrollareas");
ui.ch().on_spawn_text("Powered by bevy_ui_widgets");
ui.ch().on_spawn_insert(|| Node {
height: px(20.),
..default()
});
ui.ch()
.on_spawn_insert(|| TextLayout {
linebreak: bevy::text::LineBreak::WordBoundary,
..default()
})
.on_spawn_text(
"Example showcases implementation of reusable scrollarea (scrollable, draggable):",
);
ui.ch()
.on_spawn_insert(|| Node {
display: Display::Grid,
flex_wrap: FlexWrap::Wrap,
grid_template_columns: vec![RepeatedGridTrack::fr(2, 1.)],
grid_template_rows: vec![RepeatedGridTrack::fr(2, 1.)],
column_gap: px(20.),
row_gap: px(20.),
min_height: px(200.),
min_width: px(200.),
max_height: px(600.),
max_width: px(600.),
..default()
})
.add(|ui| {
for (idx, overflow) in [
Overflow {
x: OverflowAxis::Scroll,
y: OverflowAxis::Scroll,
},
Overflow {
x: OverflowAxis::Scroll,
y: OverflowAxis::Clip,
},
Overflow {
x: OverflowAxis::Clip,
y: OverflowAxis::Scroll,
},
Overflow {
x: OverflowAxis::Clip,
y: OverflowAxis::Clip,
},
]
.into_iter()
.enumerate()
{
ui.ch_id(("overflow", idx)).scrollarea(
Node {
min_height: px(50),
min_width: px(50),
..default()
},
Node {
display: Display::Flex,
flex_direction: FlexDirection::Column,
padding: UiRect::all(px(4)),
overflow,
..default()
},
|ui_entity| {
ui_entity
.on_spawn_insert(|| BackgroundColor(GRAY1.into()))
.add(|ui| {
let no_wrap = || TextLayout {
linebreak: bevy::text::LineBreak::NoWrap,
..default()
};
ui.ch().on_spawn_text_fn(|| format!("{:?}", overflow));
for idx in 0..30 {
ui.ch().on_spawn_insert(no_wrap).on_spawn_text_fn(|| {
format!("{idx} Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")
});
}
});
},
);
}
});
}
}
pub trait ScrollBar<Caps: CapSet> {
fn scrollarea(
self,
outer_content_node: Node,
content_node_style: Node,
content: impl FnOnce(ImmEntity<'_, '_, '_, Caps>),
) -> Self;
}
impl<Caps> ScrollBar<Caps> for ImmEntity<'_, '_, '_, Caps>
where
Caps: ImplCapsUi,
{
fn scrollarea(
self,
outer_content_node: Node,
content_node_style: Node,
content: impl FnOnce(ImmEntity<'_, '_, '_, Caps>),
) -> Self {
let grid_template = |scrollbar: bool| {
if scrollbar {
vec![RepeatedGridTrack::flex(1, 1.), RepeatedGridTrack::auto(1)]
} else {
vec![RepeatedGridTrack::flex(1, 1.)]
}
};
let horizontal = content_node_style.overflow.x == OverflowAxis::Scroll;
let vertical = content_node_style.overflow.y == OverflowAxis::Scroll;
self.on_spawn_insert(|| {
Node {
display: Display::Grid,
grid_template_columns: grid_template(horizontal),
grid_template_rows: grid_template(vertical),
..outer_content_node
}
})
.add(|ui| {
let scrollarea_content = ui.ch().on_spawn_insert(|| {
(
content_node_style,
ScrollPosition(Vec2::ZERO),
MyScrollableNode,
)
});
let scrollbar_target = scrollarea_content.entity();
content(scrollarea_content);
if vertical {
ui.ch().on_spawn_insert(|| {
(
Node {
width: px(8),
grid_row: GridPlacement::start(1),
grid_column: GridPlacement::start(2),
..default()
},
Scrollbar {
orientation: ControlOrientation::Vertical,
target: scrollbar_target,
min_thumb_length: 8.0,
},
Children::spawn(Spawn((
Node {
position_type: PositionType::Absolute,
..default()
},
Hovered::default(),
BackgroundColor(colors::GRAY2.into()),
ScrollbarThumb {
border_radius: BorderRadius::all(px(4)),
..default()
},
))),
)
});
}
if horizontal {
ui.ch().on_spawn_insert(|| {
(
Node {
min_height: px(8),
grid_row: GridPlacement::start(2),
grid_column: GridPlacement::start(1),
..default()
},
Scrollbar {
orientation: ControlOrientation::Horizontal,
target: scrollbar_target,
min_thumb_length: 8.0,
},
Children::spawn(Spawn((
Node {
position_type: PositionType::Absolute,
border_radius: BorderRadius::all(px(4)),
..default()
},
Hovered::default(),
BackgroundColor(colors::GRAY2.into()),
ScrollbarThumb {
border_radius: BorderRadius::all(px(4)),
..default()
},
))),
)
});
}
})
}
}
#[derive(Component)]
struct MyScrollableNode;
#[derive(Component, Default)]
struct ScrollState {
initial_pos: Vec2,
}
fn scroll_on_mouse(
scroll: On<Pointer<Scroll>>,
mut scroll_position_query: Query<(&mut ScrollPosition, &ComputedNode), With<MyScrollableNode>>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
if let Ok((mut scroll_position, node)) = scroll_position_query.get_mut(scroll.entity) {
let visible_size = node.size() * node.inverse_scale_factor;
let content_size = node.content_size() * node.inverse_scale_factor;
let max_range = (content_size - visible_size).max(Vec2::ZERO);
let mut delta = Vec2::new(scroll.x, scroll.y);
if keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]) {
std::mem::swap(&mut delta.x, &mut delta.y);
}
let inverse_scale_factor: f32 = node.inverse_scale_factor;
match scroll.unit {
bevy::input::mouse::MouseScrollUnit::Line => {
delta *= 28.;
}
bevy::input::mouse::MouseScrollUnit::Pixel => {
delta *= 1.0 / inverse_scale_factor;
}
}
scroll_position.0 = (scroll_position.0 - delta).min(max_range).max(Vec2::ZERO);
}
}
fn scroll_on_drag_start(
mut drag_start: On<Pointer<DragStart>>,
mut scroll_position_query: Query<(&ComputedNode, &mut ScrollState), With<MyScrollableNode>>,
) {
if let Ok((computed_node, mut state)) = scroll_position_query.get_mut(drag_start.entity) {
drag_start.propagate(false);
state.initial_pos = computed_node.scroll_position;
}
}
fn scroll_on_drag(
mut drag: On<Pointer<Drag>>,
ui_scale: Res<UiScale>,
mut scroll_position_query: Query<
(&mut ScrollPosition, &ComputedNode, &ScrollState),
With<MyScrollableNode>,
>,
) {
if let Ok((mut scroll_position, comp_node, state)) = scroll_position_query.get_mut(drag.entity)
{
let visible_size = comp_node.size();
let content_size = comp_node.content_size();
drag.propagate(false);
let max_range = (content_size - visible_size).max(Vec2::ZERO);
let distance = drag.distance / (comp_node.inverse_scale_factor * ui_scale.0);
scroll_position.0 = ((state.initial_pos - distance)
.min(max_range)
.max(Vec2::ZERO))
* comp_node.inverse_scale_factor;
}
}
#[allow(clippy::type_complexity)]
fn update_scrollbar_style_on_drag(
mut q_thumb: Query<
(&mut BackgroundColor, &Hovered, &ScrollbarDragState),
(
With<ScrollbarThumb>,
Or<(Changed<Hovered>, Changed<ScrollbarDragState>)>,
),
>,
) {
for (mut thumb_bg, Hovered(is_hovering), drag) in q_thumb.iter_mut() {
let color: Color = if *is_hovering || drag.dragging {
colors::GRAY3
} else {
colors::GRAY2
}
.into();
if thumb_bg.0 != color {
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);
}