text wrap

This commit is contained in:
2025-11-04 19:16:36 -06:00
parent 149ee23e45
commit a05dda4914
7 changed files with 257 additions and 80 deletions

View File

@@ -39,7 +39,8 @@ var clay_base = {
margin:0,
offset:{x:0, y:0},
size:null,
background_color: null
background_color: null,
clipped: false,
};
var root_item;
@@ -66,8 +67,9 @@ clay.draw = function draw(fn)
root_item = root;
root_config = Object.assign({}, clay_base);
boxes.push({
id:root,
config:root_config
id: root,
config: root_config,
parent: null,
});
fn()
lay_ctx.run();
@@ -148,8 +150,10 @@ function add_item(config)
root_config._childIndex ??= 0
if (root_config._childIndex > 0) {
var parentContain = root_config.contain || 0;
var isVStack = (parentContain & layout.contain.column) != 0;
var isHStack = (parentContain & layout.contain.row) != 0;
var directionMask = layout.contain.row | layout.contain.column;
var direction = parentContain & directionMask;
var isVStack = direction == layout.contain.column;
var isHStack = direction == layout.contain.row;
if (isVStack) {
margin.t += childGap;
@@ -178,8 +182,9 @@ function add_item(config)
lay_ctx.set_contain(item,use_config.contain);
lay_ctx.set_behave(item,use_config.behave);
boxes.push({
id:item,
config:use_config
id: item,
config: use_config,
parent: root_item,
});
lay_ctx.insert(root_item,item);
@@ -260,7 +265,6 @@ clay.textbox = function(str, on_change, ...configs) {
var point = use('point')
// Pure rendering function - no input handling
clay.draw_commands = function draw_commands(cmds, pos = {x:0,y:0})
{
for (var cmd of cmds) {

View File

@@ -1,5 +1,6 @@
var math = use('math')
var color = use('color')
var gamestate = use('gamestate')
var draw = {}
@@ -154,13 +155,23 @@ draw.circle = function render_circle(pos, radius, defl, material) {
draw.ellipse(pos, [radius,radius], defl, material)
}
draw.text = function text(text, pos, font = 'fonts/c64.8', color = {r:1,g:1,b:1,a:1}, wrap = 0) {
// wrap is the width before wrapping
// config is any additional config to pass to the text renderer
var text_base_config = {
align: 'left', // left, right, center, justify
break: 'word', // word, character
}
draw.text = function text(text, pos, font = 'fonts/c64.8', color = {r:1,g:1,b:1,a:1}, wrap = 0, config = {}) {
config.align ??= text_base_config.align
config.break ??= text_base_config.break
add_command("draw_text", {
text,
pos,
font,
wrap,
material: {color}
material: {color},
config
})
}
@@ -203,6 +214,31 @@ draw.grid = function grid(rect, spacing, thickness = 1, offset = {x: 0, y: 0}, m
}
}
draw.scissor = function(rect)
{
var screen_rect = null
if (rect && gamestate.camera) {
var bottom_left = gamestate.camera.world_to_window(rect.x, rect.y)
var top_right = gamestate.camera.world_to_window(rect.x + rect.width, rect.y + rect.height)
var screen_left = bottom_left.x
var screen_top = bottom_left.y
var screen_right = top_right.x
var screen_bottom = top_right.y
screen_rect = {
x: Math.round(screen_left),
y: Math.round(screen_top),
width: Math.round(screen_right - screen_left),
height: Math.round(screen_bottom - screen_top)
}
}
current_list.push({
cmd: "scissor",
rect: screen_rect
})
}
draw.add_command = function(cmd)
{
current_list.push(cmd)

View File

@@ -400,10 +400,6 @@ var graphics = use('graphics')
var camera = {}
prosperon.scissor = function(rect) {
device.scissor(rect)
}
// Pipeline component definitions
var default_depth_state = {
compare: "always", // never/less/equal/less_equal/greater/not_equal/greater_equal/always
@@ -587,6 +583,11 @@ var GPU = Symbol()
var cur_cam
var cmd_fns = {}
cmd_fns.scissor = function(cmd)
{
draw_queue.push(cmd)
}
cmd_fns.camera = function(cmd)
{
if (cmd.camera.surface && !cmd.camera.surface[GPU]) {
@@ -718,7 +719,7 @@ cmd_fns.draw_text = function(cmd)
if (!font[GPU])
font[GPU] = get_img_gpu(font.surface)
var size = font.text_size(cmd.text)
var size = font.text_size(cmd.text, cmd.wrap, cmd.config.break, cmd.config.align)
cmd.pos.width ??= size[0]
cmd.pos.height ??= size[1]
@@ -727,7 +728,8 @@ cmd_fns.draw_text = function(cmd)
cmd.pos,
[cmd.material.color.r, cmd.material.color.g, cmd.material.color.b, cmd.material.color.a],
cmd.wrap || 0,
font
cmd.config.break,
cmd.config.align
)
// Ensure material has diffuse property for dynamic binding
@@ -790,11 +792,29 @@ cmd_fns.draw_slice9 = function(cmd)
// Convert single slice value to LRTB object if needed
var slice_lrtb = cmd.slice
if (typeof cmd.slice == 'number') {
var slice_val = cmd.slice
if (slice_val > 0 && slice_val < 1) {
slice_lrtb = {
l: slice_val * img.width,
r: slice_val * img.width,
t: slice_val * img.height,
b: slice_val * img.height
}
} else {
slice_lrtb = {
l: slice_val,
r: slice_val,
t: slice_val,
b: slice_val
}
}
} else {
// Handle percentage values for each side individually
slice_lrtb = {
l: cmd.slice,
r: cmd.slice,
t: cmd.slice,
b: cmd.slice
l: (cmd.slice.l > 0 && cmd.slice.l < 1) ? cmd.slice.l * img.width : cmd.slice.l,
r: (cmd.slice.r > 0 && cmd.slice.r < 1) ? cmd.slice.r * img.width : cmd.slice.r,
t: (cmd.slice.t > 0 && cmd.slice.t < 1) ? cmd.slice.t * img.height : cmd.slice.t,
b: (cmd.slice.b > 0 && cmd.slice.b < 1) ? cmd.slice.b * img.height : cmd.slice.b
}
}
@@ -868,6 +888,15 @@ prosperon.create_batch = function create_batch(draw_cmds, done) {
var buffers_bound = false
for (var cmd of draw_queue) {
if (cmd.cmd == "scissor") {
if (!cmd.rect)
render_pass.scissor({x:0,y:0,width:win_size.width,height:win_size.height})
else {
render_pass.scissor(cmd.rect)
log.console(json.encode(cmd.rect))
}
continue
}
if (cmd.camera) {
if (!cmd.camera.surface && render_target != "swap") {
if (render_pass)

View File

@@ -159,23 +159,22 @@ const char *esc_color(const char *c, struct rgba *color, struct rgba defc)
return c;
}
// text is a string, font f, wrap is how long a line is before wrapping. -1to not wrap
HMM_Vec2 measure_text(const char *text, font *f, float letterSpacing, float wrap)
// 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)
{
int breakAtWord = 0;
int breakAtWord = (breakAt == WORD);
HMM_Vec2 dim = {0};
float maxWidth = 0; // Maximum width of any line
float lineWidth = 0; // Current line width
float maxWidth = 0;
float lineWidth = 0;
float lineHeight = f->ascent - f->descent;
float height = lineHeight; // Total height
const char *wordStart = text; // Start of the current word for word wrapping
float wordWidth = 0; // Width of the current word
float spaceWidth = f->Characters[' '].advance + letterSpacing; // Space character width
float height = lineHeight;
const char *wordStart = text;
float wordWidth = 0;
float spaceWidth = f->Characters[' '].advance + letterSpacing;
for (const char *c = text; *c != '\0'; c++) {
if (*c == '\n') {
// Handle explicit line breaks
maxWidth = fmaxf(maxWidth, lineWidth);
lineWidth = 0;
height += lineHeight + f->linegap;
@@ -186,24 +185,18 @@ HMM_Vec2 measure_text(const char *text, font *f, float letterSpacing, float wrap
float charWidth = f->Characters[(unsigned char)*c].advance + letterSpacing;
// Handle wrapping
if (wrap > 0 && lineWidth + charWidth > wrap) {
if (breakAtWord && *c != ' ') {
// Roll back to the last word if breaking at word boundaries
if (wordWidth > 0) {
lineWidth -= wordWidth + spaceWidth;
c = wordStart - 1; // Reset to start of the word
}
if (breakAtWord && *c != ' ' && wordWidth > 0) {
lineWidth -= wordWidth;
c = wordStart - 1;
}
// Finish the current line and reset
maxWidth = fmaxf(maxWidth, lineWidth);
lineWidth = 0;
height += lineHeight + f->linegap;
wordStart = c + 1; // Start a new word
wordStart = c + 1;
wordWidth = 0;
// Skip to next character if wrapping on letters
if (!breakAtWord) {
lineWidth += charWidth;
continue;
@@ -212,7 +205,6 @@ HMM_Vec2 measure_text(const char *text, font *f, float letterSpacing, float wrap
lineWidth += charWidth;
// Update word width if breaking at word boundaries
if (breakAtWord) {
if (*c == ' ') {
wordWidth = 0;
@@ -223,43 +215,143 @@ HMM_Vec2 measure_text(const char *text, font *f, float letterSpacing, float wrap
}
}
// Finalize dimensions
maxWidth = fmaxf(maxWidth, lineWidth);
dim.x = maxWidth;
dim.y = 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_vert *buffer = NULL;
HMM_Vec2 cursor = pos;
float lineHeight = f->ascent - f->descent;
float lineWidth = 0;
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;
}
for (const char *c = text; *c != 0; c++) {
if (*c == '\n') {
cursor.x = pos.x;
cursor.y -= lineHeight + f->linegap;
lineWidth = 0;
continue;
float charWidth = f->Characters[(unsigned char)*c].advance;
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;
}
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;
}
}
}
struct character chara = f->Characters[(unsigned char)*c];
if (wrap > 0 && lineWidth + chara.advance > wrap) {
cursor.x = pos.x;
cursor.y -= lineHeight + f->linegap;
lineWidth = 0;
// Add final line
if (lineStart < text + strlen(text)) {
Line line = {lineStart, text + strlen(text), lineWidth};
arrput(lines, line);
}
if (!isspace(*c))
draw_char_verts(&buffer, chara, cursor, color);
lineWidth += chara.advance;
cursor.x += chara.advance;
}
return buffer;
// 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;
}
arrfree(lines);
return buffer;
}

View File

@@ -12,7 +12,12 @@ typedef enum {
RIGHT,
CENTER,
JUSTIFY
} ALIGN;
} TEXT_ALIGN;
typedef enum {
WORD,
CHARACTER
} TEXT_BREAK;
struct text_vert {
HMM_Vec2 pos;
@@ -57,7 +62,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);
HMM_Vec2 measure_text(const char *text, font *f, float letterSpacing, float wrap);
struct text_vert *renderText(const char *text, HMM_Vec2 pos, font *f, colorf color, float wrap, TEXT_BREAK breakAt, TEXT_ALIGN align);
HMM_Vec2 measure_text(const char *text, font *f, float letterSpacing, float wrap, TEXT_BREAK breakAt, TEXT_ALIGN align);
#endif

View File

@@ -757,8 +757,7 @@ JSC_CCALL(input_get_events,
int event_count = 0;
while (SDL_PollEvent(&event)) {
// Process event with ImGui first
// gui_input(&event);
gui_input(&event);
WotaBuffer wb = event2wota(&event);
JSValue event_obj = wota2value(js, wb.data);

View File

@@ -68,9 +68,15 @@ JSC_CCALL(staef_font_text_size,
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 == "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));
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)
@@ -80,11 +86,17 @@ JSC_CCALL(staef_font_make_text_buffer,
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 == "character") ? CHARACTER : WORD;
const char *align = argc > 5 ? JS_ToCString(js, argv[5]) : NULL;
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);
text_vert *buffer = renderText(s, startpos, f, c, wrap, breakAt, alignAt);
ret = quads_to_mesh(js, buffer);
JS_FreeCString(js, s);
if (breakat) JS_FreeCString(js, breakat);
if (align) JS_FreeCString(js, align);
arrfree(buffer);
)
@@ -93,8 +105,8 @@ JSC_GETSET(font, linegap, number)
// Font methods
static const JSCFunctionListEntry js_font_funcs[] = {
MIST_FUNC_DEF(staef_font, text_size, 3),
MIST_FUNC_DEF(staef_font, make_text_buffer, 4),
MIST_FUNC_DEF(staef_font, text_size, 4),
MIST_FUNC_DEF(staef_font, make_text_buffer, 6),
CGETSET_ADD(font, linegap),
};