"""
pygame-menu
https://github.com/ppizarror/pygame-menu
TOGGLE SWITCH
Switch between several states.
"""
__all__ = [
'ToggleSwitch',
'ToggleSwitchManager'
]
import pygame
import pygame_menu
import pygame_menu.controls as ctrl
from abc import ABC
from pygame_menu.font import FontType, assert_font
from pygame_menu.locals import FINGERUP
from pygame_menu.utils import check_key_pressed_valid, assert_color, assert_vector, \
make_surface, get_finger_pos
from pygame_menu.widgets.core.widget import Widget, WidgetTransformationNotImplemented, \
AbstractWidgetManager
from pygame_menu._types import Any, CallbackType, Union, List, Tuple, Optional, \
ColorType, NumberType, Tuple2NumberType, Tuple2IntType, NumberInstance, \
ColorInputType, EventVectorType, Callable
# noinspection PyMissingOrEmptyDocstring
[docs]class ToggleSwitch(Widget):
"""
Toggle switch widget.
If the state of the widget changes the ``onchange`` callback is called. The
state can change by pressing LEFT/RIGHT or RETURN if the widget only has two
states. This widget can handle more than 2 states.
.. code-block:: python
onchange(state_value, **kwargs)
.. note::
ToggleSwitch only accepts translation transformation.
:param title: Toggle switch title
:param toggleswitch_id: ToggleSwitch ID
:param default_state: Default state index of the switch
:param infinite: The state can rotate
:param onchange: Callback when changing the state of the switch
:param onselect: Function when selecting the widget
:param single_click: Changes the state of the switch with 1 click instead of finding the closest position (events). If ``True`` the parameter ``infinite`` will also be ``True``
:param single_click_dir: Direction of the change if only 1 click is pressed. ``True`` for left direction, ``False`` for right
:param slider_color: Slider color
:param slider_height_factor: Height of the slider (factor of the switch height)
:param slider_thickness: Slider thickness in px
:param slider_vmargin: Vertical margin of the slider (factor of the switch height)
:param state_color: Background color of each state, it modifies the whole width of the switch
:param state_text: Text of each state of the switch
:param state_text_font: Font of the state text. If ``None`` uses the widget font
:param state_text_font_color: Color of the font of each state text
:param state_text_font_size: Font size of the state text. If ``None`` uses the widget font size
:param state_text_position: Position of the state text respect to the switch rect
:param state_values: Value of each state of the switch
:param state_width: Width of each state. For example if there's 2 states, ``state_width`` only can have 1 value
:param switch_border_color: Border color of the switch
:param switch_border_width: Border width of the switch in px
:param switch_height: Height factor respect to the title font size height
:param switch_margin: Switch margin on x-axis and y-axis (x, y) respect to the title of the widget in px
:param args: Optional arguments for callbacks
:param kwargs: Optional keyword arguments
"""
_infinite: bool
_single_click: bool
_single_click_dir: bool
_slider: Optional['pygame.Surface']
_slider_color: ColorType
_slider_height: int
_slider_height_factor: float
_slider_pos: Tuple2IntType
_slider_thickness: int
_slider_vmargin: int
_state: int
_state_color: Tuple[ColorType, ...]
_state_font: Optional['pygame.font.Font']
_state_text: Tuple[str, ...]
_state_text_font: Optional[FontType]
_state_text_font_color: Tuple[ColorType, ...]
_state_text_font_size: Optional[int]
_state_text_position: Tuple2NumberType
_state_values: Tuple[Any, ...]
_state_width: List[int]
_switch: Optional['pygame.Surface']
_switch_border_color: ColorType
_switch_border_width: int
_switch_font_rendered: List['pygame.Surface']
_switch_height: int
_switch_height_factor: float
_switch_margin: Tuple2IntType
_switch_pos: Tuple2IntType
_switch_width: int
_total_states: int
def __init__(
self,
title: Any,
toggleswitch_id: str = '',
default_state: int = 0,
infinite: bool = False,
onchange: CallbackType = None,
onselect: CallbackType = None,
single_click: bool = False,
single_click_dir: bool = True,
slider_color: ColorInputType = (255, 255, 255),
slider_height_factor: NumberType = 1,
slider_thickness: int = 25,
slider_vmargin: NumberType = 0,
state_color: Tuple[ColorInputType, ...] = ((178, 178, 178), (117, 185, 54)),
state_text: Tuple[str, ...] = ('Off', 'On'),
state_text_font: Optional[FontType] = None,
state_text_font_color: Tuple[ColorInputType, ...] = ((255, 255, 255), (255, 255, 255)),
state_text_font_size: Optional[int] = None,
state_text_position: Tuple2NumberType = (0.5, 0.5),
state_values: Tuple[Any, ...] = (False, True),
state_width: Union[Tuple[int, ...], int] = 150,
switch_border_color: ColorInputType = (40, 40, 40),
switch_border_width: int = 1,
switch_height: NumberType = 1.25,
switch_margin: Tuple2IntType = (25, 0),
*args,
**kwargs
) -> None:
super(ToggleSwitch, self).__init__(
args=args,
kwargs=kwargs,
onchange=onchange,
onselect=onselect,
title=title,
widget_id=toggleswitch_id
)
# Asserts
assert isinstance(default_state, int)
assert isinstance(infinite, bool)
assert isinstance(single_click, bool)
assert isinstance(single_click_dir, bool)
assert isinstance(state_values, tuple)
# Check the number of states
self._total_states = len(state_values)
assert 2 <= self._total_states, 'the minimum number of states is 2'
assert 0 <= default_state < self._total_states, 'invalid default state value'
# Check fonts
if state_text_font is not None:
assert_font(state_text_font)
assert isinstance(state_text_font_size, (int, type(None)))
if state_text_font_size is not None:
assert state_text_font_size > 0, \
'state text font size must be equal or greater than zero'
# Check colors
assert_vector(state_text_position, 2)
switch_border_color = assert_color(switch_border_color)
assert isinstance(switch_border_width, int) and switch_border_width >= 0, \
'border width must be equal or greater than zero'
slider_color = assert_color(slider_color)
# Check dimensions and sizes
assert slider_height_factor > 0, 'slider height factor cannot be negative'
assert slider_thickness >= 0, 'slider thickness cannot be negative'
assert isinstance(slider_vmargin, NumberInstance)
assert_vector(switch_margin, 2, int)
assert isinstance(switch_height, NumberInstance) and switch_height > 0, \
'switch height factor cannot be zero or negative'
assert isinstance(state_color, tuple) and len(state_color) == self._total_states
# Create state color
new_state_color = []
for c in state_color:
new_state_color.append(assert_color(c))
state_color = tuple(new_state_color)
# Create state texts
assert isinstance(state_text, tuple) and len(state_text) == self._total_states
for c in state_text:
assert isinstance(c, str), 'all states text must be string-type'
assert isinstance(state_text_font_color, tuple) and \
len(state_text_font_color) == self._total_states
new_state_text_font_color = []
for c in state_text_font_color:
new_state_text_font_color.append(assert_color(c))
state_text_font_color = tuple(new_state_text_font_color)
# Crete state widths
self._switch_width = 0
if isinstance(state_width, NumberInstance):
state_width = [state_width]
assert_vector(state_width, self._total_states - 1, int)
for i in range(len(state_width)):
assert isinstance(state_width[i], int), 'each state width must be an integer'
assert state_width[i] > 0, 'each state width must be greater than zero'
self._switch_width += state_width[i]
# Finals
if single_click:
infinite = True
# Store properties
self._default_value = default_state
self._switch_border_color = switch_border_color
self._switch_border_width = switch_border_width
self._infinite = infinite
self._single_click = single_click
self._single_click_dir = single_click_dir
self._slider_color = slider_color
self._slider_height_factor = slider_height_factor
self._slider_thickness = slider_thickness
self._slider_vmargin = slider_vmargin
self._state = default_state
self._state_color = state_color
self._state_text = state_text
self._state_text_font = state_text_font
self._state_text_font_color = state_text_font_color
self._state_text_font_size = state_text_font_size
self._state_text_position = state_text_position
self._state_values = state_values
self._state_width = state_width
self._switch_height_factor = float(switch_height)
self._switch_margin = switch_margin
# Compute state width accum
self._state_width_accum = [0]
accum = 0
for w in self._state_width:
accum += w
accum_width = accum - self._slider_thickness - 2 * self._switch_border_width
self._state_width_accum.append(accum_width)
# Inner properties
self._accept_events = True
self._slider_height = 0
self._slider_pos = (0, 0) # to add to (rect.x, rect.y)
self._state_font = None
self._switch_font_rendered = [] # Stores font render for each state
self._switch_height = 0
self._switch_pos = (0, 0) # horizontal pos, and delta to title
[docs] def set_value(self, state: int) -> None:
assert isinstance(state, int), 'state value can only be an integer'
assert 0 <= state < self._total_states, 'state value exceeds the total states'
self._state = state
self._render()
def scale(self, *args, **kwargs) -> 'ToggleSwitch':
raise WidgetTransformationNotImplemented()
def resize(self, *args, **kwargs) -> 'ToggleSwitch':
raise WidgetTransformationNotImplemented()
def set_max_width(self, *args, **kwargs) -> 'ToggleSwitch':
raise WidgetTransformationNotImplemented()
def set_max_height(self, *args, **kwargs) -> 'ToggleSwitch':
raise WidgetTransformationNotImplemented()
def rotate(self, *args, **kwargs) -> 'ToggleSwitch':
raise WidgetTransformationNotImplemented()
def flip(self, *args, **kwargs) -> 'ToggleSwitch':
raise WidgetTransformationNotImplemented()
[docs] def get_value(self) -> Any:
return self._state_values[self._state]
def _apply_font(self) -> None:
if self._state_text_font is None:
self._state_text_font = self._font_name
if self._state_text_font_size is None:
self._state_text_font_size = self._font_size
self._state_font = pygame_menu.font.get_font(
self._state_text_font, self._state_text_font_size
)
# Compute the height
height = self._font_render_string('TEST').get_height()
self._switch_height = int(height * self._switch_height_factor)
self._slider_height = int(self._switch_height * self._slider_height_factor) \
- 2 * self._switch_border_width
# Render the state texts
for t in range(self._total_states):
f_render = self._state_font.render(
self._state_text[t], True, self._state_text_font_color[t]
)
self._switch_font_rendered.append(f_render)
def _draw(self, surface: 'pygame.Surface') -> None:
# Draw title
surface.blit(self._surface, (self._rect.x, self._rect.y + self._switch_pos[1] - 1))
# Draw switch
switch_x = self._rect.x + self._switch_margin[0] + self._switch_pos[0]
switch_y = self._rect.y + self._switch_margin[1]
surface.blit(self._switch, (switch_x, switch_y))
# Draw switch border
if self._switch_border_width > 0:
switch_rect = self._switch.get_rect()
switch_rect.x += switch_x
switch_rect.y += switch_y
pygame.draw.rect(
surface,
self._switch_border_color,
switch_rect,
self._switch_border_width
)
# Draw switch font render
if self._state_text[self._state] != '':
text = self._switch_font_rendered[self._state]
surface.blit(text, (
int(switch_x
+ (self._switch_width - text.get_width()) * self._state_text_position[0]),
int(switch_y
+ (self._switch_height - text.get_height()) * self._state_text_position[1])
))
# Draw slider
slider_x = switch_x + self._slider_pos[0] + self._switch_border_width
slider_y = switch_y + self._slider_pos[1] + self._switch_border_width
surface.blit(self._slider, (slider_x, slider_y))
def _render(self) -> Optional[bool]:
if not self._render_hash_changed(
self._selected, self._title, self._visible, self.readonly,
self._state):
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()
# Create slider
self._slider = make_surface(self._slider_thickness, self._slider_height,
fill_color=self._slider_color)
self._slider_pos = (self._state_width_accum[self._state],
self._slider_vmargin * self._switch_height)
# Create the switch surface
self._switch = make_surface(self._switch_width, self._switch_height,
fill_color=self._state_color[self._state])
self._switch_pos = (self._rect.width,
int((self._switch_height - self._rect.height) / 2))
# Update maximum rect height
self._rect.height = max(self._rect.height, self._switch_height,
self._slider_height)
self._rect.width += self._switch_margin[0] + self._switch_width
# Finals
self.force_menu_surface_update()
def _left(self) -> None:
"""
State left.
"""
if self.readonly:
return
previous = self._state
if self._infinite:
self._state = (self._state - 1) % self._total_states
else:
self._state = max(0, self._state - 1)
if previous != self._state:
self.change()
self._sound.play_key_add()
def _right(self) -> None:
"""
State right.
"""
if self.readonly:
return
previous = self._state
if self._infinite:
self._state = (self._state + 1) % self._total_states
else:
self._state = min(self._state + 1, self._total_states - 1)
if previous != self._state:
self.change()
self._sound.play_key_add()
[docs] def update(self, events: EventVectorType) -> bool:
self.apply_update_callbacks(events)
if self.readonly or not self.is_visible():
self._readonly_check_mouseover(events)
return False
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
# 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:
self._left()
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:
self._right()
return True
# Press enter
elif keydown and event.key == ctrl.KEY_APPLY and self._total_states == 2 or \
event.type == pygame.JOYBUTTONDOWN and self._joystick_enabled and \
event.button == ctrl.JOY_BUTTON_SELECT and self._total_states == 2:
self._sound.play_key_add()
self._state = int(not self._state)
self.change()
self.active = not self.active
return True
# Click on switch; don't consider the mouse wheel (button 4 & 5)
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
rect = self.get_rect(to_real_position=True, apply_padding=False)
if rect.collidepoint(*event_pos):
# Check if mouse collides left or right as percentage, use
# only X coordinate
mouse_x, _ = event_pos
topleft, _ = rect.topleft
topright, _ = rect.topright
# Distance from title
dist = mouse_x - (topleft + self._switch_margin[0] + self._switch_pos[0])
if dist > 0: # User clicked the options, not title
# Toggle with only 1 click
if self._single_click:
if self._single_click_dir:
self._left()
else:
self._right()
return True
else:
target_index = 0
best = 1e6
# Find the closest position
for i in range(self._total_states):
dx = abs(self._state_width_accum[i] - dist)
if dx < best:
target_index = i
best = dx
if target_index != self._state:
self._sound.play_key_add()
self._state = target_index
self.change()
return True
return False
class ToggleSwitchManager(AbstractWidgetManager, ABC):
"""
ToggleSwitch manager.
"""
def toggle_switch(
self,
title: Any,
default: Union[int, bool] = 0,
onchange: CallbackType = None,
onselect: Optional[Callable[[bool, 'Widget', 'pygame_menu.Menu'], Any]] = None,
toggleswitch_id: str = '',
single_click: bool = True,
state_text: Tuple[str, ...] = ('Off', 'On'),
state_values: Tuple[Any, ...] = (False, True),
width: int = 150,
**kwargs
) -> 'pygame_menu.widgets.ToggleSwitch':
"""
Add a toggle switch to the Menu: It can switch between two states.
If user changes the status of the callback, ``onchange`` is fired:
.. code-block:: python
onchange(current_state_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
- ``infinite`` (bool) – The state can rotate. ``False`` by default
- ``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)
- ``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
- ``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
- ``single_click_dir`` (bool) - Direction of the change if only 1 click is pressed. ``True`` for left direction (default), ``False`` for right
- ``slider_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Color of the slider
- ``slider_height_factor`` (int, float) - Height of the slider (factor of the switch height). ``1`` by default
- ``slider_thickness`` (int) – Slider thickness in px. ``20`` px by default
- ``slider_vmargin`` (int, float) - Vertical margin of the slider (factor of the switch height). ``0`` by default
- ``state_color`` (tuple) – 2-item color tuple for each state
- ``state_text_font`` (str, :py:class:`pathlib.Path`, :py:class:`pygame.font.Font`, None) - Font of the state text. If ``None`` uses the widget font. ``None`` by default
- ``state_text_font_color`` (tuple) – 2-item color tuple for each font state text color
- ``state_text_font_size`` (str, None) – Font size of the state text. If ``None`` uses the widget font size
- ``state_text_position`` (tuple) - Position of the state text respect to the switch rect. ``(0.5, 0.5)`` by default
- ``switch_border_color`` (tuple, list, str, int, :py:class:`pygame.Color`) – Switch border color
- ``switch_border_width`` (int) – Switch border width
- ``switch_height`` (int, float) – Height factor respect to the title font size height
- ``switch_margin`` (tuple, list) – Switch on x-axis and y-axis (x, y) margin respect to the title of the widget in px
- ``tab_size`` (int) – Width of a tab character
.. note::
This method only handles two states. If you need more states (for example
3, or 4), prefer using :py:class:`pygame_menu.widgets.ToggleSwitch`
and add it as a generic widget.
.. 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 behaviour 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 toggle switch
:param default: Default state index of the switch; it can be ``0 (False)`` or ``1 (True)``
:param onchange: Callback executed when when changing the state of the toggle switch
:param onselect: Callback executed when selecting the widget
:param toggleswitch_id: Widget ID
:param single_click: Changes the state of the switch with 1 click instead of finding the closest position
:param state_text: Text of each state
:param state_values: Value of each state of the switch
:param width: Width of the switch box in px
:param kwargs: Optional keyword arguments
:return: Widget object
:rtype: :py:class:`pygame_menu.widgets.ToggleSwitch`
"""
if isinstance(default, (int, bool)):
assert 0 <= default <= 1, 'default value can be 0 or 1'
else:
raise ValueError(
f'invalid value type, default can be 0, False, 1, or True, but'
f'received "{default}"'
)
# Filter widget attributes to avoid passing them to the callbacks
attributes = self._filter_widget_attributes(kwargs)
infinite = kwargs.pop('infinite', False)
slider_color = kwargs.pop('slider_color', self._theme.widget_box_background_color)
slider_thickness = kwargs.pop('slider_thickness', self._theme.scrollbar_thick)
state_color = kwargs.pop('state_color', ((178, 178, 178), (117, 185, 54)))
state_text_font_color = kwargs.pop('state_text_font_color',
(self._theme.widget_box_background_color,
self._theme.widget_box_background_color))
state_text_font_size = kwargs.pop('state_text_font_size', None)
switch_border_color = kwargs.pop('switch_border_color', self._theme.widget_box_border_color)
switch_border_width = kwargs.pop('switch_border_width', self._theme.widget_box_border_width)
switch_height = kwargs.pop('switch_height', 1)
switch_margin = kwargs.pop('switch_margin', self._theme.widget_box_margin)
widget = ToggleSwitch(
default_state=default,
infinite=infinite,
onchange=onchange,
onselect=onselect,
single_click=single_click,
single_click_dir=kwargs.pop('single_click_dir', True),
slider_color=slider_color,
slider_thickness=slider_thickness,
state_color=state_color,
state_text=state_text,
state_text_font_color=state_text_font_color,
state_text_font_size=state_text_font_size,
state_values=state_values,
switch_border_color=switch_border_color,
switch_border_width=switch_border_width,
switch_height=switch_height,
switch_margin=switch_margin,
title=title,
state_width=int(width),
toggleswitch_id=toggleswitch_id,
**kwargs
)
self._configure_widget(widget=widget, **attributes)
self._append_widget(widget)
return widget