/* SPDX-License-Identifier: MIT */ /* * konui.c * * 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. */ #include "konui.h" #include #include #include #include #define KON_UI_IDS 16 #define KON_UI_TEXTQUEUE 32 #define KON_UI_MENUWIDTHS 8 #define KON_UI_POPUP_MAX 600 #define KON_UI_BAR_ID 0xB4A7C0DEu #define KON_UI_POPUP_ID 0x7F4A7C15u enum { KON_UI_NONE, KON_UI_AREA, KON_UI_BAR, KON_UI_POPUP }; /* a buffer a part of the ui is drawn to before it is copied to the real framebuffer */ typedef struct kon_uiScratch { kon_framebuffer_t fb; size_t cap; } kon_uiScratch_t; /* what a container takes over from the ui while it draws */ typedef struct kon_uiSaved { kon_framebuffer_t *fb; int cx, cy, availW, sameLine; int mouseX, mouseY; kon_rect_t last; uint32_t panelId; } kon_uiSaved_t; 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 wheel; 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; int nextWidth; kon_rect_t last; int cursor; int container; kon_uiSaved_t saved; kon_uiScratch_t areaBuf; kon_uiArea_t *area; int areaBar, areaThumbY, areaThumbH; kon_uiScratch_t popupBuf; kon_rect_t barRect; int barDrawn; int menuX; uint32_t menuOpen; uint32_t popupMenu; int popupBuilding, popupShow, popupPushed; int popupX, popupY, popupH; int popupNeed; uint32_t menuWidthId[KON_UI_MENUWIDTHS]; int menuWidth[KON_UI_MENUWIDTHS]; int menuWidthNext; }; /*** 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->nextWidth > 0 ? ui->nextWidth : ui->availW); ui->nextWidth = 0; 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; } static int kon_uiScratchSize_(kon_uiScratch_t *s, int w, int h) { size_t need; uint32_t *data; if (w <= 0 || h <= 0 || w > 16384 || h > 16384) return 0; need = (size_t)w * (size_t)h; if (need > s->cap) { data = realloc(s->fb.data, need * sizeof(uint32_t)); if (!data) return 0; s->fb.data = data; s->cap = need; } s->fb.width = w; s->fb.height = h; return 1; } /* the buffers are opaque so what is drawn in them can be copied over the screen without any blending */ static uint32_t kon_uiOpaque_(uint32_t color) { return color | 0xFF000000u; } static void kon_uiEnter_(kon_ui_t *ui, kon_framebuffer_t *fb, int x, int y, uint32_t id) { ui->saved.fb = ui->fb; ui->saved.cx = ui->cx; ui->saved.cy = ui->cy; ui->saved.availW = ui->availW; ui->saved.sameLine = ui->sameLine; ui->saved.mouseX = ui->mouseX; ui->saved.mouseY = ui->mouseY; ui->saved.last = ui->last; ui->saved.panelId = ui->panelId; ui->fb = fb; ui->mouseX -= x; ui->mouseY -= y; ui->sameLine = 0; ui->panelId = id; } static void kon_uiLeave_(kon_ui_t *ui) { ui->fb = ui->saved.fb; ui->cx = ui->saved.cx; ui->cy = ui->saved.cy; ui->availW = ui->saved.availW; ui->sameLine = ui->saved.sameLine; ui->mouseX = ui->saved.mouseX; ui->mouseY = ui->saved.mouseY; ui->last = ui->saved.last; ui->panelId = ui->saved.panelId; } /*** 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) { if (!ui) return; free(ui->areaBuf.fb.data); free(ui->popupBuf.fb.data); 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_MOUSE_WHEEL: ui->wheel += event->wheel; 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->container = KON_UI_NONE; ui->area = NULL; ui->barDrawn = 0; ui->popupBuilding = 0; ui->popupShow = 0; ui->idCount = 0; ui->sameLine = 0; ui->nextWidth = 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->popupShow && ui->popupMenu != ui->menuOpen) ui->popupShow = 0; if (!ui->barDrawn || ui->keyPressed[KON_KEY_ESCAPE]) ui->menuOpen = 0; if (ui->menuOpen && ui->mousePressed) { kon_rect_t pr = KON_RECT(ui->popupX, ui->popupY, ui->popupBuf.fb.width, ui->popupH); if (!kon_uiInside_(ui, ui->barRect) && !(ui->popupShow && kon_uiInside_(ui, pr))) ui->menuOpen = 0; } if (ui->popupShow && ui->popupMenu != ui->menuOpen) ui->popupShow = 0; /* what is open is drawn last so it ends up over everything */ if (ui->popupShow) { kon_rect_t pr = KON_RECT(ui->popupX, ui->popupY, ui->popupBuf.fb.width, ui->popupH); kon_drawImageRegion(ui->fb, pr.x, pr.y, &ui->popupBuf.fb, KON_RECT(0, 0, pr.w, pr.h)); kon_uiOutline_(ui, pr, ui->theme.border); if (kon_uiInside_(ui, pr)) ui->topPanelNext = KON_UI_POPUP_ID; } 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; ui->wheel = 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 || ui->container) 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; } /*** areas ***/ int kon_uiBeginArea(kon_ui_t *ui, const char *name, kon_uiArea_t *area) { uint32_t id = kon_uiHash_(ui, name); uint32_t barId = id ^ 0x85EBCA6Bu; int pad = ui->theme.padding, barW = pad * 2; int maxScroll, trackH, thumbH = 0, thumbY = 0; kon_rect_t whole; if (ui->panel || ui->container) return 0; if (area->w <= 0 || area->h <= 0) return 0; if (area->x >= ui->fb->width || area->y >= ui->fb->height || area->x + area->w <= 0 || area->y + area->h <= 0) return 0; if (!kon_uiScratchSize_(&ui->areaBuf, area->w, area->h)) return 0; whole = KON_RECT(area->x, area->y, area->w, area->h); if (kon_uiInside_(ui, whole)) ui->topPanelNext = id; maxScroll = area->contentH - area->h; if (maxScroll < 0) maxScroll = 0; trackH = area->h; if (maxScroll && ui->wheel && ui->topPanel == id && kon_uiInside_(ui, whole)) area->scroll -= ui->wheel * ui->lineH * 3; if (area->scroll > maxScroll) area->scroll = maxScroll; if (area->scroll < 0) area->scroll = 0; if (maxScroll) { kon_rect_t track = KON_RECT(area->x + area->w - barW, area->y, barW, trackH); thumbH = trackH * area->h / area->contentH; if (thumbH < 16) thumbH = 16; if (thumbH > trackH) thumbH = trackH; thumbY = area->y + (trackH - thumbH) * area->scroll / maxScroll; if (ui->active == 0 && ui->mousePressed && ui->topPanel == id && kon_uiInside_(ui, track)) { ui->active = barId; ui->dragY = ui->mouseY >= thumbY && ui->mouseY < thumbY + thumbH ? ui->mouseY - thumbY : thumbH / 2; } if (ui->active == barId && trackH > thumbH) { area->scroll = (ui->mouseY - ui->dragY - area->y) * maxScroll / (trackH - thumbH); if (area->scroll > maxScroll) area->scroll = maxScroll; if (area->scroll < 0) area->scroll = 0; thumbY = area->y + (trackH - thumbH) * area->scroll / maxScroll; } } else { area->scroll = 0; } ui->areaBar = maxScroll > 0; ui->areaThumbY = thumbY - area->y; ui->areaThumbH = thumbH; kon_uiEnter_(ui, &ui->areaBuf.fb, area->x, area->y, id); kon_clearFramebuffer(ui->fb, kon_uiOpaque_(ui->theme.panel)); ui->cx = pad; ui->cy = pad - area->scroll; ui->availW = area->w - pad * 2 - (ui->areaBar ? barW : 0); ui->container = KON_UI_AREA; ui->area = area; return 1; } void kon_uiEndArea(kon_ui_t *ui) { kon_uiArea_t *area = ui->area; int pad = ui->theme.padding, barW = pad * 2; uint32_t barId; if (ui->container != KON_UI_AREA || !area) return; area->contentH = ui->cy + area->scroll - ui->theme.spacing + pad; if (ui->areaBar) { kon_rect_t track = KON_RECT(ui->fb->width - barW, 0, barW, ui->fb->height); kon_rect_t thumb = KON_RECT(track.x + 1, ui->areaThumbY, barW - 2, ui->areaThumbH); barId = ui->panelId ^ 0x85EBCA6Bu; kon_uiFill_(ui, track, ui->theme.title); kon_uiFill_(ui, thumb, ui->active == barId ? ui->theme.accent : (kon_uiInside_(ui, thumb) && ui->topPanel == ui->panelId ? ui->theme.active : ui->theme.hover)); } kon_uiLeave_(ui); ui->container = KON_UI_NONE; ui->area = NULL; kon_drawImageRegion(ui->fb, area->x, area->y, &ui->areaBuf.fb, KON_RECT(0, 0, ui->areaBuf.fb.width, ui->areaBuf.fb.height)); kon_uiOutline_(ui, KON_RECT(area->x, area->y, ui->areaBuf.fb.width, ui->areaBuf.fb.height), ui->theme.border); } /*** menus ***/ int kon_uiMenuBarHeight(const kon_ui_t *ui) { return ui->lineH + ui->theme.padding; } int kon_uiBeginMenuBar(kon_ui_t *ui) { int h = kon_uiMenuBarHeight(ui); kon_rect_t r = KON_RECT(0, 0, ui->fb->width, h); if (ui->panel || ui->container) return 0; if (kon_uiInside_(ui, r)) ui->topPanelNext = KON_UI_BAR_ID; ui->barRect = r; ui->barDrawn = 1; ui->menuX = 0; ui->panelId = KON_UI_BAR_ID; ui->container = KON_UI_BAR; kon_uiFill_(ui, r, ui->theme.title); kon_fillRectangle(ui->fb, 0, h - 1, r.w, 1, ui->theme.border); return 1; } void kon_uiEndMenuBar(kon_ui_t *ui) { int below; if (ui->container != KON_UI_BAR) return; ui->container = KON_UI_NONE; ui->panelId = 0; below = kon_uiMenuBarHeight(ui) + ui->theme.padding; if (ui->cy < below) ui->cy = below; } static int kon_uiMenuWidth_(const kon_ui_t *ui, uint32_t id) { int i; for (i = 0; i < KON_UI_MENUWIDTHS; i++) { if (ui->menuWidthId[i] == id) return ui->menuWidth[i]; } return 160; } static void kon_uiSetMenuWidth_(kon_ui_t *ui, uint32_t id, int w) { int i; for (i = 0; i < KON_UI_MENUWIDTHS; i++) { if (ui->menuWidthId[i] == id) { ui->menuWidth[i] = w; return; } } i = ui->menuWidthNext; ui->menuWidthNext = (i + 1) % KON_UI_MENUWIDTHS; ui->menuWidthId[i] = id; ui->menuWidth[i] = w; } int kon_uiBeginMenu(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, barH = kon_uiMenuBarHeight(ui); int hovered, clicked, w, h, x; kon_rect_t r; if (ui->container != KON_UI_BAR) return 0; r = KON_RECT(ui->menuX, 0, kon_uiTextWidth_(ui, label, n) + pad * 3, barH - 1); ui->menuX += r.w; clicked = kon_uiBehave_(ui, id, r, &hovered); if (clicked) ui->menuOpen = ui->menuOpen == id ? 0 : id; else if (hovered && ui->menuOpen && ui->menuOpen != id) ui->menuOpen = id; if (ui->menuOpen == id) kon_uiFill_(ui, r, ui->theme.active); else if (hovered) kon_uiFill_(ui, r, ui->theme.hover); kon_uiDrawText_(ui, r.x + pad * 3 / 2, r.y + pad / 2, label, n, ui->theme.text); if (ui->menuOpen != id) return 0; w = kon_uiMenuWidth_(ui, id); if (w < 80) w = 80; if (w > 480) w = 480; h = ui->fb->height - barH; if (h > KON_UI_POPUP_MAX) h = KON_UI_POPUP_MAX; if (h < 1 || !kon_uiScratchSize_(&ui->popupBuf, w, h)) return 0; x = r.x; if (x + w > ui->fb->width) x = ui->fb->width - w; if (x < 0) x = 0; kon_uiEnter_(ui, &ui->popupBuf.fb, x, barH, KON_UI_POPUP_ID); kon_clearFramebuffer(ui->fb, kon_uiOpaque_(ui->theme.panel)); ui->cx = pad; ui->cy = pad; ui->availW = w - pad * 2; ui->popupX = x; ui->popupY = barH; ui->popupMenu = id; ui->popupNeed = 0; ui->popupBuilding = 1; ui->popupPushed = ui->idCount < KON_UI_IDS; if (ui->popupPushed) kon_uiPushId(ui, (int)id); ui->container = KON_UI_POPUP; return 1; } void kon_uiEndMenu(kon_ui_t *ui) { int h; if (!ui->popupBuilding) return; h = ui->cy - ui->theme.spacing + ui->theme.padding; if (h > ui->popupBuf.fb.height) h = ui->popupBuf.fb.height; if (h < 1) h = 1; ui->popupH = h; ui->popupShow = 1; kon_uiSetMenuWidth_(ui, ui->popupMenu, ui->popupNeed); if (ui->popupPushed) kon_uiPopId(ui); kon_uiLeave_(ui); ui->popupBuilding = 0; ui->container = KON_UI_BAR; } int kon_uiMenuItem(kon_ui_t *ui, const char *label, const char *shortcut) { uint32_t id = kon_uiHash_(ui, label); size_t n = kon_uiVisibleLength_(label); int pad = ui->theme.padding, textW = kon_uiTextWidth_(ui, label, n), shortW = 0, need, hovered, clicked; kon_rect_t r; if (!ui->popupBuilding) return 0; r = kon_uiNextRect_(ui, 0, ui->lineH); if (shortcut) shortW = kon_uiTextWidth_(ui, shortcut, strlen(shortcut)); need = pad * 4 + textW + (shortW ? pad * 4 + shortW : 0); if (need > ui->popupNeed) ui->popupNeed = need; clicked = kon_uiBehave_(ui, id, r, &hovered); if (hovered) kon_uiFill_(ui, r, ui->theme.hover); kon_uiDrawText_(ui, r.x + pad, r.y, label, n, ui->theme.text); if (shortcut) kon_uiDrawText_(ui, r.x + r.w - shortW - pad, r.y, shortcut, strlen(shortcut), ui->theme.textDim); if (clicked) ui->menuOpen = 0; return clicked; } void kon_uiMenuSeparator(kon_ui_t *ui) { if (!ui->popupBuilding) return; kon_uiSeparator(ui); } /*** 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 && ui->nextWidth <= 0 ? 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; } static int kon_uiSlider_(kon_ui_t *ui, const char *label, float *value, float min, float max, int whole) { 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 (whole) v = (float)(int)(v + (v < 0 ? -0.5f : 0.5f)); 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); if (whole) snprintf(text, sizeof(text), "%.*s: %d", (int)(n > 40 ? 40 : n), label, (int)*value); else 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_uiSliderFloat(kon_ui_t *ui, const char *label, float *value, float min, float max) { return kon_uiSlider_(ui, label, value, min, max, 0); } int kon_uiSliderInt(kon_ui_t *ui, const char *label, int *value, int min, int max) { float v = (float)*value; int changed = kon_uiSlider_(ui, label, &v, (float)min, (float)max, 1); if (changed) *value = (int)v; 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; } int kon_uiTabs(kon_ui_t *ui, const char *const *labels, int count, int *current) { int pad = ui->theme.padding, changed = 0, i; for (i = 0; i < count; i++) { size_t n = kon_uiVisibleLength_(labels[i]); int pushed = ui->idCount < KON_UI_IDS, hovered, clicked, selected; uint32_t id, color; kon_rect_t r; if (pushed) kon_uiPushId(ui, i); id = kon_uiHash_(ui, labels[i]); if (i > 0) ui->sameLine = 1; r = kon_uiNextRect_(ui, kon_uiTextWidth_(ui, labels[i], n) + pad * 3, ui->lineH + pad); clicked = kon_uiBehave_(ui, id, r, &hovered); if (clicked && *current != i) { *current = i; changed = 1; } selected = *current == i; color = selected ? ui->theme.active : kon_uiWidgetColor_(ui, id, hovered); kon_uiFill_(ui, r, color); if (selected) kon_uiFill_(ui, KON_RECT(r.x, r.y, r.w, 2), ui->theme.accent); kon_uiOutline_(ui, r, ui->theme.border); kon_uiDrawText_(ui, r.x + pad * 3 / 2, r.y + pad / 2 + 1, labels[i], n, selected ? ui->theme.text : ui->theme.textDim); if (pushed) kon_uiPopId(ui); } return changed; } int kon_uiTreeNode(kon_ui_t *ui, const char *label, int *open) { uint32_t id = kon_uiHash_(ui, label); size_t n = kon_uiVisibleLength_(label); kon_rect_t r = kon_uiNextRect_(ui, 0, ui->lineH); int hovered, clicked = kon_uiBehave_(ui, id, r, &hovered); if (clicked) *open = !*open; if (hovered) kon_uiFill_(ui, r, ui->theme.hover); kon_uiDrawText_(ui, r.x + ui->theme.padding, r.y, *open ? "-" : "+", 1, ui->theme.textDim); kon_uiDrawText_(ui, r.x + ui->lineH, r.y, label, n, ui->theme.text); return *open; } void kon_uiIndent(kon_ui_t *ui, int amount) { ui->cx += amount; ui->availW -= amount; if (ui->availW < 1) ui->availW = 1; } 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; } void kon_uiNextWidth(kon_ui_t *ui, int width) { ui->nextWidth = width; }