Skip to content

Commit ae98f5e

Browse files
committed
gui: add visualisation to the slider step feature
and update changelog
1 parent e76abba commit ae98f5e

File tree

4 files changed

+149
-25
lines changed

4 files changed

+149
-25
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page.
88
- GUI
99
- Fix `UIScrollArea.add` always returning None
1010
- Support `layer` in `UIView.add_widget()`
11+
- Fix a bug which caused `UIScrollArea` to refresh on every frame
12+
- Add stepping to `UISlider` (thanks [csd4ni3l](https://github.com/csd4ni3l))
1113
- Text objects are now lazy and can be created before the window
1214
- Introduce `arcade.SpriteSequence[T]` as a covariant supertype of `arcade.SpriteList[T]`
1315
(this is similar to Python's `Sequence[T]`, which is a supertype of `list[T]`)

arcade/examples/gui/2_widgets.py

+37-14
Original file line numberDiff line numberDiff line change
@@ -410,12 +410,35 @@ def _show_interactive_widgets(self):
410410
size_hint=(0.3, 0),
411411
)
412412
)
413-
slider_row.add(
413+
s1 = slider_row.add(UISlider(size_hint=(0.3, 1), step=1, style=UISlider.NO_STEP_STYLE))
414+
s2 = slider_row.add(
415+
UISlider(
416+
size_hint=(0.3, 1),
417+
step=5,
418+
)
419+
)
420+
s3 = slider_row.add(
414421
UISlider(
415-
size_hint=(0.2, None),
422+
size_hint=(0.3, 1),
423+
step=10,
416424
)
417425
)
418426

427+
@s1.event("on_change")
428+
def _(event: UIOnChangeEvent):
429+
s2.value = event.new_value
430+
s3.value = event.new_value
431+
432+
@s2.event("on_change")
433+
def _(event: UIOnChangeEvent):
434+
s1.value = event.new_value
435+
s3.value = event.new_value
436+
437+
@s3.event("on_change")
438+
def _(event: UIOnChangeEvent):
439+
s1.value = event.new_value
440+
s2.value = event.new_value
441+
419442
tex_slider_row = UIBoxLayout(vertical=False, size_hint=(1, 0.1), space_between=10)
420443
box.add(tex_slider_row)
421444

@@ -428,7 +451,7 @@ def _show_interactive_widgets(self):
428451
)
429452
)
430453

431-
s1 = tex_slider_row.add(
454+
ts1 = tex_slider_row.add(
432455
UITextureSlider(
433456
thumb_texture=TEX_SLIDER_THUMB_BLUE,
434457
track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_BLUE),
@@ -440,7 +463,7 @@ def _show_interactive_widgets(self):
440463
green_style["normal"].filled_track = arcade.uicolor.GREEN_GREEN_SEA
441464
green_style["hover"].filled_track = arcade.uicolor.GREEN_EMERALD
442465
green_style["press"].filled_track = arcade.uicolor.GREEN_GREEN_SEA
443-
s2 = tex_slider_row.add(
466+
ts2 = tex_slider_row.add(
444467
UITextureSlider(
445468
thumb_texture=TEX_SLIDER_THUMB_GREEN,
446469
track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_GREEN),
@@ -453,7 +476,7 @@ def _show_interactive_widgets(self):
453476
red_style["normal"].filled_track = arcade.uicolor.RED_POMEGRANATE
454477
red_style["hover"].filled_track = arcade.uicolor.RED_ALIZARIN
455478
red_style["press"].filled_track = arcade.uicolor.RED_POMEGRANATE
456-
s3 = tex_slider_row.add(
479+
ts3 = tex_slider_row.add(
457480
UITextureSlider(
458481
thumb_texture=TEX_SLIDER_THUMB_RED,
459482
track_texture=NinePatchTexture(10, 10, 10, 10, TEX_SLIDER_TRACK_RED),
@@ -462,20 +485,20 @@ def _show_interactive_widgets(self):
462485
)
463486
)
464487

465-
@s1.event("on_change")
488+
@ts1.event("on_change")
466489
def _(event: UIOnChangeEvent):
467-
s2.value = event.new_value
468-
s3.value = event.new_value
490+
ts2.value = event.new_value
491+
ts3.value = event.new_value
469492

470-
@s2.event("on_change")
493+
@ts2.event("on_change")
471494
def _(event: UIOnChangeEvent):
472-
s1.value = event.new_value
473-
s3.value = event.new_value
495+
ts1.value = event.new_value
496+
ts3.value = event.new_value
474497

475-
@s3.event("on_change")
498+
@ts3.event("on_change")
476499
def _(event: UIOnChangeEvent):
477-
s1.value = event.new_value
478-
s2.value = event.new_value
500+
ts1.value = event.new_value
501+
ts2.value = event.new_value
479502

480503
box.add(UISpace(size_hint=(0.2, 0.1)))
481504
text_area = box.add(

arcade/examples/gui/6_size_hints.py

+8-6
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,9 @@ def __init__(self):
9898
width_slider_box = center_box.add(UIBoxLayout(vertical=False, size_hint=(1, 0)))
9999
width_slider_box.add(UILabel("Modify size_hint:", bold=True))
100100
width_slider = width_slider_box.add(
101-
arcade.gui.UISlider(min_value=0, max_value=10, value=0, size_hint=None, height=30)
101+
arcade.gui.UISlider(
102+
min_value=0, max_value=1, value=0, size_hint=None, height=30, step=0.1
103+
)
102104
)
103105
width_value = width_slider_box.add(UILabel(bold=True))
104106

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

117119
def update_size_hint_value(value: float):
118120
width_value.text = f"({value:.2f})"
119-
dummy1.size_hint = (value / 10, 1)
120-
dummy1.text = f"size_hint = ({value / 10:.2f}, 1)"
121+
dummy1.size_hint = (value, 1)
122+
dummy1.text = f"size_hint = ({value:.2f}, 1)"
121123

122-
dummy2.size_hint = (1 - value / 10, 1)
123-
dummy2.text = f"size_hint = ({1 - value / 10:.2f}, 1)"
124+
dummy2.size_hint = (1 - value, 1)
125+
dummy2.text = f"size_hint = ({1 - value:.2f}, 1)"
124126

125127
@width_slider.event("on_change")
126128
def on_change(event: UIOnChangeEvent):
127129
update_size_hint_value(event.new_value)
128130

129-
initial_value = 10
131+
initial_value = 1
130132
width_slider.value = initial_value
131133
update_size_hint_value(initial_value)
132134

arcade/gui/widgets/slider.py

+102-5
Original file line numberDiff line numberDiff line change
@@ -84,20 +84,29 @@ def __init__(
8484
)
8585

8686
self.step = step
87-
self.value = self._apply_step(value)
87+
self.value = value
8888
self.min_value = min_value
8989
self.max_value = max_value
9090

9191
self._cursor_width = self.height // 3
9292

9393
# trigger render on value changes
9494
bind(self, "value", self.trigger_full_render)
95+
bind(self, "value", self._ensure_step)
9596
bind(self, "hovered", self.trigger_render)
9697
bind(self, "pressed", self.trigger_render)
9798
bind(self, "disabled", self.trigger_render)
9899

99100
self.register_event_type("on_change")
100101

102+
def _ensure_step(self):
103+
"""Ensure that the step is applied."""
104+
if self.step is not None:
105+
# this will trigger the change once again
106+
# only option to prevent this would be to make `value` a property,
107+
# which might break code of users
108+
self.value = self._apply_step(self.value)
109+
101110
def _apply_step(self, value: float):
102111
if self.step:
103112
inverse = 1 / self.step
@@ -120,9 +129,7 @@ def norm_value(self):
120129
@norm_value.setter
121130
def norm_value(self, value):
122131
"""Normalized value between 0.0 and 1.0"""
123-
self.value = self._apply_step(
124-
min(value * (self.max_value - self.min_value) + self.min_value, self.max_value)
125-
)
132+
self.value = min(value * (self.max_value - self.min_value) + self.min_value, self.max_value)
126133

127134
@property
128135
def _thumb_x(self):
@@ -147,6 +154,7 @@ def do_render(self, surface: Surface):
147154
"""Render the slider, including track and thumb."""
148155
self.prepare_render(surface)
149156
self._render_track(surface)
157+
self._render_steps(surface)
150158
self._render_thumb(surface)
151159

152160
@abstractmethod
@@ -162,6 +170,19 @@ def _render_track(self, surface: Surface):
162170
"""
163171
pass
164172

173+
@abstractmethod
174+
def _render_steps(self, surface: Surface):
175+
"""Render the steps of the slider track.
176+
177+
This method should be implemented in a slider implementation.
178+
179+
Steps should stay within self.content_rect.
180+
181+
Args:
182+
surface: Surface to render on.
183+
"""
184+
pass
185+
165186
@abstractmethod
166187
def _render_thumb(self, surface: Surface):
167188
"""Render the thumb of the slider.
@@ -236,15 +257,18 @@ class UISliderStyle(UIStyleBase):
236257
border: Border color.
237258
border_width: Width of the border.
238259
filled_track: Color of the filled track.
260+
filled_step: Color of the step in filled area.
239261
unfilled_track: Color of the unfilled track.
240-
262+
unfilled_step: Color of the step in unfilled area.
241263
"""
242264

243265
bg: RGBA255 = uicolor.WHITE_SILVER
244266
border: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE
245267
border_width: int = 2
246268
filled_track: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE
269+
filled_step: RGBA255 | None = uicolor.BLUE_PETER_RIVER
247270
unfilled_track: RGBA255 = uicolor.WHITE_SILVER
271+
unfilled_step: RGBA255 | None = uicolor.BLUE_PETER_RIVER
248272

249273

250274
class UISlider(UIStyledWidget[UISliderStyle], UIBaseSlider):
@@ -277,20 +301,54 @@ class UISlider(UIStyledWidget[UISliderStyle], UIBaseSlider):
277301
border=uicolor.BLUE_PETER_RIVER,
278302
border_width=2,
279303
filled_track=uicolor.BLUE_PETER_RIVER,
304+
filled_step=uicolor.DARK_BLUE_MIDNIGHT_BLUE,
305+
),
306+
"press": UIStyle(
307+
bg=uicolor.BLUE_PETER_RIVER,
308+
border=uicolor.DARK_BLUE_WET_ASPHALT,
309+
border_width=3,
310+
filled_track=uicolor.BLUE_PETER_RIVER,
311+
filled_step=uicolor.DARK_BLUE_MIDNIGHT_BLUE,
312+
),
313+
"disabled": UIStyle(
314+
bg=uicolor.WHITE_SILVER,
315+
border_width=1,
316+
filled_track=uicolor.GRAY_ASBESTOS,
317+
unfilled_track=uicolor.WHITE_SILVER,
318+
),
319+
}
320+
321+
NO_STEP_STYLE = {
322+
"normal": UIStyle(
323+
filled_step=None,
324+
unfilled_step=None,
325+
),
326+
"hover": UIStyle(
327+
border=uicolor.BLUE_PETER_RIVER,
328+
border_width=2,
329+
filled_track=uicolor.BLUE_PETER_RIVER,
330+
filled_step=None,
331+
unfilled_step=None,
280332
),
281333
"press": UIStyle(
282334
bg=uicolor.BLUE_PETER_RIVER,
283335
border=uicolor.DARK_BLUE_WET_ASPHALT,
284336
border_width=3,
285337
filled_track=uicolor.BLUE_PETER_RIVER,
338+
filled_step=None,
339+
unfilled_step=None,
286340
),
287341
"disabled": UIStyle(
288342
bg=uicolor.WHITE_SILVER,
289343
border_width=1,
290344
filled_track=uicolor.GRAY_ASBESTOS,
291345
unfilled_track=uicolor.WHITE_SILVER,
346+
filled_step=None,
347+
unfilled_step=None,
292348
),
293349
}
350+
"""Removing the step colors from the style.
351+
So sliders with a step value do not show the steps visually."""
294352

295353
def __init__(
296354
self,
@@ -374,6 +432,45 @@ def _render_track(self, surface: Surface):
374432
fg_slider_color,
375433
)
376434

435+
@override
436+
def _render_steps(self, surface: Surface):
437+
if not self.step:
438+
return
439+
440+
style = self.get_current_style()
441+
if style is None:
442+
warnings.warn(f"No style found for state {self.get_current_state()}", UserWarning)
443+
return
444+
445+
unfilled_steps = style.get("unfilled_step", UISlider.UIStyle.unfilled_step)
446+
filled_steps = style.get("filled_step", UISlider.UIStyle.filled_step)
447+
448+
def float_range(start, stop, step):
449+
while start < stop:
450+
yield start
451+
start += step
452+
yield stop
453+
454+
steps = list(float_range(self.min_value, self.max_value, self.step))
455+
456+
for v in steps:
457+
step_x = self._x_for_value(v) - self.content_rect.left
458+
step_color = filled_steps if v <= self.value else unfilled_steps
459+
460+
if step_color:
461+
# bigger circle for first and last step
462+
circle_size = self._cursor_width // 4
463+
if v in (steps[0], steps[-1]):
464+
circle_size = self._cursor_width // 2
465+
466+
arcade.draw_circle_filled(
467+
step_x,
468+
self.content_height // 2,
469+
circle_size,
470+
step_color,
471+
num_segments=8,
472+
)
473+
377474
@override
378475
def _render_thumb(self, surface: Surface):
379476
style = self.get_current_style()

0 commit comments

Comments
 (0)