Skip to content

gui: add visualisation to the slider step feature #2667

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

Merged
merged 1 commit into from
May 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
- GUI
- Fix `UIScrollArea.add` always returning None
- Support `layer` in `UIView.add_widget()`
- Fix a bug which caused `UIScrollArea` to refresh on every frame
- Add stepping to `UISlider` (thanks [csd4ni3l](https://github.com/csd4ni3l))
- Text objects are now lazy and can be created before the window
- Introduce `arcade.SpriteSequence[T]` as a covariant supertype of `arcade.SpriteList[T]`
(this is similar to Python's `Sequence[T]`, which is a supertype of `list[T]`)
Expand Down
51 changes: 37 additions & 14 deletions arcade/examples/gui/2_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,35 @@ def _show_interactive_widgets(self):
size_hint=(0.3, 0),
)
)
slider_row.add(
s1 = slider_row.add(UISlider(size_hint=(0.3, 1), step=1, style=UISlider.NO_STEP_STYLE))
s2 = slider_row.add(
UISlider(
size_hint=(0.3, 1),
step=5,
)
)
s3 = slider_row.add(
UISlider(
size_hint=(0.2, None),
size_hint=(0.3, 1),
step=10,
)
)

@s1.event("on_change")
def _(event: UIOnChangeEvent):
s2.value = event.new_value
s3.value = event.new_value

@s2.event("on_change")
def _(event: UIOnChangeEvent):
s1.value = event.new_value
s3.value = event.new_value

@s3.event("on_change")
def _(event: UIOnChangeEvent):
s1.value = event.new_value
s2.value = event.new_value

tex_slider_row = UIBoxLayout(vertical=False, size_hint=(1, 0.1), space_between=10)
box.add(tex_slider_row)

Expand All @@ -428,7 +451,7 @@ def _show_interactive_widgets(self):
)
)

s1 = tex_slider_row.add(
ts1 = tex_slider_row.add(
UITextureSlider(
thumb_texture=TEX_SLIDER_THUMB_BLUE,
track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_BLUE),
Expand All @@ -440,7 +463,7 @@ def _show_interactive_widgets(self):
green_style["normal"].filled_track = arcade.uicolor.GREEN_GREEN_SEA
green_style["hover"].filled_track = arcade.uicolor.GREEN_EMERALD
green_style["press"].filled_track = arcade.uicolor.GREEN_GREEN_SEA
s2 = tex_slider_row.add(
ts2 = tex_slider_row.add(
UITextureSlider(
thumb_texture=TEX_SLIDER_THUMB_GREEN,
track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_GREEN),
Expand All @@ -453,7 +476,7 @@ def _show_interactive_widgets(self):
red_style["normal"].filled_track = arcade.uicolor.RED_POMEGRANATE
red_style["hover"].filled_track = arcade.uicolor.RED_ALIZARIN
red_style["press"].filled_track = arcade.uicolor.RED_POMEGRANATE
s3 = tex_slider_row.add(
ts3 = tex_slider_row.add(
UITextureSlider(
thumb_texture=TEX_SLIDER_THUMB_RED,
track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_RED),
Expand All @@ -462,20 +485,20 @@ def _show_interactive_widgets(self):
)
)

@s1.event("on_change")
@ts1.event("on_change")
def _(event: UIOnChangeEvent):
s2.value = event.new_value
s3.value = event.new_value
ts2.value = event.new_value
ts3.value = event.new_value

@s2.event("on_change")
@ts2.event("on_change")
def _(event: UIOnChangeEvent):
s1.value = event.new_value
s3.value = event.new_value
ts1.value = event.new_value
ts3.value = event.new_value

@s3.event("on_change")
@ts3.event("on_change")
def _(event: UIOnChangeEvent):
s1.value = event.new_value
s2.value = event.new_value
ts1.value = event.new_value
ts2.value = event.new_value

box.add(UISpace(size_hint=(0.2, 0.1)))
text_area = box.add(
Expand Down
14 changes: 8 additions & 6 deletions arcade/examples/gui/6_size_hints.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ def __init__(self):
width_slider_box = center_box.add(UIBoxLayout(vertical=False, size_hint=(1, 0)))
width_slider_box.add(UILabel("Modify size_hint:", bold=True))
width_slider = width_slider_box.add(
arcade.gui.UISlider(min_value=0, max_value=10, value=0, size_hint=None, height=30)
arcade.gui.UISlider(
min_value=0, max_value=1, value=0, size_hint=None, height=30, step=0.1
)
)
width_value = width_slider_box.add(UILabel(bold=True))

Expand All @@ -116,17 +118,17 @@ def __init__(self):

def update_size_hint_value(value: float):
width_value.text = f"({value:.2f})"
dummy1.size_hint = (value / 10, 1)
dummy1.text = f"size_hint = ({value / 10:.2f}, 1)"
dummy1.size_hint = (value, 1)
dummy1.text = f"size_hint = ({value:.2f}, 1)"

dummy2.size_hint = (1 - value / 10, 1)
dummy2.text = f"size_hint = ({1 - value / 10:.2f}, 1)"
dummy2.size_hint = (1 - value, 1)
dummy2.text = f"size_hint = ({1 - value:.2f}, 1)"

@width_slider.event("on_change")
def on_change(event: UIOnChangeEvent):
update_size_hint_value(event.new_value)

initial_value = 10
initial_value = 1
width_slider.value = initial_value
update_size_hint_value(initial_value)

Expand Down
107 changes: 102 additions & 5 deletions arcade/gui/widgets/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,20 +84,29 @@ def __init__(
)

self.step = step
self.value = self._apply_step(value)
self.value = value
self.min_value = min_value
self.max_value = max_value

self._cursor_width = self.height // 3

# trigger render on value changes
bind(self, "value", self.trigger_full_render)
bind(self, "value", self._ensure_step)
bind(self, "hovered", self.trigger_render)
bind(self, "pressed", self.trigger_render)
bind(self, "disabled", self.trigger_render)

self.register_event_type("on_change")

def _ensure_step(self):
"""Ensure that the step is applied."""
if self.step is not None:
# this will trigger the change once again
# only option to prevent this would be to make `value` a property,
# which might break code of users
self.value = self._apply_step(self.value)

def _apply_step(self, value: float):
if self.step:
inverse = 1 / self.step
Expand All @@ -120,9 +129,7 @@ def norm_value(self):
@norm_value.setter
def norm_value(self, value):
"""Normalized value between 0.0 and 1.0"""
self.value = self._apply_step(
min(value * (self.max_value - self.min_value) + self.min_value, self.max_value)
)
self.value = min(value * (self.max_value - self.min_value) + self.min_value, self.max_value)

@property
def _thumb_x(self):
Expand All @@ -147,6 +154,7 @@ def do_render(self, surface: Surface):
"""Render the slider, including track and thumb."""
self.prepare_render(surface)
self._render_track(surface)
self._render_steps(surface)
self._render_thumb(surface)

@abstractmethod
Expand All @@ -162,6 +170,19 @@ def _render_track(self, surface: Surface):
"""
pass

@abstractmethod
def _render_steps(self, surface: Surface):
"""Render the steps of the slider track.

This method should be implemented in a slider implementation.

Steps should stay within self.content_rect.

Args:
surface: Surface to render on.
"""
pass

@abstractmethod
def _render_thumb(self, surface: Surface):
"""Render the thumb of the slider.
Expand Down Expand Up @@ -236,15 +257,18 @@ class UISliderStyle(UIStyleBase):
border: Border color.
border_width: Width of the border.
filled_track: Color of the filled track.
filled_step: Color of the step in filled area.
unfilled_track: Color of the unfilled track.

unfilled_step: Color of the step in unfilled area.
"""

bg: RGBA255 = uicolor.WHITE_SILVER
border: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE
border_width: int = 2
filled_track: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE
filled_step: RGBA255 | None = uicolor.BLUE_PETER_RIVER
unfilled_track: RGBA255 = uicolor.WHITE_SILVER
unfilled_step: RGBA255 | None = uicolor.BLUE_PETER_RIVER


class UISlider(UIStyledWidget[UISliderStyle], UIBaseSlider):
Expand Down Expand Up @@ -277,20 +301,54 @@ class UISlider(UIStyledWidget[UISliderStyle], UIBaseSlider):
border=uicolor.BLUE_PETER_RIVER,
border_width=2,
filled_track=uicolor.BLUE_PETER_RIVER,
filled_step=uicolor.DARK_BLUE_MIDNIGHT_BLUE,
),
"press": UIStyle(
bg=uicolor.BLUE_PETER_RIVER,
border=uicolor.DARK_BLUE_WET_ASPHALT,
border_width=3,
filled_track=uicolor.BLUE_PETER_RIVER,
filled_step=uicolor.DARK_BLUE_MIDNIGHT_BLUE,
),
"disabled": UIStyle(
bg=uicolor.WHITE_SILVER,
border_width=1,
filled_track=uicolor.GRAY_ASBESTOS,
unfilled_track=uicolor.WHITE_SILVER,
),
}

NO_STEP_STYLE = {
"normal": UIStyle(
filled_step=None,
unfilled_step=None,
),
"hover": UIStyle(
border=uicolor.BLUE_PETER_RIVER,
border_width=2,
filled_track=uicolor.BLUE_PETER_RIVER,
filled_step=None,
unfilled_step=None,
),
"press": UIStyle(
bg=uicolor.BLUE_PETER_RIVER,
border=uicolor.DARK_BLUE_WET_ASPHALT,
border_width=3,
filled_track=uicolor.BLUE_PETER_RIVER,
filled_step=None,
unfilled_step=None,
),
"disabled": UIStyle(
bg=uicolor.WHITE_SILVER,
border_width=1,
filled_track=uicolor.GRAY_ASBESTOS,
unfilled_track=uicolor.WHITE_SILVER,
filled_step=None,
unfilled_step=None,
),
}
"""Removing the step colors from the style.
So sliders with a step value do not show the steps visually."""

def __init__(
self,
Expand Down Expand Up @@ -374,6 +432,45 @@ def _render_track(self, surface: Surface):
fg_slider_color,
)

@override
def _render_steps(self, surface: Surface):
if not self.step:
return

style = self.get_current_style()
if style is None:
warnings.warn(f"No style found for state {self.get_current_state()}", UserWarning)
return

unfilled_steps = style.get("unfilled_step", UISlider.UIStyle.unfilled_step)
filled_steps = style.get("filled_step", UISlider.UIStyle.filled_step)

def float_range(start, stop, step):
while start < stop:
yield start
start += step
yield stop

steps = list(float_range(self.min_value, self.max_value, self.step))

for v in steps:
step_x = self._x_for_value(v) - self.content_rect.left
step_color = filled_steps if v <= self.value else unfilled_steps

if step_color:
# bigger circle for first and last step
circle_size = self._cursor_width // 4
if v in (steps[0], steps[-1]):
circle_size = self._cursor_width // 2

arcade.draw_circle_filled(
step_x,
self.content_height // 2,
circle_size,
step_color,
num_segments=8,
)

@override
def _render_thumb(self, surface: Surface):
style = self.get_current_style()
Expand Down
Loading