diff --git a/prosperon/clay.cm b/prosperon/clay.cm index 9201cbf1..958cd5b5 100644 --- a/prosperon/clay.cm +++ b/prosperon/clay.cm @@ -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) { diff --git a/prosperon/draw2d.cm b/prosperon/draw2d.cm index 44f34dd8..b9dae346 100644 --- a/prosperon/draw2d.cm +++ b/prosperon/draw2d.cm @@ -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) diff --git a/prosperon/prosperon.cm b/prosperon/prosperon.cm index dfc3c33a..1e4b4103 100644 --- a/prosperon/prosperon.cm +++ b/prosperon/prosperon.cm @@ -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) diff --git a/source/font.c b/source/font.c index df87dda8..78aae578 100644 --- a/source/font.c +++ b/source/font.c @@ -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; } diff --git a/source/font.h b/source/font.h index 9055f671..4411612a 100644 --- a/source/font.h +++ b/source/font.h @@ -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 diff --git a/source/qjs_sdl_input.c b/source/qjs_sdl_input.c index 4075bb66..353f5201 100644 --- a/source/qjs_sdl_input.c +++ b/source/qjs_sdl_input.c @@ -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); diff --git a/source/qjs_staef.c b/source/qjs_staef.c index 2fd65547..0be03c39 100644 --- a/source/qjs_staef.c +++ b/source/qjs_staef.c @@ -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), };