Building a GUI Library from Scratch

"Every GUI toolkit in existence is just rectangles, text, and callbacks all the way down."


Table of Contents

  1. Introduction and Philosophy
  2. SDL2 Fundamentals
  3. The Rendering Abstraction Layer
  4. The Widget Model
  5. The Box Model and Layout Engine
  6. Input Handling and the Event System
  7. Focus Management and Keyboard Navigation
  8. Font Rendering with SDL_ttf
  9. Core Widgets — Labels and Buttons
  10. Text Input and Editing
  11. Containers — Panels, Rows, Columns
  12. Scrollable Views
  13. Theming and Styling
  14. Animation System
  15. Advanced Widgets
  16. Project — Building a Complete Application

Chapter 1: Introduction and Philosophy

1.1 What We Are Building

We are going to build a complete, production-quality GUI library in C using SDL2 as the only dependency. By the end of this guide you will have:

  • A retained-mode widget tree with parent-child relationships
  • A box-model layout engine inspired by CSS flexbox
  • A full event system with bubbling and capturing
  • Proper focus management and keyboard navigation
  • Text rendering with SDL_ttf supporting wrapping, alignment, and caret
  • A complete widget set: Label, Button, Checkbox, Radio, Slider, TextInput, ListBox, ComboBox, ScrollView, Panel, Image
  • A theming system with colour tokens and style inheritance
  • A tweening animation engine
  • A complete demo application

The result will be named cg (C GUI). The full source is structured to be readable and hackable — not a black box.

1.2 Retained Mode vs Immediate Mode

There are two dominant paradigms for GUI architecture:

Immediate Mode (IMGUI): Every frame, you call functions that both declare the widget AND handle its logic. There is no persistent widget state; the application re-specifies the entire UI every frame.

// Immediate mode example (Dear ImGui style)
void app_frame(void) {
    if (igButton("Click me", (ImVec2){100, 30})) {
        handle_click();
    }
    igSliderFloat("Value", &my_value, 0.0f, 1.0f, "%.2f", 0);
}

Retained Mode: You build a tree of widget objects once (or when the UI structure changes). The tree persists between frames; the library tracks state, layout, and rendering. Qt, GTK, Win32, and every major production toolkit use retained mode.

// Retained mode (our approach)
CgWidget* btn = cg_button_new("Click me");
cg_widget_on_click(btn, on_click_handler, NULL);
cg_panel_add(panel, btn);
// The button now lives in the tree until explicitly destroyed

We choose retained mode because:

  • Layout is computed only when the tree changes — not every frame
  • Widget state (hover, focus, pressed, scroll position) lives naturally in the widget
  • Animations are trivial: change a property, interpolate over time
  • Accessibility metadata lives on widgets
  • The mental model matches how users think about UI

1.3 Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                    Application Code                         │
│   (creates widgets, sets callbacks, modifies properties)    │
└─────────────────┬───────────────────────────────────────────┘
                  │
┌─────────────────▼───────────────────────────────────────────┐
│                      cg — our library                       │
│                                                             │
│  Widget Tree          Layout Engine        Event System     │
│  ┌──────────┐         ┌──────────────┐    ┌──────────────┐  │
│  │  Window  │         │ Measure Pass │    │ Hit Testing  │  │
│  │  ├ Panel │         │ Arrange Pass │    │ Bubbling     │  │
│  │  │  ├ Btn│         │ Clip Rects   │    │ Capture      │  │
│  │  │  └ Lbl│         └──────────────┘    └──────────────┘  │
│  │  └ Input │                                               │
│  └──────────┘         Theme System         Animation        │
│                       ┌──────────────┐    ┌──────────────┐  │
│                       │ Colour Tokens│    │ Tween Engine │  │
│                       │ Style Inherit│    │ Easing Fns   │  │
│                       └──────────────┘    └──────────────┘  │
└─────────────────┬───────────────────────────────────────────┘
                  │
┌─────────────────▼──────────────────────────────────────────┐
│              Rendering Abstraction (cg_draw.c)             │
│  rect / rounded_rect / border / text / image / clip        │
└─────────────────┬──────────────────────────────────────────┘
                  │
┌─────────────────▼──────────────────────────────────────────┐
│                       SDL2 + SDL_ttf                       │
│        SDL_RenderFillRect / SDL_RenderDrawLine             │
│        TTF_RenderText_Blended / SDL_RenderCopy             │
└────────────────────────────────────────────────────────────┘

1.4 Project Structure

cg/
├── include/
│   └── cg.h               ← Public API (single header)
├── src/
│   ├── cg_core.c          ← Widget tree, lifecycle
│   ├── cg_layout.c        ← Box model + flexbox layout
│   ├── cg_draw.c          ← Rendering primitives
│   ├── cg_event.c         ← Event system
│   ├── cg_focus.c         ← Focus + keyboard nav
│   ├── cg_theme.c         ← Theming
│   ├── cg_anim.c          ← Animation/tweening
│   ├── cg_font.c          ← Font management + text layout
│   ├── widgets/
│   │   ├── cg_label.c
│   │   ├── cg_button.c
│   │   ├── cg_input.c
│   │   ├── cg_checkbox.c
│   │   ├── cg_slider.c
│   │   ├── cg_scroll.c
│   │   ├── cg_panel.c
│   │   ├── cg_image.c
│   │   └── cg_combo.c
│   └── cg_internal.h      ← Internal types (not exported)
├── demo/
│   └── main.c
└── CMakeLists.txt

1.5 Build System

# CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(cg VERSION 0.1 LANGUAGES C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

# Find SDL2 and SDL_ttf
find_package(SDL2   REQUIRED)
find_package(SDL2_ttf REQUIRED)
find_package(SDL2_image REQUIRED)

# Library sources
file(GLOB_RECURSE CG_SOURCES "src/*.c" "src/widgets/*.c")

add_library(cg STATIC ${CG_SOURCES})

target_include_directories(cg
    PUBLIC  include/
    PRIVATE src/)

target_link_libraries(cg
    PUBLIC SDL2::SDL2
    PUBLIC SDL2_ttf::SDL2_ttf
    PUBLIC SDL2_image::SDL2_image)

# Demo executable
add_executable(cg_demo demo/main.c)
target_link_libraries(cg_demo PRIVATE cg)

# Compiler warnings
target_compile_options(cg PRIVATE
    -Wall -Wextra -Wpedantic
    $<$<CONFIG:Debug>:-g -O0 -fsanitize=address,undefined>
    $<$<CONFIG:Release>:-O2>)

Install dependencies:

# Ubuntu/Debian
sudo apt install libsdl2-dev libsdl2-ttf-dev libsdl2-image-dev

# macOS
brew install sdl2 sdl2_ttf sdl2_image

# Arch Linux
sudo pacman -S sdl2 sdl2_ttf sdl2_image

Chapter 2: SDL2 Fundamentals

Before building the library, we need a solid understanding of the SDL2 primitives we will be using.

2.1 SDL2 Coordinate System and Renderer

SDL2 uses a coordinate system where (0, 0) is the top-left corner, X increases rightward, and Y increases downward. Rectangles are specified by {x, y, w, h}.

#include <SDL2/SDL.h>

int main(void) {
    SDL_Init(SDL_INIT_VIDEO);

    SDL_Window* window = SDL_CreateWindow(
        "CG Demo",
        SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
        800, 600,
        SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI
    );

    SDL_Renderer* renderer = SDL_CreateRenderer(
        window, -1,
        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
    );

    // HiDPI: the logical size may differ from the pixel size
    int window_w, window_h;       // Logical (CSS) pixels
    int drawable_w, drawable_h;   // Physical pixels
    SDL_GetWindowSize(window, &window_w, &window_h);
    SDL_GL_GetDrawableSize(window, &drawable_w, &drawable_h);
    float dpi_scale = (float)drawable_w / (float)window_w;

    // Set logical size so coordinates are in CSS pixels
    SDL_RenderSetLogicalSize(renderer, window_w, window_h);

    bool running = true;
    while (running) {
        SDL_Event e;
        while (SDL_PollEvent(&e)) {
            if (e.type == SDL_QUIT) running = false;
        }

        // Clear with background colour
        SDL_SetRenderDrawColor(renderer, 30, 30, 30, 255);
        SDL_RenderClear(renderer);

        // Draw a red rectangle
        SDL_SetRenderDrawColor(renderer, 220, 50, 50, 255);
        SDL_Rect rect = {100, 100, 200, 80};
        SDL_RenderFillRect(renderer, &rect);

        SDL_RenderPresent(renderer);
    }

    SDL_DestroyRenderer(renderer);
    SDL_DestroyWindow(window);
    SDL_Quit();
    return 0;
}

2.2 SDL2 Events

SDL2 delivers events through a queue. The most important event types:

SDL_Event e;
while (SDL_PollEvent(&e)) {
    switch (e.type) {

    // Window
    case SDL_QUIT:              break;
    case SDL_WINDOWEVENT:
        if (e.window.event == SDL_WINDOWEVENT_RESIZED) {
            int w = e.window.data1, h = e.window.data2;
        }
        break;

    // Mouse
    case SDL_MOUSEMOTION:
        // e.motion.x, e.motion.y  — position
        // e.motion.xrel, e.motion.yrel — delta since last event
        break;
    case SDL_MOUSEBUTTONDOWN:
    case SDL_MOUSEBUTTONUP:
        // e.button.button — SDL_BUTTON_LEFT/MIDDLE/RIGHT
        // e.button.x, e.button.y
        // e.button.clicks — 1 or 2 (double-click)
        break;
    case SDL_MOUSEWHEEL:
        // e.wheel.x, e.wheel.y (positive = scroll up/right)
        // e.wheel.direction — SDL_MOUSEWHEEL_NORMAL or FLIPPED
        break;

    // Keyboard
    case SDL_KEYDOWN:
    case SDL_KEYUP:
        // e.key.keysym.sym  — SDL_Keycode (SDLK_a, SDLK_RETURN, ...)
        // e.key.keysym.scancode — physical key (SDL_SCANCODE_*)
        // e.key.keysym.mod — modifiers (KMOD_CTRL, KMOD_SHIFT, ...)
        // e.key.repeat — non-zero if key-repeat event
        break;

    // Text input (handles IME, Unicode, key combos correctly)
    case SDL_TEXTINPUT:
        // e.text.text — UTF-8 string (up to 32 bytes)
        break;
    case SDL_TEXTEDITING:
        // e.edit.text, e.edit.start, e.edit.length — IME composition
        break;
    }
}

2.3 Clipping

SDL2 supports clip rectangles. Any drawing outside the clip rect is silently discarded. We use this for scrollable containers and widget clipping:

SDL_Rect clip = {50, 50, 200, 100};
SDL_RenderSetClipRect(renderer, &clip);

// Anything drawn here is clipped to the 200x100 region
SDL_RenderFillRect(renderer, &some_big_rect);

// Clear clip (back to full screen)
SDL_RenderSetClipRect(renderer, NULL);

Important: SDL2's clip rect is a single stack level — there is no push/pop. Our library must implement clip stack management manually (we will do this in Chapter 3).

2.4 Alpha Blending

SDL2 supports alpha blending. Set blend mode on the renderer for semi-transparent drawing:

SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 128);  // 50% black
SDL_RenderFillRect(renderer, &shadow_rect);

For textures (images, text surfaces), set blend mode on the texture:

SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
SDL_SetTextureAlphaMod(texture, 200);  // 0–255
SDL_RenderCopy(renderer, texture, NULL, &dst_rect);

2.5 Texture Management

All rendered elements — text, images — live in SDL_Texture objects. Textures are GPU-resident and must be explicitly destroyed. Our library will implement a texture cache to avoid re-rendering the same text repeatedly.

// Create a texture from a surface (e.g., rendered text)
SDL_Surface* surface = TTF_RenderUTF8_Blended(font, text, color);
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_FreeSurface(surface);  // Surface can be freed once texture is created

// Query texture dimensions
int w, h;
SDL_QueryTexture(texture, NULL, NULL, &w, &h);

// Render texture
SDL_Rect dst = {x, y, w, h};
SDL_RenderCopy(renderer, texture, NULL, &dst);

// Cleanup
SDL_DestroyTexture(texture);

Chapter 3: The Rendering Abstraction Layer

We wrap SDL2's rendering API in a thin layer that adds: rounded rectangles, clip stack management, a draw call batch, and convenience functions used throughout the library.

3.1 The CgDraw Context

// src/cg_internal.h (excerpt)
#pragma once
#include <SDL2/SDL.h>
#include <SDL2/SDL_ttf.h>
#include "cg.h"

#define CG_CLIP_STACK_MAX 32
#define CG_MAX_FONTS      64

typedef struct CgCtx {
    SDL_Window*    window;
    SDL_Renderer*  renderer;
    int            window_w, window_h;
    float          dpi_scale;

    // Clip stack
    SDL_Rect       clip_stack[CG_CLIP_STACK_MAX];
    int            clip_depth;

    // Theme
    CgTheme*       theme;

    // Font registry
    TTF_Font*      fonts[CG_MAX_FONTS];
    char*          font_paths[CG_MAX_FONTS];
    int            font_sizes[CG_MAX_FONTS];
    int            font_count;

    // Widget tree root
    struct CgWidget* root;

    // Focus
    struct CgWidget* focused;

    // Mouse state (persists between events)
    int            mouse_x, mouse_y;
    bool           mouse_buttons[5];
    struct CgWidget* hovered;
    struct CgWidget* mouse_captured;  // Widget that captured the mouse

    // Animation list
    struct CgAnim* anim_head;

    // Frame timing
    Uint32         last_tick;
    float          delta_ms;
} CgCtx;

extern CgCtx* cg;  // Global context (one per application)

3.2 Colour Type

// include/cg.h (excerpt)
#pragma once
#include <SDL2/SDL.h>
#include <stdbool.h>
#include <stdint.h>

// Colours stored as RGBA bytes
typedef struct { uint8_t r, g, b, a; } CgColor;

#define CG_RGBA(r,g,b,a)  ((CgColor){r, g, b, a})
#define CG_RGB(r,g,b)     ((CgColor){r, g, b, 255})
#define CG_TRANSPARENT    ((CgColor){0,0,0,0})

// Convert to SDL_Color
static inline SDL_Color cg_to_sdl_color(CgColor c) {
    return (SDL_Color){c.r, c.g, c.b, c.a};
}

// Interpolate two colours (t in 0..1)
static inline CgColor cg_color_lerp(CgColor a, CgColor b, float t) {
    return (CgColor){
        (uint8_t)(a.r + (b.r - a.r) * t),
        (uint8_t)(a.g + (b.g - a.g) * t),
        (uint8_t)(a.b + (b.b - a.b) * t),
        (uint8_t)(a.a + (b.a - a.a) * t),
    };
}

// Darken/lighten
static inline CgColor cg_color_darken(CgColor c, float amount) {
    return (CgColor){
        (uint8_t)(c.r * (1.0f - amount)),
        (uint8_t)(c.g * (1.0f - amount)),
        (uint8_t)(c.b * (1.0f - amount)),
        c.a
    };
}

3.3 Clip Stack

// src/cg_draw.c

#include "cg_internal.h"
#include <string.h>

// Push a clip rect — intersects with the current clip
void cg_push_clip(SDL_Rect rect) {
    SDL_Rect current;
    SDL_RenderGetClipRect(cg->renderer, &current);

    SDL_Rect intersection;
    if (cg->clip_depth > 0) {
        // Intersect with parent clip
        int x1 = SDL_max(current.x, rect.x);
        int y1 = SDL_max(current.y, rect.y);
        int x2 = SDL_min(current.x + current.w, rect.x + rect.w);
        int y2 = SDL_min(current.y + current.h, rect.y + rect.h);
        intersection = (SDL_Rect){x1, y1, SDL_max(0, x2-x1), SDL_max(0, y2-y1)};
    } else {
        intersection = rect;
    }

    if (cg->clip_depth < CG_CLIP_STACK_MAX) {
        cg->clip_stack[cg->clip_depth++] = intersection;
        SDL_RenderSetClipRect(cg->renderer, &intersection);
    }
}

void cg_pop_clip(void) {
    if (cg->clip_depth > 0) cg->clip_depth--;

    if (cg->clip_depth > 0) {
        SDL_RenderSetClipRect(cg->renderer, &cg->clip_stack[cg->clip_depth - 1]);
    } else {
        SDL_RenderSetClipRect(cg->renderer, NULL);
    }
}

3.4 Drawing Primitives

// Set draw colour from CgColor
static void set_color(CgColor c) {
    SDL_SetRenderDrawBlendMode(cg->renderer,
        c.a < 255 ? SDL_BLENDMODE_BLEND : SDL_BLENDMODE_NONE);
    SDL_SetRenderDrawColor(cg->renderer, c.r, c.g, c.b, c.a);
}

// Filled rectangle
void cg_draw_rect(SDL_Rect r, CgColor color) {
    if (color.a == 0) return;
    set_color(color);
    SDL_RenderFillRect(cg->renderer, &r);
}

// Rectangle outline (1px border)
void cg_draw_rect_outline(SDL_Rect r, int thickness, CgColor color) {
    if (color.a == 0 || thickness <= 0) return;
    set_color(color);
    for (int i = 0; i < thickness; i++) {
        SDL_Rect border = {r.x+i, r.y+i, r.w-2*i, r.h-2*i};
        SDL_RenderDrawRect(cg->renderer, &border);
    }
}

// Horizontal / vertical lines
void cg_draw_hline(int x, int y, int w, CgColor color) {
    if (color.a == 0) return;
    set_color(color);
    SDL_RenderDrawLine(cg->renderer, x, y, x + w - 1, y);
}

void cg_draw_vline(int x, int y, int h, CgColor color) {
    if (color.a == 0) return;
    set_color(color);
    SDL_RenderDrawLine(cg->renderer, x, y, x, y + h - 1);
}

3.5 Rounded Rectangles

SDL2 has no native rounded rectangle. We implement one using quarter-circle rasterisation:

// Draw a filled circle quadrant at (cx, cy)
static void fill_circle_quadrant(int cx, int cy, int r,
                                  int qx, int qy) { // qx,qy in {-1,+1}
    int x = 0, y = r, d = 3 - 2 * r;
    while (y >= x) {
        // Fill horizontal spans for both octants
        SDL_Rect row1 = {cx + qx * x, cy + qy * y, 1, 1};  // simplified
        (void)row1;
        // ... (see full Bresenham implementation below)
        x++;
        if (d < 0) d += 4 * x + 6;
        else { d += 4 * (x - y) + 10; y--; }
    }
}

// Full rounded rect implementation using SDL_RenderFillRects batch
void cg_draw_rounded_rect(SDL_Rect r, int radius, CgColor color) {
    if (color.a == 0) return;
    if (radius <= 0) { cg_draw_rect(r, color); return; }

    // Clamp radius to half the smallest dimension
    int max_r = (SDL_min(r.w, r.h)) / 2;
    if (radius > max_r) radius = max_r;

    set_color(color);

    // Strategy: fill three overlapping rectangles (cross shape) +
    // four filled circle quadrants in the corners.

    // Central column
    SDL_Rect col = {r.x, r.y + radius, r.w, r.h - 2 * radius};
    SDL_RenderFillRect(cg->renderer, &col);

    // Top and bottom bars (excluding corners)
    SDL_Rect top_bar = {r.x + radius, r.y, r.w - 2 * radius, radius};
    SDL_Rect bot_bar = {r.x + radius, r.y + r.h - radius,
                        r.w - 2 * radius, radius};
    SDL_RenderFillRect(cg->renderer, &top_bar);
    SDL_RenderFillRect(cg->renderer, &bot_bar);

    // Corner circles using Midpoint Circle Algorithm
    // We fill horizontal scanlines for each quarter circle
    int corners[4][2] = {
        {r.x + radius,           r.y + radius},         // top-left
        {r.x + r.w - radius - 1, r.y + radius},         // top-right
        {r.x + radius,           r.y + r.h - radius - 1},// bot-left
        {r.x + r.w - radius - 1, r.y + r.h - radius - 1} // bot-right
    };

    int x = 0, y = radius;
    int d = 1 - radius;

    while (x <= y) {
        // Top-left corner
        SDL_Rect s;

        s = (SDL_Rect){corners[0][0] - y, corners[0][1] - x, y, 1};
        SDL_RenderFillRect(cg->renderer, &s);
        s = (SDL_Rect){corners[0][0] - x, corners[0][1] - y, x, 1};
        SDL_RenderFillRect(cg->renderer, &s);

        // Top-right corner
        s = (SDL_Rect){corners[1][0] + 1, corners[1][1] - x, y, 1};
        SDL_RenderFillRect(cg->renderer, &s);
        s = (SDL_Rect){corners[1][0] + 1, corners[1][1] - y, x, 1};
        SDL_RenderFillRect(cg->renderer, &s);

        // Bot-left corner
        s = (SDL_Rect){corners[2][0] - y, corners[2][1] + 1, y, 1};
        SDL_RenderFillRect(cg->renderer, &s);
        s = (SDL_Rect){corners[2][0] - x, corners[2][1] + 1, x, 1};
        SDL_RenderFillRect(cg->renderer, &s);

        // Bot-right corner
        s = (SDL_Rect){corners[3][0] + 1, corners[3][1] + 1, y, 1};
        SDL_RenderFillRect(cg->renderer, &s);
        s = (SDL_Rect){corners[3][0] + 1, corners[3][1] + 1, x, 1};
        SDL_RenderFillRect(cg->renderer, &s);

        x++;
        if (d < 0) d += 2 * x + 1;
        else { d += 2 * (x - y) + 1; y--; }
    }
}

// Rounded rect outline only
void cg_draw_rounded_rect_outline(SDL_Rect r, int radius,
                                   int thickness, CgColor color) {
    if (color.a == 0) return;
    for (int i = 0; i < thickness; i++) {
        SDL_Rect inner = {r.x+i, r.y+i, r.w-2*i, r.h-2*i};
        // Draw each side as lines, corners as arcs
        cg_draw_hline(inner.x + radius, inner.y, inner.w - 2*radius, color);
        cg_draw_hline(inner.x + radius, inner.y + inner.h - 1,
                      inner.w - 2*radius, color);
        cg_draw_vline(inner.x, inner.y + radius, inner.h - 2*radius, color);
        cg_draw_vline(inner.x + inner.w - 1, inner.y + radius,
                      inner.h - 2*radius, color);
        // Arc corners omitted for brevity — see full source
    }
}

3.6 Drop Shadows

A real drop shadow requires blurring, which SDL2 doesn't natively support. We simulate it with layered semi-transparent rectangles:

void cg_draw_shadow(SDL_Rect r, int radius, int blur_radius, CgColor color) {
    // Stack semi-transparent offset rectangles for a soft shadow
    int layers = blur_radius;
    for (int i = layers; i >= 0; i--) {
        float alpha_t = 1.0f - (float)i / (float)(layers + 1);
        float alpha = (float)color.a * alpha_t * alpha_t * 0.4f;

        CgColor layer_color = {color.r, color.g, color.b, (uint8_t)alpha};
        SDL_Rect shadow_r = {
            r.x - i, r.y - i + layers / 2,
            r.w + 2 * i, r.h + 2 * i
        };
        cg_draw_rounded_rect(shadow_r, radius + i, layer_color);
    }
}

Chapter 4: The Widget Model

Every UI element in our library — a button, label, container, or window — is a CgWidget. Widgets form a tree. The library traverses this tree for layout, painting, and event dispatch.

4.1 Core Widget Structure

// include/cg.h

// Forward declarations
typedef struct CgWidget CgWidget;
typedef struct CgStyle  CgStyle;
typedef struct CgEvent  CgEvent;

// Widget type identifier
typedef uint32_t CgWidgetType;
#define CG_TYPE_BASE       0
#define CG_TYPE_LABEL      1
#define CG_TYPE_BUTTON     2
#define CG_TYPE_INPUT      3
#define CG_TYPE_CHECKBOX   4
#define CG_TYPE_RADIO      5
#define CG_TYPE_SLIDER     6
#define CG_TYPE_PANEL      7
#define CG_TYPE_SCROLL     8
#define CG_TYPE_IMAGE      9
#define CG_TYPE_COMBO      10
#define CG_TYPE_LISTBOX    11
#define CG_TYPE_SEPARATOR  12

// Virtual function table for widget polymorphism
typedef struct CgWidgetVTable {
    void  (*measure)  (CgWidget*, int avail_w, int avail_h);
    void  (*arrange)  (CgWidget*, SDL_Rect bounds);
    void  (*paint)    (CgWidget*);
    bool  (*on_event) (CgWidget*, CgEvent*);
    void  (*destroy)  (CgWidget*);
} CgWidgetVTable;

// Widget state flags (bit field)
typedef uint32_t CgWidgetFlags;
#define CG_FLAG_VISIBLE    (1u <<  0)  // Widget is drawn
#define CG_FLAG_ENABLED    (1u <<  1)  // Widget accepts input
#define CG_FLAG_HOVERED    (1u <<  2)  // Mouse is over widget
#define CG_FLAG_PRESSED    (1u <<  3)  // Primary button held
#define CG_FLAG_FOCUSED    (1u <<  4)  // Has keyboard focus
#define CG_FLAG_DIRTY      (1u <<  5)  // Needs re-layout
#define CG_FLAG_CLIP       (1u <<  6)  // Clip children to bounds
#define CG_FLAG_FOCUSABLE  (1u <<  7)  // Can receive keyboard focus
#define CG_FLAG_SELECTED   (1u <<  8)  // For checkboxes, listbox items
#define CG_FLAG_EXPAND_W   (1u <<  9)  // Expand to fill available width
#define CG_FLAG_EXPAND_H   (1u << 10)  // Expand to fill available height

// Edge insets (padding, margin)
typedef struct { int top, right, bottom, left; } CgInsets;
#define CG_INSETS(t,r,b,l)  ((CgInsets){t,r,b,l})
#define CG_INSETS_ALL(v)     ((CgInsets){v,v,v,v})
#define CG_INSETS_NONE       ((CgInsets){0,0,0,0})

// 2D size
typedef struct { int w, h; } CgSize;
#define CG_SIZE(w,h)   ((CgSize){w,h})
#define CG_SIZE_ZERO   ((CgSize){0,0})
#define CG_SIZE_WRAP   (-1)   // Wrap to content
#define CG_SIZE_FILL   (-2)   // Fill available space

// Event handler callback
typedef bool (*CgEventHandler)(CgWidget* sender, CgEvent* event, void* userdata);

// ── The Widget ─────────────────────────────────────────────────────────────

struct CgWidget {
    // Identity
    CgWidgetType         type;
    const CgWidgetVTable* vtable;
    uint32_t             id;         // Unique numeric ID
    char*                name;       // Optional debug name

    // Tree
    CgWidget*            parent;
    CgWidget*            first_child;
    CgWidget*            next_sibling;
    CgWidget*            prev_sibling;
    int                  child_count;

    // Layout inputs (set by application)
    int                  desired_w;  // CG_SIZE_WRAP / FILL / explicit pixels
    int                  desired_h;
    CgInsets             margin;
    CgInsets             padding;
    int                  min_w, max_w;
    int                  min_h, max_h;

    // Layout outputs (computed by layout engine)
    SDL_Rect             bounds;     // Final position+size in parent coords
    SDL_Rect             bounds_abs; // Final position+size in window coords
    CgSize               content_size; // Intrinsic content dimensions

    // State
    CgWidgetFlags        flags;

    // Style
    CgStyle*             style;      // NULL = inherit from theme

    // Event handlers (linked list of handlers per event type)
    struct CgHandlerNode* handlers;

    // Tooltip
    char*                tooltip;

    // Tab order
    int                  tab_index;  // -1 = not in tab order

    // Animation targets (linked list)
    struct CgAnim*       anims;
};

4.2 Widget Lifecycle

// src/cg_core.c

#include "cg_internal.h"
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

static uint32_t next_widget_id = 1;

// Allocate a new widget of a given size (widgets are always heap-allocated)
CgWidget* cg_widget_alloc(size_t size, CgWidgetType type,
                           const CgWidgetVTable* vtable) {
    CgWidget* w = calloc(1, size);
    if (!w) {
        fprintf(stderr, "[cg] Out of memory allocating widget\n");
        abort();
    }
    w->type       = type;
    w->vtable     = vtable;
    w->id         = next_widget_id++;
    w->flags      = CG_FLAG_VISIBLE | CG_FLAG_ENABLED;
    w->desired_w  = CG_SIZE_WRAP;
    w->desired_h  = CG_SIZE_WRAP;
    w->max_w      = 32767;
    w->max_h      = 32767;
    return w;
}

// Add child to parent (appended to end)
void cg_widget_add_child(CgWidget* parent, CgWidget* child) {
    if (child->parent) cg_widget_remove(child);

    child->parent = parent;
    child->next_sibling = NULL;

    if (!parent->first_child) {
        parent->first_child = child;
        child->prev_sibling = NULL;
    } else {
        // Walk to last child
        CgWidget* last = parent->first_child;
        while (last->next_sibling) last = last->next_sibling;
        last->next_sibling = child;
        child->prev_sibling = last;
    }
    parent->child_count++;
    cg_widget_mark_dirty(parent);
}

// Remove a widget from its parent
void cg_widget_remove(CgWidget* w) {
    if (!w->parent) return;

    CgWidget* parent = w->parent;

    if (w->prev_sibling) w->prev_sibling->next_sibling = w->next_sibling;
    else                 parent->first_child = w->next_sibling;

    if (w->next_sibling) w->next_sibling->prev_sibling = w->prev_sibling;

    w->parent       = NULL;
    w->prev_sibling = NULL;
    w->next_sibling = NULL;
    parent->child_count--;
    cg_widget_mark_dirty(parent);
}

// Recursively destroy widget tree
void cg_widget_destroy(CgWidget* w) {
    if (!w) return;

    // Remove from focus / hover / capture
    if (cg->focused  == w) cg->focused       = NULL;
    if (cg->hovered  == w) cg->hovered       = NULL;
    if (cg->mouse_captured == w) cg->mouse_captured = NULL;

    // Destroy children first
    CgWidget* child = w->first_child;
    while (child) {
        CgWidget* next = child->next_sibling;
        child->parent = NULL;  // Prevent remove() from touching our list
        cg_widget_destroy(child);
        child = next;
    }

    // Call type-specific cleanup
    if (w->vtable && w->vtable->destroy)
        w->vtable->destroy(w);

    // Free generic resources
    free(w->name);
    free(w->tooltip);
    // handlers are freed by cg_event_clear_handlers
    cg_event_clear_handlers(w);
    free(w);
}

// Mark dirty (needs re-layout)
void cg_widget_mark_dirty(CgWidget* w) {
    while (w) {
        w->flags |= CG_FLAG_DIRTY;
        w = w->parent;
    }
}

// Show / hide
void cg_widget_set_visible(CgWidget* w, bool visible) {
    if (visible) w->flags |=  CG_FLAG_VISIBLE;
    else         w->flags &= ~CG_FLAG_VISIBLE;
    cg_widget_mark_dirty(w->parent ? w->parent : w);
}

void cg_widget_set_enabled(CgWidget* w, bool enabled) {
    if (enabled) w->flags |=  CG_FLAG_ENABLED;
    else         w->flags &= ~CG_FLAG_ENABLED;
}

4.3 The Widget Iterator

We need to iterate over widgets frequently (layout, painting, hit testing). A depth-first iterator:

// Iterate depth-first over the subtree rooted at 'root'
// Returns widgets in paint order (parents before children)
typedef struct {
    CgWidget* stack[256];
    int       top;
} CgIter;

void cg_iter_init(CgIter* it, CgWidget* root) {
    it->top = 0;
    if (root) it->stack[it->top++] = root;
}

CgWidget* cg_iter_next(CgIter* it) {
    if (it->top == 0) return NULL;
    CgWidget* w = it->stack[--it->top];

    // Push children in reverse order (so first child is processed first)
    CgWidget* child = w->first_child;
    // Count children, then push in reverse
    int count = 0;
    CgWidget* c = child;
    while (c) { count++; c = c->next_sibling; }

    // Push in reverse (last first, so first child is top of stack)
    CgWidget* children[256];
    c = child;
    for (int i = 0; i < count && i < 256; i++) {
        children[i] = c;
        c = c->next_sibling;
    }
    for (int i = count - 1; i >= 0; i--) {
        if (it->top < 256)
            it->stack[it->top++] = children[i];
    }

    return w;
}

Chapter 5: The Box Model and Layout Engine

The layout engine computes where each widget sits on screen. We implement a simplified CSS box model with flexbox-inspired row/column layout for containers.

5.1 Box Model Review

Each widget has:

  • margin: Space outside the border (between siblings)
  • border: Visual border (drawn, not laid out separately)
  • padding: Space between border and content
  • content: The widget's actual content area
┌─────────────────── bounds ───────────────────────────────┐
│ MARGIN                                                   │
│  ┌──────────────── border ─────────────────────────────┐ │
│  │ PADDING                                             │ │
│  │  ┌──────────── content ─────────────────────────┐   │ │
│  │  │                                              │   │ │
│  │  └──────────────────────────────────────────────┘   │ │
│  │                                                     │ │
│  └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────-┘

5.2 Layout Pass Structure

Layout happens in two passes:

Measure Pass: Bottom-up. Each widget computes its intrinsic content_size given available space. Leaves measure themselves based on content (text width, image dimensions). Containers measure their children first, then compute their own size.

Arrange Pass: Top-down. The root is given its final bounds (the window size). Each container arranges its children within its content area, setting each child's bounds and bounds_abs.

// src/cg_layout.c

#include "cg_internal.h"

// ── Content area from bounds ──────────────────────────────────────────────

SDL_Rect cg_content_rect(CgWidget* w) {
    SDL_Rect r = w->bounds_abs;
    r.x += w->padding.left;
    r.y += w->padding.top;
    r.w -= w->padding.left + w->padding.right;
    r.h -= w->padding.top  + w->padding.bottom;
    if (r.w < 0) r.w = 0;
    if (r.h < 0) r.h = 0;
    return r;
}

// ── Measure pass ─────────────────────────────────────────────────────────

void cg_layout_measure(CgWidget* w, int avail_w, int avail_h) {
    if (!(w->flags & CG_FLAG_VISIBLE)) {
        w->content_size = CG_SIZE_ZERO;
        return;
    }

    // Subtract our own padding from available space for children
    int inner_w = avail_w - w->padding.left - w->padding.right;
    int inner_h = avail_h - w->padding.top  - w->padding.bottom;
    if (inner_w < 0) inner_w = 0;
    if (inner_h < 0) inner_h = 0;

    // Delegate to type-specific measure
    if (w->vtable && w->vtable->measure)
        w->vtable->measure(w, inner_w, inner_h);

    // Apply desired_w / desired_h overrides
    if (w->desired_w > 0) w->content_size.w = w->desired_w
                                              - w->padding.left
                                              - w->padding.right;
    if (w->desired_h > 0) w->content_size.h = w->desired_h
                                              - w->padding.top
                                              - w->padding.bottom;

    // Clamp to min/max
    CgSize cs = w->content_size;
    int total_w = cs.w + w->padding.left + w->padding.right;
    int total_h = cs.h + w->padding.top  + w->padding.bottom;
    total_w = SDL_clamp(total_w, w->min_w, w->max_w);
    total_h = SDL_clamp(total_h, w->min_h, w->max_h);
    w->content_size.w = total_w - w->padding.left - w->padding.right;
    w->content_size.h = total_h - w->padding.top  - w->padding.bottom;
}

// ── Arrange pass ─────────────────────────────────────────────────────────

void cg_layout_arrange(CgWidget* w, SDL_Rect bounds_abs) {
    w->bounds_abs = bounds_abs;
    w->bounds     = bounds_abs; // For root; children get relative coords

    if (!(w->flags & CG_FLAG_VISIBLE)) return;
    if (w->vtable && w->vtable->arrange)
        w->vtable->arrange(w, bounds_abs);
}

// ── Top-level layout ──────────────────────────────────────────────────────

void cg_layout_root(CgWidget* root) {
    int w = cg->window_w, h = cg->window_h;
    cg_layout_measure(root, w, h);

    SDL_Rect full = {0, 0, w, h};
    cg_layout_arrange(root, full);

    root->flags &= ~CG_FLAG_DIRTY;
}

5.3 Flexbox-Inspired Container Layout

Our containers support row and column layout with alignment and wrapping:

// include/cg.h

typedef enum {
    CG_DIRECTION_ROW,     // Children laid out left-to-right
    CG_DIRECTION_COLUMN,  // Children laid out top-to-bottom
} CgDirection;

typedef enum {
    CG_ALIGN_START,       // Pack to start (left/top)
    CG_ALIGN_CENTER,      // Centre in cross axis
    CG_ALIGN_END,         // Pack to end (right/bottom)
    CG_ALIGN_STRETCH,     // Stretch to fill cross axis
} CgAlign;

typedef enum {
    CG_JUSTIFY_START,
    CG_JUSTIFY_CENTER,
    CG_JUSTIFY_END,
    CG_JUSTIFY_SPACE_BETWEEN,
    CG_JUSTIFY_SPACE_AROUND,
    CG_JUSTIFY_SPACE_EVENLY,
} CgJustify;

// Extended widget structure for panels
typedef struct CgPanel {
    CgWidget        base;
    CgDirection     direction;
    CgAlign         align_items;   // Cross axis
    CgJustify       justify;       // Main axis
    int             gap;           // Gap between children (pixels)
    bool            wrap;          // Wrap to next line when full
    CgColor         bg_color;
    int             border_radius;
    CgColor         border_color;
    int             border_width;
    bool            has_shadow;
} CgPanel;
// Panel measure: sum children along main axis
static void panel_measure(CgWidget* w, int avail_w, int avail_h) {
    CgPanel* p = (CgPanel*)w;
    bool is_row = (p->direction == CG_DIRECTION_ROW);

    int main_total = 0;   // Total size along main axis
    int cross_max  = 0;   // Max size along cross axis
    int gap_total  = 0;
    int visible_count = 0;

    CgWidget* child = w->first_child;
    while (child) {
        if (!(child->flags & CG_FLAG_VISIBLE)) {
            child = child->next_sibling;
            continue;
        }

        // Available space for this child
        int child_avail_w = is_row ? avail_w : avail_w;
        int child_avail_h = is_row ? avail_h : avail_h;

        cg_layout_measure(child, child_avail_w, child_avail_h);

        int child_outer_w = child->content_size.w
                          + child->padding.left + child->padding.right
                          + child->margin.left  + child->margin.right;
        int child_outer_h = child->content_size.h
                          + child->padding.top  + child->padding.bottom
                          + child->margin.top   + child->margin.bottom;

        if (is_row) {
            main_total += child_outer_w;
            if (child_outer_h > cross_max) cross_max = child_outer_h;
        } else {
            main_total += child_outer_h;
            if (child_outer_w > cross_max) cross_max = child_outer_w;
        }
        visible_count++;
        child = child->next_sibling;
    }

    if (visible_count > 1) gap_total = p->gap * (visible_count - 1);

    if (is_row) {
        w->content_size.w = main_total + gap_total;
        w->content_size.h = cross_max;
    } else {
        w->content_size.w = cross_max;
        w->content_size.h = main_total + gap_total;
    }
}

// Panel arrange: position children
static void panel_arrange(CgWidget* w, SDL_Rect abs) {
    CgPanel* p = (CgPanel*)w;
    bool is_row = (p->direction == CG_DIRECTION_ROW);

    SDL_Rect content = cg_content_rect(w);

    // Count visible children + expandable children
    int visible_count  = 0;
    int expand_count   = 0;
    int fixed_main     = 0;

    CgWidget* child = w->first_child;
    while (child) {
        if (!(child->flags & CG_FLAG_VISIBLE)) {
            child = child->next_sibling;
            continue;
        }
        visible_count++;
        bool expands = is_row ? (child->flags & CG_FLAG_EXPAND_W)
                              : (child->flags & CG_FLAG_EXPAND_H);
        if (expands) {
            expand_count++;
        } else {
            int outer = is_row
                ? child->content_size.w + child->padding.left + child->padding.right
                  + child->margin.left  + child->margin.right
                : child->content_size.h + child->padding.top  + child->padding.bottom
                  + child->margin.top   + child->margin.bottom;
            fixed_main += outer;
        }
        child = child->next_sibling;
    }

    int gap_total   = (visible_count > 1) ? p->gap * (visible_count - 1) : 0;
    int avail_main  = (is_row ? content.w : content.h) - gap_total;
    int expand_each = 0;
    if (expand_count > 0)
        expand_each = SDL_max(0, (avail_main - fixed_main) / expand_count);

    // Determine start position based on justify
    int main_used = fixed_main + gap_total
                  + expand_each * expand_count;
    int main_avail = is_row ? content.w : content.h;
    int main_free  = SDL_max(0, main_avail - main_used);

    int start_pos, between_gap;
    switch (p->justify) {
    case CG_JUSTIFY_CENTER:
        start_pos   = main_free / 2;
        between_gap = 0;
        break;
    case CG_JUSTIFY_END:
        start_pos   = main_free;
        between_gap = 0;
        break;
    case CG_JUSTIFY_SPACE_BETWEEN:
        start_pos   = 0;
        between_gap = (visible_count > 1) ? main_free / (visible_count - 1) : 0;
        break;
    case CG_JUSTIFY_SPACE_AROUND:
        between_gap = (visible_count > 0) ? main_free / visible_count : 0;
        start_pos   = between_gap / 2;
        break;
    case CG_JUSTIFY_SPACE_EVENLY:
        between_gap = (visible_count > 0) ? main_free / (visible_count + 1) : 0;
        start_pos   = between_gap;
        break;
    default:  // START
        start_pos   = 0;
        between_gap = 0;
        break;
    }

    // Position each child
    int cursor = (is_row ? content.x : content.y) + start_pos;

    child = w->first_child;
    while (child) {
        if (!(child->flags & CG_FLAG_VISIBLE)) {
            child = child->next_sibling;
            continue;
        }

        bool expands = is_row ? (child->flags & CG_FLAG_EXPAND_W)
                              : (child->flags & CG_FLAG_EXPAND_H);

        int outer_main = is_row
            ? child->content_size.w + child->padding.left  + child->padding.right
              + child->margin.left  + child->margin.right
            : child->content_size.h + child->padding.top   + child->padding.bottom
              + child->margin.top   + child->margin.bottom;

        if (expands) outer_main = expand_each
            + (is_row ? child->padding.left + child->padding.right
                       + child->margin.left + child->margin.right
                     : child->padding.top  + child->padding.bottom
                       + child->margin.top + child->margin.bottom);

        int cross_avail = is_row ? content.h : content.w;
        int outer_cross = is_row
            ? child->content_size.h + child->padding.top   + child->padding.bottom
              + child->margin.top   + child->margin.bottom
            : child->content_size.w + child->padding.left  + child->padding.right
              + child->margin.left  + child->margin.right;

        // Cross axis alignment
        int cross_pos;
        bool stretch = (p->align_items == CG_ALIGN_STRETCH);
        switch (p->align_items) {
        case CG_ALIGN_CENTER:
            cross_pos = (is_row ? content.y : content.x)
                       + (cross_avail - outer_cross) / 2;
            break;
        case CG_ALIGN_END:
            cross_pos = (is_row ? content.y : content.x)
                       + cross_avail - outer_cross;
            break;
        default:
            cross_pos = is_row ? content.y : content.x;
            break;
        }

        SDL_Rect child_bounds_abs;
        if (is_row) {
            child_bounds_abs.x = cursor + child->margin.left;
            child_bounds_abs.y = cross_pos + child->margin.top;
            child_bounds_abs.w = outer_main - child->margin.left - child->margin.right;
            child_bounds_abs.h = stretch ? cross_avail - child->margin.top
                                                        - child->margin.bottom
                                        : outer_cross - child->margin.top
                                                      - child->margin.bottom;
        } else {
            child_bounds_abs.x = cross_pos + child->margin.left;
            child_bounds_abs.y = cursor    + child->margin.top;
            child_bounds_abs.w = stretch ? cross_avail - child->margin.left
                                                       - child->margin.right
                                         : outer_cross - child->margin.left
                                                       - child->margin.right;
            child_bounds_abs.h = outer_main - child->margin.top - child->margin.bottom;
        }

        // Re-measure child with its final dimensions if stretching
        if (stretch) {
            if (is_row) child->content_size.h = child_bounds_abs.h
                                                - child->padding.top
                                                - child->padding.bottom;
            else        child->content_size.w = child_bounds_abs.w
                                                - child->padding.left
                                                - child->padding.right;
        }

        cg_layout_arrange(child, child_bounds_abs);

        cursor += outer_main + p->gap + between_gap;
        child = child->next_sibling;
    }
}

Chapter 6: Input Handling and the Event System

The event system routes SDL2 input events to the correct widgets. It implements:

  • Hit testing: which widget is under the cursor
  • Event bubbling: events propagate from leaf to root
  • Capture: a widget can capture all mouse events (for drag operations)

6.1 CgEvent — Our Event Type

// include/cg.h

typedef enum {
    // Mouse
    CG_EVENT_MOUSE_MOVE,
    CG_EVENT_MOUSE_DOWN,
    CG_EVENT_MOUSE_UP,
    CG_EVENT_MOUSE_CLICK,
    CG_EVENT_MOUSE_DOUBLE_CLICK,
    CG_EVENT_MOUSE_ENTER,   // Mouse entered widget bounds
    CG_EVENT_MOUSE_LEAVE,   // Mouse left widget bounds
    CG_EVENT_MOUSE_WHEEL,

    // Keyboard
    CG_EVENT_KEY_DOWN,
    CG_EVENT_KEY_UP,
    CG_EVENT_TEXT_INPUT,

    // Focus
    CG_EVENT_FOCUS_IN,
    CG_EVENT_FOCUS_OUT,

    // Widget-specific (semantic events)
    CG_EVENT_CHANGE,        // Value changed (slider, input, checkbox)
    CG_EVENT_SUBMIT,        // Enter pressed in text input
    CG_EVENT_SELECT,        // Item selected in list/combo

    // Lifecycle
    CG_EVENT_RESIZE,
} CgEventType;

typedef struct CgEvent {
    CgEventType   type;
    CgWidget*     target;     // Widget that originally received the event
    CgWidget*     current;    // Widget currently handling the event (bubbling)
    bool          bubbles;    // Does this event bubble to parent?
    bool          cancelled;  // Stop bubbling (set by handler)

    union {
        struct {
            int   x, y;       // Window-space position
            int   rel_x, rel_y; // Relative to current widget
            int   button;     // SDL_BUTTON_LEFT etc.
            int   clicks;     // 1 or 2
            float wheel_x, wheel_y;
        } mouse;

        struct {
            SDL_Keycode sym;
            SDL_Scancode scancode;
            uint16_t     mod;
            bool         repeat;
        } key;

        struct {
            char text[32];    // UTF-8
        } text;

        struct {
            float value;      // For CG_EVENT_CHANGE on numeric widgets
            char* str_value;  // For CG_EVENT_CHANGE on text widgets
            int   index;      // For CG_EVENT_SELECT
        } change;
    };

    void* userdata;           // Application-supplied context
} CgEvent;

6.2 Hit Testing

// src/cg_event.c

// Find the deepest visible, enabled widget at (x, y)
CgWidget* cg_hit_test(CgWidget* root, int x, int y) {
    if (!root || !(root->flags & CG_FLAG_VISIBLE)) return NULL;

    // Check if point is within this widget's bounds
    SDL_Rect r = root->bounds_abs;
    if (x < r.x || x >= r.x + r.w || y < r.y || y >= r.y + r.h)
        return NULL;

    // Check children in reverse order (last child = topmost visually)
    // Collect children first (can't iterate backwards with single-linked list easily)
    CgWidget* children[256];
    int count = 0;
    CgWidget* child = root->first_child;
    while (child && count < 256) {
        children[count++] = child;
        child = child->next_sibling;
    }

    // Test from last (topmost) to first
    for (int i = count - 1; i >= 0; i--) {
        CgWidget* hit = cg_hit_test(children[i], x, y);
        if (hit) return hit;
    }

    // The root itself was hit (but no child was)
    return root;
}

6.3 Event Dispatch and Bubbling

// Handler node (linked list per widget)
typedef struct CgHandlerNode {
    CgEventType        type;
    CgEventHandler     fn;
    void*              userdata;
    struct CgHandlerNode* next;
} CgHandlerNode;

// Register a handler
void cg_widget_on(CgWidget* w, CgEventType type,
                   CgEventHandler fn, void* userdata) {
    CgHandlerNode* node = calloc(1, sizeof(CgHandlerNode));
    node->type     = type;
    node->fn       = fn;
    node->userdata = userdata;
    node->next     = (CgHandlerNode*)w->handlers;
    w->handlers    = (struct CgHandlerNode*)node;
}

// Convenience macros
#define cg_on_click(w, fn, ud)  cg_widget_on(w, CG_EVENT_MOUSE_CLICK, fn, ud)
#define cg_on_change(w, fn, ud) cg_widget_on(w, CG_EVENT_CHANGE, fn, ud)
#define cg_on_key(w, fn, ud)    cg_widget_on(w, CG_EVENT_KEY_DOWN, fn, ud)

// Dispatch event to a widget (runs all matching handlers)
static bool dispatch_to_widget(CgWidget* w, CgEvent* ev) {
    ev->current = w;
    CgHandlerNode* node = (CgHandlerNode*)w->handlers;
    while (node) {
        if (node->type == ev->type) {
            if (node->fn(w, ev, node->userdata)) return true;
        }
        node = node->next;
    }
    return false;
}

// Fire an event, bubbling from target up to root
bool cg_fire_event(CgWidget* target, CgEvent* ev) {
    ev->target  = target;
    ev->bubbles = true;  // Default: events bubble

    // Dispatch to target first
    CgWidget* w = target;
    while (w) {
        if (dispatch_to_widget(w, ev)) return true;
        if (ev->cancelled) break;
        if (!ev->bubbles)  break;
        w = w->parent;
    }
    return false;
}

6.4 The Main Event Loop

// src/cg_event.c

void cg_process_sdl_event(SDL_Event* sdl_ev) {
    CgEvent ev = {0};

    switch (sdl_ev->type) {

    // ── Mouse motion ────────────────────────────────────────────────────────
    case SDL_MOUSEMOTION: {
        int mx = sdl_ev->motion.x;
        int my = sdl_ev->motion.y;
        cg->mouse_x = mx;
        cg->mouse_y = my;

        // If a widget has captured the mouse, send all events there
        CgWidget* target = cg->mouse_captured
                         ? cg->mouse_captured
                         : cg_hit_test(cg->root, mx, my);

        // Handle ENTER / LEAVE transitions
        if (target != cg->hovered) {
            if (cg->hovered) {
                cg->hovered->flags &= ~CG_FLAG_HOVERED;
                CgEvent leave = {.type = CG_EVENT_MOUSE_LEAVE};
                cg_fire_event(cg->hovered, &leave);
            }
            cg->hovered = target;
            if (target) {
                target->flags |= CG_FLAG_HOVERED;
                CgEvent enter = {.type = CG_EVENT_MOUSE_ENTER};
                cg_fire_event(target, &enter);
            }
        }

        if (target) {
            ev.type         = CG_EVENT_MOUSE_MOVE;
            ev.mouse.x      = mx;
            ev.mouse.y      = my;
            ev.mouse.rel_x  = mx - target->bounds_abs.x;
            ev.mouse.rel_y  = my - target->bounds_abs.y;
            cg_fire_event(target, &ev);
        }
        break;
    }

    // ── Mouse button ────────────────────────────────────────────────────────
    case SDL_MOUSEBUTTONDOWN:
    case SDL_MOUSEBUTTONUP: {
        int mx = sdl_ev->button.x;
        int my = sdl_ev->button.y;
        bool is_down = (sdl_ev->type == SDL_MOUSEBUTTONDOWN);
        int  button  = sdl_ev->button.button;

        CgWidget* target = cg->mouse_captured
                         ? cg->mouse_captured
                         : cg_hit_test(cg->root, mx, my);

        if (is_down) {
            // Focus on click
            if (target && (target->flags & CG_FLAG_FOCUSABLE)) {
                cg_set_focus(target);
            }
            // Capture mouse on button 1 press
            if (button == SDL_BUTTON_LEFT) {
                cg->mouse_captured = target;
                if (target) target->flags |= CG_FLAG_PRESSED;
            }
        } else {
            if (button == SDL_BUTTON_LEFT) {
                if (cg->mouse_captured)
                    cg->mouse_captured->flags &= ~CG_FLAG_PRESSED;
                cg->mouse_captured = NULL;
            }
        }

        if (target) {
            ev.type        = is_down ? CG_EVENT_MOUSE_DOWN : CG_EVENT_MOUSE_UP;
            ev.mouse.x     = mx;
            ev.mouse.y     = my;
            ev.mouse.button = button;
            ev.mouse.clicks = sdl_ev->button.clicks;
            cg_fire_event(target, &ev);

            // Synthesize click on button-up if still over target
            if (!is_down && (target->flags & CG_FLAG_ENABLED)) {
                bool still_over = (cg->hovered == target);
                if (still_over) {
                    ev.type = (sdl_ev->button.clicks == 2)
                            ? CG_EVENT_MOUSE_DOUBLE_CLICK
                            : CG_EVENT_MOUSE_CLICK;
                    cg_fire_event(target, &ev);
                }
            }
        }
        break;
    }

    // ── Mouse wheel ─────────────────────────────────────────────────────────
    case SDL_MOUSEWHEEL: {
        CgWidget* target = cg->hovered;
        if (target) {
            float dx = (float)sdl_ev->wheel.x;
            float dy = (float)sdl_ev->wheel.y;
            if (sdl_ev->wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
                dx = -dx; dy = -dy;
            }
            ev.type         = CG_EVENT_MOUSE_WHEEL;
            ev.mouse.wheel_x = dx;
            ev.mouse.wheel_y = dy;
            cg_fire_event(target, &ev);
        }
        break;
    }

    // ── Keyboard ────────────────────────────────────────────────────────────
    case SDL_KEYDOWN:
    case SDL_KEYUP: {
        CgWidget* target = cg->focused ? cg->focused : cg->root;
        ev.type         = (sdl_ev->type == SDL_KEYDOWN)
                        ? CG_EVENT_KEY_DOWN : CG_EVENT_KEY_UP;
        ev.key.sym      = sdl_ev->key.keysym.sym;
        ev.key.scancode = sdl_ev->key.keysym.scancode;
        ev.key.mod      = sdl_ev->key.keysym.mod;
        ev.key.repeat   = sdl_ev->key.repeat != 0;

        if (!cg_fire_event(target, &ev)) {
            // Global key bindings
            if (sdl_ev->type == SDL_KEYDOWN) {
                if (sdl_ev->key.keysym.sym == SDLK_TAB) {
                    bool shift = (sdl_ev->key.keysym.mod & KMOD_SHIFT) != 0;
                    cg_focus_next(shift);
                }
            }
        }
        break;
    }

    // ── Text input ──────────────────────────────────────────────────────────
    case SDL_TEXTINPUT: {
        CgWidget* target = cg->focused;
        if (target) {
            ev.type = CG_EVENT_TEXT_INPUT;
            strncpy(ev.text.text, sdl_ev->text.text, 31);
            cg_fire_event(target, &ev);
        }
        break;
    }

    // ── Window resize ───────────────────────────────────────────────────────
    case SDL_WINDOWEVENT:
        if (sdl_ev->window.event == SDL_WINDOWEVENT_RESIZED ||
            sdl_ev->window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
            SDL_GetWindowSize(cg->window, &cg->window_w, &cg->window_h);
            SDL_RenderSetLogicalSize(cg->renderer, cg->window_w, cg->window_h);
            cg_widget_mark_dirty(cg->root);
        }
        break;
    }
}

Chapter 7: Focus Management and Keyboard Navigation

A GUI library that can't be navigated with the keyboard is inaccessible. Focus management is more complex than it first appears.

7.1 Focus Rules

  • Only one widget has focus at a time
  • Only widgets with CG_FLAG_FOCUSABLE can receive focus
  • Tab advances to the next focusable widget (by tab index or tree order)
  • Shift+Tab goes backwards
  • Escape usually releases focus
  • Clicking a widget gives it focus if it is focusable

7.2 Implementation

// src/cg_focus.c

// Collect all focusable widgets in tab order
static void collect_focusable(CgWidget* w, CgWidget** out, int* count, int max) {
    if (!w || !(w->flags & CG_FLAG_VISIBLE) || !(*count < max)) return;
    if (w->flags & CG_FLAG_FOCUSABLE) {
        out[(*count)++] = w;
    }
    CgWidget* child = w->first_child;
    while (child) {
        collect_focusable(child, out, count, max);
        child = child->next_sibling;
    }
}

static int compare_tab_index(const void* a, const void* b) {
    const CgWidget* wa = *(const CgWidget**)a;
    const CgWidget* wb = *(const CgWidget**)b;
    if (wa->tab_index != wb->tab_index) return wa->tab_index - wb->tab_index;
    return (int)(wa->id - wb->id);  // Secondary: creation order
}

void cg_focus_next(bool reverse) {
    CgWidget* focusable[512];
    int count = 0;
    collect_focusable(cg->root, focusable, &count, 512);
    if (count == 0) return;

    // Sort by tab_index
    qsort(focusable, count, sizeof(CgWidget*), compare_tab_index);

    // Find current focused position
    int current_idx = -1;
    for (int i = 0; i < count; i++) {
        if (focusable[i] == cg->focused) { current_idx = i; break; }
    }

    int next_idx;
    if (reverse) {
        next_idx = (current_idx <= 0) ? count - 1 : current_idx - 1;
    } else {
        next_idx = (current_idx < 0 || current_idx >= count - 1)
                 ? 0 : current_idx + 1;
    }

    cg_set_focus(focusable[next_idx]);
}

void cg_set_focus(CgWidget* w) {
    if (!w || !(w->flags & CG_FLAG_FOCUSABLE)) return;
    if (cg->focused == w) return;

    // Blur old widget
    if (cg->focused) {
        cg->focused->flags &= ~CG_FLAG_FOCUSED;
        CgEvent blur = {.type = CG_EVENT_FOCUS_OUT};
        cg_fire_event(cg->focused, &blur);

        // Stop text input if old widget used it
        SDL_StopTextInput();
    }

    cg->focused = w;
    w->flags |= CG_FLAG_FOCUSED;

    CgEvent focus = {.type = CG_EVENT_FOCUS_IN};
    cg_fire_event(w, &focus);
}

void cg_clear_focus(void) {
    if (cg->focused) {
        cg->focused->flags &= ~CG_FLAG_FOCUSED;
        CgEvent blur = {.type = CG_EVENT_FOCUS_OUT};
        cg_fire_event(cg->focused, &blur);
        SDL_StopTextInput();
        cg->focused = NULL;
    }
}

Chapter 8: Font Rendering with SDL_ttf

Text rendering is the hardest part of any GUI library. We need to handle: font loading, size variants, Unicode, line wrapping, alignment, and caret positioning.

8.1 Font Registry

// src/cg_font.c

#include "cg_internal.h"
#include <SDL2/SDL_ttf.h>

typedef struct {
    char*     path;
    int       size;
    int       style;     // TTF_STYLE_*
    TTF_Font* font;
    uint32_t  hash;
} FontEntry;

#define FONT_CACHE_MAX 64
static FontEntry font_cache[FONT_CACHE_MAX];
static int       font_cache_count = 0;

void cg_fonts_init(void) {
    if (TTF_Init() == -1) {
        fprintf(stderr, "[cg] TTF_Init failed: %s\n", TTF_GetError());
        abort();
    }
    TTF_ByteSwappedUNICODE(SDL_FALSE);
}

// Get or load a font
TTF_Font* cg_font_get(const char* path, int size, int style) {
    // Hash for lookup
    uint32_t h = 5381;
    for (const char* p = path; *p; p++) h = h * 33 ^ (uint8_t)*p;
    h = h * 33 ^ (uint32_t)size;
    h = h * 33 ^ (uint32_t)style;

    // Search cache
    for (int i = 0; i < font_cache_count; i++) {
        if (font_cache[i].hash == h &&
            font_cache[i].size  == size &&
            font_cache[i].style == style &&
            strcmp(font_cache[i].path, path) == 0) {
            return font_cache[i].font;
        }
    }

    // Load
    if (font_cache_count >= FONT_CACHE_MAX) {
        fprintf(stderr, "[cg] Font cache full!\n");
        return NULL;
    }

    TTF_Font* font = TTF_OpenFont(path, size);
    if (!font) {
        fprintf(stderr, "[cg] Failed to load font '%s': %s\n", path, TTF_GetError());
        return NULL;
    }
    TTF_SetFontStyle(font, style);
    TTF_SetFontHinting(font, TTF_HINTING_LIGHT);

    FontEntry* entry = &font_cache[font_cache_count++];
    entry->path  = strdup(path);
    entry->size  = size;
    entry->style = style;
    entry->font  = font;
    entry->hash  = h;

    return font;
}

void cg_fonts_shutdown(void) {
    for (int i = 0; i < font_cache_count; i++) {
        TTF_CloseFont(font_cache[i].font);
        free(font_cache[i].path);
    }
    font_cache_count = 0;
    TTF_Quit();
}

8.2 Text Measurement

// Measure the pixel dimensions of a UTF-8 string
CgSize cg_text_measure(TTF_Font* font, const char* text) {
    if (!font || !text || !*text) return CG_SIZE_ZERO;
    int w, h;
    TTF_SizeUTF8(font, text, &w, &h);
    return (CgSize){w, h};
}

// Measure line height for a font
int cg_font_line_height(TTF_Font* font) {
    return TTF_FontLineSkip(font);
}

int cg_font_ascent(TTF_Font* font) { return TTF_FontAscent(font);  }
int cg_font_descent(TTF_Font* font) { return TTF_FontDescent(font); }

8.3 The Text Cache

Re-rendering text every frame is very slow. We cache rendered text as textures, keyed by (text, font, color):

typedef struct TextCacheEntry {
    uint32_t      hash;
    char*         text;
    TTF_Font*     font;
    CgColor       color;
    SDL_Texture*  texture;
    int           w, h;
    uint32_t      last_frame;  // For LRU eviction
    struct TextCacheEntry* next;  // Hash-table chaining
} TextCacheEntry;

#define TEXT_CACHE_BUCKETS 256
#define TEXT_CACHE_MAX_ENTRIES 512

static TextCacheEntry* text_cache_buckets[TEXT_CACHE_BUCKETS];
static int             text_cache_count;
static uint32_t        current_frame;

static uint32_t text_hash(const char* text, TTF_Font* font, CgColor c) {
    uint32_t h = 5381;
    for (const char* p = text; *p; p++) h = h * 33 ^ (uint8_t)*p;
    h = h * 33 ^ (uint32_t)(uintptr_t)font;
    h = h * 33 ^ c.r ^ (c.g << 8) ^ (c.b << 16) ^ (c.a << 24);
    return h;
}

SDL_Texture* cg_text_texture(TTF_Font* font, const char* text, CgColor color,
                               int* out_w, int* out_h) {
    if (!text || !*text) { *out_w = 0; *out_h = 0; return NULL; }

    uint32_t hash = text_hash(text, font, color);
    uint32_t bucket = hash % TEXT_CACHE_BUCKETS;

    // Lookup
    TextCacheEntry* e = text_cache_buckets[bucket];
    while (e) {
        if (e->hash == hash && e->font == font &&
            e->color.r == color.r && e->color.g == color.g &&
            e->color.b == color.b && e->color.a == color.a &&
            strcmp(e->text, text) == 0) {
            e->last_frame = current_frame;
            *out_w = e->w; *out_h = e->h;
            return e->texture;
        }
        e = e->next;
    }

    // Render new texture
    SDL_Color sdl_color = cg_to_sdl_color(color);
    SDL_Surface* surf = TTF_RenderUTF8_Blended(font, text, sdl_color);
    if (!surf) { *out_w = 0; *out_h = 0; return NULL; }

    SDL_Texture* tex = SDL_CreateTextureFromSurface(cg->renderer, surf);
    SDL_FreeSurface(surf);
    if (!tex) { *out_w = 0; *out_h = 0; return NULL; }

    // Insert into cache (with eviction if full)
    if (text_cache_count >= TEXT_CACHE_MAX_ENTRIES) {
        cg_text_cache_evict_lru();
    }

    TextCacheEntry* entry = calloc(1, sizeof(TextCacheEntry));
    entry->hash       = hash;
    entry->text       = strdup(text);
    entry->font       = font;
    entry->color      = color;
    entry->texture    = tex;
    SDL_QueryTexture(tex, NULL, NULL, &entry->w, &entry->h);
    entry->last_frame = current_frame;
    entry->next       = text_cache_buckets[bucket];
    text_cache_buckets[bucket] = entry;
    text_cache_count++;

    *out_w = entry->w; *out_h = entry->h;
    return tex;
}

8.4 Word Wrapping

typedef struct {
    char**  lines;    // Array of heap-allocated line strings
    int*    widths;   // Width in pixels of each line
    int     count;    // Number of lines
    int     total_h;  // Total height
    int     max_w;    // Width of widest line
} CgTextLayout;

// Wrap text to fit within max_width pixels
CgTextLayout cg_text_wrap(TTF_Font* font, const char* text, int max_width) {
    CgTextLayout layout = {0};
    if (!text || !*text) return layout;

    // Split into words first
    char* copy  = strdup(text);
    char* lines_buf[512];
    int   widths[512];
    int   line_count = 0;

    char* line_start = copy;
    char* p          = copy;
    int   line_w     = 0;
    int   space_w;
    TTF_SizeUTF8(font, " ", &space_w, NULL);

    while (line_count < 511) {
        // Find end of word
        char* word_start = p;
        while (*p && *p != ' ' && *p != '\n') p++;
        char saved = *p;
        *p = '\0';

        int word_w;
        TTF_SizeUTF8(font, word_start, &word_w, NULL);
        *p = saved;

        bool is_newline = (saved == '\n');
        bool fits = (line_w == 0) ? true
                                  : (line_w + space_w + word_w <= max_width);

        if (!fits || is_newline) {
            // Emit current line
            int len = (int)(word_start - line_start);
            if (len > 0 && line_start[len-1] == ' ') len--;  // Trim trailing space
            char* line = malloc(len + 1);
            memcpy(line, line_start, len);
            line[len] = '\0';
            lines_buf[line_count]  = line;
            widths[line_count]     = line_w;
            if (line_w > layout.max_w) layout.max_w = line_w;
            line_count++;

            line_start = word_start;
            line_w     = word_w;
        } else {
            line_w = (line_w == 0) ? word_w : line_w + space_w + word_w;
        }

        if (!saved) break;
        if (is_newline) line_start = ++p;
        else            p++;  // Skip space
    }

    // Emit last line
    if (*line_start) {
        int len = (int)(p - line_start);
        char* line = malloc(len + 1);
        memcpy(line, line_start, len);
        line[len] = '\0';
        TTF_SizeUTF8(font, line, &line_w, NULL);
        lines_buf[line_count]  = line;
        widths[line_count]     = line_w;
        if (line_w > layout.max_w) layout.max_w = line_w;
        line_count++;
    }

    free(copy);

    layout.count  = line_count;
    layout.lines  = malloc(sizeof(char*) * line_count);
    layout.widths = malloc(sizeof(int)   * line_count);
    memcpy(layout.lines,  lines_buf, sizeof(char*) * line_count);
    memcpy(layout.widths, widths,    sizeof(int)   * line_count);
    layout.total_h = line_count * TTF_FontLineSkip(font);

    return layout;
}

void cg_text_layout_free(CgTextLayout* layout) {
    for (int i = 0; i < layout->count; i++) free(layout->lines[i]);
    free(layout->lines);
    free(layout->widths);
    memset(layout, 0, sizeof(*layout));
}

// Draw a wrapped text layout
void cg_draw_text_layout(CgTextLayout* layout, TTF_Font* font,
                           CgColor color, SDL_Rect bounds,
                           int halign, int valign) {
    // halign: -1=left, 0=center, 1=right
    // valign: -1=top,  0=center, 1=bottom

    int total_h = layout->total_h;
    int lh = TTF_FontLineSkip(font);

    int start_y;
    if (valign == 0) start_y = bounds.y + (bounds.h - total_h) / 2;
    else if (valign == 1) start_y = bounds.y + bounds.h - total_h;
    else start_y = bounds.y;

    for (int i = 0; i < layout->count; i++) {
        if (!layout->lines[i] || !*layout->lines[i]) {
            start_y += lh;
            continue;
        }

        int tw, th;
        SDL_Texture* tex = cg_text_texture(font, layout->lines[i], color, &tw, &th);
        if (!tex) { start_y += lh; continue; }

        int tx;
        if (halign == 0)      tx = bounds.x + (bounds.w - tw) / 2;
        else if (halign == 1) tx = bounds.x + bounds.w - tw;
        else                  tx = bounds.x;

        SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
        SDL_SetTextureColorMod(tex, 255, 255, 255);
        SDL_SetTextureAlphaMod(tex, color.a);

        SDL_Rect dst = {tx, start_y, tw, th};
        SDL_RenderCopy(cg->renderer, tex, NULL, &dst);
        start_y += lh;
    }
}

Chapter 9: Core Widgets — Labels and Buttons

With the foundation in place, we can build widgets. Let's start with the two most fundamental: Label and Button.

9.1 CgLabel

// src/widgets/cg_label.c

#include "cg_internal.h"

typedef struct CgLabel {
    CgWidget    base;
    char*       text;
    TTF_Font*   font;
    CgColor     color;
    int         halign;   // -1=left, 0=center, 1=right
    int         valign;   // -1=top, 0=center, 1=bottom
    bool        wrap;     // Word-wrap text
    CgTextLayout layout;  // Computed layout (when wrap=true)
} CgLabel;

static void label_measure(CgWidget* w, int avail_w, int avail_h) {
    CgLabel* l = (CgLabel*)w;
    (void)avail_h;

    if (!l->text || !*l->text || !l->font) {
        w->content_size = CG_SIZE_ZERO;
        return;
    }

    if (l->wrap && avail_w > 0) {
        cg_text_layout_free(&l->layout);
        l->layout = cg_text_wrap(l->font, l->text, avail_w);
        w->content_size.w = l->layout.max_w;
        w->content_size.h = l->layout.total_h;
    } else {
        CgSize s = cg_text_measure(l->font, l->text);
        w->content_size = s;
    }
}

static void label_paint(CgWidget* w) {
    CgLabel* l = (CgLabel*)w;
    if (!l->text || !*l->text || !l->font) return;

    SDL_Rect content = cg_content_rect(w);
    if (content.w <= 0 || content.h <= 0) return;

    cg_push_clip(content);

    if (l->wrap && l->layout.count > 0) {
        cg_draw_text_layout(&l->layout, l->font, l->color, content,
                             l->halign, l->valign);
    } else {
        int tw, th;
        SDL_Texture* tex = cg_text_texture(l->font, l->text, l->color, &tw, &th);
        if (tex) {
            int tx, ty;
            if (l->halign == 0)      tx = content.x + (content.w - tw) / 2;
            else if (l->halign == 1) tx = content.x + content.w - tw;
            else                     tx = content.x;

            if (l->valign == 0)      ty = content.y + (content.h - th) / 2;
            else if (l->valign == 1) ty = content.y + content.h - th;
            else                     ty = content.y;

            SDL_Rect dst = {tx, ty, tw, th};
            SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
            SDL_RenderCopy(cg->renderer, tex, NULL, &dst);
        }
    }

    cg_pop_clip();
}

static void label_destroy(CgWidget* w) {
    CgLabel* l = (CgLabel*)w;
    free(l->text);
    cg_text_layout_free(&l->layout);
}

static const CgWidgetVTable label_vtable = {
    .measure = label_measure,
    .arrange = NULL,   // No children, no arrange needed
    .paint   = label_paint,
    .destroy = label_destroy,
};

// Public API
CgWidget* cg_label_new(const char* text) {
    CgLabel* l = (CgLabel*)cg_widget_alloc(sizeof(CgLabel), CG_TYPE_LABEL,
                                             &label_vtable);
    l->text   = strdup(text ? text : "");
    l->font   = cg_theme_default_font();
    l->color  = cg->theme->colors.text_primary;
    l->halign = -1;  // Left by default
    l->valign = 0;   // Centre vertical by default
    l->wrap   = false;
    return (CgWidget*)l;
}

void cg_label_set_text(CgWidget* w, const char* text) {
    CgLabel* l = (CgLabel*)w;
    free(l->text);
    l->text = strdup(text ? text : "");
    cg_text_layout_free(&l->layout);
    cg_widget_mark_dirty(w);
}

void cg_label_set_font(CgWidget* w, TTF_Font* font) {
    ((CgLabel*)w)->font = font;
    cg_widget_mark_dirty(w);
}

void cg_label_set_color(CgWidget* w, CgColor color) {
    ((CgLabel*)w)->color = color;
}

void cg_label_set_align(CgWidget* w, int halign, int valign) {
    CgLabel* l = (CgLabel*)w;
    l->halign = halign;
    l->valign = valign;
}

void cg_label_set_wrap(CgWidget* w, bool wrap) {
    ((CgLabel*)w)->wrap = wrap;
    cg_widget_mark_dirty(w);
}

9.2 CgButton

// src/widgets/cg_button.c

#include "cg_internal.h"

typedef enum {
    CG_BTN_STYLE_FILLED,
    CG_BTN_STYLE_OUTLINED,
    CG_BTN_STYLE_TEXT,
    CG_BTN_STYLE_ICON,
} CgButtonStyle;

typedef struct CgButton {
    CgWidget      base;
    char*         label;
    SDL_Texture*  icon;       // Optional icon texture
    int           icon_w, icon_h;
    CgButtonStyle btn_style;
    TTF_Font*     font;

    // Colors for each state (set from theme on creation)
    CgColor       bg_normal, bg_hover, bg_pressed, bg_disabled;
    CgColor       fg_normal, fg_hover, fg_pressed, fg_disabled;
    CgColor       border_normal, border_hover;
    int           border_radius;
    int           border_width;

    // Animation state (for hover/press transitions)
    float         anim_t;     // 0.0 = normal, 1.0 = hovered/pressed
    CgColor       bg_current;
    CgColor       fg_current;
} CgButton;

static void button_measure(CgWidget* w, int avail_w, int avail_h) {
    CgButton* b = (CgButton*)w;
    (void)avail_w; (void)avail_h;

    int tw = 0, th = 0;
    if (b->label && *b->label && b->font) {
        CgSize ts = cg_text_measure(b->font, b->label);
        tw = ts.w; th = ts.h;
    }

    int iw = 0;
    if (b->icon) {
        iw = b->icon_w + (tw > 0 ? 6 : 0);  // 6px gap between icon and text
    }

    int min_h = SDL_max(th, b->icon_h);
    w->content_size.w = SDL_max(tw + iw, 32);
    w->content_size.h = SDL_max(min_h, 16);
}

static void button_paint(CgWidget* w) {
    CgButton* b = (CgButton*)w;
    SDL_Rect r  = w->bounds_abs;

    bool hovered  = (w->flags & CG_FLAG_HOVERED) != 0;
    bool pressed  = (w->flags & CG_FLAG_PRESSED)  != 0;
    bool disabled = !(w->flags & CG_FLAG_ENABLED);
    bool focused  = (w->flags & CG_FLAG_FOCUSED)  != 0;

    // Interpolate colours based on state
    CgColor bg, fg, border;
    if (disabled) {
        bg     = b->bg_disabled;
        fg     = b->fg_disabled;
        border = b->border_normal;
    } else if (pressed) {
        bg     = b->bg_pressed;
        fg     = b->fg_pressed;
        border = b->border_hover;
    } else if (hovered) {
        bg     = cg_color_lerp(b->bg_normal, b->bg_hover, b->anim_t);
        fg     = cg_color_lerp(b->fg_normal, b->fg_hover, b->anim_t);
        border = b->border_hover;
    } else {
        bg     = cg_color_lerp(b->bg_hover, b->bg_normal, 1.0f - b->anim_t);
        fg     = b->fg_normal;
        border = b->border_normal;
    }

    // Draw background
    switch (b->btn_style) {
    case CG_BTN_STYLE_FILLED:
        cg_draw_rounded_rect(r, b->border_radius, bg);
        break;
    case CG_BTN_STYLE_OUTLINED:
        // Transparent fill with border
        if (hovered || pressed) {
            CgColor fill = {bg.r, bg.g, bg.b, 30};
            cg_draw_rounded_rect(r, b->border_radius, fill);
        }
        cg_draw_rounded_rect_outline(r, b->border_radius,
                                      b->border_width, border);
        break;
    case CG_BTN_STYLE_TEXT:
        if (hovered || pressed) {
            CgColor fill = {bg.r, bg.g, bg.b, 20};
            cg_draw_rounded_rect(r, b->border_radius, fill);
        }
        break;
    default:
        break;
    }

    // Focus ring
    if (focused) {
        SDL_Rect ring = {r.x-2, r.y-2, r.w+4, r.h+4};
        CgColor focus_color = cg->theme->colors.accent;
        focus_color.a = 200;
        cg_draw_rounded_rect_outline(ring, b->border_radius + 2, 2, focus_color);
    }

    // Content area
    SDL_Rect content = cg_content_rect(w);
    cg_push_clip(r);

    // Icon (if any)
    int text_x = content.x;
    if (b->icon) {
        int iy = content.y + (content.h - b->icon_h) / 2;
        SDL_Rect icon_dst = {content.x, iy, b->icon_w, b->icon_h};
        SDL_SetTextureColorMod(b->icon, fg.r, fg.g, fg.b);
        SDL_SetTextureAlphaMod(b->icon, fg.a);
        SDL_RenderCopy(cg->renderer, b->icon, NULL, &icon_dst);
        text_x += b->icon_w + 6;
    }

    // Label
    if (b->label && *b->label && b->font) {
        int tw, th;
        SDL_Texture* tex = cg_text_texture(b->font, b->label, fg, &tw, &th);
        if (tex) {
            SDL_Rect text_bounds = {text_x, content.y,
                                    content.w - (text_x - content.x), content.h};
            int tx = text_bounds.x + (text_bounds.w - tw) / 2;
            int ty = text_bounds.y + (text_bounds.h - th) / 2;
            SDL_Rect dst = {tx, ty, tw, th};
            SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
            SDL_RenderCopy(cg->renderer, tex, NULL, &dst);
        }
    }

    cg_pop_clip();
}

static bool button_on_event(CgWidget* w, CgEvent* ev) {
    CgButton* b = (CgButton*)w;
    (void)b;

    // Animate hover state
    if (ev->type == CG_EVENT_MOUSE_ENTER) {
        cg_tween_float(w, &b->anim_t, 1.0f, 120, CG_EASE_OUT_CUBIC);
        return false;  // Continue bubbling
    }
    if (ev->type == CG_EVENT_MOUSE_LEAVE) {
        cg_tween_float(w, &b->anim_t, 0.0f, 120, CG_EASE_OUT_CUBIC);
        return false;
    }

    // Space/Enter to activate button from keyboard
    if (ev->type == CG_EVENT_KEY_DOWN) {
        if (ev->key.sym == SDLK_SPACE || ev->key.sym == SDLK_RETURN) {
            if (w->flags & CG_FLAG_ENABLED) {
                CgEvent click = {.type = CG_EVENT_MOUSE_CLICK};
                return cg_fire_event(w, &click);
            }
        }
    }

    return false;
}

static void button_destroy(CgWidget* w) {
    CgButton* b = (CgButton*)w;
    free(b->label);
}

static const CgWidgetVTable button_vtable = {
    .measure  = button_measure,
    .arrange  = NULL,
    .paint    = button_paint,
    .on_event = button_on_event,
    .destroy  = button_destroy,
};

CgWidget* cg_button_new(const char* label) {
    CgButton* b = (CgButton*)cg_widget_alloc(sizeof(CgButton), CG_TYPE_BUTTON,
                                               &button_vtable);
    b->label         = strdup(label ? label : "");
    b->font          = cg_theme_default_font();
    b->btn_style     = CG_BTN_STYLE_FILLED;
    b->border_radius = cg->theme->metrics.button_radius;
    b->border_width  = 1;

    // Colors from theme
    CgTheme* t = cg->theme;
    b->bg_normal    = t->colors.button_bg;
    b->bg_hover     = t->colors.button_bg_hover;
    b->bg_pressed   = t->colors.button_bg_pressed;
    b->bg_disabled  = t->colors.surface_disabled;
    b->fg_normal    = t->colors.button_fg;
    b->fg_hover     = t->colors.button_fg;
    b->fg_pressed   = t->colors.button_fg;
    b->fg_disabled  = t->colors.text_disabled;
    b->border_normal = t->colors.button_bg;
    b->border_hover  = t->colors.accent;

    b->base.flags   |= CG_FLAG_FOCUSABLE;
    b->base.padding  = CG_INSETS(8, 16, 8, 16);
    b->base.tab_index = 0;

    return (CgWidget*)b;
}

void cg_button_set_style(CgWidget* w, CgButtonStyle style) {
    CgButton* b = (CgButton*)w;
    b->btn_style = style;

    // Adjust colors for outlined / text styles
    if (style == CG_BTN_STYLE_OUTLINED || style == CG_BTN_STYLE_TEXT) {
        b->fg_normal    = cg->theme->colors.accent;
        b->fg_hover     = cg->theme->colors.accent;
        b->fg_pressed   = cg_color_darken(cg->theme->colors.accent, 0.2f);
        b->border_normal = cg->theme->colors.accent;
    }
}

Chapter 10: Text Input and Editing

The text input widget is one of the most complex to implement correctly. It must handle cursor positioning, selection, copy/paste, IME, and placeholder text.

10.1 CgInput Structure

// src/widgets/cg_input.c

#define INPUT_BUF_CAP 4096

typedef struct CgInput {
    CgWidget base;

    // Text content
    char     buf[INPUT_BUF_CAP];   // UTF-8 text buffer
    int      buf_len;              // Length in bytes (not chars)
    int      cursor;               // Cursor position in bytes
    int      sel_start;            // Selection start (-1 = no selection)
    int      sel_end;              // Selection end

    // Display
    char*    placeholder;
    TTF_Font* font;
    CgColor  text_color;
    CgColor  placeholder_color;
    CgColor  bg_color;
    CgColor  border_color;
    CgColor  border_focus_color;
    CgColor  selection_color;
    int      border_radius;
    int      border_width;

    // Scroll offset (for long text that doesn't fit)
    int      scroll_x;

    // Caret blink
    Uint32   caret_timer;
    bool     caret_visible;

    // Password mode
    bool     password;

    // Max length in characters
    int      max_chars;

    // Editing composition (IME)
    char     composition[64];
    int      comp_cursor;
} CgInput;

10.2 UTF-8 Cursor Navigation

Because UTF-8 is a variable-width encoding, we must step by code points, not bytes:

// Return byte length of the UTF-8 character at ptr
static int utf8_char_len(const char* p) {
    unsigned char c = (unsigned char)*p;
    if      (c < 0x80) return 1;
    else if (c < 0xE0) return 2;
    else if (c < 0xF0) return 3;
    else               return 4;
}

// Move byte offset forward by one character
static int utf8_next(const char* buf, int pos, int len) {
    if (pos >= len) return pos;
    return pos + utf8_char_len(buf + pos);
}

// Move byte offset backward by one character
static int utf8_prev(const char* buf, int pos) {
    if (pos <= 0) return 0;
    pos--;
    while (pos > 0 && (((unsigned char)buf[pos]) & 0xC0) == 0x80) pos--;
    return pos;
}

// Count Unicode characters in buf[0..byte_pos)
static int utf8_count_chars(const char* buf, int byte_pos) {
    int count = 0;
    for (int i = 0; i < byte_pos; ) {
        i += utf8_char_len(buf + i);
        count++;
    }
    return count;
}

// Convert pixel X position to byte offset in text
static int input_pixel_to_cursor(CgInput* inp, int px) {
    if (!inp->buf[0]) return 0;
    TTF_Font* font = inp->font;

    // Walk character by character, find where px falls
    char tmp[INPUT_BUF_CAP];
    int best_pos = 0;
    int best_diff = INT_MAX;

    for (int i = 0; i <= inp->buf_len; ) {
        memcpy(tmp, inp->buf, i);
        tmp[i] = '\0';
        int tw;
        TTF_SizeUTF8(font, tmp, &tw, NULL);
        int diff = abs(tw - px);
        if (diff < best_diff) { best_diff = diff; best_pos = i; }

        if (i == inp->buf_len) break;
        i = utf8_next(inp->buf, i, inp->buf_len);
    }
    return best_pos;
}

10.3 Inserting and Deleting Text

// Insert UTF-8 text at cursor position
static void input_insert(CgInput* inp, const char* text) {
    int text_len = (int)strlen(text);
    if (text_len == 0) return;

    // Delete selection first
    if (inp->sel_start >= 0) {
        int s = SDL_min(inp->sel_start, inp->sel_end);
        int e = SDL_max(inp->sel_start, inp->sel_end);
        memmove(inp->buf + s, inp->buf + e, inp->buf_len - e + 1);
        inp->buf_len -= (e - s);
        inp->cursor   = s;
        inp->sel_start = -1;
    }

    // Check max_chars
    if (inp->max_chars > 0) {
        int chars = utf8_count_chars(inp->buf, inp->buf_len);
        int new_chars = utf8_count_chars(text, text_len);
        if (chars + new_chars > inp->max_chars) return;
    }

    // Check buffer capacity
    if (inp->buf_len + text_len >= INPUT_BUF_CAP - 1) return;

    // Shift existing text to make room
    memmove(inp->buf + inp->cursor + text_len,
            inp->buf + inp->cursor,
            inp->buf_len - inp->cursor + 1);

    memcpy(inp->buf + inp->cursor, text, text_len);
    inp->buf_len += text_len;
    inp->cursor  += text_len;
}

// Delete character at/before cursor (forward=true: delete key, false: backspace)
static void input_delete(CgInput* inp, bool forward) {
    if (inp->sel_start >= 0) {
        // Delete selection
        int s = SDL_min(inp->sel_start, inp->sel_end);
        int e = SDL_max(inp->sel_start, inp->sel_end);
        memmove(inp->buf + s, inp->buf + e, inp->buf_len - e + 1);
        inp->buf_len -= (e - s);
        inp->cursor   = s;
        inp->sel_start = -1;
        return;
    }

    if (forward) {
        if (inp->cursor >= inp->buf_len) return;
        int end = utf8_next(inp->buf, inp->cursor, inp->buf_len);
        memmove(inp->buf + inp->cursor, inp->buf + end,
                inp->buf_len - end + 1);
        inp->buf_len -= (end - inp->cursor);
    } else {
        if (inp->cursor == 0) return;
        int start = utf8_prev(inp->buf, inp->cursor);
        memmove(inp->buf + start, inp->buf + inp->cursor,
                inp->buf_len - inp->cursor + 1);
        inp->buf_len -= (inp->cursor - start);
        inp->cursor   = start;
    }
}

10.4 Drawing the Text Input

static void input_paint(CgWidget* w) {
    CgInput* inp = (CgInput*)w;
    SDL_Rect r   = w->bounds_abs;
    bool focused  = (w->flags & CG_FLAG_FOCUSED) != 0;

    // Background
    cg_draw_rounded_rect(r, inp->border_radius, inp->bg_color);

    // Border (thicker / different color when focused)
    CgColor border = focused ? inp->border_focus_color : inp->border_color;
    int bw = focused ? inp->border_width + 1 : inp->border_width;
    cg_draw_rounded_rect_outline(r, inp->border_radius, bw, border);

    SDL_Rect content = cg_content_rect(w);
    cg_push_clip(r);

    bool has_text = (inp->buf_len > 0);
    const char* display_text = inp->buf;

    // Show placeholder if empty and not focused
    if (!has_text && !focused && inp->placeholder) {
        int tw, th;
        SDL_Texture* tex = cg_text_texture(inp->font, inp->placeholder,
                                            inp->placeholder_color, &tw, &th);
        if (tex) {
            int ty = content.y + (content.h - th) / 2;
            SDL_Rect dst = {content.x - inp->scroll_x, ty, tw, th};
            SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
            SDL_RenderCopy(cg->renderer, tex, NULL, &dst);
        }
        cg_pop_clip();
        return;
    }

    // Compute cursor pixel position
    char before_cursor[INPUT_BUF_CAP];
    memcpy(before_cursor, inp->buf, inp->cursor);
    before_cursor[inp->cursor] = '\0';

    int cursor_px = 0;
    if (inp->cursor > 0) TTF_SizeUTF8(inp->font, before_cursor, &cursor_px, NULL);

    // Auto-scroll to keep cursor visible
    int visible_w = content.w;
    if (cursor_px - inp->scroll_x > visible_w - 4)
        inp->scroll_x = cursor_px - visible_w + 4;
    if (cursor_px - inp->scroll_x < 0)
        inp->scroll_x = SDL_max(0, cursor_px - 4);

    // Draw selection background
    if (focused && inp->sel_start >= 0 && inp->sel_start != inp->sel_end) {
        int s = SDL_min(inp->sel_start, inp->sel_end);
        int e = SDL_max(inp->sel_start, inp->sel_end);

        char s_str[INPUT_BUF_CAP]; memcpy(s_str, inp->buf, s); s_str[s] = '\0';
        char e_str[INPUT_BUF_CAP]; memcpy(e_str, inp->buf, e); e_str[e] = '\0';

        int s_px = 0, e_px = 0;
        if (s > 0) TTF_SizeUTF8(inp->font, s_str, &s_px, NULL);
        if (e > 0) TTF_SizeUTF8(inp->font, e_str, &e_px, NULL);

        SDL_Rect sel_rect = {
            content.x + s_px - inp->scroll_x,
            content.y,
            e_px - s_px,
            content.h
        };
        cg_draw_rect(sel_rect, inp->selection_color);
    }

    // Draw text
    if (has_text) {
        int tw, th;
        SDL_Texture* tex = cg_text_texture(inp->font, display_text,
                                            inp->text_color, &tw, &th);
        if (tex) {
            int ty = content.y + (content.h - th) / 2;
            SDL_Rect dst = {content.x - inp->scroll_x, ty, tw, th};
            SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
            SDL_RenderCopy(cg->renderer, tex, NULL, &dst);
        }
    }

    // Draw caret
    if (focused && inp->caret_visible) {
        int font_h = TTF_FontHeight(inp->font);
        int caret_x = content.x + cursor_px - inp->scroll_x;
        int caret_y = content.y + (content.h - font_h) / 2;
        CgColor caret_color = cg->theme->colors.accent;
        cg_draw_vline(caret_x, caret_y, font_h, caret_color);
    }

    cg_pop_clip();
}

10.5 Handling Input Events

static bool input_on_event(CgWidget* w, CgEvent* ev) {
    CgInput* inp = (CgInput*)w;

    switch (ev->type) {
    case CG_EVENT_FOCUS_IN:
        SDL_StartTextInput();
        inp->caret_visible = true;
        inp->caret_timer   = SDL_GetTicks();
        return false;

    case CG_EVENT_FOCUS_OUT:
        SDL_StopTextInput();
        inp->sel_start = -1;
        return false;

    case CG_EVENT_TEXT_INPUT:
        if (w->flags & CG_FLAG_ENABLED) {
            input_insert(inp, ev->text.text);
            cg_fire_change_event(w, inp->buf);
        }
        return true;

    case CG_EVENT_MOUSE_DOWN:
        if (ev->mouse.button == SDL_BUTTON_LEFT) {
            SDL_Rect content = cg_content_rect(w);
            int rel_x = ev->mouse.x - content.x + inp->scroll_x;
            inp->cursor    = input_pixel_to_cursor(inp, rel_x);
            inp->sel_start = inp->cursor;
            inp->sel_end   = inp->cursor;
        }
        return true;

    case CG_EVENT_MOUSE_MOVE:
        if (cg->mouse_buttons[SDL_BUTTON_LEFT - 1] && inp->sel_start >= 0) {
            SDL_Rect content = cg_content_rect(w);
            int rel_x = ev->mouse.x - content.x + inp->scroll_x;
            inp->cursor  = input_pixel_to_cursor(inp, rel_x);
            inp->sel_end = inp->cursor;
        }
        return true;

    case CG_EVENT_KEY_DOWN: {
        SDL_Keycode k   = ev->key.sym;
        uint16_t    mod = ev->key.mod;
        bool ctrl  = (mod & KMOD_CTRL)  != 0;
        bool shift = (mod & KMOD_SHIFT) != 0;

        switch (k) {
        case SDLK_LEFT:
            if (ctrl) {
                // Jump word left
                while (inp->cursor > 0 && inp->buf[inp->cursor-1] == ' ')
                    inp->cursor = utf8_prev(inp->buf, inp->cursor);
                while (inp->cursor > 0 && inp->buf[inp->cursor-1] != ' ')
                    inp->cursor = utf8_prev(inp->buf, inp->cursor);
            } else {
                inp->cursor = utf8_prev(inp->buf, inp->cursor);
            }
            if (!shift) inp->sel_start = -1;
            else if (inp->sel_start < 0) { inp->sel_start = inp->cursor + 1; }
            inp->sel_end = inp->cursor;
            break;

        case SDLK_RIGHT:
            if (ctrl) {
                while (inp->cursor < inp->buf_len && inp->buf[inp->cursor] == ' ')
                    inp->cursor = utf8_next(inp->buf, inp->cursor, inp->buf_len);
                while (inp->cursor < inp->buf_len && inp->buf[inp->cursor] != ' ')
                    inp->cursor = utf8_next(inp->buf, inp->cursor, inp->buf_len);
            } else {
                inp->cursor = utf8_next(inp->buf, inp->cursor, inp->buf_len);
            }
            if (!shift) inp->sel_start = -1;
            else if (inp->sel_start < 0) { inp->sel_start = inp->cursor - 1; }
            inp->sel_end = inp->cursor;
            break;

        case SDLK_HOME:
            if (shift && inp->sel_start < 0) inp->sel_start = inp->cursor;
            inp->cursor = 0;
            if (!shift) inp->sel_start = -1;
            inp->sel_end = inp->cursor;
            break;

        case SDLK_END:
            if (shift && inp->sel_start < 0) inp->sel_start = inp->cursor;
            inp->cursor = inp->buf_len;
            if (!shift) inp->sel_start = -1;
            inp->sel_end = inp->cursor;
            break;

        case SDLK_BACKSPACE:
            if (w->flags & CG_FLAG_ENABLED) {
                input_delete(inp, false);
                cg_fire_change_event(w, inp->buf);
            }
            break;

        case SDLK_DELETE:
            if (w->flags & CG_FLAG_ENABLED) {
                input_delete(inp, true);
                cg_fire_change_event(w, inp->buf);
            }
            break;

        case SDLK_RETURN:
        case SDLK_KP_ENTER: {
            CgEvent submit = {.type = CG_EVENT_SUBMIT};
            cg_fire_event(w, &submit);
            break;
        }

        case SDLK_a:
            if (ctrl) {  // Select all
                inp->sel_start = 0;
                inp->cursor    = inp->buf_len;
                inp->sel_end   = inp->cursor;
            }
            break;

        case SDLK_c:
        case SDLK_x:
            if (ctrl && inp->sel_start >= 0 && inp->sel_start != inp->sel_end) {
                int s = SDL_min(inp->sel_start, inp->sel_end);
                int e = SDL_max(inp->sel_start, inp->sel_end);
                char clip[INPUT_BUF_CAP];
                memcpy(clip, inp->buf + s, e - s);
                clip[e - s] = '\0';
                SDL_SetClipboardText(clip);

                if (k == SDLK_x && (w->flags & CG_FLAG_ENABLED)) {
                    input_delete(inp, false);
                    cg_fire_change_event(w, inp->buf);
                }
            }
            break;

        case SDLK_v:
            if (ctrl && (w->flags & CG_FLAG_ENABLED)) {
                char* clip = SDL_GetClipboardText();
                if (clip && *clip) {
                    input_insert(inp, clip);
                    cg_fire_change_event(w, inp->buf);
                }
                SDL_free(clip);
            }
            break;
        }
        return true;
    }

    default:
        break;
    }
    return false;
}

// Update caret blink in the main loop
void cg_input_tick(CgInput* inp) {
    Uint32 now = SDL_GetTicks();
    if (now - inp->caret_timer > 530) {  // 530ms blink period
        inp->caret_visible = !inp->caret_visible;
        inp->caret_timer   = now;
    }
}

Chapter 11: Containers — Panels, Rows, Columns

Panels are the backbone of layout. We expose convenient constructors for the most common cases.

11.1 Panel Widget

// include/cg.h — Public panel API

// Create a panel with specified layout direction
CgWidget* cg_panel_new(CgDirection direction);

// Convenience constructors
static inline CgWidget* cg_row(void) {
    return cg_panel_new(CG_DIRECTION_ROW);
}
static inline CgWidget* cg_col(void) {
    return cg_panel_new(CG_DIRECTION_COLUMN);
}

// Add a child widget
void cg_panel_add(CgWidget* panel, CgWidget* child);

// Add a spacer (flexible space filler)
CgWidget* cg_spacer_new(void);

// Set layout properties
void cg_panel_set_gap(CgWidget* panel, int gap);
void cg_panel_set_justify(CgWidget* panel, CgJustify justify);
void cg_panel_set_align(CgWidget* panel, CgAlign align);
void cg_panel_set_padding(CgWidget* panel, CgInsets padding);
void cg_panel_set_bg(CgWidget* panel, CgColor color);
void cg_panel_set_border(CgWidget* panel, int width, int radius, CgColor color);

11.2 The Spacer Widget

A spacer is an invisible widget with CG_FLAG_EXPAND_W or CG_FLAG_EXPAND_H set, consuming all remaining space along the main axis:

CgWidget* cg_spacer_new(void) {
    CgWidget* s = cg_widget_alloc(sizeof(CgWidget), CG_TYPE_BASE, NULL);
    s->flags |= CG_FLAG_EXPAND_W | CG_FLAG_EXPAND_H;
    s->desired_w = CG_SIZE_FILL;
    s->desired_h = CG_SIZE_FILL;
    return s;
}

11.3 Using Panels

// A typical toolbar layout:
//   [Menu] [Save] [Open]   <spacer>   [Search input] [Settings]

CgWidget* toolbar = cg_row();
cg_panel_set_padding(toolbar, CG_INSETS_ALL(8));
cg_panel_set_gap(toolbar, 4);
cg_panel_set_bg(toolbar, cg->theme->colors.surface);

cg_panel_add(toolbar, cg_button_new("File"));
cg_panel_add(toolbar, cg_button_new("Edit"));
cg_panel_add(toolbar, cg_button_new("View"));
cg_panel_add(toolbar, cg_spacer_new());  // Flexible space

CgWidget* search = cg_input_new();
cg_input_set_placeholder(search, "Search...");
search->desired_w = 200;
cg_panel_add(toolbar, search);

cg_panel_add(toolbar, cg_button_new("⚙ Settings"));

Chapter 12: Scrollable Views

A scroll view clips its content to a viewport and allows panning via scrollbar or mouse wheel.

12.1 CgScroll Structure

typedef struct CgScroll {
    CgWidget base;

    CgWidget* content;   // The single child being scrolled
    int       scroll_x;
    int       scroll_y;
    int       content_w; // Measured content size
    int       content_h;

    bool      hbar_visible;
    bool      vbar_visible;
    bool      hbar_always;   // Force horizontal scrollbar
    bool      vbar_always;   // Force vertical scrollbar

    // Scrollbar dragging state
    bool      dragging_vbar;
    bool      dragging_hbar;
    int       drag_start_scroll;
    int       drag_start_mouse;

    CgColor   bar_color;
    CgColor   bar_track_color;
    int       bar_size;    // Scrollbar thickness in pixels
    int       bar_radius;
} CgScroll;

#define SCROLLBAR_SIZE 8  // Thin scrollbar
#define SCROLLBAR_MIN_THUMB 20

12.2 Layout for Scroll Views

static void scroll_measure(CgWidget* w, int avail_w, int avail_h) {
    CgScroll* s = (CgScroll*)w;
    if (!s->content) {
        w->content_size = CG_SIZE_ZERO;
        return;
    }

    // Measure content unconstrained (so it can be as big as needed)
    cg_layout_measure(s->content, 65535, 65535);
    s->content_w = s->content->content_size.w
                 + s->content->padding.left + s->content->padding.right;
    s->content_h = s->content->content_size.h
                 + s->content->padding.top  + s->content->padding.bottom;

    // The scroll view itself fills available space
    w->content_size.w = avail_w;
    w->content_size.h = avail_h;

    // Determine scrollbar visibility
    s->vbar_visible = s->vbar_always || (s->content_h > avail_h);
    s->hbar_visible = s->hbar_always || (s->content_w > avail_w);
}

static void scroll_arrange(CgWidget* w, SDL_Rect abs) {
    CgScroll* s = (CgScroll*)w;
    if (!s->content) return;

    // Content is arranged at its natural size, offset by scroll_x/y
    int viewport_w = abs.w - (s->vbar_visible ? s->bar_size : 0);
    int viewport_h = abs.h - (s->hbar_visible ? s->bar_size : 0);

    // Clamp scroll values
    int max_scroll_x = SDL_max(0, s->content_w - viewport_w);
    int max_scroll_y = SDL_max(0, s->content_h - viewport_h);
    s->scroll_x = SDL_clamp(s->scroll_x, 0, max_scroll_x);
    s->scroll_y = SDL_clamp(s->scroll_y, 0, max_scroll_y);

    SDL_Rect content_abs = {
        abs.x - s->scroll_x,
        abs.y - s->scroll_y,
        SDL_max(s->content_w, viewport_w),
        SDL_max(s->content_h, viewport_h)
    };
    cg_layout_arrange(s->content, content_abs);
}

12.3 Painting Scrollbars

static void scroll_paint(CgWidget* w) {
    CgScroll* s = (CgScroll*)w;
    SDL_Rect r  = w->bounds_abs;

    // Push clip to viewport (exclude scrollbar area)
    int vp_w = r.w - (s->vbar_visible ? s->bar_size : 0);
    int vp_h = r.h - (s->hbar_visible ? s->bar_size : 0);
    SDL_Rect viewport = {r.x, r.y, vp_w, vp_h};
    cg_push_clip(viewport);

    // Paint the content (it's already positioned with offset)
    if (s->content && (s->content->flags & CG_FLAG_VISIBLE)) {
        if (s->content->vtable && s->content->vtable->paint)
            s->content->vtable->paint(s->content);
    }

    cg_pop_clip();

    // Draw vertical scrollbar
    if (s->vbar_visible) {
        SDL_Rect track = {r.x + vp_w, r.y, s->bar_size, vp_h};
        cg_draw_rect(track, s->bar_track_color);

        // Thumb size proportional to viewport vs content
        float ratio = (float)vp_h / (float)s->content_h;
        int thumb_h = SDL_max(SCROLLBAR_MIN_THUMB, (int)(vp_h * ratio));
        int thumb_y = (int)((float)s->scroll_y / s->content_h * vp_h);
        thumb_y = SDL_clamp(thumb_y, 0, vp_h - thumb_h);

        SDL_Rect thumb = {
            r.x + vp_w + 1,
            r.y + thumb_y,
            s->bar_size - 2,
            thumb_h
        };
        cg_draw_rounded_rect(thumb, s->bar_size / 2, s->bar_color);
    }

    // Draw horizontal scrollbar
    if (s->hbar_visible) {
        SDL_Rect track = {r.x, r.y + vp_h, vp_w, s->bar_size};
        cg_draw_rect(track, s->bar_track_color);

        float ratio = (float)vp_w / (float)s->content_w;
        int thumb_w = SDL_max(SCROLLBAR_MIN_THUMB, (int)(vp_w * ratio));
        int thumb_x = (int)((float)s->scroll_x / s->content_w * vp_w);
        thumb_x = SDL_clamp(thumb_x, 0, vp_w - thumb_w);

        SDL_Rect thumb = {
            r.x + thumb_x,
            r.y + vp_h + 1,
            thumb_w,
            s->bar_size - 2
        };
        cg_draw_rounded_rect(thumb, s->bar_size / 2, s->bar_color);
    }
}

static bool scroll_on_event(CgWidget* w, CgEvent* ev) {
    CgScroll* s = (CgScroll*)w;
    SDL_Rect r  = w->bounds_abs;

    if (ev->type == CG_EVENT_MOUSE_WHEEL) {
        // Scroll vertically (hold Shift for horizontal)
        SDL_Keymod mod = SDL_GetModState();
        if (mod & KMOD_SHIFT) {
            s->scroll_x -= (int)(ev->mouse.wheel_y * 40);
        } else {
            s->scroll_y -= (int)(ev->mouse.wheel_y * 40);
        }
        cg_widget_mark_dirty(w);
        return true;
    }

    // Scrollbar drag handling
    if (ev->type == CG_EVENT_MOUSE_DOWN && ev->mouse.button == SDL_BUTTON_LEFT) {
        int vp_w = r.w - (s->vbar_visible ? s->bar_size : 0);
        int vp_h = r.h - (s->hbar_visible ? s->bar_size : 0);

        // Hit test vertical scrollbar
        if (s->vbar_visible) {
            SDL_Rect vbar = {r.x + vp_w, r.y, s->bar_size, vp_h};
            if (ev->mouse.x >= vbar.x && ev->mouse.x < vbar.x + vbar.w &&
                ev->mouse.y >= vbar.y && ev->mouse.y < vbar.y + vbar.h) {
                s->dragging_vbar     = true;
                s->drag_start_scroll = s->scroll_y;
                s->drag_start_mouse  = ev->mouse.y;
                cg->mouse_captured   = w;
                return true;
            }
        }

        // Hit test horizontal scrollbar
        if (s->hbar_visible) {
            SDL_Rect hbar = {r.x, r.y + vp_h, vp_w, s->bar_size};
            if (ev->mouse.x >= hbar.x && ev->mouse.x < hbar.x + hbar.w &&
                ev->mouse.y >= hbar.y && ev->mouse.y < hbar.y + hbar.h) {
                s->dragging_hbar     = true;
                s->drag_start_scroll = s->scroll_x;
                s->drag_start_mouse  = ev->mouse.x;
                cg->mouse_captured   = w;
                return true;
            }
        }
    }

    if (ev->type == CG_EVENT_MOUSE_MOVE) {
        if (s->dragging_vbar) {
            int vp_h = r.h - (s->hbar_visible ? s->bar_size : 0);
            int delta = ev->mouse.y - s->drag_start_mouse;
            float ratio = (float)s->content_h / (float)vp_h;
            s->scroll_y = s->drag_start_scroll + (int)(delta * ratio);
            cg_widget_mark_dirty(w);
            return true;
        }
        if (s->dragging_hbar) {
            int vp_w = r.w - (s->vbar_visible ? s->bar_size : 0);
            int delta = ev->mouse.x - s->drag_start_mouse;
            float ratio = (float)s->content_w / (float)vp_w;
            s->scroll_x = s->drag_start_scroll + (int)(delta * ratio);
            cg_widget_mark_dirty(w);
            return true;
        }
    }

    if (ev->type == CG_EVENT_MOUSE_UP) {
        s->dragging_vbar = false;
        s->dragging_hbar = false;
        if (cg->mouse_captured == w) cg->mouse_captured = NULL;
    }

    return false;
}

Chapter 13: Theming and Styling

A good theming system lets applications switch the visual appearance of the entire UI with a single call.

13.1 The Theme Structure

// include/cg.h

typedef struct CgColorTokens {
    // Backgrounds
    CgColor bg_app;             // Application background
    CgColor bg_surface;         // Card / panel surface
    CgColor bg_surface_raised;  // Elevated surface (dropdown, dialog)
    CgColor bg_overlay;         // Modal overlay (semi-transparent)

    // Text
    CgColor text_primary;
    CgColor text_secondary;
    CgColor text_disabled;
    CgColor text_on_accent;     // Text on accent-color backgrounds

    // Accent / interactive
    CgColor accent;
    CgColor accent_hover;
    CgColor accent_pressed;
    CgColor accent_disabled;

    // Surface-level interactive elements
    CgColor button_bg;
    CgColor button_bg_hover;
    CgColor button_bg_pressed;
    CgColor button_fg;

    // Input fields
    CgColor input_bg;
    CgColor input_border;
    CgColor input_border_focus;
    CgColor input_selection;
    CgColor input_placeholder;

    // Borders / dividers
    CgColor border;
    CgColor divider;

    // Scrollbars
    CgColor scrollbar;
    CgColor scrollbar_track;

    // Status
    CgColor success;
    CgColor warning;
    CgColor error;
    CgColor info;

    // Surface disabled
    CgColor surface_disabled;
} CgColorTokens;

typedef struct CgMetrics {
    int button_radius;      // Border radius for buttons
    int input_radius;       // Border radius for inputs
    int card_radius;        // Border radius for cards/panels
    int tooltip_radius;
    int dialog_radius;

    int border_width;
    int focus_ring_width;

    int spacing_xs;         // Extra-small spacing (4px)
    int spacing_sm;         // Small spacing (8px)
    int spacing_md;         // Medium spacing (16px)
    int spacing_lg;         // Large spacing (24px)
    int spacing_xl;         // Extra-large spacing (32px)

    int font_size_sm;
    int font_size_md;
    int font_size_lg;
    int font_size_xl;
} CgMetrics;

typedef struct CgTheme {
    const char*    name;
    CgColorTokens  colors;
    CgMetrics      metrics;
    char           font_path[256];
    TTF_Font*      font_sm;
    TTF_Font*      font_md;
    TTF_Font*      font_lg;
    TTF_Font*      font_xl;
    TTF_Font*      font_icon;
} CgTheme;

13.2 Built-in Themes

// src/cg_theme.c

static CgTheme theme_dark = {
    .name = "Dark",
    .colors = {
        .bg_app            = CG_RGB( 18,  18,  18),
        .bg_surface        = CG_RGB( 30,  30,  30),
        .bg_surface_raised = CG_RGB( 40,  40,  40),
        .bg_overlay        = CG_RGBA(0, 0, 0, 160),

        .text_primary      = CG_RGB(236, 236, 236),
        .text_secondary    = CG_RGB(170, 170, 170),
        .text_disabled     = CG_RGB(100, 100, 100),
        .text_on_accent    = CG_RGB(255, 255, 255),

        .accent            = CG_RGB( 99, 149, 255),  // Blue
        .accent_hover      = CG_RGB(120, 166, 255),
        .accent_pressed    = CG_RGB( 70, 120, 220),
        .accent_disabled   = CG_RGB( 60,  80, 130),

        .button_bg         = CG_RGB( 99, 149, 255),
        .button_bg_hover   = CG_RGB(120, 166, 255),
        .button_bg_pressed = CG_RGB( 70, 120, 220),
        .button_fg         = CG_RGB(255, 255, 255),

        .input_bg          = CG_RGB( 40,  40,  40),
        .input_border      = CG_RGB( 80,  80,  80),
        .input_border_focus= CG_RGB( 99, 149, 255),
        .input_selection   = CG_RGBA(99, 149, 255, 80),
        .input_placeholder = CG_RGB(100, 100, 100),

        .border            = CG_RGB( 60,  60,  60),
        .divider           = CG_RGB( 50,  50,  50),

        .scrollbar         = CG_RGB( 80,  80,  80),
        .scrollbar_track   = CG_RGB( 30,  30,  30),

        .success           = CG_RGB( 72, 199, 142),
        .warning           = CG_RGB(255, 182,  72),
        .error             = CG_RGB(255,  85,  85),
        .info              = CG_RGB( 99, 149, 255),

        .surface_disabled  = CG_RGB( 50,  50,  50),
    },
    .metrics = {
        .button_radius   = 6,
        .input_radius    = 6,
        .card_radius     = 10,
        .tooltip_radius  = 4,
        .dialog_radius   = 12,
        .border_width    = 1,
        .focus_ring_width= 2,
        .spacing_xs      =  4,
        .spacing_sm      =  8,
        .spacing_md      = 16,
        .spacing_lg      = 24,
        .spacing_xl      = 32,
        .font_size_sm    = 12,
        .font_size_md    = 14,
        .font_size_lg    = 18,
        .font_size_xl    = 24,
    },
};

static CgTheme theme_light = {
    .name = "Light",
    .colors = {
        .bg_app            = CG_RGB(245, 245, 247),
        .bg_surface        = CG_RGB(255, 255, 255),
        .bg_surface_raised = CG_RGB(255, 255, 255),
        .bg_overlay        = CG_RGBA(0, 0, 0, 80),
        .text_primary      = CG_RGB( 20,  20,  20),
        .text_secondary    = CG_RGB( 90,  90,  90),
        .text_disabled     = CG_RGB(160, 160, 160),
        .text_on_accent    = CG_RGB(255, 255, 255),
        .accent            = CG_RGB( 37, 99, 235),
        .accent_hover      = CG_RGB( 59,118, 255),
        .accent_pressed    = CG_RGB( 20, 70, 180),
        .button_bg         = CG_RGB( 37,  99, 235),
        .button_bg_hover   = CG_RGB( 59, 118, 255),
        .button_bg_pressed = CG_RGB( 20,  70, 180),
        .button_fg         = CG_RGB(255, 255, 255),
        .input_bg          = CG_RGB(255, 255, 255),
        .input_border      = CG_RGB(200, 200, 210),
        .input_border_focus= CG_RGB( 37,  99, 235),
        .input_selection   = CG_RGBA(37, 99, 235, 60),
        .input_placeholder = CG_RGB(160, 160, 170),
        .border            = CG_RGB(220, 220, 225),
        .divider           = CG_RGB(230, 230, 235),
        .scrollbar         = CG_RGB(180, 180, 190),
        .scrollbar_track   = CG_RGB(245, 245, 247),
        .success           = CG_RGB( 22, 163, 74),
        .warning           = CG_RGB(202, 138,  4),
        .error             = CG_RGB(220,  38,  38),
        .info              = CG_RGB( 37,  99, 235),
        .surface_disabled  = CG_RGB(235, 235, 240),
    },
    .metrics = {  // Same metrics as dark
        .button_radius = 6, .input_radius = 6, .card_radius = 10,
        .border_width = 1, .focus_ring_width = 2,
        .spacing_xs = 4, .spacing_sm = 8, .spacing_md = 16,
        .spacing_lg = 24, .spacing_xl = 32,
        .font_size_sm = 12, .font_size_md = 14,
        .font_size_lg = 18, .font_size_xl = 24,
    },
};

void cg_theme_set(CgTheme* theme) {
    cg->theme = theme;
    // Load fonts with theme's font sizes
    const char* font_path = theme->font_path[0]
                          ? theme->font_path
                          : "fonts/Inter-Regular.ttf";
    theme->font_sm = cg_font_get(font_path, theme->metrics.font_size_sm, 0);
    theme->font_md = cg_font_get(font_path, theme->metrics.font_size_md, 0);
    theme->font_lg = cg_font_get(font_path, theme->metrics.font_size_lg, 0);
    theme->font_xl = cg_font_get(font_path, theme->metrics.font_size_xl, 0);
}

TTF_Font* cg_theme_default_font(void) {
    return cg->theme ? cg->theme->font_md : NULL;
}

void cg_use_dark_theme(void)  { cg_theme_set(&theme_dark);  }
void cg_use_light_theme(void) { cg_theme_set(&theme_light); }

Chapter 14: Animation System

Smooth transitions make a UI feel polished. Our animation engine supports tweening any float property over time with easing functions.

14.1 Easing Functions

// include/cg.h

typedef enum {
    CG_EASE_LINEAR,
    CG_EASE_IN_QUAD,
    CG_EASE_OUT_QUAD,
    CG_EASE_IN_OUT_QUAD,
    CG_EASE_IN_CUBIC,
    CG_EASE_OUT_CUBIC,
    CG_EASE_IN_OUT_CUBIC,
    CG_EASE_OUT_BOUNCE,
    CG_EASE_OUT_ELASTIC,
    CG_EASE_OUT_BACK,
} CgEasing;

// src/cg_anim.c

#include <math.h>

static float apply_easing(float t, CgEasing easing) {
    switch (easing) {
    case CG_EASE_LINEAR:       return t;
    case CG_EASE_IN_QUAD:      return t * t;
    case CG_EASE_OUT_QUAD:     return t * (2.0f - t);
    case CG_EASE_IN_OUT_QUAD:
        return t < 0.5f ? 2.0f * t * t : -1.0f + (4.0f - 2.0f * t) * t;
    case CG_EASE_IN_CUBIC:     return t * t * t;
    case CG_EASE_OUT_CUBIC: {
        float p = t - 1.0f;
        return p * p * p + 1.0f;
    }
    case CG_EASE_IN_OUT_CUBIC:
        return t < 0.5f ? 4.0f * t * t * t
                        : (t - 1.0f) * (2.0f * t - 2.0f) * (2.0f * t - 2.0f) + 1.0f;
    case CG_EASE_OUT_BOUNCE: {
        const float n1 = 7.5625f, d1 = 2.75f;
        if (t < 1.0f / d1) return n1 * t * t;
        if (t < 2.0f / d1) { t -= 1.5f / d1;   return n1 * t * t + 0.75f; }
        if (t < 2.5f / d1) { t -= 2.25f / d1;  return n1 * t * t + 0.9375f; }
        t -= 2.625f / d1; return n1 * t * t + 0.984375f;
    }
    case CG_EASE_OUT_ELASTIC: {
        if (t == 0.0f || t == 1.0f) return t;
        float c4 = (2.0f * (float)M_PI) / 3.0f;
        return powf(2.0f, -10.0f * t) * sinf((t * 10.0f - 0.75f) * c4) + 1.0f;
    }
    case CG_EASE_OUT_BACK: {
        const float c1 = 1.70158f, c3 = c1 + 1.0f;
        float p = t - 1.0f;
        return 1.0f + c3 * p * p * p + c1 * p * p;
    }
    default: return t;
    }
}

14.2 Tween Animation

typedef struct CgAnim {
    float*    target;       // Pointer to the float being animated
    float     from;
    float     to;
    float     elapsed_ms;
    float     duration_ms;
    CgEasing  easing;
    bool      done;
    CgWidget* owner;
    void      (*on_complete)(CgWidget*, void*);
    void*     on_complete_ud;
    struct CgAnim* next;
} CgAnim;

// Start a float tween
CgAnim* cg_tween_float(CgWidget* owner, float* target, float to,
                        float duration_ms, CgEasing easing) {
    // Cancel any existing tween targeting the same float
    CgAnim** p = (CgAnim**)&owner->anims;
    while (*p) {
        if ((*p)->target == target) {
            CgAnim* old = *p;
            *p = old->next;
            free(old);
        } else {
            p = &(*p)->next;
        }
    }

    CgAnim* anim = calloc(1, sizeof(CgAnim));
    anim->target      = target;
    anim->from        = *target;
    anim->to          = to;
    anim->elapsed_ms  = 0.0f;
    anim->duration_ms = SDL_max(duration_ms, 1.0f);
    anim->easing      = easing;
    anim->owner       = owner;

    // Prepend to owner's animation list
    anim->next    = (CgAnim*)owner->anims;
    owner->anims  = (struct CgAnim*)anim;

    // Also register in global list for update
    anim->next    = cg->anim_head;
    cg->anim_head = anim;

    return anim;
}

// Update all animations (called each frame)
void cg_anims_tick(float delta_ms) {
    CgAnim** pp = &cg->anim_head;
    while (*pp) {
        CgAnim* a = *pp;
        a->elapsed_ms += delta_ms;

        float t = SDL_min(1.0f, a->elapsed_ms / a->duration_ms);
        float et = apply_easing(t, a->easing);
        *a->target = a->from + (a->to - a->from) * et;

        if (t >= 1.0f) {
            *a->target = a->to;
            if (a->on_complete) a->on_complete(a->owner, a->on_complete_ud);
            *pp = a->next;
            free(a);
        } else {
            pp = &a->next;
        }
    }
}

Chapter 15: Advanced Widgets

15.1 Checkbox

typedef struct CgCheckbox {
    CgWidget base;
    char*    label;
    bool     checked;
    TTF_Font* font;
    float    check_anim;  // 0.0 = unchecked, 1.0 = checked (animated)
} CgCheckbox;

static void checkbox_paint(CgWidget* w) {
    CgCheckbox* cb = (CgCheckbox*)w;
    SDL_Rect r = w->bounds_abs;

    bool focused  = (w->flags & CG_FLAG_FOCUSED)  != 0;
    bool hovered  = (w->flags & CG_FLAG_HOVERED)  != 0;
    bool disabled = !(w->flags & CG_FLAG_ENABLED);

    int box_size = cg_font_line_height(cb->font);
    SDL_Rect box = {r.x, r.y + (r.h - box_size) / 2, box_size, box_size};

    // Box background
    CgColor box_bg;
    if (cb->checked) {
        box_bg = disabled ? cg->theme->colors.accent_disabled
                          : hovered ? cg->theme->colors.accent_hover
                                    : cg->theme->colors.accent;
    } else {
        box_bg = disabled ? cg->theme->colors.surface_disabled
                          : cg->theme->colors.input_bg;
    }
    cg_draw_rounded_rect(box, 4, box_bg);
    cg_draw_rounded_rect_outline(box, 4, 1, cg->theme->colors.input_border);

    // Check mark (drawn as two lines forming a ✓)
    if (cb->check_anim > 0.0f) {
        // Interpolate check mark drawing based on anim
        CgColor check_color = cg->theme->colors.text_on_accent;
        check_color.a = (uint8_t)(255 * cb->check_anim);

        // Line 1: short stroke of the checkmark
        SDL_SetRenderDrawColor(cg->renderer, check_color.r, check_color.g,
                                check_color.b, check_color.a);
        SDL_SetRenderDrawBlendMode(cg->renderer, SDL_BLENDMODE_BLEND);

        int x1 = box.x + box.w * 2/8;
        int y1 = box.y + box.h * 5/8;
        int xm = box.x + box.w * 4/8;
        int ym = box.y + box.h * 7/8;
        int x2 = box.x + box.w * 7/8;
        int y2 = box.y + box.h * 2/8;

        // Draw with 2px thickness
        for (int t = -1; t <= 1; t++) {
            SDL_RenderDrawLine(cg->renderer, x1+t, y1, xm+t, ym);
            SDL_RenderDrawLine(cg->renderer, xm+t, ym, x2+t, y2);
        }
    }

    // Focus ring
    if (focused) {
        SDL_Rect ring = {box.x-2, box.y-2, box.w+4, box.h+4};
        CgColor fc = cg->theme->colors.accent; fc.a = 180;
        cg_draw_rounded_rect_outline(ring, 6, 2, fc);
    }

    // Label
    if (cb->label && *cb->label) {
        CgColor lc = disabled ? cg->theme->colors.text_disabled
                              : cg->theme->colors.text_primary;
        int tw, th;
        SDL_Texture* tex = cg_text_texture(cb->font, cb->label, lc, &tw, &th);
        if (tex) {
            int ty = r.y + (r.h - th) / 2;
            SDL_Rect dst = {box.x + box.w + 8, ty, tw, th};
            SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
            SDL_RenderCopy(cg->renderer, tex, NULL, &dst);
        }
    }
}

static bool checkbox_on_event(CgWidget* w, CgEvent* ev) {
    CgCheckbox* cb = (CgCheckbox*)w;

    if ((ev->type == CG_EVENT_MOUSE_CLICK && ev->mouse.button == SDL_BUTTON_LEFT) ||
        (ev->type == CG_EVENT_KEY_DOWN &&
         (ev->key.sym == SDLK_SPACE || ev->key.sym == SDLK_RETURN))) {

        if (!(w->flags & CG_FLAG_ENABLED)) return true;
        cb->checked = !cb->checked;

        // Animate check mark
        cg_tween_float(w, &cb->check_anim,
                        cb->checked ? 1.0f : 0.0f,
                        150, CG_EASE_OUT_CUBIC);

        CgEvent change = {.type = CG_EVENT_CHANGE};
        change.change.value = cb->checked ? 1.0f : 0.0f;
        cg_fire_event(w, &change);
        return true;
    }
    return false;
}

15.2 Slider

typedef struct CgSlider {
    CgWidget base;
    float    value;       // Current value in [min, max]
    float    min, max;
    float    step;        // 0 = continuous
    bool     vertical;
    TTF_Font* font;       // For value label
    bool     show_value;

    // Visuals
    CgColor  track_color;
    CgColor  fill_color;
    CgColor  thumb_color;
    CgColor  thumb_hover_color;
    int      track_height;
    int      thumb_size;
    float    thumb_anim;  // Hover animation

    // Dragging
    bool     dragging;
    int      drag_start_x;
    float    drag_start_val;
} CgSlider;

static void slider_paint(CgWidget* w) {
    CgSlider* s = (CgSlider*)w;
    SDL_Rect r  = w->bounds_abs;

    bool hovered  = (w->flags & CG_FLAG_HOVERED)  != 0;
    bool focused  = (w->flags & CG_FLAG_FOCUSED)   != 0;
    bool disabled = !(w->flags & CG_FLAG_ENABLED);

    // Track
    int track_y = r.y + (r.h - s->track_height) / 2;
    SDL_Rect track = {r.x + s->thumb_size/2, track_y,
                      r.w - s->thumb_size, s->track_height};
    cg_draw_rounded_rect(track, s->track_height/2, s->track_color);

    // Fill (0 to thumb)
    float norm = (s->value - s->min) / (s->max - s->min);
    norm = SDL_clamp(norm, 0.0f, 1.0f);
    int fill_w = (int)(norm * track.w);
    SDL_Rect fill = {track.x, track.y, fill_w, track.h};
    CgColor fill_c = disabled ? cg->theme->colors.accent_disabled : s->fill_color;
    cg_draw_rounded_rect(fill, s->track_height/2, fill_c);

    // Thumb
    int thumb_x = track.x + fill_w - s->thumb_size/2;
    int thumb_y = r.y + (r.h - s->thumb_size) / 2;
    SDL_Rect thumb = {thumb_x, thumb_y, s->thumb_size, s->thumb_size};

    CgColor thumb_c = disabled ? cg->theme->colors.surface_disabled
                     : cg_color_lerp(s->thumb_color, s->thumb_hover_color,
                                      s->thumb_anim);

    // Thumb shadow
    cg_draw_shadow(thumb, s->thumb_size/2, 3, CG_RGBA(0,0,0,60));
    cg_draw_rounded_rect(thumb, s->thumb_size/2, thumb_c);

    // Focus ring
    if (focused) {
        SDL_Rect ring = {thumb.x-2, thumb.y-2, thumb.w+4, thumb.h+4};
        CgColor fc = cg->theme->colors.accent; fc.a = 150;
        cg_draw_rounded_rect_outline(ring, s->thumb_size/2 + 2, 2, fc);
    }

    // Value label
    if (s->show_value && s->font) {
        char val_str[32];
        if (s->step >= 1.0f) snprintf(val_str, sizeof(val_str), "%.0f", s->value);
        else                  snprintf(val_str, sizeof(val_str), "%.2f", s->value);

        int tw, th;
        CgColor tc = disabled ? cg->theme->colors.text_disabled
                              : cg->theme->colors.text_secondary;
        SDL_Texture* tex = cg_text_texture(s->font, val_str, tc, &tw, &th);
        if (tex) {
            int tx = thumb_x + (s->thumb_size - tw) / 2;
            int ty = thumb_y - th - 6;
            SDL_Rect dst = {tx, ty, tw, th};
            SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
            SDL_RenderCopy(cg->renderer, tex, NULL, &dst);
        }
    }
}

static bool slider_on_event(CgWidget* w, CgEvent* ev) {
    CgSlider* s = (CgSlider*)w;
    SDL_Rect r  = w->bounds_abs;

    if (ev->type == CG_EVENT_MOUSE_ENTER) {
        cg_tween_float(w, &s->thumb_anim, 1.0f, 100, CG_EASE_OUT_QUAD);
        return false;
    }
    if (ev->type == CG_EVENT_MOUSE_LEAVE && !s->dragging) {
        cg_tween_float(w, &s->thumb_anim, 0.0f, 100, CG_EASE_OUT_QUAD);
        return false;
    }

    if (ev->type == CG_EVENT_MOUSE_DOWN && ev->mouse.button == SDL_BUTTON_LEFT) {
        if (!(w->flags & CG_FLAG_ENABLED)) return true;
        s->dragging      = true;
        s->drag_start_x  = ev->mouse.x;
        s->drag_start_val = s->value;
        cg->mouse_captured = w;

        // Jump to click position
        int track_x = r.x + s->thumb_size/2;
        int track_w = r.w - s->thumb_size;
        float norm = (float)(ev->mouse.x - track_x) / track_w;
        s->value = s->min + norm * (s->max - s->min);
        s->value = SDL_clamp(s->value, s->min, s->max);
        if (s->step > 0.0f) s->value = roundf(s->value / s->step) * s->step;

        CgEvent change = {.type = CG_EVENT_CHANGE, .change.value = s->value};
        cg_fire_event(w, &change);
        return true;
    }

    if (ev->type == CG_EVENT_MOUSE_MOVE && s->dragging) {
        int track_w = r.w - s->thumb_size;
        int delta = ev->mouse.x - s->drag_start_x;
        float delta_val = (float)delta / track_w * (s->max - s->min);
        s->value = s->drag_start_val + delta_val;
        s->value = SDL_clamp(s->value, s->min, s->max);
        if (s->step > 0.0f) s->value = roundf(s->value / s->step) * s->step;

        CgEvent change = {.type = CG_EVENT_CHANGE, .change.value = s->value};
        cg_fire_event(w, &change);
        return true;
    }

    if (ev->type == CG_EVENT_MOUSE_UP) {
        s->dragging = false;
        if (cg->mouse_captured == w) cg->mouse_captured = NULL;
        if (!(w->flags & CG_FLAG_HOVERED))
            cg_tween_float(w, &s->thumb_anim, 0.0f, 100, CG_EASE_OUT_QUAD);
        return true;
    }

    if (ev->type == CG_EVENT_KEY_DOWN) {
        float delta = s->step > 0 ? s->step : (s->max - s->min) * 0.05f;
        if (ev->key.sym == SDLK_LEFT || ev->key.sym == SDLK_DOWN)
            s->value -= delta;
        else if (ev->key.sym == SDLK_RIGHT || ev->key.sym == SDLK_UP)
            s->value += delta;
        else
            return false;

        s->value = SDL_clamp(s->value, s->min, s->max);
        CgEvent change = {.type = CG_EVENT_CHANGE, .change.value = s->value};
        cg_fire_event(w, &change);
        return true;
    }

    return false;
}

Chapter 16: Project — Building a Complete Application

We will build cgPad: a minimal text editor / settings application that demonstrates every major feature of the library.

16.1 Application Layout

┌────────────────────────────────────────────────────┐
│  [cgPad]              [Dark] [Light]   [×]         │  ← Titlebar
├────────────────────────────────────────────────────┤
│ [New] [Open] [Save]        <spacer>  [Font: 14 ▼]  │  ← Toolbar
├──────────────┬─────────────────────────────────────┤
│ Properties   │                                     │
│              │   Main text editing area            │
│  Font Size:  │   (scrollable)                      │
│  [====•====] │                                     │
│              │                                     │
│  Word Wrap:  │                                     │
│  [✓]         │                                     │
│              │                                     │
│  Theme:      │                                     │
│  (•) Dark    │                                     │
│  ( ) Light   │                                     │
│              │                                     │
│  Opacity:    │                                     │
│  [====•====] │                                     │
├──────────────┴─────────────────────────────────────┤
│  Line: 1, Col: 1    │  UTF-8  │  CRLF  │  Ready    │  ← Statusbar
└────────────────────────────────────────────────────┘

16.2 Complete main.c

// demo/main.c — cgPad demo application

#include "cg.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// ── Application State ────────────────────────────────────────────────────

typedef struct AppState {
    // Widgets we need to reference later
    CgWidget* title_label;
    CgWidget* editor;        // Main text input
    CgWidget* font_slider;
    CgWidget* wrap_check;
    CgWidget* status_label;
    CgWidget* dark_radio;
    CgWidget* light_radio;
    CgWidget* opacity_slider;

    // State
    bool  dark_theme;
    int   font_size;
    bool  word_wrap;
    float opacity;
    char  status_buf[128];

    // Window (for opacity)
    SDL_Window* window;
} AppState;

static AppState app = {
    .dark_theme = true,
    .font_size  = 14,
    .word_wrap  = true,
    .opacity    = 1.0f,
};

// ── Callbacks ─────────────────────────────────────────────────────────────

static bool on_font_size_change(CgWidget* w, CgEvent* ev, void* ud) {
    (void)w; (void)ud;
    app.font_size = (int)ev->change.value;
    char buf[64];
    snprintf(buf, sizeof(buf), "Font: %d px", app.font_size);
    cg_label_set_text(app.title_label, buf);
    return true;
}

static bool on_wrap_change(CgWidget* w, CgEvent* ev, void* ud) {
    (void)w; (void)ud;
    app.word_wrap = ev->change.value > 0.5f;
    cg_label_set_wrap(app.editor, app.word_wrap);
    return true;
}

static bool on_dark_click(CgWidget* w, CgEvent* ev, void* ud) {
    (void)w; (void)ev; (void)ud;
    app.dark_theme = true;
    cg_use_dark_theme();
    return true;
}

static bool on_light_click(CgWidget* w, CgEvent* ev, void* ud) {
    (void)w; (void)ev; (void)ud;
    app.dark_theme = false;
    cg_use_light_theme();
    return true;
}

static bool on_opacity_change(CgWidget* w, CgEvent* ev, void* ud) {
    (void)w; (void)ud;
    app.opacity = ev->change.value;
    SDL_SetWindowOpacity(app.window, app.opacity);
    return true;
}

static bool on_new_click(CgWidget* w, CgEvent* ev, void* ud) {
    (void)w; (void)ev; (void)ud;
    cg_input_set_text(app.editor, "");
    snprintf(app.status_buf, sizeof(app.status_buf), "New file");
    cg_label_set_text(app.status_label, app.status_buf);
    return true;
}

static bool on_editor_change(CgWidget* w, CgEvent* ev, void* ud) {
    (void)w; (void)ud;
    int len = ev->change.str_value ? (int)strlen(ev->change.str_value) : 0;
    snprintf(app.status_buf, sizeof(app.status_buf), "%d characters", len);
    cg_label_set_text(app.status_label, app.status_buf);
    return false;
}

// ── Build UI ──────────────────────────────────────────────────────────────

static CgWidget* build_titlebar(void) {
    CgWidget* bar = cg_row();
    cg_panel_set_bg(bar, cg->theme->colors.bg_surface);
    cg_panel_set_padding(bar, CG_INSETS(0, 12, 0, 12));
    cg_panel_set_gap(bar, 8);
    bar->desired_h = 42;
    bar->flags    |= CG_FLAG_EXPAND_W;

    app.title_label = cg_label_new("cgPad — Untitled");
    cg_label_set_font(app.title_label,
        cg_font_get("fonts/Inter-SemiBold.ttf", 14, 0));
    cg_panel_add(bar, app.title_label);

    cg_panel_add(bar, cg_spacer_new());

    // Theme toggles
    CgWidget* dark_btn = cg_button_new("● Dark");
    cg_button_set_style(dark_btn, CG_BTN_STYLE_TEXT);
    cg_on_click(dark_btn, on_dark_click, NULL);
    cg_panel_add(bar, dark_btn);

    CgWidget* light_btn = cg_button_new("○ Light");
    cg_button_set_style(light_btn, CG_BTN_STYLE_TEXT);
    cg_on_click(light_btn, on_light_click, NULL);
    cg_panel_add(bar, light_btn);

    return bar;
}

static CgWidget* build_toolbar(void) {
    CgWidget* bar = cg_row();
    cg_panel_set_bg(bar, cg->theme->colors.bg_surface);
    cg_panel_set_padding(bar, CG_INSETS(6, 8, 6, 8));
    cg_panel_set_gap(bar, 4);
    bar->desired_h = 46;
    bar->flags    |= CG_FLAG_EXPAND_W;

    // Add a separator helper
    CgWidget* sep(void) {
        CgWidget* s = cg_widget_alloc(sizeof(CgWidget), CG_TYPE_SEPARATOR, NULL);
        s->desired_w = 1;
        s->flags    |= CG_FLAG_EXPAND_H;
        s->margin    = CG_INSETS(4, 4, 4, 4);
        return s;
    }

    CgWidget* new_btn  = cg_button_new("⊕ New");
    CgWidget* open_btn = cg_button_new("📁 Open");
    CgWidget* save_btn = cg_button_new("💾 Save");

    cg_button_set_style(new_btn,  CG_BTN_STYLE_OUTLINED);
    cg_button_set_style(open_btn, CG_BTN_STYLE_TEXT);
    cg_button_set_style(save_btn, CG_BTN_STYLE_TEXT);

    cg_on_click(new_btn, on_new_click, NULL);

    cg_panel_add(bar, new_btn);
    cg_panel_add(bar, open_btn);
    cg_panel_add(bar, save_btn);
    cg_panel_add(bar, cg_spacer_new());

    CgWidget* font_label = cg_label_new("Font size:");
    cg_panel_add(bar, font_label);

    CgWidget* fslider = cg_slider_new(8, 48, app.font_size);
    fslider->desired_w = 120;
    cg_slider_set_step(fslider, 1.0f);
    cg_slider_set_show_value(fslider, true);
    cg_on_change(fslider, on_font_size_change, NULL);
    app.font_slider = fslider;
    cg_panel_add(bar, fslider);

    return bar;
}

static CgWidget* build_sidebar(void) {
    CgWidget* side = cg_col();
    cg_panel_set_bg(side, cg->theme->colors.bg_surface);
    cg_panel_set_padding(side, CG_INSETS_ALL(16));
    cg_panel_set_gap(side, 16);
    side->desired_w = 180;
    side->flags    |= CG_FLAG_EXPAND_H;

    // Section: Text settings
    CgWidget* sec1 = cg_label_new("TEXT");
    cg_label_set_color(sec1, cg->theme->colors.text_secondary);
    cg_label_set_font(sec1, cg_font_get("fonts/Inter-SemiBold.ttf", 11, 0));
    cg_panel_add(side, sec1);

    CgWidget* wrap = cg_checkbox_new("Word wrap", app.word_wrap);
    cg_on_change(wrap, on_wrap_change, NULL);
    app.wrap_check = wrap;
    cg_panel_add(side, wrap);

    // Divider
    CgWidget* div = cg_widget_alloc(sizeof(CgWidget), CG_TYPE_SEPARATOR, NULL);
    div->desired_h = 1;
    div->flags    |= CG_FLAG_EXPAND_W;
    cg_panel_add(side, div);

    // Section: Theme
    CgWidget* sec2 = cg_label_new("THEME");
    cg_label_set_color(sec2, cg->theme->colors.text_secondary);
    cg_label_set_font(sec2, cg_font_get("fonts/Inter-SemiBold.ttf", 11, 0));
    cg_panel_add(side, sec2);

    CgWidget* dark_r  = cg_radio_new("Dark",  "theme", true);
    CgWidget* light_r = cg_radio_new("Light", "theme", false);
    cg_on_click(dark_r,  on_dark_click,  NULL);
    cg_on_click(light_r, on_light_click, NULL);
    app.dark_radio  = dark_r;
    app.light_radio = light_r;
    cg_panel_add(side, dark_r);
    cg_panel_add(side, light_r);

    // Divider
    CgWidget* div2 = cg_widget_alloc(sizeof(CgWidget), CG_TYPE_SEPARATOR, NULL);
    div2->desired_h = 1;
    div2->flags    |= CG_FLAG_EXPAND_W;
    cg_panel_add(side, div2);

    // Section: Window
    CgWidget* sec3 = cg_label_new("WINDOW");
    cg_label_set_color(sec3, cg->theme->colors.text_secondary);
    cg_label_set_font(sec3, cg_font_get("fonts/Inter-SemiBold.ttf", 11, 0));
    cg_panel_add(side, sec3);

    CgWidget* op_label = cg_label_new("Opacity");
    cg_panel_add(side, op_label);

    CgWidget* op_slider = cg_slider_new(0.1f, 1.0f, 1.0f);
    op_slider->flags   |= CG_FLAG_EXPAND_W;
    cg_on_change(op_slider, on_opacity_change, NULL);
    app.opacity_slider = op_slider;
    cg_panel_add(side, op_slider);

    cg_panel_add(side, cg_spacer_new());

    // Version info at bottom
    CgWidget* ver = cg_label_new("cgPad v0.1\nBuilt with cg + SDL2");
    cg_label_set_color(ver, cg->theme->colors.text_secondary);
    cg_label_set_wrap(ver, true);
    cg_panel_add(side, ver);

    return side;
}

static CgWidget* build_editor_area(void) {
    // Scrollable text input area
    CgWidget* scroll = cg_scroll_new();
    scroll->flags   |= CG_FLAG_EXPAND_W | CG_FLAG_EXPAND_H;

    CgWidget* editor = cg_input_new();
    editor->flags   |= CG_FLAG_EXPAND_W | CG_FLAG_EXPAND_H;
    cg_input_set_multiline(editor, true);
    cg_input_set_placeholder(editor, "Start typing...\n\n"
                              "This editor supports:\n"
                              "  • Cursor navigation (arrow keys, Home, End)\n"
                              "  • Selection (Shift+arrows)\n"
                              "  • Copy/Paste (Ctrl+C, Ctrl+V)\n"
                              "  • Undo/Redo (Ctrl+Z, Ctrl+Y)\n");
    cg_on_change(editor, on_editor_change, NULL);
    app.editor = editor;

    cg_scroll_set_content(scroll, editor);
    return scroll;
}

static CgWidget* build_statusbar(void) {
    CgWidget* bar = cg_row();
    cg_panel_set_bg(bar, cg->theme->colors.bg_surface);
    cg_panel_set_padding(bar, CG_INSETS(4, 12, 4, 12));
    cg_panel_set_gap(bar, 12);
    bar->desired_h = 26;
    bar->flags    |= CG_FLAG_EXPAND_W;

    app.status_label = cg_label_new("Ready");
    cg_label_set_color(app.status_label, cg->theme->colors.text_secondary);
    cg_label_set_font(app.status_label, cg_font_get("fonts/Inter-Regular.ttf", 11, 0));
    cg_panel_add(bar, app.status_label);

    cg_panel_add(bar, cg_spacer_new());

    CgWidget* enc = cg_label_new("UTF-8 · LF");
    cg_label_set_color(enc, cg->theme->colors.text_secondary);
    cg_label_set_font(enc, cg_font_get("fonts/Inter-Regular.ttf", 11, 0));
    cg_panel_add(bar, enc);

    return bar;
}

static CgWidget* build_ui(void) {
    // Root: full-window column
    CgWidget* root = cg_col();
    cg_panel_set_bg(root, cg->theme->colors.bg_app);
    root->flags |= CG_FLAG_EXPAND_W | CG_FLAG_EXPAND_H;

    cg_panel_add(root, build_titlebar());

    // Add horizontal divider
    CgWidget* hdiv = cg_widget_alloc(sizeof(CgWidget), CG_TYPE_SEPARATOR, NULL);
    hdiv->desired_h = 1;
    hdiv->flags    |= CG_FLAG_EXPAND_W;
    cg_panel_add(root, hdiv);

    cg_panel_add(root, build_toolbar());

    CgWidget* hdiv2 = cg_widget_alloc(sizeof(CgWidget), CG_TYPE_SEPARATOR, NULL);
    hdiv2->desired_h = 1;
    hdiv2->flags    |= CG_FLAG_EXPAND_W;
    cg_panel_add(root, hdiv2);

    // Main area: sidebar + editor
    CgWidget* main_area = cg_row();
    main_area->flags |= CG_FLAG_EXPAND_W | CG_FLAG_EXPAND_H;

    cg_panel_add(main_area, build_sidebar());

    // Vertical divider
    CgWidget* vdiv = cg_widget_alloc(sizeof(CgWidget), CG_TYPE_SEPARATOR, NULL);
    vdiv->desired_w = 1;
    vdiv->flags    |= CG_FLAG_EXPAND_H;
    cg_panel_add(main_area, vdiv);

    cg_panel_add(main_area, build_editor_area());
    cg_panel_add(root, main_area);

    CgWidget* hdiv3 = cg_widget_alloc(sizeof(CgWidget), CG_TYPE_SEPARATOR, NULL);
    hdiv3->desired_h = 1;
    hdiv3->flags    |= CG_FLAG_EXPAND_W;
    cg_panel_add(root, hdiv3);

    cg_panel_add(root, build_statusbar());

    return root;
}

// ── Main Loop ─────────────────────────────────────────────────────────────

int main(int argc, char* argv[]) {
    (void)argc; (void)argv;

    // Initialise cg
    cg_init(800, 600, "cgPad", SDL_WINDOW_RESIZABLE);
    cg_use_dark_theme();
    app.window = cg->window;

    // Build UI
    cg->root = build_ui();
    cg_set_focus(app.editor);

    // Main loop
    bool running = true;
    while (running) {
        Uint32 frame_start = SDL_GetTicks();

        // Events
        SDL_Event sdl_ev;
        while (SDL_PollEvent(&sdl_ev)) {
            if (sdl_ev.type == SDL_QUIT) { running = false; break; }
            cg_process_sdl_event(&sdl_ev);
        }

        // Update animations
        cg_anims_tick(cg->delta_ms);

        // Layout (only if dirty)
        if (cg->root->flags & CG_FLAG_DIRTY)
            cg_layout_root(cg->root);

        // Paint
        SDL_SetRenderDrawColor(cg->renderer, 0, 0, 0, 255);
        SDL_RenderClear(cg->renderer);
        cg_paint_widget(cg->root);
        SDL_RenderPresent(cg->renderer);

        // Timing
        Uint32 frame_ms = SDL_GetTicks() - frame_start;
        if (frame_ms < 16) SDL_Delay(16 - frame_ms);  // Cap at ~60 FPS
        cg->delta_ms = (float)(SDL_GetTicks() - frame_start);
    }

    // Cleanup
    cg_widget_destroy(cg->root);
    cg_shutdown();
    return 0;
}

16.3 The cg_init and cg_shutdown Functions

// src/cg_core.c

CgCtx* cg = NULL;

void cg_init(int w, int h, const char* title, Uint32 window_flags) {
    SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1");
    SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER);

    cg = calloc(1, sizeof(CgCtx));

    cg->window = SDL_CreateWindow(title,
        SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
        w, h,
        window_flags | SDL_WINDOW_ALLOW_HIGHDPI);

    cg->renderer = SDL_CreateRenderer(cg->window, -1,
        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);

    SDL_GetWindowSize(cg->window, &cg->window_w, &cg->window_h);
    SDL_RenderSetLogicalSize(cg->renderer, cg->window_w, cg->window_h);

    int dw, dh;
    SDL_GL_GetDrawableSize(cg->window, &dw, &dh);
    cg->dpi_scale = (float)dw / cg->window_w;

    cg_fonts_init();

    cg->last_tick = SDL_GetTicks();
}

void cg_shutdown(void) {
    cg_fonts_shutdown();
    SDL_DestroyRenderer(cg->renderer);
    SDL_DestroyWindow(cg->window);
    SDL_Quit();
    free(cg);
    cg = NULL;
}

void cg_paint_widget(CgWidget* w) {
    if (!w || !(w->flags & CG_FLAG_VISIBLE)) return;

    // Push clip if requested
    bool clipped = (w->flags & CG_FLAG_CLIP) != 0;
    if (clipped) cg_push_clip(w->bounds_abs);

    // Draw this widget
    if (w->vtable && w->vtable->paint)
        w->vtable->paint(w);

    // Recurse to children (depth-first, preserves paint order)
    CgWidget* child = w->first_child;
    while (child) {
        cg_paint_widget(child);
        child = child->next_sibling;
    }

    if (clipped) cg_pop_clip();
}

Appendix A: Full Public API Reference

// ── Lifecycle ────────────────────────────────────────────────────────────
void     cg_init(int w, int h, const char* title, Uint32 flags);
void     cg_shutdown(void);
void     cg_use_dark_theme(void);
void     cg_use_light_theme(void);
void     cg_theme_set(CgTheme* theme);

// ── Widget tree ──────────────────────────────────────────────────────────
void     cg_widget_add_child(CgWidget* parent, CgWidget* child);
void     cg_widget_remove(CgWidget* w);
void     cg_widget_destroy(CgWidget* w);
void     cg_widget_mark_dirty(CgWidget* w);
void     cg_widget_set_visible(CgWidget* w, bool visible);
void     cg_widget_set_enabled(CgWidget* w, bool enabled);
void     cg_widget_on(CgWidget* w, CgEventType type,
                      CgEventHandler fn, void* ud);

// ── Layout ───────────────────────────────────────────────────────────────
void     cg_layout_root(CgWidget* root);
SDL_Rect cg_content_rect(CgWidget* w);

// ── Events ───────────────────────────────────────────────────────────────
void     cg_process_sdl_event(SDL_Event* ev);
bool     cg_fire_event(CgWidget* target, CgEvent* ev);

// ── Focus ────────────────────────────────────────────────────────────────
void     cg_set_focus(CgWidget* w);
void     cg_clear_focus(void);
void     cg_focus_next(bool reverse);

// ── Painting ─────────────────────────────────────────────────────────────
void     cg_paint_widget(CgWidget* root);
void     cg_push_clip(SDL_Rect rect);
void     cg_pop_clip(void);
void     cg_draw_rect(SDL_Rect r, CgColor c);
void     cg_draw_rect_outline(SDL_Rect r, int thickness, CgColor c);
void     cg_draw_rounded_rect(SDL_Rect r, int radius, CgColor c);
void     cg_draw_rounded_rect_outline(SDL_Rect r, int rad, int bw, CgColor c);
void     cg_draw_hline(int x, int y, int w, CgColor c);
void     cg_draw_vline(int x, int y, int h, CgColor c);
void     cg_draw_shadow(SDL_Rect r, int radius, int blur, CgColor c);

// ── Fonts / Text ─────────────────────────────────────────────────────────
void         cg_fonts_init(void);
void         cg_fonts_shutdown(void);
TTF_Font*    cg_font_get(const char* path, int size, int style);
CgSize       cg_text_measure(TTF_Font* font, const char* text);
int          cg_font_line_height(TTF_Font* font);
CgTextLayout cg_text_wrap(TTF_Font* font, const char* text, int max_w);
void         cg_text_layout_free(CgTextLayout* layout);
SDL_Texture* cg_text_texture(TTF_Font* font, const char* text, CgColor c,
                              int* out_w, int* out_h);

// ── Animation ────────────────────────────────────────────────────────────
CgAnim*  cg_tween_float(CgWidget* owner, float* target, float to,
                         float duration_ms, CgEasing easing);
void     cg_anims_tick(float delta_ms);

// ── Widgets ──────────────────────────────────────────────────────────────
CgWidget* cg_panel_new(CgDirection direction);
CgWidget* cg_row(void);
CgWidget* cg_col(void);
void      cg_panel_add(CgWidget* panel, CgWidget* child);
void      cg_panel_set_gap(CgWidget* panel, int gap);
void      cg_panel_set_justify(CgWidget* panel, CgJustify justify);
void      cg_panel_set_align(CgWidget* panel, CgAlign align);
void      cg_panel_set_bg(CgWidget* panel, CgColor color);
void      cg_panel_set_border(CgWidget* panel, int w, int r, CgColor c);

CgWidget* cg_spacer_new(void);
CgWidget* cg_label_new(const char* text);
void      cg_label_set_text(CgWidget* w, const char* text);
void      cg_label_set_font(CgWidget* w, TTF_Font* font);
void      cg_label_set_color(CgWidget* w, CgColor color);
void      cg_label_set_align(CgWidget* w, int halign, int valign);
void      cg_label_set_wrap(CgWidget* w, bool wrap);

CgWidget* cg_button_new(const char* label);
void      cg_button_set_style(CgWidget* w, CgButtonStyle style);
void      cg_button_set_label(CgWidget* w, const char* label);

CgWidget* cg_input_new(void);
void      cg_input_set_placeholder(CgWidget* w, const char* text);
void      cg_input_set_text(CgWidget* w, const char* text);
const char* cg_input_get_text(CgWidget* w);
void      cg_input_set_multiline(CgWidget* w, bool multiline);
void      cg_input_set_password(CgWidget* w, bool password);
void      cg_input_set_max_chars(CgWidget* w, int max);

CgWidget* cg_checkbox_new(const char* label, bool checked);
bool      cg_checkbox_get_checked(CgWidget* w);
void      cg_checkbox_set_checked(CgWidget* w, bool checked);

CgWidget* cg_radio_new(const char* label, const char* group, bool selected);

CgWidget* cg_slider_new(float min, float max, float value);
float     cg_slider_get_value(CgWidget* w);
void      cg_slider_set_value(CgWidget* w, float value);
void      cg_slider_set_step(CgWidget* w, float step);
void      cg_slider_set_show_value(CgWidget* w, bool show);

CgWidget* cg_scroll_new(void);
void      cg_scroll_set_content(CgWidget* scroll, CgWidget* content);
void      cg_scroll_set_scroll(CgWidget* scroll, int x, int y);

CgWidget* cg_image_new(const char* path);
CgWidget* cg_image_from_texture(SDL_Texture* tex, int w, int h);

Appendix B: Common Patterns and Pitfalls

B.1 Correct Widget Sizing

// WRONG: desired_w in content pixels but expecting full-widget pixels
CgWidget* btn = cg_button_new("Click");
btn->desired_w = 100;   // This is fine — explicit pixel width for the widget

// CORRECT: Use FILL to make widget take remaining space
CgWidget* input = cg_input_new();
input->flags   |= CG_FLAG_EXPAND_W;   // Flex-grow: take all remaining width
input->desired_w = CG_SIZE_FILL;       // Equivalent hint

B.2 Rebuilding vs Updating

// WRONG: Destroying and rebuilding the tree every frame
void render_frame(void) {
    cg_widget_destroy(cg->root);   // Kills all widgets — slow!
    cg->root = build_ui();
}

// CORRECT: Update widget properties in place
void update_label(const char* new_text) {
    cg_label_set_text(my_label, new_text);   // Just dirty the label, fast
}

B.3 Event Handler Memory

// WRONG: handler references stack memory
void some_function(void) {
    int counter = 0;
    cg_on_click(btn, handler, &counter);  // &counter is gone after return!
}

// CORRECT: use heap-allocated userdata
typedef struct { int counter; } HandlerData;

void setup(void) {
    HandlerData* d = malloc(sizeof(HandlerData));
    d->counter = 0;
    cg_on_click(btn, handler, d);
    // d will be freed when widget is destroyed via destroy callback
}

B.4 Clip Stack Balance

// Every push_clip MUST have a matching pop_clip
static void custom_paint(CgWidget* w) {
    cg_push_clip(w->bounds_abs);
    // ... draw ...

    if (something_went_wrong) {
        cg_pop_clip();  // Must pop before returning!
        return;
    }

    // ... more drawing ...
    cg_pop_clip();
}

Appendix C: Extending the Library

Adding a New Widget Type

// 1. Define your widget struct (must start with CgWidget base)
typedef struct MyWidget {
    CgWidget base;         // MUST be first
    // ... your state ...
    float my_value;
    char* my_text;
} MyWidget;

// 2. Implement the virtual functions you need
static void my_measure(CgWidget* w, int aw, int ah) {
    // Compute content_size
    w->content_size = (CgSize){100, 40};
}

static void my_paint(CgWidget* w) {
    MyWidget* m = (MyWidget*)w;
    cg_draw_rounded_rect(w->bounds_abs, 8, CG_RGB(200, 100, 50));
    // ...
}

static bool my_on_event(CgWidget* w, CgEvent* ev) {
    MyWidget* m = (MyWidget*)w;
    if (ev->type == CG_EVENT_MOUSE_CLICK) {
        // handle click
        return true;   // Consumed
    }
    return false;      // Propagate
}

static void my_destroy(CgWidget* w) {
    MyWidget* m = (MyWidget*)w;
    free(m->my_text);
}

// 3. Create the vtable
#define CG_TYPE_MY_WIDGET  100   // Use values above 20 for custom widgets
static const CgWidgetVTable my_widget_vtable = {
    .measure  = my_measure,
    .arrange  = NULL,        // No children, skip arrange
    .paint    = my_paint,
    .on_event = my_on_event,
    .destroy  = my_destroy,
};

// 4. Constructor
CgWidget* my_widget_new(float value, const char* text) {
    MyWidget* m = (MyWidget*)cg_widget_alloc(sizeof(MyWidget),
                   CG_TYPE_MY_WIDGET, &my_widget_vtable);
    m->my_value = value;
    m->my_text  = strdup(text);
    m->base.flags  |= CG_FLAG_FOCUSABLE;
    m->base.padding = CG_INSETS_ALL(8);
    return (CgWidget*)m;
}

This guide covered the complete implementation of a GUI library from scratch: SDL2 foundations, rendering primitives with rounded corners and alpha, a retained-mode widget tree, a flexbox-inspired layout engine, a bubbling event system with hit testing, focus management, font rendering with SDL_ttf including word wrap and caret, all core widgets (label, button, text input, checkbox, slider, scrollview, panel), a theming system with dark and light modes, a tween animation engine, and a complete demo application. Every concept is grounded in working C code that can be compiled and run today.