Clay 2D UI Layout Library

repository·main·Indexed 12 days ago

https://github.com/nicbarker/clay

A high-performance, single-header 2D UI layout library written in C. It features a flexbox-like layout model with a declarative, React-like syntax and is designed to be renderer-agnostic, making it suitable for 3D engines, WebAssembly (Wasm), and custom software renderers. The library includes bindings for Odin and examples for GLES3, Termbox2, and the Playdate console.

Tokens
21.6K
Snippets
68
Records
73
Agent score
97%

What's inside Clay

  1. GLES3 Renderer capabilities and limitations

    main

    The GLES3 renderer is a work-in-progress implementation that demonstrates the core rendering pipeline.

    Rendering Features:

    • Supports all draw commands except custom.
    • Batching (No clipping): Quad-based commands (Rectangle, Image, Border) are rendered in a single draw call. Glyphs from the same font are rendered in a single instanced draw call.
    • Clipping: When scissoring is used, the renderer flushes draw calls before and after each scissor region.
    • Assets: Supports up to 4 fonts and 4 image textures. Image textures can function as texture atlases.
    • Custom UVs: UserData can be used to provide per-image UV coordinates, enabling multiple images to share a single OpenGL texture.
    • Dependencies: Uses stb_image.h and stb_truetype.h for asset loading via a modular loading layer.

    Platform Support:

    • Uses GLFW for windowing.
    • The renderer itself is framework agnostic.
  2. How to declare child elements in Odin

    main

    Unlike the C API which uses macros, the Odin bindings use if statements to create the scope for declaring child elements. This is the equivalent of the C CLAY element macros. When you call clay.UI(), it returns a boolean that allows you to wrap child elements within an if block.

    // Odin form of element macros
    if clay.UI(clay.ID("Outer"))({ layout = { padding = clay.PaddingAll(16) }}) {
        // Child elements here
    }
  3. Understand GLES3 Renderer capabilities

    main

    The GLES3 renderer is framework-agnostic and supports most Clay draw commands. Its performance characteristics and features include:

    Rendering Performance

    • Quad-based commands: Rectangle, Image, and Border are batched into a single draw call when no clipping is present.
    • Glyphs: All glyphs sharing the same font are rendered in a single instanced draw call.
    • Clipping: When scissoring is used, the renderer flushes draw calls before and after each scissor region.

    Asset and Texture Support

    • Font/Texture Limits: Supports up to 4 fonts and 4 image textures.
    • Texture Atlases: Image textures can be used as texture atlases. You can provide per-image UV coordinates via Custom UserData to allow multiple images to share a single OpenGL texture.
    • Dependencies: Uses stb_image.h and stb_truetype.h for asset loading. The loading layer is modular and can be replaced with a custom asset pipeline.
  4. Understand Floating Elements

    main

    Floating elements (configured via Clay_FloatingElementConfig) are used for UI components that need to appear above other content, such as tooltips or modals.

    Mental Model: Think of a floating container as a completely separate UI hierarchy that is attached to a specific (x, y) coordinate on its 'parent' element.

    Characteristics:

    • By default, they attach to the top-left corner of their parent.
    • They do not affect the width or height of their parent.
    • They do not affect the positioning of sibling elements.
    • They can partially or completely occlude other elements depending on their z-index.
    • Aside from positioning, they function like standard elements (e.g., they can expand to fit their children).
  5. How floating element attachment points work

    main

    The .attachPoints field in Clay_FloatingElementConfig uses a coordinate-matching mental model. You specify a point on the floating container (.element) and a point on the target parent (.parent). Clay then aligns these two points on top of each other.

    For example, to place the bottom-center of a tooltip directly above the top-center of a button, you would use: { .element = CLAY_ATTACH_POINT_CENTER_BOTTOM, .parent = CLAY_ATTACH_POINT_CENTER_TOP }.

    CLAY(CLAY_ID("Floating"), {
        .floating = {
            .attachPoints = {
                .element = CLAY_ATTACH_POINT_LEFT_CENTER,
                .parent = CLAY_ATTACH_POINT_RIGHT_TOP
            }
        }
    }) {};
  6. Manage Element IDs with CLAY_ID, CLAY_AUTO_ID, and CLAY_IDI

    main

    Clay uses IDs to identify elements for querying state (like hover) or dimensions.

    • CLAY_ID("string"): Produces a stable ID from a string. Use this for elements that need to be queried by name or for stable transitions.
    • CLAY_AUTO_ID({ ... }): Generates a unique ID. Useful for elements that don't need persistent identity.
    • CLAY_IDI("string", index): Generates unique IDs in loops (e.g., Item0, Item1) to avoid manual string construction.

    Warning: Avoid duplicate IDs if you intend to attach floating containers to specific elements, as it may cause unexpected behavior.

    // Stable ID
    CLAY(CLAY_ID("OuterContainer"), { ...configuration }) {}
    
    // Unique ID
    CLAY_AUTO_ID({ ...configuration }) {}
    
    // ID in a loop
    for (int index = 0; index < items.length; index++) {
        CLAY(CLAY_IDI("Item", index), { ..configuration }) {}
    }
  7. Use Floating Elements for Modals and Tooltips

    main

    Standard elements are laid out within their parent and affect siblings. To create elements that overlap others (like tooltips or modals), use the CLAY_FLOATING() macro. Floating elements have a z-index and do not affect the layout of their parent or siblings.

    Use .floating.attachTo = CLAY_ATTACH_TO_PARENT to anchor a floating element to its containing element.

    CLAY(CLAY_ID("Outer"), { .layout = { .layoutDirection = CLAY_TOP_TO_BOTTOM } }) {
        CLAY_TEXT(text, &headerTextConfig);
        // This tooltip floats over the other elements
        CLAY(CLAY_ID("Tooltip"), { .floating = { .attachTo = CLAY_ATTACH_TO_PARENT } }) {}
        CLAY_TEXT(text, &headerTextConfig);
    }
  8. Understand Clay Naming Conventions

    main

    Clay uses specific prefixes to distinguish between different types of functions and macros:

    • **CAPITAL_LETTERS()**: Used for macros.
    • **Clay__** (double underscore): Used for internal functions. These are not intended for user use and are subject to change.
    • **Clay_** (single underscore): Used for public API functions that can be called by the user.
  9. Run multiple Clay instances

    main

    You can run multiple independent Clay instances in one program by managing Clay_Context pointers.

    1. Call Clay_Initialize for each instance to get a Clay_Context*.
    2. Before executing layout commands for a specific instance, call Clay_SetCurrentContext(context).

    Warning: Clay does not support multi-threading. Do not attempt to render different instances across different threads simultaneously.

    Clay_Context* instance1 = Clay_Initialize(arena1, layoutDimensions, errorHandler);
    Clay_Context* instance2 = Clay_Initialize(arena2, layoutDimensions, errorHandler);
    
    // Use instance 1
    Clay_SetCurrentContext(instance1);
    Clay_BeginLayout();
    // ... layout code ...
    Clay_RenderCommandArray renderCommands1 = Clay_EndLayout(deltaTime);
    
    // Use instance 2
    Clay_SetCurrentContext(instance2);
    Clay_BeginLayout();
    // ... layout code ...
    Clay_RenderCommandArray renderCommands2 = Clay_EndLayout(deltaTime);
  10. Build the Playdate console example for the simulator

    main

    To build the Playdate console example for use with the Playdate simulator, you must have the Playdate SDK installed. When initializing your CMake directory, you must enable the Playdate examples using the -DCLAY_INCLUDE_PLAYDATE_EXAMPLES=ON flag.

    Follow these steps:

    1. Initialize the build directory with the Playdate example flag.
    2. Build the project using CMake.

    The resulting .pdx file will be located at examples/playdate-project-example/clay_playdate_example.pdx.

    cmake -DCLAY_INCLUDE_PLAYDATE_EXAMPLES=ON cmake-build-debug
    cmake --build cmake-build-debug
  11. Build UI hierarchies with the CLAY macro

    main

    Clay UIs are constructed using the CLAY(id, { configuration }) macro. This macro creates an element in the hierarchy and supports nesting, similar to HTML. Child elements are added by opening a block {} after the macro call.

    Because Clay is pure C, you can use standard C control flow (loops, if statements, etc.) directly within your layout declarations to create dynamic UIs or reusable 'components' via functions.

    // Parent element with 8px of padding
    CLAY(CLAY_ID("parent"), { .layout = { .padding = CLAY_PADDING_ALL(8) } }) {
        // Child element 1
        CLAY_TEXT(CLAY_STRING("Hello World"), { .fontSize = 16 });
        // Child element 2 with red background
        CLAY((CLAY_ID("child"), { .backgroundColor = COLOR_RED }) {
            // etc
        }
    }
    
    // Re-usable components are just functions
    void ButtonComponent(Clay_String buttonText) {
        CLAY_AUTO_ID({ .layout = { .padding = CLAY_PADDING_ALL(8) }, .backgroundColor = COLOR_RED }) {
            CLAY_TEXT(buttonText, textConfig);
        }
    }