ledit

Text editor (WIP)
git clone git://lumidify.org/ledit.git (fast, but not encrypted)
git clone https://lumidify.org/git/ledit.git (encrypted, but very slow)
Log | Files | Refs | README | LICENSE

commit e9c86a9b95b0a349d3f34f2a15dd3afffafc3b52
parent 155d5f07022e7d2fa4de2374b4ba6c1e9023e124
Author: lumidify <nobody@lumidify.org>
Date:   Sun,  4 Apr 2021 22:29:52 +0200

Cache pre-rendered text

Diffstat:
M.gitignore | 1+
Mledit.c | 296+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
2 files changed, 231 insertions(+), 66 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -1,4 +1,5 @@ tmp ledit +ledit_old *.core *.o diff --git a/ledit.c b/ledit.c @@ -1,3 +1,4 @@ +/* FIXME: horizontal scrolling (also need cache to avoid too large pixmaps) */ #include <math.h> #include <stdio.h> #include <errno.h> @@ -31,6 +32,8 @@ struct key { void (*func)(void); /* callback function */ }; +#define MAX_CACHE_PIXELS 100 + static struct { Display *dpy; GC gc; @@ -53,6 +56,17 @@ static struct { Atom wm_delete_msg; } state; +/* FIXME: possibly use at least 32 bits int? */ +static struct { + Pixmap pix; + XftDraw *draw; + int pix_w, pix_h; + long start_offset; + int valid_height; + int begin_line, begin_softline; + int dirty; +} cache; + static void mainloop(void); static void setup(int argc, char *argv[]); static void cleanup(void); @@ -74,17 +88,21 @@ main(int argc, char *argv[]) { static struct line { PangoLayout *layout; - XftDraw *draw; char *text; size_t cap; size_t len; - Pixmap pix; int w; int h; - int pix_w; - int pix_h; + long y_offset; + /* + XftDraw *draw; + Pixmap pix; + unsigned int pix_w; + unsigned int pix_h; + */ char dirty; } *lines = NULL; + static size_t lines_num = 0; static size_t lines_cap = 0; @@ -92,7 +110,7 @@ static int cur_line = 0; static int cur_subline = 0; static int cur_index = 0; static int trailing = 0; -static int total_height = 0; +static long total_height = 0; static double cur_display_offset = 0; static void @@ -104,13 +122,96 @@ init_line(struct line *l) { pango_layout_set_wrap(l->layout, PANGO_WRAP_WORD_CHAR); l->text = NULL; l->cap = l->len = 0; - l->pix = None; + /*l->pix = None;*/ /* FIXME: does this set line height reasonably when no text yet? */ pango_layout_get_pixel_size(l->layout, &l->w, &l->h); - l->dirty = 1; + l->y_offset = 0; + //l->dirty = 1; } -static void recalc_height_absolute(void); +static void recalc_cur_line_size(void); +static void recalc_line_size_absolute(void); + +enum CachePosition { + CACHE_NONE, + CACHE_BOTH, + CACHE_TOP, + CACHE_BOTTOM +}; + +static void redraw_cache_complete(enum CachePosition pos); +static void redraw_cache_after_cur_line(void); +static void redraw_cache_only_cur_line(void); + +#define MAX_INT(a, b) ((a) > (b) ? (a) : (b)) +#define MIN_INT(a, b) ((a) < (b) ? (a) : (b)) + +/* FIXME: Implement pos */ +static void +redraw_cache_complete(enum CachePosition pos) { + long start = cur_display_offset > MAX_CACHE_PIXELS ? (long)(cur_display_offset) - MAX_CACHE_PIXELS : 0; + long end = (long)(cur_display_offset) + state.h + MAX_CACHE_PIXELS; + int start_line = 0; + int end_line = 0; + + /* FIXME: make this more efficient - we can't start at cur_line + * because that may be off screen */ + /* + for (start_line = cur_line; + lines[start_line].y_offset > cur_display_offset; + start_line--) { + } + + for (end_line = cur_line; + end_line < lines_num && + lines[end_line].y_offset + lines[end_line].h < cur_display_offset + state.h; + end_line++) { + } + */ + + for (start_line = 0; + start_line < lines_num - 1 && + lines[start_line].y_offset + lines[start_line].h <= cur_display_offset; + start_line++) { + /* NOP */ + } + for (end_line = start_line; + end_line < lines_num - 1 && + lines[end_line].y_offset + lines[end_line].h < cur_display_offset + state.h; + end_line++) { + /* NOP */ + } + + if (lines[start_line].y_offset < start) { + printf("FIX THIS CODE 1!\n"); + } + if (lines[end_line].y_offset + lines[end_line].h > end) { + printf("FIX THIS CODE 2!\n"); + } + + /* FIXME: only wipe what is necessary */ + XftDrawRect(cache.draw, &state.bg, 0, 0, cache.pix_w, cache.pix_h); + int cur_y = 0; + for (int i = start_line; i <= end_line; i++) { + if (lines[i].w > cache.pix_w || cur_y + lines[i].h > cache.pix_h) { + break; /* should never happen */ + } + pango_xft_render_layout(cache.draw, &state.fg, lines[i].layout, 0, cur_y * PANGO_SCALE); + cur_y += lines[i].h; + } + cache.valid_height = cur_y; + cache.start_offset = lines[start_line].y_offset; + cache.begin_line = start_line; + cache.dirty = 0; +} + +static void +redraw_cache_after_cur_line(void) { +} + +static void +redraw_cache_only_cur_line(void) { +} static void insert_text(struct line *l, int index, char *text, int len) { @@ -127,8 +228,8 @@ insert_text(struct line *l, int index, char *text, int len) { memcpy(l->text + index, text, len); l->len += len; pango_layout_set_text(l->layout, l->text, l->len); - recalc_height_absolute(); - l->dirty = 1; + recalc_cur_line_size(); + cache.dirty = 1; } static void insert_line_entry(int index); @@ -136,6 +237,7 @@ static void insert_line_entry(int index); static void render_line(struct line *l) { /* FIXME: check for <= 0 on size */ + /* if (l->pix == None) { l->pix = XCreatePixmap(state.dpy, state.back_buf, l->w + 10, l->h + 10, state.depth); l->pix_w = l->w + 10; @@ -153,6 +255,7 @@ render_line(struct line *l) { XftDrawRect(l->draw, &state.bg, 0, 0, l->w, l->h); pango_xft_render_layout(l->draw, &state.fg, l->layout, 0, 0); l->dirty = 0; + */ } static void @@ -171,7 +274,7 @@ append_line(int text_index, int line_index) { if (text_index != -1) { struct line *l = &lines[line_index]; int len = l->len - text_index; - new_l->pix = None; + //new_l->pix = None; new_l->len = len; new_l->cap = len + 10; new_l->text = malloc(new_l->cap); @@ -182,6 +285,7 @@ append_line(int text_index, int line_index) { pango_layout_set_text(l->layout, l->text, l->len); /* FIXME: set height here */ } + /* FIXME: update line heights, etc. */ } static void change_keyboard(char *lang); @@ -207,6 +311,31 @@ static int scroll_dragging = 0; static int scroll_grab_handle = 0; static void +set_line_cursor_attrs(int line, int index) { + if (cur_mode == NORMAL) { + PangoAttribute *attr0 = pango_attr_background_new(0, 0, 0); + PangoAttribute *attr1 = pango_attr_foreground_new(65535, 65535, 65535); + attr0->start_index = index; + attr0->end_index = index + 1; + attr1->start_index = index; + attr1->end_index = index + 1; + PangoAttribute *attr2 = pango_attr_insert_hyphens_new(FALSE); + PangoAttrList *list = pango_attr_list_new(); + pango_attr_list_insert(list, attr0); + pango_attr_list_insert(list, attr1); + pango_attr_list_insert(list, attr2); + pango_layout_set_attributes(lines[line].layout, list); + } else { + pango_layout_set_attributes(lines[line].layout, basic_attrs); + } +} + +static void +wipe_line_cursor_attrs(int line) { + pango_layout_set_attributes(lines[line].layout, basic_attrs); +} + +static void mainloop(void) { XEvent event; int xkb_event_type; @@ -348,7 +477,10 @@ mainloop(void) { XSetForeground(state.dpy, state.gc, state.bg.pixel); XFillRectangle(state.dpy, state.back_buf, state.gc, 0, 0, state.w, state.h); int h = 0; + if (cache.dirty) + redraw_cache_complete(CACHE_BOTH); /*int cur_line_height = 0;*/ + /* int tmp_w, tmp_h; int cur_line_y = 0; int cursor_displayed = 0; @@ -394,24 +526,32 @@ mainloop(void) { break; h += lines[i].h; } + */ + double offset = cur_display_offset - cache.start_offset; + if (offset < 0) { + printf("FIX THIS CODE 3!\n"); + offset = 0; + } + XCopyArea(state.dpy, cache.pix, state.back_buf, state.gc, 0, (int)offset, state.w - 10, state.h, 0, 0); need_redraw = 0; XSetForeground(state.dpy, state.gc, state.fg.pixel); PangoRectangle strong, weak; pango_layout_get_cursor_pos(lines[cur_line].layout, cur_index, &strong, &weak); - int cursor_y = strong.y / PANGO_SCALE + cur_line_y; - if (cursor_displayed && cursor_y >= 0) { + /* FIXME: long, int, etc. */ + long cursor_y = strong.y / PANGO_SCALE + lines[cur_line].y_offset; + if (cursor_y >= cur_display_offset && cursor_y < cur_display_offset + state.h) { if (cur_mode == NORMAL && cur_index == lines[cur_line].len) { XFillRectangle( state.dpy, state.back_buf, state.gc, - strong.x / PANGO_SCALE, cursor_y, + strong.x / PANGO_SCALE, cursor_y - (long)cur_display_offset, 10, strong.height / PANGO_SCALE ); } else if (cur_mode == INSERT) { XDrawLine( state.dpy, state.back_buf, state.gc, - strong.x / PANGO_SCALE, cursor_y, - strong.x / PANGO_SCALE, (strong.y + strong.height) / PANGO_SCALE + cur_line_y + strong.x / PANGO_SCALE, cursor_y - (long)cur_display_offset, + strong.x / PANGO_SCALE, (strong.y + strong.height) / PANGO_SCALE + lines[cur_line].y_offset - (long)cur_display_offset ); } } @@ -480,13 +620,20 @@ setup(int argc, char *argv[]) { state.cm = DefaultColormap(state.dpy, state.screen); memset(&attrs, 0, sizeof(attrs)); - attrs.background_pixmap = None; + attrs.background_pixel = BlackPixel(state.dpy, state.screen); attrs.colormap = state.cm; + /* this causes the window contents to be kept + * when it is resized, leading to less flicker */ + attrs.bit_gravity = NorthWestGravity; state.win = XCreateWindow(state.dpy, DefaultRootWindow(state.dpy), 0, 0, state.w, state.h, 0, state.depth, - InputOutput, state.vis, CWBackPixmap | CWColormap, &attrs); + InputOutput, state.vis, CWBackPixel | CWColormap | CWBitGravity, &attrs); state.back_buf = XdbeAllocateBackBufferName(state.dpy, state.win, XdbeBackground); + cache.pix = XCreatePixmap(state.dpy, state.back_buf, 500, 500, state.depth); + cache.pix_w = cache.pix_h = 500; + cache.draw = XftDrawCreate(state.dpy, cache.pix, state.vis, state.cm); + cache.dirty = 1; memset(&gcv, 0, sizeof(gcv)); gcv.line_width = 1; @@ -543,59 +690,56 @@ button_release(void) { static void ensure_cursor_shown(void) { - int cur_line_y = -1; - int h = 0; - /* FIXME: cache this to avoid useless recalculation */ - for (int i = 0; i < lines_num; i++) { - if (cur_line == i) { - cur_line_y = h; - break; - } - h += lines[i].h; - } - if (cur_line_y < 0) - return; PangoRectangle strong, weak; pango_layout_get_cursor_pos(lines[cur_line].layout, cur_index, &strong, &weak); - int cursor_y = strong.y / PANGO_SCALE + cur_line_y; + long cursor_y = strong.y / PANGO_SCALE + lines[cur_line].y_offset; if (cursor_y < cur_display_offset) { cur_display_offset = cursor_y; + cache.dirty = 1; } else if (cursor_y + strong.height / PANGO_SCALE > cur_display_offset + state.h) { cur_display_offset = cursor_y - state.h + strong.height / PANGO_SCALE; + cache.dirty = 1; } } static void -recalc_height(void) { - /* - int w, h; - pango_layout_get_pixel_size(lines[cur_line].layout, &w, &h); - total_height += (h - cur_line_height); - */ - if (total_height < 0) - total_height = 0; /* should never actually happen */ - /*cur_line_height = h;*/ -} - -static void -set_cur_line_height(void) { +recalc_cur_line_size(void) { int w, h; pango_layout_get_pixel_size(lines[cur_line].layout, &w, &h); - lines[cur_line].h = h; - /*cur_line_height = h;*/ + lines[cur_line].w = w; + /* if height changed, set height of current line + * and adjust offsets of all lines following it */ + if (lines[cur_line].h != h) { + int delta = h - lines[cur_line].h; + lines[cur_line].h = h; + /* protect against underflow even though + * it should never happen anyways... */ + if (delta < 0 && total_height < -delta) + total_height = 0; + else + total_height += delta; + for (int i = cur_line + 1; i < lines_num; i++) { + /* yeah, maybe I should just use a signed type... */ + if (delta < 0 && lines[i].y_offset < -delta) + lines[i].y_offset = 0; + else + lines[i].y_offset += delta; + } + } } static void -recalc_height_absolute(void) { +recalc_line_size_absolute(void) { int w, h; total_height = 0; + /* completely recalculate line sizes and offsets from scratch */ for (int i = 0; i < lines_num; i++) { + lines[i].y_offset = total_height; pango_layout_get_pixel_size(lines[i].layout, &w, &h); total_height += h; lines[i].w = w; lines[i].h = h; } - set_cur_line_height(); } static void @@ -608,11 +752,19 @@ resize_window(int w, int h) { /* 10 pixels for scrollbar */ pango_layout_set_width(lines[i].layout, (w - 10) * PANGO_SCALE); pango_layout_get_pixel_size(lines[i].layout, &tmp_w, &tmp_h); - total_height += tmp_h; lines[i].h = tmp_h; - lines[i].dirty = 1; + lines[i].w = tmp_w; + lines[i].y_offset = total_height; + total_height += tmp_h; + } + if (cache.pix_w < state.w - 10 || cache.pix_h < state.h + 2 * MAX_CACHE_PIXELS) { + XFreePixmap(state.dpy, cache.pix); + cache.pix = XCreatePixmap(state.dpy, state.back_buf, state.w, state.h + 2 * MAX_CACHE_PIXELS + 50, state.depth); + cache.pix_w = state.w; + cache.pix_h = state.h + 2 * MAX_CACHE_PIXELS + 50; + XftDrawChange(cache.draw, cache.pix); } - //set_cur_line_height(); + cache.dirty = 1; } static void @@ -651,8 +803,9 @@ backspace(void) { cur_index = i; pango_layout_set_text(l->layout, l->text, l->len); } - lines[cur_line].dirty = 1; - recalc_height_absolute(); + set_line_cursor_attrs(cur_line, cur_index); + cache.dirty = 1; + recalc_cur_line_size(); } static void @@ -677,8 +830,9 @@ delete_key(void) { l->len -= i - cur_index; pango_layout_set_text(l->layout, l->text, l->len); } - lines[cur_line].dirty = 1; - recalc_height_absolute(); + set_line_cursor_attrs(cur_line, cur_index); + cache.dirty = 1; + recalc_cur_line_size(); } static void @@ -703,7 +857,8 @@ move_cursor(int dir) { else cur_index = lines[cur_line].len; } - lines[cur_line].dirty = 1; + set_line_cursor_attrs(cur_line, cur_index); + cache.dirty = 1; } static void @@ -719,11 +874,14 @@ cursor_right(void) { static void return_key(void) { append_line(cur_index, cur_line); - lines[cur_line].dirty = 1; + /* FIXME: these aren't needed, right? This only works in insert mode + * anyways, so there's nothing to wipe */ + wipe_line_cursor_attrs(cur_line); cur_line++; - lines[cur_line].dirty = 1; + set_line_cursor_attrs(cur_line, cur_index); + cache.dirty = 1; cur_index = 0; - recalc_height_absolute(); + recalc_line_size_absolute(); } static void @@ -740,7 +898,8 @@ escape_key(void) { } else { cursor_left(); } - lines[cur_line].dirty = 1; + set_line_cursor_attrs(cur_line, cur_index); + cache.dirty = 1; /* if (cur_index > 0) cursor_left(); @@ -755,7 +914,8 @@ i_key(void) { pango_layout_set_attributes(lines[i].layout, NULL); } */ - lines[cur_line].dirty = 1; + wipe_line_cursor_attrs(cur_line); + cache.dirty = 1; } static void @@ -765,7 +925,7 @@ line_down(void) { int maxlines = pango_layout_get_line_count(lines[cur_line].layout); PangoLayoutLine *line = pango_layout_get_line_readonly(lines[cur_line].layout, lineno); if (lineno == maxlines - 1) { - lines[cur_line].dirty = 1; + wipe_line_cursor_attrs(cur_line); /* move to the next hard line */ if (cur_line < lines_num - 1) { cur_line++; @@ -785,7 +945,8 @@ line_down(void) { } if (cur_index > 0 && cur_mode == NORMAL && cur_index >= lines[cur_line].len) cursor_left(); - lines[cur_line].dirty = 1; + set_line_cursor_attrs(cur_line, cur_index); + cache.dirty = 1; } static void @@ -794,7 +955,7 @@ line_up(void) { pango_layout_index_to_line_x(lines[cur_line].layout, cur_index, 0, &lineno, &x); PangoLayoutLine *line = pango_layout_get_line_readonly(lines[cur_line].layout, lineno); if (lineno == 0) { - lines[cur_line].dirty = 1; + wipe_line_cursor_attrs(cur_line); /* move to the previous hard line */ if (cur_line > 0) { cur_line--; @@ -815,13 +976,15 @@ line_up(void) { } if (cur_index > 0 && cur_mode == NORMAL && cur_index >= lines[cur_line].len) cursor_left(); - lines[cur_line].dirty = 1; + set_line_cursor_attrs(cur_line, cur_index); + cache.dirty = 1; } static void zero_key(void) { cur_index = 0; - lines[cur_line].dirty = 1; + set_line_cursor_attrs(cur_line, cur_index); + cache.dirty = 1; } static struct key keys_en[] = { @@ -924,6 +1087,7 @@ key_press(XEvent event) { if (cur_mode == INSERT && !found && n > 0) { insert_text(&lines[cur_line], cur_index, buf, n); cur_index += n; + recalc_cur_line_size(); } ensure_cursor_shown(); }