gale.ui
gale.ui is a small widget toolkit for menus, HUDs, and forms:
Panel, Label, Button, ProgressBar, Checkbox,
ListView, Container, Window, TextBox,
PaginatedTextBox, TextInput, and Cursor, styled through a
shared Theme and driven by gale.input_handler through a
UIManager. See examples/rally for a full game built with it
(menus, a “host:port” text field, buttons) alongside gale.net.
Widget list
Widget |
Use it for |
|---|---|
|
Background chrome behind other widgets. |
|
Static or programmatically updated text ( |
|
A clickable action, fires |
|
A health/loading bar; |
|
A toggle, fires |
|
A vertical selectable list — the generalization of a classic “menu”: mouse hover/click and keyboard up/down both work. |
|
Groups any number of children; handles z-order, input dispatch, and focus movement between them. |
|
Click/Enter-to-continue dialogue/hint text; |
|
The same paginated text, but navigated with explicit “Previous”/”Next” buttons instead (e.g. a rules/help screen) — the buttons enable/disable themselves at the first/last page. |
|
A single-line editable field (a player name, a chat message, a “join code”/server address). |
|
A custom OS pointer ( |
|
A |
Mouse support
gale.input_handler’s mouse-motion binding matches an exact
relative-motion vector by default (meant for discrete swipe-style
actions), which real mouse movement essentially never produces. Bind
the wildcard fallback once in your settings.py so hover tracking
actually receives every motion event:
from gale import input_handler
input_handler.InputHandler.set_mouse_click_action(input_handler.MOUSE_BUTTON_1, "click")
input_handler.InputHandler.set_mouse_motion_action(None, "mouse_move")
UIManager.on_input already knows what to do with both once they’re
bound — clicks and motion just need to reach it through your state’s
on_input, same as any other input.
Customizing look and feel
Pass a Theme to any widget, or replace the process-wide default:
import pygame
from gale.ui.theme import Theme, set_default_theme
set_default_theme(Theme(
font=pygame.font.Font(None, 22),
accent_color=pygame.Color(255, 90, 90),
))
Widgets read their theme fresh on every render() call, so swapping
the default theme (or a widget’s own, via its theme attribute)
takes effect immediately — no need to rebuild the widget tree.
Cursors from settings.py
Following the same convention as TEXTURES/FONTS/SOUNDS:
import pygame
from gale.ui.cursor import Cursor
CURSORS = {
"pointer": Cursor(pygame.image.load(BASE_DIR / "assets" / "graphics" / "cursor.png"), hotspot=(2, 2)),
}
# In a state's enter(), to use it as the OS pointer:
settings.CURSORS["pointer"].set_as_system_cursor()
# Or pass it to a ListView for the keyboard-navigation indicator instead:
ListView(..., cursor=settings.CURSORS["pointer"])
Closable windows
Window wraps a Panel, an optional title, an optional close
(“X”) button docked at the top-right corner, and any children you pass
it, into a single Container:
from gale.ui.window import Window
from gale.ui.text_box import PaginatedTextBox
def close_help() -> None:
menu.remove_child(help_window) # or just help_window.visible = False
help_window = Window(
160, 90, 320, 220, title="How to play", on_close=close_help,
children=[PaginatedTextBox(160, 114, 320, 172, long_help_text)],
)
Set closable=False to skip the close button (e.g. a modal the
player must resolve by clicking something inside it instead), or call
window.close() yourself — from a keyboard shortcut, another
widget’s on_click, whatever — to trigger the same hide-and-call-
on_close behavior the button does.
Known limitations
TextInputonly supports ASCII-range input (pygame.KEYDOWN’sunicode), not full IME/composed-character entry — enough for names, chat, and join codes, not every language. Full Unicode support would needpygame.TEXTINPUTevents wired intogale.input_handler, which is a separate, bigger change to that module.No built-in widget animation: drive a plain attribute (
value,x, …) withgale.timer.Timer.tweeninstead, the same way the existing examples already animate everything else.No
Sliderwidget yet — add one when a real use case needs it.