Source code for pyggui.gui.button
"""
Module containing different buttons.
"""
from typing import Callable, List, Tuple, Union
import pygame
from pyggui.gui.item import Item
from pyggui.gui.text import Text
from pyggui.helpers import DirectoryReader, ImageLoader
from pyggui.gui.animation import Animator
from pyggui.helpers.helpers import create_object_repr
[docs]class DefaultButton(Item):
"""
Default button is used when the user hasn't specified an image for the button itself.
"""
def __init__(
self,
controller: 'Controller',
position: List[int] = [0, 0],
size: Tuple[int, int] = (100, 40),
on_click: Callable = lambda: None,
text: Union[str, Text] = "Button",
fill_color: Tuple[int, int, int] = (0, 0, 0),
border_color: Tuple[int, int, int] = (255, 255, 255),
movable: bool = False,
visible: bool = True
):
"""
Args:
controller (Controller): Main controller object.
position (List[int]): Position of item on screen (or page). Defaults to [0, 0]
size (Tuple[int, int]): Size of item. Defaults to (100, 40).
on_click (Callable): Callable function gets triggered once the button is clicked. Defaults to None.
text (Union[str, Text]): String or Text object to add as text to button. Defaults to 'Button'.
fill_color (Tuple[int, int, int]): Inside color of button. Defaults to white.
border_color (Tuple[int, int, int]): Border color of button, also determines the color of text if text was
passed as string. Defaults to black.
movable (bool): If item will be moved by on_click action. Used for slider buttons. Defaults to false.
visible (bool): If item is currently visible.
"""
super().__init__(controller, position, size, on_click, movable, visible)
self.controller = controller
# Check if passed argument is string => create Text object
if type(text) is str:
self.text = Text(
value=text,
font_size=16,
)
else:
self.text = text
# Set colors
self._fill_color = fill_color # These ones define the colors
self._border_color = border_color
self.fill_color = self._fill_color # These ones get used
self.border_color = self._border_color
# Set position of text and add object to items
self.text.position = self.get_text_position()
self.items.append(
self.text
)
[docs] def get_text_position(self) -> List[int]:
"""
Method calculates the position of text inside button to that it is centered.
Returns:
list[int, int]: Position of centered text.
"""
# Center text in button
x_pos = int((self.x + (self.width * 0.5)) - (self.text.width * 0.5))
y_pos = int((self.y + (self.height * 0.5)) - (self.text.height * 0.5))
return [x_pos, y_pos]
[docs] def update(self) -> None:
"""
Method updates self and re-sets text position.
"""
if self.visible:
# Call parent method
super(self.__class__, self).update()
# And then re center text
self.text.position = self.get_text_position()
[docs] def draw(self) -> None:
"""
Method draws button along with its text on screen.
"""
if self.visible:
# Switch colors if hovered
if self.hovered:
self.border_color = self._border_color
self.fill_color = self._fill_color
else:
self.border_color = self._fill_color
self.fill_color = self._border_color
pygame.draw.rect(
self.display,
self.border_color,
self.rect,
width=0,
border_radius=10
)
pygame.draw.rect(
self.display,
self.fill_color,
self.rect,
width=3,
border_radius=10
)
self.text.color = self.fill_color
self.text.render()
self.text.update()
for item in self.items:
item.draw()
def __repr__(self) -> str:
return create_object_repr(self)
[docs]class Button(Item):
"""
Class for creating a button. Button has on_click method which gets triggered once the item is clicked.
A directory path parameter should be passed for creating button with images, otherwise a DefaultButton is
created.
Directory path for button images should be structured:
/some/path/button/-
normal.png # Image or directory of images
on_click/
on_hover/
The on_click and on_hover must be directories even if holding just one file.
Animation velocity can be changed for each of the above types (normal, on_click or on hover) by accessing buttons
animated dictionary. The dictionary holds Animator objects with passed images, so for ex.:
Changing animation velocity to on_click animation:
animated['on_click'].animation_velocity = 0.7
Changing animation type is also possible but should be done using Animators set_animation method, ex.:
animated['on_click'].set_animation('loop')
"""
def __new__(cls, *args, **kwargs):
# Check if directory_path was passed
kwargs_copy = kwargs.copy() # Mutate copy so all kwargs still go through
folder_path = kwargs_copy.pop("directory_path", False)
if folder_path:
# Created instance of self is passed
return super(Button, cls).__new__(cls) # Item has default __new__ constructor, pass it only class
else:
# Return default button otherwise
return DefaultButton(*args, **kwargs)
def __init__(
self,
controller: 'Controller',
directory_path: str = None,
position: List[int] = [0, 0],
size: Tuple[int, int] = None,
on_click: Callable = None,
movable: bool = False,
visible: bool = True,
animation_velocity: Union[float, int] = 1,
text: Union[str, Text] = None
):
"""
Args:
controller (Controller): Main controller object.
directory_path (str): Path to a structured directory holding button images.
position (List[int]): Position of button on screen (or page).
size (Tuple[int, int]): Size of item. Defaults to normal images size if not passed. If size is passed it
determines the buttons hit-box from its position.
on_click (Callable): Callable function gets triggered once the button is clicked. Defaults to None.
movable (bool): If item will be moved by on_click action. Used for slider buttons. Defaults to false.
visible (bool): If item is currently visible.
animation_velocity (Union[int, float]): Velocity at which to change the current image index. If set to 1;
images will be changed at each frame, if set to 0.5; images will be changed every second frame, ...
Defaults to 1.
"""
self.directory_path = directory_path
self.animation_velocity = animation_velocity
self.images = {
"normal": [],
"on_hover": [],
"on_click": []
}
self.animated = {
"normal": None,
"on_hover": None,
"on_click": None
}
self.current_state_key = "normal"
self.image_setup() # Load images
self.clicked = False
self.image_size = tuple(self.images["normal"][0].get_rect()[2:])
# Set size and call parent innit
if not size: # Fetch images size if not passed
size = self.image_size
super().__init__(controller, position, size, on_click, movable, visible)
[docs] def image_setup(self):
"""
Method loads all images in the passed directory_path into the local animated dictionary attribute which is used
internally for animating the button.
"""
dir_structure = DirectoryReader.get_structure(self.directory_path)
# normal image or directory of image is the base needed for operating
if "normal" in dir_structure:
for name, path in dir_structure["normal"]["files"]:
self.images["normal"].append(ImageLoader.load_transparent_image(path))
else: # Check under files if a normal.extension exists
normal_image = [path for name, path in dir_structure["files"] if "normal" in name]
if normal_image:
self.images["normal"].append(ImageLoader.load_transparent_image(normal_image[0]))
else:
# Raise error if normal was not given
pass
if "on_hover" in dir_structure:
for name, path in dir_structure["on_hover"]["files"]:
self.images["on_hover"].append(ImageLoader.load_transparent_image(path))
else:
self.images["on_hover"] = self.images["normal"]
if "on_click" in dir_structure:
for name, path in dir_structure["on_click"]["files"]:
self.images["on_click"].append(ImageLoader.load_transparent_image(path))
else:
self.images["on_click"] = self.images["normal"]
# Set animated, use Animator objects.
self.animated["normal"] = Animator(images=self.images["normal"], animation_velocity=self.animation_velocity)
self.animated["on_hover"] = Animator(images=self.images["on_hover"], animation_velocity=self.animation_velocity)
self.animated["on_click"] = Animator(images=self.images["on_click"], animation_velocity=self.animation_velocity)
[docs] def update(self):
""" Overwrite parent method for updating this classes custom attributes.
Used for updating all items attached to it(sizes, positions, etc.).
"""
if self.visible:
self.hovered = self.rect.collidepoint(self.controller.input.mouse_position)
# Check if mouse was clicked on item, in the interval of the debounce time
if self.hovered:
self.current_state_key = "on_hover"
self.animated["normal"].reset_index()
# Mouse clicked in set interval
if self.mouse_clicked and self.debounce_time():
self.current_state_key = "on_click"
self.clicked = True
self.on_click()
self.was_pressed = True
# Mouse was released
elif not self.mouse_clicked:
self.was_pressed = False
# If clicked the on_click animation is ongoing
if self.clicked:
self.current_state_key = "on_click"
if self.animated["on_click"].at_end:
self.animated["on_click"].reset_index()
self.clicked = False
# Not hovering anymore
if not self.hovered:
self.current_state_key = "normal"
self.animated["on_hover"].reset_index()
# If was pressed and mouse is not on the item anymore still call on_click method works if movable = True
if self.was_pressed and self.movable: # Only check if item is movable, otherwise get multiple clicks
self.on_click()
# Update all items
for item in self.items:
item.update()
[docs] def draw(self):
""" Overwrite parent method.
Used for drawing itself and every item attached to it.
"""
if self.visible:
self.display.blit(self.animated[self.current_state_key].get(), self.position)
for item in self.items:
item.draw()
def __repr__(self) -> str:
return create_object_repr(self)