vtgl

terminal emulator implemented in OpenGL
git clone anongit@rnpnr.xyz:vtgl.git
Log | Files | Refs | Feed | LICENSE

Commit: 6136aaecbfc4f3c68a0a9c31cc7d8521b838243b
Parent: f796574d3710d49f36df8e5b06b07db5371980dd
Author: Randy Palamar
Date:   Sun, 25 Aug 2024 22:39:22 -0600

proper wide character support

Diffstat:
Mterminal.c | 33++++++++++++++++++++++++++-------
Mutil.h | 2++
Mvtgl.c | 114++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
3 files changed, 97 insertions(+), 52 deletions(-)

diff --git a/terminal.c b/terminal.c @@ -28,14 +28,26 @@ static const u8 utf8overhangmask[32] = { static Range get_word_around_cell(Term *t, iv2 cell) { - Range result = {.start = cell, .end = cell}; - Framebuffer *fb = &t->views[t->view_idx].fb; + Range result = {.start = cell, .end = cell}; + Cell *row = t->views[t->view_idx].fb.rows[cell.y]; - b32 isspace = ISSPACE(fb->rows[cell.y][cell.x].cp); - while (result.start.x > 0 && isspace == ISSPACE(fb->rows[cell.y][result.start.x - 1].cp)) + b32 isspace = ISSPACE(row[cell.x].cp); + while (result.start.x > 0) { + Cell nc = row[result.start.x - 1]; + if (!(nc.style.attr & ATTR_WDUMMY) && isspace != ISSPACE(nc.cp)) + break; result.start.x--; - while (result.end.x < t->size.w - 1 && isspace == ISSPACE(fb->rows[cell.y][result.end.x + 1].cp)) + } + while (result.end.x < t->size.w - 1) { + Cell nc = row[result.end.x + 1]; + if (!(nc.style.attr & ATTR_WDUMMY) && isspace != ISSPACE(nc.cp)) + break; result.end.x++; + } + + /* NOTE: ATTR_WDUMMY is invalid for start and end of range */ + if (row[result.start.x].style.attr & ATTR_WDUMMY) result.start.x++; + if (row[result.end.x].style.attr & ATTR_WDUMMY) result.end.x--; return result; } @@ -609,6 +621,7 @@ set_scrolling_region(Term *t, CSI *csi) t->top = t->bot; t->bot = tmp; } + cursor_move_to(t, 0, 0); } static void @@ -1081,7 +1094,7 @@ push_line(Term *t, Line *line, Arena a) } if (t->cursor.pos.x + width > t->size.w) { - /* NOTE: make space character if mode enabled else + /* NOTE: make space for character if mode enabled else * clobber whatever was on the end of the line */ if (t->mode & TM_AUTO_WRAP) push_newline(t, 1); @@ -1093,7 +1106,13 @@ push_line(Term *t, Line *line, Arena a) c->cp = cp; c->style = t->cursor.style; - /* TODO: wide characters (need some blank spaces?) */ + if (width == 2) { + c->style.attr |= ATTR_WIDE; + if (t->cursor.pos.x + 1 < t->size.w) { + Cell *nc = c + 1; + nc->style.attr |= ATTR_WDUMMY; + } + } if (t->cursor.pos.x + width < t->size.w) cursor_step_column(t, width); diff --git a/util.h b/util.h @@ -105,6 +105,8 @@ enum cell_attribute { ATTR_INVERSE = 1 << 5, ATTR_INVISIBLE = 1 << 6, ATTR_STRUCK = 1 << 7, + ATTR_WIDE = 1 << 8, + ATTR_WDUMMY = 1 << 9, /* NOTE: used to skip cells when copying */ }; typedef struct { diff --git a/vtgl.c b/vtgl.c @@ -312,6 +312,24 @@ push_cell(RenderPushBuffer *rpb, Term *t, Cell c, Rect r, f32 font_text_dy) rpb->texcolours[idx + 1].y = (cs.attr & ATTR_INVERSE)? cs.fg.rgba : cs.bg.rgba; } +static void +push_cell_row(Term *t, Cell *row, u32 len, b32 inverse, Rect cr, RenderPushBuffer *rpb) +{ + ASSERT(inverse == 0 || inverse == 1); + v2 cs = cr.size; + v2 csw = {.w = cs.w * 2, .h = cs.h}; + for (u32 c = 0; c < len; c++) { + Cell cell = row[c]; + if (cell.style.attr & ATTR_WDUMMY) + continue; + if (cell.style.attr & ATTR_WIDE) cr.size = csw; + else cr.size = cs; + cell.style.attr ^= inverse * ATTR_INVERSE; + push_cell(rpb, t, cell, cr, t->fa.deltay); + cr.pos.x += cr.size.w; + } +} + /* NOTE: In this program we render to an offscreen render target that is later drawn * to the screen as a full window quad. Therefore render_framebuffer must take care * to handle all necessary padding (window and cell). Outside of here everyone should @@ -319,32 +337,15 @@ push_cell(RenderPushBuffer *rpb, Term *t, Cell c, Rect r, f32 font_text_dy) static void render_framebuffer(Term *t, RenderPushBuffer *rpb) { - v2 tl = get_terminal_top_left(t); - v2 cs = get_cell_size(t); + v2 tl = get_terminal_top_left(t); + v2 cs = get_cell_size(t); + Rect cr = {.pos = {.x = tl.x, .y = tl.y - cs.h}, .size = cs}; TermView *tv = t->views + t->view_idx; - { - Rect cr = {.pos = {.x = tl.x, .y = tl.y - cs.h}, .size = cs}; - for (u32 r = 0; r < t->size.h; r++) { - for (u32 c = 0; c < t->size.w; c++) { - push_cell(rpb, t, tv->fb.rows[r][c], cr, t->fa.deltay); - cr.pos.x += cs.w; - } - cr.pos.x = tl.x; - cr.pos.y -= cs.h; - } - } - - /* NOTE: draw cursor */ - if (!(t->gl.mode & WIN_MODE_HIDECURSOR) && t->scroll_offset == 0) { - iv2 curs = t->cursor.pos; - Rect cr = { - .pos = {.x = tl.x + cs.w * curs.x, .y = tl.h - cs.h * (curs.y + 1)}, - .size = cs, - }; - Cell cursor = tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x]; - cursor.style.attr ^= ATTR_INVERSE; - push_cell(rpb, t, cursor, cr, t->fa.deltay); + /* NOTE: draw whole framebuffer */ + for (u32 r = 0; r < t->size.h; r++) { + push_cell_row(t, tv->fb.rows[r], t->size.w, 0, cr, rpb); + cr.pos.y -= cs.h; } /* NOTE: draw selection if active */ @@ -353,29 +354,32 @@ render_framebuffer(Term *t, RenderPushBuffer *rpb) Range sel = t->selection.range; iv2 curs = sel.start; iv2 end = sel.end; - Rect cr = { - .pos = {.x = tl.x + cs.w * curs.x, .y = tl.h - cs.h * (curs.y + 1)}, - .size = cs, - }; + cr.pos = (v2){.x = tl.x + cs.w * curs.x, .y = tl.h - cs.h * (curs.y + 1)}; /* NOTE: do full rows first */ for (; curs.y < end.y; curs.y++) { - for (; curs.x < t->size.w; curs.x++) { - Cell cell = tv->fb.rows[curs.y][curs.x]; - cell.style.attr ^= ATTR_INVERSE; - push_cell(rpb, t, cell, cr, t->fa.deltay); - cr.pos.x += cs.w; - } + u32 len = t->size.w - curs.x - 1; + push_cell_row(t, tv->fb.rows[curs.y] + curs.x, len, 1, cr, rpb); curs.x = 0; cr.pos.x = tl.x; cr.pos.y -= cs.h; } /* NOTE: do the last row */ - for (; curs.x <= end.x; curs.x++) { - Cell cell = tv->fb.rows[curs.y][curs.x]; - cell.style.attr ^= ATTR_INVERSE; - push_cell(rpb, t, cell, cr, t->fa.deltay); - cr.pos.x += cs.w; + push_cell_row(t, tv->fb.rows[curs.y] + curs.x, end.x - curs.x + 1, 1, cr, rpb); + } + + /* NOTE: draw cursor */ + if (!(t->gl.mode & WIN_MODE_HIDECURSOR) && t->scroll_offset == 0) { + iv2 curs = t->cursor.pos; + cr.pos = (v2){.x = tl.x + cs.w * curs.x, .y = tl.h - cs.h * (curs.y + 1)}; + Cell cursor = tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x]; + if (cursor.style.attr & ATTR_WDUMMY) { + cursor = tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x - 1]; + ASSERT(cursor.style.attr & ATTR_WIDE); } + if (cursor.style.attr & ATTR_WIDE) + cr.size.w *= 2; + cursor.style.attr ^= ATTR_INVERSE; + push_cell(rpb, t, cursor, cr, t->fa.deltay); } } @@ -419,6 +423,10 @@ update_selection(Term *t) return; iv2 new_p = mouse_to_cell_space(t, mouse); + if (t->views[t->view_idx].fb.rows[new_p.y][new_p.x].style.attr & ATTR_WDUMMY) { + ASSERT(t->views[t->view_idx].fb.rows[new_p.y][new_p.x - 1].style.attr & ATTR_WIDE); + new_p.x--; + } if (sel->state != SS_WORDS) { sel->range.start = sel->anchor.start; sel->range.end = new_p; @@ -463,10 +471,14 @@ KEYBIND_FN(copy) u32 last_non_space_idx = 0; for (; curs.y < end.y; curs.y++) { for (; curs.x < t->size.w && buf_curs != buf_size; curs.x++) { - /* TODO: handle the utf8 case */ - u32 cp = tv->fb.rows[curs.y][curs.x].cp; - if (!ISSPACE(cp)) last_non_space_idx = buf_curs; - buf[buf_curs++] = cp; + Cell c = tv->fb.rows[curs.y][curs.x]; + if (c.style.attr & ATTR_WDUMMY) + continue; + if (!ISSPACE(c.cp)) + last_non_space_idx = buf_curs; + s8 enc = utf8_encode(c.cp); + for (size i = 0; i < enc.len && buf_curs != buf_size; i++) + buf[buf_curs++] = enc.data[i]; } buf[last_non_space_idx + 1] = '\n'; buf_curs = last_non_space_idx + 2; @@ -474,8 +486,16 @@ KEYBIND_FN(copy) } /* NOTE: do the last row */ - for (; curs.x <= end.x && buf_curs != buf_size; curs.x++) - buf[buf_curs++] = (char)tv->fb.rows[curs.y][curs.x].cp; + for (; curs.x <= end.x && buf_curs != buf_size; curs.x++) { + Cell c = tv->fb.rows[curs.y][curs.x]; + if (c.style.attr & ATTR_WDUMMY) + continue; + if (!ISSPACE(c.cp)) + last_non_space_idx = buf_curs; + s8 enc = utf8_encode(c.cp); + for (size i = 0; i < enc.len && buf_curs != buf_size; i++) + buf[buf_curs++] = enc.data[i]; + } CLAMP(buf_curs, 0, buf_size - 1); buf[buf_curs] = 0; @@ -667,6 +687,10 @@ mouse_button_callback(GLFWwindow *win, i32 btn, i32 act, i32 mod) t->selection.state++; iv2 cell = mouse_to_cell_space(t, t->selection.mouse_start); + if (t->views[t->view_idx].fb.rows[cell.y][cell.x].style.attr & ATTR_WDUMMY) { + ASSERT(t->views[t->view_idx].fb.rows[cell.y][cell.x - 1].style.attr & ATTR_WIDE); + cell.x--; + } if (t->selection.state == SS_WORDS) { t->selection.anchor = get_word_around_cell(t, cell);