Source code for arcade.gui.widgets.slider

from __future__ import annotations

import warnings
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass
from typing import Mapping, Optional, Union

from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED
from typing_extensions import override

import arcade
from arcade import Texture, uicolor
from arcade.gui import (
    NinePatchTexture,
    Surface,
    UIEvent,
    UIInteractiveWidget,
    UIMouseDragEvent,
    UIOnClickEvent,
)
from arcade.gui.events import UIOnChangeEvent
from arcade.gui.property import Property, bind
from arcade.gui.style import UIStyleBase, UIStyledWidget
from arcade.types import RGBA255


[docs] class UIBaseSlider(UIInteractiveWidget, metaclass=ABCMeta): """Base class for sliders. A slider contains of a horizontal track and a thumb. The thumb can be moved along the track to set the value of the slider. Use the `on_change` event to get notified about value changes. Subclasses should implement the `_render_track` and `_render_thumb` methods. Args: value: Current value of the curosr of the slider. min_value: Minimum value of the slider. max_value: Maximum value of the slider. x: x coordinate of bottom left. y: y coordinate of bottom left. width: Width of the slider. height: Height of the slider. size_hint: Size hint of the slider. size_hint_min: Minimum size hint of the slider. size_hint_max: Maximum size hint of the slider. style: Used to style the slider for different states. **kwargs: Passed to UIInteractiveWidget. """ value = Property(0.0) def __init__( self, *, value: float = 0, min_value: float = 0, max_value: float = 100, x: float = 0, y: float = 0, width: float = 300, height: float = 20, size_hint=None, size_hint_min=None, size_hint_max=None, style: Union[Mapping[str, UISliderStyle], None] = None, **kwargs, ): super().__init__( x=x, y=y, width=width, height=height, size_hint=size_hint, size_hint_min=size_hint_min, size_hint_max=size_hint_max, style=style or UISlider.DEFAULT_STYLE, **kwargs, ) 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, "hovered", self.trigger_render) bind(self, "pressed", self.trigger_render) bind(self, "disabled", self.trigger_render) self.register_event_type("on_change") def _x_for_value(self, value: float): """Provides the x coordinate for the given value.""" x = self.content_rect.left val = (value - self.min_value) / (self.max_value - self.min_value) return x + self._cursor_width + val * (self.content_width - 2 * self._cursor_width) @property def norm_value(self): """Normalized value between 0.0 and 1.0""" return (self.value - self.min_value) / self.max_value @norm_value.setter def norm_value(self, value): """Normalized value between 0.0 and 1.0""" self.value = min(value * (self.max_value - self.min_value) + self.min_value, self.max_value) @property def _thumb_x(self): """Returns the current x coordinate of the thumb.""" return self._x_for_value(self.value) @_thumb_x.setter def _thumb_x(self, nx): """Set thumb x coordinate and update the value.""" rect = self.content_rect x = min(rect.right - self._cursor_width, max(nx, rect.left + self._cursor_width)) if self.width == 0: self.norm_value = 0 else: self.norm_value = (x - rect.left - self._cursor_width) / float( self.content_width - 2 * self._cursor_width )
[docs] @override def do_render(self, surface: Surface): """Render the slider, including track and thumb.""" self.prepare_render(surface) self._render_track(surface) self._render_thumb(surface)
@abstractmethod def _render_track(self, surface: Surface): """Render the track of the slider. This method should be implemented in a slider implementation. Track 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. This method should be implemented in a slider implementation. Thumb should stay within self.content_rect. x coordinate of the thumb should be self._thumb_x. Args: surface: Surface to render on. """ pass
[docs] @override def on_event(self, event: UIEvent) -> Optional[bool]: """ Args: event: Event to handle. Returns: True if event was handled, False otherwise. """ if self.disabled: return EVENT_UNHANDLED if super().on_event(event): return EVENT_HANDLED if isinstance(event, UIMouseDragEvent): if self.pressed: old_value = self.value self._thumb_x = event.x self.dispatch_event("on_change", UIOnChangeEvent(self, old_value, self.value)) return EVENT_HANDLED return EVENT_UNHANDLED
[docs] @override def on_click(self, event: UIOnClickEvent): """Handle click events to set the value of the slider. A new value is calculated based on the click position and the slider's width and the `on_change` event is dispatched. Args: event: Click event. """ old_value = self.value self._thumb_x = event.x self.dispatch_event("on_change", UIOnChangeEvent(self, old_value, self.value))
[docs] def on_change(self, event: UIOnChangeEvent): """To be implemented by the user, triggered when the cursor's value is changed. Args: event: Event containing the old and new value of the cursor. """ pass
[docs] @dataclass class UISliderStyle(UIStyleBase): """Used to style the slider for different states. Below is its use case. .. code:: py button = UITextureButton(style={"normal": UITextureButton.UIStyle(...),}) Args: bg: Background color. border: Border color. border_width: Width of the border. filled_track: Color of the filled track. unfilled_track: Color of the unfilled track. """ bg: RGBA255 = uicolor.WHITE_SILVER border: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE border_width: int = 2 filled_track: RGBA255 = uicolor.DARK_BLUE_MIDNIGHT_BLUE unfilled_track: RGBA255 = uicolor.WHITE_SILVER
[docs] class UISlider(UIStyledWidget[UISliderStyle], UIBaseSlider): """A simple slider. A slider consists of a horizontal track and a thumb. The thumb can be moved along the track to set the value of the slider. Use the `on_change` event to get notified about value changes. There are four states of the UISlider i.e. normal, hovered, pressed and disabled. Args: value: Current value of the cursor of the slider. min_value: Minimum value of the slider. max_value: Maximum value of the slider. x: x coordinate of bottom left. y: y coordinate of bottom left. width: Width of the slider. height: Height of the slider. style: Used to style the slider for different states. """ UIStyle = UISliderStyle DEFAULT_STYLE = { "normal": UIStyle(), "hover": UIStyle( border=uicolor.BLUE_PETER_RIVER, border_width=2, filled_track=uicolor.BLUE_PETER_RIVER, ), "press": UIStyle( bg=uicolor.BLUE_PETER_RIVER, border=uicolor.DARK_BLUE_WET_ASPHALT, border_width=3, filled_track=uicolor.BLUE_PETER_RIVER, ), "disabled": UIStyle( bg=uicolor.WHITE_SILVER, border_width=1, filled_track=uicolor.GRAY_ASBESTOS, unfilled_track=uicolor.WHITE_SILVER, ), } def __init__( self, *, value: float = 0, min_value: float = 0, max_value: float = 100, x: float = 0, y: float = 0, width: float = 300, height: float = 25, size_hint=None, size_hint_min=None, size_hint_max=None, style: Union[dict[str, UISliderStyle], None] = None, **kwargs, ): super().__init__( value=value, min_value=min_value, max_value=max_value, x=x, y=y, width=width, height=height, size_hint=size_hint, size_hint_min=size_hint_min, size_hint_max=size_hint_max, style=style or UISlider.DEFAULT_STYLE, **kwargs, )
[docs] @override def get_current_state(self) -> str: """Get the current state of the slider. Returns: ""normal"", ""hover"", ""press"" or ""disabled"". """ if self.disabled: return "disabled" elif self.pressed: return "press" elif self.hovered: return "hover" else: return "normal"
@override def _render_track(self, surface: Surface): style = self.get_current_style() if style is None: warnings.warn(f"No style found for state {self.get_current_state()}", UserWarning) return bg_slider_color = style.get("unfilled_track", UISlider.UIStyle.unfilled_track) fg_slider_color = style.get("filled_track", UISlider.UIStyle.filled_track) slider_height = self.content_height // 3 slider_left_x = self._x_for_value(self.min_value) slider_right_x = self._x_for_value(self.max_value) cursor_center_x = self._thumb_x slider_bottom = (self.content_height - slider_height) // 2 arcade.draw_lbwh_rectangle_filled( slider_left_x - self.content_rect.left, slider_bottom, slider_right_x - slider_left_x, slider_height, bg_slider_color, ) arcade.draw_lbwh_rectangle_filled( slider_left_x - self.content_rect.left, slider_bottom, cursor_center_x - slider_left_x, slider_height, fg_slider_color, ) @override def _render_thumb(self, surface: Surface): style = self.get_current_style() if style is None: warnings.warn(f"No style found for state {self.get_current_state()}", UserWarning) return border_width = style.get("border_width", UISlider.UIStyle.border_width) cursor_color = style.get("bg", UISlider.UIStyle.bg) cursor_outline_color = style.get("border", UISlider.UIStyle.border) cursor_radius = self._cursor_width cursor_center_x = self._thumb_x slider_center_y = self.content_height // 2 rel_cursor_x = cursor_center_x - self.content_rect.left arcade.draw_circle_filled(rel_cursor_x, slider_center_y, cursor_radius, cursor_color) arcade.draw_circle_filled( rel_cursor_x, slider_center_y, cursor_radius // 4, cursor_outline_color ) arcade.draw_circle_outline( rel_cursor_x, slider_center_y, cursor_radius, cursor_outline_color, border_width, )
[docs] class UITextureSlider(UISlider): """A custom slider subclass which supports textures. You can copy this as-is into your own project, or you can modify the class to have more features as needed. Args: track_texture: Texture for the track, should be a NinePatchTexture. thumb_texture: Texture for the thumb. style: Used to style the slider for different states. **kwargs: Passed to UISlider. """ def __init__( self, track_texture: Union[Texture, NinePatchTexture], thumb_texture: Union[Texture, NinePatchTexture], style=None, **kwargs, ): self._track_tex = track_texture self._thumb_tex = thumb_texture super().__init__(style=style or UISlider.DEFAULT_STYLE, **kwargs) @override def _render_track(self, surface: Surface): style = self.get_current_style() if style is None: warnings.warn(f"No style found for state {self.get_current_state()}", UserWarning) return surface.draw_texture(0, 0, self.width, self.height, self._track_tex) # TODO accept these as constructor params slider_height = self.height // 4 slider_left_x = self._x_for_value(self.min_value) cursor_center_x = self._thumb_x slider_bottom = (self.height - slider_height) // 2 # slider if style.filled_track: arcade.draw_lbwh_rectangle_filled( slider_left_x - self.left, slider_bottom, cursor_center_x - slider_left_x, slider_height, style.filled_track, ) @override def _render_thumb(self, surface: Surface): cursor_center_x = self._thumb_x rel_cursor_x = cursor_center_x - self.left surface.draw_texture( x=rel_cursor_x - self._thumb_tex.width // 4 + 2, y=0, width=self._thumb_tex.width // 2, height=self.height, tex=self._thumb_tex, )