text wrap
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
194
source/font.c
194
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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user