vtgl

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

Commit: f46fdce43af7076fd42711e2ce42bb4f03fb188e
Parent: 9cd994eee2ed8eddabc4a9d9a2dc69f5df404207
Author: Randy Palamar
Date:   Tue, 20 Aug 2024 22:57:11 -0600

basic mouse selection and copying

Word selection still to come!

Diffstat:
Mmain.c | 10++++++----
Mterminal.c | 4++--
Mutil.h | 24+++++++++++++++++++++---
Mvtgl.c | 195++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
4 files changed, 208 insertions(+), 25 deletions(-)

diff --git a/main.c b/main.c @@ -16,7 +16,7 @@ static void do_debug(GLCtx *gl) { } static char *libname = "./vtgl.so"; static void *libhandle; -typedef void do_terminal_fn(Term *, Arena); +typedef void do_terminal_fn(Term *); static do_terminal_fn *do_terminal; typedef void init_callbacks_fn(GLCtx *); @@ -298,11 +298,13 @@ main(void) check_shaders(&term.gl, memory); f32 current_time = (f32)glfwGetTime(); - term.gl.dt = current_time - last_time; - last_time = current_time; + term.gl.dt = current_time - last_time; + last_time = current_time; + + term.arena_for_frame = memory; glfwPollEvents(); - do_terminal(&term, memory); + do_terminal(&term); glfwSwapBuffers(term.gl.window); } diff --git a/terminal.c b/terminal.c @@ -257,7 +257,7 @@ dump_csi(CSI *csi) static void erase_in_display(Term *t, CSI *csi) { - uv2 cpos = t->cursor.pos; + iv2 cpos = t->cursor.pos; switch (csi->argv[0]) { case 0: /* Erase Below (default) */ fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w); @@ -283,7 +283,7 @@ erase_in_display(Term *t, CSI *csi) static void erase_in_line(Term *t, CSI *csi) { - uv2 cpos = t->cursor.pos; + iv2 cpos = t->cursor.pos; switch (csi->argv[0]) { case 0: /* Erase to Right */ fb_clear_region(t, cpos.y, cpos.y, cpos.x, t->size.w); diff --git a/util.h b/util.h @@ -29,13 +29,14 @@ #define MAX(a, b) ((a) >= (b) ? (a) : (b)) #define CLAMP(x, a, b) ((x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)) +#define ISSPACE(c) ((c) == ' ' || (c) == '\n' || (c) == '\t') #define ISPRINT(c) BETWEEN((c), ' ', '~') /* NOTE: GLFW does not sequentially number keys so switch statement will never be optimized */ #define ENCODE_KEY(action, mod, key) (((action) << 24) | ((mod) << 16) | ((key) & 0xFFFF)) #define BACKLOG_SIZE (16 * MEGABYTE) -#define BACKLOG_LINES (1024UL) +#define BACKLOG_LINES (8192UL) #define ALT_BACKLOG_SIZE (2 * MEGABYTE) #define ALT_BACKLOG_LINES (1024UL) @@ -108,7 +109,7 @@ typedef struct { } Cell; typedef struct { - uv2 pos; + iv2 pos; CellStyle state; } Cursor; @@ -155,7 +156,7 @@ typedef struct { * a state machine. Any time a line hasn't played to completion we must * restart it from the original location lest it unintentionally cause a * screen scroll. */ - uv2 last_cursor_pos; + iv2 last_cursor_pos; size last_line_idx; } TermView; @@ -278,6 +279,20 @@ typedef union { void *v; } Arg; +#define DOUBLE_CLICK_TIME 0.5f +enum selection_states { + SS_NONE, + SS_CHAR, + SS_WORDS, +}; + +typedef struct { + iv2 start, end; + v2 mouse_start; + f32 click_param; + enum selection_states state; +} Selection; + enum terminal_mode { TM_ALTSCREEN = 1 << 0, TM_REPLACE = 1 << 1, @@ -287,6 +302,9 @@ typedef struct { GLCtx gl; FontAtlas fa; + Arena arena_for_frame; + + Selection selection; Cursor cursor; Cursor saved_cursors[2]; TermView views[2]; diff --git a/vtgl.c b/vtgl.c @@ -200,9 +200,9 @@ push_empty_cell_rect(RenderPushBuffer *rpb, Term *t, u32 minrow, u32 maxrow, u32 v2 cs = get_cell_size(t); v2 size = {.x = (maxcol - mincol + 1) * cs.w, .y = (maxrow - minrow + 1) * cs.h}; - v2 pos = {.x = mincol * cs.w, .y = t->gl.window_size.h - cs.h * (minrow + 1)}; + v2 pos = {.x = mincol * cs.w, .y = t->gl.window_size.h - cs.h * (maxrow + 1)}; - Colour colour = g_colours.data[g_colours.bgidx]; + Colour colour = g_colours.data[g_colours.fgidx]; push_char(rpb, &t->gl, size, pos, (v2){0}, (uv2){.y = colour.rgba}, 0); } @@ -273,28 +273,163 @@ static void render_framebuffer(Term *t, RenderPushBuffer *rpb) { v2 cs = get_cell_size(t); - Rect cr = {.pos = {.y = t->gl.window_size.h - cs.h}, .size = cs}; TermView *tv = t->views + t->view_idx; - for (u32 r = 0; r < t->size.h; r++) { - for (u32 c = 0; c < t->size.w; c++) { - push_cell(rpb, &t->gl, tv->fb.rows[r][c], cr, t->fa.deltay); - cr.pos.x += cs.w; + { + Rect cr = {.pos = {.y = t->gl.window_size.h - 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->gl, tv->fb.rows[r][c], cr, t->fa.deltay); + cr.pos.x += cs.w; + } + cr.pos.x = 0; + cr.pos.y -= cs.h; } - cr.pos.x = 0; - cr.pos.y -= cs.h; } /* NOTE: draw cursor */ /* TODO: hide cursor doesn't get reset properly */ //if (!(t->gl.mode & WIN_MODE_HIDECURSOR)) { + Rect cr = { + .pos = { + .x = cs.w * t->cursor.pos.x, + .y = t->gl.window_size.h - cs.h * (t->cursor.pos.y + 1), + }, + .size = cs, + }; Cell cursor = tv->fb.rows[t->cursor.pos.y][t->cursor.pos.x]; - cr.pos.y = t->gl.window_size.h - cs.h * (t->cursor.pos.y+ 1); - cr.pos.x = t->cursor.pos.x* cs.w; cursor.style.attr ^= ATTR_INVERSE; push_cell(rpb, &t->gl, cursor, cr, t->fa.deltay); } + + /* NOTE: draw selection if active */ + /* TODO: combine with original push_cell? */ + if (t->selection.end.x != -1) { + iv2 curs = t->selection.start; + Rect cr = { + .pos = { + .x = cs.w * curs.x, + .y = t->gl.window_size.h - cs.h * (curs.y + 1), + }, + .size = cs, + }; + /* NOTE: do full rows first */ + for (; curs.y < t->selection.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->gl, cell, cr, t->fa.deltay); + cr.pos.x += cs.w; + } + curs.x = 0; + cr.pos.x = 0; + cr.pos.y -= cs.h; + } + /* NOTE: do the last row */ + for (; curs.x < t->selection.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); + cr.pos.x += cs.w; + } + } +} + +static iv2 +mouse_to_cell_space(Term *t, v2 mouse) +{ + iv2 result = {0}; + v2 cell_size = get_cell_size(t); + + /* TODO: this needs to be adjusted for padding */ + f32 delta_x = t->gl.window_size.x - t->size.w * cell_size.w; + f32 delta_y = t->gl.window_size.h - t->size.h * cell_size.h; + + result.x = (i32)((mouse.x + delta_x) / cell_size.w); + result.y = (i32)((mouse.y + delta_y) / cell_size.h) - 1; + + CLAMP(result.x, 0, t->size.w - 1); + CLAMP(result.y, 0, t->size.h - 1); + + return result; +} + +static void +update_selection(Term *t) +{ + Selection *sel = &t->selection; + b32 held = glfwGetMouseButton(t->gl.window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; + + sel->click_param -= t->gl.dt; + if (sel->click_param < 0) { + sel->click_param = 0; + if (!held) + sel->state = SS_NONE; + } + + if (!held) + return; + + f64 xpos, ypos; + 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) + return; + + iv2 newp = mouse_to_cell_space(t, mouse); + + if (0 && t->selection.state == SS_WORDS) { + /* TODO: word selection */ + } else { + if (newp.x <= sel->start.x && newp.y <= sel->start.y) { + if (sel->end.x == -1) + sel->end = sel->start; + sel->start = newp; + } else { + sel->end = newp; + } + } +} + +KEYBIND_FN(copy) +{ + if (t->selection.end.x == -1) + return 1; + + TermView *tv = t->views + t->view_idx; + iv2 curs = t->selection.start; + i32 buf_curs = 0; + + /* NOTE: super piggy but we are only holding onto it for the function duration */ + Arena arena = t->arena_for_frame; + size buf_size = 1 * MEGABYTE; + char *buf = alloc(&arena, char, buf_size); + + /* NOTE: do full rows first */ + u32 last_non_space_idx = 0; + for (; curs.y < t->selection.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; + } + buf[last_non_space_idx + 1] = '\n'; + buf_curs = last_non_space_idx + 2; + curs.x = 0; + } + + /* NOTE: do the last row */ + for (; curs.x < t->selection.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); + buf[buf_curs] = 0; + glfwSetClipboardString(0, buf); + + return 1; } KEYBIND_FN(paste) @@ -444,6 +579,30 @@ key_callback(GLFWwindow *win, i32 key, i32 sc, i32 act, i32 mods) } static void +mouse_button_callback(GLFWwindow *win, i32 btn, i32 act, i32 mod) +{ + Term *t = glfwGetWindowUserPointer(win); + /* TODO: map other mouse buttons */ + if (btn != GLFW_MOUSE_BUTTON_LEFT) + return; + + if (act == GLFW_RELEASE) + return; + + f64 xpos, ypos; + glfwGetCursorPos(win, &xpos, &ypos); + t->selection.end = (iv2){.x = -1, .y = -1}; + t->selection.mouse_start = (v2){.x = xpos, .y = ypos}; + t->selection.click_param = DOUBLE_CLICK_TIME; + + if (t->selection.state != SS_WORDS) + t->selection.state++; + + iv2 cell = mouse_to_cell_space(t, t->selection.mouse_start); + t->selection.start = cell; +} + +static void char_callback(GLFWwindow *win, u32 codepoint) { Term *t = glfwGetWindowUserPointer(win); @@ -451,6 +610,7 @@ char_callback(GLFWwindow *win, u32 codepoint) t->scroll_offset = 0; t->gl.flags |= NEEDS_FULL_BLIT; } + t->selection.end = (iv2){.x = -1, .y = -1}; os_child_put_char(t->child, codepoint); } @@ -497,12 +657,13 @@ init_callbacks(GLCtx *gl) glfwSetCharCallback(gl->window, char_callback); glfwSetFramebufferSizeCallback(gl->window, fb_callback); glfwSetKeyCallback(gl->window, key_callback); + glfwSetMouseButtonCallback(gl->window, mouse_button_callback); //glfwSetWindowRefreshCallback(gl->window, refresh_callback); glfwSetScrollCallback(gl->window, scroll_callback); } DEBUG_EXPORT void -do_terminal(Term *t, Arena a) +do_terminal(Term *t) { static f32 last_frame_time; f32 frame_start_time = (f32)glfwGetTime(); @@ -534,14 +695,16 @@ do_terminal(Term *t, Arena a) } if (t->gl.flags & (NEEDS_BLIT|NEEDS_FULL_BLIT)) - blit_lines(t, a, parsed_lines); + blit_lines(t, t->arena_for_frame, parsed_lines); + + update_selection(t); /* NOTE: reset the camera/viewport */ glUseProgram(t->gl.programs[SHADER_RENDER]); glUniform1i(t->gl.render.texslot, 0); glBindFramebuffer(GL_FRAMEBUFFER, t->gl.fb); - RenderPushBuffer *rpb = alloc(&a, RenderPushBuffer, 1); + RenderPushBuffer *rpb = alloc(&t->arena_for_frame, RenderPushBuffer, 1); clear_colour(); render_framebuffer(t, rpb); @@ -560,8 +723,8 @@ do_terminal(Term *t, Arena a) src_bl.y = cursor_pos.y; } - { - s8 fps = s8alloc(&a, 64); + if (0) { + s8 fps = s8alloc(&t->arena_for_frame, 64); fps.len = snprintf((char *)fps.data, fps.len, "Render Time: %0.02f ms/f", last_frame_time * 1e3); v2 ts = measure_text(&t->gl, fps, 1); v2 pos = {