ModernGL
Quick Guide
| Parameter | Type | Description |
|---|---|---|
context |
moderngl.Context |
The active ModernGL graphics context |
driver |
cui.Windower |
Either cui.GLFW or cui.MGLW |
Introduction
ModernGLBackend is CoshUI's backend for raw OpenGL rendering via ModernGL. Unlike PygameBackend or RaylibBackend, ModernGL isn't a full game framework — it's a thin wrapper around OpenGL itself — which means CoshUI needs to know both the graphics context to render into and which windowing system is supplying input and window size. That's where the Windower comes in.
import moderngl
import glfw
import coshui as cui
glfw.init()
window = glfw.create_window(800, 600, "My App", None, None)
glfw.make_context_current(window)
context = moderngl.create_context()
# Created once, outside the loop, since shader compilation is expensive
backend = cui.ModernGLBackend(context, cui.GLFW)
while not glfw.window_should_close(window):
glfw.poll_events()
with cui.CoshUIRenderer(backend):
cui.Label(id="hello", text="Hello, ModernGL!")
glfw.swap_buffers(window)
It's important to construct ModernGLBackend once, outside your render loop, since its constructor compiles all of CoshUI's shaders (rect, text, and image) up front. Recreating it every frame would mean recompiling every shader every frame.
Windowers
CoshUI supports two windowers for ModernGL: cui.GLFW and cui.MGLW (moderngl-window). The windower is responsible for two things — reporting the window size via _get_size(), and polling mouse input via _poll_input() — both of which CoshUI calls automatically every frame.
MGLW Input Caveat
The MGLW driver reads mouse position from moderngl_window.window()._mouse_pos, which moderngl-window only populates if you've already wired up mouse movement tracking yourself (typically by calling _calc_mouse_delta() or setting _mouse_pos in your own on_mouse_position_event). If you're seeing no mouse interaction with MGLW, this is almost always why.
How Rendering Works
ModernGLBackend.flush() walks the render stack and dispatches each RenderContext to _draw_rect, _draw_text, or _draw_image. Each of these uploads a small quad (6 vertices, 2 triangles) to a reusable vertex buffer and draws it using a dedicated shader program for that draw type.
Rects and Borders
Rect drawing uses a signed-distance-field (SDF) approach in rect.frag, which computes whether each pixel falls inside the rounded rect shape, the border ring, or outside entirely — entirely on the GPU. This is what allows asymmetric border radii and anti-aliased rounded corners without needing a CPU-side fallback like RaylibBackend does. Border radius values are clamped to half the rect's smallest dimension to avoid visual artifacts on small Nodes with large radii.
Text
Text rendering uses a glyph atlas system (gl_font.py) built with freetype-py. Each unique (font_path, font_size) combination generates a 512x512 texture atlas containing the printable ASCII glyph set, which is then cached and reused. Glyph quads are batched into a single vertex buffer per line and drawn in one draw call. The text shader (text.frag) samples the alpha channel of the atlas texture to render anti-aliased glyph edges.
Images
Images are loaded via Pillow (PIL.Image) and uploaded as a texture once, then cached by file path. They're drawn as a textured quad using the image shader.
Clipping
Clipping uses OpenGL's scissor test (context.scissor), converting CoshUI's clip rect from top-left-origin screen coordinates into OpenGL's bottom-left-origin scissor coordinates.
Input Polling
Input polling is delegated entirely to the chosen Windower driver. ModernGLBackend.poll_input() simply calls self.driver._poll_input(), which updates CoshInput's mouse position, delta, and pressed state based on whichever windowing library is active.