Skip to content
21 changes: 21 additions & 0 deletions _release-content/migration-guides/number_input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
title: "Add scrubbing / dragging to number_input widget"
pull_requests: [24636, 24701]
---

The API for the `FeathersNumberInput` has changed. To programmatically update the value, instead
of triggering an `UpdateNumberInput` event, you should insert a `NumberInputValue` component.
This makes it easier to specify the initial value at creation.

```wgsl
// BEFORE
commands.trigger(UpdateNumberInput {
entity: input_ent,
value: NumberInputValue::F32(new_value),
});

// AFTER
commands
.entity(input_ent)
.insert(NumberInputValue::F32(new_value));
```
41 changes: 41 additions & 0 deletions _release-content/release-notes/number_input.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
title: "Add scrubbing / dragging to number_input widget"
authors: ["@viridia"]
pull_requests: [24636, 24701]
---

The `FeathersNumberInput` widget has been substantially overhauled, with several new features.

Bevy's editor design draws heavily from Blender, particularly in its
Comment thread
alice-i-cecile marked this conversation as resolved.
Outdated
[numeric input fields](https://docs.blender.org/manual/en/latest/interface/controls/buttons/fields.html),
which support multiple editing modes — including "scrubbing" (click-and-drag) and direct keyboard
entry. The updated feathers widget is now much closer to feature parity with Blender.

The widget supports editing numbers of different data types: `f32`, `f64`, `i32` and `i64`.

The behavior of the widget can be configured through the use of several optional components:

- `HardLimit` specifies the minimum and maximum range for the value. If this component is absent,
then the natural range of the data type is used.
- `SoftLimit` specifies the range that is accessible via dragging. Numbers that are entered by
typing can exceed this limit.
- `NumberInputPrecision` is used to specify the number of decimal points of precision when dragging,
so that you don't get a bunch of digits jumping around. This only quantizes the value when
dragging, not when typing.
- `Step` is used to indicate the delta value when incrementing and decrementing.

When `SoftLimit` is present, the widget will look and feel like a slider: it will draw a slide
bar in the background, and the drag speed will be calculated such that changes in the bar's size
will be synchronized with movement of the mouse.

If `SoftLimit` is _not_ present, then the widget behaves more like a "scrubber", where there is
no slide bar, and drag speed is calculated based on a heuristic that takes into account precision,
step, and the current input value.

In either of this cases, a non-drag click event will activate "typing" mode, where a value can
be entered by typing digits.

Like all feathers widgets, this is a "controlled" widget, which means that the internal numeric
value is not automatically updated, but instead relies on the application's event handlers to
update the widget state in response to `ValueChange` events. Check out the `feathers_number_input`
example to see how to write such a handler trivially.
28 changes: 21 additions & 7 deletions crates/bevy_feathers/src/controls/number_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ fn number_input_on_insert_disabled(
q_number_input: Query<Has<InteractionDisabled>, With<FeathersNumberInput>>,
mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>,
theme: Res<UiTheme>,
input_focus: Res<InputFocus>,
mut commands: Commands,
) {
let text_input_id = q_children
Expand All @@ -595,6 +596,7 @@ fn number_input_on_insert_disabled(
is_disabled,
false,
hovered,
input_focus.get() == Some(text_id),
&mut gradient,
&mut commands,
);
Expand All @@ -608,6 +610,7 @@ fn number_input_on_remove_disabled(
q_number_input: Query<Has<InteractionDisabled>, With<FeathersNumberInput>>,
mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>,
theme: Res<UiTheme>,
input_focus: Res<InputFocus>,
mut commands: Commands,
) {
let text_input_id = q_children
Expand All @@ -624,6 +627,7 @@ fn number_input_on_remove_disabled(
is_disabled,
false,
hovered,
input_focus.get() == Some(text_id),
&mut gradient,
&mut commands,
);
Expand All @@ -644,6 +648,7 @@ fn number_input_init(
>,
mut q_text_input: Query<(&mut EditableText, &Hovered, &mut BackgroundGradient)>,
theme: Res<UiTheme>,
input_focus: Res<InputFocus>,
mut commands: Commands,
) {
let text_id = insert.event_target();
Expand All @@ -665,6 +670,7 @@ fn number_input_init(
is_disabled,
false,
hovered,
input_focus.get() == Some(text_id),
&mut gradient,
&mut commands,
);
Expand All @@ -678,8 +684,8 @@ fn number_input_hovered(
q_number_input: Query<Has<InteractionDisabled>, With<FeathersNumberInput>>,
mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>,
theme: Res<UiTheme>,
mut commands: Commands,
input_focus: Res<InputFocus>,
mut commands: Commands,
) {
let text_id = insert.event_target();
if let Ok((&Hovered(hovered), mut gradient)) = q_text_input.get_mut(text_id)
Expand All @@ -692,6 +698,7 @@ fn number_input_hovered(
is_disabled,
false,
hovered,
input_focus.get() == Some(text_id),
&mut gradient,
&mut commands,
);
Expand Down Expand Up @@ -863,7 +870,7 @@ fn scrubber_on_drag_start(
mut q_text_input: Query<&mut BackgroundGradient>,
mut q_scrubber: Query<(&ComputedNode, &mut ScrubberDragState)>,
q_parent: Query<&ChildOf>,
focus: Res<InputFocus>,
input_focus: Res<InputFocus>,
theme: Res<UiTheme>,
mut commands: Commands,
) {
Expand All @@ -872,7 +879,7 @@ fn scrubber_on_drag_start(
&& let Ok((input_value, soft_limit, precision, step, disabled)) = q_root.get(root_id)
&& let Ok(mut gradient) = q_text_input.get_mut(text_id)
&& !disabled
&& focus.get() != Some(text_id)
&& input_focus.get() != Some(text_id)
&& let Ok((node, mut drag)) = q_scrubber.get_mut(drag_start.event_target())
{
let slider_size = (node.size().x * node.inverse_scale_factor).max(1.0) as f64;
Expand Down Expand Up @@ -911,6 +918,7 @@ fn scrubber_on_drag_start(
disabled,
true,
false,
input_focus.get() == Some(text_id),
&mut gradient,
&mut commands,
);
Expand Down Expand Up @@ -971,12 +979,12 @@ fn scrubber_on_drag_end(
mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>,
mut q_scrubber: Query<&mut ScrubberDragState>,
q_parent: Query<&ChildOf>,
focus: Res<InputFocus>,
input_focus: Res<InputFocus>,
mut commands: Commands,
theme: Res<UiTheme>,
) {
if let Ok(&ChildOf(text_id)) = q_parent.get(drag_end.event_target())
&& focus.get() != Some(text_id)
&& input_focus.get() != Some(text_id)
&& let Ok(&ChildOf(root_id)) = q_parent.get(text_id)
&& let Ok((soft_limit, hard_limit, precision, disabled)) = q_root.get(root_id)
&& let Ok(mut drag_state) = q_scrubber.get_mut(drag_end.entity)
Expand All @@ -1001,6 +1009,7 @@ fn scrubber_on_drag_end(
disabled,
false,
hovered,
false,
&mut gradient,
&mut commands,
);
Expand All @@ -1015,6 +1024,7 @@ fn scrubber_on_drag_cancel(
mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>,
mut q_scrubber: Query<&mut ScrubberDragState>,
theme: Res<UiTheme>,
input_focus: Res<InputFocus>,
mut commands: Commands,
) {
if let Ok(&ChildOf(text_id)) = q_parent.get(drag_cancel.event_target())
Expand All @@ -1027,6 +1037,7 @@ fn scrubber_on_drag_cancel(
false,
false,
hovered,
input_focus.get() == Some(text_id),
&mut gradient,
&mut commands,
);
Expand Down Expand Up @@ -1058,6 +1069,7 @@ fn set_slidebar_styles(
disabled: bool,
pressed: bool,
hovered: bool,
focused: bool,
gradient: &mut BackgroundGradient,
commands: &mut Commands,
) {
Expand All @@ -1071,14 +1083,16 @@ fn set_slidebar_styles(
tokens::SLIDER_BAR
});

let bg_color = theme.color(&if disabled {
let bg_color = theme.color(&if focused {
tokens::TEXT_INPUT_BG
} else if disabled {
tokens::SLIDER_BG_DISABLED
} else if pressed {
tokens::SLIDER_BG_PRESSED
} else if hovered {
tokens::SLIDER_BG_HOVER
} else {
tokens::TEXT_INPUT_BG
tokens::SLIDER_BG
});

let font_color_token = match disabled {
Expand Down
Loading