"""
pygame-menu
https://github.com/ppizarror/pygame-menu
RANGE SLIDER
Slider bar between one/two numeric ranges.
License:
-------------------------------------------------------------------------------
The MIT License (MIT)
Copyright 2017-2021 Pablo Pizarro R. @ppizarror
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-------------------------------------------------------------------------------
"""
__all__ = [
# Class
'RangeSlider',
# Input types
'RangeSliderRangeValueType',
'RangeSliderValueFormatType',
'RangeSliderValueType'
]
import math
import pygame
import pygame_menu
import pygame_menu.controls as ctrl
from pygame_menu.locals import POSITION_NORTH, POSITION_SOUTH
from pygame_menu.font import FontType, assert_font
from pygame_menu.locals import FINGERUP, FINGERDOWN, FINGERMOTION
from pygame_menu.utils import check_key_pressed_valid, assert_color, assert_vector, \
make_surface, get_finger_pos, assert_position, parse_padding, is_callable
from pygame_menu.widgets.core.widget import Widget, WidgetTransformationNotImplemented
from pygame_menu._types import Any, CallbackType, Union, List, Tuple, Optional, \
ColorType, NumberType, Tuple2IntType, NumberInstance, ColorInputType, \
EventVectorType, Vector2NumberType, VectorType, PaddingType, Tuple4IntType, \
Callable, Dict
RangeSliderRangeValueType = Union[Vector2NumberType, VectorType]
RangeSliderValueFormatType = Callable[[NumberType], str]
RangeSliderValueType = Union[NumberType, Vector2NumberType]
# noinspection PyMissingOrEmptyDocstring
[docs]class RangeSlider(Widget):
"""
Range slider widget. Offers 1 or 2 sliders for defining a unique value or
a range of numeric ones.
If the state of the widget changes the ``onchange`` callback is called. The
state can change by pressing LEFT/RIGHT, or by mouse/touch events.
.. code-block:: python
onchange(range_value, **kwargs)
If pressing return key on the widget:
.. code-block:: python
onreturn(range_value, **kwargs)
.. note::
RangeSlider only accepts translation transformation.
:param title: Range slider title
:param rangeslider_id: RangeSlider ID
:param default_value: Default range value, can accept a number or a tuple/list of 2 elements (min, max). If a single number is provided the rangeslider only accepts 1 value, if 2 are provided, the range is enabled (2 values)
:param range_values: Tuple/list of 2 elements of min/max values of the range slider. Also range can accept a list of numbers, in which case the values of the range slider will be discrete. List must be sorted
:param range_width: Width of the range in px
:param increment: Increment of the value if using left/right keys; used only if the range values are not discrete
:param onchange: Callback when changing the value of the range slider
:param onreturn: Callback when pressing return on the range slider
:param onselect: Function when selecting the widget
:param range_box_color: Color of the range box between the sliders
:param range_box_color_readonly: Color of the range box if widget in readonly state
:param range_box_enabled: Enables a range box between two sliders
:param range_box_height_factor: Height of the range box (factor of the range title height)
:param range_box_single_slider: Enables range box if there's only 1 slider instead of 2
:param range_line_color: Color of the range line
:param range_line_height: Height of the range line in px
:param range_margin: Range margin on x-axis and y-axis (x, y) from title in px
:param range_text_value_color: Color of the range values text
:param range_text_value_enabled: Enables the range values text
:param range_text_value_font: Font of the ranges value. If ``None`` the same font as the widget is used
:param range_text_value_font_height: Height factor of the range value font (factor of the range title height)
:param range_text_value_margin_f: Margin of the range text values (factor of the range title height)
:param range_text_value_position: Position of the range text values, can be NORTH or SOUTH. See :py:mod:`pygame_menu.locals`
:param range_text_value_tick_color: Color of the range text value tick
:param range_text_value_tick_enabled: Range text value tick enabled
:param range_text_value_tick_hfactor: Height factor of the range text value tick (factor of the range title height)
:param range_text_value_tick_number: Number of range value text, the values are placed uniformly distributed
:param range_text_value_tick_thick: Thickness of the range text value tick in px
:param repeat_keys_initial_ms: Time in ms before keys are repeated when held in ms
:param repeat_keys_interval_ms: Interval between key press repetition when held in ms
:param slider_color: Slider color
:param slider_height_factor: Height of the slider (factor of the range title height)
:param slider_sel_highlight_color: Color of the selected slider highlight box effect
:param slider_sel_highlight_enabled: Selected slider is highlighted
:param slider_sel_highlight_thick: Thickness of the selected slider highlight
:param slider_selected_color: Selected slider color
:param slider_text_value_bgcolor: Background color of the value text on each slider
:param slider_text_value_color: Color of value text on each slider
:param slider_text_value_enabled: Enables a value text on each slider
:param slider_text_value_font: Font of the slider value. If ``None`` the same font as the widget is used
:param slider_text_value_font_height: Height factor of the slider font (factor of the range title height)
:param slider_text_value_margin_f: Margin of the slider text values (factor of the range title height)
:param slider_text_value_padding: Padding of the slider text values
:param slider_text_value_position: Position of the slider text values, can be NORTH or SOUTH. See :py:mod:`pygame_menu.locals`
:param slider_text_value_triangle: Draws a triangle between slider text value and slider
:param slider_thickness: Slider thickness in px
:param slider_vmargin: Vertical margin of the slider (factor of the range title height)
:param value_format: Function that format the value and returns a string that is used in the range and slider text
:param args: Optional arguments for callbacks
:param kwargs: Optional keyword arguments
"""
_font_range_value: Optional['pygame.font.Font']
_font_slider_value: Optional['pygame.font.Font']
_increment: NumberType
_increment_shift_factor: float
_keyrepeat_counters: Dict[int, int]
_keyrepeat_initial_interval_ms: NumberType
_keyrepeat_interval_ms: NumberType
_range_box: 'pygame.Surface'
_range_box_color: ColorType
_range_box_color_readonly: ColorType
_range_box_enabled: bool
_range_box_height: int
_range_box_height_factor: NumberType
_range_box_pos: Tuple2IntType
_range_box_single_slider: bool
_range_line: 'pygame.Surface'
_range_line_color: ColorType
_range_line_height: int
_range_line_pos: Tuple2IntType
_range_margin: Tuple2IntType
_range_pos: Tuple2IntType
_range_text_value_color: ColorType
_range_text_value_enabled: bool
_range_text_value_font: Optional[FontType]
_range_text_value_font_height: NumberType
_range_text_value_margin: int
_range_text_value_margin_factor: NumberType
_range_text_value_position: str
_range_text_value_surfaces: List['pygame.Surface']
_range_text_value_surfaces_pos: List[Tuple2IntType]
_range_text_value_tick_color: ColorType
_range_text_value_tick_enabled: bool
_range_text_value_tick_height: int
_range_text_value_tick_height_factor: NumberType
_range_text_value_tick_number: int
_range_text_value_tick_surfaces: List['pygame.Surface']
_range_text_value_tick_surfaces_pos: List[Tuple2IntType]
_range_text_value_tick_thickness: int
_range_values: RangeSliderRangeValueType
_range_width: int
_scrolling: bool # Slider is scrolling
_selected_mouse: bool
_single: bool # Range single or double
_slider: List['pygame.Surface']
_slider_color: ColorType
_slider_height: int
_slider_height_factor: NumberType
_slider_pos: Tuple[Tuple2IntType, Tuple2IntType]
_slider_selected: Tuple[bool, bool]
_slider_selected_color: ColorType
_slider_selected_highlight_color: ColorType
_slider_selected_highlight_enabled: bool
_slider_selected_highlight_thickness: int
_slider_text_value_bgcolor: ColorType
_slider_text_value_color: ColorType
_slider_text_value_enabled: bool
_slider_text_value_font: Optional[FontType]
_slider_text_value_font_height: NumberType
_slider_text_value_margin: int
_slider_text_value_margin_factor: NumberType
_slider_text_value_padding: Tuple4IntType
_slider_text_value_position: str
_slider_text_value_surfaces: List['pygame.Surface']
_slider_text_value_surfaces_pos: List[Tuple2IntType]
_slider_text_value_triangle: bool
_slider_text_value_vmargin: int
_slider_thickness: int
_slider_vmargin: NumberType
_value: List[NumberType] # Public value of the slider, generated from the hidden
_value_format: RangeSliderValueFormatType
_value_hidden: List[NumberType] # Hidden value of the slider, modified by events
def __init__(
self,
title: Any,
rangeslider_id: str = '',
default_value: RangeSliderValueType = 0,
range_values: RangeSliderRangeValueType = (0, 1),
range_width: int = 150,
increment: NumberType = 0.1,
onchange: CallbackType = None,
onreturn: CallbackType = None,
onselect: CallbackType = None,
range_box_color: ColorInputType = (6, 119, 206, 170),
range_box_color_readonly: ColorInputType = (200, 200, 200, 170),
range_box_enabled: bool = True,
range_box_height_factor: NumberType = 0.45,
range_box_single_slider: bool = False,
range_line_color: ColorInputType = (100, 100, 100),
range_line_height: int = 2,
range_margin: Tuple2IntType = (25, 0),
range_text_value_color: ColorInputType = (80, 80, 80),
range_text_value_enabled: bool = True,
range_text_value_font: Optional[FontType] = None,
range_text_value_font_height: NumberType = 0.4,
range_text_value_margin_f: NumberType = 0.8,
range_text_value_position: str = POSITION_SOUTH,
range_text_value_tick_color: ColorInputType = (60, 60, 60),
range_text_value_tick_enabled: bool = True,
range_text_value_tick_hfactor: NumberType = 0.35,
range_text_value_tick_number: int = 2,
range_text_value_tick_thick: int = 1,
repeat_keys_initial_ms: NumberType = 400,
repeat_keys_interval_ms: NumberType = 50,
slider_color: ColorInputType = (120, 120, 120),
slider_height_factor: NumberType = 0.7,
slider_sel_highlight_color: ColorInputType = (0, 0, 0),
slider_sel_highlight_enabled: bool = True,
slider_sel_highlight_thick: int = 1,
slider_selected_color: ColorInputType = (180, 180, 180),
slider_text_value_bgcolor: ColorInputType = (140, 140, 140),
slider_text_value_color: ColorInputType = (0, 0, 0),
slider_text_value_enabled: bool = True,
slider_text_value_font: Optional[FontType] = None,
slider_text_value_font_height: NumberType = 0.4,
slider_text_value_margin_f: NumberType = 1,
slider_text_value_padding: PaddingType = (0, 4),
slider_text_value_position: str = POSITION_NORTH,
slider_text_value_triangle: bool = True,
slider_thickness: int = 15,
slider_vmargin: NumberType = 0,
value_format: RangeSliderValueFormatType = lambda x: str(round(x, 3)),
*args,
**kwargs
) -> None:
super(RangeSlider, self).__init__(
args=args,
kwargs=kwargs,
onchange=onchange,
onreturn=onreturn,
onselect=onselect,
title=title,
widget_id=rangeslider_id
)
# Check ranges
assert_vector(range_values, 0)
assert len(range_values) >= 2, \
'range values length must be equal or greater than 2'
if len(range_values) == 2:
assert range_values[1] > range_values[0], \
'range values must be increasing and different numeric values'
else:
for i in range(len(range_values) - 1):
assert range_values[i] < range_values[i + 1], \
'range values list must be ordered and different'
assert isinstance(range_width, int)
assert range_width > 0, 'range width must be greater than zero'
# Check default value
if not isinstance(default_value, NumberInstance):
assert_vector(default_value, 2)
assert default_value[0] < default_value[1], \
'default value vector must be ordered and different (min,max)'
assert default_value[0] >= range_values[0], \
f'minimum default value ({default_value[0]}) must be equal or ' \
f'greater than minimum value of the range ({range_values[0]})'
assert default_value[1] <= range_values[-1], \
f'maximum default value ({default_value[1]}) must be lower or ' \
f'equal than maximum value of the range ({range_values[-1]})'
default_value = tuple(default_value)
else:
assert range_values[0] <= default_value <= range_values[-1], \
f'default value ({default_value}) must be between minimum and maximum' \
f' of the range values ({range_values[0]}, {range_values[-1]}), that ' \
f'is, it must satisfy {range_values[0]}<={default_value}<={range_values[-1]}'
# If range is discrete, check default value within list
if len(range_values) > 2:
if not isinstance(default_value, NumberInstance):
assert default_value[0] in range_values, \
f'min default value ({default_value[0]}) must be within range'
assert default_value[1] in range_values, \
f'max default value ({default_value[1]}) must be within range'
else:
assert default_value in range_values, \
f'default value ({default_value}) must be within range values'
# Check increment
assert isinstance(increment, NumberInstance)
assert increment >= 0, 'increment must be equal or greater than zero'
# Check fonts
if range_text_value_font is not None:
assert_font(range_text_value_font)
assert isinstance(range_text_value_font_height, NumberInstance)
assert 0 < range_text_value_font_height, \
'range text value font height must be greater than zero'
if slider_text_value_font is not None:
assert_font(slider_text_value_font)
assert isinstance(slider_text_value_font_height, NumberInstance)
assert 0 < slider_text_value_font_height, \
'slider text value font height must be greater than zero'
# Check colors
range_box_color = assert_color(range_box_color)
range_box_color_readonly = assert_color(range_box_color_readonly)
range_line_color = assert_color(range_line_color)
range_text_value_color = assert_color(range_text_value_color)
range_text_value_tick_color = assert_color(range_text_value_tick_color)
slider_color = assert_color(slider_color)
slider_selected_color = assert_color(slider_selected_color)
slider_sel_highlight_color = assert_color(slider_sel_highlight_color)
slider_text_value_bgcolor = assert_color(slider_text_value_bgcolor)
slider_text_value_color = assert_color(slider_text_value_color)
# Check dimensions and sizes
assert isinstance(range_box_height_factor, NumberInstance)
assert 0 < range_box_height_factor, \
'height factor must be greater than zero'
assert isinstance(range_text_value_margin_f, NumberInstance)
assert 0 < range_text_value_margin_f, \
'height factor must be greater than zero'
assert isinstance(slider_text_value_margin_f, NumberInstance)
assert 0 < slider_text_value_margin_f, \
'height factor must be greater than zero'
assert isinstance(slider_height_factor, NumberInstance)
assert 0 < slider_height_factor, \
'height factor must be greater than zero'
assert isinstance(slider_vmargin, NumberInstance)
slider_text_value_padding = parse_padding(slider_text_value_padding)
assert isinstance(slider_thickness, int)
assert slider_thickness > 0, \
'slider thickness must be greater than zero'
assert isinstance(range_line_height, int)
assert range_line_height >= 0, \
'range line height must be equal or greater than zero'
assert_vector(range_margin, 2, int)
assert isinstance(range_text_value_tick_number, int)
if range_text_value_enabled:
assert range_text_value_tick_number >= 2, \
'number of range value must be equal or greater than 2'
assert isinstance(range_text_value_tick_thick, int)
assert range_text_value_tick_thick >= 1, \
'range text tick thickness must be equal or greater than 1 px'
assert isinstance(slider_sel_highlight_thick, int)
assert slider_sel_highlight_thick >= 1, \
'selected highlight thickness must be equal or greater than 1 px'
# Check positions
assert_position(range_text_value_position)
assert range_text_value_position in (POSITION_NORTH, POSITION_SOUTH), \
'range text value position must be north or south'
assert_position(slider_text_value_position)
assert slider_text_value_position in (POSITION_NORTH, POSITION_SOUTH), \
'slider text value position must be north or south'
# Check boolean
assert isinstance(range_box_enabled, bool)
assert isinstance(range_box_single_slider, bool)
assert isinstance(range_text_value_enabled, bool)
assert isinstance(range_text_value_enabled, bool)
assert isinstance(slider_sel_highlight_enabled, bool)
assert isinstance(slider_text_value_enabled, bool)
assert isinstance(slider_text_value_triangle, bool)
# Check the value format function
assert is_callable(value_format)
assert isinstance(value_format(0), str), \
'value_format must be a function that accepts only 1 argument ' \
'(value) and must return a string'
# Single value
single = isinstance(default_value, NumberInstance)
# Convert default value to list
if isinstance(default_value, NumberInstance):
default_value = [default_value, 0]
else:
default_value = [default_value[0], default_value[1]]
# Store properties
self._clock = pygame.time.Clock()
self._default_value = tuple(default_value)
self._increment = increment
self._increment_shift_factor = 0.5
self._keyrepeat_counters = {} # {event.key: (counter_int, event.unicode)} (look for "***")
self._keyrepeat_initial_interval_ms = repeat_keys_initial_ms
self._keyrepeat_interval_ms = repeat_keys_interval_ms
self._range_box_color = range_box_color
self._range_box_color_readonly = range_box_color_readonly
self._range_box_enabled = range_box_enabled
self._range_box_height_factor = range_box_height_factor
self._range_box_single_slider = range_box_single_slider
self._range_line_color = range_line_color
self._range_line_height = range_line_height
self._range_margin = range_margin
self._range_pos = (0, 0)
self._range_text_value_color = range_text_value_color
self._range_text_value_enabled = range_text_value_enabled
self._range_text_value_font = range_text_value_font
self._range_text_value_font_height = range_text_value_font_height
self._range_text_value_margin = 0
self._range_text_value_margin_factor = range_text_value_margin_f
self._range_text_value_position = range_text_value_position
self._range_text_value_tick_color = range_text_value_tick_color
self._range_text_value_tick_enabled = range_text_value_tick_enabled
self._range_text_value_tick_height = 0
self._range_text_value_tick_height_factor = range_text_value_tick_hfactor
self._range_text_value_tick_number = range_text_value_tick_number
self._range_text_value_tick_thickness = range_text_value_tick_thick
self._range_values = tuple(range_values)
self._range_width = range_width
self._scrolling = False
self._selected_mouse = False
self._single = single
self._slider_color = slider_color
self._slider_height = 0
self._slider_height_factor = slider_height_factor
self._slider_selected = (True, False)
self._slider_selected_color = slider_selected_color
self._slider_selected_highlight_color = slider_sel_highlight_color
self._slider_selected_highlight_enabled = slider_sel_highlight_enabled
self._slider_selected_highlight_thickness = slider_sel_highlight_thick
self._slider_text_value_bgcolor = slider_text_value_bgcolor
self._slider_text_value_color = slider_text_value_color
self._slider_text_value_enabled = slider_text_value_enabled
self._slider_text_value_font = slider_text_value_font
self._slider_text_value_font_height = slider_text_value_font_height
self._slider_text_value_margin_factor = slider_text_value_margin_f
self._slider_text_value_padding = slider_text_value_padding
self._slider_text_value_position = slider_text_value_position
self._slider_text_value_triangle = slider_text_value_triangle
self._slider_text_value_vmargin = 0
self._slider_thickness = slider_thickness
self._slider_vmargin = slider_vmargin
self._value = default_value
self._value_format = value_format
self._value_hidden = default_value.copy() # Used when dragging mouse on discrete range
[docs] def value_changed(self) -> bool:
if self._single:
return self.get_value() != self._default_value[0]
return self.get_value() != self._default_value
[docs] def reset_value(self) -> 'Widget':
if self._single:
self.set_value(self._default_value[0])
else:
self.set_value(self._default_value)
return self
[docs] def set_value(self, value: RangeSliderValueType) -> None:
if self._single:
assert isinstance(value, NumberInstance)
# noinspection PyTypeChecker
assert self._range_values[0] <= value <= self._range_values[-1], \
f'value ({value}) must be within range {self._range_values[0]} <=' \
f' {value} <= {self._range_values[1]}'
if len(self._range_values) > 2:
assert value in self._range_values, \
'value must be between range values discrete list'
value = [value, 0]
else:
assert_vector(value, 2)
assert self._range_values[0] <= value[0], \
'value must be equal or greater than minimum range value'
assert value[1] <= self._range_values[-1], \
'value must be lower or equal than maximum range value'
assert value[0] < value[1], 'value vector must be ordered'
if len(self._range_values) > 2:
assert value[0] in self._range_values
assert value[1] in self._range_values
value = [value[0], value[1]]
self._value = value
self._value_hidden = self._value.copy()
self._render()
def scale(self, *args, **kwargs) -> 'RangeSlider':
raise WidgetTransformationNotImplemented()
def resize(self, *args, **kwargs) -> 'RangeSlider':
raise WidgetTransformationNotImplemented()
def set_max_width(self, *args, **kwargs) -> 'RangeSlider':
raise WidgetTransformationNotImplemented()
def set_max_height(self, *args, **kwargs) -> 'RangeSlider':
raise WidgetTransformationNotImplemented()
def rotate(self, *args, **kwargs) -> 'RangeSlider':
raise WidgetTransformationNotImplemented()
def flip(self, *args, **kwargs) -> 'RangeSlider':
raise WidgetTransformationNotImplemented()
[docs] def get_value(self) -> Union[NumberType, Tuple[NumberType, NumberType]]:
if self._single:
return self._value[0]
return self._value[0], self._value[1]
def _apply_font(self) -> None:
if self._range_text_value_font is None:
self._range_text_value_font = self._font_name
if self._slider_text_value_font is None:
self._slider_text_value_font = self._font_name
self._font_range_value = pygame_menu.font.get_font(
self._range_text_value_font,
int(self._range_text_value_font_height * self._font_size)
)
self._font_slider_value = pygame_menu.font.get_font(
self._slider_text_value_font,
int(self._slider_text_value_font_height * self._font_size)
)
# Compute the height
height = self._font_render_string('TEST').get_height()
self._range_box_height = int(height * self._range_box_height_factor)
self._slider_height = int(height * self._slider_height_factor)
self._range_text_value_margin = int(height * self._range_text_value_margin_factor)
self._range_text_value_tick_height = int(height * self._range_text_value_tick_height_factor)
self._slider_text_value_margin = int(height * self._slider_text_value_margin_factor)
def _draw(self, surface: 'pygame.Surface') -> None:
# Draw title
surface.blit(self._surface, self._rect.topleft)
# Draw range line
surface.blit(self._range_line, (self._range_line_pos[0] + self._rect.x,
self._range_line_pos[1] + self._rect.y))
# Draw range values and ticks
for i in range(len(self._range_text_value_surfaces)):
if self._range_text_value_enabled:
surface.blit(self._range_text_value_surfaces[i],
(self._rect.x + self._range_text_value_surfaces_pos[i][0],
self._rect.y + self._range_text_value_surfaces_pos[i][1]))
if self._range_text_value_tick_enabled:
tick_i = self._range_text_value_tick_surfaces[i]
surface.blit(tick_i,
(self._rect.x + self._range_text_value_tick_surfaces_pos[i][0],
self._rect.y + self._range_text_value_tick_surfaces_pos[i][1]))
# Draw range box
if self._range_box_enabled and (not self._single or self._range_box_single_slider):
surface.blit(self._range_box, (self._range_box_pos[0] + self._rect.x,
self._range_box_pos[1] + self._rect.y))
# Draw sliders
surface.blit(self._slider[0], (self._slider_pos[0][0] + self._rect.x,
self._slider_pos[0][1] + self._rect.y))
if not self._single:
surface.blit(self._slider[1], (self._slider_pos[1][0] + self._rect.x,
self._slider_pos[1][1] + self._rect.y))
# Draw slider highlighted
if self._slider_selected[0] and not self.readonly and self.is_selected():
pygame.draw.rect(
surface, self._slider_selected_highlight_color,
self._get_slider_inflate_rect(0), self._slider_selected_highlight_thickness
)
if self._slider_selected[1] and not self.readonly and self.is_selected():
pygame.draw.rect(
surface, self._slider_selected_highlight_color,
self._get_slider_inflate_rect(1), self._slider_selected_highlight_thickness
)
[docs] def draw_after_if_selected(self, surface: Optional['pygame.Surface']) -> 'RangeSlider':
super(RangeSlider, self).draw_after_if_selected(surface)
self.last_surface = surface
# Draw slider value
if self._slider_text_value_enabled and not self.readonly:
surface.blit(
self._slider_text_value_surfaces[0],
(self._slider_text_value_surfaces_pos[0][0] + self._rect.x,
self._slider_text_value_surfaces_pos[0][1] + self._rect.y)
)
if not self._single:
surface.blit(
self._slider_text_value_surfaces[1],
(self._slider_text_value_surfaces_pos[1][0] + self._rect.x,
self._slider_text_value_surfaces_pos[1][1] + self._rect.y)
)
return self
def _get_slider_inflate_rect(
self,
pos: int,
inflate: Optional[Tuple2IntType] = None,
to_real_position: bool = False,
to_absolute_position: bool = False,
real_position_visible: bool = True
) -> 'pygame.Rect':
"""
Return the slider inflate rect.
:param pos: Which slider 0/1
:param inflate: Inflate in x, y
:param to_real_position: Transform the widget rect to real coordinates. Used by events
:param to_absolute_position: Transform the widget rect to absolute coordinates. Used by events
:param real_position_visible: Return only the visible width/height if ``to_real_position=True``
:return: Slider inflated rect
"""
if inflate is None:
inflate = (0, 0)
s = self._slider[pos]
rect = s.get_rect()
rect.x += self._slider_pos[pos][0] + self._rect.x
rect.y += self._slider_pos[pos][1] + self._rect.y
rect = pygame.Rect(
int(rect.x - inflate[0] / 2),
int(rect.y - inflate[1] / 2),
int(rect.width + inflate[0]),
int(rect.height + inflate[1])
)
if self._scrollarea is not None:
assert not (to_real_position and to_absolute_position), \
'real and absolute positions cannot be True at the same time'
if to_real_position:
rect = self._scrollarea.to_real_position(rect, visible=real_position_visible)
elif to_absolute_position:
rect = self._scrollarea.to_absolute_position(rect)
return rect
def _render(self) -> Optional[bool]:
if not hasattr(self, '_font_range_value'):
return False
if not self._render_hash_changed(
self._selected, self._title, self._visible, self.readonly,
self._range_values, self._slider_selected, self._value[0],
self._value[1], self._scrolling, self._selected_mouse):
return True
# Create basic title
self._surface = self._render_string(self._title, self.get_font_color_status())
self._rect.width, self._rect.height = self._surface.get_size()
self._range_pos = (self._rect.width + self._range_margin[0],
int(self._rect.height / 2) + self._range_margin[1])
# Create slider
sel_s = self._slider_selected[0] and self._selected and not self.readonly, \
self._slider_selected[1] and self._selected and not self.readonly
self._slider = [
make_surface(self._slider_thickness, self._slider_height,
fill_color=(self._slider_color if not self.readonly else self._font_readonly_color)
if not sel_s[0] else self._slider_selected_color),
make_surface(self._slider_thickness, self._slider_height,
fill_color=(self._slider_color if not self.readonly else self._font_readonly_color)
if not sel_s[1] else self._slider_selected_color)
]
slider_vm = int(self._slider_vmargin * self._rect.height) # Vertical margin
if self._single:
self._slider_pos = (
(self._range_pos[0] + self._get_pos_range(self._value[0], self._slider[0]),
self._range_pos[1] + slider_vm - int(self._slider_height / 2)),
(0, 0)
)
else:
self._slider_pos = (
(self._range_pos[0] + self._get_pos_range(self._value[0], self._slider[0]),
self._range_pos[1] + slider_vm - int(self._slider_height / 2)),
(self._range_pos[0] + self._get_pos_range(self._value[1], self._slider[1]),
self._range_pos[1] + slider_vm - int(self._slider_height / 2))
)
# Create the range line
self._range_line = make_surface(self._range_width, self._range_line_height,
fill_color=self._range_line_color)
self._range_line_pos = (self._range_pos[0],
int(self._range_pos[1] - self._range_line_height / 2))
# Create the range font surfaces
range_values: List[NumberType] = []
if len(self._range_values) == 2:
d_val = (self._range_values[1] - self._range_values[0]) \
/ (self._range_text_value_tick_number - 1)
v_i = self._range_values[0]
for i in range(self._range_text_value_tick_number):
range_values.append(v_i)
v_i += d_val
else:
for i in self._range_values:
range_values.append(i)
# Create surfaces for each range value text & position + ticks
self._range_text_value_surfaces = []
self._range_text_value_surfaces_pos = []
self._range_text_value_tick_surfaces = []
self._range_text_value_tick_surfaces_pos = []
range_value_pos = 1 if self._range_text_value_position == POSITION_SOUTH else -1
for i in range_values:
s = self._font_range_value.render(
self._value_format(i), self._font_antialias,
self._range_text_value_color if not self.readonly else self._font_readonly_color)
self._range_text_value_surfaces.append(s)
s_x = self._range_pos[0] + self._get_pos_range(i, s)
s_y = self._range_pos[1] + self._range_text_value_margin / 2 * range_value_pos
self._range_text_value_surfaces_pos.append((int(s_x), int(s_y)))
# Create the tick
s_tick = make_surface(self._range_text_value_tick_thickness,
self._range_text_value_tick_height,
fill_color=self._range_text_value_tick_color)
self._range_text_value_tick_surfaces.append(s_tick)
t_x = self._range_pos[0] + self._get_pos_range(i)
t_y = self._range_pos[1] - s_tick.get_height() / 2
self._range_text_value_tick_surfaces_pos.append((int(t_x), int(t_y)))
# Computes the height of the ranges value text for modifying the rect box sizing
range_values_size = 0, 0 # Stores sizing
if self._range_text_value_enabled:
s = self._range_text_value_surfaces[0]
range_values_size = (int(s.get_width() * 0.7),
int(self._range_text_value_surfaces_pos[0][1] + s.get_height() * 0.9))
# Create the range box surface
if not self._single or self._range_box_single_slider:
r_pos = 0 if self._single else self._get_pos_range(self._value[0])
r_width = self._get_distance_between_sliders()
self._range_box = make_surface(
max(0, r_width), self._range_box_height,
fill_color=self._range_box_color if not self.readonly else self._range_box_color_readonly)
self._range_box_pos = (self._range_pos[0] + r_pos,
self._range_pos[1] - int(self._range_box.get_height() / 2))
# Create the slider values
self._slider_text_value_surfaces = []
self._slider_text_value_surfaces_pos = []
for v in self._value:
t = self._font_slider_value.render(self._value_format(v), self._font_antialias,
self._slider_text_value_color) # Value text
st = make_surface(
t.get_width() + self._slider_text_value_padding[1] + self._slider_text_value_padding[3],
t.get_height() + self._slider_text_value_padding[0] + self._slider_text_value_padding[2],
fill_color=self._slider_text_value_bgcolor)
st.blit(t, (self._slider_text_value_padding[1],
self._slider_text_value_padding[0] + self._slider_text_value_vmargin))
# Create surface that considers st and the triangle
tri_height = int(self._slider_text_value_margin / 2) - int(self._slider_height / 2)
if tri_height and self._slider_text_value_triangle:
st_root = make_surface(st.get_width(), st.get_height() + tri_height)
st_root.blit(st, (0, 0))
mid = st.get_width() / 2
hig = st.get_height()
w = tri_height / math.sqrt(3)
tri_poly = ((mid - w, hig), (mid + w, hig), (mid, hig + tri_height))
pygame.draw.polygon(st_root, self._slider_text_value_bgcolor, tri_poly)
else:
st_root = st
self._slider_text_value_surfaces.append(st_root)
st_x = self._range_pos[0] + self._get_pos_range(v, st)
st_y = self._range_pos[1] - int(self._slider_text_value_margin / 2) - st.get_height()
self._slider_text_value_surfaces_pos.append((st_x, st_y))
# Update maximum rect height
self._rect.height = max(self._rect.height, self._slider_height,
self._range_line_height, range_values_size[1])
self._rect.height += self._range_margin[1]
self._rect.width += self._range_width + self._range_margin[0] + range_values_size[0]
# Finals
self.force_menu_surface_update()
def _get_pos_range(self, value: NumberType, surface: Optional['pygame.Surface'] = None) -> int:
"""
Return the position of the surface within range slider.
:param value: Value
:param surface: Surface or None
:return: Position in px
"""
sw = surface.get_width() / 2 if surface is not None else 0
if len(self._range_values) == 2:
d = float(value - self._range_values[0]) / (self._range_values[1] - self._range_values[0])
return int(d * self._range_width - sw)
# Find nearest position
n, t = self._find_nearest_discrete_range(value), len(self._range_values)
return int((float(n) / (t - 1)) * self._range_width - sw)
def _get_distance_between_sliders(self) -> int:
"""
Returns the distance between both sliders.
:return: Distance in px
"""
if not self._single:
return self._get_pos_range(self._value[1]) - self._get_pos_range(self._value[0])
return self._get_pos_range(self._value[0])
def _find_nearest_discrete_range(self, value: NumberType) -> int:
"""
Return the nearest position of value from range discrete list.
:param value: Value to find
:return: Position of the list
"""
n = 0 # Position of the nearest value
m = math.inf # Maximum distance
t = len(self._range_values) # Number of values
for j in range(t):
k = abs(self._range_values[j] - value)
if k < m:
m = k # Update max
n = j
return n
def _update_value(self, delta: NumberType) -> bool:
"""
Updates the value of the active slider by delta.
:param delta: Delta value
:return: True if value changed
"""
if self.readonly:
return False
old_value = self._value.copy()
old_value_hidden = self._value_hidden.copy()
slider_idx = 0 if self._slider_selected[0] else 1
self._value_hidden[slider_idx] += delta
if self._value_hidden[0] >= self._value_hidden[1] and not self._single:
self._value_hidden = old_value_hidden
else:
self._value_hidden[slider_idx] = max(
self._range_values[0], min(self._range_values[-1], self._value_hidden[slider_idx]))
# Update real value
if len(self._range_values) == 2:
self._value = self._value_hidden
else:
# Find nearest
val_index_nearest = self._find_nearest_discrete_range(self._value_hidden[slider_idx])
self._value[slider_idx] = self._range_values[val_index_nearest]
if self._value[0] >= self._value[1] and not self._single:
self._value = old_value
changed = old_value_hidden != self._value_hidden
if changed:
self.change()
return changed
def _blur(self) -> None:
self._selected_mouse = False
def _focus(self) -> None:
self._selected_mouse = False
def _left_right(self, event, left: bool) -> bool:
"""
Process left and right event keys.
:param event: Event
:param left: ``True`` if left, right otherwise
:return: ``True`` if updated
"""
self._value_hidden = self._value.copy() # Update hidden to real value
if event.key not in self._keyrepeat_counters:
self._keyrepeat_counters[event.key] = 0
keys_pressed = pygame.key.get_pressed()
# If not discrete, apply delta as increment
if len(self._range_values) == 2:
mod = 1 if not (keys_pressed[pygame.K_LSHIFT] or keys_pressed[pygame.K_RSHIFT]) \
else self._increment_shift_factor
if left:
mod *= -1
delta = self._increment * mod
# If discrete, find the current index and subtract +-1, then find the delta
# as the difference between two states
else:
if not self._single:
slider_idx = 0 if self._slider_selected[0] else 1
else:
slider_idx = 0
current_val_idx = self._find_nearest_discrete_range(self._value[slider_idx])
new_val_idx = current_val_idx
if left:
new_val_idx = max(0, new_val_idx - 1)
else:
new_val_idx = min(len(self._range_values) - 1, new_val_idx + 1)
delta = self._range_values[new_val_idx] - self._range_values[current_val_idx]
if self._update_value(delta):
self._sound.play_key_add()
return True
return False
def _test_get_pos_value(self, value: NumberType, dx: int = 0, dy: int = 0) -> Tuple2IntType:
"""
Return the position of the value in real coordinates, used for testing.
:param value: Value to get position
:param dx: Delta for x position in px
:param dy: Delta for y position in px
:return: (x, y) position
"""
rect = self.get_rect(to_real_position=True, apply_padding=False)
rect.x += self._range_pos[0] + self._get_pos_range(value)
rect.y += self._range_pos[1]
return rect.x + dx, rect.y + dy
[docs] def update(self, events: EventVectorType) -> bool:
self.apply_update_callbacks(events)
self._clock.tick(60)
if self.readonly or not self.is_visible():
return False
# Get time clock
time_clock = self._clock.get_time()
updated = False
events = self._merge_events(events) # Extend events with custom events
for event in events:
if event.type == pygame.KEYDOWN: # Check key is valid
if self._ignores_keyboard_nonphysical() and not check_key_pressed_valid(event):
continue
# Check mouse over
self._check_mouseover(event)
# Events
keydown = self._keyboard_enabled and event.type == pygame.KEYDOWN
joy_hatmotion = self._joystick_enabled and event.type == pygame.JOYHATMOTION
joy_axismotion = self._joystick_enabled and event.type == pygame.JOYAXISMOTION
joy_button_down = self._joystick_enabled and event.type == pygame.JOYBUTTONDOWN
# Left button
if keydown and event.key == ctrl.KEY_LEFT or \
joy_hatmotion and event.value == ctrl.JOY_LEFT or \
joy_axismotion and event.axis == ctrl.JOY_AXIS_X and event.value < ctrl.JOY_DEADZONE:
if self._left_right(event, True):
return True
# Right button
elif keydown and event.key == ctrl.KEY_RIGHT or \
joy_hatmotion and event.value == ctrl.JOY_RIGHT or \
joy_axismotion and event.axis == ctrl.JOY_AXIS_X and event.value > -ctrl.JOY_DEADZONE:
if self._left_right(event, False):
return True
# Press enter
elif keydown and event.key == ctrl.KEY_APPLY or \
joy_button_down and event.button == ctrl.JOY_BUTTON_SELECT:
self.apply()
return True
# Tab, switch active slider
elif keydown and event.key == ctrl.KEY_TAB:
if self._single:
continue
self._slider_selected = (False, True) if self._slider_selected[0] else (True, False)
return True
# Releases key
elif event.type == pygame.KEYUP and self._keyboard_enabled:
if event.key in self._keyrepeat_counters:
del self._keyrepeat_counters[event.key]
# User clicks the slider rect
elif event.type == pygame.MOUSEBUTTONDOWN and self._mouse_enabled or \
event.type == FINGERDOWN and self._touchscreen_enabled and \
self._menu is not None:
event_pos = get_finger_pos(self._menu, event)
# Check which slider is clicked
rect_slider_0 = self._get_slider_inflate_rect(0, to_real_position=True)
rect_slider_1 = self._get_slider_inflate_rect(1, to_real_position=True)
rc_1 = rect_slider_0.collidepoint(*event_pos)
rc_2 = rect_slider_1.collidepoint(*event_pos)
old_slider_selected = self._slider_selected
if not self._single:
# Check sliders does not collide each other
dist_sliders = self._get_distance_between_sliders()
sliders_intersect = dist_sliders <= (self._slider_thickness - 1)
if rc_1 and not sliders_intersect:
self._slider_selected = (True, False)
elif rc_2 and not sliders_intersect:
self._slider_selected = (False, True)
if old_slider_selected != self._slider_selected:
updated = True
self._render()
# Check if slider is clicked
self._scrolling = bool(rc_1 or rc_2)
self._selected_mouse = True
# User releases the mouse
elif event.type == pygame.MOUSEBUTTONUP and self._mouse_enabled and \
event.button in (1, 2, 3) or \
event.type == FINGERUP and self._touchscreen_enabled and \
self._menu is not None:
event_pos = get_finger_pos(self._menu, event)
# If collides and not scroll, update the value to the clicked position
rect = self.get_rect(to_real_position=True, apply_padding=False)
if rect.collidepoint(*event_pos) and not self._scrolling and self._selected_mouse:
mouse_x, _ = event_pos
topleft, _ = rect.topleft
topright, _ = rect.topright
# Distance from title
dist = mouse_x - (topleft + self._range_pos[0])
# Current slider active value
val = self._value[0] if self._single else self._value[0 if self._slider_selected[0] else 1]
val_px = self._get_pos_range(val)
delta = (self._range_values[-1] - self._range_values[0]) * (dist - val_px) / self._range_width
if delta != 0 and dist >= 0: # If clicked true value
if self._update_value(delta):
updated = True
self._selected_mouse = False
# Disables scrolling
if self._scrolling:
self._scrolling = False
self._value_hidden = self._value.copy()
updated = True
# User scrolls clicked slider
elif (event.type == pygame.MOUSEMOTION and self._mouse_enabled and hasattr(event, 'rel') or
event.type == FINGERMOTION and self._touchscreen_enabled and self._menu is not None) and \
self._scrolling and self._selected_mouse:
rel = event.rel[0] if event.type == pygame.MOUSEMOTION else \
event.dx * 2 * self._menu.get_window_size()[0]
delta = (self._range_values[-1] - self._range_values[0]) * rel / self._range_width
# Check mouse position
mx, my = event.pos if event.type == pygame.MOUSEMOTION else \
get_finger_pos(self._menu, event)
rect = self.get_rect(to_real_position=True, apply_padding=False)
# Compute position of mouse within valid range
dist_x = mx - (rect.x + self._range_pos[0])
dist_y = my - rect.y
# Get current slider pos if not single
x_max_min = 0, self._range_width
if not self._single:
slider_idx = 0 if self._slider_selected[0] else 1
slider_pos = self._get_pos_range(self._value[slider_idx])
x_max_min = slider_pos - 1, slider_pos
# Check slider within rect
if delta < 0:
in_x = -self._slider_height <= dist_x <= x_max_min[1]
else:
in_x = x_max_min[0] <= dist_x <= self._range_width + self._slider_height
in_y = 0 <= dist_y <= rect.height
# Checks mouse changed position and within rect position
if delta != 0 and in_x and in_y:
if self._update_value(delta):
updated = True
# Update key counters:
for key in self._keyrepeat_counters:
self._keyrepeat_counters[key] += time_clock # Update clock
# Generate new key events if enough time has passed:
if self._keyrepeat_counters[key] >= self._keyrepeat_initial_interval_ms:
self._keyrepeat_counters[key] = self._keyrepeat_initial_interval_ms - self._keyrepeat_interval_ms
self._add_event(pygame.event.Event(pygame.KEYDOWN, key=key))
return updated