Commit: 9f07a3c8ab0721beb511b6f28186481a901cfdce
Parent: 978e8aeb16dc03fe47a476093c5f4fc4a593daa2
Author: Randy Palamar
Date:   Fri, 23 Aug 2024 21:10:08 -0600
add word and elastic selections
Diffstat:
| M | terminal.c |  |  | 14 | ++++++++++++++ | 
| M | util.c |  |  | 23 | +++++++++++++++++++++++ | 
| M | util.h |  |  | 13 | ++++++++----- | 
| M | vtgl.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);
 }