ltkx

GUI toolkit for X11 (WIP)
git clone git://lumidify.org/ltkx.git
Log | Files | Refs | README | LICENSE

commit 05e4c9d086c3db2ad726f789cee1b9f8d7ea07b9
parent 93dfd2ced8dc537d2a667857f39b07bcc8307892
Author: lumidify <nobody@lumidify.org>
Date:   Wed,  6 May 2020 20:59:25 +0200

Start work on textedit

Diffstat:
MNOTES | 6++++++
Mtest1.c | 3++-
Mtext-hb.c | 8+++++++-
Atextedit_wip.c | 798+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atextedit_wip.h | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 988 insertions(+), 2 deletions(-)

diff --git a/NOTES b/NOTES @@ -24,3 +24,9 @@ an abstract LtkTextLine that can be rendered without caring about such details. This would easily allow one file for shaping with harfbuzz, one for just using the regular kerning with stb_truetype, etc. + +Text editor tags (highlight, etc.) - just store in +linked list or something similar in sorted order; +when looping over characters to draw them, keep a +pointer to the current tag, then change it to the +next one if the end of its bound comes diff --git a/test1.c b/test1.c @@ -34,7 +34,8 @@ int main(int argc, char *argv[]) LtkButton *button3 = ltk_create_button(window1, "I'm a button!", NULL); ltk_grid_widget(button3, grid1, 1, 0, 1, 1, LTK_STICKY_TOP | LTK_STICKY_BOTTOM | LTK_STICKY_RIGHT); //LtkButton *button4 = ltk_create_button(window1, "I'm a button!", NULL); - LtkButton *button4 = ltk_create_button(window1, "ہمارے بارے میں blablabla", NULL); + //LtkButton *button4 = ltk_create_button(window1, "ہمارے بارے میں blablabla", NULL); + LtkButton *button4 = ltk_create_button(window1, "پَیدایش", NULL); ltk_grid_widget(button4, grid1, 1, 1, 1, 1, LTK_STICKY_LEFT | LTK_STICKY_BOTTOM); ltk_mainloop(); } diff --git a/text-hb.c b/text-hb.c @@ -500,7 +500,7 @@ ltk_create_text_segment(LtkTextManager *tm, uint32_t *text, unsigned int len, ui hb_direction_t dir = hb_script_get_horizontal_direction(script); hb_buffer_set_direction(buf, dir); hb_buffer_set_script(buf, script); - hb_buffer_add_codepoints(buf, ts->str, len, 0, len); + hb_buffer_add_codepoints(buf, ts->str, len, 1, len-1); /* According to https://harfbuzz.github.io/the-distinction-between-levels-0-and-1.html * this should be level 1 clustering instead of level 0 */ hb_buffer_set_cluster_level(buf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); @@ -511,11 +511,16 @@ ltk_create_text_segment(LtkTextManager *tm, uint32_t *text, unsigned int len, ui float scale = stbtt_ScaleForMappingEmToPixels(&font->info, size); LtkGlyph *last_glyph = NULL; + /* FIXME: read https://harfbuzz.github.io/harfbuzz-hb-buffer.html#hb-glyph-position-t-struct */ + int x_min = INT_MAX, x_max = INT_MIN, y_min = INT_MAX, y_max = INT_MIN; int x_abs = 0, y_abs = 0, x1_abs, y1_abs, x2_abs, y2_abs; /* magic, do not touch */ LtkGlyph *glyph; for (int i = 0; i < text_len; i++) { + if (len == 7) { + printf("%d\n", ginf[i].cluster); + } gi = &ginf[i]; gp = &gpos[i]; glyph = malloc(sizeof(LtkGlyph)); @@ -572,6 +577,7 @@ ltk_create_text_segment(LtkTextManager *tm, uint32_t *text, unsigned int len, ui ts->y_max = y_max; font->refs++; + /* FIXME: destroy hb_buffer */ return ts; } diff --git a/textedit_wip.c b/textedit_wip.c @@ -0,0 +1,798 @@ +/* + * This file is part of the Lumidify ToolKit (LTK) + * Copyright (c) 2017, 2018, 2020 lumidify <nobody@lumidify.org> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <limits.h> +#include <X11/Xlib.h> +#include <X11/Xutil.h> +#include "stb_truetype.h" /* http://nothings.org/stb/stb_truetype.h */ +#include <fontconfig/fontconfig.h> +#include "khash.h" +#include <fribidi.h> +#include <harfbuzz/hb.h> +#include <harfbuzz/hb-ot.h> +#include "text-hb.h" +#include "ltk.h" + +extern Ltk *ltk_global; + +/* These unicode routines are taken from + * https://github.com/JeffBezanson/cutef8 */ + +/* is c the start of a utf8 sequence? */ +#define isutf(c) (((c)&0xC0)!=0x80) + +static const uint32_t offsetsFromUTF8[6] = { + 0x00000000UL, 0x00003080UL, 0x000E2080UL, + 0x03C82080UL, 0xFA082080UL, 0x82082080UL +}; + +/* next character without NUL character terminator */ +uint32_t u8_nextmemchar(const char *s, size_t *i) +{ + uint32_t ch = 0; + size_t sz = 0; + do { + ch <<= 6; + ch += (unsigned char)s[(*i)++]; + sz++; + } while (!isutf(s[*i])); + ch -= offsetsFromUTF8[sz-1]; + + return ch; +} + +/* number of characters in NUL-terminated string */ +size_t u8_strlen(const char *s) +{ + size_t count = 0; + size_t i = 0, lasti; + + while (1) { + lasti = i; + while (s[i] > 0) + i++; + count += (i-lasti); + if (s[i++]==0) break; + (void)(isutf(s[++i]) || isutf(s[++i]) || ++i); + count++; + } + return count; +} + +size_t u8_wc_toutf8(char *dest, uint32_t ch) +{ + if (ch < 0x80) { + dest[0] = (char)ch; + return 1; + } + if (ch < 0x800) { + dest[0] = (ch>>6) | 0xC0; + dest[1] = (ch & 0x3F) | 0x80; + return 2; + } + if (ch < 0x10000) { + dest[0] = (ch>>12) | 0xE0; + dest[1] = ((ch>>6) & 0x3F) | 0x80; + dest[2] = (ch & 0x3F) | 0x80; + return 3; + } + if (ch < 0x110000) { + dest[0] = (ch>>18) | 0xF0; + dest[1] = ((ch>>12) & 0x3F) | 0x80; + dest[2] = ((ch>>6) & 0x3F) | 0x80; + dest[3] = (ch & 0x3F) | 0x80; + return 4; + } + return 0; +} + +LtkTextManager * +ltk_init_text(char *font_name) +{ + LtkTextManager *tm = malloc(sizeof(LtkTextManager)); + if (!tm) { + (void)printf("Memory exhausted when trying to create text manager."); + exit(1); + } + tm->font_paths = kh_init(fontid); + tm->font_cache = kh_init(fontstruct); + tm->glyph_cache = kh_init(glyphcache); + tm->font_id_cur = 0; + ltk_load_default_font(tm, font_name); + + return tm; +} + +void +ltk_destroy_text_manager(LtkTextManager *tm) +{ + int k; + + kh_destroy(fontid, tm->font_paths); + + for (k = kh_begin(tm->font_cache); k != kh_end(tm->font_cache); k++) { + if (kh_exist(tm->font_cache, k)) { + ltk_destroy_font(kh_value(tm->font_cache, k)); + } + } + kh_destroy(fontstruct, tm->font_cache); + + for (k = kh_begin(tm->glyph_cache); k != kh_end(tm->glyph_cache); k++) { + if (kh_exist(tm->glyph_cache, k)) { + ltk_destroy_glyph_cache(kh_value(tm->glyph_cache, k)); + } + } + kh_destroy(glyphcache, tm->glyph_cache); + + free(tm); +} + +LtkGlyphInfo * +ltk_create_glyph_info(LtkFont *font, unsigned int id, float scale) +{ + LtkGlyphInfo *glyph = malloc(sizeof(LtkGlyphInfo)); + if (!glyph) { + (void)printf("Out of memory!\n"); + exit(1); + } + + glyph->id = id; + glyph->refs = 0; + glyph->alphamap = stbtt_GetGlyphBitmap( + &font->info, scale, scale, id, &glyph->w, + &glyph->h, &glyph->xoff, &glyph->yoff + ); + + return glyph; +} + +void +ltk_destroy_glyph_info(LtkGlyphInfo *gi) +{ + free(gi->alphamap); + free(gi); +} + +LtkGlyphInfo * +ltk_get_glyph_info(LtkFont *font, unsigned int id, float scale, khash_t(glyphinfo) *cache) +{ + int ret; + khint_t k; + LtkGlyphInfo *glyph; + k = kh_get(glyphinfo, cache, id); + if (k == kh_end(cache)) { + glyph = ltk_create_glyph_info(font, id, scale); + /* FIXME: error checking with ret */ + k = kh_put(glyphinfo, cache, id, &ret); + kh_value(cache, k) = glyph; + } else { + glyph = kh_value(cache, k); + } + + return glyph; +} + +khint_t +ltk_create_glyph_cache(LtkTextManager *tm, uint16_t font_id, uint16_t font_size) +{ + khash_t(glyphinfo) *cache = kh_init(glyphinfo); + int ret; + khint_t k; + /* I guess I can just ignore ret for now */ + k = kh_put(glyphcache, tm->glyph_cache, font_id << 16 + font_size, &ret); + kh_value(tm->glyph_cache, k) = cache; + + return k; +} + +void +ltk_destroy_glyph_cache(khash_t(glyphinfo) *cache) +{ + int k; + for (k = kh_begin(cache); k != kh_end(cache); k++) { + if (kh_exist(cache, k)) { + ltk_destroy_glyph_info(kh_value(cache, k)); + } + } + kh_destroy(glyphinfo, cache); +} + +void +ltk_load_default_font(LtkTextManager *tm, char *name) +{ + FcPattern *match; + FcResult result; + char *file; + int index; + uint16_t font; + + tm->fcpattern = FcNameParse(name); + FcPatternAddString(tm->fcpattern, FC_FONTFORMAT, "truetype"); + FcConfigSubstitute(NULL, tm->fcpattern, FcMatchPattern); + FcDefaultSubstitute(tm->fcpattern); + match = FcFontMatch(NULL, tm->fcpattern, &result); + + FcPatternGetString (match, FC_FILE, 0, (FcChar8 **) &file); + /* FIXME: Why is index never used? This is the index within the font file, + so it might be important, although I'm not sure if stb_truetype even + supports it */ + FcPatternGetInteger (match, FC_INDEX, 0, &index); + + tm->default_font = ltk_get_font(tm, file); + + FcPatternDestroy (match); +} + +LtkFont * +ltk_create_font(char *path, uint16_t id) +{ + long len; + LtkFont *font = malloc(sizeof(LtkFont)); + if (!font) { + (void)fprintf(stderr, "Out of memory!\n"); + exit(1); + } + char *contents = ltk_read_file(path, &len); + if (!stbtt_InitFont(&font->info, contents, 0)) + { + (void)fprintf(stderr, "Failed to load font %s\n", path); + exit(1); + } + /* FIXME: make use of the destroy function (last argument to hb_blob_create - see hb-blob.cc in harfbuzz source) */ + hb_blob_t *blob = hb_blob_create(contents, len, HB_MEMORY_MODE_READONLY, NULL, NULL); + hb_face_t *face = hb_face_create(blob, 0); + /* FIXME: need to use destroy function in order for the original file data to be freed? */ + hb_blob_destroy(blob); + font->hb = hb_font_create(face); + hb_face_destroy(face); + hb_ot_font_set_funcs(font->hb); + font->id = id; + font->refs = 0; + + return font; +} + +void +ltk_destroy_font(LtkFont *font) +{ + free(font->info.data); + hb_font_destroy(font->hb); + free(font); +} + +uint16_t +ltk_load_font(LtkTextManager *tm, char *path) +{ + LtkFont *font = ltk_create_font(path, tm->font_id_cur++); + int ret; + khint_t k; + /* FIXME: does kh_destroy also free these copied strings properly? */ + char *key = strdup(path); + k = kh_put(fontid, tm->font_paths, key, &ret); + kh_value(tm->font_paths, k) = font->id; + k = kh_put(fontstruct, tm->font_cache, (khint_t) font->id, &ret); + kh_value(tm->font_cache, k) = font; + k = kh_get(fontid, tm->font_paths, path); + + return font->id; +} + +uint16_t +ltk_get_font(LtkTextManager *tm, char *path) +{ + int ret; + khint_t k; + uint16_t id; + k = kh_get(fontid, tm->font_paths, path); + if (k == kh_end(tm->font_paths)) { + id = ltk_load_font(tm, path); + } else { + id = kh_value(tm->font_paths, k); + } + + return id; +} + +/* FIXME: allow to either use fribidi for basic shaping and don't use harfbuzz then, + or just use harfbuzz (then fribidi doesn't need to do any shaping) */ +LtkTextLine * +ltk_create_text_line(LtkTextManager *tm, char *text, uint16_t fontid, uint16_t size) +{ + /* NOTE: This doesn't actually take fontid into account right now - should it? */ + LtkTextLine *tl = malloc(sizeof(LtkTextLine)); + tl->start_segment = NULL; + LtkTextSegment *cur_ts = NULL; + LtkTextSegment *new_ts = NULL; + uint16_t cur_font_id = fontid; + int k; + LtkFont *font; + + unsigned int ulen = u8_strlen(text); + FriBidiChar *log_str = malloc(sizeof(FriBidiChar) * ulen); + size_t inc = 0; + for (int i = 0; i < ulen; i++) { + log_str[i] = u8_nextmemchar(text, &inc); + } + FriBidiCharType *pbase_dir = malloc(sizeof(FriBidiCharType) * ulen); + for (int i = 0; i < ulen; i++) { + pbase_dir[i] = FRIBIDI_TYPE_ON; + } + FriBidiChar *vis_str = malloc(sizeof(FriBidiChar) * ulen); + ulen = fribidi_charset_to_unicode(FRIBIDI_CHAR_SET_UTF8, text, strlen(text), log_str); + fribidi_log2vis(log_str, ulen, pbase_dir, vis_str, NULL, NULL, NULL); + + hb_unicode_funcs_t *ufuncs = hb_unicode_funcs_get_default(); + hb_script_t cur_script = hb_unicode_script(ufuncs, vis_str[0]); + hb_script_t last_script = cur_script; + size_t pos = 0; + size_t last_pos = 0; + size_t start_pos = 0; + uint32_t ch; + + for (int p = 0; p <= ulen; p++) { + cur_script = hb_unicode_script(ufuncs, vis_str[p]); + if (p == ulen || + (last_script != cur_script && + cur_script != HB_SCRIPT_INHERITED && + cur_script != HB_SCRIPT_COMMON)) { + FcPattern *pat = FcPatternDuplicate(tm->fcpattern); + FcPattern *match; + FcResult result; + FcPatternAddBool(pat, FC_SCALABLE, 1); + FcConfigSubstitute(NULL, pat, FcMatchPattern); + FcDefaultSubstitute(pat); + FcCharSet *cs = FcCharSetCreate(); + for (int i = start_pos; i < p; i++) { + FcCharSetAddChar(cs, vis_str[i]); + } + FcPatternAddCharSet(pat, FC_CHARSET, cs); + match = FcFontMatch(NULL, pat, &result); + char *file; + FcPatternGetString(match, FC_FILE, 0, &file); + cur_font_id = ltk_get_font(tm, file); + k = kh_get(fontstruct, tm->font_cache, cur_font_id); + font = kh_value(tm->font_cache, k); + FcPatternDestroy(match); + FcPatternDestroy(pat); + // handle case that this is the last character + if (p == ulen) { + last_script = cur_script; + } + /* FIXME: There should be better handling for cases + where an error occurs while creating the segment */ + new_ts = ltk_create_text_segment( + tm, vis_str + start_pos, + p - start_pos, cur_font_id, + size, last_script + ); + if (!new_ts) continue; + new_ts->next = NULL; + if (!tl->start_segment) tl->start_segment = new_ts; + if (cur_ts) cur_ts->next = new_ts; + cur_ts = new_ts; + + start_pos = p; + last_script = cur_script; + } + } + + free(vis_str); + free(log_str); + free(pbase_dir); + + /* calculate width of text line + NOTE: doesn't work with mixed horizontal and vertical text */ + LtkTextSegment *ts = tl->start_segment; + int is_hor = HB_DIRECTION_IS_HORIZONTAL(ts->dir); + tl->y_max = tl->x_max = INT_MIN; + tl->y_min = tl->x_min = INT_MAX; + tl->w = tl->h = 0; + while (ts) { + if (HB_DIRECTION_IS_HORIZONTAL(ts->dir) != is_hor) { + (void)fprintf(stderr, "WARNING: mixed horizontal/vertical text is not supported; ignoring\n"); + continue; + } + if (is_hor) { + if (tl->y_max < ts->y_max) { + tl->y_max = ts->y_max; + } + if (tl->y_min > ts->y_min) { + tl->y_min = ts->y_min; + } + tl->w += ts->w; + } else { + if (tl->x_max < ts->x_max) { + tl->x_max = ts->x_max; + } + if (tl->x_min > ts->x_min) { + tl->x_min = ts->x_min; + } + tl->h += ts->h; + } + ts = ts->next; + } + if (is_hor) { + tl->h = tl->y_max - tl->y_min; + } else { + tl->w = tl->x_max - tl->x_min; + } + + return tl; +} + +void +ltk_destroy_text_line(LtkTextLine *tl) { + LtkTextSegment *last_ts; + LtkTextSegment *cur_ts = tl->start_segment; + while (cur_ts) { + last_ts = cur_ts; + cur_ts = cur_ts->next; + ltk_destroy_text_segment(last_ts); + } +} + +/* FIXME: could use unsigned int for fontid and size as long as there is code to check neither of them become too large + -> in case I want to get rid of uint_16_t, etc. */ +LtkTextSegment * +ltk_create_text_segment(LtkTextManager *tm, uint32_t *text, unsigned int len, uint16_t fontid, uint16_t size, hb_script_t script) +{ + /* (x1*, y1*): top left corner (relative to origin and absolute) + (x2*, y2*): bottom right corner (relative to origin and absolute) */ + LtkFont *font; + khash_t(glyphinfo) *glyph_cache; + khint_t k; + + k = kh_get(fontstruct, tm->font_cache, fontid); + font = kh_value(tm->font_cache, k); + + uint32_t attr = fontid << 16 + size; + /* FIXME: turn this into ltk_get_glyph_cache */ + k = kh_get(glyphcache, tm->glyph_cache, attr); + if (k == kh_end(tm->glyph_cache)) { + k = ltk_create_glyph_cache(tm, fontid, size); + } + glyph_cache = kh_value(tm->glyph_cache, k); + + LtkTextSegment *ts = malloc(sizeof(LtkTextSegment)); + if (!ts) { + (void)fprintf(stderr, "Out of memory!\n"); + exit(1); + } + ts->str = malloc(sizeof(uint32_t) * (len + 1)); + memcpy(ts->str, text, len * sizeof(uint32_t)); + ts->str[len] = '\0'; + ts->font_id = fontid; + ts->font_size = size; + + hb_buffer_t *buf; + hb_glyph_info_t *ginf, *gi; + hb_glyph_position_t *gpos, *gp; + unsigned int text_len = 0; + if (len < 1) { + (void)printf("WARNING: ltk_render_text_segment: length of text is less than 1.\n"); + return NULL; + } + + buf = hb_buffer_create(); + hb_direction_t dir = hb_script_get_horizontal_direction(script); + hb_buffer_set_direction(buf, dir); + hb_buffer_set_script(buf, script); + hb_buffer_add_codepoints(buf, ts->str, len, 0, len); + /* According to https://harfbuzz.github.io/the-distinction-between-levels-0-and-1.html + * this should be level 1 clustering instead of level 0 */ + hb_buffer_set_cluster_level(buf, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); + hb_shape(font->hb, buf, NULL, 0); + ts->dir = hb_buffer_get_direction(buf); + ginf = hb_buffer_get_glyph_infos(buf, &text_len); + gpos = hb_buffer_get_glyph_positions(buf, &text_len); + float scale = stbtt_ScaleForMappingEmToPixels(&font->info, size); + LtkGlyph *last_glyph = NULL; + + int x_min = INT_MAX, x_max = INT_MIN, y_min = INT_MAX, y_max = INT_MIN; + int x_abs = 0, y_abs = 0, x1_abs, y1_abs, x2_abs, y2_abs; + /* magic, do not touch */ + LtkGlyph *glyph; + for (int i = 0; i < text_len; i++) { + gi = &ginf[i]; + gp = &gpos[i]; + glyph = malloc(sizeof(LtkGlyph)); + glyph->info = ltk_get_glyph_info(font, gi->codepoint, scale, glyph_cache); + glyph->info->refs++; + /* FIXME: round instead of just casting */ + glyph->x_offset = (int)(gp->x_offset * scale); + glyph->y_offset = (int)(gp->y_offset * scale); + glyph->x_advance = (int)(gp->x_advance * scale); + glyph->y_advance = (int)(gp->y_advance * scale); + glyph->next = NULL; + if (i == 0) { + ts->start_glyph = glyph; + } else { + last_glyph->next = glyph; + } + last_glyph = glyph; + + /* Calculate position in order to determine full size of text segment */ + x1_abs = x_abs + glyph->info->xoff + glyph->x_offset; + y1_abs = y_abs + glyph->info->yoff - glyph->y_offset; + /* Okay, wait, so should I check if the script is horizontal, and then add + x_advance instead of glyph->info->w? It seems that the glyph width is + usually smaller than x_advance, and spaces etc. are completely lost + because their glyph width is 0. I have to distinguish between horizontal + and vertical scripts, though because to calculate the maximum y position + for horizontal scripts, I still need to use the glyph height since + y_advance doesn't really do much there. I dunno, at least *something* + works now... */ + /* FIXME: THIS PROBABLY DOESN'T REALLY WORK */ + if (HB_DIRECTION_IS_HORIZONTAL(dir)) { + x2_abs = x1_abs + glyph->x_advance; + y2_abs = y1_abs + glyph->info->h; + } else { + x2_abs = x1_abs + glyph->info->w; + y2_abs = y1_abs - glyph->y_advance; + } + glyph->x_abs = x1_abs; + glyph->y_abs = y1_abs; + if (x1_abs < x_min) x_min = x1_abs; + if (y1_abs < y_min) y_min = y1_abs; + if (x2_abs > x_max) x_max = x2_abs; + if (y2_abs > y_max) y_max = y2_abs; + x_abs += glyph->x_advance; + y_abs -= glyph->y_advance; + } + ts->start_x = -x_min; + ts->start_y = -y_min; + ts->w = x_max - x_min; + ts->h = y_max - y_min; + ts->x_min = x_min; + ts->y_min = y_min; + ts->x_max = x_max; + ts->y_max = y_max; + + font->refs++; + + return ts; +} + +void +ltk_destroy_glyph(LtkGlyph *glyph, khash_t(glyphinfo) *cache) +{ + int k; + if (--glyph->info->refs < 1) { + k = kh_get(glyphinfo, cache, glyph->info->id); + kh_del(glyphinfo, cache, k); + ltk_destroy_glyph_info(glyph->info); + } + free(glyph); +} + +void +ltk_destroy_text_segment(LtkTextSegment *ts) +{ + LtkGlyph *glyph, *next_glyph; + khash_t(glyphinfo) *gcache; + LtkFont *font; + int k; + glyph = ts->start_glyph; + k = kh_get(glyphinfo, ltk_global->tm->glyph_cache, ts->font_id << 16 + ts->font_size); + gcache = kh_value(ltk_global->tm->glyph_cache, k); + do { + next_glyph = glyph->next; + ltk_destroy_glyph(glyph, gcache); + } while (glyph = next_glyph); + k = kh_get(fontstruct, ltk_global->tm->font_cache, ts->font_id); + font = kh_value(ltk_global->tm->font_cache, k); + if (--font->refs < 1) { + kh_del(fontstruct, ltk_global->tm->font_cache, k); + ltk_destroy_font(font); + } + free(ts->str); + free(ts); +} + +/* based on http://codemadness.org/git/dwm-font/file/drw.c.html#l315 */ +XImage * +ltk_render_text_line( + LtkTextLine *tl, + Display *dpy, + Window window, + GC gc, + Colormap colormap, + XColor fg, + XColor bg) +{ + XWindowAttributes attrs; + XGetWindowAttributes(dpy, window, &attrs); + int depth = attrs.depth; + XImage *img = XCreateImage(dpy, CopyFromParent, depth, ZPixmap, 0, NULL, tl->w, tl->h, 32, 0); + img->data = calloc(img->bytes_per_line, img->height); + XInitImage(img); + int b; + for (int i = 0; i < tl->h; i++) { + b = img->bytes_per_line * i; + for (int j = 0; j < tl->w; j++) { + img->data[b++] = bg.blue / 257; + img->data[b++] = bg.green / 257; + img->data[b++] = bg.red / 257; + b++; + } + } + + LtkTextSegment *ts = tl->start_segment; + int x = 0; + int y = 0; + int is_hor = HB_DIRECTION_IS_HORIZONTAL(ts->dir); + do { + if (is_hor) { + y = tl->h - tl->y_max; + ltk_render_text_segment(ts, x + ts->start_x, y, img, fg); + x += ts->w; + } else { + x = tl->w - tl->x_max; + ltk_render_text_segment(ts, x, y + ts->start_y, img, fg); + y += ts->h; + } + } while (ts = ts->next); + + return img; +} + +void +ltk_render_text_segment( + LtkTextSegment *ts, + unsigned int start_x, + unsigned int start_y, + XImage *img, + XColor fg) +{ + LtkGlyph *glyph = ts->start_glyph; + int x_cur = start_x; + int y_cur = start_y; + int x, y; + double a; + int b; + do { + x = x_cur + glyph->info->xoff + glyph->x_offset; + y = y_cur + glyph->info->yoff - glyph->y_offset; + for (int i = 0; i < glyph->info->h; i++) { + for (int j = 0; j < glyph->info->w; j++) { + b = (y + i) * img->bytes_per_line + (x + j) * 4; + a = glyph->info->alphamap[i * glyph->info->w + j] / 255.0; + img->data[b] = (fg.blue * a + (1 - a) * (uint16_t)img->data[b] * 257) / 257; + img->data[b + 1] = (fg.green * a + (1 - a) * (uint16_t)img->data[b + 1] * 257) / 257; + img->data[b + 2] = (fg.red * a + (1 - a) * (uint16_t)img->data[b + 2] * 257) / 257; + } + } + x_cur += glyph->x_advance; + y_cur -= glyph->y_advance; + } while (glyph = glyph->next); +} + +/* +Notes: When inserting a character, check what the direction of the surrounding +script is - if it is the same (or a weak direction), then just insert the char +without re-doing the bidi algorithm. Only redo the whole line/paragraph is the +dir of the inserted character is different. Assume the text is LTR until a strong +character is inserted, i.e. the line should keep a flag if it already has a strong +dir set. +When reordering, the cursor needs to be kept in the right place: +a) The logical to visual mapping from FriBidi needs to be used to find the position +of the cursor in the text that gets passed to Harfbuzz. +b) The cluster values from Harfbuzz need to be used to figure out where the cursor +is on the screen. +Harfbuzz also should be given some context - maybe even completely reshape the +text until the last and next space? That would only be a problem with weird very +long words (mainly nonsense). At least maybe take up to five or so chars on each +side and pass even more with the optional context-passing feature for hb_shape. +Question: Does both the logical and visual text need to be stored? The logical text +is technically the master copy, but the visual text is what gets passed to harfbuzz. +-> Actually, since the visual text has to be split into script runs anyways, maybe +just append text to that (and also add it to the master buffer simultaneously). As +soon as a different script is inserted, everything has to be redone anyways. Also, +when reshaping with context, only the text in the current run has to be passed at all. +*/ + +struct ltk_gap_buffer * +ltk_gap_buffer_create(void) { + struct ltk_gap_buffer *gb = malloc(sizeof(struct ltk_gap_buffer)); + if (!gb) + goto error; + gb->buf = malloc(4 * sizeof(uint32_t)); + if (!gb->buf) + goto error; + gb->buf_size = 8; + gb->gap_left = 0; + gb->gap_size = 8; + gb->gap_end_left = 8; + return gb; +error: + (void)fprintf(stderr, "Out of memory while trying to allocate gap buffer\n"); + exit(1); +} + +void +ltk_gap_buffer_resize_gap(struct ltk_gap_buffer *gb, int len) { + /* FIXME: Should this use realloc? It's usually more efficient, but + in this case, I would still need to copy the part after the gap + manually, so it could potentially be copied twice, which really + wouldn't be good. Maybe use realloc if only a small part is after + the gap and just regular malloc otherwise? */ + int new_size = gb->buf_size - gb->gap-size + len; + struct ltk_gap_buffer *new = malloc(new_size * sizeof(uint32_t)); + if (!new) { + (void)fprintf(stderr, "Out of memory while trying to resize gap buffer\n"); + exit(1); + } + for (int i = 0; i < gb->gap_left; i++) { + new[i] = gb->buf[i]; + } + for (int i = gb->gap_left + gb->gap_size; i < gb->gap_end_left) { + new[i - gb->gap_size + len] = gb->buf[i]; + } + free(gb->buf); + gb->buf = new; +} + +void +ltk_gap_buffer_insert(struct ltk_gap_buffer *gb, uint32_t *new, size_t start, size_t len) { + if (gb->gap_size < len) + ltk_gap_buffer_resize_gap(gb, len + 8); + for (int i = 0; i < len; i++) { + gb->buf[gb->gap_left + i] = new[start + i]; + } + gb->gap_left = gb->gap_left + len; + gb->gap_size -= len; +} + +void +ltk_gap_buffer_insert_single(struct ltk_gap_buffer *gb, uint32_t new) { + ltk_gap_buffer_insert(gb, &new, 0, 1); +} + +void +ltk_gap_buffer_move_gap(struct ltk_gap_buffer *gb, size_t pos) { + if (pos == gb->gap_left) + return; + if (pos < 0 || pos >= gb->gap_end_left - gb->gap_size) { + (void)fprintf(stderr, "Index out of range while moving gap buffer gap\n"); + return; + } + if (pos >= gb->gap_left) { + for (int i = gb->gap_left; i < pos) { + gb->buf[i] = gb->buf[i + gb->gap_size]; + } + } else { + for (int i = gb->gap_left - 1; i >= pos; i--) { + gb->buf[i + gb->gap_size] = gb->buf[i]; + } + } + gb->gap_left = pos; +} + +void +ltk_gap_buffer_destroy(struct ltk_gap_buffer *gb) { + free(gb->buf); + free(gb); +} diff --git a/textedit_wip.h b/textedit_wip.h @@ -0,0 +1,175 @@ +/* + * This file is part of the Lumidify ToolKit (LTK) + * Copyright (c) 2017, 2018, 2020 lumidify <nobody@lumidify.org> + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef _TEXT_HB_H_ +#define _TEXT_HB_H_ + +/* +Requires the following includes: +<X11/Xlib.h>, <X11/Xutil.h>, "stb_truetype.h", +"khash.h", <harfbuzz/hb.h>, <fribidi.h>, +<fontconfig/fontconfig.h> +*/ + +/* Contains glyph info specific to one run of text */ +typedef struct ltk_glyph { + LtkGlyphInfo *info; + int x_offset; /* additional x offset given by harfbuzz */ + int y_offset; /* additional y offset given by harfbuzz */ + int x_advance; + int y_advance; + int x_abs; + int y_abs; + uint32_t cluster; /* index of char in original text - from harfbuzz */ +}; + +struct ltk_gap_buffer { + uint32_t *buf; + size_t buf_size; + size_t gap_left; + size_t gap_size; + size_t gap_end_left; +}; + +struct ltk_text_buffer { + uint32_t *buffer; + struct ltk_glyph *glyphs; + size_t buf_size; + size_t buf_left; + size_t buf_gap_size; + size_t glyphs_size; + size_t glyphs_left; + size_t glyphs_gap_size; +}; + +typedef struct { + stbtt_fontinfo info; + hb_font_t *hb; + uint16_t id; + unsigned int refs; +} LtkFont; + +/* Contains general info on glyphs that doesn't change regardless of the context */ +typedef struct _LtkGlyphInfo { + unsigned int id; + unsigned char *alphamap; + unsigned int w; + unsigned int h; + unsigned int xoff; /* x offset from origin to top left corner of glyph */ + unsigned int yoff; /* y offset from origin to top left corner of glyph */ + unsigned int refs; + /* FIXME: does refs need to be long? It could cause problems if a + program tries to cache/"keep alive" a lot of pages of text. */ +} LtkGlyphInfo; + +/* Single segment of text with same font */ +typedef struct LtkTextSegment { + uint16_t font_id; + uint16_t font_size; + unsigned int w; + unsigned int h; + int start_x; + int start_y; + int x_min; + int y_min; + int x_max; + int y_max; + hb_direction_t dir; + uint32_t *str; + LtkGlyph *start_glyph; + struct LtkTextSegment *next; +} LtkTextSegment; + +/* Single line of text */ +typedef struct { + unsigned int w; + unsigned int h; + int x_max; + int x_min; + int y_max; + int y_min; + FriBidiParType dir; + LtkTextSegment *start_segment; +} LtkTextLine; + +/* Hash definitions */ +/* glyph id -> glyph info struct */ +KHASH_MAP_INIT_INT(glyphinfo, LtkGlyphInfo*) +/* font path, size -> glyph cache hash */ +KHASH_MAP_INIT_INT(glyphcache, khash_t(glyphinfo)*) +/* font path -> font id */ +KHASH_MAP_INIT_STR(fontid, uint16_t) +/* font id -> font struct */ +KHASH_MAP_INIT_INT(fontstruct, LtkFont*) + +typedef struct LtkTextManager { + khash_t(fontid) *font_paths; + khash_t(fontstruct) *font_cache; + khash_t(glyphcache) *glyph_cache; + FcPattern *fcpattern; + uint16_t default_font; + uint16_t font_id_cur; +} LtkTextManager; + +LtkTextManager *ltk_init_text(char *font_name); + +void ltk_destroy_text_manager(LtkTextManager *tm); + +LtkGlyphInfo *ltk_create_glyph_info(LtkFont *font, unsigned int id, float scale); + +void ltk_destroy_glyph_info(LtkGlyphInfo *gi); + +LtkGlyphInfo *ltk_get_glyph_info(LtkFont *font, unsigned int id, float scale, khash_t(glyphinfo) *cache); + +khint_t ltk_create_glyph_cache(LtkTextManager *tm, uint16_t font_id, uint16_t font_size); + +void ltk_destroy_glyph_cache(khash_t(glyphinfo) *cache); + +void ltk_load_default_font(LtkTextManager *tm, char *name); + +LtkFont *ltk_create_font(char *path, uint16_t id); + +void ltk_destroy_font(LtkFont *font); + +/* FIXME: need to figure out how exactly the whole font system is going to work, especially with default fonts, etc. */ +uint16_t ltk_load_font(LtkTextManager *tm, char *path); + +uint16_t ltk_get_font(LtkTextManager *tm, char *path); + +/* TODO: different sizes, colors, styles, etc. */ +LtkTextLine *ltk_create_text_line(LtkTextManager *tm, char *text, uint16_t fontid, uint16_t size); + +/* FIXME: could use unsigned int for fontid and size as long as there is code to check neither of them become too large + -> in case I want to get rid of uint_16_t, etc. */ +LtkTextSegment *ltk_create_text_segment(LtkTextManager *tm, uint32_t *text, unsigned int text_len, uint16_t fontid, uint16_t size, hb_script_t script); + +void ltk_destroy_glyph(LtkGlyph *glyph, khash_t(glyphinfo) *cache); + +void ltk_destroy_text_segment(LtkTextSegment *ts); + +XImage *ltk_render_text_line(LtkTextLine *tl, Display *dpy, Window window, GC gc, Colormap colormap, XColor fg, XColor bg); + +void ltk_render_text_segment(LtkTextSegment *ts, unsigned int start_x, unsigned int start_y, XImage *img, XColor fg); + + +#endif