From ac6f4a20bb4d5782d61967c61c9d47d52d2c869e Mon Sep 17 00:00:00 2001 From: KonAki Date: Mon, 12 Oct 2026 13:45:27 +0800 Subject: add textbox module: panels, wrapped pages, typewriter, faces, menus --- kon/kontextbox.c | 595 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ kon/kontextbox.h | 186 +++++++++++++++++ 2 files changed, 781 insertions(+) create mode 100644 kon/kontextbox.c create mode 100644 kon/kontextbox.h (limited to 'kon') 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 +#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]; + } + } +} 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 -- cgit