1use bevy_app::{App, Plugin, PostUpdate};
2use bevy_ecs::{
3 component::Component,
4 entity::Entity,
5 hierarchy::{ChildOf, Children},
6 observer::On,
7 query::{With, Without},
8 reflect::ReflectComponent,
9 system::{Query, Res},
10};
11use bevy_math::Vec2;
12use bevy_picking::events::{Cancel, Drag, DragEnd, DragStart, Pointer, Press};
13use bevy_reflect::{prelude::ReflectDefault, Reflect};
14use bevy_ui::{
15 ComputedNode, ComputedUiRenderTargetInfo, Node, ScrollPosition, UiGlobalTransform, UiScale, Val,
16};
17
18#[derive(Debug, Default, Clone, Copy, PartialEq, Reflect)]
21#[reflect(PartialEq, Clone, Default)]
22pub enum ControlOrientation {
23 Horizontal,
25 #[default]
27 Vertical,
28}
29
30#[derive(Component, Debug, Reflect)]
52#[reflect(Component)]
53pub struct Scrollbar {
54 pub target: Entity,
56 pub orientation: ControlOrientation,
58 pub min_thumb_length: f32,
63}
64
65#[derive(Component, Debug)]
68#[require(CoreScrollbarDragState)]
69#[derive(Reflect)]
70#[reflect(Component)]
71pub struct CoreScrollbarThumb;
72
73impl Scrollbar {
74 pub fn new(target: Entity, orientation: ControlOrientation, min_thumb_length: f32) -> Self {
82 Self {
83 target,
84 orientation,
85 min_thumb_length,
86 }
87 }
88}
89
90#[derive(Component, Default, Reflect)]
93#[reflect(Component, Default)]
94pub struct CoreScrollbarDragState {
95 pub dragging: bool,
97 drag_origin: f32,
99}
100
101fn scrollbar_on_pointer_down(
102 mut ev: On<Pointer<Press>>,
103 q_thumb: Query<&ChildOf, With<CoreScrollbarThumb>>,
104 mut q_scrollbar: Query<(
105 &Scrollbar,
106 &ComputedNode,
107 &ComputedUiRenderTargetInfo,
108 &UiGlobalTransform,
109 )>,
110 mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<Scrollbar>>,
111 ui_scale: Res<UiScale>,
112) {
113 if q_thumb.contains(ev.entity) {
114 ev.propagate(false);
116 } else if let Ok((scrollbar, node, node_target, transform)) = q_scrollbar.get_mut(ev.entity) {
117 ev.propagate(false);
119
120 let local_pos = transform.try_inverse().unwrap().transform_point2(
122 ev.event().pointer_location.position * node_target.scale_factor() / ui_scale.0,
123 ) + node.size() * 0.5;
124
125 let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) else {
127 return;
128 };
129
130 let visible_size = (scroll_content.size() - scroll_content.scrollbar_size)
134 * scroll_content.inverse_scale_factor;
135 let content_size = scroll_content.content_size() * scroll_content.inverse_scale_factor;
136 let max_range = (content_size - visible_size).max(Vec2::ZERO);
137
138 fn adjust_scroll_pos(scroll_pos: &mut f32, click_pos: f32, step: f32, range: f32) {
139 *scroll_pos =
140 (*scroll_pos + if click_pos > *scroll_pos { step } else { -step }).clamp(0., range);
141 }
142
143 match scrollbar.orientation {
144 ControlOrientation::Horizontal => {
145 if node.size().x > 0. {
146 let click_pos = local_pos.x * content_size.x / node.size().x;
147 adjust_scroll_pos(&mut scroll_pos.x, click_pos, visible_size.x, max_range.x);
148 }
149 }
150 ControlOrientation::Vertical => {
151 if node.size().y > 0. {
152 let click_pos = local_pos.y * content_size.y / node.size().y;
153 adjust_scroll_pos(&mut scroll_pos.y, click_pos, visible_size.y, max_range.y);
154 }
155 }
156 }
157 }
158}
159
160fn scrollbar_on_drag_start(
161 mut ev: On<Pointer<DragStart>>,
162 mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>,
163 q_scrollbar: Query<&Scrollbar>,
164 q_scroll_area: Query<&ScrollPosition>,
165) {
166 if let Ok((ChildOf(thumb_parent), mut drag)) = q_thumb.get_mut(ev.entity) {
167 ev.propagate(false);
168 if let Ok(scrollbar) = q_scrollbar.get(*thumb_parent)
169 && let Ok(scroll_area) = q_scroll_area.get(scrollbar.target)
170 {
171 drag.dragging = true;
172 drag.drag_origin = match scrollbar.orientation {
173 ControlOrientation::Horizontal => scroll_area.x,
174 ControlOrientation::Vertical => scroll_area.y,
175 };
176 }
177 }
178}
179
180fn scrollbar_on_drag(
181 mut ev: On<Pointer<Drag>>,
182 mut q_thumb: Query<(&ChildOf, &mut CoreScrollbarDragState), With<CoreScrollbarThumb>>,
183 mut q_scrollbar: Query<(&ComputedNode, &Scrollbar)>,
184 mut q_scroll_pos: Query<(&mut ScrollPosition, &ComputedNode), Without<Scrollbar>>,
185 ui_scale: Res<UiScale>,
186) {
187 if let Ok((ChildOf(thumb_parent), drag)) = q_thumb.get_mut(ev.entity)
188 && let Ok((node, scrollbar)) = q_scrollbar.get_mut(*thumb_parent)
189 {
190 ev.propagate(false);
191 let Ok((mut scroll_pos, scroll_content)) = q_scroll_pos.get_mut(scrollbar.target) else {
192 return;
193 };
194
195 if drag.dragging {
196 let distance = ev.event().distance / ui_scale.0;
197
198 let visible_size = (scroll_content.size() - scroll_content.scrollbar_size)
199 * scroll_content.inverse_scale_factor;
200 let content_size = scroll_content.content_size() * scroll_content.inverse_scale_factor;
201
202 let scrollbar_size = (node.size() * node.inverse_scale_factor).max(Vec2::ONE);
203
204 match scrollbar.orientation {
205 ControlOrientation::Horizontal => {
206 let range = (content_size.x - visible_size.x).max(0.);
207 scroll_pos.x = (drag.drag_origin
208 + (distance.x * content_size.x) / scrollbar_size.x)
209 .clamp(0., range);
210 }
211 ControlOrientation::Vertical => {
212 let range = (content_size.y - visible_size.y).max(0.);
213 scroll_pos.y = (drag.drag_origin
214 + (distance.y * content_size.y) / scrollbar_size.y)
215 .clamp(0., range);
216 }
217 };
218 }
219 }
220}
221
222fn scrollbar_on_drag_end(
223 mut ev: On<Pointer<DragEnd>>,
224 mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>,
225) {
226 if let Ok(mut drag) = q_thumb.get_mut(ev.entity) {
227 ev.propagate(false);
228 if drag.dragging {
229 drag.dragging = false;
230 }
231 }
232}
233
234fn scrollbar_on_drag_cancel(
235 mut ev: On<Pointer<Cancel>>,
236 mut q_thumb: Query<&mut CoreScrollbarDragState, With<CoreScrollbarThumb>>,
237) {
238 if let Ok(mut drag) = q_thumb.get_mut(ev.entity) {
239 ev.propagate(false);
240 if drag.dragging {
241 drag.dragging = false;
242 }
243 }
244}
245
246fn update_scrollbar_thumb(
247 q_scroll_area: Query<(&ScrollPosition, &ComputedNode)>,
248 q_scrollbar: Query<(&Scrollbar, &ComputedNode, &Children)>,
249 mut q_thumb: Query<&mut Node, With<CoreScrollbarThumb>>,
250) {
251 for (scrollbar, scrollbar_node, children) in q_scrollbar.iter() {
252 let Ok(scroll_area) = q_scroll_area.get(scrollbar.target) else {
253 continue;
254 };
255
256 let visible_size = (scroll_area.1.size() - scroll_area.1.scrollbar_size)
258 * scroll_area.1.inverse_scale_factor;
259
260 let content_size = scroll_area.1.content_size() * scroll_area.1.inverse_scale_factor;
262
263 let track_length = scrollbar_node.size() * scrollbar_node.inverse_scale_factor;
265
266 fn size_and_pos(
267 content_size: f32,
268 visible_size: f32,
269 track_length: f32,
270 min_size: f32,
271 mut offset: f32,
272 ) -> (f32, f32) {
273 let thumb_size = if content_size > visible_size {
274 (track_length * visible_size / content_size)
275 .max(min_size)
276 .min(track_length)
277 } else {
278 track_length
279 };
280
281 if content_size > visible_size {
282 let max_offset = content_size - visible_size;
283
284 offset = offset.clamp(0.0, max_offset);
286 } else {
287 offset = 0.0;
288 }
289
290 let thumb_pos = if content_size > visible_size {
291 offset * (track_length - thumb_size) / (content_size - visible_size)
292 } else {
293 0.
294 };
295
296 (thumb_size, thumb_pos)
297 }
298
299 for child in children {
300 if let Ok(mut thumb) = q_thumb.get_mut(*child) {
301 match scrollbar.orientation {
302 ControlOrientation::Horizontal => {
303 let (thumb_size, thumb_pos) = size_and_pos(
304 content_size.x,
305 visible_size.x,
306 track_length.x,
307 scrollbar.min_thumb_length,
308 scroll_area.0.x,
309 );
310
311 thumb.top = Val::Px(0.);
312 thumb.bottom = Val::Px(0.);
313 thumb.left = Val::Px(thumb_pos);
314 thumb.width = Val::Px(thumb_size);
315 }
316 ControlOrientation::Vertical => {
317 let (thumb_size, thumb_pos) = size_and_pos(
318 content_size.y,
319 visible_size.y,
320 track_length.y,
321 scrollbar.min_thumb_length,
322 scroll_area.0.y,
323 );
324
325 thumb.left = Val::Px(0.);
326 thumb.right = Val::Px(0.);
327 thumb.top = Val::Px(thumb_pos);
328 thumb.height = Val::Px(thumb_size);
329 }
330 };
331 }
332 }
333 }
334}
335
336pub struct ScrollbarPlugin;
338
339impl Plugin for ScrollbarPlugin {
340 fn build(&self, app: &mut App) {
341 app.add_observer(scrollbar_on_pointer_down)
342 .add_observer(scrollbar_on_drag_start)
343 .add_observer(scrollbar_on_drag_end)
344 .add_observer(scrollbar_on_drag_cancel)
345 .add_observer(scrollbar_on_drag)
346 .add_systems(PostUpdate, update_scrollbar_thumb);
347 }
348}