summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--kon/kontextbox.c595
-rw-r--r--kon/kontextbox.h186
2 files changed, 781 insertions, 0 deletions
diff --git a/kon/kontextbox.c b/kon/kontextbox.c
new file mode 100644
index 0000000..34db120
--- /dev/null
+++ b/kon/kontextbox.c
@@ -0,0 +1,595 @@
+/* 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 <math.h>
+#include <string.h>
+
+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];
+ }
+ }
+}
diff --git a/kon/kontextbox.h b/kon/kontextbox.h
new file mode 100644
index 0000000..25c60b0
--- /dev/null
+++ b/kon/kontextbox.h
@@ -0,0 +1,186 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * kontextbox.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 KONTEXTBOX_H
+#define KONTEXTBOX_H
+
+#include "konsofren.h"
+#include "konfont.h"
+#include "kon2d.h"
+
+/*** panels ***/
+
+typedef enum kon_panelMode {
+ KON_PANEL_STRETCH, /* the image is stretched over the whole panel */
+ KON_PANEL_SLICE /* nine patch: the corners stay as they are, the edges and the middle stretch */
+} kon_panelMode_t;
+
+/* what a box, a name plate or a menu is drawn on. every part is optional: colours with alpha 0 and a NULL image draw nothing.
+ the fill is drawn first, then the image over it, then the border */
+typedef struct kon_panel {
+ uint32_t fill; /* 0xAARRGGBB, a low alpha lets what is behind show through */
+ uint32_t border;
+ int borderWidth;
+ const kon_image *image;
+ kon_rect_t src; /* w == 0 is the whole image */
+ kon_panelMode_t mode;
+ int slice; /* with KON_PANEL_SLICE: the size of the corners in image pixels */
+} kon_panel_t;
+
+/* alpha from 0 to 1 fades the whole panel */
+void kon_drawPanel(kon_framebuffer_t *fb, const kon_panel_t *panel, int x, int y, int width, int height, float alpha);
+
+/*** text box ***/
+
+#define KON_TEXTBOX_TEXT 1024 /* bytes of text kept, a longer text is cut */
+#define KON_TEXTBOX_NAME 64
+#define KON_TEXTBOX_LINES 64 /* lines kept after wrapping */
+#define KON_TEXTBOX_CHOICES 8
+#define KON_TEXTBOX_LABEL 64
+
+typedef enum kon_textboxState {
+ KON_TEXTBOX_EMPTY,
+ KON_TEXTBOX_TYPING, /* the letters are still coming in */
+ KON_TEXTBOX_WAITING, /* the page is all there, waiting for kon_textboxAdvance */
+ KON_TEXTBOX_CHOOSING /* a menu is up, see kon_textboxConfirm */
+} kon_textboxState_t;
+
+typedef enum kon_side {
+ KON_SIDE_LEFT,
+ KON_SIDE_RIGHT
+} kon_side_t;
+
+typedef enum kon_align {
+ KON_ALIGN_LEFT,
+ KON_ALIGN_CENTER,
+ KON_ALIGN_RIGHT
+} kon_align_t;
+
+typedef enum kon_choiceMode {
+ KON_CHOICES_LIST, /* the rows of a menu inside the box's panel */
+ KON_CHOICES_BUTTONS /* every choice is a panel of its own, the box's panel is not drawn. the usual look in a visual novel */
+} kon_choiceMode_t;
+
+typedef enum kon_portraitMode {
+ KON_PORTRAIT_INSIDE, /* in the middle of the box's height, inside its padding */
+ KON_PORTRAIT_HANG /* standing on the bottom of the box, a tall picture reaches above the top edge */
+} kon_portraitMode_t;
+
+/* one box for a message in an RPG, a visual novel or a plain popup, and with kon_textboxChoices it is the menu too.
+ fill in the public fields after kon_textboxInit, the rest is private. the box does not read input, the game calls kon_textboxAdvance
+ and kon_textboxSelect when it wants to. sprites in a visual novel belong to the stage, the box only holds the face that sits inside it */
+typedef struct kon_textbox {
+ kon_font_t *font;
+ int width, height;
+ kon_tween_t x, y, alpha; /* where it is and how solid, see kon_textboxMove and kon_textboxFade */
+
+ kon_panel_t panel, plate;
+ int padding;
+ int marginLeft, marginRight; /* extra room kept clear of text on each side, for a sprite that stands over the box */
+ int showEmpty; /* draw the panel while there is nothing to say, for a box that stays on screen. otherwise an empty box is not drawn */
+ int platePadding;
+ int plateOffsetX, plateOffsetY; /* the name plate sits on the top left of the box, moved by this */
+ uint32_t textColor, nameColor, choiceColor, selectColor, selectFill;
+ kon_align_t align; /* of the lines of a message */
+ kon_align_t choiceAlign; /* of the text of a choice */
+ kon_choiceMode_t choiceMode;
+ kon_panel_t button, buttonSelected; /* for KON_CHOICES_BUTTONS, a button is as wide as the box and as tall as a line plus the padding */
+ int buttonGap;
+ float speed; /* letters a second, 0 shows each page at once */
+ int lineSpacing; /* extra pixels between lines */
+
+ const kon_image *portrait;
+ kon_rect_t portraitSrc; /* w == 0 is the whole image */
+ kon_side_t portraitSide;
+ kon_portraitMode_t portraitMode;
+
+ /* private */
+ kon_textboxState_t state;
+ char text[KON_TEXTBOX_TEXT];
+ char name[KON_TEXTBOX_NAME];
+ int length;
+ int lineStart[KON_TEXTBOX_LINES], lineLength[KON_TEXTBOX_LINES];
+ int lineCount, linesPerPage, page, pageCount;
+ int pageChars;
+ float shown;
+ char labels[KON_TEXTBOX_CHOICES][KON_TEXTBOX_LABEL];
+ int choiceCount, selected;
+} kon_textbox_t;
+
+/* x, y is the top left. the font has to stay alive as long as the box is used. the defaults are the pastel on dark palette */
+void kon_textboxInit(kon_textbox_t *box, kon_font_t *font, int x, int y, int width, int height);
+
+/* shows text, wrapped to the box and split into pages that fit. name may be NULL. replaces what was there, menu included.
+ the text is copied. new lines in it start a new line in the box */
+void kon_textboxSay(kon_textbox_t *box, const char *name, const char *text);
+
+/* a face from a sheet of equally sized faces, counted left to right then top to bottom, 144 by 144 and 4 across in RPG Maker.
+ an index off the sheet takes the face out */
+void kon_textboxFace(kon_textbox_t *box, const kon_image *sheet, int frameWidth, int frameHeight, int index, kon_side_t side);
+
+/* the face inside the box, NULL takes it out. src NULL is the whole image. the text is wrapped again to fit beside it */
+void kon_textboxPortrait(kon_textbox_t *box, const kon_image *image, const kon_rect_t *src, kon_side_t side, kon_portraitMode_t mode);
+
+/* the action key. finishes the letters coming in, or turns to the next page, or ends the message. returns 1 when the last
+ page was dismissed, the box is empty again */
+int kon_textboxAdvance(kon_textbox_t *box);
+
+/* 1 when the page is complete, and 1 if there is a page after it, for a "more" mark you draw yourself */
+int kon_textboxWaiting(const kon_textbox_t *box);
+int kon_textboxMore(const kon_textbox_t *box);
+
+/* shows a menu in place of a message, the labels are copied. at most KON_TEXTBOX_CHOICES, the rest are dropped.
+ kon_textboxFitChoices makes the box just big enough for them, at least minWidth wide */
+void kon_textboxChoices(kon_textbox_t *box, const char *const *labels, int count);
+void kon_textboxFitChoices(kon_textbox_t *box, int minWidth);
+
+/* moves the selection by delta, wrapping round */
+void kon_textboxSelect(kon_textbox_t *box, int delta);
+void kon_textboxHighlight(kon_textbox_t *box, int index);
+
+/* the choice under a point (in the pixels you draw at), or -1 */
+int kon_textboxChoiceAt(const kon_textbox_t *box, int x, int y);
+
+/* returns the selected choice and closes the menu, -1 if there is none */
+int kon_textboxConfirm(kon_textbox_t *box);
+
+void kon_textboxClear(kon_textbox_t *box);
+
+/* puts the box against the left, in the middle or against the right of a view, margin pixels from the edge, at once.
+ RPG Maker's choice position. a menu with no panel at all (fill and border 0 on panel, button and buttonSelected) is its transparent window */
+void kon_textboxPlaceX(kon_textbox_t *box, int viewWidth, kon_align_t align, int margin);
+
+/* puts the box in the middle of a view of that size, along the axes you ask for, at once */
+void kon_textboxCenter(kon_textbox_t *box, int viewWidth, int viewHeight, int horizontal, int vertical);
+
+void kon_textboxMove(kon_textbox_t *box, float x, float y, float seconds, kon_ease_t ease);
+void kon_textboxFade(kon_textbox_t *box, float alpha, float seconds, kon_ease_t ease);
+
+/* returns 1 on the frame the page finished appearing */
+int kon_textboxUpdate(kon_textbox_t *box, float dt);
+
+void kon_textboxDraw(kon_textbox_t *box, kon_framebuffer_t *fb);
+
+#endif