/* SPDX-License-Identifier: MIT */ /* * kontextbox.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 "kontextbox.h" #include #include static int iround(float v) { return (int)floorf(v + 0.5f); } /* the alpha of a colour times a share from 0 to 1 */ static uint32_t fade(uint32_t color, float alpha) { uint32_t a = color >> 24; if (!(alpha > 0.0f)) return 0; if (alpha >= 1.0f) return color; a = (uint32_t)((float)a * alpha + 0.5f); return (a << 24) | (color & 0x00FFFFFFu); } static kon_rect_t wholeOf(const kon_image *image, kon_rect_t src) { if (src.w > 0 && src.h > 0) return src; src.x = src.y = 0; src.w = image->width; src.h = image->height; return src; } /*** panels ***/ static void drawPatch(kon_framebuffer_t *fb, const kon_image *image, kon_rect_t src, int x, int y, int w, int h, uint32_t tint) { if (src.w <= 0 || src.h <= 0 || w <= 0 || h <= 0) return; kon_drawImageTint(fb, image, src, (float)x, (float)y, 0.0f, 0.0f, (float)w / (float)src.w, (float)h / (float)src.h, 0.0f, KON_FILTER_NEAREST, tint); } void kon_drawPanel(kon_framebuffer_t *fb, const kon_panel_t *panel, int x, int y, int width, int height, float alpha) { int b = panel->borderWidth; uint32_t color; if (!fb || width <= 0 || height <= 0 || !(alpha > 0.0f)) return; color = fade(panel->fill, alpha); if (color >> 24) kon_fillRectangle(fb, x, y, width, height, color); if (panel->image) { kon_rect_t src = wholeOf(panel->image, panel->src); uint32_t tint = fade(0xFFFFFFFFu, alpha); if (panel->mode == KON_PANEL_SLICE && panel->slice > 0) { int m = panel->slice, mw = m, mh = m, i, j; int sx[4], sy[4], dx[4], dy[4]; if (mw * 2 > src.w) mw = src.w / 2; if (mh * 2 > src.h) mh = src.h / 2; sx[0] = src.x; sx[1] = src.x + mw; sx[2] = src.x + src.w - mw; sx[3] = src.x + src.w; sy[0] = src.y; sy[1] = src.y + mh; sy[2] = src.y + src.h - mh; sy[3] = src.y + src.h; if (mw * 2 > width) mw = width / 2; if (mh * 2 > height) mh = height / 2; dx[0] = x; dx[1] = x + mw; dx[2] = x + width - mw; dx[3] = x + width; dy[0] = y; dy[1] = y + mh; dy[2] = y + height - mh; dy[3] = y + height; for (j = 0; j < 3; j++) { for (i = 0; i < 3; i++) { kon_rect_t part = KON_RECT(sx[i], sy[j], sx[i + 1] - sx[i], sy[j + 1] - sy[j]); drawPatch(fb, panel->image, part, dx[i], dy[j], dx[i + 1] - dx[i], dy[j + 1] - dy[j], tint); } } } else { drawPatch(fb, panel->image, src, x, y, width, height, tint); } } color = fade(panel->border, alpha); if (b > 0 && (color >> 24)) { if (b * 2 > width) b = width / 2; if (b * 2 > height) b = height / 2; kon_fillRectangle(fb, x, y, width, b, color); kon_fillRectangle(fb, x, y + height - b, width, b, color); kon_fillRectangle(fb, x, y + b, b, height - b * 2, color); kon_fillRectangle(fb, x + width - b, y + b, b, height - b * 2, color); } } /*** layout ***/ typedef struct layout { int x, y, width, height; /* the room for text */ int boxX, boxY; int innerX, innerY, innerWidth, innerHeight; int lineHeight, ascent; } layout_t; static int fontHeight(const kon_textbox_t *box) { int h = 0; kon_measureText(box->font, "A", NULL, &h); return h > 0 ? h : 8; } static void layoutOf(const kon_textbox_t *box, layout_t *l) { int edge = box->padding + (box->panel.borderWidth > 0 ? box->panel.borderWidth : 0); int h = fontHeight(box); l->boxX = iround(kon_tweenValue(&box->x)); l->boxY = iround(kon_tweenValue(&box->y)); l->innerX = l->boxX + edge; l->innerY = l->boxY + edge; l->innerWidth = box->width - edge * 2; l->innerHeight = box->height - edge * 2; if (l->innerWidth < 1) l->innerWidth = 1; if (l->innerHeight < 1) l->innerHeight = 1; l->x = l->innerX + (box->marginLeft > 0 ? box->marginLeft : 0); l->y = l->innerY; l->width = l->innerWidth - (box->marginLeft > 0 ? box->marginLeft : 0) - (box->marginRight > 0 ? box->marginRight : 0); l->height = l->innerHeight; if (l->width < 1) l->width = 1; if (box->portrait) { int pw = wholeOf(box->portrait, box->portraitSrc).w; l->width -= pw + box->padding; if (l->width < 1) l->width = 1; if (box->portraitSide == KON_SIDE_LEFT) l->x += pw + box->padding; } l->lineHeight = h + box->lineSpacing; if (l->lineHeight < 1) l->lineHeight = 1; l->ascent = h * 4 / 5; } /*** wrapping ***/ static int widthOf(kon_textbox_t *box, int start, int end) { char saved = box->text[end]; int w = 0; box->text[end] = '\0'; kon_measureText(box->font, &box->text[start], &w, NULL); box->text[end] = saved; return w; } /* where text of that width starts in a room of width, for an alignment */ static int alignedX(int x, int room, int width, kon_align_t align) { if (align == KON_ALIGN_CENTER) return x + (room - width) / 2; if (align == KON_ALIGN_RIGHT) return x + room - width; return x; } static void wrap(kon_textbox_t *box, int avail) { int i = 0, len = box->length; box->lineCount = 0; if (avail < 1) avail = 1; while (i < len && box->lineCount < KON_TEXTBOX_LINES) { int start = i, fit = i, j = i, n; for (;;) { int k = j; while (k < len && box->text[k] != ' ' && box->text[k] != '\n') k++; if (k > start && widthOf(box, start, k) > avail) { if (fit == start) { /* one word wider than the box, it is cut where it runs out */ fit = start + 1; while (fit < k && widthOf(box, start, fit + 1) <= avail) fit++; } break; } fit = k; if (k >= len || box->text[k] == '\n') break; j = k; while (j < len && box->text[j] == ' ') j++; if (j >= len) break; } n = fit - start; while (n > 0 && box->text[start + n - 1] == ' ') n--; box->lineStart[box->lineCount] = start; box->lineLength[box->lineCount] = n; box->lineCount++; i = fit; while (i < len && box->text[i] == ' ') i++; if (i < len && box->text[i] == '\n') i++; } } static int linesOnPage(const kon_textbox_t *box) { int first = box->page * box->linesPerPage, n = box->lineCount - first; if (n > box->linesPerPage) n = box->linesPerPage; return n > 0 ? n : 0; } static void startPage(kon_textbox_t *box) { int first = box->page * box->linesPerPage, n = linesOnPage(box), i; box->pageChars = 0; for (i = 0; i < n; i++) box->pageChars += box->lineLength[first + i]; box->shown = 0.0f; box->state = KON_TEXTBOX_TYPING; if (!(box->speed > 0.0f) || box->pageChars == 0) { box->shown = (float)box->pageChars; box->state = KON_TEXTBOX_WAITING; } } static void rewrap(kon_textbox_t *box) { layout_t l; layoutOf(box, &l); wrap(box, l.width); box->linesPerPage = l.height / l.lineHeight; if (box->linesPerPage < 1) box->linesPerPage = 1; box->pageCount = (box->lineCount + box->linesPerPage - 1) / box->linesPerPage; } /*** text box ***/ void kon_textboxInit(kon_textbox_t *box, kon_font_t *font, int x, int y, int width, int height) { kon_textbox_t zero = {0}; *box = zero; box->font = font; box->width = width; box->height = height; kon_tweenSet(&box->x, (float)x); kon_tweenSet(&box->y, (float)y); kon_tweenSet(&box->alpha, 1.0f); box->panel.fill = 0xE026223Au; box->panel.border = 0xFF9A93B3u; box->panel.borderWidth = 1; box->plate.fill = 0xF0332E4Eu; box->plate.border = 0xFF9A93B3u; box->plate.borderWidth = 1; box->padding = 8; box->button.fill = 0xE026223Au; box->button.border = 0xFF9A93B3u; box->button.borderWidth = 1; box->buttonSelected = box->button; box->buttonSelected.fill = 0xFFF2A7C3u; box->buttonGap = 6; box->platePadding = 4; box->plateOffsetX = 8; box->textColor = 0xFFE4DDF0u; box->nameColor = 0xFFF2A7C3u; box->choiceColor = 0xFFE4DDF0u; box->selectColor = 0xFF1B1826u; box->selectFill = 0xFFF2A7C3u; box->speed = 40.0f; box->lineSpacing = 2; } void kon_textboxClear(kon_textbox_t *box) { box->state = KON_TEXTBOX_EMPTY; box->text[0] = '\0'; box->name[0] = '\0'; box->length = 0; box->lineCount = box->page = box->pageCount = box->pageChars = 0; box->shown = 0.0f; box->choiceCount = box->selected = 0; } static void copyString(char *to, size_t size, const char *from) { size_t n = strlen(from); if (n >= size) n = size - 1; memcpy(to, from, n); to[n] = '\0'; } void kon_textboxSay(kon_textbox_t *box, const char *name, const char *text) { size_t i, n = 0; kon_textboxClear(box); if (name) copyString(box->name, sizeof box->name, name); if (!text) text = ""; for (i = 0; text[i] && n < KON_TEXTBOX_TEXT - 1; i++) { if (text[i] == '\r') continue; box->text[n++] = text[i]; } box->text[n] = '\0'; box->length = (int)n; rewrap(box); startPage(box); } void kon_textboxPortrait(kon_textbox_t *box, const kon_image *image, const kon_rect_t *src, kon_side_t side, kon_portraitMode_t mode) { box->portrait = image; box->portraitSrc = src ? *src : (kon_rect_t){0, 0, 0, 0}; box->portraitSide = side; box->portraitMode = mode; if (box->state == KON_TEXTBOX_TYPING || box->state == KON_TEXTBOX_WAITING) { rewrap(box); if (box->page >= box->pageCount) box->page = box->pageCount > 0 ? box->pageCount - 1 : 0; startPage(box); box->shown = (float)box->pageChars; box->state = KON_TEXTBOX_WAITING; } } void kon_textboxFace(kon_textbox_t *box, const kon_image *sheet, int frameWidth, int frameHeight, int index, kon_side_t side) { kon_rect_t src = {0, 0, 0, 0}; if (sheet) src = kon_frameRect(sheet, frameWidth, frameHeight, index); if (!sheet || src.w <= 0 || src.x + src.w > sheet->width || src.y + src.h > sheet->height) { kon_textboxPortrait(box, NULL, NULL, side, KON_PORTRAIT_INSIDE); return; } kon_textboxPortrait(box, sheet, &src, side, KON_PORTRAIT_INSIDE); } int kon_textboxAdvance(kon_textbox_t *box) { switch (box->state) { case KON_TEXTBOX_TYPING: box->shown = (float)box->pageChars; box->state = KON_TEXTBOX_WAITING; return 0; case KON_TEXTBOX_WAITING: if (box->page + 1 < box->pageCount) { box->page++; startPage(box); return 0; } kon_textboxClear(box); return 1; case KON_TEXTBOX_EMPTY: case KON_TEXTBOX_CHOOSING: break; } return 0; } int kon_textboxWaiting(const kon_textbox_t *box) { return box->state == KON_TEXTBOX_WAITING; } int kon_textboxMore(const kon_textbox_t *box) { return box->state == KON_TEXTBOX_WAITING && box->page + 1 < box->pageCount; } int kon_textboxUpdate(kon_textbox_t *box, float dt) { kon_tweenStep(&box->x, dt); kon_tweenStep(&box->y, dt); kon_tweenStep(&box->alpha, dt); if (box->state != KON_TEXTBOX_TYPING || !(dt > 0.0f)) return 0; box->shown += box->speed * dt; if (box->shown < (float)box->pageChars) return 0; box->shown = (float)box->pageChars; box->state = KON_TEXTBOX_WAITING; return 1; } void kon_textboxPlaceX(kon_textbox_t *box, int viewWidth, kon_align_t align, int margin) { int x = margin; if (align == KON_ALIGN_CENTER) x = (viewWidth - box->width) / 2; else if (align == KON_ALIGN_RIGHT) x = viewWidth - box->width - margin; kon_tweenSet(&box->x, (float)x); } void kon_textboxCenter(kon_textbox_t *box, int viewWidth, int viewHeight, int horizontal, int vertical) { if (horizontal) kon_tweenSet(&box->x, (float)((viewWidth - box->width) / 2)); if (vertical) kon_tweenSet(&box->y, (float)((viewHeight - box->height) / 2)); } void kon_textboxMove(kon_textbox_t *box, float x, float y, float seconds, kon_ease_t ease) { kon_tweenTo(&box->x, x, seconds, ease); kon_tweenTo(&box->y, y, seconds, ease); } void kon_textboxFade(kon_textbox_t *box, float alpha, float seconds, kon_ease_t ease) { kon_tweenTo(&box->alpha, alpha, seconds, ease); } /*** choices ***/ void kon_textboxChoices(kon_textbox_t *box, const char *const *labels, int count) { int i; kon_textboxClear(box); if (!labels || count < 1) return; if (count > KON_TEXTBOX_CHOICES) count = KON_TEXTBOX_CHOICES; for (i = 0; i < count; i++) copyString(box->labels[i], sizeof box->labels[i], labels[i] ? labels[i] : ""); box->choiceCount = count; box->selected = 0; box->state = KON_TEXTBOX_CHOOSING; } /* where choice i is drawn */ static kon_rect_t choiceRect(const kon_textbox_t *box, const layout_t *l, int i) { kon_rect_t r; if (box->choiceMode == KON_CHOICES_BUTTONS) { int rowHeight = l->lineHeight + box->padding * 2; r = KON_RECT(l->boxX, l->boxY + i * (rowHeight + box->buttonGap), box->width, rowHeight); } else { r = KON_RECT(l->innerX, l->innerY + i * l->lineHeight, l->innerWidth, l->lineHeight); } return r; } void kon_textboxFitChoices(kon_textbox_t *box, int minWidth) { layout_t l; int i, widest = 0, fit; if (box->state != KON_TEXTBOX_CHOOSING) return; layoutOf(box, &l); for (i = 0; i < box->choiceCount; i++) { int w = 0; kon_measureText(box->font, box->labels[i], &w, NULL); if (w > widest) widest = w; } if (box->choiceMode == KON_CHOICES_BUTTONS) { int rowHeight = l.lineHeight + box->padding * 2; fit = widest + (box->padding + (box->button.borderWidth > 0 ? box->button.borderWidth : 0)) * 2; box->height = box->choiceCount * rowHeight + (box->choiceCount - 1) * box->buttonGap; } else { int edge = box->padding + (box->panel.borderWidth > 0 ? box->panel.borderWidth : 0); fit = widest + edge * 2; box->height = box->choiceCount * l.lineHeight + edge * 2; } box->width = fit > minWidth ? fit : minWidth; } void kon_textboxSelect(kon_textbox_t *box, int delta) { if (box->state != KON_TEXTBOX_CHOOSING || box->choiceCount < 1) return; box->selected = (box->selected + delta % box->choiceCount + box->choiceCount) % box->choiceCount; } void kon_textboxHighlight(kon_textbox_t *box, int index) { if (box->state == KON_TEXTBOX_CHOOSING && index >= 0 && index < box->choiceCount) box->selected = index; } int kon_textboxChoiceAt(const kon_textbox_t *box, int x, int y) { layout_t l; int i; if (box->state != KON_TEXTBOX_CHOOSING) return -1; layoutOf(box, &l); for (i = 0; i < box->choiceCount; i++) { kon_rect_t r = choiceRect(box, &l, i); if (x >= r.x && x < r.x + r.w && y >= r.y && y < r.y + r.h) return i; } return -1; } int kon_textboxConfirm(kon_textbox_t *box) { int chosen; if (box->state != KON_TEXTBOX_CHOOSING) return -1; chosen = box->selected; kon_textboxClear(box); return chosen; } /*** drawing ***/ /* the line is cut off with a terminator for a moment */ static void drawLine(kon_textbox_t *box, kon_framebuffer_t *fb, int x, int baseline, int start, int length, uint32_t color) { char saved = box->text[start + length]; box->text[start + length] = '\0'; kon_drawText(fb, box->font, x, baseline, &box->text[start], color); box->text[start + length] = saved; } void kon_textboxDraw(kon_textbox_t *box, kon_framebuffer_t *fb) { layout_t l; float alpha = kon_tweenValue(&box->alpha); int bx = iround(kon_tweenValue(&box->x)), by = iround(kon_tweenValue(&box->y)); int i; if (!fb || !box->font || !(alpha > 0.0f)) return; if (box->state == KON_TEXTBOX_EMPTY && !box->showEmpty) return; layoutOf(box, &l); if (!(box->state == KON_TEXTBOX_CHOOSING && box->choiceMode == KON_CHOICES_BUTTONS)) kon_drawPanel(fb, &box->panel, bx, by, box->width, box->height, alpha); if (box->name[0] && box->state != KON_TEXTBOX_CHOOSING) { int w = 0, h = fontHeight(box); int pad = box->platePadding; kon_measureText(box->font, box->name, &w, NULL); kon_drawPanel(fb, &box->plate, bx + box->plateOffsetX, by - (h + pad * 2) + box->plateOffsetY, w + pad * 2, h + pad * 2, alpha); kon_drawText(fb, box->font, bx + box->plateOffsetX + pad, by - (h + pad * 2) + box->plateOffsetY + pad + l.ascent, box->name, fade(box->nameColor, alpha)); } if (box->portrait) { kon_rect_t src = wholeOf(box->portrait, box->portraitSrc); int px = box->portraitSide == KON_SIDE_LEFT ? l.innerX : l.innerX + l.innerWidth - src.w; int py = box->portraitMode == KON_PORTRAIT_HANG ? by + box->height - src.h : l.innerY + (l.innerHeight - src.h) / 2; kon_drawImageTint(fb, box->portrait, src, (float)px, (float)py, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, KON_FILTER_NEAREST, fade(0xFFFFFFFFu, alpha)); } if (box->state == KON_TEXTBOX_CHOOSING) { int buttons = box->choiceMode == KON_CHOICES_BUTTONS; for (i = 0; i < box->choiceCount; i++) { kon_rect_t r = choiceRect(box, &l, i); int chosen = i == box->selected, inset = buttons ? box->padding : 2, w = 0; if (!buttons && r.y + r.h > by + box->height) break; if (buttons) kon_drawPanel(fb, chosen ? &box->buttonSelected : &box->button, r.x, r.y, r.w, r.h, alpha); else if (chosen) kon_fillRectangle(fb, r.x, r.y, r.w, r.h, fade(box->selectFill, alpha)); kon_measureText(box->font, box->labels[i], &w, NULL); kon_drawText(fb, box->font, alignedX(r.x + inset, r.w - inset * 2, w, box->choiceAlign), r.y + (r.h - fontHeight(box)) / 2 + l.ascent, box->labels[i], fade(chosen ? box->selectColor : box->choiceColor, alpha)); } return; } if (box->state == KON_TEXTBOX_TYPING || box->state == KON_TEXTBOX_WAITING) { int first = box->page * box->linesPerPage, n = linesOnPage(box); int left = (int)box->shown; for (i = 0; i < n && left > 0; i++) { int len = box->lineLength[first + i]; if (len > left) len = left; drawLine(box, fb, alignedX(l.x, l.width, widthOf(box, box->lineStart[first + i], box->lineStart[first + i] + box->lineLength[first + i]), box->align), l.y + i * l.lineHeight + (l.lineHeight - fontHeight(box)) / 2 + l.ascent, box->lineStart[first + i], len, fade(box->textColor, alpha)); left -= box->lineLength[first + i]; } } }