quakeforge/libs/ui/text.c
Bill Currie 32346b6123 [ui] Add support for simple text strings
The same underlying mechanism is used for both simple text strings and
passages, but without the intervening hierarchy of paragraphs etc.
Results in only the one view for a simple text string.
2023-07-01 19:42:02 +09:00

433 lines
13 KiB
C

/*
text.c
Font text management
Copyright (C) 2022 Bill Currie <bill@taniwha.org>
Author: Bill Currie <bill@taniwha.org>
Date: 2022/8/26
This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to:
Free Software Foundation, Inc.
59 Temple Place - Suite 330
Boston, MA 02111-1307, USA
*/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#include <math.h>
#include "QF/mathlib.h"
#include "QF/quakefs.h"
#include "QF/sys.h"
#include "QF/ui/font.h"
#include "QF/ui/passage.h"
#include "QF/ui/text.h"
#include "QF/ui/view.h"
#include "compat.h"
static void
text_passage_glyphs_destroy (void *_glyphset)
{
glyphset_t *glyphset = _glyphset;
free (glyphset->glyphs);
}
static void
text_features_create (void *_features)
{
featureset_t *features = _features;
*features = (featureset_t) DARRAY_STATIC_INIT (4);
}
static void
text_features_destroy (void *_features)
{
featureset_t *features = _features;
DARRAY_CLEAR (features);
}
const component_t text_components[text_comp_count] = {
[text_passage_glyphs] = {
.size = sizeof (glyphset_t),
.name = "passage glyphs",
.destroy = text_passage_glyphs_destroy,
},
[text_glyphs] = {
.size = sizeof (glyphref_t),
.name = "glyphs",
},
[text_script] = {
.size = sizeof (script_component_t),
.name = "script",
},
[text_font] = {
.size = sizeof (font_t *),
.name = "font",
},
[text_features] = {
.size = sizeof (featureset_t),
.name = "features",
.create = text_features_create,
.destroy = text_features_destroy,
},
};
typedef struct glyphnode_s {
struct glyphnode_s *next;
uint32_t ent;
uint32_t count;
glyphobj_t *glyphs;
int mins[2];
int maxs[2];
} glyphnode_t;
static view_resize_f text_flow_funcs[] = {
[text_right_down] = view_flow_right_down,
[text_left_down] = view_flow_left_down,
[text_down_right] = view_flow_down_right,
[text_up_right] = view_flow_up_right,
[text_right_up] = view_flow_right_up,
[text_left_up] = view_flow_left_up,
[text_down_left] = view_flow_down_left,
[text_up_left] = view_flow_up_left,
};
static void
layout_glyphs (glyphnode_t *node, font_t *font, unsigned glpyhCount,
hb_glyph_info_t *glyphInfo, hb_glyph_position_t *glyphPos)
{
int x = 0, y = 0;
for (unsigned k = 0; k < glpyhCount; k++) {
uint32_t glyphid = glyphInfo[k].codepoint;
vec2i_t bearing = font->glyph_bearings[glyphid];
int xp = glyphPos[k].x_offset / 64;
int yp = glyphPos[k].y_offset / 64;
int xa = glyphPos[k].x_advance / 64;
int ya = glyphPos[k].y_advance / 64;
xp += bearing[0];
yp += bearing[1];
int xm = xp + font->glyph_rects[glyphid].width;
int ym = yp - font->glyph_rects[glyphid].height;
if (xm - xp == 0) {
xm = xa;
}
if (ym - yp == 0) {
ym = ya;
}
node->mins[0] = min (node->mins[0], xp + x);
node->mins[1] = min (node->mins[1], ym + y);
node->maxs[0] = max (node->maxs[0], xm + x);
node->maxs[1] = max (node->maxs[1], yp + y);
node->glyphs[k] = (glyphobj_t) {
.glyphid = glyphid,
.x = x + xp,
.y = y - yp,
.fontid = font->fontid,
};
x += xa;
y += ya;
}
}
static void
configure_textview (view_t textview, glyphobj_t *glyphs, glyphnode_t *node,
uint32_t glyphref_base, uint32_t c_glyphs)
{
glyphref_t glyph_ref = {
.start = glyphref_base,
.count = node->count,
};
for (uint32_t k = 0; k < node->count; k++) {
glyphobj_t *go = node->glyphs + k;
glyphs[glyphref_base + k] = (glyphobj_t) {
.glyphid = go->glyphid,
.x = go->x + node->mins[0],
.y = go->y + node->maxs[1],
.fontid = go->fontid,
};
}
Ent_SetComponent (textview.id, c_glyphs, textview.reg, &glyph_ref);
View_SetPos (textview, node->mins[0], -node->mins[1]);
View_SetLen (textview, node->maxs[0] - node->mins[0],
node->maxs[1] - node->mins[1]);
}
view_t
Text_PassageView (text_system_t textsys, font_t *font, passage_t *passage)
{
auto reg = textsys.reg;
uint32_t c_script = textsys.text_base + text_script;
uint32_t c_features = textsys.text_base + text_features;
uint32_t c_glyphs = textsys.text_base + text_glyphs;
uint32_t c_passage_glyphs = textsys.text_base + text_passage_glyphs;
hierarchy_t *h = passage->hierarchy;
psg_text_t *text_objects = h->components[passage_type_text_obj];
glyphnode_t *glyph_nodes = 0;
glyphnode_t **head = &glyph_nodes;
hb_font_t *fnt = hb_ft_font_create (font->face, 0);
hb_buffer_t *buffer = hb_buffer_create ();
hb_buffer_allocation_successful (buffer);
hb_script_t psg_script = HB_SCRIPT_LATIN;
text_dir_e psg_direction = text_right_down;
hb_language_t psg_language = hb_language_from_string ("en", 2);
featureset_t passage_features = DARRAY_STATIC_INIT (0);
featureset_t *psg_features = &passage_features;
uint32_t glyph_count = 0;
// passages have only the root, paragraphs, and text objects in their
// hierarchies
for (uint32_t i = 0; i < h->childCount[0]; i++) {
uint32_t paragraph = h->childIndex[0] + i;
uint32_t para_ent = h->ent[paragraph];
hb_script_t para_script = psg_script;
text_dir_e para_direction = psg_direction;
hb_language_t para_language = psg_language;
featureset_t *para_features = psg_features;;
if (Ent_HasComponent (para_ent, c_script, reg)) {
script_component_t *s = Ent_GetComponent (para_ent, c_script, reg);
para_script = s->script;
para_language = s->language;
para_direction = s->direction;
}
if (Ent_HasComponent (para_ent, c_features, reg)) {
para_features = Ent_GetComponent (para_ent, c_features, reg);
}
for (uint32_t j = 0; j < h->childCount[paragraph]; j++) {
uint32_t textind = h->childIndex[paragraph] + j;
uint32_t text_ent = h->ent[textind];
psg_text_t *textobj = &text_objects[textind];
const char *str = passage->text + textobj->text;
uint32_t len = textobj->size;
hb_script_t txt_script = para_script;
text_dir_e txt_direction = para_direction;
hb_language_t txt_language = para_language;
featureset_t *txt_features = para_features;;
if (Ent_HasComponent (text_ent, c_script, reg)) {
script_component_t *s = Ent_GetComponent (text_ent, c_script,
reg);
txt_script = s->script;
txt_language = s->language;
txt_direction = s->direction;
}
if (Ent_HasComponent (text_ent, c_features, reg)) {
txt_features = Ent_GetComponent (text_ent, c_features, reg);
}
hb_buffer_reset (buffer);
hb_buffer_set_direction (buffer, txt_direction | HB_DIRECTION_LTR);
hb_buffer_set_script (buffer, txt_script);
hb_buffer_set_language (buffer, txt_language);
hb_buffer_add_utf8 (buffer, str, len, 0, len);
hb_shape (fnt, buffer, txt_features->a, txt_features->size);
unsigned c;
auto glyphInfo = hb_buffer_get_glyph_infos (buffer, &c);
auto glyphPos = hb_buffer_get_glyph_positions (buffer, &c);
*head = alloca (sizeof (glyphnode_t));
**head = (glyphnode_t) {
.ent = h->ent[textind],
.count = c,
.glyphs = alloca (c * sizeof (glyphobj_t)),
.mins = { 0, 0 },
.maxs = { INT32_MIN, INT32_MIN },
};
glyph_count += c;
layout_glyphs (*head, font, c, glyphInfo, glyphPos);
head = &(*head)->next;
}
}
ecs_system_t viewsys = { reg, textsys.view_base };
view_t passage_view = View_AddToEntity (h->ent[0], viewsys, nullview);
glyphref_t passage_ref = {};
glyphobj_t *glyphs = malloc (glyph_count * sizeof (glyphobj_t));
glyphnode_t *g = glyph_nodes;
// paragraph flow
int psg_vertical = !!(psg_direction & 2);
for (uint32_t i = 0; i < h->childCount[0]; i++) {
uint32_t paragraph = h->childIndex[0] + i;
view_t paraview = View_AddToEntity (h->ent[paragraph], viewsys,
passage_view);
glyphref_t pararef = { .start = passage_ref.count };
for (uint32_t j = 0; j < h->childCount[paragraph]; j++, g = g->next) {
uint32_t to = h->childIndex[paragraph] + j;
view_t textview = View_AddToEntity (h->ent[to], viewsys,
paraview);
configure_textview (textview, glyphs, g, passage_ref.count,
c_glyphs);
View_SetGravity (textview, grav_flow);
passage_ref.count += g->count;
}
pararef.count = passage_ref.count - pararef.start;
Ent_SetComponent (paraview.id, c_glyphs, reg, &pararef);
uint32_t para_ent = h->ent[paragraph];
text_dir_e para_direction = psg_direction;
if (Ent_HasComponent (para_ent, c_script, reg)) {
script_component_t *s = Ent_GetComponent (para_ent, c_script,
reg);
para_direction = s->direction;
}
View_SetOnResize (paraview, text_flow_funcs[para_direction]);
View_SetResize (paraview, !psg_vertical, psg_vertical);
View_SetGravity (paraview, grav_northwest);
View_Control (paraview)->flow_size = 1;
}
Ent_SetComponent (passage_view.id, c_glyphs, reg, &passage_ref);
glyphset_t glyphset = {
.glyphs = glyphs,
.count = glyph_count,
};
Ent_SetComponent (passage_view.id, c_passage_glyphs, reg, &glyphset);
hb_buffer_destroy (buffer);
hb_font_destroy (fnt);
return passage_view;
}
view_t
Text_StringView (text_system_t textsys, view_t parent,
font_t *font, const char *str, uint32_t len,
script_component_t *sc, featureset_t *fs)
{
auto reg = textsys.reg;
uint32_t c_glyphs = textsys.text_base + text_glyphs;
uint32_t c_passage_glyphs = textsys.text_base + text_passage_glyphs;
glyphnode_t *glyph_nodes = 0;
glyphnode_t **head = &glyph_nodes;
hb_font_t *fnt = hb_ft_font_create (font->face, 0);
hb_buffer_t *buffer = hb_buffer_create ();
hb_buffer_allocation_successful (buffer);
hb_script_t script = HB_SCRIPT_LATIN;
text_dir_e direction = text_right_down;
hb_language_t language = hb_language_from_string ("en", 2);
featureset_t text_features = DARRAY_STATIC_INIT (0);
featureset_t *features = &text_features;
if (sc) {
script = sc->script;
language = sc->language;
direction = sc->direction;
}
if (fs) {
features = fs;
}
uint32_t glyph_count = 0;
hb_buffer_reset (buffer);
hb_buffer_set_direction (buffer, direction | HB_DIRECTION_LTR);
hb_buffer_set_script (buffer, script);
hb_buffer_set_language (buffer, language);
hb_buffer_add_utf8 (buffer, str, len, 0, len);
hb_shape (fnt, buffer, features->a, features->size);
unsigned c;
auto glyphInfo = hb_buffer_get_glyph_infos (buffer, &c);
auto glyphPos = hb_buffer_get_glyph_positions (buffer, &c);
*head = alloca (sizeof (glyphnode_t));
**head = (glyphnode_t) {
.ent = nullent,
.count = c,
.glyphs = alloca (c * sizeof (glyphobj_t)),
.mins = { 0, 0 },
.maxs = { INT32_MIN, INT32_MIN },
};
glyph_count += c;
layout_glyphs (*head, font, c, glyphInfo, glyphPos);
head = &(*head)->next;
ecs_system_t viewsys = { reg, textsys.view_base };
view_t stringview = View_New (viewsys, parent);
glyphref_t passage_ref = {};
glyphobj_t *glyphs = malloc (glyph_count * sizeof (glyphobj_t));
glyphnode_t *g = glyph_nodes;
configure_textview (stringview, glyphs, g, passage_ref.count, c_glyphs);
passage_ref.count += g->count;
glyphset_t glyphset = {
.glyphs = glyphs,
.count = glyph_count,
};
Ent_SetComponent (stringview.id, c_passage_glyphs, reg, &glyphset);
hb_buffer_destroy (buffer);
hb_font_destroy (fnt);
return stringview;
}
void
Text_SetScript (text_system_t textsys, uint32_t textid, const char *lang,
hb_script_t script, text_dir_e dir)
{
script_component_t scr = {
.language = hb_language_from_string (lang, strlen (lang)),
.script = script,
.direction = dir,
};
Ent_SetComponent (textid, textsys.text_base + text_script, textsys.reg,
&scr);
}
void
Text_SetFont (text_system_t textsys, uint32_t textid, font_t *font)
{
Ent_SetComponent (textid, textsys.text_base + text_font, textsys.reg,
&font);
}
void
Text_SetFeatures (text_system_t textsys, uint32_t textid,
featureset_t *features)
{
uint32_t features_comp = textsys.text_base + text_features;
if (Ent_HasComponent (textid, features_comp, textsys.reg)) {
Ent_RemoveComponent (textid, features_comp, textsys.reg);
}
featureset_t *f = Ent_SetComponent (textid, features_comp, textsys.reg, 0);
DARRAY_RESIZE (f, features->size);
memcpy (f->a, features->a, features->size * sizeof (hb_feature_t));
}
void
Text_AddFeature (text_system_t textsys, uint32_t textid, hb_feature_t feature)
{
uint32_t features_comp = textsys.text_base + text_features;
if (!Ent_HasComponent (textid, features_comp, textsys.reg)) {
Ent_SetComponent (textid, features_comp, textsys.reg, 0);
}
featureset_t *f = Ent_GetComponent (textid, features_comp, textsys.reg);
DARRAY_APPEND (f, feature);
}