Skip to content

LamaCorp/graphalama

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Graphalama

Aim

Pygame is a great and simple way to create basic windowed applications. It is therefore a privileged entry point for python learners to get out of the console. However, people often ask for GUI features in pygame: "How can I have a simple button ?" or "How can the user enter their name ?".

The goal of this library is to answer all those questions and provide the easiest possible interface to create, update, control and manipulate what we will call widgets. (Buttons, TextBoxes, Sliders, Switches...)

Our principles

  • KISS: Keep it sweet and simple
  • Simple and ugly are not synonyms
  • Simple can be powerful
  • Simple should be customisable
  • No code is better than ugly code

Installation

You can install a stable version using pip

pip install graphalama

Or a developer version with

git clone https://gitlab.com/lama-corp/graphalama
cd graphalama
python setup.py install -U

How to use

Most simple pygame applications look like this

import pygame
pygame.init()

# Setup of variables, objects...
stop = False
screen = pygame.display.get((400, 400))

# Main loop
while not stop:

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            stop = True
        else:
            # Handle inputs, if the user click on some clickable places, buttons...
            # Handle the keys pressed to move players, input text...
            # It's the logic of the game that depends of inputs
            ...

    # Then you have an internal logic, applying gravity, running AIs, anything that changes itself every frame

    # And finally you render everything with some more or less advanced technics

    screen.fill((255, 255, 255))
	pygame.display.update()

For any widget you need to have at least 3 parts in the code to implement it: creation, handling the input, and rendering it. Some widget can have an internal logic going like clock or timer but we'll come back on that later.

Say you want to add a button in your window. A big green play button. We reduce each step to the minimum.

  • Creation : play_button = Button("PLAY", bg_color=GREEN, function=play)
  • Input handling (clicks): play_button.update(event)
  • Rendering: play_button.render(screen)

A minimal hello world would look like this:

import pygame
from graphalama.widgets import *

pygame.init()

screen = pygame.display.set_mode((800, 500))

# creation
hello_text = SimpleText("Hello World!")

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            quit()
        # a text doesn't change on input so we don't need to update it

    screen.fill((255, 255, 255))
    hello_text.render(screen)
    pygame.display.flip()

Customisation

Before knowing how to customise your widgets so they match your style, we have to understand what is a widget. A widget is made of three things:

  • a Shadow: optional, it provides a nice depth to our application and accentuate our widgets
  • a Shape: defines the shape of the background. It also defines the borders, the size and where you can click/select the widget. To list a few common: Rectangle, Circle, RoundedRect...
  • a content: the information of the widget, it can be the widget's own content or a child widget (did you know that we can put any widget inside a Button ?)

Those three layers are mixed together to create the widget. To customise our widgets we can therefore easily change one of those three layers.

Common parameters

First, let's have a look to the common parameters that aren't part of the shadow, shape or content.

Position

With graphalama, as with pygame a position is a couple of integers, (0, 0) being the topleft corner and (x, y) being the x-st pixel on the y-st line of the screen. Every time we'll need a position as an argument for a function it will be a tuple (x, y) of integers.

Graphalama has a powerful positioning system for the widgets, whose goal is to be able to design resizable applications without more effort. A widget's position is defined by two elements: its coordinates pos and its anchor. The anchor defines where the widget is anchored, that is, where it will stay when the window is resized and where it will be first put on the screen.

For instance, if the anchor is TOPLEFT, the widget will always stay at the same distance of the top and the left of the screen, no matter how the window is resized. Its pos will then be the topleft corner of the widget. This is the default anchor, so we can place our widgets like we would do in most frameworks.

But say, we want a widget to always stay on the right side of the screen (like a "next" button), we just need to set the anchor to RIGHT and our widget will stay at say 5 pixels from the right border even if we resize the window. Here pos will be the middle point on the right of the widget. Here's a small drawing to explain it:

This button was created in a 800x500 window with

Button("Next ->", quit, (770, 250), RoundedRect((200, 100)), bg_color=GREEN, anchor=RIGHT)

If we resize the window, it will always stay 30px from the right edge.

Two conclude, there are four things to remember:

  • We can anchor a widget to one or more side of the window by setting the anchor to RIGHT, LEFT, TOP and BOTTOM. Those constants are defined in graphalama.constants.
  • We can combine the anchors with the pipe symbol, |, so we anchor a widget to the top right corner with anchor=TOP|RIGHT
  • Not only the position, but also the size can be controlled with anchors. If anchor=LEFT|RIGHT, the distance from the widget to both side will stay constant. Therefore the width will stretch if the window gets bigger and shrink if the window gets smaller.
  • The pos of a widget always reflect the anchor: if the anchor is BOTTOM|RIGHT the pos will be the bottom right of the widget. If it is LEFT, pos will be the midleft of the widget. However we can always get the topleft of the widget with widget.topleft. widget.x and widget.y also work.

That's everything for a very technical part, but it gets very intuitive once we get used to it and saves a lot of time.

Color

Graphalama has a great range of options for coloring widgets. No more of the common monochromatic widgets without transparency!

Usually widgets have three colors:

  • color, the foreground color, ie. the text of a button
  • bg_color, the background color, that's how the shape is filled
  • border_color, the color of the border

Those three colors accepts different types, and you can pass any of them at any time:

  • a RGB or RGBA tuple of integers between 0 and 255, you can find a lot of pre-defined colors in graphalama.constants
  • a Gradient from one color to another (both can be RGB or RGBA)
  • a MultiGradient to have a multicolored gradient
  • a ImageBrush to draw an image
  • a ImageListBrush to draw multiple images and swap between them.

Example of differents colors and transparency You can get the code for this example here.

What actually is a color ? It is an object with a paint method that takes a Surface and colorize it.

RGB or RGBA tuples

Tuples like (R, G, B) or (R, G, B, A) are both valid colors, with 0 <= R, G, B, A <= 255. They are automatically converted to a Color object for ease of use. And as expected, they fill the whole area with the same color.

Gradients

A Gradient(start, end, horizontal) will fill a shape with a linear interpolation between the two colors. start and end must be two tuples and if horizontal == False, the gradient will be drawn vertical, so start will be at the top of the shape and end at the bottom.

Multigradients
):

A Multigradient(*colors, positions=None, horizontal=True) is like a Gradient but with more than two colors.

Example for an equally spaced blue-yellow-orange-red gradient:

from graphalama.constants import BLUE, YELLOW, ORANGE, RED
MultiGradient(BLUE, YELLOW, ORANGE, RED)

You can also choose where the color points are. Here the orange-red part will take the left half of the gradient whereas the blue-yellow and yellow-orange would take only a fourth. The positions are between 0 and

MultiGradient(BLUE, YELLOW, ORANGE, RED, positions=(0, 1/4, 1/2, 1))

Of course, the number of positions must match the number of colors, but the positions do not have to start at 0 and end at 1. For instance is the positions are (1/2, 2/3, 1), the first half of the surface will be exactly of the first color.

If horizontal == False, then the gradient is drawn top to bottom and not left to right.

Custom Shape

Let's start again with our play button: Button("PLAY", bg_color=GREEN, function=play). The default shape is a Rectangle, So that's what it would look like:

But you're not limited to rectangles, you can have a rounded rectangle, a circle or even any parametric shape or any custom shape (we'll come back on creating your custom shapes later)

You can get the code for this example here.

There are quite a few things to note here. First of all, if we don't precise the position of the widgets, the will just stack vertically in the center of the screen. Then, we can give any shape to a widget. Every widget can have any shape, we just need to pass it as the shape argument in the object initialisation. Shapes are found in graphalama.shapes, they all accept the same parameters :

  • First the size of the widget, if none is given the widget will just fit its content
  • Then the arguments for the specific shape, ie. the rounding of the rectangle, the equations of the parametric curve...
  • The border comes next, which specifies the width of the widget's border
  • The padding is then the space between the content of the widget and the border.
  • Finally comes min_size and max_size, those are the minimum and maximum size the widget can take if the window is resized

Finally, a shape object is a container for all the information about the size, border, shape, padding. If we want to change the size of a widget at runtime, we need to set widget.shape.size, widget.shape.height or widget.shape.width.

Therefore a shape can be set only to ONE widget. Widgets can not share references to the same shape.

Padding

The padding is the space in pixels between the content of the widget and its border. It does not adapt on screen resize. The Padding class is defined in shapes.py and accepts four constructors.

Padding(padding) -> Padding: Every side has the same size
Padding(horizontal, vertical) -> Padding: left and right have the same size and top and bottom too
Padding(left, top, right, bottom) -> Padding: The four sides have different sizes

Shape objects also accept integers for the padding. In the case an integer is given, it is the padding for all sides. The default padding is 2 pixels.

Custom content

There's gonna be some heavy work there, so I wont comment on the current system.

Custom shadow

Shadows put a highlight on our widgets, by creating a deeper contrast between the widget and the background. Shadows are fond in graphalama.shadows and they are optional: you can pass NoShadow to any widget and... it won't show nor calculate any shadow. Otherwise, a shadow accepts 4 optional arguments. Here's the signature

Shadow(dx=2, dy=2, blur=2, strength=100)

The two firsts are the offset between the widget and the shadow, they shift the shadow by dx and dy to the right and down respectively.

Optionally, you can blur the shadow, for a smoother result. If blur is 0 the shadow will have the same shape as the widget with the same sharp borders. If blur is positive, then a Gaussian blur is applied to the shape. Note that this requires pillow. If you don't have pillow installed, it won't blur the shadow, without raising any exception.

The strength is an integer between 0 and 255, it's how dark the shadow is.

Here's an example from shapes_example, where the widgets are

wid = WidgetList([
    Button("Random Shadow", change_shadow, (400, 250), RoundedRect((400, 250), 50, 1, 2),
           bg_color=(150, 232, 230), border_color=NICE_BLUE, shadow=NoShadow(), anchor=ALLANCHOR),
    Widget((55, 35),  RoundedRect(), bg_color=Monokai.PINK, anchor=TOP),
    Widget((150, 35), RoundedRect(), bg_color=Monokai.BLUE,   shadow=Shadow(5, 5, 0), anchor=TOP),
    Widget((245, 35), RoundedRect(), bg_color=Monokai.ORANGE, shadow=Shadow(-5, 5, 10, 200), anchor=TOP),
    Widget((340, 35), RoundedRect(), bg_color=Monokai.GREEN,  shadow=Shadow(5, 5, 5), anchor=TOP),
    Widget((435, 35), RoundedRect(), bg_color=Monokai.YELLOW, shadow=Shadow(0, 0, 10), anchor=TOP),
    Widget((530, 35), RoundedRect(), bg_color=Monokai.PURPLE, shadow=Shadow(5, 5, 0, 200), anchor=TOP),
    Widget((625, 35), RoundedRect(), bg_color=Monokai.BROWN,  shadow=Shadow(-2, -2, 5), anchor=TOP),
    Widget((720, 35), RoundedRect(), bg_color=Monokai.BLACK,  shadow=Shadow(5, 20, 5), anchor=TOP),
    SimpleText("Shadow(dx, dy, blur, strength)", (400, 490), anchor=BOTTOM)
    ])

Other elements

Animations

Drawings

Font

There's gonna be some heavy work there, so I wont comment on the current system.

Widgets

Button

SimpleText

Tips

The Pos class

WidgetList

You will often find yourself with a list of widget that you want to render and update. WidgetList is a thin wraper around the builtin list class with the methods render(screen), update(event) and resize(new_size, old_size) implemented, so it acts like a widget.

WidgetList can be contained in other WidgetList.

Creating your own stuff

Widgets

Notes

A special thank to risson for his help and support. Also, I don't maintain the library actively, but if you open issues I will look into them because I like to see people use what I made. If you open feature requests, I will not implement them except maybe during a lonely rainy Saturday...