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

Spinner Widget #101

Open
wants to merge 73 commits into
base: master
Choose a base branch
from
Open

Spinner Widget #101

wants to merge 73 commits into from

Conversation

jimorc
Copy link
Contributor

@jimorc jimorc commented Jan 21, 2025

Spinner widget displays a integer and buttons for incrementing and decrementing the integer value.
The spinner has a minimum and maximum value, as well as a step value (the amount to increment or decrement by.
The initial value is set either to the minimum value, or to that of a bound integer.

It is possible to set the minimum, maximum, and step values either when the widget is created, or later via a call to Spinner.SetMinMaxStep. In either case, the values are validated as follows:

  • max > min
  • step > 0
  • step <= max - min
    The increment and decrement buttons are enabled/disabled based on the spinner value. For example:
  • if the value = min, then the decrement button is disabled.
  • if the value = max, then the increment button is disabled.
    The spinner value may be set by:
  • Clicking the increment/decrement buttons.
  • Changing the bound integer value.
  • Calling Spinner.SetValue.
  • Via the keyboard when the widget has focus:
    • KeyUp or '+' to increment the value.
    • KeyDown or '-' to decrement the value.
  • Via the mouse scroller or via touchpad when the widget has focus.

If either the bound value or the value passed to SetValue is outside the range of min to max, the value is reset to be within the range. For example, if value < min, then the value is set to min; if value > max, then the value is set to max.

All the standard widget functionality is provided. For example:

  • Enable/Disable
  • FocusGained/FocusLost
  • Bind/Unbind an integer value

The Spinner widget is tested and supported on the desktop only; functionality has not been provided for Android or iOS.
The code has been tested on Ubuntu 24.04, Windows 11, and MacOS 15.1

jimorc added 30 commits January 12, 2025 08:56
Currently just displays a gray rectangle.
spinnerButton is not fully drawn yet.
Limited functionality provided.
This moves spinnerBox functionality to the spinnerRenderer.
Allows deleting spinnerBox and spinnerLayout.
While some drawing and positioning of objects takes place, they are not the right size or in the correct positions.
Calculate max size that the value text can be.
Size Spinner widget accordingly.
Position value text.
Position both upButton and downButton for equal vertical padding.
Value maintained between min and max.
Spinner was too wide.
Background is a rendering property, not a button property.
Deleted container from renderer because it is not needed.
Changed from input background color to background color. Depending on the theme, this may have no visual effect.
Change made because spinner is not really an input.
This allows the spinner button background to be visible. Without this change, the button background could be the same color as the spinner background.
Change button background and lines to indicate disabled.
SetValue checks the value to ensure that it remains between min and max.
Spinner.upButtonClicked and Spinner.downButtonClicked modified to call SetValue.
Spinner.Refresh modified to disable either spinnerButton as appropriate.
Grab focus when mouse clicked inside Spinner widget.
Change look of Spinner when disabled.
Do not allow input when disabled.
Add Spinner.Disable and Spinner.Enable methods.
Spinner.Disable disables spinner and spinner buttons.
Spinner.Enable enables Spinner and one or both buttons based on spinner value.
Move button disables from spinnerRenderer.Refresh to Spinner.SetValue method.
Increment Spinner value when KeyUp or "+" pressed.
Decrement Spinner value when KeyDown or "-" pressed.
Process these keys only when Spinner is focused and not disabled.
Increment/decrement Spinner value on mouse scroll.
Ignore scroll if Spinner is disabled.
Increment/decrement Spinner value based on scroller input.
OnChanged method called when Spinner value changes.
@andydotxyz
Copy link
Member

This PR only implements an integer spinner. It would be possible to implement a float32 spinner, a float64 spinner, and other integer types as well by using generics. Should I do that?

I would think that a float64 would be good too. I would not bother with all the variations.

That would align with the data binding package where we have Int and Float types.

Widgets that have generic parameters in their constructors feel clunky to me and impede peoples learning due to the added complexity. Just the two types could of course wrap an inner generic version simplifying extension if you wanted later?

@jimorc
Copy link
Contributor Author

jimorc commented Jan 25, 2025

I would think that a float64 would be good too. I would not bother with all the variations.

That would align with the data binding package where we have Int and Float types.

I eventually came to the same conclusion.

Widgets that have generic parameters in their constructors feel clunky to me and impede peoples learning due to the added complexity. Just the two types could of course wrap an inner generic version simplifying extension if you wanted later?

I tried creating a widget with generic parameters yesterday before you sent this message and eventually came to the same conclusion: very ugly and not very intuitive.

I attempted to create a generic parameter for binding.Int and binding.Float. I was unable to do so at the time, but with a bit more thinking, I might be able to do it with a change to the binding API. So, not worth it.

I will modify the PR to contain an IntSpinner and a Float64Spinner.

jimorc added 18 commits January 25, 2025 06:33
This is first change needed to create a float64 spinner.
This base button shares fields and methods that would be common to intSpinnerButton and future float64SpinnerButton.
Rquired adding float64SpinnerRenderer and much functionality which almost duplicates IntSpinner functionality.
Only contains functionality referencing baseSpinner fields.
Move functionality to new maxTextSize function.
The button size is based on the spinner height.
Spinner height is based on text height and theme properties.
Therefore, there is no difference in button size between int and float spinners.
Modify spinnerRenderer.Layout to call spinner button MinSize rather than recalculate it.
The minimum button size is fixed based on spinner size and theme properties, so can be calculated when button is created.
Delete intSpinnerButton and float64SpinnerButton widgets. The spinner buttons do not have a dependency on the spinners they are contained in, so all functionality can be in a base spinner button.
This button is not the base for any other button.
This method did not provide any useful functionality not in Button.Resize.
Also move Tapped to baseSpinner.
These buttons do not have spinner specific code in them.
…to a spinnerColors function

IntSpinner and Float64Spinner spinnerColors methods contained identical code.
Only needs to be called once, so not needed in renderer Refresh method.
Enable/disable button based on whether parent spinner is disabled, and whether spinner value is at corresponding limit.
Discuss IntSpinner and Float64Spinner.
Show Float64Spinner in dark theme.
@jimorc
Copy link
Contributor Author

jimorc commented Jan 26, 2025

This PR is now ready for review and possible merge. I have taken it as far as I can. There is now an IntSpinner widget and a Float64Spinner widget rather than a Spinner widget.

@jimorc
Copy link
Contributor Author

jimorc commented Jan 28, 2025

Let me have another run at this. I have come up with a way to have a single Spinner storing the value in a float64 and allowing (forcing) the user (programmer) to specify a format containing one of the following strings: "%d" for displaying integer values and "%.Xf" where X is an unsigned integer for displaying a float with X digits after the decimal point. Doing this would allow displaying values such as "100 %" and "10.03" or "-27.3".

Spinner uses float64s internally and displays its value based on a format passed as an argument to each of the NewSpinner functions.
baseSpinner not needed because there is only one Spinner type.
baseSpinner methods moved to Spinner.
@jimorc
Copy link
Contributor Author

jimorc commented Jan 28, 2025

NOW this is ready as far as I can tell. There is a single Spinner that contains float64 values and a print (display) format. If the format contains "%d" or "%+d", the value is converted to an integer for display; otherwise, the value remains as a floating point value and is displayed as specified by the format. Invalid formats result in an error display.

@jimorc
Copy link
Contributor Author

jimorc commented Feb 9, 2025

Ready for review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants