diff --git a/_release-content/migration-guides/number_input.md b/_release-content/migration-guides/number_input.md new file mode 100644 index 0000000000000..2f1bdd56a3832 --- /dev/null +++ b/_release-content/migration-guides/number_input.md @@ -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)); +``` diff --git a/_release-content/release-notes/number_input.md b/_release-content/release-notes/number_input.md new file mode 100644 index 0000000000000..d1bfd420b6566 --- /dev/null +++ b/_release-content/release-notes/number_input.md @@ -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. + +Blender's [numeric input](https://docs.blender.org/manual/en/latest/interface/controls/buttons/fields.html) +is great, and we've borrowed its best elements. This includes support for 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. diff --git a/crates/bevy_feathers/src/controls/number_input.rs b/crates/bevy_feathers/src/controls/number_input.rs index eebcb86d986e2..9ad205dae4c02 100644 --- a/crates/bevy_feathers/src/controls/number_input.rs +++ b/crates/bevy_feathers/src/controls/number_input.rs @@ -579,6 +579,7 @@ fn number_input_on_insert_disabled( q_number_input: Query, With>, mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>, theme: Res, + input_focus: Res, mut commands: Commands, ) { let text_input_id = q_children @@ -595,6 +596,7 @@ fn number_input_on_insert_disabled( is_disabled, false, hovered, + input_focus.get() == Some(text_id), &mut gradient, &mut commands, ); @@ -608,6 +610,7 @@ fn number_input_on_remove_disabled( q_number_input: Query, With>, mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>, theme: Res, + input_focus: Res, mut commands: Commands, ) { let text_input_id = q_children @@ -624,6 +627,7 @@ fn number_input_on_remove_disabled( is_disabled, false, hovered, + input_focus.get() == Some(text_id), &mut gradient, &mut commands, ); @@ -644,6 +648,7 @@ fn number_input_init( >, mut q_text_input: Query<(&mut EditableText, &Hovered, &mut BackgroundGradient)>, theme: Res, + input_focus: Res, mut commands: Commands, ) { let text_id = insert.event_target(); @@ -665,6 +670,7 @@ fn number_input_init( is_disabled, false, hovered, + input_focus.get() == Some(text_id), &mut gradient, &mut commands, ); @@ -678,8 +684,8 @@ fn number_input_hovered( q_number_input: Query, With>, mut q_text_input: Query<(&Hovered, &mut BackgroundGradient)>, theme: Res, - mut commands: Commands, input_focus: Res, + mut commands: Commands, ) { let text_id = insert.event_target(); if let Ok((&Hovered(hovered), mut gradient)) = q_text_input.get_mut(text_id) @@ -692,6 +698,7 @@ fn number_input_hovered( is_disabled, false, hovered, + input_focus.get() == Some(text_id), &mut gradient, &mut commands, ); @@ -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, + input_focus: Res, theme: Res, mut commands: Commands, ) { @@ -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; @@ -911,6 +918,7 @@ fn scrubber_on_drag_start( disabled, true, false, + input_focus.get() == Some(text_id), &mut gradient, &mut commands, ); @@ -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, + input_focus: Res, mut commands: Commands, theme: Res, ) { 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) @@ -1001,6 +1009,7 @@ fn scrubber_on_drag_end( disabled, false, hovered, + false, &mut gradient, &mut commands, ); @@ -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, + input_focus: Res, mut commands: Commands, ) { if let Ok(&ChildOf(text_id)) = q_parent.get(drag_cancel.event_target()) @@ -1027,6 +1037,7 @@ fn scrubber_on_drag_cancel( false, false, hovered, + input_focus.get() == Some(text_id), &mut gradient, &mut commands, ); @@ -1058,6 +1069,7 @@ fn set_slidebar_styles( disabled: bool, pressed: bool, hovered: bool, + focused: bool, gradient: &mut BackgroundGradient, commands: &mut Commands, ) { @@ -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 {