mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-04 04:06:06 -05:00
This way, all lines are treated as having a hard linebreak, until it's cleared when we do an auto-wrap. This change alone causes issues when reflowing text, as now all trailing lines in an otherwise empty window are treated as hard linebreaks, causing the new grid to insert lots of unwanted, empty lines. Fix by doing two things: * *clear* the linebreak flag when we pull in new lines for the new grid. We only want to set it explicitly, when an old row has its linebreak flag set. * Coalesce empty lines with linebreak=true, and only "emit" them as new liens in the new grid if they are followed by non-empty lines.
1676 lines
54 KiB
C
1676 lines
54 KiB
C
#include "grid.h"
|
|
|
|
#include <limits.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#define LOG_MODULE "grid"
|
|
#define LOG_ENABLE_DBG 0
|
|
#include "log.h"
|
|
#include "debug.h"
|
|
#include "macros.h"
|
|
#include "sixel.h"
|
|
#include "stride.h"
|
|
#include "util.h"
|
|
#include "xmalloc.h"
|
|
|
|
#define TIME_REFLOW 0
|
|
|
|
#if defined(TIME_REFLOW)
|
|
#include "misc.h"
|
|
#endif
|
|
|
|
/*
|
|
* "sb" (scrollback relative) coordinates
|
|
*
|
|
* The scrollback relative row number 0 is the *first*, and *oldest*
|
|
* row in the scrollback history (and thus the *first* row to be
|
|
* scrolled out). Thus, a higher number means further *down* in the
|
|
* scrollback, with the *highest* number being at the bottom of the
|
|
* screen, where new input appears.
|
|
*/
|
|
|
|
int
|
|
grid_row_abs_to_sb(const struct grid *grid, int screen_rows, int abs_row)
|
|
{
|
|
const int scrollback_start = grid->offset + screen_rows;
|
|
int rebased_row = abs_row - scrollback_start + grid->num_rows;
|
|
|
|
rebased_row &= grid->num_rows - 1;
|
|
return rebased_row;
|
|
}
|
|
|
|
int
|
|
grid_row_sb_to_abs(const struct grid *grid, int screen_rows, int sb_rel_row)
|
|
{
|
|
const int scrollback_start = grid->offset + screen_rows;
|
|
int abs_row = sb_rel_row + scrollback_start;
|
|
|
|
abs_row &= grid->num_rows - 1;
|
|
return abs_row;
|
|
}
|
|
|
|
int
|
|
grid_sb_start_ignore_uninitialized(const struct grid *grid, int screen_rows)
|
|
{
|
|
int scrollback_start = grid->offset + screen_rows;
|
|
scrollback_start &= grid->num_rows - 1;
|
|
|
|
while (grid->rows[scrollback_start] == NULL) {
|
|
scrollback_start++;
|
|
scrollback_start &= grid->num_rows - 1;
|
|
}
|
|
|
|
return scrollback_start;
|
|
}
|
|
|
|
int
|
|
grid_row_abs_to_sb_precalc_sb_start(const struct grid *grid, int sb_start,
|
|
int abs_row)
|
|
{
|
|
int rebased_row = abs_row - sb_start + grid->num_rows;
|
|
rebased_row &= grid->num_rows - 1;
|
|
return rebased_row;
|
|
}
|
|
|
|
int
|
|
grid_row_sb_to_abs_precalc_sb_start(const struct grid *grid, int sb_start,
|
|
int sb_rel_row)
|
|
{
|
|
int abs_row = sb_rel_row + sb_start;
|
|
abs_row &= grid->num_rows - 1;
|
|
return abs_row;
|
|
}
|
|
|
|
static void
|
|
ensure_row_has_extra_data(struct row *row)
|
|
{
|
|
if (row->extra == NULL)
|
|
row->extra = xcalloc(1, sizeof(*row->extra));
|
|
}
|
|
|
|
static void
|
|
verify_no_overlapping_ranges_of_type(const struct row_ranges *ranges,
|
|
enum row_range_type type)
|
|
{
|
|
#if defined(_DEBUG)
|
|
for (size_t i = 0; i < ranges->count; i++) {
|
|
const struct row_range *r1 = &ranges->v[i];
|
|
|
|
for (size_t j = i + 1; j < ranges->count; j++) {
|
|
const struct row_range *r2 = &ranges->v[j];
|
|
xassert(r1 != r2);
|
|
|
|
if ((r1->start <= r2->start && r1->end >= r2->start) ||
|
|
(r1->start <= r2->end && r1->end >= r2->end))
|
|
{
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
BUG("OSC-8 URI overlap: %s: %d-%d: %s: %d-%d",
|
|
r1->uri.uri, r1->start, r1->end,
|
|
r2->uri.uri, r2->start, r2->end);
|
|
break;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
BUG("underline overlap: %d-%d, %d-%d",
|
|
r1->start, r1->end, r2->start, r2->end);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
verify_no_overlapping_ranges(const struct row_data *extra)
|
|
{
|
|
verify_no_overlapping_ranges_of_type(&extra->uri_ranges, ROW_RANGE_URI);
|
|
verify_no_overlapping_ranges_of_type(&extra->underline_ranges, ROW_RANGE_UNDERLINE);
|
|
}
|
|
|
|
static void
|
|
verify_ranges_of_type_are_sorted(const struct row_ranges *ranges,
|
|
enum row_range_type type)
|
|
{
|
|
#if defined(_DEBUG)
|
|
const struct row_range *last = NULL;
|
|
|
|
for (size_t i = 0; i < ranges->count; i++) {
|
|
const struct row_range *r = &ranges->v[i];
|
|
|
|
if (last != NULL) {
|
|
if (last->start >= r->start || last->end >= r->end) {
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
BUG("OSC-8 URI not sorted correctly: "
|
|
"%s: %d-%d came before %s: %d-%d",
|
|
last->uri.uri, last->start, last->end,
|
|
r->uri.uri, r->start, r->end);
|
|
break;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
BUG("underline ranges not sorted correctly: "
|
|
"%d-%d came before %d-%d",
|
|
last->start, last->end, r->start, r->end);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
last = r;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
verify_ranges_are_sorted(const struct row_data *extra)
|
|
{
|
|
verify_ranges_of_type_are_sorted(&extra->uri_ranges, ROW_RANGE_URI);
|
|
verify_ranges_of_type_are_sorted(&extra->underline_ranges, ROW_RANGE_UNDERLINE);
|
|
}
|
|
|
|
static void
|
|
range_ensure_size(struct row_ranges *ranges, int count_to_add)
|
|
{
|
|
if (ranges->count + count_to_add > ranges->size) {
|
|
ranges->size = ranges->count + count_to_add;
|
|
ranges->v = xrealloc(ranges->v, ranges->size * sizeof(ranges->v[0]));
|
|
}
|
|
|
|
xassert(ranges->count + count_to_add <= ranges->size);
|
|
}
|
|
|
|
/*
|
|
* Be careful! This function may xrealloc() the URI range vector, thus
|
|
* invalidating pointers into it.
|
|
*/
|
|
static void
|
|
range_insert(struct row_ranges *ranges, size_t idx, int start, int end,
|
|
enum row_range_type type, const union row_range_data *data)
|
|
{
|
|
range_ensure_size(ranges, 1);
|
|
|
|
xassert(idx <= ranges->count);
|
|
|
|
const size_t move_count = ranges->count - idx;
|
|
memmove(&ranges->v[idx + 1],
|
|
&ranges->v[idx],
|
|
move_count * sizeof(ranges->v[0]));
|
|
|
|
ranges->count++;
|
|
|
|
struct row_range *r = &ranges->v[idx];
|
|
r->start = start;
|
|
r->end = end;
|
|
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
r->uri.id = data->uri.id;
|
|
r->uri.uri = xstrdup(data->uri.uri);
|
|
break;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
r->underline = data->underline;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
range_append_by_ref(struct row_ranges *ranges, int start, int end,
|
|
enum row_range_type type, const union row_range_data *data)
|
|
{
|
|
range_ensure_size(ranges, 1);
|
|
|
|
struct row_range *r = &ranges->v[ranges->count++];
|
|
|
|
r->start = start;
|
|
r->end = end;
|
|
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
r->uri.id = data->uri.id;;
|
|
r->uri.uri = data->uri.uri;
|
|
break;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
r->underline = data->underline;
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
range_append(struct row_ranges *ranges, int start, int end,
|
|
enum row_range_type type, const union row_range_data *data)
|
|
{
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
range_append_by_ref(
|
|
ranges, start, end, type,
|
|
&(union row_range_data){.uri = {.id = data->uri.id,
|
|
.uri = xstrdup(data->uri.uri)}});
|
|
break;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
range_append_by_ref(ranges, start, end, type, data);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
range_delete(struct row_ranges *ranges, enum row_range_type type, size_t idx)
|
|
{
|
|
xassert(idx < ranges->count);
|
|
grid_row_range_destroy(&ranges->v[idx], type);
|
|
|
|
const size_t move_count = ranges->count - idx - 1;
|
|
memmove(&ranges->v[idx],
|
|
&ranges->v[idx + 1],
|
|
move_count * sizeof(ranges->v[0]));
|
|
ranges->count--;
|
|
}
|
|
|
|
struct grid *
|
|
grid_snapshot(const struct grid *grid)
|
|
{
|
|
struct grid *clone = xmalloc(sizeof(*clone));
|
|
clone->num_rows = grid->num_rows;
|
|
clone->num_cols = grid->num_cols;
|
|
clone->offset = grid->offset;
|
|
clone->view = grid->view;
|
|
clone->cursor = grid->cursor;
|
|
clone->saved_cursor = grid->saved_cursor;
|
|
clone->kitty_kbd = grid->kitty_kbd;
|
|
clone->rows = xcalloc(grid->num_rows, sizeof(clone->rows[0]));
|
|
memset(&clone->scroll_damage, 0, sizeof(clone->scroll_damage));
|
|
memset(&clone->sixel_images, 0, sizeof(clone->sixel_images));
|
|
|
|
tll_foreach(grid->scroll_damage, it)
|
|
tll_push_back(clone->scroll_damage, it->item);
|
|
|
|
for (int r = 0; r < grid->num_rows; r++) {
|
|
const struct row *row = grid->rows[r];
|
|
|
|
if (row == NULL)
|
|
continue;
|
|
|
|
struct row *clone_row = xmalloc(sizeof(*row));
|
|
clone->rows[r] = clone_row;
|
|
|
|
clone_row->cells = xmalloc(grid->num_cols * sizeof(clone_row->cells[0]));
|
|
clone_row->linebreak = row->linebreak;
|
|
clone_row->dirty = row->dirty;
|
|
clone_row->shell_integration = row->shell_integration;
|
|
|
|
for (int c = 0; c < grid->num_cols; c++)
|
|
clone_row->cells[c] = row->cells[c];
|
|
|
|
const struct row_data *extra = row->extra;
|
|
|
|
if (extra != NULL) {
|
|
struct row_data *clone_extra = xcalloc(1, sizeof(*clone_extra));
|
|
clone_row->extra = clone_extra;
|
|
|
|
range_ensure_size(&clone_extra->uri_ranges, extra->uri_ranges.count);
|
|
range_ensure_size(&clone_extra->underline_ranges, extra->underline_ranges.count);
|
|
|
|
for (int i = 0; i < extra->uri_ranges.count; i++) {
|
|
const struct row_range *range = &extra->uri_ranges.v[i];
|
|
range_append(
|
|
&clone_extra->uri_ranges,
|
|
range->start, range->end, ROW_RANGE_URI, &range->data);
|
|
}
|
|
|
|
for (int i = 0; i < extra->underline_ranges.count; i++) {
|
|
const struct row_range *range = &extra->underline_ranges.v[i];
|
|
range_append_by_ref(
|
|
&clone_extra->underline_ranges, range->start, range->end,
|
|
ROW_RANGE_UNDERLINE, &range->data);
|
|
}
|
|
} else
|
|
clone_row->extra = NULL;
|
|
}
|
|
|
|
tll_foreach(grid->sixel_images, it) {
|
|
int original_width = it->item.original.width;
|
|
int original_height = it->item.original.height;
|
|
pixman_image_t *original_pix = it->item.original.pix;
|
|
pixman_format_code_t original_pix_fmt = pixman_image_get_format(original_pix);
|
|
int original_stride = stride_for_format_and_width(original_pix_fmt, original_width);
|
|
|
|
size_t original_size = original_stride * original_height;
|
|
void *new_original_data = xmemdup(it->item.original.data, original_size);
|
|
|
|
pixman_image_t *new_original_pix = pixman_image_create_bits_no_clear(
|
|
original_pix_fmt, original_width, original_height,
|
|
new_original_data, original_stride);
|
|
|
|
void *new_scaled_data = NULL;
|
|
pixman_image_t *new_scaled_pix = NULL;
|
|
int scaled_width = -1;
|
|
int scaled_height = -1;
|
|
|
|
if (it->item.scaled.data != NULL) {
|
|
scaled_width = it->item.scaled.width;
|
|
scaled_height = it->item.scaled.height;
|
|
|
|
pixman_image_t *scaled_pix = it->item.scaled.pix;
|
|
pixman_format_code_t scaled_pix_fmt = pixman_image_get_format(scaled_pix);
|
|
int scaled_stride = stride_for_format_and_width(scaled_pix_fmt, scaled_width);
|
|
|
|
size_t scaled_size = scaled_stride * scaled_height;
|
|
new_scaled_data = xmemdup(it->item.scaled.data, scaled_size);
|
|
|
|
new_scaled_pix = pixman_image_create_bits_no_clear(
|
|
scaled_pix_fmt, scaled_width, scaled_height, new_scaled_data,
|
|
scaled_stride);
|
|
}
|
|
|
|
struct sixel six = {
|
|
.pix = (it->item.pix == it->item.original.pix
|
|
? new_original_pix
|
|
: (it->item.pix == it->item.scaled.pix
|
|
? new_scaled_pix
|
|
: NULL)),
|
|
.width = it->item.width,
|
|
.height = it->item.height,
|
|
.rows = it->item.rows,
|
|
.cols = it->item.cols,
|
|
.pos = it->item.pos,
|
|
.opaque = it->item.opaque,
|
|
.cell_width = it->item.cell_width,
|
|
.cell_height = it->item.cell_height,
|
|
.original = {
|
|
.data = new_original_data,
|
|
.pix = new_original_pix,
|
|
.width = original_width,
|
|
.height = original_height,
|
|
},
|
|
.scaled = {
|
|
.data = new_scaled_data,
|
|
.pix = new_scaled_pix,
|
|
.width = scaled_width,
|
|
.height = scaled_height,
|
|
},
|
|
};
|
|
|
|
tll_push_back(clone->sixel_images, six);
|
|
}
|
|
|
|
return clone;
|
|
}
|
|
|
|
void
|
|
grid_free(struct grid *grid)
|
|
{
|
|
if (grid == NULL)
|
|
return;
|
|
|
|
for (int r = 0; r < grid->num_rows; r++)
|
|
grid_row_free(grid->rows[r]);
|
|
|
|
tll_foreach(grid->sixel_images, it) {
|
|
sixel_destroy(&it->item);
|
|
tll_remove(grid->sixel_images, it);
|
|
}
|
|
|
|
free(grid->rows);
|
|
tll_free(grid->scroll_damage);
|
|
}
|
|
|
|
void
|
|
grid_swap_row(struct grid *grid, int row_a, int row_b)
|
|
{
|
|
xassert(grid->offset >= 0);
|
|
xassert(row_a != row_b);
|
|
|
|
int real_a = (grid->offset + row_a) & (grid->num_rows - 1);
|
|
int real_b = (grid->offset + row_b) & (grid->num_rows - 1);
|
|
|
|
struct row *a = grid->rows[real_a];
|
|
struct row *b = grid->rows[real_b];
|
|
|
|
grid->rows[real_a] = b;
|
|
grid->rows[real_b] = a;
|
|
}
|
|
|
|
struct row *
|
|
grid_row_alloc(int cols, bool initialize)
|
|
{
|
|
struct row *row = xmalloc(sizeof(*row));
|
|
row->dirty = false;
|
|
row->linebreak = true;
|
|
row->extra = NULL;
|
|
row->shell_integration.prompt_marker = false;
|
|
row->shell_integration.cmd_start = -1;
|
|
row->shell_integration.cmd_end = -1;
|
|
|
|
if (initialize) {
|
|
row->cells = xcalloc(cols, sizeof(row->cells[0]));
|
|
for (size_t c = 0; c < cols; c++)
|
|
row->cells[c].attrs.clean = 1;
|
|
} else
|
|
row->cells = xmalloc(cols * sizeof(row->cells[0]));
|
|
|
|
return row;
|
|
}
|
|
|
|
void
|
|
grid_row_free(struct row *row)
|
|
{
|
|
if (row == NULL)
|
|
return;
|
|
|
|
grid_row_reset_extra(row);
|
|
free(row->extra);
|
|
free(row->cells);
|
|
free(row);
|
|
}
|
|
|
|
void
|
|
grid_resize_without_reflow(
|
|
struct grid *grid, int new_rows, int new_cols,
|
|
int old_screen_rows, int new_screen_rows)
|
|
{
|
|
struct row *const *old_grid = grid->rows;
|
|
const int old_rows = grid->num_rows;
|
|
const int old_cols = grid->num_cols;
|
|
|
|
struct row **new_grid = xcalloc(new_rows, sizeof(new_grid[0]));
|
|
|
|
tll(struct sixel) untranslated_sixels = tll_init();
|
|
tll_foreach(grid->sixel_images, it)
|
|
tll_push_back(untranslated_sixels, it->item);
|
|
tll_free(grid->sixel_images);
|
|
|
|
int new_offset = 0;
|
|
|
|
/* Copy old lines, truncating them if old rows were longer */
|
|
for (int r = 0, n = min(old_screen_rows, new_screen_rows); r < n; r++) {
|
|
const int old_row_idx = (grid->offset + r) & (old_rows - 1);
|
|
const int new_row_idx = (new_offset + r) & (new_rows - 1);
|
|
|
|
const struct row *old_row = old_grid[old_row_idx];
|
|
xassert(old_row != NULL);
|
|
|
|
struct row *new_row = grid_row_alloc(new_cols, false);
|
|
new_grid[new_row_idx] = new_row;
|
|
|
|
memcpy(new_row->cells,
|
|
old_row->cells,
|
|
sizeof(struct cell) * min(old_cols, new_cols));
|
|
|
|
new_row->dirty = old_row->dirty;
|
|
new_row->linebreak = false;
|
|
new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker;
|
|
new_row->shell_integration.cmd_start = min(old_row->shell_integration.cmd_start, new_cols - 1);
|
|
new_row->shell_integration.cmd_end = min(old_row->shell_integration.cmd_end, new_cols - 1);
|
|
|
|
if (new_cols > old_cols) {
|
|
/* Clear "new" columns */
|
|
memset(&new_row->cells[old_cols], 0,
|
|
sizeof(struct cell) * (new_cols - old_cols));
|
|
new_row->dirty = true;
|
|
} else if (old_cols > new_cols) {
|
|
/* Make sure we don't cut a multi-column character in two */
|
|
for (int i = new_cols; i > 0 && old_row->cells[i].wc > CELL_SPACER; i--)
|
|
new_row->cells[i - 1].wc = 0;
|
|
}
|
|
|
|
/* Map sixels on current "old" row to current "new row" */
|
|
tll_foreach(untranslated_sixels, it) {
|
|
if (it->item.pos.row != old_row_idx)
|
|
continue;
|
|
|
|
struct sixel sixel = it->item;
|
|
sixel.pos.row = new_row_idx;
|
|
|
|
if (sixel.pos.col < new_cols)
|
|
tll_push_back(grid->sixel_images, sixel);
|
|
else
|
|
sixel_destroy(&it->item);
|
|
tll_remove(untranslated_sixels, it);
|
|
}
|
|
|
|
/* Copy URI ranges, truncating them if necessary */
|
|
const struct row_data *old_extra = old_row->extra;
|
|
if (old_extra == NULL)
|
|
continue;
|
|
|
|
ensure_row_has_extra_data(new_row);
|
|
struct row_data *new_extra = new_row->extra;
|
|
|
|
range_ensure_size(&new_extra->uri_ranges, old_extra->uri_ranges.count);
|
|
range_ensure_size(&new_extra->underline_ranges, old_extra->underline_ranges.count);
|
|
|
|
for (int i = 0; i < old_extra->uri_ranges.count; i++) {
|
|
const struct row_range *range = &old_extra->uri_ranges.v[i];
|
|
|
|
if (range->start >= new_cols) {
|
|
/* The whole range is truncated */
|
|
continue;
|
|
}
|
|
|
|
const int start = range->start;
|
|
const int end = min(range->end, new_cols - 1);
|
|
range_append(&new_extra->uri_ranges, start, end, ROW_RANGE_URI, &range->data);
|
|
}
|
|
|
|
for (int i = 0; i < old_extra->underline_ranges.count; i++) {
|
|
const struct row_range *range = &old_extra->underline_ranges.v[i];
|
|
|
|
if (range->start >= new_cols) {
|
|
/* The whole range is truncated */
|
|
continue;
|
|
}
|
|
|
|
const int start = range->start;
|
|
const int end = min(range->end, new_cols - 1);
|
|
range_append_by_ref(&new_extra->underline_ranges, start, end, ROW_RANGE_UNDERLINE, &range->data);
|
|
}
|
|
}
|
|
|
|
/* Clear "new" lines */
|
|
for (int r = min(old_screen_rows, new_screen_rows); r < new_screen_rows; r++) {
|
|
struct row *new_row = grid_row_alloc(new_cols, false);
|
|
new_grid[(new_offset + r) & (new_rows - 1)] = new_row;
|
|
|
|
memset(new_row->cells, 0, sizeof(struct cell) * new_cols);
|
|
new_row->dirty = true;
|
|
}
|
|
|
|
#if defined(_DEBUG)
|
|
for (size_t r = 0; r < new_rows; r++) {
|
|
const struct row *row = new_grid[r];
|
|
|
|
if (row == NULL)
|
|
continue;
|
|
if (row->extra == NULL)
|
|
continue;
|
|
|
|
verify_no_overlapping_ranges(row->extra);
|
|
verify_ranges_are_sorted(row->extra);
|
|
}
|
|
#endif
|
|
|
|
/* Free old grid */
|
|
for (int r = 0; r < grid->num_rows; r++)
|
|
grid_row_free(old_grid[r]);
|
|
free(grid->rows);
|
|
|
|
grid->rows = new_grid;
|
|
grid->num_rows = new_rows;
|
|
grid->num_cols = new_cols;
|
|
|
|
grid->view = grid->offset = new_offset;
|
|
|
|
/* Keep cursor at current position, but clamp to new dimensions */
|
|
struct coord cursor = grid->cursor.point;
|
|
if (cursor.row == old_screen_rows - 1) {
|
|
/* 'less' breaks if the cursor isn't at the bottom */
|
|
cursor.row = new_screen_rows - 1;
|
|
}
|
|
cursor.row = min(cursor.row, new_screen_rows - 1);
|
|
cursor.col = min(cursor.col, new_cols - 1);
|
|
grid->cursor.point = cursor;
|
|
|
|
struct coord saved_cursor = grid->saved_cursor.point;
|
|
if (saved_cursor.row == old_screen_rows - 1)
|
|
saved_cursor.row = new_screen_rows - 1;
|
|
saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1);
|
|
saved_cursor.col = min(saved_cursor.col, new_cols - 1);
|
|
grid->saved_cursor.point = saved_cursor;
|
|
|
|
grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)];
|
|
xassert(grid->cur_row != NULL);
|
|
|
|
grid->cursor.lcf = false;
|
|
grid->saved_cursor.lcf = false;
|
|
|
|
/* Free sixels we failed to "map" to the new grid */
|
|
tll_foreach(untranslated_sixels, it)
|
|
sixel_destroy(&it->item);
|
|
tll_free(untranslated_sixels);
|
|
|
|
#if defined(_DEBUG)
|
|
for (int r = 0; r < new_screen_rows; r++)
|
|
grid_row_in_view(grid, r);
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
reflow_range_start(struct row_range *range, enum row_range_type type,
|
|
struct row *new_row, int new_col_idx)
|
|
{
|
|
ensure_row_has_extra_data(new_row);
|
|
|
|
struct row_ranges *new_ranges = NULL;
|
|
switch (type) {
|
|
case ROW_RANGE_URI: new_ranges = &new_row->extra->uri_ranges; break;
|
|
case ROW_RANGE_UNDERLINE: new_ranges = &new_row->extra->underline_ranges; break;
|
|
}
|
|
|
|
if (new_ranges == NULL)
|
|
BUG("unhandled range type");
|
|
|
|
range_append_by_ref(new_ranges, new_col_idx, -1, type, &range->data);
|
|
|
|
switch (type) {
|
|
case ROW_RANGE_URI: range->uri.uri = NULL; break; /* Owned by new_ranges */
|
|
case ROW_RANGE_UNDERLINE: break;
|
|
}
|
|
}
|
|
|
|
static void
|
|
reflow_range_end(struct row_range *range, enum row_range_type type,
|
|
struct row *new_row, int new_col_idx)
|
|
{
|
|
struct row_data *extra = new_row->extra;
|
|
struct row_ranges *ranges = NULL;
|
|
|
|
switch (type) {
|
|
case ROW_RANGE_URI: ranges = &extra->uri_ranges; break;
|
|
case ROW_RANGE_UNDERLINE: ranges = &extra->underline_ranges; break;
|
|
}
|
|
|
|
if (ranges == NULL)
|
|
BUG("unhandled range type");
|
|
|
|
xassert(ranges->count > 0);
|
|
|
|
struct row_range *new_range = &ranges->v[ranges->count - 1];
|
|
xassert(new_range->end < 0);
|
|
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
xassert(new_range->uri.id == range->uri.id);
|
|
break;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
xassert(new_range->underline.style == range->underline.style);
|
|
xassert(new_range->underline.color_src == range->underline.color_src);
|
|
xassert(new_range->underline.color == range->underline.color);
|
|
break;
|
|
}
|
|
|
|
new_range->end = new_col_idx;
|
|
}
|
|
|
|
static struct row *
|
|
_line_wrap(struct grid *old_grid, struct row **new_grid, struct row *row,
|
|
int *row_idx, int *col_idx, int row_count, int col_count)
|
|
{
|
|
*col_idx = 0;
|
|
*row_idx = (*row_idx + 1) & (row_count - 1);
|
|
|
|
struct row *new_row = new_grid[*row_idx];
|
|
|
|
if (new_row == NULL) {
|
|
/* Scrollback not yet full, allocate a completely new row */
|
|
new_row = grid_row_alloc(col_count, false);
|
|
new_grid[*row_idx] = new_row;
|
|
|
|
/* *clear* linebreak, since we only want to set it when we
|
|
reach the end of an old row, with linebreak=true */
|
|
new_row->linebreak = false;
|
|
} else {
|
|
/* Scrollback is full, need to reuse a row */
|
|
grid_row_reset_extra(new_row);
|
|
new_row->shell_integration.prompt_marker = false;
|
|
new_row->shell_integration.cmd_start = -1;
|
|
new_row->shell_integration.cmd_end = -1;
|
|
|
|
/* *clear* linebreak, since we only want to set it when we
|
|
reach the end of an old row, with linebreak=true */
|
|
new_row->linebreak = false;
|
|
|
|
tll_foreach(old_grid->sixel_images, it) {
|
|
if (it->item.pos.row == *row_idx) {
|
|
sixel_destroy(&it->item);
|
|
tll_remove(old_grid->sixel_images, it);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* TODO: detect if the reused row is covered by the
|
|
* selection. Of so, cancel the selection. The problem: we
|
|
* don't know if we've translated the selection coordinates
|
|
* yet.
|
|
*/
|
|
}
|
|
|
|
struct row_data *extra = row->extra;
|
|
if (extra == NULL)
|
|
return new_row;
|
|
|
|
/*
|
|
* URI ranges are per row. Thus, we need to 'close' the still-open
|
|
* ranges on the previous row, and re-open them on the
|
|
* next/current row.
|
|
*/
|
|
if (extra->uri_ranges.count > 0) {
|
|
struct row_range *range =
|
|
&extra->uri_ranges.v[extra->uri_ranges.count - 1];
|
|
|
|
if (range->end < 0) {
|
|
|
|
/* Terminate URI range on the previous row */
|
|
range->end = col_count - 1;
|
|
|
|
/* Open a new range on the new/current row */
|
|
ensure_row_has_extra_data(new_row);
|
|
range_append(&new_row->extra->uri_ranges, 0, -1,
|
|
ROW_RANGE_URI, &range->data);
|
|
}
|
|
}
|
|
|
|
if (extra->underline_ranges.count > 0) {
|
|
struct row_range *range =
|
|
&extra->underline_ranges.v[extra->underline_ranges.count - 1];
|
|
|
|
if (range->end < 0) {
|
|
|
|
/* Terminate URI range on the previous row */
|
|
range->end = col_count - 1;
|
|
|
|
/* Open a new range on the new/current row */
|
|
ensure_row_has_extra_data(new_row);
|
|
range_append(&new_row->extra->underline_ranges, 0, -1,
|
|
ROW_RANGE_UNDERLINE, &range->data);
|
|
}
|
|
}
|
|
|
|
return new_row;
|
|
}
|
|
|
|
static struct {
|
|
int scrollback_start;
|
|
int rows;
|
|
} tp_cmp_ctx;
|
|
|
|
static int
|
|
tp_cmp(const void *_a, const void *_b)
|
|
{
|
|
const struct coord *a = *(const struct coord **)_a;
|
|
const struct coord *b = *(const struct coord **)_b;
|
|
|
|
int scrollback_start = tp_cmp_ctx.scrollback_start;
|
|
int num_rows = tp_cmp_ctx.rows;
|
|
|
|
int a_row = (a->row - scrollback_start + num_rows) & (num_rows - 1);
|
|
int b_row = (b->row - scrollback_start + num_rows) & (num_rows - 1);
|
|
|
|
xassert(a_row >= 0);
|
|
xassert(a_row < num_rows || num_rows == 0);
|
|
xassert(b_row >= 0);
|
|
xassert(b_row < num_rows || num_rows == 0);
|
|
|
|
if (a_row < b_row)
|
|
return -1;
|
|
if (a_row > b_row)
|
|
return 1;
|
|
|
|
xassert(a_row == b_row);
|
|
|
|
if (a->col < b->col)
|
|
return -1;
|
|
if (a->col > b->col)
|
|
return 1;
|
|
|
|
xassert(a->col == b->col);
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
grid_resize_and_reflow(
|
|
struct grid *grid, const struct terminal *term, int new_rows, int new_cols,
|
|
int old_screen_rows, int new_screen_rows,
|
|
size_t tracking_points_count,
|
|
struct coord *const _tracking_points[static tracking_points_count])
|
|
{
|
|
#if defined(TIME_REFLOW) && TIME_REFLOW
|
|
struct timespec start;
|
|
clock_gettime(CLOCK_MONOTONIC, &start);
|
|
#endif
|
|
|
|
struct row *const *old_grid = grid->rows;
|
|
const int old_rows = grid->num_rows;
|
|
const int old_cols = grid->num_cols;
|
|
|
|
/* Is viewpoint tracking current grid offset? */
|
|
const bool view_follows = grid->view == grid->offset;
|
|
|
|
int new_col_idx = 0;
|
|
int new_row_idx = 0;
|
|
|
|
struct row **new_grid = xcalloc(new_rows, sizeof(new_grid[0]));
|
|
struct row *new_row = new_grid[new_row_idx];
|
|
|
|
xassert(new_row == NULL);
|
|
new_row = grid_row_alloc(new_cols, false);
|
|
new_grid[new_row_idx] = new_row;
|
|
|
|
/* Start at the beginning of the old grid's scrollback. That is,
|
|
* at the output that is *oldest* */
|
|
int offset = grid->offset + old_screen_rows;
|
|
|
|
tll(struct sixel) untranslated_sixels = tll_init();
|
|
tll_foreach(grid->sixel_images, it)
|
|
tll_push_back(untranslated_sixels, it->item);
|
|
tll_free(grid->sixel_images);
|
|
|
|
/* Turn cursor coordinates into grid absolute coordinates */
|
|
struct coord cursor = grid->cursor.point;
|
|
cursor.row += grid->offset;
|
|
cursor.row &= old_rows - 1;
|
|
|
|
struct coord saved_cursor = grid->saved_cursor.point;
|
|
saved_cursor.row += grid->offset;
|
|
saved_cursor.row &= old_rows - 1;
|
|
|
|
size_t tp_count =
|
|
tracking_points_count +
|
|
1 + /* cursor */
|
|
1 + /* saved cursor */
|
|
!view_follows + /* viewport */
|
|
1; /* terminator */
|
|
|
|
struct coord *tracking_points[tp_count];
|
|
memcpy(tracking_points, _tracking_points, tracking_points_count * sizeof(_tracking_points[0]));
|
|
tracking_points[tracking_points_count] = &cursor;
|
|
tracking_points[tracking_points_count + 1] = &saved_cursor;
|
|
|
|
struct coord viewport = {0, grid->view};
|
|
if (!view_follows)
|
|
tracking_points[tracking_points_count + 2] = &viewport;
|
|
|
|
/* Not thread safe! */
|
|
tp_cmp_ctx.scrollback_start = offset;
|
|
tp_cmp_ctx.rows = old_rows;
|
|
qsort(
|
|
tracking_points, tp_count - 1, sizeof(tracking_points[0]), &tp_cmp);
|
|
|
|
/* NULL terminate */
|
|
struct coord terminator = {-1, -1};
|
|
tracking_points[tp_count - 1] = &terminator;
|
|
struct coord **next_tp = &tracking_points[0];
|
|
|
|
LOG_DBG("scrollback-start=%d", offset);
|
|
for (size_t i = 0; i < tp_count - 1; i++) {
|
|
LOG_DBG("TP #%zu: row=%d, col=%d",
|
|
i, tracking_points[i]->row, tracking_points[i]->col);
|
|
}
|
|
|
|
int coalesced_linebreaks = 0;
|
|
|
|
/*
|
|
* Walk the old grid
|
|
*/
|
|
for (int r = 0; r < old_rows; r++) {
|
|
|
|
const size_t old_row_idx = (offset + r) & (old_rows - 1);
|
|
|
|
/* Unallocated (empty) rows we can simply skip */
|
|
const struct row *old_row = old_grid[old_row_idx];
|
|
if (old_row == NULL)
|
|
continue;
|
|
|
|
/* Map sixels on current "old" row to current "new row" */
|
|
tll_foreach(untranslated_sixels, it) {
|
|
if (it->item.pos.row != old_row_idx)
|
|
continue;
|
|
|
|
struct sixel sixel = it->item;
|
|
sixel.pos.row = new_row_idx;
|
|
|
|
tll_push_back(grid->sixel_images, sixel);
|
|
tll_remove(untranslated_sixels, it);
|
|
}
|
|
|
|
#define line_wrap() \
|
|
new_row = _line_wrap( \
|
|
grid, new_grid, new_row, &new_row_idx, &new_col_idx, \
|
|
new_rows, new_cols)
|
|
|
|
/* Find last non-empty cell */
|
|
int col_count = 0;
|
|
for (int c = old_cols - 1; c >= 0; c--) {
|
|
const struct cell *cell = &old_row->cells[c];
|
|
if (!(cell->wc == 0 || cell->wc == CELL_SPACER)) {
|
|
col_count = c + 1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!old_row->linebreak && col_count > 0) {
|
|
/* Don't truncate logical lines */
|
|
while (col_count < old_cols && old_row->cells[col_count].wc == 0)
|
|
col_count++;
|
|
}
|
|
|
|
xassert(col_count >= 0 && col_count <= old_cols);
|
|
|
|
/* Do we have a (at least one) tracking point on this row */
|
|
struct coord *tp;
|
|
if (unlikely((*next_tp)->row == old_row_idx)) {
|
|
tp = *next_tp;
|
|
|
|
/* Find the *last* tracking point on this row */
|
|
struct coord *last_on_row = tp;
|
|
for (struct coord **iter = next_tp; (*iter)->row == old_row_idx; iter++)
|
|
last_on_row = *iter;
|
|
|
|
/* And make sure its end point is included in the col range */
|
|
xassert(last_on_row->row == old_row_idx);
|
|
col_count = max(col_count, last_on_row->col + 1);
|
|
} else
|
|
tp = NULL;
|
|
|
|
/* Does this row have any URIs? */
|
|
struct row_range *uri_range, *uri_range_terminator;
|
|
struct row_range *underline_range, *underline_range_terminator;
|
|
const struct row_data *extra = old_row->extra;
|
|
|
|
if (extra != NULL && extra->uri_ranges.count > 0) {
|
|
uri_range = &extra->uri_ranges.v[0];
|
|
uri_range_terminator = &extra->uri_ranges.v[extra->uri_ranges.count];
|
|
|
|
/* Make sure the *last* URI range's end point is included
|
|
* in the copy */
|
|
const struct row_range *last_on_row =
|
|
&extra->uri_ranges.v[extra->uri_ranges.count - 1];
|
|
col_count = max(col_count, last_on_row->end + 1);
|
|
} else
|
|
uri_range = uri_range_terminator = NULL;
|
|
|
|
if (extra != NULL && extra->underline_ranges.count > 0) {
|
|
underline_range = &extra->underline_ranges.v[0];
|
|
underline_range_terminator = &extra->underline_ranges.v[extra->underline_ranges.count];
|
|
|
|
const struct row_range *last_on_row =
|
|
&extra->underline_ranges.v[extra->underline_ranges.count - 1];
|
|
col_count = max(col_count, last_on_row->end + 1);
|
|
} else
|
|
underline_range = underline_range_terminator = NULL;
|
|
|
|
if (unlikely(col_count > 0 && coalesced_linebreaks > 0)) {
|
|
for (size_t apa = 0; apa < coalesced_linebreaks; apa++) {
|
|
/* Erase the remaining cells */
|
|
memset(&new_row->cells[new_col_idx], 0,
|
|
(new_cols - new_col_idx) * sizeof(new_row->cells[0]));
|
|
new_row->linebreak = true;
|
|
|
|
if (r + 1 < old_rows)
|
|
line_wrap();
|
|
}
|
|
|
|
coalesced_linebreaks = 0;
|
|
}
|
|
|
|
for (int c = 0; c < col_count;) {
|
|
const struct cell *old = &old_row->cells[c];
|
|
|
|
/* Row full, emit newline and get a new, fresh, row */
|
|
xassert(new_col_idx <= new_cols);
|
|
if (unlikely(new_col_idx >= new_cols))
|
|
line_wrap();
|
|
|
|
char32_t wc = old->wc;
|
|
int width = 1;
|
|
|
|
if (unlikely(wc >= CELL_COMB_CHARS_LO && wc <= CELL_COMB_CHARS_HI)) {
|
|
const struct composed *composed =
|
|
composed_lookup(term->composed, wc - CELL_COMB_CHARS_LO);
|
|
|
|
width = composed->forced_width > 0 ? composed->forced_width : composed->width;
|
|
} else if (unlikely(c + 1 < col_count && (old + 1)->wc >= CELL_SPACER + 1)) {
|
|
/* Wide character, get its width from the next cell's
|
|
SPACER value */
|
|
width = (old + 1)->wc - CELL_SPACER + 1;
|
|
}
|
|
|
|
/*
|
|
* Check if character fits, if not, emit spacers, and push
|
|
the character to the next row */
|
|
if (unlikely(new_col_idx + width > new_cols && width <= new_cols)) {
|
|
for (; new_col_idx < new_cols; new_col_idx++) {
|
|
new_row->cells[new_col_idx].wc = CELL_SPACER;
|
|
new_row->cells[new_col_idx].attrs = (struct attributes){0};
|
|
}
|
|
line_wrap();
|
|
}
|
|
|
|
new_row->shell_integration.prompt_marker = old_row->shell_integration.prompt_marker;
|
|
|
|
for (int i = 0; i < width; i++) {
|
|
if (unlikely(uri_range != NULL && uri_range != uri_range_terminator)) {
|
|
if (unlikely(uri_range->start == c)) {
|
|
reflow_range_start(
|
|
uri_range, ROW_RANGE_URI, new_row, new_col_idx);
|
|
}
|
|
|
|
if (unlikely(uri_range->end == c)) {
|
|
reflow_range_end(
|
|
uri_range, ROW_RANGE_URI, new_row, new_col_idx);
|
|
grid_row_uri_range_destroy(uri_range);
|
|
uri_range++;
|
|
}
|
|
}
|
|
|
|
if (unlikely(underline_range != NULL && underline_range != underline_range_terminator)) {
|
|
if (unlikely(underline_range->start == c)) {
|
|
reflow_range_start(
|
|
underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx);
|
|
}
|
|
|
|
if (unlikely(underline_range->end == c)) {
|
|
reflow_range_end(
|
|
underline_range, ROW_RANGE_UNDERLINE, new_row, new_col_idx);
|
|
grid_row_underline_range_destroy(underline_range);
|
|
underline_range++;
|
|
}
|
|
}
|
|
|
|
if (unlikely(tp != NULL)) {
|
|
if (unlikely(tp->col == c)) {
|
|
do {
|
|
xassert(tp->row == old_row_idx);
|
|
|
|
tp->row = new_row_idx;
|
|
tp->col = new_col_idx;
|
|
|
|
next_tp++;
|
|
tp = *next_tp;
|
|
} while (tp->row == old_row_idx && tp->col == c);
|
|
|
|
if (tp->row != old_row_idx)
|
|
tp = NULL;
|
|
|
|
LOG_DBG("next TP (tp=%p): %dx%d",
|
|
(void*)tp, (*next_tp)->row, (*next_tp)->col);
|
|
}
|
|
}
|
|
|
|
if (unlikely(old_row->shell_integration.cmd_start == c))
|
|
new_row->shell_integration.cmd_start = new_col_idx;
|
|
|
|
if (unlikely(old_row->shell_integration.cmd_end == c))
|
|
new_row->shell_integration.cmd_end = new_col_idx;
|
|
|
|
if (unlikely(width > new_cols)) {
|
|
/* Wide character no longer fits on a row, replace
|
|
it with a single space */
|
|
new_row->cells[new_col_idx++].wc = 0;
|
|
c++;
|
|
|
|
/* Walk past the SPACER cells */
|
|
for (int i = 1; i < width; i++, c++, old++)
|
|
;
|
|
|
|
/* Continue with next character in the *old* grid */
|
|
break;
|
|
}
|
|
|
|
new_row->cells[new_col_idx++] = *old;
|
|
old++;
|
|
c++;
|
|
}
|
|
}
|
|
|
|
if (old_row->linebreak) {
|
|
if (col_count > 0) {
|
|
/* Erase the remaining cells */
|
|
memset(&new_row->cells[new_col_idx], 0,
|
|
(new_cols - new_col_idx) * sizeof(new_row->cells[0]));
|
|
new_row->linebreak = true;
|
|
|
|
if (r + 1 < old_rows) {
|
|
/* Not the last (old) row */
|
|
line_wrap();
|
|
} else if (new_row->extra != NULL) {
|
|
if (new_row->extra->uri_ranges.count > 0) {
|
|
/*
|
|
* line_wrap() "closes" still-open URIs. Since
|
|
* this is the *last* row, and since we're
|
|
* line-breaking due to a hard line-break (rather
|
|
* than running out of cells in the "new_row"),
|
|
* there shouldn't be an open URI (it would have
|
|
* been closed when we reached the end of the URI
|
|
* while reflowing the last "old" row).
|
|
*/
|
|
int last_idx = new_row->extra->uri_ranges.count - 1;
|
|
xassert(new_row->extra->uri_ranges.v[last_idx].end >= 0);
|
|
}
|
|
|
|
if (new_row->extra->underline_ranges.count > 0) {
|
|
int last_idx = new_row->extra->underline_ranges.count - 1;
|
|
xassert(new_row->extra->underline_ranges.v[last_idx].end >= 0);
|
|
}
|
|
}
|
|
} else {
|
|
/*
|
|
* rows have linebreak=true by default. But we don't
|
|
* want trailing empty lines to result in actual lines
|
|
* in the new grid (think: empty window with prompt at
|
|
* the top)
|
|
*/
|
|
coalesced_linebreaks++;
|
|
}
|
|
}
|
|
|
|
grid_row_free(old_grid[old_row_idx]);
|
|
grid->rows[old_row_idx] = NULL;
|
|
|
|
#undef line_wrap
|
|
}
|
|
|
|
/* Erase the remaining cells */
|
|
memset(&new_row->cells[new_col_idx], 0,
|
|
(new_cols - new_col_idx) * sizeof(new_row->cells[0]));
|
|
|
|
for (struct coord **tp = next_tp; *tp != &terminator; tp++) {
|
|
LOG_DBG("TP: row=%d, col=%d (old cols: %d, new cols: %d)",
|
|
(*tp)->row, (*tp)->col, old_cols, new_cols);
|
|
}
|
|
xassert(old_rows == 0 || *next_tp == &terminator);
|
|
|
|
#if defined(_DEBUG)
|
|
/* Verify all URI ranges have been "closed" */
|
|
for (int r = 0; r < new_rows; r++) {
|
|
const struct row *row = new_grid[r];
|
|
|
|
if (row == NULL)
|
|
continue;
|
|
if (row->extra == NULL)
|
|
continue;
|
|
|
|
for (size_t i = 0; i < row->extra->uri_ranges.count; i++)
|
|
xassert(row->extra->uri_ranges.v[i].end >= 0);
|
|
for (size_t i = 0; i < row->extra->underline_ranges.count; i++)
|
|
xassert(row->extra->underline_ranges.v[i].end >= 0);
|
|
|
|
verify_no_overlapping_ranges(row->extra);
|
|
verify_ranges_are_sorted(row->extra);
|
|
}
|
|
|
|
/* Verify all old rows have been free:d */
|
|
for (int i = 0; i < old_rows; i++)
|
|
xassert(grid->rows[i] == NULL);
|
|
#endif
|
|
|
|
/* Set offset such that the last reflowed row is at the bottom */
|
|
grid->offset = new_row_idx - new_screen_rows + 1;
|
|
|
|
while (grid->offset < 0)
|
|
grid->offset += new_rows;
|
|
while (new_grid[grid->offset] == NULL)
|
|
grid->offset = (grid->offset + 1) & (new_rows - 1);
|
|
|
|
/* Ensure all visible rows have been allocated */
|
|
for (int r = 0; r < new_screen_rows; r++) {
|
|
int idx = (grid->offset + r) & (new_rows - 1);
|
|
if (new_grid[idx] == NULL)
|
|
new_grid[idx] = grid_row_alloc(new_cols, true);
|
|
}
|
|
|
|
/* Free old grid (rows already free:d) */
|
|
free(grid->rows);
|
|
|
|
grid->rows = new_grid;
|
|
grid->num_rows = new_rows;
|
|
grid->num_cols = new_cols;
|
|
|
|
/*
|
|
* Set new viewport, making sure it's not too far down.
|
|
*
|
|
* This is done by using scrollback-start relative cooardinates,
|
|
* and bounding the new viewport to (grid_rows - screen_rows).
|
|
*/
|
|
int sb_view = grid_row_abs_to_sb(
|
|
grid, new_screen_rows, view_follows ? grid->offset : viewport.row);
|
|
grid->view = grid_row_sb_to_abs(
|
|
grid, new_screen_rows, min(sb_view, new_rows - new_screen_rows));
|
|
|
|
/* Convert absolute coordinates to screen relative */
|
|
cursor.row -= grid->offset;
|
|
while (cursor.row < 0)
|
|
cursor.row += grid->num_rows;
|
|
cursor.row = min(cursor.row, new_screen_rows - 1);
|
|
cursor.col = min(cursor.col, new_cols - 1);
|
|
|
|
saved_cursor.row -= grid->offset;
|
|
while (saved_cursor.row < 0)
|
|
saved_cursor.row += grid->num_rows;
|
|
saved_cursor.row = min(saved_cursor.row, new_screen_rows - 1);
|
|
saved_cursor.col = min(saved_cursor.col, new_cols - 1);
|
|
|
|
if (grid->cursor.lcf) {
|
|
if (cursor.col + 1 < new_cols) {
|
|
cursor.col++;
|
|
grid->cursor.lcf = false;
|
|
}
|
|
}
|
|
|
|
if (grid->saved_cursor.lcf) {
|
|
if (saved_cursor.col + 1 < new_cols) {
|
|
saved_cursor.col++;
|
|
grid->saved_cursor.lcf = false;
|
|
}
|
|
}
|
|
|
|
grid->cur_row = new_grid[(grid->offset + cursor.row) & (new_rows - 1)];
|
|
xassert(grid->cur_row != NULL);
|
|
|
|
grid->cursor.point = cursor;
|
|
grid->saved_cursor.point = saved_cursor;
|
|
|
|
/* Free sixels we failed to "map" to the new grid */
|
|
tll_foreach(untranslated_sixels, it)
|
|
sixel_destroy(&it->item);
|
|
tll_free(untranslated_sixels);
|
|
|
|
#if defined(TIME_REFLOW) && TIME_REFLOW
|
|
struct timespec stop;
|
|
clock_gettime(CLOCK_MONOTONIC, &stop);
|
|
|
|
struct timespec diff;
|
|
timespec_sub(&stop, &start, &diff);
|
|
LOG_INFO("reflowed %d -> %d rows in %lds %ldns",
|
|
old_rows, new_rows,
|
|
(long)diff.tv_sec,
|
|
diff.tv_nsec);
|
|
#endif
|
|
}
|
|
|
|
static bool
|
|
ranges_match(const struct row_range *r1, const struct row_range *r2,
|
|
enum row_range_type type)
|
|
{
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
/* TODO: also match URI? */
|
|
return r1->uri.id == r2->uri.id;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
return r1->underline.style == r2->underline.style &&
|
|
r1->underline.color_src == r2->underline.color_src &&
|
|
r1->underline.color == r2->underline.color;
|
|
}
|
|
|
|
BUG("invalid range type");
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
range_match_data(const struct row_range *r, const union row_range_data *data,
|
|
enum row_range_type type)
|
|
{
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
return r->uri.id == data->uri.id;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
return r->underline.style == data->underline.style &&
|
|
r->underline.color_src == data->underline.color_src &&
|
|
r->underline.color == data->underline.color;
|
|
}
|
|
|
|
BUG("invalid range type");
|
|
return false;
|
|
}
|
|
|
|
static void
|
|
grid_row_range_put(struct row_ranges *ranges, int col,
|
|
const union row_range_data *data, enum row_range_type type)
|
|
{
|
|
size_t insert_idx = 0;
|
|
bool replace = false;
|
|
bool run_merge_pass = false;
|
|
|
|
for (int i = ranges->count - 1; i >= 0; i--) {
|
|
struct row_range *r = &ranges->v[i];
|
|
|
|
const bool matching = range_match_data(r, data, type);
|
|
|
|
if (matching && r->end + 1 == col) {
|
|
/* Extend existing range tail */
|
|
r->end++;
|
|
return;
|
|
}
|
|
|
|
else if (r->end < col) {
|
|
insert_idx = i + 1;
|
|
break;
|
|
}
|
|
|
|
else if (r->start > col)
|
|
continue;
|
|
|
|
else {
|
|
xassert(r->start <= col);
|
|
xassert(r->end >= col);
|
|
|
|
if (matching)
|
|
return;
|
|
|
|
if (r->start == r->end) {
|
|
replace = true;
|
|
run_merge_pass = true;
|
|
insert_idx = i;
|
|
} else if (r->start == col) {
|
|
run_merge_pass = true;
|
|
r->start++;
|
|
insert_idx = i;
|
|
} else if (r->end == col) {
|
|
run_merge_pass = true;
|
|
r->end--;
|
|
insert_idx = i + 1;
|
|
} else {
|
|
xassert(r->start < col);
|
|
xassert(r->end > col);
|
|
|
|
union row_range_data insert_data;
|
|
switch (type) {
|
|
case ROW_RANGE_URI: insert_data.uri = r->uri; break;
|
|
case ROW_RANGE_UNDERLINE: insert_data.underline = r->underline; break;
|
|
}
|
|
|
|
range_insert(ranges, i + 1, col + 1, r->end, type, &insert_data);
|
|
|
|
/* The insertion may xrealloc() the vector, making our
|
|
* 'old' pointer invalid */
|
|
r = &ranges->v[i];
|
|
r->end = col - 1;
|
|
xassert(r->start <= r->end);
|
|
|
|
insert_idx = i + 1;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
xassert(insert_idx <= ranges->count);
|
|
|
|
if (replace) {
|
|
grid_row_range_destroy(&ranges->v[insert_idx], type);
|
|
ranges->v[insert_idx] = (struct row_range){
|
|
.start = col,
|
|
.end = col,
|
|
};
|
|
|
|
switch (type) {
|
|
case ROW_RANGE_URI:
|
|
ranges->v[insert_idx].uri.id = data->uri.id;
|
|
ranges->v[insert_idx].uri.uri = xstrdup(data->uri.uri);
|
|
break;
|
|
|
|
case ROW_RANGE_UNDERLINE:
|
|
ranges->v[insert_idx].underline = data->underline;
|
|
break;
|
|
}
|
|
} else
|
|
range_insert(ranges, insert_idx, col, col, type, data);
|
|
|
|
if (run_merge_pass) {
|
|
for (size_t i = 1; i < ranges->count; i++) {
|
|
struct row_range *r1 = &ranges->v[i - 1];
|
|
struct row_range *r2 = &ranges->v[i];
|
|
|
|
if (ranges_match(r1, r2, type) && r1->end + 1 == r2->start) {
|
|
r1->end = r2->end;
|
|
range_delete(ranges, type, i);
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
grid_row_uri_range_put(struct row *row, int col, const char *uri, uint64_t id)
|
|
{
|
|
ensure_row_has_extra_data(row);
|
|
|
|
grid_row_range_put(
|
|
&row->extra->uri_ranges, col,
|
|
&(union row_range_data){.uri = {.id = id, .uri = (char *)uri}},
|
|
ROW_RANGE_URI);
|
|
|
|
verify_no_overlapping_ranges(row->extra);
|
|
verify_ranges_are_sorted(row->extra);
|
|
}
|
|
|
|
void
|
|
grid_row_underline_range_put(struct row *row, int col, struct underline_range_data data)
|
|
{
|
|
ensure_row_has_extra_data(row);
|
|
|
|
grid_row_range_put(
|
|
&row->extra->underline_ranges, col,
|
|
&(union row_range_data){.underline = data},
|
|
ROW_RANGE_UNDERLINE);
|
|
|
|
verify_no_overlapping_ranges(row->extra);
|
|
verify_ranges_are_sorted(row->extra);
|
|
}
|
|
|
|
UNITTEST
|
|
{
|
|
struct row_data row_data = {.uri_ranges = {0}};
|
|
struct row row = {.extra = &row_data};
|
|
|
|
#define verify_range(idx, _start, _end, _id) \
|
|
do { \
|
|
xassert(idx < row_data.uri_ranges.count); \
|
|
xassert(row_data.uri_ranges.v[idx].start == _start); \
|
|
xassert(row_data.uri_ranges.v[idx].end == _end); \
|
|
xassert(row_data.uri_ranges.v[idx].uri.id == _id); \
|
|
} while (0)
|
|
|
|
grid_row_uri_range_put(&row, 0, "http://foo.bar", 123);
|
|
grid_row_uri_range_put(&row, 1, "http://foo.bar", 123);
|
|
grid_row_uri_range_put(&row, 2, "http://foo.bar", 123);
|
|
grid_row_uri_range_put(&row, 3, "http://foo.bar", 123);
|
|
xassert(row_data.uri_ranges.count == 1);
|
|
verify_range(0, 0, 3, 123);
|
|
|
|
/* No-op */
|
|
grid_row_uri_range_put(&row, 0, "http://foo.bar", 123);
|
|
xassert(row_data.uri_ranges.count == 1);
|
|
verify_range(0, 0, 3, 123);
|
|
|
|
/* Replace head */
|
|
grid_row_uri_range_put(&row, 0, "http://head", 456);
|
|
xassert(row_data.uri_ranges.count == 2);
|
|
verify_range(0, 0, 0, 456);
|
|
verify_range(1, 1, 3, 123);
|
|
|
|
/* Replace tail */
|
|
grid_row_uri_range_put(&row, 3, "http://tail", 789);
|
|
xassert(row_data.uri_ranges.count == 3);
|
|
verify_range(1, 1, 2, 123);
|
|
verify_range(2, 3, 3, 789);
|
|
|
|
/* Replace tail + extend head */
|
|
grid_row_uri_range_put(&row, 2, "http://tail", 789);
|
|
xassert(row_data.uri_ranges.count == 3);
|
|
verify_range(1, 1, 1, 123);
|
|
verify_range(2, 2, 3, 789);
|
|
|
|
/* Replace + extend tail */
|
|
grid_row_uri_range_put(&row, 1, "http://head", 456);
|
|
xassert(row_data.uri_ranges.count == 2);
|
|
verify_range(0, 0, 1, 456);
|
|
verify_range(1, 2, 3, 789);
|
|
|
|
/* Replace + extend, then splice */
|
|
grid_row_uri_range_put(&row, 1, "http://tail", 789);
|
|
grid_row_uri_range_put(&row, 2, "http://splice", 000);
|
|
xassert(row_data.uri_ranges.count == 4);
|
|
verify_range(0, 0, 0, 456);
|
|
verify_range(1, 1, 1, 789);
|
|
verify_range(2, 2, 2, 000);
|
|
verify_range(3, 3, 3, 789);
|
|
|
|
for (size_t i = 0; i < row_data.uri_ranges.count; i++)
|
|
grid_row_uri_range_destroy(&row_data.uri_ranges.v[i]);
|
|
free(row_data.uri_ranges.v);
|
|
|
|
#undef verify_range
|
|
}
|
|
|
|
static void
|
|
grid_row_range_erase(struct row_ranges *ranges, enum row_range_type type,
|
|
int start, int end)
|
|
{
|
|
xassert(start <= end);
|
|
|
|
/* Split up, or remove, URI ranges affected by the erase */
|
|
for (int i = ranges->count - 1; i >= 0; i--) {
|
|
struct row_range *old = &ranges->v[i];
|
|
|
|
if (old->end < start)
|
|
return;
|
|
|
|
if (old->start > end)
|
|
continue;
|
|
|
|
if (start <= old->start && end >= old->end) {
|
|
/* Erase range covers URI completely - remove it */
|
|
range_delete(ranges, type, i);
|
|
}
|
|
|
|
else if (start > old->start && end < old->end) {
|
|
/*
|
|
* Erase range erases a part in the middle of the URI
|
|
*
|
|
* Must copy, since range_insert() may xrealloc() (thus
|
|
* causing 'old' to be invalid) before it dereferences
|
|
* old->data
|
|
*/
|
|
union row_range_data data = old->data;
|
|
range_insert(ranges, i + 1, end + 1, old->end, type, &data);
|
|
|
|
/* The insertion may xrealloc() the vector, making our
|
|
* 'old' pointer invalid */
|
|
old = &ranges->v[i];
|
|
old->end = start - 1;
|
|
return; /* There can be no more URIs affected by the erase range */
|
|
}
|
|
|
|
else if (start <= old->start && end >= old->start) {
|
|
/* Erase range erases the head of the URI */
|
|
xassert(start <= old->start);
|
|
old->start = end + 1;
|
|
}
|
|
|
|
else if (start <= old->end && end >= old->end) {
|
|
/* Erase range erases the tail of the URI */
|
|
xassert(end >= old->end);
|
|
old->end = start - 1;
|
|
return; /* There can be no more overlapping URIs */
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
grid_row_uri_range_erase(struct row *row, int start, int end)
|
|
{
|
|
xassert(row->extra != NULL);
|
|
grid_row_range_erase(&row->extra->uri_ranges, ROW_RANGE_URI, start, end);
|
|
}
|
|
|
|
void
|
|
grid_row_underline_range_erase(struct row *row, int start, int end)
|
|
{
|
|
xassert(row->extra != NULL);
|
|
grid_row_range_erase(&row->extra->underline_ranges, ROW_RANGE_UNDERLINE, start, end);
|
|
}
|
|
|
|
UNITTEST
|
|
{
|
|
struct row_data row_data = {.uri_ranges = {0}};
|
|
struct row row = {.extra = &row_data};
|
|
const union row_range_data data = {
|
|
.uri = {
|
|
.id = 0,
|
|
.uri = (char *)"dummy",
|
|
},
|
|
};
|
|
|
|
/* Try erasing a row without any URIs */
|
|
grid_row_uri_range_erase(&row, 0, 200);
|
|
xassert(row_data.uri_ranges.count == 0);
|
|
|
|
range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data);
|
|
range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data);
|
|
xassert(row_data.uri_ranges.count == 2);
|
|
xassert(row_data.uri_ranges.v[1].start == 11);
|
|
xassert(row_data.uri_ranges.v[1].end == 20);
|
|
verify_no_overlapping_ranges(&row_data);
|
|
verify_ranges_are_sorted(&row_data);
|
|
|
|
/* Erase both URis */
|
|
grid_row_uri_range_erase(&row, 1, 20);
|
|
xassert(row_data.uri_ranges.count == 0);
|
|
verify_no_overlapping_ranges(&row_data);
|
|
verify_ranges_are_sorted(&row_data);
|
|
|
|
/* Two URIs, then erase second half of the first, first half of
|
|
the second */
|
|
range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data);
|
|
range_append(&row_data.uri_ranges, 11, 20, ROW_RANGE_URI, &data);
|
|
grid_row_uri_range_erase(&row, 5, 15);
|
|
xassert(row_data.uri_ranges.count == 2);
|
|
xassert(row_data.uri_ranges.v[0].start == 1);
|
|
xassert(row_data.uri_ranges.v[0].end == 4);
|
|
xassert(row_data.uri_ranges.v[1].start == 16);
|
|
xassert(row_data.uri_ranges.v[1].end == 20);
|
|
verify_no_overlapping_ranges(&row_data);
|
|
verify_ranges_are_sorted(&row_data);
|
|
|
|
grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI);
|
|
grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI);
|
|
row_data.uri_ranges.count = 0;
|
|
|
|
/* One URI, erase middle part of it */
|
|
range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data);
|
|
grid_row_uri_range_erase(&row, 5, 6);
|
|
xassert(row_data.uri_ranges.count == 2);
|
|
xassert(row_data.uri_ranges.v[0].start == 1);
|
|
xassert(row_data.uri_ranges.v[0].end == 4);
|
|
xassert(row_data.uri_ranges.v[1].start == 7);
|
|
xassert(row_data.uri_ranges.v[1].end == 10);
|
|
verify_no_overlapping_ranges(&row_data);
|
|
verify_ranges_are_sorted(&row_data);
|
|
|
|
grid_row_range_destroy(&row_data.uri_ranges.v[0], ROW_RANGE_URI);
|
|
grid_row_range_destroy(&row_data.uri_ranges.v[1], ROW_RANGE_URI);
|
|
row_data.uri_ranges.count = 0;
|
|
|
|
/*
|
|
* Regression test: erasing the middle part of an URI causes us to
|
|
* insert a new URI (we split the partly erased URI into two).
|
|
*
|
|
* The insertion logic typically triggers an xrealloc(), which, in
|
|
* some cases, *moves* the entire URI vector to a new base
|
|
* address. grid_row_uri_range_erase() did not account for this,
|
|
* and tried to update the 'end' member in the URI range we just
|
|
* split. This causes foot to crash when the xrealloc() has moved
|
|
* the URI range vector.
|
|
*
|
|
* (note: we're only verifying we don't crash here, hence the lack
|
|
* of assertions).
|
|
*/
|
|
free(row_data.uri_ranges.v);
|
|
row_data.uri_ranges.v = NULL;
|
|
row_data.uri_ranges.size = 0;
|
|
range_append(&row_data.uri_ranges, 1, 10, ROW_RANGE_URI, &data);
|
|
xassert(row_data.uri_ranges.size == 1);
|
|
|
|
grid_row_uri_range_erase(&row, 5, 7);
|
|
xassert(row_data.uri_ranges.count == 2);
|
|
|
|
grid_row_ranges_destroy(&row_data.uri_ranges, ROW_RANGE_URI);
|
|
free(row_data.uri_ranges.v);
|
|
}
|