"""
pygame-menu
https://github.com/ppizarror/pygame-menu
RANGE SLIDER
Slider bar between one/two numeric ranges.
"""
from __future__ import annotations
__all__ = [
# Class
"RangeSlider",
"RangeSliderManager",
# Input types
"RangeSliderRangeValueType",
"RangeSliderValueFormatType",
"RangeSliderValueType",
]
import math
from abc import ABC
from collections.abc import Callable
from typing import Any, Union
import pygame
import pygame_menu
from pygame_menu._types import (
CallbackType,
ColorInputType,
ColorType,
EventVectorType,
NumberInstance,
NumberType,
PaddingType,
Tuple2IntType,
Tuple4IntType,
Vector2NumberType,
VectorType,
)
from pygame_menu.font import FontType, assert_font
from pygame_menu.locals import (
FINGERDOWN,
FINGERMOTION,
FINGERUP,
POSITION_NORTH,
POSITION_SOUTH,
)
from pygame_menu.utils import (
assert_color,
assert_position,
assert_vector,
check_key_pressed_valid,
get_finger_pos,
make_surface,
parse_padding,
)
from pygame_menu.widgets.core.widget import (
AbstractWidgetManager,
Widget,
WidgetTransformationNotImplemented,
)
RangeSliderRangeValueType = Union[Vector2NumberType, VectorType]
RangeSliderValueFormatType = Callable[[NumberType], str]
RangeSliderValueType = Union[NumberType, Vector2NumberType]
[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 (apply) 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 (apply) 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: Enable key repeat
:param repeat_keys_initial_ms: Time in milliseconds before keys are repeated when held in milliseconds
:param repeat_keys_interval_ms: Interval between key press repetition when held in milliseconds
: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: pygame.font.Font | None
_font_slider_value: pygame.font.Font | None
_increment: NumberType
_increment_shift_factor: float
_keyrepeat: bool
_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: FontType | None
_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: FontType | None
_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: FontType | None = 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: bool = True,
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: FontType | None = 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().__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 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"
)
# Check keyrepeat
assert isinstance(repeat_keys, bool)
assert isinstance(repeat_keys_initial_ms, NumberInstance)
assert isinstance(repeat_keys_interval_ms, NumberInstance)
assert repeat_keys_initial_ms > 0, (
"repeat keys initial ms cannot be lower or equal than zero"
)
assert repeat_keys_interval_ms > 0, (
"repeat keys interval ms cannot be lower or equal than zero"
)
# 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 = repeat_keys
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) # type: ignore
return self
[docs]
def set_default_value(self, value: RangeSliderValueType) -> RangeSlider:
"""
Set the RangeSlider default value.
Ensures the default value is stored in the correct internal format.
:param value: Default range slider value, can be a number or a tuple/list of 2 elements.
:return: Self reference
"""
# Call the existing set_value to handle validation and internal format conversion
# This ensures 'value' is valid and updates self._value to the correct internal list format.
self.set_value(value)
# Store the internally formatted _value (which is a list like [val, 0] or [min, max])
# as the new _default_value. Convert to tuple for immutability and consistency with __init__.
self._default_value = tuple(self._value)
return self
[docs]
def set_value(self, value: RangeSliderValueType) -> None:
if self._single:
assert isinstance(value, NumberInstance)
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) -> 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: pygame.Surface | None) -> RangeSlider:
super().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: Tuple2IntType | None = 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) -> bool | None:
if not hasattr(self, "_font_range_value"):
return False
elif 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()
return None
def _get_pos_range(
self, value: NumberType, surface: pygame.Surface | None = 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 the 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():
self._readonly_check_mouseover(events)
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 self._ctrl.left(event, self)
or joy_hatmotion
and self._ctrl.joy_left(event, self)
or joy_axismotion
and self._ctrl.joy_axis_x_left(event, self)
):
if self._left_right(event, True):
return True
# Right button
elif (
keydown
and self._ctrl.right(event, self)
or joy_hatmotion
and self._ctrl.joy_right(event, self)
or joy_axismotion
and self._ctrl.joy_axis_x_right(event, self)
):
if self._left_right(event, False):
return True
# Press enter
elif (
keydown
and self._ctrl.apply(event, self)
or joy_button_down
and self._ctrl.joy_select(event, self)
):
self.apply()
return True
# Tab, switch active slider
elif keydown and self._ctrl.tab(event, self):
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
# 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 (
self._scrolling
and self._selected_mouse
and (
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
)
):
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
if self._keyrepeat:
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
class RangeSliderManager(AbstractWidgetManager, ABC):
"""
RangeSlider manager.
"""
def range_slider(
self,
title: str,
default: RangeSliderValueType,
range_values: RangeSliderRangeValueType,
increment: NumberType | None = None,
onchange: CallbackType = None,
onreturn: CallbackType = None,
onselect: Callable[[bool, Widget, pygame_menu.Menu], Any] | None = None,
rangeslider_id: str = "",
value_format: RangeSliderValueFormatType = lambda x: str(round(x, 3)),
width: int = 150,
**kwargs,
) -> pygame_menu.widgets.RangeSlider:
"""
Add a range slider to the Menu: 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 (apply) on the widget:
.. code-block:: python
onreturn(range_value, **kwargs)
If ``onselect`` is defined, the callback is executed as follows, where
``selected`` is a boolean representing the selected status:
.. code-block:: python
onselect(selected, widget, menu)
kwargs (Optional)
- ``align`` (str) – Widget `alignment <https://pygame-menu.readthedocs.io/en/latest/_source/themes.html#alignment>`_
- ``background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, :py:class:`pygame_menu.baseimage.BaseImage`) – Color of the background. ``None`` for no-color
- ``background_inflate`` (tuple, list) – Inflate background on x-axis and y-axis (x, y) in px
- ``border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget border color. ``None`` for no-color
- ``border_inflate`` (tuple, list) – Widget border inflate on x-axis and y-axis (x, y) in px
- ``border_position`` (str, tuple, list) – Widget border positioning. It can be a single position, or a tuple/list of positions. Only are accepted: north, south, east, and west. See :py:mod:`pygame_menu.locals`
- ``border_width`` (int) – Border width in px. If ``0`` disables the border
- ``cursor`` (int, :py:class:`pygame.cursors.Cursor`, None) – Cursor of the widget if the mouse is placed over
- ``float`` (bool) - If ``True`` the widget don't contribute width/height to the Menu widget positioning computation, and don't add one unit to the rows
- ``float_origin_position`` (bool) - If ``True`` the widget position is set to the top-left position of the Menu if the widget is floating
- ``font_background_color`` (tuple, list, str, int, :py:class:`pygame.Color`, None) – Widget font background color
- ``font_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Widget font color
- ``font_name`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) – Widget font path
- ``font_shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Font shadow color
- ``font_shadow_offset`` (int) – Font shadow offset in px
- ``font_shadow_position`` (str) – Font shadow position, see locals for position
- ``font_shadow`` (bool) – Font shadow is enabled or disabled
- ``font_size`` (int) – Font size of the widget
- ``margin`` (tuple, list) – Widget (left, bottom) margin in px
- ``padding`` (int, float, tuple, list) – Widget padding according to CSS rules. General shape: (top, right, bottom, left)
- ``range_box_color_readonly`` (tuple, list, str, int, :py:class:`pygame.Color`) - Color of the range box if widget in readonly state
- ``range_box_color`` (tuple, list, str, int, :py:class:`pygame.Color`) - Color of the range box between the sliders
- ``range_box_enabled`` (bool) - Enables a range box between two sliders
- ``range_box_height_factor`` (int, float) - Height of the range box (factor of the range title height)
- ``range_box_single_slider`` (bool) - Enables range box if there's only 1 slider instead of 2
- ``range_line_color`` (tuple, list, str, int, :py:class:`pygame.Color`) - Color of the range line
- ``range_line_height`` (int) - Height of the range line in px
- ``range_margin`` (tuple, list) - Range margin on x-axis and y-axis (x, y) from title in px
- ``range_text_value_color`` (tuple, list, str, int, :py:class:`pygame.Color`) - Color of the range values text
- ``range_text_value_enabled`` (bool) - Enables the range values text
- ``range_text_value_font_height`` (int, float) - Height factor of the range value font (factor of the range title height)
- ``range_text_value_font`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) - Font of the ranges value. If ``None`` the same font as the widget is used
- ``range_text_value_margin_f`` (int, float) - Margin of the range text values (factor of the range title height)
- ``range_text_value_position`` (str) - Position of the range text values, can be NORTH or SOUTH. See :py:mod:`pygame_menu.locals`
- ``range_text_value_tick_color`` (tuple, list, str, int, :py:class:`pygame.Color`) - Color of the range text value tick
- ``range_text_value_tick_enabled`` (bool) - Range text value tick enabled
- ``range_text_value_tick_hfactor`` (bool) Height factor of the range text value tick (factor of the range title height)
- ``range_text_value_tick_number`` (int) - Number of range value text, the values are placed uniformly distributed
- ``range_text_value_tick_thick`` (int) - Thickness of the range text value tick in px
- ``readonly_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget if readonly mode
- ``readonly_selected_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget if readonly mode and is selected
- ``repeat_keys`` (bool) - Enable key repeat. ``True`` by default
- ``repeat_keys_initial_ms`` (int) - Time in milliseconds before keys are repeated when held in milliseconds. ``400`` by default
- ``repeat_keys_interval_ms`` (int) - Interval between key press repetition when held in milliseconds. ``50`` by default
- ``selection_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the selected widget; only affects the font color
- ``selection_effect`` (:py:class:`pygame_menu.widgets.core.Selection`) – Widget selection effect
- ``shadow_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the widget shadow
- ``shadow_radius`` (int) - Border radius of the shadow
- ``shadow_type`` (str) - Shadow type, it can be ``'rectangular'`` or ``'ellipse'``
- ``shadow_width`` (int) - Width of the shadow. If ``0`` the shadow is disabled
- ``slider_color`` (tuple, list, str, int, :py:class:`pygame.Color`) - Slider color
- ``slider_height_factor`` (int, float) - Height of the slider (factor of the range title height)
- ``slider_sel_highlight_color`` (tuple, list, str, int, :py:class:`pygame.Color`) - Color of the selected slider highlight box effect
- ``slider_sel_highlight_enabled`` (bool) - Selected slider is highlighted
- ``slider_sel_highlight_thick`` (int) - Thickness of the selected slider highlight
- ``slider_selected_color`` (tuple, list, str, int, :py:class:`pygame.Color`) - Selected slider color
- ``slider_text_value_bgcolor`` (tuple, list, str, int, :py:class:`pygame.Color`) - Background color of the value text on each slider
- ``slider_text_value_color`` (tuple, list, str, int, :py:class:`pygame.Color`) - Color of value text on each slider
- ``slider_text_value_enabled`` (bool) - Enables a value text on each slider
- ``slider_text_value_font_height`` (int, float) - Height factor of the slider font (factor of the range title height)
- ``slider_text_value_font`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`) - Font of the slider value. If ``None`` the same font as the widget is used
- ``slider_text_value_margin_f`` (int, float) - Margin of the slider text values (factor of the range title height)
- ``slider_text_value_padding`` (int, float, tuple, list) – Padding of the slider text values
- ``slider_text_value_position`` (str) - Position of the slider text values, can be NORTH or SOUTH. See :py:mod:`pygame_menu.locals`
- ``slider_text_value_triangle`` (bool) - Draws a triangle between slider text value and slider
- ``slider_thickness`` (int) - Slider thickness in px
- ``slider_vmargin`` (int, float) - Vertical margin of the slider (factor of the range title height)
- ``tab_size`` (int) – Width of a tab character
.. note::
All theme-related optional kwargs use the default Menu theme if not
defined.
.. note::
This is applied only to the base Menu (not the currently displayed,
stored in ``_current`` pointer); for such behavior apply to
:py:meth:`pygame_menu.menu.Menu.get_current` object.
.. warning::
Be careful with kwargs collision. Consider that all optional documented
kwargs keys are removed from the object.
:param title: Title of the range slider
:param default: 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 increment: Increment of the value if using left/right keys; used only if the range values are not discrete
:param onchange: Callback executed when changing the value of the range slider
:param onreturn: Callback executed when pressing return (apply) on the range slider
:param onselect: Callback executed when selecting the widget
:param rangeslider_id: ID of the range slider
:param value_format: Function that format the value and returns a string that is used in the range and slider text
:param width: Width of the range in px
:param kwargs: Optional keyword arguments
:return: Widget object
:rtype: :py:class:`pygame_menu.widgets.RangeSlider`
"""
assert_vector(range_values, 0)
if len(range_values) == 2:
assert isinstance(increment, NumberInstance), (
"increment must be defined if the range values are not discrete"
)
else:
if increment is None:
increment = 1
# Filter widget attributes to avoid passing them to the callbacks
attributes = self._filter_widget_attributes(kwargs)
range_margin = kwargs.pop("range_margin", self._theme.widget_box_margin)
range_line_color = kwargs.pop("range_line_color", self._theme.widget_font_color)
range_text_value_color = kwargs.pop(
"range_text_value_color", self._theme.widget_font_color
)
range_text_value_font_height = kwargs.pop("range_text_value_font_height", 0.6)
range_text_value_tick_hfactor = kwargs.pop("range_text_value_tick_hfactor", 0.5)
slider_text_value_font_height = kwargs.pop("slider_text_value_font_height", 0.6)
widget = RangeSlider(
title=title,
rangeslider_id=rangeslider_id,
default_value=default,
range_values=range_values,
range_width=width,
increment=increment,
onchange=onchange,
onreturn=onreturn,
onselect=onselect,
range_line_color=range_line_color,
range_margin=range_margin,
range_text_value_color=range_text_value_color,
range_text_value_font_height=range_text_value_font_height,
range_text_value_tick_hfactor=range_text_value_tick_hfactor,
slider_text_value_font_height=slider_text_value_font_height,
value_format=value_format,
**kwargs,
)
self._configure_widget(widget=widget, **attributes)
self._append_widget(widget)
return widget