diff options
Diffstat (limited to 'kon')
| -rw-r--r-- | kon/konui.c | 444 | ||||
| -rw-r--r-- | kon/konui.h | 48 |
2 files changed, 491 insertions, 1 deletions
diff --git a/kon/konui.c b/kon/konui.c index 13a51d3..85ecd72 100644 --- a/kon/konui.c +++ b/kon/konui.c @@ -33,6 +33,32 @@ #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; @@ -42,6 +68,7 @@ struct kon_ui { int mouseX, mouseY; int mouseDown, mousePressed, mouseReleased; + int wheel; int keyPressed[KON_KEY_COUNT]; unsigned int text[KON_UI_TEXTQUEUE]; int textCount; @@ -61,6 +88,26 @@ struct kon_ui { int sameLine; 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 ***/ @@ -171,6 +218,62 @@ static uint32_t kon_uiWidgetColor_(const kon_ui_t *ui, uint32_t id, int hovered) 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) @@ -203,6 +306,9 @@ kon_ui_t *kon_uiCreate(kon_font_t *font) void kon_uiDestroy(kon_ui_t *ui) { + if (!ui) return; + free(ui->areaBuf.fb.data); + free(ui->popupBuf.fb.data); free(ui); } @@ -234,6 +340,9 @@ void kon_uiFeedEvent(kon_ui_t *ui, const kon_event_t *event) 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; @@ -253,6 +362,11 @@ void kon_uiBegin(kon_ui_t *ui, kon_framebuffer_t *fb) 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->cx = ui->theme.padding; @@ -262,6 +376,25 @@ void kon_uiBegin(kon_ui_t *ui, kon_framebuffer_t *fb) 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; @@ -269,6 +402,7 @@ void kon_uiEnd(kon_ui_t *ui) ui->mousePressed = 0; ui->mouseReleased = 0; + ui->wheel = 0; memset(ui->keyPressed, 0, sizeof(ui->keyPressed)); ui->textCount = 0; ui->fb = NULL; @@ -314,7 +448,7 @@ int kon_uiBeginPanel(kon_ui_t *ui, const char *title, kon_uiPanel_t *panel) kon_rect_t whole, bar, arrow; int h, hovered; - if (ui->panel) return 0; + 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); @@ -372,6 +506,259 @@ void kon_uiEndPanel(kon_ui_t *ui) 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) @@ -550,6 +937,61 @@ int kon_uiTextInput(kon_ui_t *ui, const char *label, char *buffer, size_t size) 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); diff --git a/kon/konui.h b/kon/konui.h index 9b253ca..98a1f08 100644 --- a/kon/konui.h +++ b/kon/konui.h @@ -71,6 +71,46 @@ int kon_uiWantsKeyboard(const kon_ui_t *ui); int kon_uiBeginPanel(kon_ui_t *ui, const char *title, kon_uiPanel_t *panel); void kon_uiEndPanel(kon_ui_t *ui); +/*** areas ***/ + +/* a fixed part of the window with no title and no dragging, for a layout that stays put: a side bar, a list, a preview. what is inside + is clipped to it and scrolls with the wheel when it is taller than the area. you own this and fill in x, y, w and h, the ui keeps + scroll and contentH. contentH is what the last frame needed, so the scroll bar shows up one frame late */ +typedef struct kon_uiArea { + int x, y, w, h; + int scroll; + int contentH; +} kon_uiArea_t; + +/* returns 0 when it is not drawn (nothing of it is on screen, or it is inside a panel, an area or a menu). always call kon_uiEndArea */ +int kon_uiBeginArea(kon_ui_t *ui, const char *id, kon_uiArea_t *area); +void kon_uiEndArea(kon_ui_t *ui); + +/*** menus ***/ + +/* a bar along the top of the window, kon_uiMenuBarHeight is how tall it is so you can lay the rest out under it. + kon_uiBeginMenu gives 1 while that menu is open, and then its items are kon_uiMenuItem, which gives 1 when one is clicked. + always call kon_uiEndMenu and kon_uiEndMenuBar. what is open is drawn over everything at the end of the frame. + a menu is as long as its items, up to 600 pixels, the rest is cut off + + if (kon_uiBeginMenuBar(ui)) { + if (kon_uiBeginMenu(ui, "File")) { + if (kon_uiMenuItem(ui, "Open", "Ctrl+O")) open(); + kon_uiMenuSeparator(ui); + if (kon_uiMenuItem(ui, "Quit", NULL)) quit(); + } + kon_uiEndMenu(ui); + } + kon_uiEndMenuBar(ui); +*/ +int kon_uiBeginMenuBar(kon_ui_t *ui); +void kon_uiEndMenuBar(kon_ui_t *ui); +int kon_uiMenuBarHeight(const kon_ui_t *ui); +int kon_uiBeginMenu(kon_ui_t *ui, const char *label); +int kon_uiMenuItem(kon_ui_t *ui, const char *label, const char *shortcut); +void kon_uiMenuSeparator(kon_ui_t *ui); +void kon_uiEndMenu(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, ...); @@ -83,6 +123,14 @@ 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); +/* a row of tabs, current is the one shown. returns 1 when another one was clicked */ +int kon_uiTabs(kon_ui_t *ui, const char *const *labels, int count, int *current); + +/* a line with a + or - in front that opens and closes. returns the new open state, show what is inside when it is 1 and use + kon_uiIndent to push it in */ +int kon_uiTreeNode(kon_ui_t *ui, const char *label, int *open); +void kon_uiIndent(kon_ui_t *ui, int amount); + void kon_uiSeparator(kon_ui_t *ui); void kon_uiSameLine(kon_ui_t *ui); void kon_uiPushId(kon_ui_t *ui, int id); |
