"""
pygame-menu
https://github.com/ppizarror/pygame-menu
TOGGLE SWITCH
Switch between several states.
"""
__all__ = ['ToggleSwitch']
import pygame
import pygame_menu
import pygame_menu.controls as ctrl
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
from pygame_menu._types import Any, CallbackType, Union, List, Tuple, Optional, \
ColorType, NumberType, Tuple2NumberType, Tuple2IntType, NumberInstance, \
ColorInputType, EventVectorType
# 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.
:return: None
"""
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.
:return: None
"""
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():
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