From c02b42f7107da83321d6c3ffd3280329e4a04fed Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Thu, 6 Nov 2025 14:47:36 -0600 Subject: [PATCH] attempting to fix text wrapping ... --- prosperon/clay.cm | 17 +- prosperon/prosperon.cm | 1 + source/font.c | 363 ++++++++++++++++++++--------------------- source/font.h | 3 +- source/qjs_staef.c | 5 +- 5 files changed, 183 insertions(+), 206 deletions(-) diff --git a/prosperon/clay.cm b/prosperon/clay.cm index 3badea81..299e5bb3 100644 --- a/prosperon/clay.cm +++ b/prosperon/clay.cm @@ -248,7 +248,10 @@ clay.text = function text(str, ...configs) var config = rectify_configs(configs); config.size ??= [0,0]; config.font = graphics.get_font(config.font) - var tsize = config.font.text_size(str, 0, config.size.x, config.text_break, config.text_align); + config.text = str + var tsize = config.font.text_size(str, 0, config.size[0], config.text_break, config.text_align); + log.console(json.encode(config)) + log.console(json.encode(tsize)) tsize.x = Math.ceil(tsize.x) tsize.y = Math.ceil(tsize.y) config.size = config.size.map((x,i) => Math.max(x, tsize[i])); @@ -278,18 +281,6 @@ clay.button = function button(str, action, config = {}) config.action = action; } -clay.textbox = function(str, on_change, ...configs) { - var config = rectify_configs(configs) - config.on_change = on_change - config.text = str - config.font = graphics.get_font(config.font) - var tsize = config.font.text_size(str, 0, 0, config.text_break, config.text_align) - config.size ??= [0,0] - config.size = [Math.ceil(tsize.x), Math.ceil(tsize.y)] - config.size = [Math.max(config.size[0], config.size[0]), Math.max(config.size[1], config.size[1])] - add_item(config) -} - var point = use('point') clay.draw_commands = function draw_commands(tree_root, pos = {x:0,y:0}) diff --git a/prosperon/prosperon.cm b/prosperon/prosperon.cm index 1e4b4103..9bdad807 100644 --- a/prosperon/prosperon.cm +++ b/prosperon/prosperon.cm @@ -723,6 +723,7 @@ cmd_fns.draw_text = function(cmd) cmd.pos.width ??= size[0] cmd.pos.height ??= size[1] + log.console(json.encode(cmd)) var mesh = font.make_text_buffer( cmd.text, cmd.pos, diff --git a/source/font.c b/source/font.c index 13cafc89..02e34973 100644 --- a/source/font.c +++ b/source/font.c @@ -18,6 +18,7 @@ struct sFont *use_font; void font_free(JSRuntime *rt, font *f) { + if (f->surface) SDL_DestroySurface(f->surface); free(f); } @@ -50,10 +51,11 @@ struct sFont *MakeFont(void *ttf_buffer, size_t len, int height) { int ascent, descent, linegap; stbtt_GetFontVMetrics(&fontinfo, &ascent, &descent, &linegap); - float emscale = stbtt_ScaleForMappingEmToPixels(&fontinfo, height); - newfont->ascent = ascent*emscale; - newfont->descent = descent*emscale; - newfont->linegap = linegap*emscale; + 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->surface = SDL_CreateSurface(packsize,packsize, SDL_PIXELFORMAT_RGBA32); if (!newfont->surface) printf("SDL ERROR: %s\n", SDL_GetError()); for (int i = 0; i < packsize; i++) @@ -160,204 +162,187 @@ const char *esc_color(const char *c, struct rgba *color, struct rgba defc) } // 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 letterSpacing, float wrap, TEXT_BREAK breakAt, TEXT_ALIGN align) +HMM_Vec2 measure_text(const char *text, font *f, float letter_spacing, float wrap, TEXT_BREAK break_at, TEXT_ALIGN align) { - int breakAtWord = (breakAt == WORD); - HMM_Vec2 dim = {0}; - float maxWidth = 0; - float lineWidth = 0; - float lineHeight = f->ascent - f->descent; - float height = lineHeight; - - const char *wordStart = text; - float wordWidth = 0; - float spaceWidth = f->Characters[' '].advance + letterSpacing; + int break_at_word = (break_at == WORD); + float lh = f->line_height; + float height = lh; + float max_width = 0, line_width = 0; + int line_count = 1; // Start with 1 line - for (const char *c = text; *c != '\0'; ) { - if (*c == '\n') { - maxWidth = fmaxf(maxWidth, lineWidth); - lineWidth = 0; - height += lineHeight + f->linegap; - wordStart = c + 1; - wordWidth = 0; - c++; - continue; - } + const char *word_start = text; + float word_width = 0; - float charWidth = f->Characters[(unsigned char)*c].advance + letterSpacing; - - if (wrap > 0 && lineWidth + charWidth > wrap) { - if (breakAtWord && *c != ' ' && wordWidth > 0 && wordWidth <= wrap) { - // Backtrack to word start only if the word would fit on a new line - lineWidth -= wordWidth; - c = wordStart; - } else { - // Character-based wrapping or word too long to fit - maxWidth = fmaxf(maxWidth, lineWidth); - lineWidth = 0; - height += lineHeight + f->linegap; - wordStart = c; - wordWidth = 0; - - if (!breakAtWord || *c == ' ') { - c++; - continue; - } - // For word-based wrapping with long words, don't advance c here - let it be processed in this line - } - } else { - c++; - } - - lineWidth += charWidth; - - if (breakAtWord) { - if (*c == ' ') { - wordWidth = 0; - wordStart = c + 1; - } else { - wordWidth += charWidth; - } - } + for (const char *p = text; *p; ) { + if (*p == '\n') { + if (line_width > max_width) max_width = line_width; + line_width = 0; + height += lh; + line_count++; + word_start = p + 1; + word_width = 0; + p++; + continue; } - maxWidth = fmaxf(maxWidth, lineWidth); - dim.x = maxWidth; - dim.y = height; + unsigned char ch = (unsigned char)*p; + float adv = f->Characters[ch].advance; + /* add letter spacing unless this will be end-of-line char; unknown yet, so include it here; + wrap logic below uses same width */ + float char_w = adv + letter_spacing; - return dim; + if (wrap > 0 && line_width + char_w > wrap) { + if (break_at_word && *p != ' ' && word_width > 0 && word_width <= wrap) { + line_width -= word_width; + p = word_start; + } else { + if (line_width > max_width) max_width = line_width; + line_width = 0; + height += lh; + line_count++; + word_start = p; + word_width = 0; + /* if not word-breaking or current is space, consume it here */ + if (!break_at_word || *p == ' ') { p++; } + continue; + } + } else { + p++; + } + + line_width += char_w; + + if (break_at_word) { + if (*p == ' ') { + word_width = 0; + word_start = p + 1; + } else { + word_width += char_w; + } + } + } + + if (line_width > max_width) max_width = line_width; + + printf("measure_text: Number of lines: %d\n", line_count); + + HMM_Vec2 dim = { max_width, height }; + 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 breakAt, TEXT_ALIGN align) { - text_vert *buffer = NULL; - int breakAtWord = (breakAt == WORD); - - float lineHeight = f->ascent - f->descent; - float spaceWidth = f->Characters[' '].advance; - - // First pass: calculate line breaks and widths - typedef struct { - const char *start; - const char *end; - float width; - } Line; - - Line *lines = NULL; - const char *lineStart = text; - const char *wordStart = text; - float lineWidth = 0; - float wordWidth = 0; - - for (const char *c = text; *c != '\0'; c++) { - if (*c == '\n') { - Line line = {lineStart, c, lineWidth}; - arrput(lines, line); - lineStart = c + 1; - wordStart = c + 1; - lineWidth = 0; - wordWidth = 0; - continue; - } +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; + int break_at_word = (break_at == WORD); + float lh = f->line_height; - float charWidth = f->Characters[(unsigned char)*c].advance; + typedef struct { const char *start, *end; float width; } line_t; + line_t *lines = NULL; - if (wrap > 0 && lineWidth + charWidth > wrap) { - const char *breakPoint = c; - float breakWidth = lineWidth; - - if (breakAtWord && *c != ' ' && wordWidth > 0 && wordStart > lineStart) { - breakPoint = wordStart; - breakWidth = lineWidth - wordWidth; - } + const char *line_start = text, *word_start = text; + float line_width = 0, word_width = 0; - Line line = {lineStart, breakPoint, breakWidth}; - arrput(lines, line); - - lineStart = breakPoint; - if (breakAtWord && *lineStart == ' ') { - lineStart++; - } - - c = lineStart - 1; - wordStart = lineStart; - lineWidth = 0; - wordWidth = 0; - continue; - } - - lineWidth += charWidth; - - if (breakAtWord) { - if (*c == ' ') { - wordWidth = 0; - wordStart = c + 1; - } else { - wordWidth += charWidth; - } - } + for (const char *p = text; *p; p++) { + if (*p == '\n') { + line_t L = { line_start, p, line_width }; + arrput(lines, L); + line_start = p + 1; + word_start = p + 1; + line_width = 0; + word_width = 0; + continue; } - - // Add final line - if (lineStart < text + strlen(text)) { - Line line = {lineStart, text + strlen(text), lineWidth}; - arrput(lines, line); + + unsigned char ch = (unsigned char)*p; + float adv = f->Characters[ch].advance; + float char_w = adv + 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(lines, L); + + 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; } - - // Second pass: render with alignment - HMM_Vec2 cursor = pos; - - for (int i = 0; i < arrlen(lines); i++) { - Line *line = &lines[i]; - float startX = pos.x; - float extraSpace = 0; - int spaceCount = 0; - - // Calculate alignment offset - if (wrap > 0) { - switch (align) { - case RIGHT: - startX = pos.x + wrap - line->width; - break; - case CENTER: - startX = pos.x + (wrap - line->width) / 2.0f; - break; - case JUSTIFY: - if (i < arrlen(lines) - 1 && *(line->end) != '\n') { - for (const char *c = line->start; c < line->end; c++) { - if (*c == ' ') spaceCount++; - } - if (spaceCount > 0) { - extraSpace = (wrap - line->width) / spaceCount; - } - } - break; - case LEFT: - default: - break; - } - } - - cursor.x = startX; - - for (const char *c = line->start; c < line->end; c++) { - struct character chara = f->Characters[(unsigned char)*c]; - - if (!isspace(*c)) { - draw_char_verts(&buffer, chara, cursor, color); - } - - cursor.x += chara.advance; - - if (align == JUSTIFY && *c == ' ') { - cursor.x += extraSpace; - } - } - - cursor.x = pos.x; - cursor.y -= lineHeight + f->linegap; + + line_width += char_w; + + if (break_at_word) { + if (*p == ' ') { + word_width = 0; + word_start = p + 1; + } else { + word_width += char_w; + } } - - arrfree(lines); - return buffer; + } + + if (line_start < text + strlen(text)) { + line_t L = { line_start, text + strlen(text), line_width }; + arrput(lines, L); + } + + printf("renderText: Number of lines: %d\n", arrlen(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; } diff --git a/source/font.h b/source/font.h index 4411612a..30f459bf 100644 --- a/source/font.h +++ b/source/font.h @@ -52,6 +52,7 @@ struct sFont { float ascent; // pixels float descent; // pixels float linegap; //pixels + float line_height; // pixels struct character Characters[256]; SDL_Surface *surface; }; @@ -62,7 +63,7 @@ typedef struct Character glyph; void font_free(JSRuntime *rt,font *f); struct sFont *MakeFont(void *data, size_t len, int height); -struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, colorf color, float wrap, TEXT_BREAK breakAt, TEXT_ALIGN align); +struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, colorf color, float wrap, TEXT_BREAK breakAt, TEXT_ALIGN align, float letter_spacing); HMM_Vec2 measure_text(const char *text, font *f, float letterSpacing, float wrap, TEXT_BREAK breakAt, TEXT_ALIGN align); #endif diff --git a/source/qjs_staef.c b/source/qjs_staef.c index 0be03c39..7615a09b 100644 --- a/source/qjs_staef.c +++ b/source/qjs_staef.c @@ -72,7 +72,6 @@ JSC_CCALL(staef_font_text_size, int breakAt = (breakat == "character") ? CHARACTER : WORD; const char *align = argc > 4 ? JS_ToCString(js, argv[4]) : NULL; int alignAt = (align == "right") ? RIGHT : (align == "center") ? CENTER : (align == "justify") ? JUSTIFY : LEFT; - ret = vec22js(js, measure_text(str, f, letterSpacing, wrap, breakAt, alignAt)); JS_FreeCString(js, str); if (breakat) JS_FreeCString(js, breakat); @@ -92,7 +91,7 @@ JSC_CCALL(staef_font_make_text_buffer, int alignAt = (align == "right") ? RIGHT : (align == "center") ? CENTER : (align == "justify") ? JUSTIFY : LEFT; HMM_Vec2 startpos = {.x = rectpos.x, .y = rectpos.y }; - text_vert *buffer = renderText(s, startpos, f, c, wrap, breakAt, alignAt); + 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); @@ -105,7 +104,7 @@ JSC_GETSET(font, linegap, number) // Font methods static const JSCFunctionListEntry js_font_funcs[] = { - MIST_FUNC_DEF(staef_font, text_size, 4), + MIST_FUNC_DEF(staef_font, text_size, 5), MIST_FUNC_DEF(staef_font, make_text_buffer, 6), CGETSET_ADD(font, linegap), };