Skip to content

[Core] Nested clip elements behave incorrectly #466

@ambareesh1510

Description

@ambareesh1510

When an element X has the clip property set and contains elements that also have the clip property, X fails to clip its contents properly.

Minimal reproducible example and pictures:
With the Raylib renderer (modified from the `raylib-sidebar-scrolling-container` example):
    Clay_BeginLayout();
    CLAY({
        .layout = {
            .sizing = {
                .width = CLAY_SIZING_GROW(0),
                .height = CLAY_SIZING_GROW(0),
            },
            .padding = { 16, 16, 16, 16 },
            .childGap = 16,
            .layoutDirection = CLAY_TOP_TO_BOTTOM,
            .childAlignment = {
                .y = CLAY_ALIGN_X_CENTER,
            },
        },
        .backgroundColor = { 0, 0, 0, 255 },
    }) {
        CLAY({
            .layout = {
                .sizing = {
                    .width = CLAY_SIZING_GROW(0),
                    .height = CLAY_SIZING_PERCENT(0.5),
                },
                .padding = { 16, 16, 16, 16 },
                .childGap = 16,
                .layoutDirection = CLAY_TOP_TO_BOTTOM,
            },
            .clip = {
                .vertical = true,
                .childOffset = Clay_GetScrollOffset(),
            },
            .backgroundColor = {50, 50, 50, 255}
        }) {
            for (size_t i = 0; i < 4; i++) {
                CLAY({
                    .clip = {
                        .horizontal = true,
                        .childOffset = Clay_GetScrollOffset(),
                    },
                    .layout = {
                        .sizing = {
                            .height = CLAY_SIZING_PERCENT(0.4),
                        },
                    },
                }) {
                    CLAY_TEXT(
                        CLAY_STRING("long text long text long text long text long text long text long text long text long text long text long text long text"),
                        CLAY_TEXT_CONFIG({
                            .fontId = 0,
                            .letterSpacing = 5,
                            .fontSize = 26,
                            .textColor = {255,255,255,255}
                        })
                    );
                }
            }
        }
    }
    return Clay_EndLayout();
Image

To the best of my knowledge, this behavior is a consequence of three issues:

  1. When the Raylib renderer encounters two SCISSOR_START commands without a SCISSOR_END in between, it overwrites the clip bounding box from the first SCISSOR_START with the one from the second SCISSOR_START, and the SCISSOR_END corresponding to the second SCISSOR_START disables all clipping. This causes elements rendered after the first SCISSOR_END but before the second one to not be clipped. (I fixed this by maintaining a stack of bounding boxes inside the renderer; a SCISSOR_START causes the corresponding bounding box to be pushed onto the stack, and a SCISSOR_END pops an element off the stack and restores clipping corresponding to the bounding box that is now on the top of the stack.)
  2. If a clip element A contains another clip element B, it is possible for the bounding box provided in the SCISSOR_START command for element B to not be contained within the bounding box provided in the SCISSOR_START for element A. (This caused problems for me because naively calling Raylib's BeginScissorMode() with the bounding box provided by the command fails to account for the encompassing clip elements. To fix this, the bounding box of a new SCISSOR_START command should itself be clipped according to the bounding box of the top element of the stack.)
  3. When a clip element is off screen, neither the SCISSOR_START nor the SCISSOR_END for that element should be generated, but in reality the SCISSOR_END command alone is generated. This violates the invariant that a SCISSOR_END always has an accompanying SCISSOR_START (and puts the bounding box stack in an invalid state if it is implemented as described in (1)).

On a somewhat related note, the maximum number of scroll elements seems to be (somewhat arbitrarily) limited to 10.

I have my own fixes for all of these issues and I'm happy to make a PR, but I'd like to clarify some architectural questions first. Issue (3) is clearly a problem with the Clay layout algorithm itself, but I implemented fixes for (1) and (2) directly in the renderer. Is this the preferred separation of responsibilities? I can certainly see an argument for implementing the fixes directly in the layout algorithm, since the "correct" behavior seems pretty universally desirable.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions