Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI Slider Widget #7116

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open

UI Slider Widget #7116

wants to merge 44 commits into from

Conversation

Pietrek14
Copy link
Contributor

@Pietrek14 Pietrek14 commented Jan 7, 2023

Objective

Added a premade UI slider widget. Fixes #6196.

Solution

  • Added a RelativeCursorPosition component that updates alongside Interaction, which stores the cursor position relative to the node.
  • Added a Slider component that stores values specific to sliders
  • Added a SliderBundle and a SliderHandleBundle
  • Added update_slider_value and update_slider_handle systems
  • Made a slider example in the ui directory

Changelog

  • There is an easy way to create UI sliders now
  • Added:
    • RelativeCursorPosition component
    • Slider component
    • SliderBundle
    • SliderHandleBundle
    • update_slider_value and update_slider_handle systems
    • slider example
  • Edited:
    • Reorganized systems related to widgets into seperate plugins

You might've seen the same pull request before. Unfortunately, I had been inactive for some time and when I returned, the original pull request (#6236) had merge conflicts. I've tried fixing them, but failed miserably, since I've never done it before and decided to just write the slider again and open another PR.

@Pietrek14 Pietrek14 mentioned this pull request Jan 7, 2023
@Weibye Weibye self-requested a review January 7, 2023 15:06
@alice-i-cecile alice-i-cecile added C-Enhancement A new feature A-UI Graphical user interfaces, styles, layouts, and widgets X-Controversial There is active debate or serious implications around merging this PR labels Jan 8, 2023
@alice-i-cecile
Copy link
Member

This is only controversial because it's the first real widget, and will set the tone for others.

Copy link
Contributor

@Weibye Weibye left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After taking a closer look at Angular Material Slider Example I understand better your initial approach. Apologies for not seeing what you were seeing at the time.

This is the slider widget as you specified it earlier.

struct SliderWidget {
    value: f32
    min: f32
    max: f32
    step: f32
}

Let's consider an example of a slider of Unicorns:

In the Angular Material example, the contents of the slider widget is concrete. Meaning that min, max and step and value refers to unicorns.

  • What is the minimum and maximum amount of unicorns the user can specify?
  • step is how many unicorns should we increase or decrease with anytime we move the slider.
  • When "submitting the form" the contents of value is the final amount of unicorns you wanted.

This means that you encode "what the values of the slider-widget means" on a widget level (cars, unicorns, particles, ++) and in order to figure out where to position the slider-handle you calculate the abstract progress: value / max - min.


Over in the previous pr #6236 I gave the feedback that perhaps all this was unnecessary and in order to know the position of the slider-handle we kinda only need the value as long as value is kept in a range of 0.0..=1.0

struct SliderWidget {
    value: f32
}

And while that lets us know where to place the slider-handle it really does not address "where do we translate from the abstract progress value 0.0..=1.0 into Unicorns, which is what we really are interested in when "submitting the form" or otherwise polling the data.

And at this point I really don't have a good answer to that.


So I propose we go with your initial design as that is something I can see answering both these issues:

struct SliderWidget {
    value: f32
    min: f32
    max: f32
    step: f32
}

On a detail level, would it make sense to have the slider handle store its own progress value and split them up into multiple systems?

Idea:

struct SliderWidget {
    value: f32
    min: f32
    max: f32
    step: f32,
    slider_handle: Entity
}
struct SliderHandle(f32);

// One system that checks user input and updates the SliderWidget values
fn update_slider_widget( .. ) { .. }

// One system that updates the slider-handles based on the SliderWidgets
fn update_slider_handles(widgets: Query<&SliderWidget>, handles: Query<&mut SliderHandle>) {
   for widget in &mut widgets {
       if let Ok(mut handle) = handles.get(widget.slider_handle) {
            handle.0 = widget.value / widget.max - widget.min;
       }
   }
}

// One system that updates the styles based on the slider-handle values
fn update_slider_handle_positions(handles: Query<&SliderHandle, &Node, &mut Style>, ) {
    for (handle, node, mut style) in &handles {
        // do the style.position update here
    }
}

@Pietrek14
Copy link
Contributor Author

I don't think we want to couple the Slider component together with the slider handle, as someone might want a slider without it. Besides, we wouldn't be able to give a sensible default to an Entity value. I'd rather keep the Slider component a more abstract representation to make it more universal.

Copy link
Contributor

@Weibye Weibye left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall I think this makes sense. Some nitpicks on code-style: If you run cargo fmt it will format the code for you in the format desired for the project.

Edit: I see you already have fixed it, so disregard my review

.ambiguous_with(bevy_text::update_text2d_layout),
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All files should have an empty new-line at the end of the file

Comment on lines 18 to 21
impl Plugin for WidgetPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(ButtonPlugin)
.add_plugin(TextPlugin)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to be a mix between tabs and spaces in your code. Make sure your indentation settings are correct, and I'm pretty sure the rest of Bevy is using 4 spaces for indentation.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this should have been handled by rustfmt, I'm a bit surprised CI passed on this

@IceSentry
Copy link
Contributor

This doesn't necessarily have to be fixed in this PR, but when sliding with the mouse, I would expect the handle to stay centered with the mouse, but moving the mouse changes the point where the handle was grabbed by the mouse. This seems a bit weird to me, but not a deal breaker for the initial PR

slider_jb0a4tw3Nh

@IceSentry
Copy link
Contributor

In general, I like where this is going, but I think this could be split in a couple of simpler PRs. For example, the RelativeCursorPosition could be it's own PR and I think introducing a WidgetPlugin + ImagePlugin could also be it's own PR. With this, this PR would be strictly limited to only adding support for a SliderWidget and nothing else.

Copy link
Contributor

@Weibye Weibye left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely on the right path!

The slider example should showcase a continuous slider as well as a stepped slider. Especially we need to showcase how min and max of the slider can be used to represent different things.

@@ -197,6 +198,43 @@ pub struct ButtonBundle {
pub z_index: ZIndex,
}

/// A UI node that is a slider
#[derive(Bundle, Clone, Debug, Default)]
pub struct SliderBundle {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move this over to the slider.rs and have everything be contained there?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Honestly think we should do the same with ButtonBundle, TextBundle and ImageBundle and just keep the core building blocks for UI in node_bundles.rs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda like having every bundles in a single file and not need to search around to find each bundle definition, but I understand that this won't scale super well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know if I want to do this in this PR. It was originally supposed to be a simple slider widget, but it's slowly turning into a widget rework.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's a case of it being the first "somewhat" advanced widget and will define the structure for all widgets. But I do agree with @IceSentry that we should split it apart into 3 PRs:

  1. Widget restructuring with WidgetPlugin
  2. RelativeCursorPosition functionality that will be useful for multiple systems
  3. SliderWidget that depends on both of the previous PRs

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's continue the WidgetPlugin discussion over here, if necessary #7190


/// A UI node that is a slider
#[derive(Bundle, Clone, Debug)]
pub struct SliderHandleBundle {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here

Copy link
Contributor

@Weibye Weibye left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely on the right path!

The slider example should showcase a continuous slider as well as a stepped slider. Especially we need to showcase how min and max of the slider can be used to represent different things.

@Pietrek14
Copy link
Contributor Author

Relevant PRs: #7197, #7199

@Alsctiho
Copy link

I wonder whether the slider value will be updated if the UI is invisible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-UI Graphical user interfaces, styles, layouts, and widgets C-Enhancement A new feature X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

A slider UI widget
5 participants