vtgl

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

Commit: 9f07a3c8ab0721beb511b6f28186481a901cfdce
Parent: 978e8aeb16dc03fe47a476093c5f4fc4a593daa2
Author: Randy Palamar
Date:   Fri, 23 Aug 2024 21:10:08 -0600

add word and elastic selections

Diffstat:
Mterminal.c | 14++++++++++++++
Mutil.c | 23+++++++++++++++++++++++
Mutil.h | 13++++++++-----
Mvtgl.c | 64+++++++++++++++++++++++++++++++++++++++++-----------------------
4 files changed, 86 insertions(+), 28 deletions(-)

diff --git a/terminal.c b/terminal.c @@ -23,6 +23,20 @@ static const u8 utf8overhangmask[32] = { 0, 0, 0, 0, 0, 0, 0, 0 }; +static Range +get_word_around_cell(Term *t, iv2 cell) +{ + Range result = {.start = cell, .end = cell}; + Framebuffer *fb = &t->views[t->view_idx].fb; + + 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)) + result.start.x--; + while (result.end.x < t->size.w - 1 && isspace == ISSPACE(fb->rows[cell.y][result.end.x + 1].cp)) + result.end.x++; + return result; +} + static void set_window_title(GLFWwindow *win, Arena a, s8 title) { diff --git a/util.c b/util.c @@ -10,6 +10,29 @@ equal_iv2(iv2 a, iv2 b) return result; } +static b32 +is_valid_range(Range r) +{ + b32 result = !equal_iv2(r.end, INVALID_RANGE_END); + return result; +} + +static Range +normalize_range(Range r) +{ + Range result; + if (r.start.y < r.end.y) { + result = r; + } else if (r.end.y < r.start.y) { + result = (Range){.start = r.end, .end = r.start}; + } else { + result.start.y = result.end.y = r.start.y; + result.start.x = MIN(r.start.x, r.end.x); + result.end.x = MAX(r.start.x, r.end.x); + } + return result; +} + static void __attribute__((noreturn)) die(char *fmt, ...) { diff --git a/util.h b/util.h @@ -86,6 +86,9 @@ typedef struct { u8 *beg, *end; } Arena; typedef struct { size len; u8 *data; } s8; #define s8(s) (s8){.len = ARRAY_COUNT(s) - 1, .data = (u8 *)s} +typedef struct { iv2 start, end; } Range; +#define INVALID_RANGE_END (iv2){.x = -1, .y = -1} + enum cell_attribute { ATTR_NULL = 0, ATTR_BOLD = 1 << 0, @@ -286,12 +289,12 @@ enum selection_states { }; typedef struct { - iv2 start, end; - v2 mouse_start; - f32 click_param; - enum selection_states state; + Range range; + Range anchor; + v2 mouse_start; + f32 click_param; + enum selection_states state; } Selection; -#define INVALID_SELECTION_POINT (iv2){.x = -1, .y = -1} enum terminal_mode { TM_ALTSCREEN = 1 << 0, diff --git a/vtgl.c b/vtgl.c @@ -138,8 +138,6 @@ update_font_textures(Term *t) static void update_uniforms(Term *t, enum shader_stages stage) { - ASSERT(stage != SHADER_LAST); - switch (stage) { case SHADER_RENDER: for (u32 i = 0; i < ARRAY_COUNT(t->gl.render.uniforms); i++) { @@ -341,14 +339,16 @@ render_framebuffer(Term *t, RenderPushBuffer *rpb) /* NOTE: draw selection if active */ /* TODO: combine with original push_cell? */ - if (!equal_iv2(t->selection.end, INVALID_SELECTION_POINT)) { - iv2 curs = t->selection.start; + if (is_valid_range(t->selection.range)) { + 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, }; /* NOTE: do full rows first */ - for (; curs.y < t->selection.end.y; curs.y++) { + 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; @@ -360,7 +360,7 @@ render_framebuffer(Term *t, RenderPushBuffer *rpb) cr.pos.y -= cs.h; } /* NOTE: do the last row */ - for (; curs.x < t->selection.end.x; curs.x++) { + 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->gl, cell, cr, t->fa.deltay); @@ -405,31 +405,43 @@ update_selection(Term *t) glfwGetCursorPos(t->gl.window, &xpos, &ypos); v2 mouse = {.x = xpos, .y = ypos}; - if (mouse.x == sel->mouse_start.x && mouse.y == sel->mouse_start.y) + if (sel->state != SS_WORDS && (mouse.x == sel->mouse_start.x && mouse.y == sel->mouse_start.y)) return; - iv2 newp = mouse_to_cell_space(t, mouse); - - if (0 && t->selection.state == SS_WORDS) { - /* TODO: word selection */ + iv2 new_p = mouse_to_cell_space(t, mouse); + if (sel->state != SS_WORDS) { + sel->range.start = sel->anchor.start; + sel->range.end = new_p; } else { - if (newp.x <= sel->start.x && newp.y <= sel->start.y) { - if (equal_iv2(t->selection.end, INVALID_SELECTION_POINT)) - sel->end = sel->start; - sel->start = newp; + Range word = get_word_around_cell(t, new_p); + if (sel->anchor.start.y < word.start.y) { + sel->range.start = sel->anchor.start; + sel->range.end = word.end; + } else if (sel->anchor.start.y > word.start.y) { + sel->range.start = word.start; + sel->range.end = sel->anchor.end; } else { - sel->end = newp; + if (word.start.x < sel->anchor.start.x) { + sel->range.start = word.start; + sel->range.end = sel->anchor.end; + } else { + sel->range.start = sel->anchor.start; + sel->range.end = word.end; + } } } + sel->range = normalize_range(sel->range); } KEYBIND_FN(copy) { - if (equal_iv2(t->selection.end, INVALID_SELECTION_POINT)) + if (!is_valid_range(t->selection.range)) return 1; TermView *tv = t->views + t->view_idx; - iv2 curs = t->selection.start; + Range sel = t->selection.range; + iv2 curs = sel.start; + iv2 end = sel.end; i32 buf_curs = 0; /* NOTE: super piggy but we are only holding onto it for the function duration */ @@ -439,7 +451,7 @@ KEYBIND_FN(copy) /* NOTE: do full rows first */ u32 last_non_space_idx = 0; - for (; curs.y < t->selection.end.y; curs.y++) { + 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; @@ -452,7 +464,7 @@ KEYBIND_FN(copy) } /* NOTE: do the last row */ - for (; curs.x < t->selection.end.x && buf_curs != buf_size; curs.x++) + for (; curs.x <= end.x && buf_curs != buf_size; curs.x++) buf[buf_curs++] = (char)tv->fb.rows[curs.y][curs.x].cp; CLAMP(buf_curs, 0, buf_size - 1); @@ -623,7 +635,7 @@ mouse_button_callback(GLFWwindow *win, i32 btn, i32 act, i32 mod) f64 xpos, ypos; glfwGetCursorPos(win, &xpos, &ypos); - t->selection.end = (iv2){.x = -1, .y = -1}; + t->selection.range.end = (iv2){.x = -1, .y = -1}; t->selection.mouse_start = (v2){.x = xpos, .y = ypos}; t->selection.click_param = DOUBLE_CLICK_TIME; @@ -631,7 +643,13 @@ 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); - t->selection.start = cell; + + if (t->selection.state == SS_WORDS) { + t->selection.anchor = get_word_around_cell(t, cell); + } else { + t->selection.anchor = (Range){.start = cell, .end = cell}; + t->selection.range.end = INVALID_RANGE_END; + } } static void @@ -642,7 +660,7 @@ char_callback(GLFWwindow *win, u32 codepoint) t->scroll_offset = 0; t->gl.flags |= NEEDS_FULL_BLIT; } - t->selection.end = INVALID_SELECTION_POINT; + t->selection.range.end = INVALID_RANGE_END; os_child_put_char(t->child, codepoint); }