Skip to content

Commit 1fcaf19

Browse files
authored
Merge pull request #112 from yuchanns/feat/no-break-text-groups
feat(text): add no-break wrap groups
2 parents 2215d26 + fc8d019 commit 1fcaf19

3 files changed

Lines changed: 203 additions & 86 deletions

File tree

docs/material_text.lua

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -101,12 +101,17 @@ end
101101
---Text material module.
102102
---
103103
---Tagged text stream 支持 `[i42]` icon、`[hex]` 临时颜色、`[sN]` style 切换、
104-
---`[s]` 回到 style 0,以及 `[[` 输出字面量 `[`。`[sN]` 中的 `N` 是
105-
---0-based style id,Lua style 数组第一项对应 style 0。
104+
---`[s]` 回到 style 0、`[wN]` / `[w]` 标记不可拆分换行组,以及 `[[`
105+
---输出字面量 `[`。`[sN]` 中的 `N` 是 0-based style id,Lua style 数组
106+
---第一项对应 style 0。`[wN]` 中的 `N` 是调用方自定义的正整数 group id;
107+
---group 本身比行宽更宽时会退回普通逐字换行。
106108
---
107109
---Tagged text streams support `[i42]` icons, `[hex]` temporary colors, `[sN]`
108-
---style switching, `[s]` reset to style 0, and `[[` for a literal `[`.
109-
---`N` in `[sN]` is a 0-based style id. The first Lua style entry is style 0.
110+
---style switching, `[s]` reset to style 0, `[wN]` / `[w]` no-break wrap
111+
---groups, and `[[` for a literal `[`. `N` in `[sN]` is a 0-based style id.
112+
---The first Lua style entry is style 0. `N` in `[wN]` is a caller-defined
113+
---positive group id. A group wider than the line falls back to normal
114+
---per-character wrapping.
110115
---@class soluna.material.text
111116
local mattext = {}
112117

src/material_text.c

Lines changed: 162 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -658,7 +658,7 @@ tohex(char c) {
658658
}
659659

660660
static const char *
661-
parse_bracket(struct block_context *ctx, const char *str, int *icon) {
661+
parse_bracket(struct block_context *ctx, const char *str, int *icon, int *group) {
662662
char c = *str;
663663
int hex = -1;
664664
if (c == 'i') {
@@ -698,11 +698,116 @@ parse_bracket(struct block_context *ctx, const char *str, int *icon) {
698698
} else if (c == 'n') {
699699
ctx->fs = &ctx->s->s[0];
700700
ctx->color = ctx->fs->color;
701+
} else if (c == 'w') {
702+
++str;
703+
int word_group = 0;
704+
while (*str >= '0' && *str <= '9') {
705+
word_group = word_group * 10 + (*str - '0');
706+
++str;
707+
}
708+
*group = word_group;
701709
}
702710
// todo: other command
703711
return skip_bracket(str);
704712
}
705713

714+
enum text_token_type {
715+
TEXT_TOKEN_END,
716+
TEXT_TOKEN_GROUP,
717+
TEXT_TOKEN_NEWLINE,
718+
TEXT_TOKEN_SPACE,
719+
TEXT_TOKEN_GLYPH,
720+
};
721+
722+
struct text_token {
723+
enum text_token_type type;
724+
int advance_x;
725+
int dy;
726+
int codepoint;
727+
int font;
728+
};
729+
730+
static const char *
731+
next_text_token(const char *str, struct block_context *ctx, int *group,
732+
struct text_token *token) {
733+
struct font_manager *mgr = ctx->s->font;
734+
for (;;) {
735+
uint32_t val = 0;
736+
str = utf8_decode(str, &val);
737+
if (str == NULL || val == 0) {
738+
token->type = TEXT_TOKEN_END;
739+
return str;
740+
}
741+
if (val <= 32) {
742+
if (val == '\n') {
743+
token->type = TEXT_TOKEN_NEWLINE;
744+
return str;
745+
}
746+
struct font_glyph g, og;
747+
if (font_manager_glyph(mgr, ctx->fs->fontid, ' ', ctx->fs->size, &g, &og) == NULL) {
748+
token->type = TEXT_TOKEN_SPACE;
749+
token->advance_x = g.advance_x;
750+
return str;
751+
}
752+
continue;
753+
}
754+
755+
int icon = 0;
756+
if (val == '[') {
757+
if (*str != '[') {
758+
int previous_group = *group;
759+
str = parse_bracket(ctx, str, &icon, group);
760+
if (*group != previous_group) {
761+
token->type = TEXT_TOKEN_GROUP;
762+
return str;
763+
}
764+
if (!icon)
765+
continue;
766+
} else {
767+
++str;
768+
}
769+
}
770+
771+
int codepoint = val;
772+
int font = ctx->fs->fontid;
773+
int dy = 0;
774+
if (icon > 0) {
775+
codepoint = icon - 1;
776+
font = FONT_ICON;
777+
dy = -ctx->fs->ascent;
778+
}
779+
struct font_glyph g, og;
780+
if (font_manager_glyph(mgr, font, codepoint, ctx->fs->size, &g, &og) == NULL) {
781+
token->type = TEXT_TOKEN_GLYPH;
782+
token->advance_x = g.advance_x;
783+
token->dy = dy;
784+
token->codepoint = codepoint;
785+
token->font = font;
786+
return str;
787+
}
788+
}
789+
}
790+
791+
static int
792+
measure_word_group(const char *str, struct block_context *ctx, int group) {
793+
struct block_context probe = *ctx;
794+
int current_group = group;
795+
int width = 0;
796+
for (;;) {
797+
struct text_token token;
798+
str = next_text_token(str, &probe, &current_group, &token);
799+
if (token.type == TEXT_TOKEN_END || token.type == TEXT_TOKEN_GROUP ||
800+
token.type == TEXT_TOKEN_NEWLINE)
801+
break;
802+
if (token.type == TEXT_TOKEN_SPACE || token.type == TEXT_TOKEN_GLYPH) {
803+
width += token.advance_x;
804+
if (width > probe.width)
805+
break;
806+
}
807+
}
808+
return width;
809+
}
810+
706811
static int
707812
ltext_(lua_State *L, struct styles *s, int gen_layout, int external) {
708813
const char * str = luaL_checkstring(L, 1);
@@ -713,7 +818,6 @@ ltext_(lua_State *L, struct styles *s, int gen_layout, int external) {
713818
ctx.width = luaL_optinteger(L, 2, MAX_WIDTH);
714819
ctx.height = luaL_optinteger(L, 3, MAX_HEIGHT);
715820

716-
struct font_manager *mgr = s->font;
717821
ctx.x = 0;
718822
ctx.color = s->s[0].color;
719823
ctx.y = 0;
@@ -724,7 +828,6 @@ ltext_(lua_State *L, struct styles *s, int gen_layout, int external) {
724828
ctx.line_height = 0;
725829
ctx.alignment = lua_tointeger(L, lua_upvalueindex(5));
726830

727-
char * buffer = NULL;
728831
struct text_primitive * prim = NULL;
729832
struct layout * pos = NULL;
730833
if (gen_layout) {
@@ -739,88 +842,76 @@ ltext_(lua_State *L, struct styles *s, int gen_layout, int external) {
739842
pos->line_count = 0;
740843
set_layout_styles(L, pos, s, external);
741844
} else {
742-
buffer = (char *)malloc(count * sizeof(struct text_primitive)+1);
743-
prim = (struct text_primitive *)buffer;
845+
prim = (struct text_primitive *)malloc((size_t)count * sizeof(*prim) + 1);
846+
if (prim == NULL)
847+
return luaL_error(L, "Out of memory");
744848
}
745849
int n = 0;
850+
int group = 0;
746851
for (;;) {
747-
uint32_t val = 0;
748-
str = utf8_decode(str, &val);
749-
if (str == NULL || val == 0)
852+
struct text_token token;
853+
str = next_text_token(str, &ctx, &group, &token);
854+
if (token.type == TEXT_TOKEN_END)
750855
break;
751-
if (val <= 32) {
752-
if (val == '\n') {
753-
if (ctx.x > ctx.line_width)
754-
ctx.line_width = ctx.x;
755-
if (newline(&ctx, prim, n, pos))
756-
break;
757-
} else {
758-
struct font_glyph g, og;
759-
if (font_manager_glyph(mgr, ctx.fs->fontid, ' ', ctx.fs->size, &g, &og) == NULL) {
856+
if (token.type == TEXT_TOKEN_GROUP) {
857+
if (group > 0 && ctx.x > 0) {
858+
int group_width = measure_word_group(str, &ctx, group);
859+
if (group_width <= ctx.width &&
860+
ctx.x + group_width > ctx.width) {
760861
if (ctx.x > ctx.line_width)
761862
ctx.line_width = ctx.x;
762-
if (advance_space(&ctx, prim, pos, &n, g.advance_x))
863+
if (newline(&ctx, prim, n, pos))
763864
break;
764865
}
765866
}
867+
continue;
868+
}
869+
if (token.type == TEXT_TOKEN_NEWLINE) {
870+
if (ctx.x > ctx.line_width)
871+
ctx.line_width = ctx.x;
872+
if (newline(&ctx, prim, n, pos))
873+
break;
874+
continue;
875+
}
876+
if (token.type == TEXT_TOKEN_SPACE) {
877+
if (ctx.x > ctx.line_width)
878+
ctx.line_width = ctx.x;
879+
if (advance_space(&ctx, prim, pos, &n, token.advance_x))
880+
break;
881+
continue;
882+
}
883+
884+
if (prim) {
885+
prim[n].pos.x = ctx.x * PIXEL_SCALE;
886+
prim[n].pos.y = (ctx.y + token.dy) * PIXEL_SCALE;
887+
prim[n].pos.sr = 0;
888+
prim[n].pos.sprite = -material_id;
766889
} else {
767-
int icon = 0;
768-
if (val == '[') {
769-
if (*str != '[') {
770-
str = parse_bracket(&ctx, str, &icon);
771-
if (!icon) {
772-
continue;
773-
}
774-
} else {
775-
++str;
776-
}
777-
}
778-
int dy = 0;
779-
int codepoint = val;
780-
int font = ctx.fs->fontid;
781-
782-
if (icon > 0) {
783-
codepoint = icon -1;
784-
font = FONT_ICON;
785-
786-
dy = - ctx.fs->ascent;
787-
}
788-
890+
// assert(pos != NULL)
891+
set_position(pos, n, &ctx);
892+
}
893+
894+
if (ctx.x > ctx.line_width)
895+
ctx.line_width = ctx.x;
896+
if (advance(pos, &ctx, token.advance_x)) {
897+
if (newline(&ctx, prim, n, pos))
898+
break;
789899
if (prim) {
790900
prim[n].pos.x = ctx.x * PIXEL_SCALE;
791-
prim[n].pos.y = ( ctx.y + dy ) * PIXEL_SCALE;
792-
prim[n].pos.sr = 0;
793-
prim[n].pos.sprite = -material_id;
901+
prim[n].pos.y = (ctx.y + token.dy) * PIXEL_SCALE;
794902
} else {
795-
// assert(pos != NULL)
796903
set_position(pos, n, &ctx);
797904
}
798-
799-
struct font_glyph g, og;
800-
if (font_manager_glyph(mgr, font, codepoint, ctx.fs->size, &g, &og) == NULL) {
801-
if (ctx.x > ctx.line_width)
802-
ctx.line_width = ctx.x;
803-
if (advance(pos, &ctx, g.advance_x)) {
804-
if (newline(&ctx, prim, n, pos))
805-
break;
806-
if (prim) {
807-
prim[n].pos.x = ctx.x * PIXEL_SCALE;
808-
prim[n].pos.y = ( ctx.y + dy ) * PIXEL_SCALE;
809-
} else {
810-
set_position(pos, n, &ctx);
811-
}
812-
advance(pos, &ctx, g.advance_x);
813-
}
814-
if (prim) {
815-
prim[n].u.text.header.sprite = -1;
816-
prim[n].u.text.codepoint = codepoint;
817-
prim[n].u.text.font = font;
818-
prim[n].u.text.size = ctx.fs->size;
819-
prim[n].u.text.color = ctx.color;
820-
}
821-
++n;
822-
}
905+
advance(pos, &ctx, token.advance_x);
906+
}
907+
if (prim) {
908+
prim[n].u.text.header.sprite = -1;
909+
prim[n].u.text.codepoint = token.codepoint;
910+
prim[n].u.text.font = token.font;
911+
prim[n].u.text.size = ctx.fs->size;
912+
prim[n].u.text.color = ctx.color;
823913
}
914+
++n;
824915
}
825916
if (ctx.x > ctx.line_width)
826917
ctx.line_width = ctx.x;
@@ -864,7 +955,8 @@ ltext_(lua_State *L, struct styles *s, int gen_layout, int external) {
864955
prim[i].pos.y += offy;
865956
}
866957
}
867-
lua_pushexternalstring(L, buffer, n * sizeof(struct text_primitive), free_primitive, NULL);
958+
lua_pushexternalstring(L, (char *)prim,
959+
n * sizeof(struct text_primitive), free_primitive, NULL);
868960
lua_pushinteger(L, height);
869961
return 2;
870962
} else {

0 commit comments

Comments
 (0)