Module: forge.gui
Build Requirement: make GUI=1
Backend: raylib + raygui (vendored — no external install needed)
This guide teaches you how to build graphical applications with FORGE, from opening your first window to building a full interactive app. It assumes you already know FORGE basics (variables, procedures, loops).
- Building with GUI Support
- Your First Window
- The Render Loop
- Drawing Shapes
- Drawing Text
- Custom Fonts
- Handling Input
- Widgets
- Scrollable Text Log
- Color Constants
- Complete API Reference
- Example: Hello GUI
- Example: NMEA Terminal
FORGE includes raylib and raygui in its vendor/ folder, so no external
installation is needed. Just build with the GUI=1 flag:
make clean
make GUI=1This produces a forge binary that can run .fg files using forge.gui.
Without GUI=1, the forge binary still works for all non-GUI programs.
If a program tries to call gui.* functions in a non-GUI build, it prints an
error and exits:
FORGE: GUI support not compiled. Rebuild with: make GUI=1
System requirements (Linux): OpenGL, X11, and pthread development headers. On Debian/Ubuntu, these are usually pre-installed. If not:
sudo apt-get install libgl1-mesa-dev libx11-devEvery GUI program follows the same pattern:
import forge.gui
proc main() -> void:
gui.init_window(800, 600, "My First Window")
gui.set_target_fps(60)
while gui.window_open():
gui.begin_draw()
gui.clear(30, 30, 35, 255)
gui.draw_text("Hello, GUI!", 300, 280, 24, 255, 255, 255, 255)
gui.end_draw()
gui.close_window()
What each line does:
| Line | Purpose |
|---|---|
gui.init_window(w, h, title) |
Creates a window of the given size with a title bar |
gui.set_target_fps(60) |
Limits the render loop to 60 frames per second |
gui.window_open() |
Returns true until the user clicks the close button |
gui.begin_draw() |
Starts a new frame — must be called before any drawing |
gui.clear(r, g, b, a) |
Fills the window with a background color |
gui.end_draw() |
Finishes the frame and displays it on screen |
gui.close_window() |
Cleans up and destroys the window |
Run it:
./forge run my_first_window.fgGUI programs are not like console programs. Instead of running once and exiting, they run in a loop that redraws the screen every frame (typically 60 times per second).
while gui.window_open():
gui.begin_draw()
# 1. Clear the screen
gui.clear(30, 30, 35, 255)
# 2. Draw everything
gui.draw_text("Frame: " + str(gui.get_fps()), 10, 10, 20, 255, 255, 255, 255)
# 3. Handle input (check keys, mouse, buttons)
if gui.is_key_pressed(256): # 256 = Escape key
break
gui.end_draw()
Key concept: Everything you draw only lasts for one frame. On the next frame, you clear the screen and draw everything again. This is called immediate-mode rendering.
All drawing functions take RGBA color values (0–255 each):
# Filled rectangle
gui.draw_rect(x, y, width, height, r, g, b, a)
# Rectangle outline
gui.draw_rect_lines(x, y, width, height, r, g, b, a)
# Filled circle
gui.draw_circle(center_x, center_y, radius, r, g, b, a)
# Circle outline
gui.draw_circle_lines(center_x, center_y, radius, r, g, b, a)
# Line between two points
gui.draw_line(x1, y1, x2, y2, r, g, b, a)
Example — draw a red rectangle with a green circle:
gui.draw_rect(100, 100, 200, 150, 200, 50, 50, 255)
gui.draw_circle(200, 175, 50.0, 50, 200, 50, 255)
gui.draw_text(text, x, y, font_size, r, g, b, a)
Parameters:
text— the string to displayx, y— pixel position (top-left corner of text)font_size— size in pixels (14 = small, 20 = normal, 30 = large)r, g, b, a— color (RGBA, 0–255)
Measuring text width (useful for centering):
var width: int = gui.measure_text("Hello", 20)
var centered_x: int = (800 - width) / 2
gui.draw_text("Hello", centered_x, 300, 20, 255, 255, 255, 255)
By default, FORGE uses raylib's built-in bitmap font. To use a nicer font, see Section 6 — Custom Fonts.
FORGE's GUI layer defaults to raylib's built-in bitmap font, which is
functional but coarse. You can load any TrueType (.ttf) or OpenType
(.otf) font from disk and make it the active font for all text
rendering — draw_text, measure_text, the scrollable log panels, and
color buttons — without changing any of your existing draw calls.
Call gui.load_font after gui.init_window and before the render
loop. It returns a slot id (0–7) that you pass to gui.set_font:
gui.init_window(900, 650, "My App")
gui.set_target_fps(60)
var font_id: int = gui.load_font("/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 20)
if font_id >= 0:
gui.set_font(font_id)
The size parameter (here 20) sets the base rendering resolution of the
font. Load at roughly the pixel size you plan to draw at for the sharpest
results. From that point on every text call uses the loaded font
automatically — no extra parameters needed.
Pass -1 to gui.set_font at any time to go back to the raylib bitmap
font:
gui.set_font(-1) # revert to built-in default
When you no longer need a font (typically at shutdown), free its GPU memory
with gui.unload_font. If the unloaded font was active, rendering reverts
to the default automatically.
# --- after the render loop ---
if font_id >= 0:
gui.unload_font(font_id)
gui.close_window()
Up to 8 fonts can be loaded simultaneously (slot ids 0–7). Each call to
gui.load_font finds the next free slot; you can hold references to
multiple fonts and switch between them with gui.set_font.
These are available on most distributions:
| Font file | Style |
|---|---|
/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf |
Crisp monospace, widely installed |
/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf |
Metric-compatible with Courier New |
/usr/share/fonts/truetype/ubuntu/UbuntuMono-R.ttf |
Ubuntu's monospace (fonts-ubuntu package) |
Install Ubuntu fonts if needed:
sudo apt install fonts-ubuntu| Function | Returns | Description |
|---|---|---|
gui.load_font(path, size) |
int | Load a TTF/OTF file. Returns slot id (0–7) or -1 on failure. |
gui.set_font(id) |
void | Activate a loaded font. Pass -1 to revert to default. |
gui.unload_font(id) |
void | Free a font slot and its GPU memory. |
# Was the key pressed THIS frame? (one-shot, for menus/toggles)
if gui.is_key_pressed(key_code):
# do something once
# Is the key being HELD DOWN? (for continuous movement)
if gui.is_key_down(key_code):
# do something every frame
# Was the key released THIS frame?
if gui.is_key_released(key_code):
# do something once
# Get the last key pressed (0 if none)
var key: int = gui.get_key_pressed()
Common key codes:
| Key | Code | Key | Code |
|---|---|---|---|
| Escape | 256 | Enter | 257 |
| Space | 32 | Backspace | 259 |
| Up | 265 | Down | 264 |
| Left | 263 | Right | 262 |
| A–Z | 65–90 | 0–9 | 48–57 |
var mx: int = gui.mouse_x()
var my: int = gui.mouse_y()
if gui.is_mouse_pressed(0): # 0 = left button
# clicked this frame
if gui.is_mouse_down(0): # held down
# dragging
Widgets are interactive GUI elements. They are drawn AND checked in a single call — this is called immediate-mode GUI.
if gui.button(x, y, width, height, "Click Me"):
# This code runs when the button is clicked
print("Button was clicked!")
A button with custom background and text colors (great for toggle states):
# gui.color_button(x, y, w, h, text, bg_r,g,b,a, text_r,g,b,a)
if gui.color_button(10, 10, 100, 30, "Active", 0,180,0,255, 0,0,0,255):
# clicked
Static text inside a box (styled by raygui theme):
gui.label(x, y, width, height, "Status: Ready")
var checked: bool = false
# Inside render loop:
checked = gui.checkbox(x, y, 20, 20, "Enable logging", checked)
var volume: float = 0.5
# Inside render loop:
volume = gui.slider(x, y, 200, 20, 0.0, 1.0, volume)
An editable text input field. Click to activate, type to edit:
var name: str = ""
# Inside render loop:
name = gui.textbox(x, y, 200, 30, name, 128)
# 128 = maximum character length
A dropdown selector. Items are semicolon-separated:
var baud_idx: int = 0
# Inside render loop:
baud_idx = gui.dropdown(x, y, 120, 30, "4800;9600;19200;38400", baud_idx)
Note: The dropdown opens on click and closes on selection. The return value is the index of the selected item (0-based).
The log widget is a scrollable, colored text display — perfect for terminal output, chat messages, or data feeds.
# gui.log_create(id, x, y, width, height, max_lines, font_size)
gui.log_create(0, 10, 50, 780, 400, 4096, 16)
id— A number 0–7 identifying this log (you can have up to 8)max_lines— Maximum lines in the buffer (older lines are discarded)font_size— Text size in pixels
# gui.log_add(id, text, r, g, b, a)
gui.log_add(0, "System started", 0, 255, 100, 255) # green
gui.log_add(0, "WARNING: low memory", 255, 200, 0, 255) # yellow
gui.log_add(0, "ERROR: file not found", 255, 60, 60, 255) # red
Call this every frame inside your render loop:
gui.log_draw(0)
The log automatically:
- Scrolls to the bottom when new lines arrive
- Supports mouse-wheel scrolling to view history
- Shows a scrollbar when content overflows
- Clips text to the panel boundaries
gui.log_clear(0) # Remove all lines
var n: int = gui.log_count(0) # Get number of lines
FORGE GUI uses RGBA values (0–255). Here are useful presets:
| Color | R | G | B | A |
|---|---|---|---|---|
| Black | 0 | 0 | 0 | 255 |
| White | 255 | 255 | 255 | 255 |
| Red | 255 | 60 | 60 | 255 |
| Green | 0 | 200 | 0 | 255 |
| Blue | 60 | 120 | 255 | 255 |
| Yellow | 255 | 255 | 0 | 255 |
| Cyan | 0 | 255 | 255 | 255 |
| Dark background | 30 | 30 | 35 | 255 |
| Light gray text | 180 | 180 | 180 | 255 |
| Transparent | 0 | 0 | 0 | 0 |
Alpha channel: 255 = fully opaque, 0 = fully transparent.
| Function | Returns | Description |
|---|---|---|
gui.init_window(w, h, title) |
void | Create a window |
gui.close_window() |
void | Destroy the window |
gui.window_open() |
bool | true until close is requested |
gui.set_target_fps(fps) |
void | Set frame rate limit |
gui.get_fps() |
int | Get current frames per second |
gui.get_dt() |
float | Seconds since last frame |
| Function | Returns | Description |
|---|---|---|
gui.begin_draw() |
void | Start a frame |
gui.end_draw() |
void | End a frame (present to screen) |
gui.clear(r, g, b, a) |
void | Fill background |
| Function | Returns | Description |
|---|---|---|
gui.draw_line(x1, y1, x2, y2, r, g, b, a) |
void | Draw a line |
gui.draw_rect(x, y, w, h, r, g, b, a) |
void | Filled rectangle |
gui.draw_rect_lines(x, y, w, h, r, g, b, a) |
void | Rectangle outline |
gui.draw_circle(cx, cy, radius, r, g, b, a) |
void | Filled circle |
gui.draw_circle_lines(cx, cy, radius, r, g, b, a) |
void | Circle outline |
| Function | Returns | Description |
|---|---|---|
gui.draw_text(text, x, y, size, r, g, b, a) |
void | Draw text |
gui.measure_text(text, size) |
int | Get text width in pixels |
| Function | Returns | Description |
|---|---|---|
gui.load_font(path, size) |
int | Load a TTF/OTF font. Returns slot id (0–7) or -1 on failure. Call after init_window. |
gui.set_font(id) |
void | Activate a loaded font for all text rendering. Pass -1 to revert to default. |
gui.unload_font(id) |
void | Free a font slot and its GPU memory. |
| Function | Returns | Description |
|---|---|---|
gui.is_key_pressed(key) |
bool | Key pressed this frame |
gui.is_key_down(key) |
bool | Key currently held |
gui.is_key_released(key) |
bool | Key released this frame |
gui.get_key_pressed() |
int | Last key pressed (0 = none) |
| Function | Returns | Description |
|---|---|---|
gui.mouse_x() |
int | Mouse X position |
gui.mouse_y() |
int | Mouse Y position |
gui.is_mouse_pressed(btn) |
bool | Button pressed this frame |
gui.is_mouse_down(btn) |
bool | Button currently held |
| Function | Returns | Description |
|---|---|---|
gui.button(x, y, w, h, text) |
bool | Standard button |
gui.color_button(x, y, w, h, text, bg_rgba, tx_rgba) |
bool | Colored button |
gui.label(x, y, w, h, text) |
void | Static label |
gui.checkbox(x, y, w, h, text, checked) |
int | Checkbox toggle |
gui.slider(x, y, w, h, min, max, value) |
float | Value slider |
gui.textbox(x, y, w, h, text, max_len) |
str | Editable text input |
gui.dropdown(x, y, w, h, items, selected) |
int | Dropdown selector |
| Function | Returns | Description |
|---|---|---|
gui.log_create(id, x, y, w, h, max, size) |
void | Create log panel |
gui.log_add(id, text, r, g, b, a) |
void | Add colored line |
gui.log_clear(id) |
void | Clear all lines |
gui.log_draw(id) |
void | Render the log panel |
gui.log_count(id) |
int | Number of lines |
| Function | Returns | Description |
|---|---|---|
gui.set_style_dark() |
void | Dark theme for raygui widgets |
gui.set_style_light() |
void | Default light theme |
A minimal program demonstrating a window, text, shapes, and a button:
import forge.gui
proc main() -> void:
gui.init_window(640, 480, "Hello GUI")
gui.set_target_fps(60)
gui.set_style_dark()
var clicks: int = 0
while gui.window_open():
gui.begin_draw()
gui.clear(30, 30, 35, 255)
# Title
gui.draw_text("FORGE GUI Demo", 220, 30, 28, 255, 255, 255, 255)
# Draw a colored rectangle
gui.draw_rect(200, 100, 240, 80, 60, 120, 200, 255)
gui.draw_text("A blue box", 260, 130, 20, 255, 255, 255, 255)
# Button
if gui.button(250, 250, 140, 40, "Click Me"):
clicks = clicks + 1
# Show click count
gui.draw_text("Clicks: " + str(clicks), 270, 310, 20, 200, 200, 200, 255)
gui.end_draw()
gui.close_window()
See examples/nmea_terminal.fg for a complete, working NMEA 0183 serial
terminal built entirely in FORGE. It demonstrates:
- Scrollable log with colored text
- Serial port connection (using
forge.serial) - NMEA sentence validation (using
forge.nmea) - Toggle buttons, dropdown, textbox
- TX/RX activity indicators
- Status bar layout
- Custom font loading (DejaVu Sans Mono via
gui.load_font)
Run it with:
./forge run examples/nmea_terminal.fgFORGE GUI Library Guide v0.1 — Fragillidae Software