<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Clay - UI Layout Library</title>
    <style>
        html,
        body {
            width: 100%;
            height: 100%;
            overflow: hidden;
            padding: 0;
            margin: 0;
            pointer-events: none;
        }

        @font-face {
            font-family: 'Calistoga';
            font-style: normal;
            font-weight: 400;
            src: url('/fonts/Calistoga-Regular.ttf') format('truetype');
        }

        @font-face {
            font-family: 'Quicksand';
            font-style: normal;
            font-weight: 400;
            src: url('/fonts/Quicksand-Semibold.ttf') format('truetype');
        }

        body>canvas {
            width: 100%;
            height: 100%;
        }
    </style>
</head>
<script type="module">
    const CLAY_RENDER_COMMAND_TYPE_NONE = 0;
    const CLAY_RENDER_COMMAND_TYPE_RECTANGLE = 1;
    const CLAY_RENDER_COMMAND_TYPE_BORDER = 2;
    const CLAY_RENDER_COMMAND_TYPE_TEXT = 3;
    const CLAY_RENDER_COMMAND_TYPE_IMAGE = 4;
    const CLAY_RENDER_COMMAND_TYPE_SCISSOR_START = 5;
    const CLAY_RENDER_COMMAND_TYPE_SCISSOR_END = 6;
    const CLAY_RENDER_COMMAND_TYPE_CUSTOM = 7;
    const GLOBAL_FONT_SCALING_FACTOR = 0.8;
    let renderCommandSize = 0;
    let scratchSpaceAddress = 0;
    let heapSpaceAddress = 0;
    let memoryDataView;
    let textDecoder = new TextDecoder("utf-8");
    let previousFrameTime;
    let fontsById = [
        // YOUR FONTS HERE
    ];
    let elementCache = {};
    let imageCache = {};
    let colorDefinition = {
        type: 'struct', members: [
            { name: 'r', type: 'float' },
            { name: 'g', type: 'float' },
            { name: 'b', type: 'float' },
            { name: 'a', type: 'float' },
        ]
    };
    let stringDefinition = {
        type: 'struct', members: [
            { name: 'length', type: 'uint32_t' },
            { name: 'chars', type: 'uint32_t' },
        ]
    };
    let borderDefinition = {
        type: 'struct', members: [
            { name: 'width', type: 'uint32_t' },
            { name: 'color', ...colorDefinition },
        ]
    };
    let cornerRadiusDefinition = {
        type: 'struct', members: [
            { name: 'topLeft', type: 'float' },
            { name: 'topRight', type: 'float' },
            { name: 'bottomLeft', type: 'float' },
            { name: 'bottomRight', type: 'float' },
        ]
    };
    let rectangleConfigDefinition = {
        name: 'rectangle', type: 'struct', members: [
            { name: 'color', ...colorDefinition },
            { name: 'cornerRadius', ...cornerRadiusDefinition },
            { name: 'link', ...stringDefinition },
            { name: 'cursorPointer', type: 'uint8_t' },
        ]
    };
    let borderConfigDefinition = {
        name: 'text', type: 'struct', members: [
            { name: 'left', ...borderDefinition },
            { name: 'right', ...borderDefinition },
            { name: 'top', ...borderDefinition },
            { name: 'bottom', ...borderDefinition },
            { name: 'betweenChildren', ...borderDefinition },
            { name: 'cornerRadius', ...cornerRadiusDefinition }
        ]
    };
    let textConfigDefinition = {
        name: 'text', type: 'struct', members: [
            { name: 'textColor', ...colorDefinition },
            { name: 'fontId', type: 'uint16_t' },
            { name: 'fontSize', type: 'uint16_t' },
            { name: 'letterSpacing', type: 'uint16_t' },
            { name: 'lineHeight', type: 'uint16_t' },
            { name: 'wrapMode', type: 'uint32_t' },
            { name: 'disablePointerEvents', type: 'uint8_t' }
        ]
    };
    let imageConfigDefinition = {
        name: 'image', type: 'struct', members: [
            { name: 'imageData', type: 'uint32_t' },
            {
                name: 'sourceDimensions', type: 'struct', members: [
                    { name: 'width', type: 'float' },
                    { name: 'height', type: 'float' },
                ]
            },
            { name: 'sourceURL', ...stringDefinition }
        ]
    };
    let customConfigDefinition = {
        name: 'custom', type: 'struct', members: [
            { name: 'customData', type: 'uint32_t' },
        ]
    }
    let renderCommandDefinition = {
        name: 'CLay_RenderCommand',
        type: 'struct',
        members: [
            {
                name: 'boundingBox', type: 'struct', members: [
                    { name: 'x', type: 'float' },
                    { name: 'y', type: 'float' },
                    { name: 'width', type: 'float' },
                    { name: 'height', type: 'float' },
                ]
            },
            { name: 'config', type: 'uint32_t' },
            { name: 'text', ...stringDefinition },
            { name: 'id', type: 'uint32_t' },
            { name: 'commandType', type: 'uint32_t', },
        ]
    };

    function getStructTotalSize(definition) {
        switch (definition.type) {
            case 'union':
            case 'struct': {
                let totalSize = 0;
                for (const member of definition.members) {
                    let result = getStructTotalSize(member);
                    if (definition.type === 'struct') {
                        totalSize += result;
                    } else {
                        totalSize = Math.max(totalSize, result);
                    }
                }
                return totalSize;
            }
            case 'float': return 4;
            case 'uint32_t': return 4;
            case 'uint16_t': return 2;
            case 'uint8_t': return 1;
            default: {
                throw "Unimplemented C data type " + definition.type
            }
        }
    }

    function readStructAtAddress(address, definition) {
        switch (definition.type) {
            case 'union':
            case 'struct': {
                let struct = { __size: 0 };
                for (const member of definition.members) {
                    let result = readStructAtAddress(address, member);
                    struct[member.name] = result;
                    if (definition.type === 'struct') {
                        struct.__size += result.__size;
                        address += result.__size;
                    } else {
                        struct.__size = Math.max(struct.__size, result.__size);
                    }
                }
                return struct;
            }
            case 'float': return { value: memoryDataView.getFloat32(address, true), __size: 4 };
            case 'uint32_t': return { value: memoryDataView.getUint32(address, true), __size: 4 };
            case 'uint16_t': return { value: memoryDataView.getUint16(address, true), __size: 2 };
            case 'uint8_t': return { value: memoryDataView.getUint16(address, true), __size: 1 };
            default: {
                throw "Unimplemented C data type " + definition.type
            }
        }
    }

    function getTextDimensions(text, font) {
        // re-use canvas object for better performance
        window.canvasContext.font = font;
        let metrics = window.canvasContext.measureText(text);
        return { width: metrics.width, height: metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent };
    }

    function createMainArena(arenaStructAddress, arenaMemoryAddress) {
        let memorySize = instance.exports.Clay_MinMemorySize();
        // Last arg is address to store return value
        instance.exports.Clay_CreateArenaWithCapacityAndMemory(arenaStructAddress, memorySize, arenaMemoryAddress);
    }
    async function init() {
        await Promise.all(fontsById.map(f => document.fonts.load(`12px "${f}"`)));
        window.htmlRoot = document.body.appendChild(document.createElement('div'));
        window.canvasRoot = document.body.appendChild(document.createElement('canvas'));
        window.canvasContext = window.canvasRoot.getContext("2d");
        window.mousePositionXThisFrame = 0;
        window.mousePositionYThisFrame = 0;
        window.mouseWheelXThisFrame = 0;
        window.mouseWheelYThisFrame = 0;
        window.touchDown = false;
        let zeroTimeout = null;
        addEventListener("wheel", (event) => {
            window.mouseWheelXThisFrame = event.deltaX * -0.1;
            window.mouseWheelYThisFrame = event.deltaY * -0.1;
            clearTimeout(zeroTimeout);
            zeroTimeout = setTimeout(() => {
                window.mouseWheelXThisFrame = 0;
                window.mouseWheelYThisFrame = 0;
            }, 10);
        });

        function handleTouch(event) {
            if (event.touches.length === 1) {
                window.touchDown = true;
                window.mousePositionXThisFrame = event.changedTouches[0].pageX;
                window.mousePositionYThisFrame = event.changedTouches[0].pageY;
            }
        }

        document.addEventListener("touchstart", handleTouch);
        document.addEventListener("touchmove", handleTouch);
        document.addEventListener("touchend", () => {
            window.touchDown = false;
            window.mousePositionXThisFrame = 0;
            window.mousePositionYThisFrame = 0;
        })

        document.addEventListener("mousemove", (event) => {
            window.mousePositionXThisFrame = event.x;
            window.mousePositionYThisFrame = event.y;
        });

        document.addEventListener("mousedown", (event) => {
            window.mouseDown = true;
        });

        document.addEventListener("keydown", (event) => {
            if (event.key === "ArrowDown") {
                window.arrowKeyDownPressedThisFrame = true;
            }
            if (event.key === "ArrowUp") {
                window.arrowKeyUpPressedThisFrame = true;
            }
            if (event.key === "d") {
                window.dKeyPressedThisFrame = true;
            }
        });

        const importObject = {
            clay: {
                measureTextFunction: (addressOfDimensions, textToMeasure, addressOfConfig) => {
                    let stringLength = memoryDataView.getUint32(textToMeasure, true);
                    let pointerToString = memoryDataView.getUint32(textToMeasure + 4, true);
                    let textConfig = readStructAtAddress(addressOfConfig, textConfigDefinition);
                    let textDecoder = new TextDecoder("utf-8");
                    let text = textDecoder.decode(memoryDataView.buffer.slice(pointerToString, pointerToString + stringLength));
                    let sourceDimensions = getTextDimensions(text, `${Math.round(textConfig.fontSize.value * GLOBAL_FONT_SCALING_FACTOR)}px ${fontsById[textConfig.fontId.value]}`);
                    memoryDataView.setFloat32(addressOfDimensions, sourceDimensions.width, true);
                    memoryDataView.setFloat32(addressOfDimensions + 4, sourceDimensions.height, true);
                }
            },
        };
        const { instance } = await WebAssembly.instantiateStreaming(
            fetch("./index.wasm"), importObject
        );
        memoryDataView = new DataView(new Uint8Array(instance.exports.memory.buffer).buffer);
        scratchSpaceAddress = instance.exports.__heap_base.value;
        heapSpaceAddress = instance.exports.__heap_base.value + 1024;
        let arenaAddress = scratchSpaceAddress;
        window.instance = instance;
        createMainArena(arenaAddress, heapSpaceAddress);
        instance.exports.Clay_Initialize(arenaAddress);
        renderCommandSize = getStructTotalSize(renderCommandDefinition);
        renderLoop();
    }

    function renderLoopCanvas() {
    // Note: Rendering to canvas needs to be scaled up by window.devicePixelRatio in both width and height.
    // e.g. if we're working on a device where devicePixelRatio is 2, we need to render
    // everything at width^2 x height^2 resolution, then scale back down with css to get the correct pixel density.
        let capacity = memoryDataView.getUint32(scratchSpaceAddress, true);
        let length = memoryDataView.getUint32(scratchSpaceAddress + 4, true);
        let arrayOffset = memoryDataView.getUint32(scratchSpaceAddress + 8, true);
        window.canvasRoot.width = window.innerWidth * window.devicePixelRatio;
        window.canvasRoot.height = window.innerHeight * window.devicePixelRatio;
        window.canvasRoot.style.width = window.innerWidth + 'px';
        window.canvasRoot.style.height = window.innerHeight + 'px';
        let ctx = window.canvasContext;
        let scale = window.devicePixelRatio;
        for (let i = 0; i < length; i++, arrayOffset += renderCommandSize) {
            let renderCommand = readStructAtAddress(arrayOffset, renderCommandDefinition);
            let boundingBox = renderCommand.boundingBox;
            
            // note: commandType is packed to uint8_t and has 3 garbage bytes of padding
            switch(renderCommand.commandType.value & 0xff) {
                case (CLAY_RENDER_COMMAND_TYPE_NONE): {
                    break;
                }
                case (CLAY_RENDER_COMMAND_TYPE_RECTANGLE): {
                    let config = readStructAtAddress(renderCommand.config.value, rectangleConfigDefinition);
                    let color = config.color;
                    ctx.beginPath();
                    window.canvasContext.fillStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                    window.canvasContext.roundRect(
                        boundingBox.x.value * scale, // x
                        boundingBox.y.value * scale, // y
                        boundingBox.width.value * scale, // width
                        boundingBox.height.value * scale,
                        [config.cornerRadius.topLeft.value * scale, config.cornerRadius.topRight.value * scale, config.cornerRadius.bottomRight.value * scale, config.cornerRadius.bottomLeft.value * scale]) // height;
                    ctx.fill();
                    ctx.closePath();
                    // Handle link clicks
                    let linkContents = config.link.length.value > 0 ? textDecoder.decode(new Uint8Array(memoryDataView.buffer.slice(config.link.chars.value, config.link.chars.value + config.link.length.value))) : 0;
                    memoryDataView.setUint32(0, renderCommand.id.value, true);
                    if (linkContents.length > 0 && (window.mouseDownThisFrame || window.touchDown) && instance.exports.Clay_PointerOver(0)) {
                        window.location.href = linkContents;
                    }
                    break;
                }
                case (CLAY_RENDER_COMMAND_TYPE_BORDER): {
                    let config = readStructAtAddress(renderCommand.config.value, borderConfigDefinition);
                    ctx.beginPath();
                    ctx.moveTo(boundingBox.x.value * scale, boundingBox.y.value * scale);
                    // Top Left Corner
                    if (config.cornerRadius.topLeft.value > 0) {
                        let lineWidth = config.top.width.value;
                        let halfLineWidth = lineWidth / 2;
                        ctx.moveTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + config.cornerRadius.topLeft.value + halfLineWidth) * scale);
                        let color = config.top.color;
                        ctx.lineWidth = lineWidth * scale;
                        ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                        ctx.arcTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale, (boundingBox.x.value + config.cornerRadius.topLeft.value + halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale, config.cornerRadius.topLeft.value * scale);
                        ctx.stroke();
                    }
                    // Top border
                    if (config.top.width.value > 0) {
                        let lineWidth = config.top.width.value;
                        let halfLineWidth = lineWidth / 2;
                        let color = config.top.color;
                        ctx.lineWidth = lineWidth * scale;
                        ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                        ctx.moveTo((boundingBox.x.value + config.cornerRadius.topLeft.value + halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale);
                        ctx.lineTo((boundingBox.x.value + boundingBox.width.value - config.cornerRadius.topRight.value - halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale);
                        ctx.stroke();
                    }
                    // Top Right Corner
                    if (config.cornerRadius.topRight.value > 0) {
                        let lineWidth = config.top.width.value;
                        let halfLineWidth = lineWidth / 2;
                        ctx.moveTo((boundingBox.x.value + boundingBox.width.value - config.cornerRadius.topRight.value - halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale);
                        let color = config.top.color;
                        ctx.lineWidth = lineWidth * scale;
                        ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                        ctx.arcTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + halfLineWidth) * scale, (boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + config.cornerRadius.topRight.value + halfLineWidth) * scale, config.cornerRadius.topRight.value * scale);
                        ctx.stroke();
                    }
                    // Right border
                    if (config.right.width.value > 0) {
                        let color = config.right.color;
                        let lineWidth = config.right.width.value;
                        let halfLineWidth = lineWidth / 2;
                        ctx.lineWidth = lineWidth * scale;
                        ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                        ctx.moveTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + config.cornerRadius.topRight.value + halfLineWidth) * scale);
                        ctx.lineTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - config.cornerRadius.topRight.value - halfLineWidth) * scale);
                        ctx.stroke();
                    }
                    // Bottom Right Corner
                    if (config.cornerRadius.bottomRight.value > 0) {
                        let color = config.top.color;
                        let lineWidth = config.top.width.value;
                        let halfLineWidth = lineWidth / 2;
                        ctx.moveTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - config.cornerRadius.bottomRight.value - halfLineWidth) * scale);
                        ctx.lineWidth = lineWidth * scale;
                        ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                        ctx.arcTo((boundingBox.x.value + boundingBox.width.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale, (boundingBox.x.value + boundingBox.width.value - config.cornerRadius.bottomRight.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale, config.cornerRadius.bottomRight.value * scale);
                        ctx.stroke();
                    }
                    // Bottom Border
                    if (config.bottom.width.value > 0) {
                        let color = config.bottom.color;
                        let lineWidth = config.bottom.width.value;
                        let halfLineWidth = lineWidth / 2;
                        ctx.lineWidth = lineWidth * scale;
                        ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                        ctx.moveTo((boundingBox.x.value + config.cornerRadius.bottomLeft.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale);
                        ctx.lineTo((boundingBox.x.value + boundingBox.width.value - config.cornerRadius.bottomRight.value - halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale);
                        ctx.stroke();
                    }
                    // Bottom Left Corner
                    if (config.cornerRadius.bottomLeft.value > 0) {
                        let color = config.bottom.color;
                        let lineWidth = config.bottom.width.value;
                        let halfLineWidth = lineWidth / 2;
                        ctx.moveTo((boundingBox.x.value + config.cornerRadius.bottomLeft.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale);
                        ctx.lineWidth = lineWidth * scale;
                        ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                        ctx.arcTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - halfLineWidth) * scale, (boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - config.cornerRadius.bottomLeft.value - halfLineWidth) * scale, config.cornerRadius.bottomLeft.value * scale);
                        ctx.stroke();
                    }
                    // Left Border
                    if (config.left.width.value > 0) {
                        let color = config.left.color;
                        let lineWidth = config.left.width.value;
                        let halfLineWidth = lineWidth / 2;
                        ctx.lineWidth = lineWidth * scale;
                        ctx.strokeStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                        ctx.moveTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + boundingBox.height.value - config.cornerRadius.bottomLeft.value - halfLineWidth) * scale);
                        ctx.lineTo((boundingBox.x.value + halfLineWidth) * scale, (boundingBox.y.value + config.cornerRadius.bottomRight.value + halfLineWidth) * scale);
                        ctx.stroke();
                    }
                    ctx.closePath();
                    break;
                }
                case (CLAY_RENDER_COMMAND_TYPE_TEXT): {
                    let config = readStructAtAddress(renderCommand.config.value, textConfigDefinition);
                    let textContents = renderCommand.text;
                    let stringContents = new Uint8Array(memoryDataView.buffer.slice(textContents.chars.value, textContents.chars.value + textContents.length.value));
                    let fontSize = config.fontSize.value * GLOBAL_FONT_SCALING_FACTOR * scale;
                    ctx.font = `${fontSize}px ${fontsById[config.fontId.value]}`;
                    let color = config.textColor;
                    ctx.textBaseline = 'middle';
                    ctx.fillStyle = `rgba(${color.r.value}, ${color.g.value}, ${color.b.value}, ${color.a.value / 255})`;
                    ctx.fillText(textDecoder.decode(stringContents), boundingBox.x.value * scale, (boundingBox.y.value + boundingBox.height.value / 2 + 1) * scale);
                    break;
                }
                case (CLAY_RENDER_COMMAND_TYPE_SCISSOR_START): {
                    window.canvasContext.save();
                    window.canvasContext.beginPath();
                    window.canvasContext.rect(boundingBox.x.value * scale, boundingBox.y.value * scale, boundingBox.width.value * scale, boundingBox.height.value * scale);
                    window.canvasContext.clip();
                    window.canvasContext.closePath();
                    break;
                }
                case (CLAY_RENDER_COMMAND_TYPE_SCISSOR_END): {
                    window.canvasContext.restore();
                    break;
                }
                case (CLAY_RENDER_COMMAND_TYPE_IMAGE): {
                    let config = readStructAtAddress(renderCommand.config.value, imageConfigDefinition);
                    let src = textDecoder.decode(new Uint8Array(memoryDataView.buffer.slice(config.sourceURL.chars.value, config.sourceURL.chars.value + config.sourceURL.length.value)));
                    if (!imageCache[src]) {
                        imageCache[src] = {
                            image: new Image(),
                            loaded: false,
                        }
                        imageCache[src].image.onload = () => imageCache[src].loaded = true;
                        imageCache[src].image.src = src;
                    } else if (imageCache[src].loaded) {
                        ctx.drawImage(imageCache[src].image, boundingBox.x.value * scale, boundingBox.y.value * scale, boundingBox.width.value * scale, boundingBox.height.value * scale);
                    }
                    break;
                }
                case (CLAY_RENDER_COMMAND_TYPE_CUSTOM): break;
            }
        }
    }

    function renderLoop(currentTime) {
        const elapsed = currentTime - previousFrameTime;
        previousFrameTime = currentTime;
        instance.exports.UpdateDrawFrame(scratchSpaceAddress, window.innerWidth, window.innerHeight, window.mouseWheelXThisFrame, window.mouseWheelYThisFrame, window.mousePositionXThisFrame, window.mousePositionYThisFrame, window.touchDown, window.mouseDown, elapsed / 1000);
        renderLoopCanvas();
        requestAnimationFrame(renderLoop);
        window.mouseDown = false;
    }
    init();
</script>

<body>
</body>

</html>