diff --git a/CHANGELOG.md b/CHANGELOG.md index e571842f..dc49a9d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,8 @@ font glyphs (https://codeberg.org/dnkl/foot/issues/198) * Trailing comments in `foot.ini` must now be preceded by a space or tab (https://codeberg.org/dnkl/foot/issues/270) +* Double- or triple clicking then dragging now extends the selection + word- or line-wise (https://codeberg.org/dnkl/foot/issues/267). ### Deprecated diff --git a/extract.c b/extract.c index 7b5da229..a6492716 100644 --- a/extract.c +++ b/extract.c @@ -69,14 +69,17 @@ extract_finish(struct extraction_context *ctx, char **text, size_t *len) /* Selection of empty cells only */ if (!ensure_size(ctx, 1)) goto out; - ctx->buf[ctx->idx] = L'\0'; + ctx->buf[ctx->idx++] = L'\0'; } else { assert(ctx->idx > 0); - assert(ctx->idx < ctx->size); + assert(ctx->idx <= ctx->size); if (ctx->buf[ctx->idx - 1] == L'\n') ctx->buf[ctx->idx - 1] = L'\0'; - else - ctx->buf[ctx->idx] = L'\0'; + else { + if (!ensure_size(ctx, 1)) + goto out; + ctx->buf[ctx->idx++] = L'\0'; + } } size_t _len = wcstombs(NULL, ctx->buf, 0); diff --git a/input.c b/input.c index e4a1f306..d046bb4a 100644 --- a/input.c +++ b/input.c @@ -275,7 +275,8 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SELECT_BEGIN: if (selection_enabled(term, seat) && cursor_is_on_grid) { selection_start( - term, seat->mouse.col, seat->mouse.row, SELECTION_NORMAL); + term, seat->mouse.col, seat->mouse.row, + SELECTION_NORMAL, SELECTION_SEMANTIC_NONE, false); return true; } return false; @@ -283,7 +284,8 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SELECT_BEGIN_BLOCK: if (selection_enabled(term, seat) && cursor_is_on_grid) { selection_start( - term, seat->mouse.col, seat->mouse.row, SELECTION_BLOCK); + term, seat->mouse.col, seat->mouse.row, + SELECTION_BLOCK, SELECTION_SEMANTIC_NONE, false); return true; } return false; @@ -298,23 +300,26 @@ execute_binding(struct seat *seat, struct terminal *term, case BIND_ACTION_SELECT_WORD: if (selection_enabled(term, seat) && cursor_is_on_grid) { - selection_mark_word( - seat, term, seat->mouse.col, seat->mouse.row, false, serial); + selection_start( + term, seat->mouse.col, seat->mouse.row, + SELECTION_NORMAL, SELECTION_SEMANTIC_WORD, false); return true; } return false; case BIND_ACTION_SELECT_WORD_WS: if (selection_enabled(term, seat) && cursor_is_on_grid) { - selection_mark_word( - seat, term, seat->mouse.col, seat->mouse.row, true, serial); + selection_start( + term, seat->mouse.col, seat->mouse.row, + SELECTION_NORMAL, SELECTION_SEMANTIC_WORD, true); return true; } return false; case BIND_ACTION_SELECT_ROW: if (selection_enabled(term, seat) && cursor_is_on_grid) { - selection_mark_row(seat, term, seat->mouse.row, serial); + selection_start(term, seat->mouse.col, seat->mouse.row, + SELECTION_NORMAL, SELECTION_SEMANTIC_ROW, false); return true; } return false; diff --git a/search.c b/search.c index a7b6f112..c4fd8f05 100644 --- a/search.c +++ b/search.c @@ -222,7 +222,8 @@ search_update_selection(struct terminal *term, assert(selection_row >= 0 && selection_row < term->grid->num_rows); - selection_start(term, start_col, selection_row, SELECTION_NORMAL); + selection_start(term, start_col, selection_row, + SELECTION_NORMAL, SELECTION_SEMANTIC_NONE, false); } /* Update selection endpoint */ diff --git a/selection.c b/selection.c index db5a0b9f..f9642719 100644 --- a/selection.c +++ b/selection.c @@ -236,9 +236,157 @@ selection_to_text(const struct terminal *term) return extract_finish(ctx, &text, NULL) ? text : NULL; } +static void +find_word_boundary_left(struct terminal *term, struct coord *pos, + bool spaces_only) +{ + const struct row *r = grid_row_in_view(term->grid, pos->row); + wchar_t c = r->cells[pos->col].wc; + + while (c == CELL_MULT_COL_SPACER) { + assert(pos->col > 0); + if (pos->col == 0) + return; + pos->col--; + c = r->cells[pos->col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && + c < (CELL_COMB_CHARS_LO + term->composed_count)) + { + c = term->composed[c - CELL_COMB_CHARS_LO].base; + } + + bool initial_is_space = c == 0 || iswspace(c); + bool initial_is_delim = + !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool initial_is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + while (true) { + int next_col = pos->col - 1; + int next_row = pos->row; + + /* Linewrap */ + if (next_col < 0) { + next_col = term->cols - 1; + if (--next_row < 0) + break; + } + + const struct row *row = grid_row_in_view(term->grid, next_row); + + c = row->cells[next_col].wc; + while (c == CELL_MULT_COL_SPACER) { + assert(next_col > 0); + if (--next_col < 0) + return; + c = row->cells[next_col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && + c < (CELL_COMB_CHARS_LO + term->composed_count)) + { + c = term->composed[c - CELL_COMB_CHARS_LO].base; + } + + bool is_space = c == 0 || iswspace(c); + bool is_delim = + !is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + if (initial_is_space && !is_space) + break; + if (initial_is_delim && !is_delim) + break; + if (initial_is_word && !is_word) + break; + + pos->col = next_col; + pos->row = next_row; + } +} + +static void +find_word_boundary_right(struct terminal *term, struct coord *pos, + bool spaces_only) +{ + const struct row *r = grid_row_in_view(term->grid, pos->row); + wchar_t c = r->cells[pos->col].wc; + + while (c == CELL_MULT_COL_SPACER) { + assert(pos->col > 0); + if (pos->col == 0) + return; + pos->col--; + c = r->cells[pos->col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && + c < (CELL_COMB_CHARS_LO + term->composed_count)) + { + c = term->composed[c - CELL_COMB_CHARS_LO].base; + } + + bool initial_is_space = c == 0 || iswspace(c); + bool initial_is_delim = + !initial_is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool initial_is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + while (true) { + int next_col = pos->col + 1; + int next_row = pos->row; + + /* Linewrap */ + if (next_col >= term->cols) { + next_col = 0; + if (++next_row >= term->rows) + break; + } + + const struct row *row = grid_row_in_view(term->grid, next_row); + + c = row->cells[next_col].wc; + while (c == CELL_MULT_COL_SPACER) { + if (++next_col >= term->cols) { + next_col = 0; + if (++next_row >= term->rows) + return; + } + c = row->cells[next_col].wc; + } + + if (c >= CELL_COMB_CHARS_LO && + c < (CELL_COMB_CHARS_LO + term->composed_count)) + { + c = term->composed[c - CELL_COMB_CHARS_LO].base; + } + + bool is_space = c == 0 || iswspace(c); + bool is_delim = + !is_space && !isword(c, spaces_only, term->conf->word_delimiters); + bool is_word = + c != 0 && isword(c, spaces_only, term->conf->word_delimiters); + + if (initial_is_space && !is_space) + break; + if (initial_is_delim && !is_delim) + break; + if (initial_is_word && !is_word) + break; + + pos->col = next_col; + pos->row = next_row; + } +} + void selection_start(struct terminal *term, int col, int row, - enum selection_kind kind) + enum selection_kind kind, + enum selection_semantic semantic, + bool spaces_only) { selection_cancel(term); @@ -248,9 +396,43 @@ selection_start(struct terminal *term, int col, int row, row, col); term->selection.kind = kind; - term->selection.start = (struct coord){col, term->grid->view + row}; - term->selection.end = (struct coord){-1, -1}; + term->selection.semantic = semantic; term->selection.ongoing = true; + term->selection.spaces_only = spaces_only; + + switch (semantic) { + case SELECTION_SEMANTIC_NONE: + term->selection.start = (struct coord){col, term->grid->view + row}; + term->selection.end = (struct coord){-1, -1}; + + term->selection.pivot.start = term->selection.start; + term->selection.pivot.end = term->selection.end; + break; + + case SELECTION_SEMANTIC_WORD: { + struct coord start = {col, row}, end = {col, row}; + find_word_boundary_left(term, &start, spaces_only); + find_word_boundary_right(term, &end, spaces_only); + + term->selection.start = (struct coord){ + start.col, term->grid->view + start.row}; + + term->selection.pivot.start = term->selection.start; + term->selection.pivot.end = (struct coord){end.col, term->grid->view + end.row}; + + selection_update(term, end.col, end.row); + break; + } + + case SELECTION_SEMANTIC_ROW: + term->selection.start = (struct coord){0, term->grid->view + row}; + term->selection.pivot.start = term->selection.start; + term->selection.pivot.end = (struct coord){term->cols - 1, term->grid->view + row}; + + selection_update(term, term->cols - 1, row); + break; + } + } /* Context used while (un)marking selected cells, to be able to @@ -379,61 +561,150 @@ selection_update(struct terminal *term, int col, int row) struct coord new_start = term->selection.start; struct coord new_end = {col, term->grid->view + row}; - size_t start_row_idx = new_start.row & (term->grid->num_rows - 1); - size_t end_row_idx = new_end.row & (term->grid->num_rows - 1); - const struct row *row_start = term->grid->rows[start_row_idx]; - const struct row *row_end = term->grid->rows[end_row_idx]; - /* Adjust start point if the selection has changed 'direction' */ if (!(new_end.row == new_start.row && new_end.col == new_start.col)) { - enum selection_direction new_direction; + enum selection_direction new_direction = term->selection.direction; - if (new_end.row > new_start.row || - (new_end.row == new_start.row && new_end.col > new_start.col)) + struct coord *pivot_start = &term->selection.pivot.start; + struct coord *pivot_end = &term->selection.pivot.end; + + if (new_end.row < pivot_start->row || + (new_end.row == pivot_start->row && new_end.col < pivot_start->col)) { - /* New end point is after the start point */ - new_direction = SELECTION_RIGHT; - } else { - /* The new end point is before the start point */ + /* New end point is before the start point */ new_direction = SELECTION_LEFT; + } else { + /* The new end point is after the start point */ + new_direction = SELECTION_RIGHT; } if (term->selection.direction != new_direction) { - if (term->selection.direction != SELECTION_UNDIR) { - if (new_direction == SELECTION_LEFT) { + if (term->selection.direction == SELECTION_UNDIR && + pivot_end->row < 0) + { + /* First, make sure ‘start’ isn’t in the middle of a + * multi-column character */ + while (true) { + const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; + const struct cell *cell = &row->cells[pivot_start->col]; + + if (cell->wc != CELL_MULT_COL_SPACER) + break; + + /* Multi-column chars don’t cross rows */ + assert(pivot_start->col > 0); + if (pivot_start->col == 0) + break; + + pivot_start->col--; + } + + /* + * Setup pivot end to be one character *before* start + * Which one we move, the end or start point, depends + * on the initial selection direction. + */ + + *pivot_end = *pivot_start; + + if (new_direction == SELECTION_RIGHT) { bool keep_going = true; while (keep_going) { - const wchar_t wc = row_start->cells[new_start.col].wc; + const struct row *row = term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]; + const wchar_t wc = row->cells[pivot_end->col].wc; + keep_going = wc == CELL_MULT_COL_SPACER; - new_start.col--; - if (new_start.col < 0) { - new_start.col = term->cols - 1; - new_start.row--; - } + if (pivot_end->col == 0) { + if (pivot_end->row - term->grid->view <= 0) + break; + pivot_end->col = term->cols - 1; + pivot_end->row--; + } else + pivot_end->col--; } } else { bool keep_going = true; while (keep_going) { - const wchar_t wc = new_start.col < term->cols - 1 - ? row_start->cells[new_start.col + 1].wc - : 0; + const struct row *row = term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]; + const wchar_t wc = pivot_start->col < term->cols - 1 + ? row->cells[pivot_start->col + 1].wc : 0; keep_going = wc == CELL_MULT_COL_SPACER; - new_start.col++; - if (new_start.col >= term->cols) { - new_start.col = 0; - new_start.row++; - } + if (pivot_start->col >= term->cols - 1) { + if (pivot_start->row - term->grid->view >= term->rows - 1) + break; + pivot_start->col = 0; + pivot_start->row++; + } else + pivot_start->col++; } } + + assert(term->grid->rows[pivot_start->row & (term->grid->num_rows - 1)]-> + cells[pivot_start->col].wc != CELL_MULT_COL_SPACER); + assert(term->grid->rows[pivot_end->row & (term->grid->num_rows - 1)]-> + cells[pivot_end->col].wc != CELL_MULT_COL_SPACER); } + if (new_direction == SELECTION_LEFT) { + assert(pivot_end->row >= 0); + new_start = *pivot_end; + } else + new_start = *pivot_start; + term->selection.direction = new_direction; } } + switch (term->selection.semantic) { + case SELECTION_SEMANTIC_NONE: + break; + + case SELECTION_SEMANTIC_WORD: + switch (term->selection.direction) { + case SELECTION_LEFT: { + struct coord end = {col, row}; + find_word_boundary_left(term, &end, term->selection.spaces_only); + new_end = (struct coord){end.col, term->grid->view + end.row}; + break; + } + + case SELECTION_RIGHT: { + struct coord end = {col, row}; + find_word_boundary_right(term, &end, term->selection.spaces_only); + new_end = (struct coord){end.col, term->grid->view + end.row}; + break; + } + + case SELECTION_UNDIR: + break; + } + break; + + case SELECTION_SEMANTIC_ROW: + switch (term->selection.direction) { + case SELECTION_LEFT: + new_end.col = 0; + break; + + case SELECTION_RIGHT: + new_end.col = term->cols - 1; + break; + + case SELECTION_UNDIR: + break; + } + break; + } + + size_t start_row_idx = new_start.row & (term->grid->num_rows - 1); + size_t end_row_idx = new_end.row & (term->grid->num_rows - 1); + + const struct row *row_start = term->grid->rows[start_row_idx]; + const struct row *row_end = term->grid->rows[end_row_idx]; + /* If an end point is in the middle of a multi-column character, * expand the selection to cover the entire character */ if (new_start.row < new_end.row || @@ -677,6 +948,8 @@ selection_cancel(struct terminal *term) term->selection.kind = SELECTION_NONE; term->selection.start = (struct coord){-1, -1}; term->selection.end = (struct coord){-1, -1}; + term->selection.pivot.start = (struct coord){-1, -1}; + term->selection.pivot.end = (struct coord){-1, -1}; term->selection.direction = SELECTION_UNDIR; term->selection.ongoing = false; } @@ -733,81 +1006,6 @@ selection_primary_unset(struct seat *seat) primary->text = NULL; } -void -selection_mark_word(struct seat *seat, struct terminal *term, int col, int row, - bool spaces_only, uint32_t serial) -{ - selection_cancel(term); - - struct coord start = {col, row}; - struct coord end = {col, row}; - - const struct row *r = grid_row_in_view(term->grid, start.row); - wchar_t c = r->cells[start.col].wc; - - if (!(c == 0 || !isword(c, spaces_only, term->conf->word_delimiters))) { - while (true) { - int next_col = start.col - 1; - int next_row = start.row; - - /* Linewrap */ - if (next_col < 0) { - next_col = term->cols - 1; - if (--next_row < 0) - break; - } - - const struct row *row = grid_row_in_view(term->grid, next_row); - - c = row->cells[next_col].wc; - if (c == 0 || !isword(c, spaces_only, term->conf->word_delimiters)) - break; - - start.col = next_col; - start.row = next_row; - } - } - - r = grid_row_in_view(term->grid, end.row); - c = r->cells[end.col].wc; - - if (!(c == 0 || !isword(c, spaces_only, term->conf->word_delimiters))) { - while (true) { - int next_col = end.col + 1; - int next_row = end.row; - - /* Linewrap */ - if (next_col >= term->cols) { - next_col = 0; - if (++next_row >= term->rows) - break; - } - - const struct row *row = grid_row_in_view(term->grid, next_row); - - c = row->cells[next_col].wc; - if (c == '\0' || !isword(c, spaces_only, term->conf->word_delimiters)) - break; - - end.col = next_col; - end.row = next_row; - } - } - - selection_start(term, start.col, start.row, SELECTION_NORMAL); - selection_update(term, end.col, end.row); - selection_finalize(seat, term, serial); -} - -void -selection_mark_row( - struct seat *seat, struct terminal *term, int row, uint32_t serial) -{ - selection_start(term, 0, row, SELECTION_NORMAL); - selection_update(term, term->cols - 1, row); - selection_finalize(seat, term, serial); -} - static bool fdm_scroll_timer(struct fdm *fdm, int fd, int events, void *data) { diff --git a/selection.h b/selection.h index 1b74efb5..21e983e8 100644 --- a/selection.h +++ b/selection.h @@ -10,7 +10,9 @@ extern const struct zwp_primary_selection_device_v1_listener primary_selection_d bool selection_enabled(const struct terminal *term, struct seat *seat); void selection_start( - struct terminal *term, int col, int row, enum selection_kind kind); + struct terminal *term, int col, int row, + enum selection_kind kind, enum selection_semantic semantic, + bool spaces_only); void selection_update(struct terminal *term, int col, int row); void selection_finalize( struct seat *seat, struct terminal *term, uint32_t serial); @@ -24,12 +26,6 @@ bool selection_on_rows(const struct terminal *term, int start, int end); void selection_view_up(struct terminal *term, int new_view); void selection_view_down(struct terminal *term, int new_view); -void selection_mark_word( - struct seat *seat, struct terminal *term, int col, int row, - bool spaces_only, uint32_t serial); -void selection_mark_row( - struct seat *seat, struct terminal *term, int row, uint32_t serial); - void selection_clipboard_unset(struct seat *seat); void selection_primary_unset(struct seat *seat); diff --git a/terminal.h b/terminal.h index 0774aaa1..a8bd5b0b 100644 --- a/terminal.h +++ b/terminal.h @@ -181,6 +181,7 @@ enum mouse_reporting { enum cursor_style { CURSOR_BLOCK, CURSOR_UNDERLINE, CURSOR_BAR }; enum selection_kind { SELECTION_NONE, SELECTION_NORMAL, SELECTION_BLOCK }; +enum selection_semantic { SELECTION_SEMANTIC_NONE, SELECTION_SEMANTIC_WORD, SELECTION_SEMANTIC_ROW}; enum selection_direction {SELECTION_UNDIR, SELECTION_LEFT, SELECTION_RIGHT}; enum selection_scroll_direction {SELECTION_SCROLL_NOT, SELECTION_SCROLL_UP, SELECTION_SCROLL_DOWN}; @@ -359,10 +360,17 @@ struct terminal { struct { enum selection_kind kind; + enum selection_semantic semantic; enum selection_direction direction; struct coord start; struct coord end; bool ongoing; + bool spaces_only; /* SELECTION_SEMANTIC_WORD */ + + struct { + struct coord start; + struct coord end; + } pivot; struct { int fd;