Skip to content

Commit 8a0367e

Browse files
Add vertical slider support to bevy_ui_widgets slider (#21827)
# Objective - Fixes the issue of vertical sliders not being functional. Previously, when creating a vertical slider, the slider drag behavior was still horizontal, meaning dragging left/right would change the value instead of dragging up/down. Additionally, clicking on the slider track was offset and didn't correctly map to the clicked position. ## Solution The slider widget now automatically detects its orientation based on the node's dimensions (`height > width` for vertical, otherwise horizontal) and adjusts its interaction behavior accordingly: 1. **Orientation Detection**: Added automatic detection of slider orientation by comparing `node.size().y` to `node.size().x` in both `slider_on_pointer_down` and `slider_on_drag` functions. 2. **Drag Direction Fix**: - For vertical sliders, the drag calculation now uses the Y-axis (`distance.y`) instead of X-axis - The Y coordinate is properly inverted (since screen Y increases downward) to match expected behavior (dragging up increases value) - Thumb size calculation uses `thumb.size().y` for vertical sliders instead of always using `thumb.size().x` 3. **Click Position Fix**: - Fixed coordinate conversion from Bevy's center-origin coordinate system to top/left-origin coordinates - For vertical sliders: converts `local_pos.y` from `[-height/2, +height/2]` to `[0, height]` before calculating the slider value - Accounts for thumb size offset to center the calculation properly - Inverts the Y coordinate for vertical sliders since Y increases downward 4. **Track Size Calculation**: Uses `node.size().y - thumb_size` for vertical sliders and `node.size().x - thumb_size` for horizontal sliders when calculating the available track space. The changes are backward compatible - horizontal sliders continue to work exactly as before, and the orientation detection is transparent to users of the API. ## Testing - **Manual Testing**: Created test application with both vertical and horizontal sliders to verify: - Vertical sliders respond correctly to vertical drag movements (up = increase, down = decrease) - Horizontal sliders continue to work correctly with horizontal drag movements - Clicking anywhere on the slider track correctly snaps to that position for both orientations - Thumb positioning updates correctly during drag operations - Multiple sliders can coexist without interfering with each other - **Edge Cases Tested**: - Clicking at the very top/bottom of vertical sliders - Clicking at the very left/right of horizontal sliders - Dragging from one extreme to the other - Rapid clicking and dragging interactions **Areas that may need more testing:** - Sliders with non-standard aspect ratios (very wide vertical sliders or very tall horizontal sliders) - Sliders with custom transforms/rotations applied - Sliders in nested UI hierarchies with complex transforms **How reviewers can test:** 1. Create a vertical slider (height > width) and verify: - Dragging up increases the value - Dragging down decreases the value - Clicking anywhere on the track snaps to that exact position 2. Create a horizontal slider (width > height) and verify it still works as before 3. Test with multiple sliders of both orientations simultaneously **Platforms tested:** - macOS (Apple Silicon) ## Showcase ### Before - Vertical sliders were unusable - dragging would move horizontally instead of vertically - Clicking on vertical slider tracks was offset, with clicks near the bottom snapping to the middle instead https://github.com/user-attachments/assets/9bda83e7-f46b-4626-9df3-a558526a8ab2 ### After - Vertical sliders work correctly with intuitive up/down drag behavior - Clicking anywhere on the slider track accurately snaps to the clicked position - Both vertical and horizontal sliders work seamlessly together https://github.com/user-attachments/assets/cca24288-4cbc-4ceb-828f-9ce7a735ade0 ### Code Example ```rust // Vertical slider - now works correctly! commands.spawn(( Node { width: Val::Px(12.0), height: Val::Px(300.0), // height > width = vertical ..default() }, Slider::default(), SliderValue(50.0), SliderRange::new(0.0, 100.0), // ... thumb and track children )); // Horizontal slider - continues to work as before commands.spawn(( Node { width: Val::Px(300.0), height: Val::Px(12.0), // width > height = horizontal ..default() }, Slider::default(), SliderValue(50.0), SliderRange::new(0.0, 100.0), // ... thumb and track children )); ``` The orientation is automatically detected based on the node dimensions - no API changes required! --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
1 parent 8c6e490 commit 8a0367e

File tree

4 files changed

+386
-17
lines changed

4 files changed

+386
-17
lines changed

Cargo.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3491,6 +3491,18 @@ description = "Illustrates how to access `winit::window::Window`'s `hittest` fun
34913491
category = "UI (User Interface)"
34923492
wasm = false
34933493

3494+
[[example]]
3495+
name = "vertical_slider"
3496+
path = "examples/ui/vertical_slider.rs"
3497+
doc-scrape-examples = true
3498+
required-features = ["experimental_bevy_ui_widgets"]
3499+
3500+
[package.metadata.example.vertical_slider]
3501+
name = "Vertical Slider"
3502+
description = "Simple example showing vertical and horizontal slider widgets with snap behavior and value labels"
3503+
category = "UI (User Interface)"
3504+
wasm = true
3505+
34943506
[[example]]
34953507
name = "font_atlas_debug"
34963508
path = "examples/ui/font_atlas_debug.rs"

crates/bevy_ui_widgets/src/slider.rs

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -264,22 +264,49 @@ pub(crate) fn slider_on_pointer_down(
264264
return;
265265
}
266266

267+
// Detect orientation: vertical if height > width
268+
let is_vertical = node.size().y > node.size().x;
269+
267270
// Find thumb size by searching descendants for the first entity with SliderThumb
268271
let thumb_size = q_children
269272
.iter_descendants(press.entity)
270-
.find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
273+
.find_map(|child_id| {
274+
q_thumb.get(child_id).ok().map(|thumb| {
275+
if is_vertical {
276+
thumb.size().y
277+
} else {
278+
thumb.size().x
279+
}
280+
})
281+
})
271282
.unwrap_or(0.0);
272283

273284
// Detect track click.
274285
let local_pos = transform.try_inverse().unwrap().transform_point2(
275286
press.pointer_location.position * node_target.scale_factor() / ui_scale.0,
276287
);
277-
let track_width = node.size().x - thumb_size;
288+
let track_size = if is_vertical {
289+
node.size().y - thumb_size
290+
} else {
291+
node.size().x - thumb_size
292+
};
293+
278294
// Avoid division by zero
279-
let click_val = if track_width > 0. {
280-
local_pos.x * range.span() / track_width + range.center()
295+
let click_val = if track_size > 0. {
296+
if is_vertical {
297+
// For vertical sliders: bottom-to-top (0 at bottom, max at top)
298+
// local_pos.y ranges from -height/2 (top) to +height/2 (bottom)
299+
let y_from_bottom = (node.size().y / 2.0) - local_pos.y;
300+
let adjusted_y = y_from_bottom - thumb_size / 2.0;
301+
adjusted_y * range.span() / track_size + range.start()
302+
} else {
303+
// For horizontal sliders: convert from center-origin to left-origin
304+
let x_from_left = local_pos.x + node.size().x / 2.0;
305+
let adjusted_x = x_from_left - thumb_size / 2.0;
306+
adjusted_x * range.span() / track_size + range.start()
307+
}
281308
} else {
282-
0.
309+
range.center()
283310
};
284311

285312
// Compute new value from click position
@@ -330,7 +357,6 @@ pub(crate) fn slider_on_drag(
330357
mut event: On<Pointer<Drag>>,
331358
mut q_slider: Query<
332359
(
333-
&SliderValue,
334360
&ComputedNode,
335361
&SliderRange,
336362
Option<&SliderPrecision>,
@@ -345,23 +371,42 @@ pub(crate) fn slider_on_drag(
345371
mut commands: Commands,
346372
ui_scale: Res<UiScale>,
347373
) {
348-
if let Ok((value, node, range, precision, transform, drag, disabled)) =
349-
q_slider.get_mut(event.entity)
374+
if let Ok((node, range, precision, transform, drag, disabled)) = q_slider.get_mut(event.entity)
350375
{
351376
event.propagate(false);
352377
if drag.dragging && !disabled {
378+
// Detect orientation: vertical if height > width
379+
let is_vertical = node.size().y > node.size().x;
380+
353381
let mut distance = event.distance / ui_scale.0;
354382
distance.y *= -1.;
355383
let distance = transform.transform_vector2(distance);
384+
356385
// Find thumb size by searching descendants for the first entity with SliderThumb
357386
let thumb_size = q_children
358387
.iter_descendants(event.entity)
359-
.find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x))
388+
.find_map(|child_id| {
389+
q_thumb.get(child_id).ok().map(|thumb| {
390+
if is_vertical {
391+
thumb.size().y
392+
} else {
393+
thumb.size().x
394+
}
395+
})
396+
})
360397
.unwrap_or(0.0);
361-
let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0);
398+
399+
let slider_size = if is_vertical {
400+
((node.size().y - thumb_size) * node.inverse_scale_factor).max(1.0)
401+
} else {
402+
((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0)
403+
};
404+
405+
let drag_distance = if is_vertical { distance.y } else { distance.x };
406+
362407
let span = range.span();
363408
let new_value = if span > 0. {
364-
drag.offset + (distance.x * span) / slider_width
409+
drag.offset + (drag_distance * span) / slider_size
365410
} else {
366411
range.start() + span * 0.5
367412
};
@@ -371,12 +416,10 @@ pub(crate) fn slider_on_drag(
371416
.unwrap_or(new_value),
372417
);
373418

374-
if rounded_value != value.0 {
375-
commands.trigger(ValueChange {
376-
source: event.entity,
377-
value: rounded_value,
378-
});
379-
}
419+
commands.trigger(ValueChange {
420+
source: event.entity,
421+
value: rounded_value,
422+
});
380423
}
381424
}
382425
}

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,7 @@ Example | Description
595595
[UI Texture Slice Flipping and Tiling](../examples/ui/ui_texture_slice_flip_and_tile.rs) | Illustrates how to flip and tile images with 9 Slicing in UI
596596
[UI Transform](../examples/ui/ui_transform.rs) | An example demonstrating how to translate, rotate and scale UI elements.
597597
[UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements
598+
[Vertical Slider](../examples/ui/vertical_slider.rs) | Simple example showing vertical and horizontal slider widgets with snap behavior and value labels
598599
[Viewport Debug](../examples/ui/viewport_debug.rs) | An example for debugging viewport coordinates
599600
[Viewport Node](../examples/ui/viewport_node.rs) | Demonstrates how to create a viewport node with picking support
600601
[Virtual Keyboard](../examples/ui/virtual_keyboard.rs) | Example demonstrating a virtual keyboard widget

0 commit comments

Comments
 (0)