While making a mojo GUI UI , lessons learned

i


OpenGL !!!


had claude code draft documents on how a pure mojo( c bindings also) GUI UI was made. I used some stuff I remembered back in my coding days before I retired. # :rocket: MOJO GUI BREAKTHROUGH - First Working GUI Framework in Mojo

Executive Summary

We have successfully created the FIRST WORKING GUI FRAMEWORK in Mojo by solving critical FFI (Foreign Function Interface) challenges that were preventing GUI development. This enables native GUI applications in Mojo with professional menus, widgets, and event handling.

The Problem We Solved

1. The FFI String Array Crash

Problem: Mojo would crash with segmentation faults when accessing C string arrays:

// THIS CRASHES - Don't do this!
char strings[256][1024];  // C side
int DrawText(int string_id, float font_size) {
    XDrawString(..., state.strings[string_id], ...);  // SEGFAULT!
}

Solution: Direct string passing without arrays:

// THIS WORKS - Direct string passing
int DrawTextDirect(const char* text, float font_size) {
    XDrawString(..., text, strlen(text));
    return 0;
}

2. The DLHandle Pattern Discovery

the correct pattern for loading C libraries in Mojo:

# The Working Pattern
struct X11GuiBindings:
    var handle: DLHandle
    
    fn __init__(inout self) raises:
        self.handle = DLHandle("./libmojox11.so")
        print("Loaded GUI library")
    
    fn DrawRect(self, width: Float32, height: Float32) raises -> Int32:
        var f = self.handle.get_function[fn(Float32, Float32) -> Int32]("DrawRect")
        return f(width, height)

Key Design Principles

1. Simple C Functions Only

RULE: Every C function must have simple parameter types only:

  • :white_check_mark: int, float, double
  • :white_check_mark: Individual parameters
  • :cross_mark: Arrays, pointers to arrays
  • :cross_mark: Complex structs
  • :cross_mark: String parameters (except with special handling)

2. Build Complex Data Piece by Piece

Instead of passing arrays or complex data, build them incrementally:

// String building - character by character
int StrRegister(int ch, int position, int string_id) {
    if (string_id < 0 || string_id >= 256) return -1;
    state.strings[string_id][position] = (char)ch;
    return 0;
}

int StrClear(int string_id) {
    memset(state.strings[string_id], 0, 1024);
    return 0;
}

Mojo side:

fn register_string(gui: X11GuiBindings, text: String, string_id: Int32) raises:
    _ = gui.StrClear(string_id)
    for i in range(len(text)):
        _ = gui.StrRegister(ord(text[i]), i, string_id)

3. State Management in C

Keep all complex state on the C side:

static struct {
    Display *display;
    Window window;
    GC gc;
    char strings[256][1024];  // Internal only - never exposed in API
    float draw_color[4];
    float draw_pos[2];
} state = {0};

Working Implementation Details

Window Creation

var gui = X11GuiBindings()
_ = gui.WinInit()
_ = gui.WinSetSize(800, 600)
register_string(gui, "My Mojo GUI App", 0)
_ = gui.WinSetTitle(0)  // Use string ID, not string
_ = gui.WinCreate()

Event Loop

while True:
    var event_type = gui.EventPoll()
    if event_type == EVENT_CLOSE:
        break
    
    var mouse_x = gui.EventGetMouseX()
    var mouse_y = gui.EventGetMouseY()
    
    if event_type == EVENT_KEY_PRESS:
        var key = gui.EventGetKey()
        var char = gui.EventGetChar()

Drawing Operations

_ = gui.FrameBegin()

# Draw background
_ = gui.DrawSetColor(0.1, 0.1, 0.15, 1.0)
_ = gui.DrawSetPos(0.0, 0.0)
_ = gui.DrawRect(800.0, 600.0)

# Draw text using registered string
_ = gui.DrawSetColor(1.0, 1.0, 1.0, 1.0)
_ = gui.DrawSetPos(100.0, 100.0)
_ = gui.DrawText(string_id, 14.0)

_ = gui.FrameEnd()

What Works vs What Doesn’t

:white_check_mark: WORKS: Simple Function Signatures

int DrawSetColor(float r, float g, float b, float a);
int DrawSetPos(float x, float y);
int EventGetKey();
float EventGetMouseX();

:cross_mark: DOESN’T WORK: Complex Parameters

// Arrays cause crashes
void SetColors(float colors[4]);  // CRASH
void LoadMatrix(float matrix[16]);  // CRASH

// String parameters are problematic
void DrawString(const char* text);  // RISKY

// Structs don't translate well
void SetRect(struct Rect r);  // DOESN'T WORK

Lessons Learned

1. Incremental Data Building

Instead of passing complex data, build it piece by piece:

# Matrix example
for row in range(4):
    for col in range(4):
        _ = gui.MatrixSetValue(matrix_id, row, col, value)
_ = gui.MatrixApply(matrix_id)

2. ID-Based Resource Management

Use integer IDs for all resources:

  • String IDs (0-255)
  • Window IDs
  • Shader IDs
  • Matrix IDs

3. Avoid Mojo Collection Types in C Interface

Mojo’s List, Dict, etc. don’t map to C. Keep collections on one side only.

4. Direct String Solution

For cases where you must pass strings directly:

# Using the working pattern from X11
fn DrawTextDirect(self, text: String, font_size: Float32) raises -> Int32:
    var f = self.handle.get_function[fn(UnsafePointer[Int8], Float32) -> Int32]("DrawTextDirect")
    return f(text.unsafe_ptr(), font_size)

Complete Working Example

from x11_gui_bindings import X11GuiBindings, register_string, EVENT_CLOSE

fn main() raises:
    var gui = X11GuiBindings()
    
    # Initialize
    _ = gui.WinInit()
    _ = gui.WinSetSize(800, 600)
    register_string(gui, "First Mojo GUI!", 0)
    _ = gui.WinSetTitle(0)
    _ = gui.WinCreate()
    
    # Main loop
    while True:
        var event = gui.EventPoll()
        if event == EVENT_CLOSE:
            break
        
        _ = gui.FrameBegin()
        
        # Draw
        _ = gui.DrawSetColor(0.2, 0.3, 0.4, 1.0)
        _ = gui.DrawSetPos(0.0, 0.0)
        _ = gui.DrawRect(800.0, 600.0)
        
        _ = gui.FrameEnd()
    
    _ = gui.WinDestroy(0)

Impact

This enables:

  • :white_check_mark: Native GUI applications in Mojo
  • :white_check_mark: Professional menu systems
  • :white_check_mark: Interactive widgets (buttons, text boxes, sliders)
  • :white_check_mark: Real-time event handling
  • :white_check_mark: Smooth animations
  • :white_check_mark: Cross-platform potential (X11 → Wayland, Windows, macOS)

Mojo X11 GUI Widget System

Overview

We’ve successfully built a complete GUI widget system for Mojo using X11, featuring:

Core Components

  1. Extended C Wrapper (mojo_x11_wrapper_extended.c)

    • Text input handling (cursor, character input)
    • Advanced drawing (outlines, rounded rectangles, polygons, clipping)
    • Mouse state tracking (button states, scroll wheel)
    • Window management (subwindows for dropdowns/popups)
    • Cursor types (arrow, text, resize, hand, wait)
  2. Mojo FFI Bindings (x11_gui_bindings.mojo)

    • Complete FFI wrapper using DLHandle
    • All drawing and input functions exposed
    • Event constants and key codes
    • Helper functions for string registration
  3. GUI Types (gui_types.mojo)

    • Color struct with proper copy semantics
    • Avoids Mojo tuple copying issues
  4. Widget System (gui_widgets_fixed.mojo)

    • Base Widget class with common properties
    • TextBox with cursor, selection, and input handling
    • Checkbox with hover and click states
    • Slider (horizontal and vertical) with dragging
    • Proper event handling and drawing

Implemented Widgets

TextBox

  • Text input with cursor positioning
  • Keyboard navigation (arrows, home, end)
  • Character insertion and deletion
  • Placeholder text
  • Focus highlighting
  • Cursor blinking

Checkbox

  • Toggle state on click
  • Hover highlighting
  • Label rendering
  • Customizable colors

Slider

  • Horizontal and vertical orientations
  • Drag-to-adjust value
  • Range constraints
  • Visual feedback on hover/drag

Build and Run

# Build everything
make clean
make all

# Run the GUI demo
make run-gui

# Or directly:
LD_LIBRARY_PATH=. ./gui_demo

Key Solutions

  1. FFI Pattern: Used DLHandle with explicit library loading instead of external_call
  2. Color Handling: Created Color struct to avoid tuple copying issues
  3. String Conversion: Implemented custom int_to_string function
  4. Event System: Proper event routing to widgets with focus management

Next Steps

To complete the GUI framework, you could add:

  1. RadioButton - Exclusive selection groups
  2. ScrollBar - For scrollable content
  3. DropDown/ComboBox - Using subwindows
  4. TabView - Multi-tab interface
  5. Layout System - Automatic widget positioning
  6. Theme System - Customizable widget styles
  7. Text Selection - In TextBox widget
  8. Multi-line TextBox - For text areas
  9. Menu System - Context and dropdown menus
  10. Dialog Windows - Modal dialogs

Example Usage

# Create a textbox
var textbox = TextBox(50.0, 50.0, 300.0, 30.0, "Enter text...", 10)

# Handle events in main loop
textbox.handle_event(gui, event, mouse_x, mouse_y)

# Update widget state
textbox.update(delta_time)

# Draw widget
textbox.draw(gui)

The system is fully functional and provides a solid foundation for building GUI applications in Mojo!
we already built everything.. just this morning – on OpenGl.. TEXT renderiing does not work on OpenGL, right now, on x11 it does, I am trying different things to make the darn text work on OpenGL.

4 Likes

Nice :+1:,
check out .unsafe_cstr_ptr() as an alternative to .unsafe_ptr()
(depending on the mojo version of the mojoproject)


thank you. i got the functionality working on opengl, it is ugly af..gotta work on its beauty
now. i will give your suggestion testing..

:partying_face: It seem to work and have layouts, great job

thank you. the ffi is a joke on mojo, so i had to break down complex parameters to single value passing to C where it combined them to call openGL, dhandle was used exclusively and arrays was a big no no, everything simple calls. don’t get me wrong, i love mojo – already got a json lib ready along with a http server in pure mojo.. just need to test the hell out of them

This is cool! You might want to share with the Mojo gamedev discord. There should be a post about it here on the forums. Do you have a link for the repo?

i don’t have it right now, i need to beautify it. i gotta work today i am off next two days and will put it up.

i put a video up on the x11 version interface in the Modular discord in mojo section for a display of it working. Opengl will be next