491 lines
13 KiB
C
491 lines
13 KiB
C
#include "cell.h"
|
|
#include "prosperon.h"
|
|
#include "HandmadeMath.h"
|
|
#include "stb_ds.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include <ctype.h>
|
|
#include <limits.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define STB_TRUETYPE_IMPLEMENTATION
|
|
#define STB_TRUETYPE_NO_STDIO
|
|
#include "stb_truetype.h"
|
|
|
|
#include "stb_ds.h"
|
|
|
|
#include "HandmadeMath.h"
|
|
|
|
typedef enum {
|
|
LEFT,
|
|
RIGHT,
|
|
CENTER,
|
|
JUSTIFY
|
|
} TEXT_ALIGN;
|
|
|
|
typedef enum {
|
|
WORD,
|
|
CHARACTER
|
|
} TEXT_BREAK;
|
|
|
|
struct text_char {
|
|
rect pos;
|
|
rect uv;
|
|
HMM_Vec4 color;
|
|
};
|
|
|
|
struct shader;
|
|
struct window;
|
|
|
|
struct character {
|
|
float advance;
|
|
rect quad;
|
|
rect uv;
|
|
float xoff;
|
|
float yoff;
|
|
float width;
|
|
float height;
|
|
};
|
|
|
|
// text data
|
|
struct sFont {
|
|
uint32_t height; /* in pixels */
|
|
float ascent; // pixels
|
|
float descent; // pixels
|
|
float linegap; //pixels
|
|
float line_height; // pixels
|
|
struct character Characters[256];
|
|
unsigned char *pixels; // RGBA8 pixel data
|
|
int atlas_size; // width and height of atlas (square)
|
|
};
|
|
|
|
typedef struct sFont font;
|
|
typedef struct Character glyph;
|
|
|
|
|
|
typedef struct { const char *start, *end; float width; } line_t;
|
|
typedef struct { line_t *lines; float max_width; } layout_lines;
|
|
|
|
struct sFont *use_font;
|
|
|
|
void font_free(JSRuntime *rt, font *f)
|
|
{
|
|
if (f->pixels) free(f->pixels);
|
|
free(f);
|
|
}
|
|
|
|
struct sFont *MakeFont(void *ttf_buffer, size_t len, int height) {
|
|
if (!ttf_buffer)
|
|
return NULL;
|
|
|
|
int packsize = 1024;
|
|
|
|
struct sFont *newfont = calloc(1, sizeof(struct sFont));
|
|
newfont->height = height;
|
|
|
|
unsigned char *bitmap = malloc(packsize * packsize);
|
|
|
|
stbtt_packedchar glyphs[95];
|
|
|
|
stbtt_pack_context pc;
|
|
|
|
int pad = 2;
|
|
|
|
stbtt_PackBegin(&pc, bitmap, packsize, packsize, 0, pad, NULL);
|
|
stbtt_PackFontRange(&pc, ttf_buffer, 0, height, 32, 95, glyphs);
|
|
stbtt_PackEnd(&pc);
|
|
|
|
stbtt_fontinfo fontinfo;
|
|
if (!stbtt_InitFont(&fontinfo, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer, 0))) {
|
|
}
|
|
|
|
int ascent, descent, linegap;
|
|
|
|
stbtt_GetFontVMetrics(&fontinfo, &ascent, &descent, &linegap);
|
|
float s = stbtt_ScaleForPixelHeight(&fontinfo, height);
|
|
newfont->ascent = ascent * s;
|
|
newfont->descent = descent * s; /* descent is negative */
|
|
newfont->linegap = linegap * s;
|
|
newfont->line_height = (newfont->ascent - newfont->descent) + newfont->linegap;
|
|
newfont->atlas_size = packsize;
|
|
newfont->pixels = malloc(packsize * packsize * 4); // RGBA8
|
|
for (int i = 0; i < packsize; i++) {
|
|
for (int j = 0; j < packsize; j++) {
|
|
int idx = (i * packsize + j) * 4;
|
|
newfont->pixels[idx + 0] = 255; // R
|
|
newfont->pixels[idx + 1] = 255; // G
|
|
newfont->pixels[idx + 2] = 255; // B
|
|
newfont->pixels[idx + 3] = bitmap[i * packsize + j]; // A
|
|
}
|
|
}
|
|
|
|
for (unsigned char c = 32; c < 127; c++) {
|
|
stbtt_packedchar glyph = glyphs[c - 32];
|
|
|
|
rect uv;
|
|
uv.x = (glyph.x0) / (float)packsize;
|
|
uv.w = (glyph.x1-glyph.x0) / (float)packsize;
|
|
uv.y = (glyph.y1) / (float)packsize;
|
|
uv.h = (glyph.y0-glyph.y1) / (float)packsize;
|
|
newfont->Characters[c].uv = uv;
|
|
|
|
rect quad;
|
|
quad.x = glyph.xoff;
|
|
quad.y = -glyph.yoff2; // Top of glyph relative to baseline
|
|
quad.w = glyph.xoff2 - glyph.xoff;
|
|
quad.h = glyph.yoff2 - glyph.yoff; // Height from baseline
|
|
newfont->Characters[c].quad = quad;
|
|
newfont->Characters[c].advance = glyph.xadvance;
|
|
}
|
|
|
|
free(bitmap);
|
|
|
|
return newfont;
|
|
}
|
|
|
|
layout_lines layout_text_lines(const char *text, font *f,
|
|
float letter_spacing,
|
|
float wrap,
|
|
TEXT_BREAK break_at)
|
|
{
|
|
layout_lines out = {0};
|
|
int break_at_word = (break_at == WORD);
|
|
|
|
const char *line_start = text;
|
|
const char *word_start = text;
|
|
float line_width = 0;
|
|
float word_width = 0;
|
|
|
|
float max_width = 0;
|
|
|
|
for (const char *p = text; *p; p++) {
|
|
if (*p == '\n') {
|
|
line_t L = { line_start, p, line_width };
|
|
arrput(out.lines, L);
|
|
if (line_width > max_width) max_width = line_width;
|
|
|
|
line_start = p + 1;
|
|
word_start = p + 1;
|
|
line_width = 0;
|
|
word_width = 0;
|
|
continue;
|
|
}
|
|
|
|
unsigned char ch = (unsigned char)*p;
|
|
float char_w = f->Characters[ch].advance + letter_spacing;
|
|
|
|
if (wrap > 0 && line_width + char_w > wrap) {
|
|
const char *break_pt = p;
|
|
float break_w = line_width;
|
|
|
|
if (break_at_word && *p != ' ' && word_width > 0 && word_start > line_start) {
|
|
break_pt = word_start;
|
|
break_w = line_width - word_width;
|
|
}
|
|
|
|
line_t L = { line_start, break_pt, break_w };
|
|
arrput(out.lines, L);
|
|
if (break_w > max_width) max_width = break_w;
|
|
|
|
line_start = break_pt;
|
|
if (break_at_word && *line_start == ' ') line_start++;
|
|
|
|
p = line_start - 1;
|
|
word_start = line_start;
|
|
line_width = 0;
|
|
word_width = 0;
|
|
continue;
|
|
}
|
|
|
|
line_width += char_w;
|
|
|
|
if (break_at_word) {
|
|
if (*p == ' ') {
|
|
word_width = 0;
|
|
word_start = p + 1;
|
|
} else {
|
|
word_width += char_w;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (line_start < text + strlen(text)) {
|
|
line_t L = { line_start, text + strlen(text), line_width };
|
|
arrput(out.lines, L);
|
|
if (line_width > max_width) max_width = line_width;
|
|
}
|
|
|
|
out.max_width = max_width;
|
|
return out;
|
|
}
|
|
|
|
void sdrawCharacter(struct text_vert **buffer, stbtt_packedchar c, HMM_Vec2 cursor, float scale, struct rgba color) {
|
|
struct text_vert vert;
|
|
|
|
// vert.pos.x = cursor.X + c.leftbearing;
|
|
// vert.pos.y = cursor.Y + c.topbearing;
|
|
// vert.wh = c.size;
|
|
|
|
// if (vert.pos.x > frame.l || vert.pos.y > frame.t || (vert.pos.y + vert.wh.y) < frame.b || (vert.pos.x + vert.wh.x) < frame.l) return;
|
|
|
|
// vert.uv.x = c.rect.x;
|
|
// vert.uv.y = c.rect.y;
|
|
// vert.st.x = c.rect.w;
|
|
// vert.st.y = c.rect.h;
|
|
rgba2floats(vert.color.e, color);
|
|
|
|
arrput(*buffer, vert);
|
|
}
|
|
|
|
void draw_char_verts(struct text_vert **buffer, struct character c, HMM_Vec2 cursor, colorf color)
|
|
{
|
|
// packedchar has
|
|
// Adds four verts: bottom left, bottom right, top left, top right
|
|
text_vert bl;
|
|
bl.pos.x = cursor.X + c.quad.x;
|
|
bl.pos.y = cursor.Y + c.quad.y;
|
|
bl.uv.x = c.uv.x;
|
|
bl.uv.y = c.uv.y;
|
|
bl.color = color;
|
|
arrput(*buffer, bl);
|
|
|
|
|
|
text_vert br = bl;
|
|
br.pos.x += c.quad.w;
|
|
br.uv.x += c.uv.w;
|
|
arrput(*buffer, br);
|
|
|
|
text_vert ul = bl;
|
|
ul.pos.y += c.quad.h;
|
|
ul.uv.y += c.uv.h;
|
|
arrput(*buffer, ul);
|
|
|
|
text_vert ur = ul;
|
|
ur.pos.x = br.pos.x;
|
|
ur.uv.x = br.uv.x;
|
|
arrput(*buffer, ur);
|
|
}
|
|
|
|
const char *esc_color(const char *c, struct rgba *color, struct rgba defc)
|
|
{
|
|
struct rgba d;
|
|
if (!color) color = &d;
|
|
if (*c != '\033') return c;
|
|
c++;
|
|
if (*c != '[') return c;
|
|
c++;
|
|
if (*c == '0') {
|
|
*color = defc;
|
|
c++;
|
|
return c;
|
|
}
|
|
else if (!strncmp(c, "38;2;", 5)) {
|
|
c += 5;
|
|
*color = (struct rgba){0,0,0,255};
|
|
color->r = atoi(c);
|
|
c = strchr(c, ';')+1;
|
|
color->g = atoi(c);
|
|
c = strchr(c,';')+1;
|
|
color->b = atoi(c);
|
|
c = strchr(c,';')+1;
|
|
return c;
|
|
}
|
|
return c;
|
|
}
|
|
|
|
// text is a string, font f, wrap is how long a line is before wrapping. -1 to not wrap
|
|
HMM_Vec2 measure_text(const char *text, font *f, float letter_spacing, float wrap, TEXT_BREAK break_at, TEXT_ALIGN align)
|
|
{
|
|
layout_lines lay = layout_text_lines(text, f, letter_spacing, wrap, break_at);
|
|
|
|
float lh = f->line_height;
|
|
int line_count = arrlen(lay.lines);
|
|
float height = line_count > 0 ? line_count * lh : 0;
|
|
|
|
float width = lay.max_width;
|
|
if (wrap > 0 && align != LEFT && align != JUSTIFY) {
|
|
// For RIGHT or CENTER we might shift lines visually, but width is still max(line.width, wrap ? min(wrap, ...) ).
|
|
// Usually you'd report max(wrap, width) or just wrap, depending on how you want containers to size.
|
|
}
|
|
|
|
HMM_Vec2 dim = { width, height };
|
|
|
|
arrfree(lay.lines);
|
|
return dim;
|
|
}
|
|
|
|
/* pos given in screen coordinates */
|
|
struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, colorf color,
|
|
float wrap, TEXT_BREAK break_at, TEXT_ALIGN align,
|
|
float letter_spacing)
|
|
{
|
|
text_vert *buffer = NULL;
|
|
float lh = f->line_height;
|
|
|
|
layout_lines lay = layout_text_lines(text, f, letter_spacing, wrap, break_at);
|
|
line_t *lines = lay.lines;
|
|
|
|
HMM_Vec2 cursor = pos;
|
|
|
|
for (int i = 0; i < arrlen(lines); i++) {
|
|
line_t *L = &lines[i];
|
|
float start_x = pos.x;
|
|
float extra_space = 0;
|
|
int space_count = 0;
|
|
|
|
if (wrap > 0) {
|
|
switch (align) {
|
|
case RIGHT: start_x = pos.x + wrap - L->width; break;
|
|
case CENTER: start_x = pos.x + (wrap - L->width) * 0.5f; break;
|
|
case JUSTIFY:
|
|
if (i < arrlen(lines) - 1 && *(L->end) != '\n') {
|
|
for (const char *p = L->start; p < L->end; p++) if (*p == ' ') space_count++;
|
|
if (space_count > 0) extra_space = (wrap - L->width) / space_count;
|
|
}
|
|
break;
|
|
case LEFT: default: break;
|
|
}
|
|
}
|
|
|
|
cursor.x = start_x;
|
|
|
|
const char *p = L->start;
|
|
while (p < L->end) {
|
|
unsigned char ch = (unsigned char)*p;
|
|
struct character g = f->Characters[ch];
|
|
|
|
if (!isspace(*p)) draw_char_verts(&buffer, g, cursor, color);
|
|
|
|
cursor.x += g.advance;
|
|
/* add letter spacing unless this is last char of the line */
|
|
if (p + 1 < L->end) cursor.x += letter_spacing;
|
|
if (align == JUSTIFY && *p == ' ') cursor.x += extra_space;
|
|
|
|
p++;
|
|
}
|
|
|
|
cursor.x = pos.x;
|
|
cursor.y -= lh;
|
|
}
|
|
|
|
arrfree(lines);
|
|
return buffer;
|
|
}
|
|
|
|
|
|
// QuickJS class for font
|
|
QJSCLASS(font,)
|
|
|
|
// Font constructor
|
|
JSC_CCALL(staef_font_new,
|
|
size_t len;
|
|
void *data = js_get_blob_data(js, &len, argv[0]);
|
|
if (data == -1) return JS_EXCEPTION;
|
|
if (!data) return JS_ThrowReferenceError(js, "could not get array buffer data");
|
|
|
|
double height = js2number(js, argv[1]);
|
|
font *f = MakeFont(data, len, (int)height);
|
|
if (!f) return JS_ThrowReferenceError(js, "could not create font");
|
|
|
|
ret = font2js(js, f);
|
|
|
|
// Create texture data object for the font's atlas
|
|
if (f->pixels) {
|
|
JSValue texData = JS_NewObject(js);
|
|
JS_SetPropertyStr(js, texData, "width", JS_NewInt32(js, f->atlas_size));
|
|
JS_SetPropertyStr(js, texData, "height", JS_NewInt32(js, f->atlas_size));
|
|
JS_SetPropertyStr(js, texData, "format", JS_NewString(js, "rgba8"));
|
|
|
|
size_t byte_size = f->atlas_size * f->atlas_size * 4;
|
|
JS_SetPropertyStr(js, texData, "pixels", js_new_blob_stoned_copy(js, f->pixels, byte_size));
|
|
|
|
JS_SetPropertyStr(js, ret, "texture", texData);
|
|
}
|
|
)
|
|
|
|
// Calculate text size
|
|
JSC_CCALL(staef_font_text_size,
|
|
font *f = js2font(js, self);
|
|
const char *str = JS_ToCString(js, argv[0]);
|
|
float letterSpacing = argc > 1 ? js2number(js, argv[1]) : 0;
|
|
float wrap = argc > 2 ? js2number(js, argv[2]) : -1;
|
|
const char *breakat = argc > 3 ? JS_ToCString(js, argv[3]) : NULL;
|
|
int breakAt = (breakat && strcmp(breakat, "character") == 0) ? CHARACTER : WORD;
|
|
const char *align = argc > 4 ? JS_ToCString(js, argv[4]) : NULL;
|
|
int alignAt = (align && strcmp(align, "right") == 0) ? RIGHT : (align && strcmp(align, "center") == 0) ? CENTER : (align && strcmp(align, "justify") == 0) ? JUSTIFY : LEFT;
|
|
ret = vec22js(js, measure_text(str, f, letterSpacing, wrap, breakAt, alignAt));
|
|
JS_FreeCString(js, str);
|
|
if (breakat) JS_FreeCString(js, breakat);
|
|
if (align) JS_FreeCString(js, align);
|
|
)
|
|
|
|
// Generate text buffer (mesh data)
|
|
JSC_CCALL(staef_font_make_text_buffer,
|
|
font *f = js2font(js, self);
|
|
const char *s = JS_ToCString(js, argv[0]);
|
|
rect rectpos = js2rect(js, argv[1]);
|
|
colorf c = js2color(js, argv[2]);
|
|
float wrap = argc > 3 ? js2number(js, argv[3]) : -1;
|
|
const char *breakat = argc > 4 ? JS_ToCString(js, argv[4]) : NULL;
|
|
int breakAt = (breakat && strcmp(breakat, "character") == 0) ? CHARACTER : WORD;
|
|
const char *align = argc > 5 ? JS_ToCString(js, argv[5]) : NULL;
|
|
int alignAt = (align && strcmp(align, "right") == 0) ? RIGHT : (align && strcmp(align, "center") == 0) ? CENTER : (align && strcmp(align, "justify") == 0) ? JUSTIFY : LEFT;
|
|
|
|
HMM_Vec2 startpos = {.x = rectpos.x, .y = rectpos.y };
|
|
text_vert *buffer = renderText(s, startpos, f, c, wrap, breakAt, alignAt, 0);
|
|
ret = quads_to_mesh(js, buffer);
|
|
JS_FreeCString(js, s);
|
|
if (breakat) JS_FreeCString(js, breakat);
|
|
if (align) JS_FreeCString(js, align);
|
|
arrfree(buffer);
|
|
)
|
|
|
|
// Font property getters/setters
|
|
JSC_GETSET(font, linegap, number)
|
|
JSC_GETSET(font, ascent, number)
|
|
JSC_GETSET(font, descent, number)
|
|
JSC_GETSET(font, line_height, number)
|
|
JSC_GETSET(font, height, number)
|
|
|
|
// Font methods
|
|
static const JSCFunctionListEntry js_font_funcs[] = {
|
|
MIST_FUNC_DEF(staef_font, text_size, 5),
|
|
MIST_FUNC_DEF(staef_font, make_text_buffer, 6),
|
|
CGETSET_ADD(font, linegap),
|
|
CGETSET_ADD(font, ascent),
|
|
CGETSET_ADD(font, descent),
|
|
CGETSET_ADD(font, line_height),
|
|
CGETSET_ADD(font, height),
|
|
};
|
|
|
|
// Font constructor function
|
|
static JSValue js_font_constructor(JSContext *ctx, JSValueConst new_target, int argc, JSValueConst *argv)
|
|
{
|
|
return js_staef_font_new(ctx, JS_NULL, argc, argv);
|
|
}
|
|
|
|
// Initialize the staef module
|
|
CELL_USE_INIT(
|
|
JSValue mod = JS_NewObject(js);
|
|
JSValue proto;
|
|
|
|
// Initialize font class
|
|
JS_NewClassID(&js_font_id);
|
|
JS_NewClass(JS_GetRuntime(js), js_font_id, &js_font_class);
|
|
proto = JS_NewObject(js);
|
|
JS_SetPropertyFunctionList(js, proto, js_font_funcs, countof(js_font_funcs));
|
|
JS_SetClassProto(js, js_font_id, proto);
|
|
|
|
// Create font constructor
|
|
JSValue font_ctor = JS_NewCFunction2(js, js_font_constructor, "font", 2, JS_CFUNC_constructor, 0);
|
|
JS_SetConstructor(js, font_ctor, proto);
|
|
|
|
// Add font constructor to module (lowercase to match "new staef.font")
|
|
JS_SetPropertyStr(js, mod, "font", font_ctor);
|
|
|
|
return mod;
|
|
)
|