ltkx

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

commit 528b74094b0aa1d585efe5ad28903758d467e8a6
parent 0e40d68a8b98ca331bfad96d3293d9c01c5875ca
Author: lumidify <nobody@lumidify.org>
Date:   Sun, 17 May 2020 14:56:19 +0200

Improve bidi text rendering

Diffstat:
MNOTES | 6++++++
Marray.h | 13++++++++++++-
Mtest1.c | 4++--
Mtext_buffer.c | 327+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mtext_buffer.h | 14++++++++++----
Mtext_edit.c | 24+++++++++++++++++-------
Mtext_edit.h | 2+-
7 files changed, 275 insertions(+), 115 deletions(-)

diff --git a/NOTES b/NOTES @@ -30,3 +30,9 @@ 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 + +Create a version of ltk_fatal that exits the actual program +and creates a "failsafe" error window (just using basic +XDrawString, etc.) to show errors (like index out of bounds, +etc.). Still print the error, of course, in case creating +a window doesn't work. diff --git a/array.h b/array.h @@ -43,7 +43,9 @@ void ltk_array_insert_##name(struct ltk_array_##name *ar, size_t index, \ void ltk_array_resize_##name(struct ltk_array_##name *ar, size_t size); \ void ltk_array_destroy_##name(struct ltk_array_##name *ar); \ void ltk_array_clear_##name(struct ltk_array_##name *ar); \ -void ltk_array_append_##name(struct ltk_array_##name *ar, type elem); +void ltk_array_append_##name(struct ltk_array_##name *ar, type elem); \ +void ltk_array_destroy_deep_##name(struct ltk_array_##name *ar, \ + void (*destroy_func)(type)); #define LTK_ARRAY_INIT_IMPL(name, type) \ struct ltk_array_##name * \ @@ -137,6 +139,15 @@ ltk_array_destroy_##name(struct ltk_array_##name *ar) { \ free(ar->buf); \ ar->buf = NULL; \ free(ar); \ +} \ + \ +void \ +ltk_array_destroy_deep_##name(struct ltk_array_##name *ar, \ + void (*destroy_func)(type)) { \ + for (int i = 0; i < ar->len; i++) { \ + destroy_func(ar->buf[i]); \ + } \ + ltk_array_destroy_##name(ar); \ } #endif /* _LTK_ARRAY_H_ */ diff --git a/test1.c b/test1.c @@ -39,9 +39,9 @@ int main(int argc, char *argv[]) //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, "پَیدایش", NULL); - LtkTextEdit *edit = ltk_create_text_edit(window1, "ہمارے بارے میں blablabla"); + LtkTextEdit *edit = ltk_create_text_edit(window1, "ہمارے بارے میں blabla bla"); LtkButton *button4 = ltk_create_button(window1, "ہمارے بارے میں blablabla", &bob3, edit); ltk_grid_widget(button4, grid1, 1, 0, 1, 1, LTK_STICKY_TOP | LTK_STICKY_BOTTOM | LTK_STICKY_RIGHT); - ltk_grid_widget(edit, grid1, 1, 1, 1, 1, LTK_STICKY_LEFT | LTK_STICKY_BOTTOM | LTK_STICKY_TOP); + ltk_grid_widget(edit, grid1, 1, 1, 1, 1, LTK_STICKY_LEFT | LTK_STICKY_BOTTOM | LTK_STICKY_TOP | LTK_STICKY_RIGHT); ltk_mainloop(); } diff --git a/text_buffer.c b/text_buffer.c @@ -44,85 +44,151 @@ LTK_ARRAY_INIT_IMPL(uint32, uint32_t) LTK_ARRAY_INIT_IMPL(script, hb_script_t) LTK_ARRAY_INIT_IMPL(level, FriBidiLevel) LTK_ARRAY_INIT_IMPL(int, int) +LTK_ARRAY_INIT_IMPL(line, struct ltk_soft_line *) -/* based on http://codemadness.org/git/dwm-font/file/drw.c.html#l315 */ -/* FIXME: rename this once everything is cleaned up (currently conflicts with the - old render function */ -XImage * -ltk_render_text_line_new( - struct ltk_text_line *tl, - int max_width, - Display *dpy, - Window window, - GC gc, - Colormap colormap, - XColor fg, - XColor bg) -{ - XImage *img; - int par_is_rtl = FRIBIDI_IS_RTL(tl->dir); - int cur_y = 0; - int cur_x = par_is_rtl ? max_width : 0; - ltk_array_clear_int(tl->wrap_indeces); - ltk_array_append_uint32(tl->wrap_indeces, 0); +void +ltk_soft_line_destroy(struct ltk_soft_line *sl) { + if (sl->img) XDestroyImage(sl->img); + free(sl); +} + +struct ltk_array_line * +ltk_text_line_wrap(struct ltk_text_line *tl, int max_width) { + /* FIXME: if max_width == -1, don't wrap */ + struct ltk_array_line *soft_lines = ltk_array_create_line(1); + int par_is_rtl = tl->dir == HB_DIRECTION_RTL; - /* FIXME: wrap bidi text properly */ - /* FIXME: THIS IS UGLY */ struct ltk_text_run *cur = par_is_rtl ? tl->last_run : tl->first_run; + LtkGlyph *glyph; + struct ltk_soft_line *sl = malloc(sizeof(struct ltk_soft_line)); + if (!sl) goto error; + sl->glyph_index = cur->dir == HB_DIRECTION_RTL ? cur->len - 1 : 0; + sl->run = cur; + sl->len = 0; + sl->w = 0; + ltk_array_append_line(soft_lines, sl); + int last_linebreak = par_is_rtl ? tl->w : 0; + int cur_start = 0; + /* FIXME: also calculate max height of each line */ while (cur) { - if (par_is_rtl) { - for (int i = cur->num_glyphs - 1; i >= 0; i--) { - cur_x -= cur->glyphs[i].x_advance; - if (cur_x < 0) { - int j = 0; - for (j = i; j < cur->num_glyphs; j++) { - if (cur->glyphs[j].cluster != cur->glyphs[i].cluster) { - /* must decrease one again so the actual - last character is used */ - j--; + printf("%d %d\n", cur->len, cur->num_glyphs); + if (cur->len == 1) { + printf("%d\n", tl->log_buf->buf[cur->glyphs[0].cluster]); + } + if (sl->w + cur->w <= max_width) { + sl->w += cur->w; + sl->len += cur->len; + cur = par_is_rtl ? cur->last : cur->next; + continue; + } + if (cur->dir == HB_DIRECTION_RTL) { + cur_start = cur->glyphs[cur->len - 1].x_abs + cur->glyphs[cur->len - 1].info->w; + int i = cur->len - 1; + while (i >= 0) { + glyph = &cur->glyphs[i]; + int cur_w = sl->w + cur_start - glyph->x_abs; + if (cur_w > max_width) { + /* FIXME: fix behavior when line isn't wide enough for single char */ + for (int j = i; j < cur->len; j++) { + if (cur->glyphs[j].cluster != glyph->cluster || j == cur->len - 1) { + int new_index; + if (j == cur->len - 1 && + cur->glyphs[j].cluster == glyph->cluster) { + new_index = j; + i = j; + last_linebreak = cur_start; + } else { + new_index = j - 1; + i = j - 1; + last_linebreak = cur->glyphs[j].x_abs; + sl->len++; + } + sl->w += cur_start - last_linebreak; + cur_start = last_linebreak; + sl = malloc(sizeof(struct ltk_soft_line)); + if (!sl) goto error; + sl->glyph_index = new_index; + sl->run = cur; + sl->len = 0; + sl->w = 0; + ltk_array_append_line(soft_lines, sl); break; + } else { + sl->len--; } } - i = j; - /* FIXME: handle case that this is the same as the last index */ - ltk_array_append_uint32(tl->wrap_indeces, cur->glyphs[i].cluster); - cur_x = max_width; + } else { + sl->len++; + i--; } } + if (sl->run == cur) + sl->w = last_linebreak - cur->glyphs[0].x_abs; + else + sl->w += cur->w; } else { - for (int i = 0; i < cur->num_glyphs; i++) { - cur_x += cur->glyphs[i].x_advance; - if (cur_x > max_width) { - int j = 0; - for (j = i; j >= 0; j--) { - if (cur->glyphs[j].cluster != cur->glyphs[i].cluster) { - /* must increase one again so the actual - next character is used */ - j++; + cur_start = cur->glyphs[0].x_abs; + int i = 0; + while (i < cur->len) { + glyph = &cur->glyphs[i]; + int cur_w = sl->w + glyph->x_abs + glyph->info->w - cur_start; + if (cur_w > max_width) { + for (int j = i; j >= 0; j--) { + if (cur->glyphs[j].cluster != glyph->cluster || j == 0) { + int new_index; + if (j == 0 && cur->glyphs[j].cluster == glyph->cluster) { + new_index = j; + i = j; + last_linebreak = cur_start; + } else { + new_index = j + 1; + i = j + 1; + last_linebreak = cur->glyphs[j + 1].x_abs; + sl->len++; + } + sl->w += last_linebreak - cur_start; + cur_start = last_linebreak; + sl = malloc(sizeof(struct ltk_soft_line)); + if (!sl) goto error; + sl->glyph_index = new_index; + sl->run = cur; + sl->len = 0; + sl->w = 0; + ltk_array_append_line(soft_lines, sl); break; + } else { + sl->len--; } } - i = j; - /* FIXME: handle case that this is the same as the last index */ - ltk_array_append_uint32(tl->wrap_indeces, cur->glyphs[i].cluster); - cur_x = 0; + } else { + sl->len++; + i++; } + } + if (sl->run == cur) + sl->w = cur->glyphs[cur->len - 1].x_abs + cur->glyphs[cur->len - 1].info->w - last_linebreak; + else + sl->w += cur->w; } cur = par_is_rtl ? cur->last : cur->next; } + return soft_lines; +error: + (void)fprintf(stderr, "Error allocating memory while wrapping text line.\n"); + exit(1); +} - XWindowAttributes attrs; - XGetWindowAttributes(dpy, window, &attrs); - int depth = attrs.depth; - img = XCreateImage(dpy, CopyFromParent, depth, ZPixmap, 0, NULL, max_width, (tl->h + tl->line_gap) * tl->wrap_indeces->len, 32, 0); +XImage * +ltk_create_ximage(Display *dpy, int w, int h, int depth, XColor bg) { + XImage *img = XCreateImage(dpy, CopyFromParent, depth, ZPixmap, 0, NULL, w, h, 32, 0); img->data = calloc(img->bytes_per_line, img->height); XInitImage(img); int b; - for (int i = 0; i < tl->h * tl->wrap_indeces->len; i++) { + for (int i = 0; i < h; i++) { b = img->bytes_per_line * i; - for (int j = 0; j < max_width; j++) { + for (int j = 0; j < w; j++) { img->data[b++] = bg.blue / 257; img->data[b++] = bg.green / 257; img->data[b++] = bg.red / 257; @@ -130,55 +196,112 @@ ltk_render_text_line_new( } } - cur = par_is_rtl ? tl->last_run : tl->first_run; - int x, y; + return img; +} + +/* based on http://codemadness.org/git/dwm-font/file/drw.c.html#l315 */ +void +ltk_draw_glyph(LtkGlyph *glyph, XImage *img, int x, int y, XColor fg) { double a; - int cur_line_x = 0; - int cur_line = 0; + int b; + for (int i = 0; i < glyph->info->h; i++) { + for (int j = 0; j < glyph->info->w; j++) { + if (y + i >= img->height || x + j >= img->width) + continue; + 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; + } + } +} + +/* FIXME: rename this once everything is cleaned up (currently conflicts with the + old render function */ +struct ltk_array_line * +ltk_render_text_line_new( + struct ltk_text_line *tl, + int max_width, + Display *dpy, + Window window, + GC gc, + Colormap colormap, + XColor fg, + XColor bg) +{ LtkGlyph *glyph; - /* FIXME: ints are compared with size_t's in various places. Maybe I should fix that. */ - /* FIXME: how should an empty line be handled? This doesn't use a do-for - loop in case tl->first_run is NULL, but I should probably decide what - to do in that case */ - int index; - while (cur) { - for (int k = 0; k < cur->num_glyphs; k++) { - index = par_is_rtl ? cur->num_glyphs - k - 1 : k; - glyph = &cur->glyphs[index]; - x = par_is_rtl ? max_width - ((tl->w - glyph->x_abs) - cur_line_x) : glyph->x_abs - cur_line_x; - /* FIXME: use the computed indeces from above */ - if (par_is_rtl && x < 0) { - cur_line++; - cur_line_x = (tl->w - glyph->x_abs - glyph->info->w); - x = max_width - ((tl->w - glyph->x_abs) - cur_line_x); - } else if (!par_is_rtl && x + glyph->info->w > max_width) { - cur_line++; - cur_line_x = glyph->x_abs; - x = glyph->x_abs - cur_line_x; - } - y = glyph->y_abs + (tl->h + tl->line_gap) * cur_line; - /* FIXME: remove this when everything's fixed */ - if (x < 0) - x = 0; - if (x > max_width - glyph->info->w) - x = max_width - glyph->info->w; - if (y < 0) - y = 0; - if (y > img->height - glyph->info->h) - y = img->height - glyph->info->h; - 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; + int par_is_rtl = tl->dir == HB_DIRECTION_RTL; + /* FIXME: can't soft_lines just be a normal array instead of pointer array? */ + struct ltk_array_line *soft_lines = ltk_text_line_wrap(tl, max_width); + + XWindowAttributes attrs; + XGetWindowAttributes(dpy, window, &attrs); + int depth = attrs.depth; + + for (int i = 0; i < soft_lines->len; i++) { + struct ltk_soft_line *sl = soft_lines->buf[i]; + sl->img = ltk_create_ximage(dpy, sl->w, tl->h, depth, bg); + struct ltk_text_run *cur = sl->run; + size_t cur_len = 0; + int cur_border = par_is_rtl ? sl->w : 0; + while (cur && cur_len < sl->len) { + int start_index; + /* FIXME: the borders here aren't correct since they are + x_abs + glyph->info->w, which may not be the same thing + as the actual x_abs of the bordering glyph */ + if (cur->dir == HB_DIRECTION_RTL) { + start_index = cur == sl->run ? sl->glyph_index : cur->len - 1; + int local_border = cur->glyphs[start_index].x_abs + cur->glyphs[start_index].info->w; + int end_index; + if (start_index + 1 < sl->len - cur_len) { + end_index = 0; + } else { + end_index = start_index - (sl->len - cur_len) + 1; + } + for (int i = start_index; i >= 0 && cur_len < sl->len; i--) { + cur_len++; + int x; + if (par_is_rtl) { + x = cur_border - (local_border - cur->glyphs[i].x_abs); + } else { + x = cur_border + (cur->glyphs[i].x_abs - cur->glyphs[end_index].x_abs); + } + ltk_draw_glyph(&cur->glyphs[i], sl->img, x, cur->glyphs[i].y_abs, fg); + } + if (par_is_rtl) + cur_border -= local_border - cur->glyphs[0].x_abs; + else + cur_border += local_border - cur->glyphs[0].x_abs; + } else { + start_index = cur == sl->run ? sl->glyph_index : 0; + int local_border = cur->glyphs[start_index].x_abs; + int end_index; + if (cur->len - start_index < sl->len - cur_len) { + end_index = cur->len - 1; + } else { + end_index = start_index + sl->len - cur_len - 1; + } + for (int i = start_index; i < cur->len && cur_len < sl->len; i++) { + cur_len++; + int x; + if (par_is_rtl) { + x = cur_border - (cur->glyphs[end_index].x_abs + cur->glyphs[end_index].info->w - cur->glyphs[i].x_abs); + } else { + x = cur_border + (cur->glyphs[i].x_abs - local_border); + } + ltk_draw_glyph(&cur->glyphs[i], sl->img, x, cur->glyphs[i].y_abs, fg); } + if (par_is_rtl) + cur_border -= cur->glyphs[cur->len - 1].x_abs + cur->glyphs[cur->len - 1].info->w - local_border; + else + cur_border += cur->glyphs[cur->len - 1].x_abs + cur->glyphs[cur->len - 1].info->w - local_border; } + cur = par_is_rtl ? cur->last : cur->next; } - cur = par_is_rtl ? cur->last : cur->next; } - return img; + + return soft_lines; } /* Begin stuff stolen from raqm */ @@ -566,10 +689,15 @@ ltk_text_line_destroy_runs(struct ltk_text_run *runs) { static void ltk_text_line_recalculate(LtkTextManager *tm, struct ltk_text_line *tl) { + FriBidiCharType par_dir = FRIBIDI_TYPE_ON; fribidi_log2vis( tl->log_buf->buf, tl->log_buf->len, - &tl->dir, tl->vis_buf->buf, tl->log2vis->buf, tl->vis2log->buf, tl->bidi_levels->buf + &par_dir, tl->vis_buf->buf, tl->log2vis->buf, tl->vis2log->buf, tl->bidi_levels->buf ); + if (FRIBIDI_IS_RTL(par_dir)) + tl->dir = HB_DIRECTION_RTL; + else + tl->dir = HB_DIRECTION_LTR; struct ltk_text_run *old_runs = tl->first_run; ltk_text_line_itemize(tl); struct ltk_text_run *cur = tl->first_run; @@ -630,7 +758,6 @@ ltk_text_line_create(void) { line->cur_run = NULL; line->next = NULL; line->height = 0; - line->dir = FRIBIDI_TYPE_ON; line->len = 0; line->cursor_pos = 0; /* FIXME */ diff --git a/text_buffer.h b/text_buffer.h @@ -55,11 +55,16 @@ struct ltk_text_run { hb_direction_t dir; }; -struct ltk_line_break { - size_t index; +struct ltk_soft_line { + size_t glyph_index; + size_t len; + int w; struct ltk_text_run *run; + XImage *img; }; +LTK_ARRAY_INIT_DECL(line, struct ltk_soft_line *) + struct ltk_text_line { struct ltk_array_uint32 *log_buf; /* buffer of the logical text */ struct ltk_array_script *scripts; @@ -73,7 +78,7 @@ struct ltk_text_line { struct ltk_text_run *cur_run; /* current node in the linked list of runs */ struct ltk_text_line *next; /* next text line in the buffer */ unsigned int height; /* height of the line (including wrapping) */ - FriBidiCharType dir; /* overall paragraph direction */ + hb_direction_t dir; /* overall paragraph direction */ size_t len; uint16_t font_size; int y_max; @@ -90,7 +95,8 @@ struct ltk_text_buffer { unsigned int line_gap; }; -XImage *ltk_render_text_line_new(struct ltk_text_line *tl, int max_width, +void ltk_soft_line_destroy(struct ltk_soft_line *sl); +struct ltk_array_line *ltk_render_text_line_new(struct ltk_text_line *tl, int max_width, Display *dpy, Window window, GC gc, Colormap colormap, XColor fg, XColor bg); void ltk_text_line_insert_text(struct ltk_text_line *tl, uint32_t *text, size_t len); struct ltk_text_line *ltk_text_line_create(void); diff --git a/text_edit.c b/text_edit.c @@ -44,9 +44,17 @@ ltk_draw_text_edit(LtkTextEdit *te) { XColor bg = ltk_global->theme->window->bg; LtkRect rect = te->widget.rect; LtkWindow *window = te->widget.window; - if (!te->img) - te->img = ltk_render_text_line_new(te->tl, rect.w, ltk_global->display, window->xwindow, window->gc, ltk_global->colormap, fg, bg); - XPutImage(ltk_global->display, window->xwindow, window->gc, te->img, 0, 0, rect.x, rect.y, te->img->width, te->img->height); + if (!te->soft_lines) + te->soft_lines = ltk_render_text_line_new(te->tl, rect.w, ltk_global->display, window->xwindow, window->gc, ltk_global->colormap, fg, bg); + XSetForeground(ltk_global->display, window->gc, fg.pixel); + XFillRectangle(ltk_global->display, window->xwindow, window->gc, rect.x, rect.y, rect.w, rect.h); + for (int i = 0; i < te->soft_lines->len; i++) { + XImage *img = te->soft_lines->buf[i]->img; + int x = rect.x; + if (te->tl->dir == HB_DIRECTION_RTL) + x += rect.w - img->width; + XPutImage(ltk_global->display, window->xwindow, window->gc, img, 0, 0, x, rect.y + te->tl->h * i, img->width, img->height); + } } LtkTextEdit * @@ -57,21 +65,23 @@ ltk_create_text_edit(LtkWindow *window, const char *text) { ltk_fill_widget_defaults(&te->widget, window, &ltk_draw_text_edit, &ltk_destroy_text_edit, 1); te->tl = ltk_text_line_create(); ltk_text_line_insert_utf8(te->tl, text); - te->img = NULL; + te->soft_lines = NULL; return te; } void ltk_text_edit_insert_text(LtkTextEdit *te, const char *text) { ltk_text_line_insert_utf8(te->tl, text); - if (te->img) XDestroyImage(te->img); - te->img = NULL; + if (te->soft_lines) + ltk_array_destroy_deep_line(te->soft_lines, &ltk_soft_line_destroy); + te->soft_lines = NULL; /* FIXME: Need to "queue redraw" for whole window */ ltk_draw_text_edit(te); } void ltk_destroy_text_edit(LtkTextEdit *te) { ltk_text_line_destroy(te->tl); - if (te->img) XDestroyImage(te->img); + if (te->soft_lines) + ltk_array_destroy_deep_line(te->soft_lines, &ltk_soft_line_destroy); free(te); } diff --git a/text_edit.h b/text_edit.h @@ -26,8 +26,8 @@ typedef struct { LtkWidget widget; - XImage *img; struct ltk_text_line *tl; + struct ltk_array_line *soft_lines; } LtkTextEdit; /* FIXME: standardize ltk_<widget>_destroy, etc. instead of ltk_destroy_<widget> */