This commit is contained in:
lbia.xyz 2022-10-04 02:14:53 +02:00
commit fd187cc491
No known key found for this signature in database
GPG key ID: 6774C7B38E986DF1
50 changed files with 1530 additions and 481 deletions

View file

@ -9,6 +9,8 @@
#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <pixman.h>
#define LOG_MODULE "selection"
#define LOG_ENABLE_DBG 0
#include "log.h"
@ -64,41 +66,85 @@ selection_get_end(const struct terminal *term)
bool
selection_on_rows(const struct terminal *term, int row_start, int row_end)
{
xassert(term->selection.coords.end.row >= 0);
LOG_DBG("on rows: %d-%d, range: %d-%d (offset=%d)",
term->selection.coords.start.row, term->selection.coords.end.row,
row_start, row_end, term->grid->offset);
if (term->selection.coords.end.row < 0)
return false;
xassert(term->selection.coords.start.row != -1);
row_start += term->grid->offset;
row_end += term->grid->offset;
xassert(row_end >= row_start);
const struct coord *start = &term->selection.coords.start;
const struct coord *end = &term->selection.coords.end;
if ((row_start <= start->row && row_end >= start->row) ||
(row_start <= end->row && row_end >= end->row))
const struct grid *grid = term->grid;
const int sb_start = grid->offset + term->rows;
/* Use scrollback relative coords when checking for overlap */
const int rel_row_start =
grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start);
const int rel_row_end =
grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, row_start);
int rel_sel_start =
grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, start->row);
int rel_sel_end =
grid_row_abs_to_sb_precalc_sb_start(grid, sb_start, end->row);
if (rel_sel_start > rel_sel_end) {
int tmp = rel_sel_start;
rel_sel_start = rel_sel_end;
rel_sel_end = tmp;
}
if ((rel_row_start <= rel_sel_start && rel_row_end >= rel_sel_start) ||
(rel_row_start <= rel_sel_end && rel_row_end >= rel_sel_end))
{
/* The range crosses one of the selection boundaries */
return true;
}
/* For the last check we must ensure start <= end */
if (start->row > end->row) {
const struct coord *tmp = start;
start = end;
end = tmp;
}
if (row_start >= start->row && row_end <= end->row)
if (rel_row_start >= rel_sel_start && rel_row_end <= rel_sel_end)
return true;
return false;
}
void
selection_scroll_up(struct terminal *term, int rows)
{
xassert(term->selection.coords.end.row >= 0);
const int rel_row_start =
grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row);
const int rel_row_end =
grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row);
const int actual_start = min(rel_row_start, rel_row_end);
if (actual_start - rows < 0) {
/* Part of the selection will be scrolled out, cancel it */
selection_cancel(term);
}
}
void
selection_scroll_down(struct terminal *term, int rows)
{
xassert(term->selection.coords.end.row >= 0);
const int rel_row_start =
grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.start.row);
const int rel_row_end =
grid_row_abs_to_sb(term->grid, term->rows, term->selection.coords.end.row);
const int actual_end = max(rel_row_start, rel_row_end);
if (actual_end + rows <= term->grid->num_rows) {
/* Part of the selection will be scrolled out, cancel it */
selection_cancel(term);
}
}
void
selection_view_up(struct terminal *term, int new_view)
{
@ -137,14 +183,14 @@ foreach_selected_normal(
const struct coord *start = &_start;
const struct coord *end = &_end;
const int scrollback_start = term->grid->offset + term->rows;
const int grid_rows = term->grid->num_rows;
/* Start/end rows, relative to the scrollback start */
/* Start/end rows, relative to the scrollback start */
const int rel_start_row =
(start->row - scrollback_start + grid_rows) & (grid_rows - 1);
grid_row_abs_to_sb(term->grid, term->rows, start->row);
const int rel_end_row =
(end->row - scrollback_start + grid_rows) & (grid_rows - 1);
grid_row_abs_to_sb(term->grid, term->rows, end->row);
int start_row, end_row;
int start_col, end_col;
@ -200,14 +246,13 @@ foreach_selected_block(
const struct coord *start = &_start;
const struct coord *end = &_end;
const int scrollback_start = term->grid->offset + term->rows;
const int grid_rows = term->grid->num_rows;
/* Start/end rows, relative to the scrollback start */
const int rel_start_row =
(start->row - scrollback_start + grid_rows) & (grid_rows - 1);
grid_row_abs_to_sb(term->grid, term->rows, start->row);
const int rel_end_row =
(end->row - scrollback_start + grid_rows) & (grid_rows - 1);
grid_row_abs_to_sb(term->grid, term->rows, end->row);
struct coord top_left = {
.row = (rel_start_row < rel_end_row
@ -564,111 +609,216 @@ selection_start(struct terminal *term, int col, int row,
}
/* Context used while (un)marking selected cells, to be able to
* exclude empty cells */
struct mark_context {
const struct row *last_row;
int empty_count;
uint8_t **keep_selection;
static pixman_region32_t
pixman_region_for_coords_normal(const struct terminal *term,
const struct coord *start,
const struct coord *end)
{
pixman_region32_t region;
pixman_region32_init(&region);
const int rel_start_row =
grid_row_abs_to_sb(term->grid, term->rows, start->row);
const int rel_end_row =
grid_row_abs_to_sb(term->grid, term->rows, end->row);
if (rel_start_row < rel_end_row) {
/* First partial row (start ->)*/
pixman_region32_union_rect(
&region, &region,
start->col, rel_start_row,
term->cols - start->col, 1);
/* Full rows between start and end */
if (rel_start_row + 1 < rel_end_row) {
pixman_region32_union_rect(
&region, &region,
0, rel_start_row + 1,
term->cols, rel_end_row - rel_start_row - 1);
}
/* Last partial row (-> end) */
pixman_region32_union_rect(
&region, &region,
0, rel_end_row,
end->col + 1, 1);
} else if (rel_start_row > rel_end_row) {
/* First partial row (end ->) */
pixman_region32_union_rect(
&region, &region,
end->col, rel_end_row,
term->cols - end->col, 1);
/* Full rows between end and start */
if (rel_end_row + 1 < rel_start_row) {
pixman_region32_union_rect(
&region, &region,
0, rel_end_row + 1,
term->cols, rel_start_row - rel_end_row - 1);
}
/* Last partial row (-> start) */
pixman_region32_union_rect(
&region, &region,
0, rel_start_row,
start->col + 1, 1);
} else {
const int start_col = min(start->col, end->col);
const int end_col = max(start->col, end->col);
pixman_region32_union_rect(
&region, &region,
start_col, rel_start_row,
end_col + 1 - start_col, 1);
}
return region;
}
static pixman_region32_t
pixman_region_for_coords_block(const struct terminal *term,
const struct coord *start, const struct coord *end)
{
pixman_region32_t region;
pixman_region32_init(&region);
const int rel_start_row =
grid_row_abs_to_sb(term->grid, term->rows, start->row);
const int rel_end_row =
grid_row_abs_to_sb(term->grid, term->rows, end->row);
pixman_region32_union_rect(
&region, &region,
min(start->col, end->col), min(rel_start_row, rel_end_row),
abs(start->col - end->col) + 1, abs(rel_start_row - rel_end_row) + 1);
return region;
}
/* Returns a pixman region representing the selection between start
* and end (given the current selection kind), in *scrollback
* relative coordinates* */
static pixman_region32_t
pixman_region_for_coords(const struct terminal *term,
const struct coord *start, const struct coord *end)
{
switch (term->selection.kind) {
default: return pixman_region_for_coords_normal(term, start, end);
case SELECTION_BLOCK: return pixman_region_for_coords_block(term, start, end);
}
}
enum mark_selection_variant {
MARK_SELECTION_MARK_AND_DIRTY,
MARK_SELECTION_UNMARK_AND_DIRTY,
MARK_SELECTION_MARK_FOR_RENDER,
};
static bool
unmark_selected(struct terminal *term, struct row *row, struct cell *cell,
int row_no, int col, void *data)
{
if (!cell->attrs.selected)
return true;
struct mark_context *ctx = data;
const uint8_t *keep_selection =
ctx->keep_selection != NULL ? ctx->keep_selection[row_no] : NULL;
if (keep_selection != NULL) {
unsigned idx = (unsigned)col / 8;
unsigned ofs = (unsigned)col % 8;
if (keep_selection[idx] & (1 << ofs)) {
/* Were updating the selection, and this cell is still
* going to be selected */
return true;
}
}
row->dirty = true;
cell->attrs.selected = false;
cell->attrs.clean = false;
return true;
}
static bool
premark_selected(struct terminal *term, struct row *row, struct cell *cell,
int row_no, int col, void *data)
{
struct mark_context *ctx = data;
xassert(ctx != NULL);
if (ctx->last_row != row) {
ctx->last_row = row;
ctx->empty_count = 0;
}
if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) {
ctx->empty_count++;
return true;
}
uint8_t *keep_selection = ctx->keep_selection[row_no];
if (keep_selection == NULL) {
keep_selection = xcalloc((term->grid->num_cols + 7) / 8, sizeof(keep_selection[0]));
ctx->keep_selection[row_no] = keep_selection;
}
/* Tell unmark to leave this be */
for (int i = 0; i < ctx->empty_count + 1; i++) {
unsigned idx = (unsigned)(col - i) / 8;
unsigned ofs = (unsigned)(col - i) % 8;
keep_selection[idx] |= 1 << ofs;
}
ctx->empty_count = 0;
return true;
}
static bool
mark_selected(struct terminal *term, struct row *row, struct cell *cell,
int row_no, int col, void *data)
{
struct mark_context *ctx = data;
xassert(ctx != NULL);
if (ctx->last_row != row) {
ctx->last_row = row;
ctx->empty_count = 0;
}
if (cell->wc == 0 && term->selection.kind != SELECTION_BLOCK) {
ctx->empty_count++;
return true;
}
for (int i = 0; i < ctx->empty_count + 1; i++) {
struct cell *c = &row->cells[col - i];
if (!c->attrs.selected) {
row->dirty = true;
c->attrs.selected = true;
c->attrs.clean = false;
}
}
ctx->empty_count = 0;
return true;
}
static void
reset_modify_context(struct mark_context *ctx)
mark_selected_region(struct terminal *term, pixman_box32_t *boxes,
size_t count, enum mark_selection_variant mark_variant)
{
ctx->last_row = NULL;
ctx->empty_count = 0;
const bool selected =
mark_variant == MARK_SELECTION_MARK_AND_DIRTY ||
mark_variant == MARK_SELECTION_MARK_FOR_RENDER;
const bool dirty_cells =
mark_variant == MARK_SELECTION_MARK_AND_DIRTY ||
mark_variant == MARK_SELECTION_UNMARK_AND_DIRTY;
const bool highlight_empty =
mark_variant != MARK_SELECTION_MARK_FOR_RENDER ||
term->selection.kind == SELECTION_BLOCK;
for (size_t i = 0; i < count; i++) {
const pixman_box32_t *box = &boxes[i];
LOG_DBG("%s selection in region: %dx%d - %dx%d",
selected ? "marking" : "unmarking",
box->x1, box->y1,
box->x2, box->y2);
int abs_row_start = grid_row_sb_to_abs(
term->grid, term->rows, box->y1);
for (int r = abs_row_start, rel_r = box->y1;
rel_r < box->y2;
r = (r + 1) & (term->grid->num_rows - 1), rel_r++)
{
struct row *row = term->grid->rows[r];
xassert(row != NULL);
if (dirty_cells)
row->dirty = true;
for (int c = box->x1, empty_count = 0; c < box->x2; c++) {
struct cell *cell = &row->cells[c];
if (cell->wc == 0 && !highlight_empty) {
/*
* We used to highlight empty cells *if* they were
* followed by non-empty cell(s), since this
* corresponds to what gets extracted when the
* selection is copied (that is, empty cells
* between non-empty cells are converted to
* spaces).
*
* However, they way we handle selection updates
* (diffing the old selection area against the
* new one, using pixman regions), means we
* cant correctly update the state of empty
* cells. The result is random empty cells being
* rendered as selected when they shouldnt.
*
* Fix by *never* highlighting selected empty
* cells (they still get converted to spaces when
* copied, if followed by non-empty cells).
*/
empty_count++;
/*
* When the selection is *modified*, empty cells
* are treated just like non-empty cells; they are
* marked as selected, and dirtied.
*
* This is due to how the algorithm for updating
* the selection works; it uses regions to
* calculate the difference between the old and
* the new selection. This makes it impossible
* to tell if an empty cell is a *trailing* empty
* cell (that should not be highlighted), or an
* empty cells between non-empty cells (that
* *should* be highlighted).
*
* Then, when a frame is rendered, we loop the
* *visibible* cells that belong to the
* selection. At this point, we *can* tell if an
* empty cell is trailing or not.
*
* So, what we need to do is check if a
* selected, and empty cell has been marked as
* selected, temporarily unmark (forcing it dirty,
* to ensure it gets re-rendered). If it is *not*
* a trailing empty cell, it will get re-tagged as
* selected in the for-loop below.
*/
cell->attrs.clean = false;
cell->attrs.selected = false;
continue;
}
for (int j = 0; j < empty_count + 1; j++) {
xassert(c - j >= 0);
struct cell *cell = &row->cells[c - j];
if (dirty_cells)
cell->attrs.clean = false;
cell->attrs.selected = selected;
}
empty_count = 0;
}
}
}
}
static void
@ -678,33 +828,46 @@ selection_modify(struct terminal *term, struct coord start, struct coord end)
xassert(start.row != -1 && start.col != -1);
xassert(end.row != -1 && end.col != -1);
uint8_t **keep_selection =
xcalloc(term->grid->num_rows, sizeof(keep_selection[0]));
struct mark_context ctx = {.keep_selection = keep_selection};
/* Premark all cells that *will* be selected */
foreach_selected(term, start, end, &premark_selected, &ctx);
reset_modify_context(&ctx);
pixman_region32_t previous_selection;
if (term->selection.coords.end.row >= 0) {
/* Unmark previous selection, ignoring cells that are part of
* the new selection */
foreach_selected(term, term->selection.coords.start, term->selection.coords.end,
&unmark_selected, &ctx);
reset_modify_context(&ctx);
}
previous_selection = pixman_region_for_coords(
term,
&term->selection.coords.start,
&term->selection.coords.end);
} else
pixman_region32_init(&previous_selection);
pixman_region32_t current_selection = pixman_region_for_coords(
term, &start, &end);
pixman_region32_t no_longer_selected;
pixman_region32_init(&no_longer_selected);
pixman_region32_subtract(
&no_longer_selected, &previous_selection, &current_selection);
pixman_region32_t newly_selected;
pixman_region32_init(&newly_selected);
pixman_region32_subtract(
&newly_selected, &current_selection, &previous_selection);
/* Clear selection in cells no longer selected */
int n_rects = -1;
pixman_box32_t *boxes = NULL;
boxes = pixman_region32_rectangles(&no_longer_selected, &n_rects);
mark_selected_region(term, boxes, n_rects, MARK_SELECTION_UNMARK_AND_DIRTY);
boxes = pixman_region32_rectangles(&newly_selected, &n_rects);
mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_AND_DIRTY);
pixman_region32_fini(&newly_selected);
pixman_region32_fini(&no_longer_selected);
pixman_region32_fini(&current_selection);
pixman_region32_fini(&previous_selection);
term->selection.coords.start = start;
term->selection.coords.end = end;
/* Mark new selection */
foreach_selected(term, start, end, &mark_selected, &ctx);
render_refresh(term);
for (size_t i = 0; i < term->grid->num_rows; i++)
free(keep_selection[i]);
free(keep_selection);
}
static void
@ -945,9 +1108,26 @@ selection_dirty_cells(struct terminal *term)
if (term->selection.coords.start.row < 0 || term->selection.coords.end.row < 0)
return;
foreach_selected(
term, term->selection.coords.start, term->selection.coords.end, &mark_selected,
&(struct mark_context){0});
pixman_region32_t selection = pixman_region_for_coords(
term, &term->selection.coords.start, &term->selection.coords.end);
pixman_region32_t view = pixman_region_for_coords(
term,
&(struct coord){0, term->grid->view},
&(struct coord){term->cols - 1, term->grid->view + term->rows - 1});
pixman_region32_t visible_and_selected;
pixman_region32_init(&visible_and_selected);
pixman_region32_intersect(&visible_and_selected, &selection, &view);
int n_rects = -1;
pixman_box32_t *boxes =
pixman_region32_rectangles(&visible_and_selected, &n_rects);
mark_selected_region(term, boxes, n_rects, MARK_SELECTION_MARK_FOR_RENDER);
pixman_region32_fini(&visible_and_selected);
pixman_region32_fini(&view);
pixman_region32_fini(&selection);
}
static void
@ -957,27 +1137,37 @@ selection_extend_normal(struct terminal *term, int col, int row,
const struct coord *start = &term->selection.coords.start;
const struct coord *end = &term->selection.coords.end;
if (start->row > end->row ||
(start->row == end->row && start->col > end->col))
const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row);
int rel_start_row = grid_row_abs_to_sb(term->grid, term->rows, start->row);
int rel_end_row = grid_row_abs_to_sb(term->grid, term->rows, end->row);
if (rel_start_row > rel_end_row ||
(rel_start_row == rel_end_row && start->col > end->col))
{
const struct coord *tmp = start;
start = end;
end = tmp;
}
xassert(start->row < end->row || start->col < end->col);
int tmp_row = rel_start_row;
rel_start_row = rel_end_row;
rel_end_row = tmp_row;
}
struct coord new_start, new_end;
enum selection_direction direction;
if (row < start->row || (row == start->row && col < start->col)) {
if (rel_row < rel_start_row ||
(rel_row == rel_start_row && col < start->col))
{
/* Extend selection to start *before* current start */
new_start = *end;
new_end = (struct coord){col, row};
direction = SELECTION_LEFT;
}
else if (row > end->row || (row == end->row && col > end->col)) {
else if (rel_row > rel_end_row ||
(rel_row == rel_end_row && col > end->col))
{
/* Extend selection to end *after* current end */
new_start = *start;
new_end = (struct coord){col, row};
@ -987,10 +1177,10 @@ selection_extend_normal(struct terminal *term, int col, int row,
else {
/* Shrink selection from start or end, depending on which one is closest */
const int linear = row * term->cols + col;
const int linear = rel_row * term->cols + col;
if (abs(linear - (start->row * term->cols + start->col)) <
abs(linear - (end->row * term->cols + end->col)))
if (abs(linear - (rel_start_row * term->cols + start->col)) <
abs(linear - (rel_end_row * term->cols + end->col)))
{
/* Move start point */
new_start = *end;
@ -1065,33 +1255,41 @@ selection_extend_block(struct terminal *term, int col, int row)
const struct coord *start = &term->selection.coords.start;
const struct coord *end = &term->selection.coords.end;
const int rel_start_row =
grid_row_abs_to_sb(term->grid, term->rows, start->row);
const int rel_end_row =
grid_row_abs_to_sb(term->grid, term->rows, end->row);
struct coord top_left = {
.row = min(start->row, end->row),
.row = rel_start_row < rel_end_row ? start->row : end->row,
.col = min(start->col, end->col),
};
struct coord top_right = {
.row = min(start->row, end->row),
.row = top_left.row,
.col = max(start->col, end->col),
};
struct coord bottom_left = {
.row = max(start->row, end->row),
.row = rel_start_row > rel_end_row ? start->row : end->row,
.col = min(start->col, end->col),
};
struct coord bottom_right = {
.row = max(start->row, end->row),
.row = bottom_left.row,
.col = max(start->col, end->col),
};
const int rel_row = grid_row_abs_to_sb(term->grid, term->rows, row);
const int rel_top_row = grid_row_abs_to_sb(term->grid, term->rows, top_left.row);
const int rel_bottom_row = grid_row_abs_to_sb(term->grid, term->rows, bottom_left.row);
struct coord new_start;
struct coord new_end;
enum selection_direction direction = SELECTION_UNDIR;
if (row <= top_left.row ||
abs(row - top_left.row) < abs(row - bottom_left.row))
if (rel_row <= rel_top_row ||
abs(rel_row - rel_top_row) < abs(rel_row - rel_bottom_row))
{
/* Move one of the top corners */
@ -1207,6 +1405,19 @@ selection_finalize(struct seat *seat, struct terminal *term, uint32_t serial)
}
}
static bool
unmark_selected(struct terminal *term, struct row *row, struct cell *cell,
int row_no, int col, void *data)
{
if (!cell->attrs.selected)
return true;
row->dirty = true;
cell->attrs.selected = false;
cell->attrs.clean = false;
return true;
}
void
selection_cancel(struct terminal *term)
{
@ -1219,7 +1430,7 @@ selection_cancel(struct terminal *term)
if (term->selection.coords.start.row >= 0 && term->selection.coords.end.row >= 0) {
foreach_selected(
term, term->selection.coords.start, term->selection.coords.end,
&unmark_selected, &(struct mark_context){0});
&unmark_selected, NULL);
render_refresh(term);
}