thi-ng/umbrella

View on GitHub
packages/imgui/src/components/dropdown.ts

Summary

Maintainability
A
0 mins
Test Coverage
import type { Maybe } from "@thi.ng/api";
import { polygon } from "@thi.ng/geom/polygon";
import { isLayout } from "@thi.ng/layout/checks";
import { gridLayout } from "@thi.ng/layout/grid-layout";
import { clamp0 } from "@thi.ng/math/interval";
import { hash } from "@thi.ng/vectors/hash";
import { Key, type ComponentOpts } from "../api.js";
import type { IMGUI } from "../gui.js";
import { buttonH } from "./button.js";

export interface DropDownOpts extends Omit<ComponentOpts, "label"> {
    /**
     * Index of selected item.
     */
    value: number;
    items: string[];
    label: string;
}

export const dropdown = ({
    gui,
    layout,
    id,
    value,
    items,
    label,
    info,
}: DropDownOpts) => {
    const open = gui.state<boolean>(id, () => false);
    const nested = isLayout(layout)
        ? layout.nest(1, [1, open ? items.length : 1])
        : gridLayout(layout.x, layout.y, layout.w, 1, layout.ch, layout.gap);
    let res: Maybe<number>;
    const box = nested.next();
    const { x, y, w, h } = box;
    const key = hash([x, y, w, h, ~~gui.disabled]);
    const tx = x + w - gui.theme.pad - 4;
    const ty = y + h / 2;
    const draw = gui.draw;
    if (open) {
        const bt = buttonH({
            gui,
            layout: box,
            id: `${id}-title`,
            label,
            info,
        });
        draw &&
            gui.add(
                gui.resource(id, key + 1, () => __triangle(gui, tx, ty, true))
            );
        if (bt) {
            gui.setState(id, false);
        } else {
            for (let i = 0, n = items.length; i < n; i++) {
                if (
                    buttonH({
                        gui,
                        layout: nested,
                        id: `${id}-${i}`,
                        label: items[i],
                    })
                ) {
                    i !== value && (res = i);
                    gui.setState(id, false);
                }
            }
            if (gui.focusID.startsWith(`${id}-`)) {
                switch (gui.key) {
                    case Key.ESC:
                        gui.setState(id, false);
                        break;
                    case Key.UP:
                        return __update(gui, id, clamp0(value - 1));
                    case Key.DOWN:
                        return __update(
                            gui,
                            id,
                            Math.min(items.length - 1, value + 1)
                        );
                    default:
                }
            }
        }
    } else {
        if (
            buttonH({
                gui,
                layout: box,
                id: `${id}-${value}`,
                label: items[value],
                labelHover: label,
                info,
            })
        ) {
            gui.setState(id, true);
        }
        draw &&
            gui.add(
                gui.resource(id, key + 2, () => __triangle(gui, tx, ty, false))
            );
    }
    return res;
};

/** @internal */
const __update = (gui: IMGUI, id: string, next: number) => {
    gui.focusID = `${id}-${next}`;
    return next;
};

/** @internal */
const __triangle = (gui: IMGUI, x: number, y: number, open: boolean) => {
    const s = open ? 2 : -2;
    return polygon(
        [
            [x - 4, y + s],
            [x + 4, y + s],
            [x, y - s],
        ],
        {
            fill: gui.textColor(false),
        }
    );
};