From 08a03cadc3d584a82424e6201eb60d2406cabb43 Mon Sep 17 00:00:00 2001 From: KonAki Date: Thu, 8 Oct 2026 16:54:47 +0800 Subject: Add konui, a small immediate mode ui --- kon/konui.h | 624 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 624 insertions(+) create mode 100644 kon/konui.h (limited to 'kon/konui.h') diff --git a/kon/konui.h b/kon/konui.h new file mode 100644 index 0000000..e03b7fe --- /dev/null +++ b/kon/konui.h @@ -0,0 +1,624 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konui.h + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef KONUI_H +#define KONUI_H + +#include +#include + +#include "konwinlib.h" +#include "konsofren.h" +#include "konfont.h" + +/*** definition ***/ + +typedef struct kon_ui kon_ui_t; + +typedef struct kon_uiTheme { + uint32_t panel, title, text, textDim, widget, hover, active, accent, border; + int padding, spacing; +} kon_uiTheme_t; + +/* you own this, the ui moves it when the title bar is dragged. w is required, h = 0 sizes to the content */ +typedef struct kon_uiPanel { + int x, y, w, h; + int collapsed; + int contentH; +} kon_uiPanel_t; + +kon_ui_t *kon_uiCreate(kon_font_t *font); +void kon_uiDestroy(kon_ui_t *ui); +kon_uiTheme_t *kon_uiTheme(kon_ui_t *ui); + +/* mouse coordinates have to be in the space of the framebuffer you draw to. use kon_windowToGame for the game one */ +void kon_uiFeedEvent(kon_ui_t *ui, const kon_event_t *event); + +void kon_uiBegin(kon_ui_t *ui, kon_framebuffer_t *fb); +void kon_uiEnd(kon_ui_t *ui); + +/* true when the ui is using the mouse or keyboard, so the game should leave them alone */ +int kon_uiWantsMouse(const kon_ui_t *ui); +int kon_uiWantsKeyboard(const kon_ui_t *ui); + +/* always call kon_uiEndPanel, even when this returned 0 (collapsed) */ +int kon_uiBeginPanel(kon_ui_t *ui, const char *title, kon_uiPanel_t *panel); +void kon_uiEndPanel(kon_ui_t *ui); + +/* anything after ## in a label is part of the id but not drawn, so "ok##a" and "ok##b" are different buttons */ +void kon_uiLabel(kon_ui_t *ui, const char *text); +void kon_uiLabelf(kon_ui_t *ui, const char *format, ...); +int kon_uiButton(kon_ui_t *ui, const char *label); +int kon_uiCheckbox(kon_ui_t *ui, const char *label, int *value); +int kon_uiSliderFloat(kon_ui_t *ui, const char *label, float *value, float min, float max); +int kon_uiSliderInt(kon_ui_t *ui, const char *label, int *value, int min, int max); +int kon_uiSelectable(kon_ui_t *ui, const char *label, int selected); + +/* returns 1 when enter is pressed. label is shown while the field is empty */ +int kon_uiTextInput(kon_ui_t *ui, const char *label, char *buffer, size_t size); + +void kon_uiSeparator(kon_ui_t *ui); +void kon_uiSameLine(kon_ui_t *ui); +void kon_uiPushId(kon_ui_t *ui, int id); +void kon_uiPopId(kon_ui_t *ui); + +kon_rect_t kon_uiLastRect(const kon_ui_t *ui); + +#endif + +/*** implementation ***/ + +#ifdef KONUI_IMPLEMENTATION + +#include +#include +#include +#include + +#define KON_UI_IDS 16 +#define KON_UI_TEXTQUEUE 32 + +struct kon_ui { + kon_font_t *font; + kon_framebuffer_t *fb; + kon_uiTheme_t theme; + int lineH, ascent; + + int mouseX, mouseY; + int mouseDown, mousePressed, mouseReleased; + int keyPressed[KON_KEY_COUNT]; + unsigned int text[KON_UI_TEXTQUEUE]; + int textCount; + + uint32_t active, focus; + uint32_t topPanel, topPanelNext; + int wantsMouse; + int focusKept; + int dragX, dragY; + + uint32_t ids[KON_UI_IDS]; + int idCount; + + kon_uiPanel_t *panel; + uint32_t panelId; + int cx, cy, availW; + int sameLine; + kon_rect_t last; + int cursor; +}; + +/*** private helpers ***/ + +static uint32_t kon_uiHash_(const kon_ui_t *ui, const char *s) +{ + uint32_t h = ui->idCount ? ui->ids[ui->idCount - 1] : 2166136261u; + + while (*s) { + h ^= (unsigned char)*s++; + h *= 16777619u; + } + return h ? h : 1; +} + +static size_t kon_uiVisibleLength_(const char *s) +{ + size_t n = 0; + + while (s[n]) { + if (s[n] == '#' && s[n + 1] == '#') break; + n++; + } + return n; +} + +static int kon_uiInside_(const kon_ui_t *ui, kon_rect_t r) +{ + return ui->mouseX >= r.x && ui->mouseX < r.x + r.w && ui->mouseY >= r.y && ui->mouseY < r.y + r.h; +} + +/* the mouse can only reach this widget if its panel is the topmost one under it */ +static int kon_uiReachable_(const kon_ui_t *ui) +{ + return ui->topPanel == ui->panelId; +} + +static int kon_uiTextWidth_(const kon_ui_t *ui, const char *s, size_t n) +{ + char buffer[256]; + int w = 0; + + if (n > sizeof(buffer) - 1) n = sizeof(buffer) - 1; + memcpy(buffer, s, n); + buffer[n] = 0; + kon_measureText(ui->font, buffer, &w, NULL); + return w; +} + +static void kon_uiDrawText_(const kon_ui_t *ui, int x, int y, const char *s, size_t n, uint32_t color) +{ + char buffer[256]; + + if (n > sizeof(buffer) - 1) n = sizeof(buffer) - 1; + memcpy(buffer, s, n); + buffer[n] = 0; + kon_drawText(ui->fb, ui->font, x, y + ui->ascent, buffer, color); +} + +static kon_rect_t kon_uiNextRect_(kon_ui_t *ui, int w, int h) +{ + kon_rect_t r; + + if (ui->sameLine) { + r.x = ui->last.x + ui->last.w + ui->theme.spacing; + r.y = ui->last.y; + } else { + r.x = ui->cx; + r.y = ui->cy; + } + r.w = w > 0 ? w : ui->availW; + r.h = h; + + if (!ui->sameLine) ui->cy += h + ui->theme.spacing; + ui->sameLine = 0; + ui->last = r; + return r; +} + +static void kon_uiFill_(const kon_ui_t *ui, kon_rect_t r, uint32_t color) +{ + kon_fillRectangle(ui->fb, r.x, r.y, r.w, r.h, color); +} + +static void kon_uiOutline_(const kon_ui_t *ui, kon_rect_t r, uint32_t color) +{ + kon_drawRectangle(ui->fb, r.x, r.y, r.w - 1, r.h - 1, color); +} + +/* hover and press handling shared by everything clickable. returns 1 when it was clicked */ +static int kon_uiBehave_(kon_ui_t *ui, uint32_t id, kon_rect_t r, int *hovered) +{ + int over = kon_uiReachable_(ui) && kon_uiInside_(ui, r) && (ui->active == 0 || ui->active == id); + int clicked = 0; + + if (over && ui->mousePressed) ui->active = id; + if (ui->active == id && ui->mouseReleased) { + clicked = over; + ui->active = 0; + } + if (hovered) *hovered = over; + return clicked; +} + +static uint32_t kon_uiWidgetColor_(const kon_ui_t *ui, uint32_t id, int hovered) +{ + if (ui->active == id) return ui->theme.active; + return hovered ? ui->theme.hover : ui->theme.widget; +} + +/*** ui ***/ + +kon_ui_t *kon_uiCreate(kon_font_t *font) +{ + kon_ui_t *ui; + int h = 0; + + if (!font) return NULL; + ui = calloc(1, sizeof(kon_ui_t)); + if (!ui) return NULL; + + ui->font = font; + kon_measureText(font, "A", NULL, &h); + ui->ascent = h * 4 / 5; + ui->lineH = h + 4; + + ui->theme.panel = 0xE0202028; + ui->theme.title = 0xFF383848; + ui->theme.text = 0xFFE8E8F0; + ui->theme.textDim = 0xFF8888A0; + ui->theme.widget = 0xFF404058; + ui->theme.hover = 0xFF505070; + ui->theme.active = 0xFF6060A0; + ui->theme.accent = 0xFF7088E0; + ui->theme.border = 0xFF101018; + ui->theme.padding = 4; + ui->theme.spacing = 3; + return ui; +} + +void kon_uiDestroy(kon_ui_t *ui) +{ + free(ui); +} + +kon_uiTheme_t *kon_uiTheme(kon_ui_t *ui) +{ + return &ui->theme; +} + +void kon_uiFeedEvent(kon_ui_t *ui, const kon_event_t *event) +{ + switch (event->type) { + case KON_EVENT_MOUSE_MOVE: + ui->mouseX = event->mouseX; + ui->mouseY = event->mouseY; + break; + case KON_EVENT_MOUSE_DOWN: + ui->mouseX = event->mouseX; + ui->mouseY = event->mouseY; + if (event->mouseButton == KON_MOUSE_LEFT) { + ui->mouseDown = 1; + ui->mousePressed = 1; + } + break; + case KON_EVENT_MOUSE_UP: + ui->mouseX = event->mouseX; + ui->mouseY = event->mouseY; + if (event->mouseButton == KON_MOUSE_LEFT) { + ui->mouseDown = 0; + ui->mouseReleased = 1; + } + break; + case KON_EVENT_KEY_DOWN: + if (event->key >= 0 && event->key < KON_KEY_COUNT) ui->keyPressed[event->key] = 1; + break; + case KON_EVENT_TEXT: + if (ui->textCount < KON_UI_TEXTQUEUE) ui->text[ui->textCount++] = event->codepoint; + break; + default: + break; + } +} + +void kon_uiBegin(kon_ui_t *ui, kon_framebuffer_t *fb) +{ + ui->fb = fb; + ui->topPanel = ui->topPanelNext; + ui->topPanelNext = 0; + ui->focusKept = 0; + ui->panel = NULL; + ui->panelId = 0; + ui->idCount = 0; + ui->sameLine = 0; + ui->cx = ui->theme.padding; + ui->cy = ui->theme.padding; + ui->availW = fb->width - ui->theme.padding * 2; +} + +void kon_uiEnd(kon_ui_t *ui) +{ + if (ui->mousePressed && !ui->focusKept) ui->focus = 0; + if (!ui->mouseDown && ui->active) ui->active = 0; + + ui->wantsMouse = ui->topPanelNext != 0 || ui->active != 0; + + ui->mousePressed = 0; + ui->mouseReleased = 0; + memset(ui->keyPressed, 0, sizeof(ui->keyPressed)); + ui->textCount = 0; + ui->fb = NULL; +} + +int kon_uiWantsMouse(const kon_ui_t *ui) +{ + return ui->wantsMouse; +} + +int kon_uiWantsKeyboard(const kon_ui_t *ui) +{ + return ui->focus != 0; +} + +void kon_uiPushId(kon_ui_t *ui, int id) +{ + uint32_t h = ui->idCount ? ui->ids[ui->idCount - 1] : 2166136261u; + + if (ui->idCount == KON_UI_IDS) return; + h ^= (uint32_t)id; + h *= 16777619u; + ui->ids[ui->idCount++] = h; +} + +void kon_uiPopId(kon_ui_t *ui) +{ + if (ui->idCount) ui->idCount--; +} + +kon_rect_t kon_uiLastRect(const kon_ui_t *ui) +{ + return ui->last; +} + +/*** panels ***/ + +int kon_uiBeginPanel(kon_ui_t *ui, const char *title, kon_uiPanel_t *panel) +{ + uint32_t id = kon_uiHash_(ui, title); + uint32_t dragId = id ^ 0x9E3779B9u; + int pad = ui->theme.padding, titleH = ui->lineH + pad; + kon_rect_t whole, bar, arrow; + int h, hovered; + + if (ui->panel) return 0; + if (panel->w <= 0) panel->w = 220; + + h = panel->collapsed ? titleH : (panel->h > 0 ? panel->h : panel->contentH); + if (h < titleH) h = titleH; + + whole = KON_RECT(panel->x, panel->y, panel->w, h); + bar = KON_RECT(panel->x, panel->y, panel->w, titleH); + arrow = KON_RECT(panel->x, panel->y, titleH, titleH); + + if (kon_uiInside_(ui, whole)) ui->topPanelNext = id; + + ui->panelId = id; + ui->panel = panel; + + if (kon_uiBehave_(ui, id ^ 1u, arrow, &hovered)) panel->collapsed = !panel->collapsed; + + if (ui->active == 0 && ui->mousePressed && kon_uiReachable_(ui) && kon_uiInside_(ui, bar) && !kon_uiInside_(ui, arrow)) { + ui->active = dragId; + ui->dragX = ui->mouseX - panel->x; + ui->dragY = ui->mouseY - panel->y; + } + if (ui->active == dragId) { + panel->x = ui->mouseX - ui->dragX; + panel->y = ui->mouseY - ui->dragY; + if (panel->x < 40 - panel->w) panel->x = 40 - panel->w; + if (panel->x > ui->fb->width - 40) panel->x = ui->fb->width - 40; + if (panel->y < 0) panel->y = 0; + if (panel->y > ui->fb->height - titleH) panel->y = ui->fb->height - titleH; + if (ui->mouseReleased) ui->active = 0; + whole.x = bar.x = arrow.x = panel->x; + whole.y = bar.y = arrow.y = panel->y; + } + + kon_uiFill_(ui, whole, ui->theme.panel); + kon_uiFill_(ui, bar, ui->theme.title); + kon_uiOutline_(ui, whole, ui->theme.border); + kon_uiDrawText_(ui, arrow.x + pad, arrow.y + pad / 2, panel->collapsed ? "+" : "-", 1, ui->theme.textDim); + kon_uiDrawText_(ui, panel->x + titleH, panel->y + pad / 2, title, kon_uiVisibleLength_(title), ui->theme.text); + + ui->cx = panel->x + pad; + ui->cy = panel->y + titleH + pad; + ui->availW = panel->w - pad * 2; + ui->sameLine = 0; + return !panel->collapsed; +} + +void kon_uiEndPanel(kon_ui_t *ui) +{ + if (!ui->panel) return; + ui->panel->contentH = ui->cy - ui->panel->y + ui->theme.padding - ui->theme.spacing; + ui->panel = NULL; + ui->panelId = 0; + ui->cx = ui->theme.padding; + ui->cy = ui->theme.padding; + ui->availW = ui->fb->width - ui->theme.padding * 2; +} + +/*** widgets ***/ + +void kon_uiLabel(kon_ui_t *ui, const char *text) +{ + size_t n = kon_uiVisibleLength_(text); + kon_rect_t r = kon_uiNextRect_(ui, kon_uiTextWidth_(ui, text, n) + 1, ui->lineH); + + kon_uiDrawText_(ui, r.x, r.y, text, n, ui->theme.text); +} + +void kon_uiLabelf(kon_ui_t *ui, const char *format, ...) +{ + char buffer[256]; + va_list args; + + va_start(args, format); + vsnprintf(buffer, sizeof(buffer), format, args); + va_end(args); + kon_uiLabel(ui, buffer); +} + +int kon_uiButton(kon_ui_t *ui, const char *label) +{ + uint32_t id = kon_uiHash_(ui, label); + size_t n = kon_uiVisibleLength_(label); + int pad = ui->theme.padding, textW = kon_uiTextWidth_(ui, label, n); + kon_rect_t r = kon_uiNextRect_(ui, ui->sameLine ? textW + pad * 2 : 0, ui->lineH + pad); + int hovered, clicked; + + clicked = kon_uiBehave_(ui, id, r, &hovered); + kon_uiFill_(ui, r, kon_uiWidgetColor_(ui, id, hovered)); + kon_uiOutline_(ui, r, ui->theme.border); + kon_uiDrawText_(ui, r.x + (r.w - textW) / 2, r.y + pad / 2 + 1, label, n, ui->theme.text); + return clicked; +} + +int kon_uiCheckbox(kon_ui_t *ui, const char *label, int *value) +{ + uint32_t id = kon_uiHash_(ui, label); + size_t n = kon_uiVisibleLength_(label); + int box = ui->lineH, pad = ui->theme.padding; + kon_rect_t r = kon_uiNextRect_(ui, box + pad + kon_uiTextWidth_(ui, label, n) + 1, box); + kon_rect_t b = KON_RECT(r.x, r.y, box, box); + int hovered, clicked = kon_uiBehave_(ui, id, r, &hovered); + + if (clicked) *value = !*value; + kon_uiFill_(ui, b, kon_uiWidgetColor_(ui, id, hovered)); + kon_uiOutline_(ui, b, ui->theme.border); + if (*value) kon_uiFill_(ui, KON_RECT(b.x + 3, b.y + 3, b.w - 6, b.h - 6), ui->theme.accent); + kon_uiDrawText_(ui, r.x + box + pad, r.y, label, n, ui->theme.text); + return clicked; +} + +int kon_uiSliderFloat(kon_ui_t *ui, const char *label, float *value, float min, float max) +{ + uint32_t id = kon_uiHash_(ui, label); + size_t n = kon_uiVisibleLength_(label); + int pad = ui->theme.padding; + kon_rect_t r = kon_uiNextRect_(ui, 0, ui->lineH + pad); + char text[64]; + int hovered, changed = 0, fill; + float t; + + kon_uiBehave_(ui, id, r, &hovered); + + if (ui->active == id && r.w > 1 && max > min) { + float v; + + t = (float)(ui->mouseX - r.x) / (float)(r.w - 1); + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + v = min + t * (max - min); + if (v != *value) { + *value = v; + changed = 1; + } + } + + t = max > min ? (*value - min) / (max - min) : 0.0f; + if (t < 0.0f) t = 0.0f; + if (t > 1.0f) t = 1.0f; + fill = (int)(t * (float)(r.w - 2)); + + kon_uiFill_(ui, r, kon_uiWidgetColor_(ui, id, hovered)); + kon_uiFill_(ui, KON_RECT(r.x + 1, r.y + 1, fill, r.h - 2), ui->theme.accent); + kon_uiOutline_(ui, r, ui->theme.border); + + snprintf(text, sizeof(text), "%.*s: %.2f", (int)(n > 40 ? 40 : n), label, (double)*value); + kon_uiDrawText_(ui, r.x + pad, r.y + pad / 2 + 1, text, strlen(text), ui->theme.text); + return changed; +} + +int kon_uiSliderInt(kon_ui_t *ui, const char *label, int *value, int min, int max) +{ + float v = (float)*value; + int changed = kon_uiSliderFloat(ui, label, &v, (float)min, (float)max); + + if (changed) *value = (int)(v + (v < 0 ? -0.5f : 0.5f)); + return changed; +} + +int kon_uiSelectable(kon_ui_t *ui, const char *label, int selected) +{ + uint32_t id = kon_uiHash_(ui, label); + size_t n = kon_uiVisibleLength_(label); + int pad = ui->theme.padding; + kon_rect_t r = kon_uiNextRect_(ui, 0, ui->lineH); + int hovered, clicked = kon_uiBehave_(ui, id, r, &hovered); + + if (selected) kon_uiFill_(ui, r, ui->theme.active); + else if (hovered) kon_uiFill_(ui, r, ui->theme.hover); + kon_uiDrawText_(ui, r.x + pad, r.y, label, n, ui->theme.text); + return clicked; +} + +int kon_uiTextInput(kon_ui_t *ui, const char *label, char *buffer, size_t size) +{ + uint32_t id = kon_uiHash_(ui, label); + size_t n = kon_uiVisibleLength_(label); + int pad = ui->theme.padding, hovered, entered = 0, i; + size_t length = strlen(buffer); + kon_rect_t r = kon_uiNextRect_(ui, 0, ui->lineH + pad); + + kon_uiBehave_(ui, id, r, &hovered); + if (hovered && ui->mousePressed) { + ui->focus = id; + ui->cursor = (int)length; + } + if (ui->focus == id) { + ui->focusKept = ui->focusKept || (hovered && ui->mousePressed); + if (ui->cursor > (int)length) ui->cursor = (int)length; + + for (i = 0; i < ui->textCount; i++) { + unsigned int c = ui->text[i]; + + if (c < 32 || c > 126 || length + 1 >= size) continue; + memmove(buffer + ui->cursor + 1, buffer + ui->cursor, length - (size_t)ui->cursor + 1); + buffer[ui->cursor++] = (char)c; + length++; + } + if (ui->keyPressed[KON_KEY_BACKSPACE] && ui->cursor > 0) { + memmove(buffer + ui->cursor - 1, buffer + ui->cursor, length - (size_t)ui->cursor + 1); + ui->cursor--; + length--; + } + if (ui->keyPressed[KON_KEY_DELETE] && ui->cursor < (int)length) { + memmove(buffer + ui->cursor, buffer + ui->cursor + 1, length - (size_t)ui->cursor); + length--; + } + if (ui->keyPressed[KON_KEY_LEFT] && ui->cursor > 0) ui->cursor--; + if (ui->keyPressed[KON_KEY_RIGHT] && ui->cursor < (int)length) ui->cursor++; + if (ui->keyPressed[KON_KEY_ENTER]) entered = 1; + if (ui->keyPressed[KON_KEY_ESCAPE]) ui->focus = 0; + } + + kon_uiFill_(ui, r, ui->focus == id ? ui->theme.hover : ui->theme.widget); + kon_uiOutline_(ui, r, ui->focus == id ? ui->theme.accent : ui->theme.border); + + if (length == 0 && ui->focus != id) { + kon_uiDrawText_(ui, r.x + pad, r.y + pad / 2 + 1, label, n, ui->theme.textDim); + } else { + kon_uiDrawText_(ui, r.x + pad, r.y + pad / 2 + 1, buffer, length, ui->theme.text); + if (ui->focus == id) { + int cx = r.x + pad + kon_uiTextWidth_(ui, buffer, (size_t)ui->cursor); + + kon_uiFill_(ui, KON_RECT(cx, r.y + 2, 1, r.h - 4), ui->theme.text); + } + } + return entered; +} + +void kon_uiSeparator(kon_ui_t *ui) +{ + kon_rect_t r = kon_uiNextRect_(ui, 0, 1); + + kon_uiFill_(ui, r, ui->theme.border); +} + +void kon_uiSameLine(kon_ui_t *ui) +{ + ui->sameLine = 1; +} + +#endif /* KONUI_IMPLEMENTATION */ -- cgit