-
Notifications
You must be signed in to change notification settings - Fork 0
Documentation
This document will talk about the structure of the package and the design choices that were made. This will include for example the class hierarchy, advanced features like conditional styling and common mistakes.
The package is divided into multiple subpackages:
pygame_utils_likeablejuniper
├─ core
│ └─ element.py
├─ element
│ ├─ label.py
│ ├─ button.py
│ ├─ container.py
│ ...
├─ style
│ ├─ style.py
│ └─ theme.py
└─ layout
└─ layout.py
Although this may look complicated to import, you can still import everything you need as usual:
from pygame_utils_likeablejuniper import Label, Button, VerticalLayoutThis works because the __init__.py file creates an importable alias for all user-relevant classes.
The rest of this document will focus on the subpackages and the classes contained therein, and also how to write your own subclasses and have them work with the existing classes.
The only significant class contained in the core subpackage is GUIElement. This is the base class for all major classes in the subpackage element. This class just describes some drawable and updatable element that the user can see and/or interact with. Its public API looks like this:
class GUIElement:
def __init__(self, rect: list[float], style: Style | None, base_style: Style)
def update(self, events: Iterable[pygame.Event])
def draw(self, screen: pygame.Surface)
def update_style(self, style: Style)
def add_conditional_style(self, condition: Callable[[Self], bool], style: Style)GUIElement also has some protected and private methods. Of these, only the protected method is relevant for understanding how elements work:
def _rerender(self)Each element also tracks certain builtin booleans which you can use in style conditions, for example hovered, enabled or visible.
Constructing a GUIElement requires only one thing: a rect. Rects are a concept from pygame. They are also just a generally convenient and short method of representing rectangle-like areas. Every GUIElement covers a rectangle-like area, there are no truly "round" elements (this is similar to popular rendering implementations, like HTML). The rect contains 4 floats, the elements x and y position and the elements width and height.
You may optionally pass a Style-object, but this will be covered in the corresponding chapter about styles.
Every GUIElement has a hook for reacting to pygame events such as button presses or scrolling. Each element also has a method which draws the element to the given pygame surface. Since you create the elements, you are responsible for calling these methods. The methods need to be called every frame and always need to be passed the correct parameters or they will not work.
The update method requires an Iterable of pygame events, which you can obtain simply by calling pg.event.get() every frame and passing the result to the element.
The draw method requires a pygame surface, this should usually be your screen (or similar) variable obtained via pg.display.get_surface() or pg.display.set_mode(). If you know what you're doing, you may also specify any surface to split your application into multiple separately drawable surfaces.
You can avoid calling these methods on every element manually by adding all elements to a container and then calling update and draw on the container once. You can even nest containers!
Since these are two style methods, they will be covered in the style chapter.
This method reloads all drawing-relevant data for this element. This is one of the core pieces of an element, as it vastly improves performance over simply calculating all data every frame. Unfortunately this also makes elements a bit more difficult to understand.
Whenever something about an element changes that influences the way it is displayed (for example it becomes hovered or the text is updated), this method is called. If you feel like you need to call this method manually for changes to style, text or anything visible to update, you're probably using the API wrong.
Since this method is only protected, it can and should be overriden by extending classes. Since most elements define properties which are unique to them, they also need extra render logic. To keep performance high, it is not recommended to calculate that data every frame, and instead calculate it during _rerender and then save it to instance variables.
If you are interested in creating your own elements, look at the corresponding documentation.
Each element implementation has a file. That file not only defines the actual GUIElement implementation itself, but also the styles for that element. Some elements may also define multiple variations of the same element.
The Label element is one of these classes, although you should probably only ever use one of them. The implementation defines two types of labels; a classic Label and a StaticLabel. The main difference is the usage of _rerender. Label does not override the _rerender method, and therefore does not have the expected performance savings. The StaticLabel class on the other hand, does override the _rerender method. This means that, when you have text which changes pretty much every frame (for example a countdown with millisecond accuracy), you can use the basic Label class. For all other cases, it is recommended to use StaticLabel, since StaticLabel's worst case (_rerender being called every frame) is just barely worse than Label. The API for both labels is identical.
from pygame_utils_likeablejuniper import StaticLabel
label = StaticLabel([10, 10, 200, 50], "Hello Pygame")
while running:
label.update(pg.event.get())
label.draw(screen)The Button element is, apart from the label, one of the most basic GUI elements you could think of. The button contains the expected on-click functionality, provided through a Callable type argument. You may pass a lambda or method reference which takes no arguments and returns any type, the result is just ignored.
from pygame_utils_likeablejuniper import Button
button = Button([10, 10, 200, 50], "Click Me!", lambda: print("I was clicked!"))
while running:
button.update(pg.event.get())
button.draw(screen)