From 047308b156190ac037339e29a018ed814b770832 Mon Sep 17 00:00:00 2001 From: Michael Grant Date: Mon, 10 Aug 2026 12:04:37 +0100 Subject: [PATCH] image: retain ordered placements in the grid Replace the per-cell image marker with sparse placement spans attached to grid lines. A placement owns all of its spans and records the input protocol, application image and placement IDs, z-index, and creation order. This retains overlapping image layers without storing a list in every grid cell. Grid operations move, split, clip, and remove only the affected spans. Use the input protocol to determine image/text interaction: later text damages SIXEL spans, while Kitty placements remain and are ordered by their z-index. Rendering then adapts that one logical scene for each client, rather than changing its semantics according to whether the outer terminal uses Kitty or SIXEL. --- grid.c | 203 ++------- image-kitty.c | 169 +++++-- image-sixel.c | 33 +- image.c | 955 ++++++++++++++++++++++++++++++++------- input.c | 41 +- regress/image-support.sh | 66 ++- screen-redraw.c | 8 +- screen-write.c | 44 +- style.c | 3 - tmux.h | 66 ++- tty-draw.c | 26 +- tty.c | 11 - window-copy.c | 12 +- 13 files changed, 1165 insertions(+), 472 deletions(-) diff --git a/grid.c b/grid.c index 695fea087..f94128514 100644 --- a/grid.c +++ b/grid.c @@ -41,9 +41,6 @@ /* Default grid cell data. */ const struct grid_cell grid_default_cell = { { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 8, 0 -#ifdef ENABLE_IMAGES - , 0, 0, 0 -#endif }; /* @@ -52,38 +49,16 @@ const struct grid_cell grid_default_cell = { */ static const struct grid_cell grid_padding_cell = { { { '!' }, 0, 0, 0 }, 0, GRID_FLAG_PADDING, 8, 8, 8, 0 -#ifdef ENABLE_IMAGES - , 0, 0, 0 -#endif }; /* Cleared grid cell data. */ static const struct grid_cell grid_cleared_cell = { { { ' ' }, 0, 1, 1 }, 0, GRID_FLAG_CLEARED, 8, 8, 8, 0 -#ifdef ENABLE_IMAGES - , 0, 0, 0 -#endif }; static const struct grid_cell_entry grid_cleared_entry = { { .data = { 0, 8, 8, ' ' } }, GRID_FLAG_CLEARED }; -#ifdef ENABLE_IMAGES -/* Return the image referenced by a stored cell, or zero. */ -static u_int -grid_entry_image(struct grid_line *gl, struct grid_cell_entry *gce) -{ - struct grid_extd_entry *gee; - - if (~gce->flags & GRID_FLAG_EXTENDED || gce->offset >= gl->extdsize) - return (0); - gee = &gl->extddata[gce->offset]; - if (~gee->flags & GRID_FLAG_IMAGE) - return (0); - return (image_get_id_by_grid_id(gee->image_id)); -} -#endif - #ifdef __APPLE__ void grid_check_is_clear(struct grid *gd) @@ -111,6 +86,9 @@ grid_check_is_clear(struct grid *gd) assert(gl->extdsize == 0); assert(gl->flags == 0); assert(gl->time == 0); +#ifdef ENABLE_IMAGES + assert(gl->images == NULL); +#endif } } #else @@ -159,10 +137,6 @@ grid_need_extended_cell(const struct grid_cell_entry *gce, return (1); if (gc->flags & GRID_FLAG_TAB) return (1); -#ifdef ENABLE_IMAGES - if (gc->flags & GRID_FLAG_IMAGE) - return (1); -#endif return (0); } @@ -210,11 +184,6 @@ grid_extended_cell(struct grid_line *gl, struct grid_cell_entry *gce, gee->bg = gc->bg; gee->us = gc->us; gee->link = gc->link; -#ifdef ENABLE_IMAGES - gee->image_id = image_get_grid_id(gc->image_id); - gee->image_x = gc->image_x; - gee->image_y = gc->image_y; -#endif return (gee); } @@ -302,12 +271,6 @@ grid_clear_cell(struct grid *gd, u_int px, u_int py, u_int bg, int moved) struct grid_extd_entry *gee; u_int old_offset = gce->offset; int had_extd = (gce->flags & GRID_FLAG_EXTENDED); -#ifdef ENABLE_IMAGES - u_int image_id = grid_entry_image(gl, gce); - - if (!moved && image_id != 0) - image_free(image_id); -#endif memcpy(gce, &grid_cleared_entry, sizeof *gce); if (!moved && had_extd && old_offset < gl->extdsize) { @@ -354,13 +317,6 @@ grid_cells_look_equal(const struct grid_cell *gc1, const struct grid_cell *gc2) return (0); if (gc1->link != gc2->link) return (0); -#ifdef ENABLE_IMAGES - if ((gc1->flags & GRID_FLAG_IMAGE) && - (gc1->image_id != gc2->image_id || - gc1->image_x != gc2->image_x || - gc1->image_y != gc2->image_y)) - return (0); -#endif return (1); } @@ -393,10 +349,6 @@ static void grid_free_line(struct grid *gd, u_int py) { struct grid_line *gl = &gd->linedata[py]; -#ifdef ENABLE_IMAGES - struct grid_cell_entry *gce; - u_int image_id, px; -#endif #ifdef __APPLE__ assert(gl->cellused <= gl->cellsize); @@ -405,12 +357,7 @@ grid_free_line(struct grid *gd, u_int py) #endif #ifdef ENABLE_IMAGES - for (px = 0; px < gl->cellsize; px++) { - gce = &gl->celldata[px]; - image_id = grid_entry_image(gl, gce); - if (image_id != 0) - image_free(image_id); - } + image_grid_free_line(gd, gl); #endif free(gl->celldata); free(gl->extddata); @@ -456,6 +403,9 @@ void grid_destroy(struct grid *gd) { grid_free_lines(gd, 0, gd->hsize + gd->sy); +#ifdef ENABLE_IMAGES + image_grid_free(gd); +#endif free(gd->linedata); free(gd); } @@ -683,11 +633,6 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc) gc->bg = gee->bg; gc->us = gee->us; gc->link = gee->link; -#ifdef ENABLE_IMAGES - gc->image_id = image_get_id_by_grid_id(gee->image_id); - gc->image_x = gee->image_x; - gc->image_y = gee->image_y; -#endif if (gc->flags & GRID_FLAG_TAB) grid_set_tab(gc, gee->data); @@ -708,11 +653,6 @@ grid_get_cell1(struct grid_line *gl, u_int px, struct grid_cell *gc) gc->us = 8; utf8_set(&gc->data, gce->data.data); gc->link = 0; -#ifdef ENABLE_IMAGES - gc->image_id = 0; - gc->image_x = 0; - gc->image_y = 0; -#endif } /* Get cell for reading. */ @@ -732,9 +672,6 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc) { struct grid_line *gl; struct grid_cell_entry *gce; -#ifdef ENABLE_IMAGES - u_int old_id, new_id = 0; -#endif if (grid_check_y(gd, __func__, py) != 0) return; @@ -746,17 +683,6 @@ grid_set_cell(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc) gl->cellused = px + 1; gce = &gl->celldata[px]; -#ifdef ENABLE_IMAGES - old_id = grid_entry_image(gl, gce); - if (gc->flags & GRID_FLAG_IMAGE) - new_id = gc->image_id; - if (old_id != new_id) { - if (old_id != 0) - image_free(old_id); - if (new_id != 0) - image_ref(new_id); - } -#endif if (grid_need_extended_cell(gce, gc)) grid_extended_cell(gl, gce, gc); else @@ -782,10 +708,6 @@ grid_set_cells(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc, struct grid_line *gl; struct grid_cell_entry *gce; struct grid_extd_entry *gee; - const struct grid_cell *new_gc; -#ifdef ENABLE_IMAGES - struct grid_cell old_gc, image_gc; -#endif u_int i; if (grid_check_y(gd, __func__, py) != 0) @@ -799,39 +721,11 @@ grid_set_cells(struct grid *gd, u_int px, u_int py, const struct grid_cell *gc, for (i = 0; i < slen; i++) { gce = &gl->celldata[px + i]; - new_gc = gc; -#ifdef ENABLE_IMAGES - { - u_int old_id = grid_entry_image(gl, gce); - u_int new_id; - - if (old_id != 0 && (~gc->flags & GRID_FLAG_IMAGE)) { - grid_get_cell1(gl, px + i, &old_gc); - memcpy(&image_gc, gc, sizeof image_gc); - image_gc.flags |= GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED; - image_gc.image_id = old_gc.image_id; - image_gc.image_x = old_gc.image_x; - image_gc.image_y = old_gc.image_y; - new_gc = &image_gc; - } - if (new_gc->flags & GRID_FLAG_IMAGE) - new_id = new_gc->image_id; - else - new_id = 0; - - if (old_id != new_id) { - if (old_id != 0) - image_free(old_id); - if (new_id != 0) - image_ref(new_id); - } - } -#endif - if (grid_need_extended_cell(gce, new_gc)) { - gee = grid_extended_cell(gl, gce, new_gc); + if (grid_need_extended_cell(gce, gc)) { + gee = grid_extended_cell(gl, gce, gc); gee->data = utf8_build_one(s[i]); } else - grid_store_cell(gce, new_gc, s[i]); + grid_store_cell(gce, gc, s[i]); } } @@ -844,6 +738,9 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg) if (nx == 0 || ny == 0) return; +#ifdef ENABLE_IMAGES + image_grid_damage(gd, px, py, nx, ny); +#endif if (px == 0 && nx == gd->sx) { grid_clear_lines(gd, py, ny, bg); @@ -879,6 +776,10 @@ grid_clear(struct grid *gd, u_int px, u_int py, u_int nx, u_int ny, u_int bg) void grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg) { + struct grid_line *gl; +#ifdef ENABLE_IMAGES + struct image_line *images; +#endif u_int yy; if (ny == 0) @@ -890,8 +791,18 @@ grid_clear_lines(struct grid *gd, u_int py, u_int ny, u_int bg) return; for (yy = py; yy < py + ny; yy++) { - grid_free_line(gd, yy); + gl = &gd->linedata[yy]; +#ifdef ENABLE_IMAGES + image_grid_damage(gd, 0, yy, gd->sx, 1); + images = gl->images; +#endif + free(gl->celldata); + free(gl->extddata); + memset(gl, 0, sizeof *gl); grid_empty_line(gd, yy, bg); +#ifdef ENABLE_IMAGES + gl->images = images; +#endif } if (py != 0) gd->linedata[py - 1].flags &= ~GRID_LINE_WRAPPED; @@ -946,9 +857,6 @@ grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx, { struct grid_line *gl; u_int xx; -#ifdef ENABLE_IMAGES - u_int image_id; -#endif if (nx == 0 || px == dx) return; @@ -960,14 +868,7 @@ grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, u_int nx, grid_expand_line(gd, py, px + nx, 8); grid_expand_line(gd, py, dx + nx, 8); #ifdef ENABLE_IMAGES - /* References in the source range are transferred, not duplicated. */ - for (xx = dx; xx < dx + nx; xx++) { - if (xx >= px && xx < px + nx) - continue; - image_id = grid_entry_image(gl, &gl->celldata[xx]); - if (image_id != 0) - image_free(image_id); - } + image_grid_move_cells(gd, dx, px, py, nx); #endif memmove(&gl->celldata[dx], &gl->celldata[px], nx * sizeof *gl->celldata); @@ -1341,14 +1242,6 @@ grid_string_cells(struct grid *gd, u_int px, u_int py, u_int nx, grid_get_cell(gd, xx, py, &gc); if (gc.flags & GRID_FLAG_PADDING) continue; -#ifdef ENABLE_IMAGES - if (gc.flags & GRID_FLAG_IMAGE) { - utf8_set(&gc.data, ' '); - gc.flags &= ~(GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED); - gc.image_id = gc.image_x = gc.image_y = 0; - } -#endif - if (lastgc != NULL && (flags & GRID_STRING_WITH_SEQUENCES)) { grid_string_cells_code(*lastgc, &gc, code, sizeof code, flags, s, &has_link); @@ -1416,13 +1309,17 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, struct grid_line *dstl, *srcl; u_int yy; #ifdef ENABLE_IMAGES - u_int px, image_id; + u_int original_dy, original_sy; #endif if (dy + ny > dst->hsize + dst->sy) ny = dst->hsize + dst->sy - dy; if (sy + ny > src->hsize + src->sy) ny = src->hsize + src->sy - sy; +#ifdef ENABLE_IMAGES + original_dy = dy; + original_sy = sy; +#endif grid_free_lines(dst, dy, ny); for (yy = 0; yy < ny; yy++) { @@ -1430,6 +1327,9 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, dstl = &dst->linedata[dy]; memcpy(dstl, srcl, sizeof *dstl); +#ifdef ENABLE_IMAGES + dstl->images = NULL; +#endif if (srcl->cellsize != 0) { dstl->celldata = xreallocarray(NULL, srcl->cellsize, sizeof *dstl->celldata); @@ -1446,16 +1346,12 @@ grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, u_int sy, } else dstl->extddata = NULL; -#ifdef ENABLE_IMAGES - for (px = 0; px < dstl->cellsize; px++) { - image_id = grid_entry_image(dstl, &dstl->celldata[px]); - if (image_id != 0) - image_ref(image_id); - } -#endif sy++; dy++; } +#ifdef ENABLE_IMAGES + image_grid_duplicate_lines(dst, original_dy, src, original_sy, ny); +#endif } /* Mark line as dead. */ @@ -1473,14 +1369,14 @@ grid_reflow_has_image(struct grid_line *gl) struct grid_cell gc; u_int i; +#ifdef ENABLE_IMAGES + if (image_grid_line_has_images(gl)) + return (1); +#endif if (~gl->flags & GRID_LINE_EXTENDED) return (0); for (i = 0; i < gl->cellused; i++) { grid_get_cell1(gl, i, &gc); -#ifdef ENABLE_IMAGES - if (gc.flags & GRID_FLAG_IMAGE) - return (1); -#endif /* Kitty Unicode placeholder base character (U+10EEEE). */ if (gc.data.size >= 4 && gc.data.data[0] == 0xf4 && gc.data.data[1] == 0x8e && gc.data.data[2] == 0xbb && @@ -1672,15 +1568,6 @@ grid_reflow_split(struct grid *target, struct grid *gd, u_int sx, u_int yy, } width += gc.data.width; grid_set_cell(target, xx, line, &gc); -#ifdef ENABLE_IMAGES - /* - * The tail of the original line is discarded below. Transfer - * its image reference to the new cell rather than duplicating - * it. - */ - if (gc.flags & GRID_FLAG_IMAGE) - image_free(gc.image_id); -#endif xx++; } if (flags & GRID_LINE_WRAPPED) @@ -1727,7 +1614,7 @@ grid_reflow(struct grid *gd, u_int sx) if (gl->flags & GRID_LINE_DEAD) continue; - /* Keep image markers at their original cell coordinates. */ + /* Keep image layers at their original cell coordinates. */ if (grid_reflow_has_image(gl)) { grid_reflow_move(target, gl); continue; diff --git a/image-kitty.c b/image-kitty.c index 8a43a8df5..49786ed0a 100644 --- a/image-kitty.c +++ b/image-kitty.c @@ -82,6 +82,7 @@ struct kitty_state { u_int rows; u_int image_id; u_int placement_id; + int32_t z; u_int quiet; int no_cursor; int virtual; @@ -95,6 +96,7 @@ struct kitty_state { struct kitty_placement { u_int placement_id; u_int server_id; + int32_t z; struct kitty_placement *next; }; @@ -256,7 +258,7 @@ kitty_images_collect(struct tty *tty) static void kitty_place(struct tty *tty, struct kitty_image_cache *cache, struct image *im, u_int source_x, u_int source_y, u_int width, - u_int height, u_int destination_x, u_int destination_y) + u_int height, u_int destination_x, u_int destination_y, int32_t z) { char control[192]; u_int px, py, pwidth, pheight, sx, sy; @@ -271,6 +273,9 @@ kitty_place(struct tty *tty, struct kitty_image_cache *cache, sx - px; pheight = ((uint64_t)(source_y + height) * canvas_height + sy - 1) / sy - py; + /* Account for the duplicate-pixel border added by kitty_upload(). */ + px++; + py++; placement = xcalloc(1, sizeof *placement); do { placement->id = ++cache->next_placement; @@ -283,9 +288,10 @@ kitty_place(struct tty *tty, struct kitty_image_cache *cache, cache->placements = placement; tty_cursor(tty, destination_x, destination_y); xsnprintf(control, sizeof control, - "\033_Ga=p,i=%u,p=%llu,x=%u,y=%u,w=%u,h=%u,c=%u,r=%u,C=1," - "q=2\033\\", cache->kitty_id, (unsigned long long)placement->id, px, py, - pwidth, pheight, width, height); + "\033_Ga=p,i=%u,p=%llu,x=%u,y=%u,w=%u,h=%u,c=%u,r=%u,z=%d," + "C=1,q=2\033\\", cache->kitty_id, + (unsigned long long)placement->id, px, py, pwidth, pheight, width, + height, z); tty_puts(tty, control); } @@ -303,6 +309,7 @@ kitty_upload(struct tty *tty, struct image *im) size_t image_size; int encodedlen; u_int id, width, height, canvas_width, canvas_height; + u_int upload_width, upload_height; for (cache = ko->images; cache != NULL; cache = cache->next) { if (cache->server_id != image_get_id(im)) @@ -332,15 +339,36 @@ kitty_upload(struct tty *tty, struct image *im) pixels = image_get_pixels(im, &stride, &image_size); image_get_size(im, &width, &height); image_get_canvas_size(im, &canvas_width, &canvas_height); - if ((uint64_t)canvas_width * canvas_height * 4 > IMAGE_SIZE_LIMIT) + if (canvas_width > UINT_MAX - 2 || canvas_height > UINT_MAX - 2) return (NULL); - padded = xcalloc((size_t)canvas_width * canvas_height, 4); + upload_width = canvas_width + 2; + upload_height = canvas_height + 2; + if ((uint64_t)upload_width * upload_height * 4 > IMAGE_SIZE_LIMIT) + return (NULL); + /* + * Pad the upload with duplicate edge pixels. Kitty linearly filters scaled + * textures against transparent border pixels, which otherwise darkens the + * outermost pixels of an opaque image. + */ + padded = xcalloc((size_t)upload_width * upload_height, 4); for (row = 0; row < height; row++) - memcpy(padded + (size_t)row * canvas_width * 4, + memcpy(padded + ((size_t)(row + 1) * upload_width + 1) * 4, pixels + row * stride, (size_t)width * 4); + for (row = 1; row <= canvas_height; row++) { + memcpy(padded + (size_t)row * upload_width * 4, + padded + ((size_t)row * upload_width + 1) * 4, 4); + memcpy(padded + ((size_t)row * upload_width + upload_width - 1) * 4, + padded + ((size_t)row * upload_width + upload_width - 2) * 4, + 4); + } + memcpy(padded, padded + (size_t)upload_width * 4, + (size_t)upload_width * 4); + memcpy(padded + (size_t)(upload_height - 1) * upload_width * 4, + padded + (size_t)(upload_height - 2) * upload_width * 4, + (size_t)upload_width * 4); pixels = padded; - width = canvas_width; - height = canvas_height; + width = upload_width; + height = upload_height; image_size = (size_t)width * height * 4; stride = (size_t)width * 4; for (offset = 0; offset < image_size; offset += size) { @@ -387,6 +415,7 @@ kitty_draw_rect(struct tty *tty, const struct image_rect *rectangle, struct image *im; u_int source_x, source_y; u_int width, height, destination_x, destination_y; + int32_t z; im = image_rect_get_image(rectangle); kitty_images_collect(tty); @@ -395,8 +424,9 @@ kitty_draw_rect(struct tty *tty, const struct image_rect *rectangle, return; image_rect_get_coords(rectangle, &source_x, &source_y, &width, &height, &destination_x, &destination_y); + z = image_rect_get_z(rectangle); kitty_place(tty, cache, im, source_x, source_y, width, height, - destination_x, destination_y); + destination_x, destination_y, z); } /* Parse an unsigned Kitty graphics control value. */ @@ -418,6 +448,25 @@ kitty_number(const char *s, size_t len, u_int *value) return (0); } +/* Parse a signed Kitty graphics control value. */ +static int +kitty_signed_number(const char *s, size_t len, int32_t *value) +{ + char copy[32]; + const char *errstr; + long long ll; + + if (len == 0 || len >= sizeof copy) + return (-1); + memcpy(copy, s, len); + copy[len] = '\0'; + ll = strtonum(copy, INT32_MIN, INT32_MAX, &errstr); + if (errstr != NULL) + return (-1); + *value = ll; + return (0); +} + /* Parse a Kitty graphics control string. */ static int kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) @@ -456,6 +505,11 @@ kitty_control(struct kitty_state *ks, const u_char *buf, size_t len) case 'o': ks->compression = value[0]; break; + case 'z': + if (kitty_signed_number((const char *)value, valuelen, + &ks->z) != 0) + return (-1); + break; case 'f': case 's': case 'v': @@ -534,6 +588,26 @@ kitty_placements_free_all(struct kitty_context *kc) kitty_placements_free(source); } +/* Remove all parser-side placements at a Kitty z-index. */ +static void +kitty_placements_remove_z(struct kitty_context *kc, int32_t z) +{ + struct kitty_source *source; + struct kitty_placement **pp, *placement; + + for (source = kc->sources; source != NULL; source = source->next) { + for (pp = &source->placements; (placement = *pp) != NULL; ) { + if (placement->z != z) { + pp = &placement->next; + continue; + } + *pp = placement->next; + image_free(placement->server_id); + free(placement); + } + } +} + /* Free Kitty graphics parser state. */ void kitty_free_state(void *state) @@ -600,7 +674,7 @@ kitty_source_set(struct kitty_context *kc, u_int id, struct image *im) /* Associate a placement ID with an image. */ static u_int kitty_placement_set(struct kitty_context *kc, u_int image_id, - u_int placement_id, struct image *im) + u_int placement_id, int32_t z, struct image *im) { struct kitty_source *source; struct kitty_placement *placement; @@ -623,6 +697,7 @@ kitty_placement_set(struct kitty_context *kc, u_int image_id, source->placements = placement; } old_id = placement->server_id; + placement->z = z; image_ref(image_get_id(im)); placement->server_id = image_get_id(im); if (old_id != 0) @@ -864,7 +939,7 @@ kitty_place_image(struct image *source, struct kitty_state *ks, u_int xpixel, struct image * kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, u_int ypixel, u_int *image_id, u_int *replace_id, u_int *quiet, - char *action, int *status) + char *action, char *delete, u_int *placement_id, int32_t *z, int *status) { struct kitty_context *kc = *state; struct kitty_state *ks; @@ -886,6 +961,9 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, *replace_id = 0; *quiet = 0; *action = '\0'; + *delete = '\0'; + *placement_id = 0; + *z = 0; *status = KITTY_PARSE_ERROR; ks = kc->transfer; semi = memchr(buf, ';', len); @@ -902,6 +980,9 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, *image_id = ks->image_id; *quiet = ks->quiet; *action = ks->action; + *delete = ks->delete; + *placement_id = ks->placement_id; + *z = ks->z; if (kitty_control(ks, buf, controllen) != 0 || ks->medium != 'd' || (payloadlen != 0 && @@ -911,6 +992,9 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, *image_id = ks->image_id; *quiet = ks->quiet; *action = ks->action; + *delete = ks->delete; + *placement_id = ks->placement_id; + *z = ks->z; if (ks->more) { kc->transfer = ks; *status = KITTY_PARSE_MORE; @@ -937,7 +1021,7 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, image_free(image_get_id(source)); if (im != NULL) { *replace_id = kitty_placement_set(kc, ks->image_id, - ks->placement_id, im); + ks->placement_id, ks->z, im); *status = KITTY_PARSE_OK; } } @@ -963,7 +1047,10 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, } } else if (ks->delete == 'I') im = kitty_source_remove(kc, ks->image_id); - else + else if (ks->delete == 'z' || ks->delete == 'Z') { + kitty_placements_remove_z(kc, ks->z); + im = NULL; + } else goto fail; if ((ks->delete == 'i' || ks->delete == 'I') && im == NULL) @@ -1040,7 +1127,7 @@ kitty_parse_image(void **state, const u_char *buf, size_t len, u_int xpixel, *status = KITTY_PARSE_ERROR; else if (ks->placement_id != 0) (void)kitty_placement_set(kc, ks->image_id, - ks->placement_id, im); + ks->placement_id, ks->z, im); } else if (ks->virtual) { im = kitty_place_image(source, ks, xpixel, ypixel); if (im == NULL) @@ -1119,10 +1206,11 @@ kitty_placeholder_index(uint32_t value, u_int *index) return (0); } -/* Convert a Kitty Unicode placeholder cell into an image marker. */ +/* Resolve a Kitty Unicode placeholder to an image and source cell. */ int -kitty_placeholder_to_cell(void *state, struct grid_cell *gc, - const struct grid_cell *left) +kitty_placeholder_to_image(void *state, struct grid *gd, struct grid_cell *gc, + u_int grid_x, u_int grid_y, struct image **image, u_int *source_x, + u_int *source_y, u_int *image_id, u_int *placement_id, int32_t *z) { struct kitty_context *kc = state; struct kitty_source *source; @@ -1130,8 +1218,7 @@ kitty_placeholder_to_cell(void *state, struct grid_cell *gc, uint32_t value; size_t offset = 0; u_int values[3], nvalues = 0, id, x, y, sx, sy; - struct utf8_data data; - int fg, bg, us; + u_int left_x, left_y; if (kc == NULL || !kitty_placeholder_character(gc->data.data, gc->data.size, &offset, @@ -1166,29 +1253,33 @@ kitty_placeholder_to_cell(void *state, struct grid_cell *gc, if (nvalues >= 1) y = values[0]; - else if (left != NULL && left->flags & GRID_FLAG_IMAGE && - left->image_id == image_get_id(im)) - y = left->image_y; - else - return (0); + else { + if (grid_x == 0 || !image_grid_get_source(gd, grid_x - 1, + gd->hsize + grid_y, im, &left_x, &left_y)) + return (0); + y = left_y; + } if (nvalues >= 2) x = values[1]; - else if (left != NULL && left->flags & GRID_FLAG_IMAGE && - left->image_id == image_get_id(im) && left->image_x != UINT_MAX) - x = left->image_x + 1; - else - return (0); + else { + if (grid_x == 0 || !image_grid_get_source(gd, grid_x - 1, + gd->hsize + grid_y, im, &left_x, &left_y) || + left_x == UINT_MAX) + return (0); + x = left_x + 1; + } if (x >= sx || y >= sy) return (0); - utf8_copy(&data, &gc->data); - fg = gc->fg; - bg = gc->bg; - us = gc->us; - image_set_cell(gc, im, x, y); - utf8_copy(&gc->data, &data); - gc->fg = fg; - gc->bg = bg; - gc->us = us; + *image = im; + *source_x = x; + *source_y = y; + *image_id = id; + if (gc->us & COLOUR_FLAG_RGB) + *placement_id = gc->us & 0xffffff; + else + *placement_id = 0; + *z = 0; + utf8_set(&gc->data, ' '); return (1); } diff --git a/image-sixel.c b/image-sixel.c index 6a883edea..c82f8c463 100644 --- a/image-sixel.c +++ b/image-sixel.c @@ -603,6 +603,7 @@ sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, { struct sixel_image *new; u_int cx, cy, pox, poy, psx, psy, tsx, tsy, px, py; + uint64_t x0, x1, y0, y1; u_int x, y, i; /* @@ -625,10 +626,19 @@ sixel_scale(struct sixel_image *si, u_int cell_w, u_int cell_h, u_int ox, if (cell_h == 0) cell_h = si->cell_h; - pox = ox * si->cell_w; - poy = oy * si->cell_h; - psx = sx * si->cell_w; - psy = sy * si->cell_h; + /* + * Map cell boundaries over the actual raster, not the rounded-up cell + * canvas. Otherwise a raster shorter than its last cell row produces an + * empty strip when it is scaled for output. + */ + x0 = (uint64_t)ox * si->sx / cx; + x1 = (uint64_t)(ox + sx) * si->sx / cx; + y0 = (uint64_t)oy * si->sy / cy; + y1 = (uint64_t)(oy + sy) * si->sy / cy; + pox = x0; + poy = y0; + psx = x1 - x0; + psy = y1 - y0; tsx = sx * cell_w; tsy = sy * cell_h; @@ -1445,6 +1455,21 @@ sixel_draw_rect(struct tty *tty, const struct image_rect *rectangle, free(data); } +/* Remove old SIXEL pixels before replaying a dirty image area. */ +void +sixel_redraw_start(struct tty *tty, u_int x, u_int y, u_int sx, u_int sy) +{ + u_int yy; + + for (yy = y; yy < y + sy; yy++) { + tty_cursor(tty, x, yy); + if (tty_term_has(tty->term, TTYC_ECH)) + tty_putcode_i(tty, TTYC_ECH, sx); + else + tty_repeat_space(tty, sx); + } +} + /* Convert a SIXEL image to a fallback screen. */ struct screen * sixel_to_screen(struct sixel_image *si) diff --git a/image.c b/image.c index 4b950cd24..cb925e07f 100644 --- a/image.c +++ b/image.c @@ -40,6 +40,7 @@ struct image_sample { #define IMAGE_SAMPLE_COLUMNS 2 #define IMAGE_SAMPLE_ROWS 6 #define IMAGE_FLAG_NO_CURSOR 0x1 +#define IMAGE_Z_BELOW_BACKGROUND (INT32_MIN / 2) struct image_cell { struct image_sample whole; struct image_sample samples[IMAGE_SAMPLE_ROWS][IMAGE_SAMPLE_COLUMNS]; @@ -48,7 +49,6 @@ struct image_cell { /* Immutable image data and cell geometry. */ struct image { u_int id; - u_short grid_id; u_int references; u_int flags; u_int parent_id; @@ -74,18 +74,59 @@ RB_HEAD(images, image); struct image_rect { struct image *image; struct grid_cell cell; + int32_t z; u_int source_x; u_int source_y; - u_int width; - u_int height; + u_int sx; + u_int sy; u_int destination_x; u_int destination_y; }; +/* One contiguous row of a placement in the grid. */ +struct image_span { + u_int x; + u_int sx; + u_int source_x; + u_int source_y; + struct image_line *line; + struct image_placement *placement; + TAILQ_ENTRY(image_span) line_entry; + TAILQ_ENTRY(image_span) placement_entry; +}; +TAILQ_HEAD(image_spans, image_span); + +/* Image spans attached to one grid line. */ +struct image_line { + struct image_spans spans; +}; + +#define IMAGE_INPUT_SIXEL 0 +#define IMAGE_INPUT_KITTY 1 + +/* One logical image placement, shared by all of its row spans. */ +struct image_placement { + struct image_store *store; + struct image *image; + u_int input; + u_int app_image_id; + u_int app_placement_id; + int32_t z; + uint64_t serial; + struct image_spans spans; + TAILQ_ENTRY(image_placement) entry; +}; +TAILQ_HEAD(image_placements, image_placement); + +/* Placements belonging to one grid. */ +struct image_store { + struct grid *grid; + uint64_t next_serial; + struct image_placements placements; +}; + static struct images images = RB_INITIALIZER(&images); static u_int image_next_id; -static u_short image_next_grid_id; -static struct image *image_grid_ids[USHRT_MAX + 1]; struct image_backend { const char *name; @@ -104,7 +145,7 @@ static const struct image_backend image_backend_kitty = { kitty_draw_rect, kitty_free_output, kitty_geometry_changed }; static const struct image_backend image_backend_sixel = { - "sixel", IMAGE_BACKEND_GRAPHICAL|IMAGE_BACKEND_TEMPORAL, + "sixel", IMAGE_BACKEND_GRAPHICAL, sixel_draw_rect, sixel_free_output, sixel_geometry_changed }; @@ -147,6 +188,8 @@ image_redraw_start(struct tty *tty, u_int x, u_int y, u_int width, image_tty_update(tty); if (tty->image_backend == &image_backend_kitty) kitty_redraw_start(tty, x, y, width, height); + else if (tty->image_backend == &image_backend_sixel) + sixel_redraw_start(tty, x, y, width, height); } /* Return the flags for a terminal's image backend. */ @@ -188,6 +231,499 @@ image_cmp(struct image *a, struct image *b) } RB_GENERATE_STATIC(images, image, entry, image_cmp); +/* Return the ordering band for a placement. */ +static int +image_placement_band(const struct image_placement *placement) +{ + if (placement->input == IMAGE_INPUT_SIXEL) + return (1); + if (placement->z < 0) + return (0); + return (2); +} + +/* Compare two placements in logical drawing order. */ +static int +image_placement_cmp(const struct image_placement *a, + const struct image_placement *b) +{ + int aband, bband; + + aband = image_placement_band(a); + bband = image_placement_band(b); + if (aband != bband) + return (aband < bband ? -1 : 1); + if (a->input == IMAGE_INPUT_KITTY && a->z != b->z) + return (a->z < b->z ? -1 : 1); + if (a->input == IMAGE_INPUT_KITTY && + a->app_image_id != b->app_image_id) + return (a->app_image_id < b->app_image_id ? -1 : 1); + if (a->serial != b->serial) + return (a->serial < b->serial ? -1 : 1); + return (0); +} + +/* Allocate the image store for a grid when first needed. */ +static struct image_store * +image_store_get(struct grid *gd) +{ + struct image_store *store = gd->images; + + if (store == NULL) { + store = xcalloc(1, sizeof *store); + store->grid = gd; + TAILQ_INIT(&store->placements); + gd->images = store; + } + return (store); +} + +/* Allocate the image span list for a grid line when first needed. */ +static struct image_line * +image_line_get(struct grid_line *gl) +{ + struct image_line *line = gl->images; + + if (line == NULL) { + line = xcalloc(1, sizeof *line); + TAILQ_INIT(&line->spans); + gl->images = line; + } + return (line); +} + +/* Create a logical image placement. */ +static struct image_placement * +image_placement_create(struct grid *gd, struct image *im, u_int input, + u_int app_image_id, u_int app_placement_id, int32_t z) +{ + struct image_store *store = image_store_get(gd); + struct image_placement *placement; + + placement = xcalloc(1, sizeof *placement); + placement->store = store; + placement->image = im; + placement->input = input; + placement->app_image_id = app_image_id; + placement->app_placement_id = app_placement_id; + placement->z = z; + placement->serial = ++store->next_serial; + if (placement->serial == 0) + placement->serial = ++store->next_serial; + TAILQ_INIT(&placement->spans); + TAILQ_INSERT_TAIL(&store->placements, placement, entry); + image_ref(im->id); + return (placement); +} + +/* Free a placement which no longer has any spans. */ +static void +image_placement_free(struct image_placement *placement) +{ + if (!TAILQ_EMPTY(&placement->spans)) + fatalx("freeing image placement with spans"); + TAILQ_REMOVE(&placement->store->placements, placement, entry); + image_free(placement->image->id); + free(placement); +} + +/* Insert a span into both its line and placement lists. */ +static struct image_span * +image_span_add(struct image_line *line, struct image_placement *placement, + u_int x, u_int width, u_int source_x, u_int source_y) +{ + struct image_span *span, *at; + + if (width == 0) + return (NULL); + span = xcalloc(1, sizeof *span); + span->x = x; + span->sx = width; + span->source_x = source_x; + span->source_y = source_y; + span->line = line; + span->placement = placement; + TAILQ_FOREACH(at, &line->spans, line_entry) { + if (image_placement_cmp(placement, at->placement) < 0 || + (placement == at->placement && x < at->x)) { + TAILQ_INSERT_BEFORE(at, span, line_entry); + goto inserted; + } + } + TAILQ_INSERT_TAIL(&line->spans, span, line_entry); +inserted: + TAILQ_INSERT_TAIL(&placement->spans, span, placement_entry); + return (span); +} + +/* Unlink and free one span without pruning its placement. */ +static void +image_span_free(struct image_span *span) +{ + TAILQ_REMOVE(&span->line->spans, span, line_entry); + TAILQ_REMOVE(&span->placement->spans, span, placement_entry); + free(span); +} + +/* Remove a range from selected spans on a line. */ +static void +image_line_remove(struct image_line *line, u_int x, u_int width, int input) +{ + struct image_span *span, *next; + u_int end, span_end, right; + + if (line == NULL || width == 0) + return; + end = x + width; + if (end < x) + end = UINT_MAX; + TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) { + if (input != -1 && span->placement->input != (u_int)input) + continue; + span_end = span->x + span->sx; + if (span_end <= x || span->x >= end) + continue; + if (span->x < x && span_end > end) { + right = span_end - end; + span->sx = x - span->x; + image_span_add(line, span->placement, end, right, + span->source_x + end - span->x, span->source_y); + continue; + } + if (span->x < x) { + span->sx = x - span->x; + continue; + } + if (span_end > end) { + span->source_x += end - span->x; + span->sx = span_end - end; + span->x = end; + continue; + } + image_span_free(span); + } +} + +/* Remove placement records which have no remaining spans. */ +static void +image_store_prune(struct image_store *store) +{ + struct image_placement *placement, *next; + + if (store == NULL) + return; + TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { + if (TAILQ_EMPTY(&placement->spans)) + image_placement_free(placement); + } +} + +/* Remove temporal image data overwritten by text. */ +void +image_grid_damage(struct grid *gd, u_int x, u_int y, u_int width, + u_int height) +{ + u_int row; + + if (gd->images == NULL || width == 0 || height == 0) + return; + if (y >= gd->hsize + gd->sy) + return; + if (height > gd->hsize + gd->sy - y) + height = gd->hsize + gd->sy - y; + for (row = y; row < y + height; row++) + image_line_remove(gd->linedata[row].images, x, width, + IMAGE_INPUT_SIXEL); + image_store_prune(gd->images); +} + +/* Free all image spans belonging to a grid line. */ +void +image_grid_free_line(struct grid *gd, struct grid_line *gl) +{ + struct image_line *line = gl->images; + struct image_span *span, *next; + + if (line == NULL) + return; + TAILQ_FOREACH_SAFE(span, &line->spans, line_entry, next) + image_span_free(span); + free(line); + gl->images = NULL; + image_store_prune(gd->images); +} + +/* Free the empty image store when a grid is destroyed. */ +void +image_grid_free(struct grid *gd) +{ + if (gd->images == NULL) + return; + image_store_prune(gd->images); + if (!TAILQ_EMPTY(&gd->images->placements)) + fatalx("freeing grid with image placements"); + free(gd->images); + gd->images = NULL; +} + +/* Move image spans with a range of grid cells. */ +void +image_grid_move_cells(struct grid *gd, u_int dx, u_int px, u_int py, + u_int nx) +{ + struct image_line *line; + struct image_span *span; + struct image_move { + struct image_placement *placement; + u_int x, sx, source_x, source_y; + } *moves = NULL; + size_t count = 0; + u_int start, end, span_end; + + if (gd->images == NULL || nx == 0 || px == dx || + py >= gd->hsize + gd->sy) + return; + line = gd->linedata[py].images; + if (line == NULL) + return; + end = px + nx; + TAILQ_FOREACH(span, &line->spans, line_entry) { + span_end = span->x + span->sx; + start = (span->x > px ? span->x : px); + if (start >= end || span_end <= px) + continue; + if (span_end > end) + span_end = end; + moves = xreallocarray(moves, count + 1, sizeof *moves); + moves[count].placement = span->placement; + moves[count].x = dx + start - px; + moves[count].sx = span_end - start; + moves[count].source_x = span->source_x + start - span->x; + moves[count].source_y = span->source_y; + count++; + } + image_line_remove(line, px, nx, -1); + image_line_remove(line, dx, nx, -1); + for (size_t i = 0; i < count; i++) + image_span_add(line, moves[i].placement, moves[i].x, + moves[i].sx, moves[i].source_x, moves[i].source_y); + free(moves); + image_store_prune(gd->images); +} + +/* Duplicate image spans alongside a group of grid lines. */ +void +image_grid_duplicate_lines(struct grid *dst, u_int dy, struct grid *src, + u_int sy, u_int ny) +{ + struct image_map { + struct image_placement *source; + struct image_placement *destination; + } *maps = NULL; + struct image_placement *placement; + struct image_line *source_line, *destination_line; + struct image_span *span; + size_t count = 0, i; + u_int row; + + for (row = 0; row < ny; row++) { + source_line = src->linedata[sy + row].images; + if (source_line == NULL) + continue; + destination_line = image_line_get(&dst->linedata[dy + row]); + TAILQ_FOREACH(span, &source_line->spans, line_entry) { + placement = NULL; + for (i = 0; i < count; i++) { + if (maps[i].source == span->placement) { + placement = maps[i].destination; + break; + } + } + if (placement == NULL) { + placement = image_placement_create(dst, + span->placement->image, span->placement->input, + span->placement->app_image_id, + span->placement->app_placement_id, + span->placement->z); + maps = xreallocarray(maps, count + 1, sizeof *maps); + maps[count].source = span->placement; + maps[count].destination = placement; + count++; + } + image_span_add(destination_line, placement, span->x, + span->sx, span->source_x, span->source_y); + } + } + free(maps); +} + +/* Copy clipped image spans between grid areas. */ +void +image_grid_copy_area(struct grid *dst, u_int destination_x, + u_int destination_y, struct grid *src, u_int source_x, u_int source_y, + u_int sx, u_int sy) +{ + struct image_map { + struct image_placement *source; + struct image_placement *destination; + } *maps = NULL; + struct image_placement *placement; + struct image_line *source_line, *destination_line; + struct image_span *span; + size_t count = 0, i; + u_int row, start, end, span_end; + + if (dst == src || sx == 0 || sy == 0) + return; + if (destination_y >= dst->hsize + dst->sy || + source_y >= src->hsize + src->sy) + return; + if (sy > dst->hsize + dst->sy - destination_y) + sy = dst->hsize + dst->sy - destination_y; + if (sy > src->hsize + src->sy - source_y) + sy = src->hsize + src->sy - source_y; + end = source_x + sx; + if (end < source_x) + end = UINT_MAX; + + for (row = 0; row < sy; row++) { + source_line = src->linedata[source_y + row].images; + if (source_line == NULL) + continue; + destination_line = image_line_get( + &dst->linedata[destination_y + row]); + TAILQ_FOREACH(span, &source_line->spans, line_entry) { + span_end = span->x + span->sx; + start = (span->x > source_x ? span->x : source_x); + if (start >= end || span_end <= source_x) + continue; + if (span_end > end) + span_end = end; + + placement = NULL; + for (i = 0; i < count; i++) { + if (maps[i].source == span->placement) { + placement = maps[i].destination; + break; + } + } + if (placement == NULL) { + placement = image_placement_create(dst, + span->placement->image, span->placement->input, + span->placement->app_image_id, + span->placement->app_placement_id, + span->placement->z); + maps = xreallocarray(maps, count + 1, + sizeof *maps); + maps[count].source = span->placement; + maps[count].destination = placement; + count++; + } + image_span_add(destination_line, placement, + destination_x + start - source_x, span_end - start, + span->source_x + start - span->x, span->source_y); + } + } + free(maps); +} + +/* Return whether a grid line contains any image spans. */ +int +image_grid_line_has_images(const struct grid_line *gl) +{ + return (gl->images != NULL && !TAILQ_EMPTY(&gl->images->spans)); +} + +/* Return whether a grid rectangle contains any image spans. */ +int +image_grid_check_area(struct grid *gd, u_int x, u_int y, u_int width, + u_int height) +{ + struct image_line *line; + struct image_span *span; + u_int row, end; + + if (gd->images == NULL || width == 0 || height == 0 || + y >= gd->hsize + gd->sy) + return (0); + end = x + width; + if (height > gd->hsize + gd->sy - y) + height = gd->hsize + gd->sy - y; + for (row = y; row < y + height; row++) { + line = gd->linedata[row].images; + if (line == NULL) + continue; + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (span->x < end && span->x + span->sx > x) + return (1); + } + } + return (0); +} + +/* Find source coordinates for an image span at one grid cell. */ +int +image_grid_get_source(struct grid *gd, u_int x, u_int y, struct image *im, + u_int *source_x, u_int *source_y) +{ + struct image_line *line; + struct image_span *span, *found = NULL; + + if (y >= gd->hsize + gd->sy || + (line = gd->linedata[y].images) == NULL) + return (0); + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (span->placement->image == im && x >= span->x && + x < span->x + span->sx) + found = span; + } + if (found == NULL) + return (0); + *source_x = found->source_x + x - found->x; + *source_y = found->source_y; + return (1); +} + +/* Add one Kitty Unicode-placeholder cell to an image placement. */ +void +image_place_cell_kitty(struct screen_write_ctx *ctx, struct image *im, + u_int x, u_int y, u_int source_x, u_int source_y, u_int image_id, + u_int placement_id, int32_t z) +{ + struct grid *gd = ctx->s->grid; + struct image_store *store = image_store_get(gd); + struct image_placement *placement = NULL, *candidate; + struct image_line *line; + struct image_span *span; + + TAILQ_FOREACH_REVERSE(candidate, &store->placements, + image_placements, entry) { + if (candidate->input == IMAGE_INPUT_KITTY && + candidate->image == im && + candidate->app_image_id == image_id && + candidate->app_placement_id == placement_id && + candidate->z == z) { + placement = candidate; + break; + } + } + if (placement == NULL) + placement = image_placement_create(gd, im, IMAGE_INPUT_KITTY, + image_id, placement_id, z); + line = image_line_get(&gd->linedata[gd->hsize + y]); + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (span->placement == placement && span->x + span->sx == x && + span->source_y == source_y && + span->source_x + span->sx == source_x) { + span->sx++; + image_redraw_area(ctx, x, y, 1, 1); + return; + } + } + image_span_add(line, placement, x, 1, source_x, source_y); + image_redraw_area(ctx, x, y, 1, 1); +} + /* Average a rectangle of image pixels into one fallback sample. */ static void image_sample(struct image *im, uint64_t sample_x, uint64_t sample_y, @@ -369,28 +905,25 @@ image_rect_get_coords(const struct image_rect *rectangle, { *source_x = rectangle->source_x; *source_y = rectangle->source_y; - *width = rectangle->width; - *height = rectangle->height; + *width = rectangle->sx; + *height = rectangle->sy; *destination_x = rectangle->destination_x; *destination_y = rectangle->destination_y; } +/* Return the output z-index for a drawing rectangle. */ +int32_t +image_rect_get_z(const struct image_rect *rectangle) +{ + return (rectangle->z); +} + /* Create and register an immutable image. */ static struct image * image_create1(u_int width, u_int height, u_int canvas_width, u_int canvas_height, u_int sx, u_int sy, size_t stride, u_char *pixels) { struct image *im; - u_int i; - - for (i = 0; i < USHRT_MAX; i++) { - if (++image_next_grid_id == 0) - image_next_grid_id++; - if (image_grid_ids[image_next_grid_id] == NULL) - break; - } - if (i == USHRT_MAX) - return (NULL); im = xcalloc(1, sizeof *im); do { @@ -400,7 +933,6 @@ image_create1(u_int width, u_int height, u_int canvas_width, } while (image_find(im->id) != NULL); im->references = 1; - im->grid_id = image_next_grid_id; im->source_id = im->id; im->width = width; im->height = height; @@ -413,7 +945,6 @@ image_create1(u_int width, u_int height, u_int canvas_width, im->pixels = pixels; RB_INSERT(images, &images, im); - image_grid_ids[im->grid_id] = im; log_debug("%s: image %u is %ux%u pixels on %ux%u canvas, " "%ux%u cells", __func__, im->id, width, height, canvas_width, canvas_height, sx, sy); @@ -468,28 +999,6 @@ image_create_view(struct image *source, u_int x, u_int y, u_int width, return (im); } -/* Return the compact grid ID for an image. */ -u_short -image_get_grid_id(u_int id) -{ - struct image *im = image_find(id); - - if (im == NULL) - return (0); - return (im->grid_id); -} - -/* Return the server image ID for a compact grid ID. */ -u_int -image_get_id_by_grid_id(u_short grid_id) -{ - struct image *im; - - if (grid_id == 0 || (im = image_grid_ids[grid_id]) == NULL) - return (0); - return (im->id); -} - /* Add a reference to an image. */ void image_ref(u_int id) @@ -516,7 +1025,6 @@ image_free(u_int id) log_debug("%s: freeing image %u", __func__, id); RB_REMOVE(images, &images, im); - image_grid_ids[im->grid_id] = NULL; if (im->parent_id == 0) free(im->pixels); else @@ -538,7 +1046,6 @@ image_get_cell(struct image *im, u_int x, u_int y) return (&im->cells[(size_t)y * im->sx + x]); } -/* Character-cell fallback for clients without a graphical image protocol. */ /* Fill a grid cell with a fallback image glyph. */ void image_get_fallback_cell(__unused struct tty *tty, struct image *im, u_int x, @@ -554,45 +1061,44 @@ image_get_fallback_cell(__unused struct tty *tty, struct image *im, u_int x, if (cell != NULL) level = cell->whole.brightness * (sizeof ramp - 2) / 255; utf8_set(&out->data, ramp[level]); - out->flags &= ~(GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED); } -/* Get the terminal cell used to draw an image marker. */ +/* Return one for a fallback cell, minus one to continue along an image line. */ int -image_get_draw_cell(struct tty *tty, const struct grid_cell *gc, - struct grid_cell *out, const struct tty_style_ctx *style_ctx) +image_get_fallback_at(struct tty *tty, struct screen *s, u_int x, u_int y, + const struct grid_cell *gc, struct grid_cell *out, + const struct tty_style_ctx *style_ctx) { - struct image *im = image_find(gc->image_id); + struct image_line *line; + struct image_span *span, *found = NULL; + struct image_placement *placement; - if (image_backend_flags(tty) & IMAGE_BACKEND_GRAPHICAL) { - memcpy(out, gc, sizeof *out); - out->flags &= ~(GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED| - GRID_FLAG_SELECTED); - return (1); - } - if (gc->flags & GRID_FLAG_IMAGE_DAMAGED) { - memcpy(out, gc, sizeof *out); - out->flags &= ~(GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED); + if (image_backend_flags(tty) & IMAGE_BACKEND_GRAPHICAL || + y >= s->grid->sy) return (0); + line = s->grid->linedata[s->grid->hsize + y].images; + if (line == NULL) + return (0); + TAILQ_FOREACH(span, &line->spans, line_entry) { + if (x >= span->x && x < span->x + span->sx) + found = span; } - image_get_fallback_cell(tty, im, gc->image_x, gc->image_y, gc, out, + if (found == NULL) + return (-1); + placement = found->placement; + if (placement->input == IMAGE_INPUT_KITTY && placement->z < 0) { + if (gc->data.size != 1 || gc->data.data[0] != ' ') + return (-1); + if (placement->z < IMAGE_Z_BELOW_BACKGROUND && + !COLOUR_DEFAULT(gc->bg)) + return (-1); + } + image_get_fallback_cell(tty, placement->image, + found->source_x + x - found->x, found->source_y, gc, out, style_ctx); - return (0); + return (1); } -/* Store an image marker in a grid cell. */ -void -image_set_cell(struct grid_cell *gc, struct image *im, u_int x, u_int y) -{ - /* Keep the cell contents as the underlay for transparent pixels. */ - gc->flags &= ~GRID_FLAG_IMAGE_DAMAGED; - gc->flags |= GRID_FLAG_IMAGE; - gc->image_id = im->id; - gc->image_x = x; - gc->image_y = y; -} - -/* Convert a cell-aligned image rectangle into source pixel coordinates. */ /* Convert an image cell rectangle to pixel coordinates. */ void image_get_pixel_rect(const struct image *im, u_int x, u_int y, @@ -711,73 +1217,85 @@ image_png_decode(const u_char *data, size_t size, size_t limit, u_int *width, return (pixels); } -/* Clear image markers with an image ID from a screen. */ +/* Remove one placement and all of its spans. */ +static void +image_remove_placement(struct image_placement *placement) +{ + struct image_span *span, *next; + + TAILQ_FOREACH_SAFE(span, &placement->spans, placement_entry, next) + image_span_free(span); + image_placement_free(placement); +} + +/* Clear image placements with an internal image ID from a screen. */ void image_clear(struct screen_write_ctx *ctx, u_int id) { - struct screen *s = ctx->s; - struct grid *gd = s->grid; - struct grid_cell gc; - struct image *im; - u_int x, y; + struct image_store *store = ctx->s->grid->images; + struct image_placement *placement, *next; - for (y = 0; y < gd->hsize + gd->sy; y++) { - for (x = 0; x < gd->sx; x++) { - grid_get_cell(gd, x, y, &gc); - im = NULL; - if (gc.flags & GRID_FLAG_IMAGE) - im = image_find(gc.image_id); - if (im != NULL && (id == 0 || gc.image_id == id || - im->source_id == id)) { - if (y >= gd->hsize) - image_redraw_area(ctx, x, y - gd->hsize, - 1, 1); - gc.flags &= ~(GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED); - gc.image_id = gc.image_x = gc.image_y = 0; - grid_set_cell(gd, x, y, &gc); - } - } + if (store == NULL) + return; + TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { + if (id != 0 && placement->image->id != id && + placement->image->source_id != id) + continue; + image_remove_placement(placement); } + if (ctx->wp != NULL) + ctx->wp->flags |= PANE_REDRAW; } -/* Return if a screen area contains image markers. */ -static int -image_check_area(struct screen *s, u_int px, u_int py, u_int nx, u_int ny) +/* Clear Kitty placements selected by application identity or z-index. */ +void +image_clear_kitty(struct screen_write_ctx *ctx, char how, u_int image_id, + u_int placement_id, int32_t z) { - struct grid_cell gc; - u_int x, y, ex, ey; - u_int sx = screen_size_x(s), sy = screen_size_y(s); + struct image_store *store = ctx->s->grid->images; + struct image_placement *placement, *next; + int matched; - if (px >= sx || py >= sy || nx == 0 || ny == 0) - return (0); - if (nx > sx - px) - ex = sx; - else - ex = px + nx; - if (ny > sy - py) - ey = sy; - else - ey = py + ny; - for (y = py; y < ey; y++) { - for (x = px; x < ex; x++) { - grid_view_get_cell(s->grid, x, y, &gc); - if (gc.flags & GRID_FLAG_IMAGE) - return (1); + if (store == NULL) + return; + TAILQ_FOREACH_SAFE(placement, &store->placements, entry, next) { + if (placement->input != IMAGE_INPUT_KITTY) + continue; + matched = 0; + switch (how) { + case 'a': case 'A': + matched = 1; + break; + case 'i': + matched = (placement->app_image_id == image_id && + (placement_id == 0 || + placement->app_placement_id == placement_id)); + break; + case 'I': + matched = (placement->app_image_id == image_id); + break; + case 'z': case 'Z': + matched = (placement->z == z); + break; } + if (matched) + image_remove_placement(placement); } - return (0); + if (ctx->wp != NULL) + ctx->wp->flags |= PANE_REDRAW; } -/* Redraw image markers in a screen area. */ +/* Redraw image layers in a screen area. */ void image_redraw_area(struct screen_write_ctx *ctx, u_int px, u_int py, u_int nx, u_int ny) { - if (ctx->wp != NULL && image_check_area(ctx->s, px, py, nx, ny)) + if (ctx->wp != NULL && image_grid_check_area(ctx->s->grid, px, + ctx->s->grid->hsize + py, nx, ny)) ctx->wp->flags |= PANE_REDRAW; } -/* Redraw all image markers on a screen. */ +/* Redraw all image layers on a screen. */ void image_redraw_all(struct screen_write_ctx *ctx) { @@ -792,80 +1310,140 @@ image_redraw_scroll(struct screen_write_ctx *ctx, __unused u_int lines) image_redraw_all(ctx); } -/* Draw a span's graphical image markers before or after its text. */ +/* Draw a clipped part of one image span. */ +static void +image_draw_span(const struct image_backend *backend, struct tty *tty, + struct screen *s, struct image_span *span, u_int start, u_int end, + u_int px, u_int py, u_int atx, u_int aty, + const struct tty_style_ctx *style_ctx) +{ + struct image_placement *placement = span->placement; + struct image_rect rectangle; + + rectangle.image = placement->image; + grid_view_get_cell(s->grid, start, py, &rectangle.cell); + if (placement->input == IMAGE_INPUT_SIXEL) + rectangle.z = 0; + else if (placement->z >= 0 && placement->z < INT32_MAX) + rectangle.z = placement->z + 1; + else + rectangle.z = placement->z; + rectangle.source_x = span->source_x + start - span->x; + rectangle.source_y = span->source_y; + rectangle.sx = end - start; + rectangle.sy = 1; + rectangle.destination_x = atx + start - px; + rectangle.destination_y = aty; + backend->draw_rect(tty, &rectangle, style_ctx); +} + +/* Return whether a cell contains a glyph or text decoration. */ +static int +image_cell_has_text(struct grid *gd, u_int x, u_int y) +{ + struct grid_cell gc; + + grid_view_get_cell(gd, x, y, &gc); + if (gc.data.size != 1 || gc.data.data[0] != ' ') + return (1); + return (gc.attr != 0); +} + +/* Draw a span's graphical image layers before or after its text. */ void image_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, u_int atx, u_int aty, int before, const struct tty_style_ctx *style_ctx) { const struct image_backend *backend; - struct image_rect rectangle; - struct grid_cell gc, next; - struct image *im; - u_int i, run; + struct image_line *line; + struct image_span *span; + struct image_placement *placement; + u_int start, end, span_end, draw_end; + int blank_only; image_tty_update(tty); backend = tty->image_backend; if (~backend->flags & IMAGE_BACKEND_GRAPHICAL) return; - if (before && (~backend->flags & IMAGE_BACKEND_TEMPORAL)) + if (py >= s->grid->sy) return; - - for (i = 0; i < nx; i += run) { - grid_view_get_cell(s->grid, px + i, py, &gc); - if (~gc.flags & GRID_FLAG_IMAGE) { - run = 1; - continue; - } - if (backend->flags & IMAGE_BACKEND_TEMPORAL) { - if ((before && (~gc.flags & GRID_FLAG_IMAGE_DAMAGED)) || - (!before && (gc.flags & GRID_FLAG_IMAGE_DAMAGED))) { - run = 1; + line = s->grid->linedata[s->grid->hsize + py].images; + if (line == NULL) + return; + end = px + nx; + TAILQ_FOREACH(span, &line->spans, line_entry) { + placement = span->placement; + blank_only = 0; + if (placement->input == IMAGE_INPUT_KITTY && + placement->z < 0) { + if (backend == &image_backend_sixel && + placement->z >= IMAGE_Z_BELOW_BACKGROUND) { + if (before) + continue; + blank_only = 1; + } else if (!before) continue; - } - } - im = image_find(gc.image_id); - if (im == NULL) { - run = 1; + } else if (before) + continue; + span_end = span->x + span->sx; + start = (span->x > px ? span->x : px); + if (start >= end || span_end <= px) + continue; + if (span_end > end) + span_end = end; + if (!blank_only) { + image_draw_span(backend, tty, s, span, start, span_end, + px, py, atx, aty, style_ctx); continue; } - for (run = 1; i + run < nx; run++) { - grid_view_get_cell(s->grid, px + i + run, py, &next); - if (~next.flags & GRID_FLAG_IMAGE) - break; - if ((backend->flags & IMAGE_BACKEND_TEMPORAL) && - ((before && (~next.flags & GRID_FLAG_IMAGE_DAMAGED)) || - (!before && (next.flags & GRID_FLAG_IMAGE_DAMAGED)))) - break; - if (next.image_id != gc.image_id || - next.image_y != gc.image_y || - next.image_x != gc.image_x + run) - break; + while (start < span_end) { + while (start < span_end && + image_cell_has_text(s->grid, start, py)) + start++; + draw_end = start; + while (draw_end < span_end && + !image_cell_has_text(s->grid, draw_end, py)) + draw_end++; + if (start < draw_end) + image_draw_span(backend, tty, s, span, start, + draw_end, px, py, atx, aty, style_ctx); + start = draw_end; } - rectangle.image = im; - memcpy(&rectangle.cell, &gc, sizeof rectangle.cell); - rectangle.source_x = gc.image_x; - rectangle.source_y = gc.image_y; - rectangle.width = run; - rectangle.height = 1; - rectangle.destination_x = atx + i; - rectangle.destination_y = aty; - backend->draw_rect(tty, &rectangle, style_ctx); } } -/* - * Put image marker cells at the cursor. The pixel object is immutable; only - * the marker rectangle is clipped to the available pane cells. - */ -void -image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg) +/* Return whether an image cell contains at least one nontransparent pixel. */ +static int +image_cell_has_alpha(struct image *im, u_int x, u_int y) +{ + u_int px, py, sx, sy, xx, yy; + const u_char *pixels; + + image_get_pixel_rect(im, x, y, 1, 1, &px, &py, &sx, &sy); + if (sx == 0 || sy == 0) + return (0); + pixels = im->pixels; + for (yy = py; yy < py + sy; yy++) { + for (xx = px; xx < px + sx; xx++) { + if (pixels[(size_t)yy * im->stride + (size_t)xx * 4 + 3] != 0) + return (1); + } + } + return (0); +} + +/* Place an image at the cursor using the supplied input semantics. */ +static void +image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg, + u_int input, u_int app_image_id, u_int app_placement_id, int32_t z) { struct screen *s = ctx->s; struct grid *gd = s->grid; - struct grid_cell gc; + struct image_placement *placement; + struct image_line *line; u_int cx = s->cx, cy = s->cy; - u_int x, y, sx, sy, lines, origin_y = 0; + u_int x, y, run, sx, sy, lines, origin_y = 0; sx = im->sx; if (sx > screen_size_x(s) - cx) @@ -889,14 +1467,41 @@ image_write(struct screen_write_ctx *ctx, struct image *im, u_int bg) sy -= origin_y; } + placement = image_placement_create(gd, im, input, app_image_id, + app_placement_id, z); for (y = 0; y < sy; y++) { - for (x = 0; x < sx; x++) { - grid_view_get_cell(gd, cx + x, cy + y, &gc); - image_set_cell(&gc, im, x, origin_y + y); - grid_view_set_cell(gd, cx + x, cy + y, &gc); + line = image_line_get(&gd->linedata[gd->hsize + cy + y]); + for (x = 0; x < sx; x += run) { + if (!image_cell_has_alpha(im, x, origin_y + y)) { + run = 1; + continue; + } + for (run = 1; x + run < sx; run++) { + if (!image_cell_has_alpha(im, x + run, + origin_y + y)) + break; + } + image_span_add(line, placement, cx + x, run, x, + origin_y + y); } } + image_store_prune(gd->images); image_redraw_area(ctx, cx, cy, sx, sy); if (!(im->flags & IMAGE_FLAG_NO_CURSOR)) screen_write_cursormove(ctx, 0, cy + sy, 0); } + +/* Place an image received through SIXEL. */ +void +image_write_sixel(struct screen_write_ctx *ctx, struct image *im, u_int bg) +{ + image_write(ctx, im, bg, IMAGE_INPUT_SIXEL, 0, 0, 0); +} + +/* Place an image received through the Kitty graphics protocol. */ +void +image_write_kitty(struct screen_write_ctx *ctx, struct image *im, u_int bg, + u_int image_id, u_int placement_id, int32_t z) +{ + image_write(ctx, im, bg, IMAGE_INPUT_KITTY, image_id, placement_id, z); +} diff --git a/input.c b/input.c index 6565fed47..9acf11a1b 100644 --- a/input.c +++ b/input.c @@ -947,6 +947,7 @@ input_reset(struct input_ctx *ictx, int clear) screen_write_start_pane(sctx, wp, &wp->base); else screen_write_start(sctx, &wp->base); + sctx->flags |= SCREEN_WRITE_INPUT; screen_write_reset(sctx); screen_write_stop(sctx); } @@ -1074,6 +1075,7 @@ input_parse_buffer(struct window_pane *wp, const u_char *buf, size_t len) screen_write_start_pane(sctx, wp, &wp->base); else screen_write_start(sctx, &wp->base); + sctx->flags |= SCREEN_WRITE_INPUT; log_debug("%s: %%%u %s, %zu bytes: %.*s", __func__, wp->id, ictx->state->name, len, (int)len, buf); @@ -1093,6 +1095,7 @@ input_parse_screen(struct input_ctx *ictx, struct screen *s, return; screen_write_start_callback(sctx, s, cb, arg); + sctx->flags |= SCREEN_WRITE_INPUT; input_parse(ictx, buf, len); screen_write_stop(sctx); } @@ -2809,7 +2812,9 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len) struct window_pane *wp = ictx->wp; struct image *im; u_int image_id = 0, replace_id = 0, quiet = 0; - char action = '\0'; + u_int placement_id = 0; + int32_t z = 0; + char action = '\0', delete = '\0'; int status; if (wp == NULL) @@ -2817,7 +2822,7 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len) im = kitty_parse_image(&ictx->kitty_state, buf, len, wp->window->xpixel, wp->window->ypixel, &image_id, &replace_id, &quiet, &action, - &status); + &delete, &placement_id, &z, &status); if (status == KITTY_PARSE_MORE) return (1); if (status != KITTY_PARSE_OK) { @@ -2835,14 +2840,15 @@ input_handle_kitty(struct input_ctx *ictx, const u_char *buf, size_t len) image_clear(sctx, replace_id); if (im != NULL) { if (action == 'd') - image_clear(sctx, image_get_id(im)); + image_clear_kitty(sctx, delete, image_id, placement_id, z); else - image_write(sctx, im, ictx->cell.cell.bg); + image_write_kitty(sctx, im, ictx->cell.cell.bg, image_id, + placement_id, z); image_free(image_get_id(im)); if (quiet == 0 && image_id != 0) input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id); - } else if (action == 'd' && image_id == 0) - image_clear(sctx, 0); + } else if (action == 'd') + image_clear_kitty(sctx, delete, image_id, placement_id, z); else if ((action == 't' || action == 'q' || action == 'u') && quiet == 0) input_reply(ictx, 0, "\033_Gi=%u;OK\033\\", image_id); return (1); @@ -2927,9 +2933,10 @@ input_top_bit_set(struct input_ctx *ictx) struct screen_write_ctx *sctx = &ictx->ctx; struct utf8_data *ud = &ictx->utf8data; #ifdef ENABLE_IMAGES - struct grid_cell gc, left; - const struct grid_cell *gcl; - u_int x; + struct grid_cell gc; + struct image *im; + u_int x, source_x, source_y, image_id, placement_id; + int32_t z; #endif ictx->flags &= ~INPUT_LAST; @@ -2962,18 +2969,12 @@ input_top_bit_set(struct input_ctx *ictx) if (sctx->s->cx != 0) { x = sctx->s->cx - 1; /* cx-1 is the cell just written. */ grid_view_get_cell(sctx->s->grid, x, sctx->s->cy, &gc); - /* The preceding cell provides context for implicit coordinates. */ - if (x == 0) { - gcl = NULL; - } else { - grid_view_get_cell(sctx->s->grid, x - 1, sctx->s->cy, &left); - gcl = &left; - } - - /* Convert a recognized Kitty placeholder in gc to an image marker. */ - if (kitty_placeholder_to_cell(ictx->kitty_state, &gc, gcl)) { + if (kitty_placeholder_to_image(ictx->kitty_state, + sctx->s->grid, &gc, x, sctx->s->cy, &im, &source_x, + &source_y, &image_id, &placement_id, &z)) { grid_view_set_cell(sctx->s->grid, x, sctx->s->cy, &gc); - image_redraw_area(sctx, x, sctx->s->cy, 1, 1); + image_place_cell_kitty(sctx, im, x, sctx->s->cy, source_x, + source_y, image_id, placement_id, z); } } #endif diff --git a/regress/image-support.sh b/regress/image-support.sh index f71286790..de81a15ed 100755 --- a/regress/image-support.sh +++ b/regress/image-support.sh @@ -1,6 +1,6 @@ #!/bin/sh -# Grid-resident image markers: Kitty input, history, copy and overwrite. +# Grid-resident image layers: Kitty input, history, copy and overwrite. PATH=/bin:/usr/bin TERM=screen @@ -30,7 +30,7 @@ sleep 1 $TMUX capture-pane -pS- >$TMP || exit 1 grep '_G' $TMP >/dev/null && exit 1 -# Copy mode duplicates the backing grid, including image marker references. +# Copy mode duplicates the backing grid, including image layers. $TMUX copy-mode || exit 1 $TMUX send-keys -X history-top || exit 1 $TMUX capture-pane -p >$TMP || exit 1 @@ -63,7 +63,7 @@ $TMUX new-window -d " sleep 1 [ "$($TMUX display-message -pt:3 '#{cursor_y}')" = 1 ] || exit 1 -# SIXEL input reaches the same grid marker and copy-mode paths. +# SIXEL input reaches the same grid layer and copy-mode paths. $TMUX new-window -d "cat '$FIXTURE'; sleep 10" sleep 1 [ "$($TMUX display-message -pt:4 '#{cursor_y}')" -gt 0 ] || exit 1 @@ -145,7 +145,7 @@ $TMUX capture-pane -pS0 -E3 >$TMP || exit 1 [ "$(sed -n 2p $TMP)" = "*" ] || exit 1 [ "$(sed -n 3p $TMP)" = "@" ] || exit 1 -# Image marker rows remain cell-aligned when a narrower terminal causes text +# Image rows remain cell-aligned when a narrower terminal causes text # reflow. The ten-column rows are clipped to five columns, not split into four # wrapped rows. $TMUX2 new-window -d " @@ -225,7 +225,7 @@ $TMUX capture-pane -pS0 -E3 >$TMP || exit 1 [ -z "$(sed -n 2p $TMP)" ] || exit 1 [ "$(sed -n 4p $TMP)" = " @@" ] || exit 1 -# Image markers retain the cells beneath them. Deleting a transparent image +# Image layers retain the cells beneath them. Deleting a transparent image # must reveal the original text rather than replacing it with spaces. PLACEMENT_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' " printf 'XY' @@ -259,6 +259,44 @@ $TMUX capture-pane -pS0 -E1 >$TMP || exit 1 [ "$(sed -n 1p $TMP)" = "test" ] || exit 1 [ "$(sed -n 2p $TMP)" = "test" ] || exit 1 +# Kitty z-indexes and SIXEL's temporal plane are properties of the input, not +# of the outer terminal. Exercise the logical scene through the fallback +# backend: higher nonnegative z-indexes cover lower ones and text, ordinary +# negative z-indexes cover backgrounds but not text, and very negative ones +# remain below the background. +Z_WINDOW=$($TMUX2 new-window -dP -F '#{window_id}' " + printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=21;/wAA/w==\033\\' + printf '\033_Ga=t,q=2,f=32,s=1,v=1,i=22;/////w==\033\\' + printf 'X\r' + printf '\033_Ga=p,q=2,C=1,i=21,p=1,z=2,c=1,r=1\033\\' + printf '\033_Ga=p,q=2,C=1,i=22,p=2,z=1,c=1,r=1\033\\' + printf '\033[2;1H\033[41m \033[0m\r' + printf '\033_Ga=p,q=2,C=1,i=21,p=3,z=-1,c=1,r=1\033\\' + printf '\033[3;1HY\r' + printf '\033_Ga=p,q=2,C=1,i=21,p=4,z=-1,c=1,r=1\033\\' + printf '\033[4;1H\033[41m \033[0m\r' + printf '\033_Ga=p,q=2,C=1,i=21,p=5,z=-1073741825,c=1,r=1\033\\' + $TEST_TMUX wait-for image-z-top-$$ + printf '\033_Ga=d,d=z,q=2,z=2\033\\' + $TEST_TMUX wait-for image-z-lower-$$ + printf '\033_Ga=d,d=z,q=2,z=1\033\\' + sleep 10") || exit 1 +$TMUX2 select-window -t"$Z_WINDOW" || exit 1 +sleep 1 +$TMUX capture-pane -pS0 -E3 >$TMP || exit 1 +[ "$(sed -n 1p $TMP)" = "." ] || exit 1 +[ "$(sed -n 2p $TMP)" = "." ] || exit 1 +[ "$(sed -n 3p $TMP)" = "Y" ] || exit 1 +[ -z "$(sed -n 4p $TMP)" ] || exit 1 +$TMUX2 wait-for -S image-z-top-$$ || exit 1 +sleep 1 +$TMUX capture-pane -pS0 -E0 >$TMP || exit 1 +[ "$(sed -n 1p $TMP)" = "@" ] || exit 1 +$TMUX2 wait-for -S image-z-lower-$$ || exit 1 +sleep 1 +$TMUX capture-pane -pS0 -E0 >$TMP || exit 1 +[ "$(sed -n 1p $TMP)" = "X" ] || exit 1 + # A weighted median at the maximum channel level must still leave colours on # both sides of the split. This skewed black, grey and white image used to stop # palette generation after one colour instead of producing three. @@ -277,4 +315,22 @@ $TMUX send-keys Enter || exit 1 sleep 1 grep -a '#2;2;' $TMP >/dev/null || exit 1 +# A pane redraw on a SIXEL terminal must not clear the status line, which is +# outside the window scene and may not be redrawn after the pane scrolls. +$TMUX kill-server 2>/dev/null +$TMUX2 kill-server 2>/dev/null +$TMUX2 new-session -d -x 20 -y 8 || exit 1 +$TMUX2 set -g 'status-format[0]' 'STATUS' || exit 1 +$TMUX2 set -as terminal-features ',*:sixel' || exit 1 +$TMUX new-session -d -x 20 -y 8 || exit 1 +$TMUX set -g status off || exit 1 +$TMUX send-keys -l "$TMUX2 attach-session" || exit 1 +$TMUX send-keys Enter || exit 1 +sleep 1 +$TMUX2 send-keys -l 'seq 30' || exit 1 +$TMUX2 send-keys Enter || exit 1 +sleep 1 +$TMUX capture-pane -p >$TMP || exit 1 +[ "$(tail -n 1 $TMP)" = "STATUS" ] || exit 1 + exit 0 diff --git a/screen-redraw.c b/screen-redraw.c index 4c0e62cbf..e262e1bd5 100644 --- a/screen-redraw.c +++ b/screen-redraw.c @@ -1969,8 +1969,12 @@ redraw_draw(struct client *c, struct window_pane *wp, int flags) } tty_sync_start(tty); #ifdef ENABLE_IMAGES - if (wp == NULL && (flags & REDRAW_PANE)) - image_redraw_start(tty, 0, 0, tty->sx, tty->sy); + if (wp == NULL && (flags & REDRAW_PANE)) { + y = 0; + if (dctx.flags & REDRAW_STATUS_TOP) + y = dctx.status_lines; + image_redraw_start(tty, 0, y, scene->sx, scene->sy); + } #endif tty_update_mode(tty, tty->mode & ~CURSOR_MODES, NULL); diff --git a/screen-write.c b/screen-write.c index feed21351..c05cb22c8 100644 --- a/screen-write.c +++ b/screen-write.c @@ -34,6 +34,10 @@ static void screen_write_collect_clear(struct screen_write_ctx *, u_int, static void screen_write_collect_scroll(struct screen_write_ctx *, u_int); static void screen_write_collect_flush(struct screen_write_ctx *, int, const char *); +#ifdef ENABLE_IMAGES +static void screen_write_image_damage(struct screen_write_ctx *, u_int, + u_int, u_int, u_int); +#endif static int screen_write_overwrite(struct screen_write_ctx *, struct grid_cell *, u_int); static int screen_write_combine(struct screen_write_ctx *, @@ -377,6 +381,19 @@ screen_write_init(struct screen_write_ctx *ctx, struct screen *s) ctx->bg = 8; } +#ifdef ENABLE_IMAGES +/* Damage temporal images when application input writes text cells. */ +static void +screen_write_image_damage(struct screen_write_ctx *ctx, u_int x, u_int y, + u_int sx, u_int sy) +{ + image_redraw_area(ctx, x, y, sx, sy); + if (ctx->flags & SCREEN_WRITE_INPUT) + image_grid_damage(ctx->s->grid, x, ctx->s->grid->hsize + y, + sx, sy); +} +#endif + /* Initialize writing with a pane. */ void screen_write_start_pane(struct screen_write_ctx *ctx, struct window_pane *wp, @@ -1339,6 +1356,9 @@ screen_write_alignmenttest(struct screen_write_ctx *ctx) #ifdef ENABLE_IMAGES image_redraw_all(ctx); + if (ctx->flags & SCREEN_WRITE_INPUT) + image_grid_damage(s->grid, 0, s->grid->hsize, + screen_size_x(s), screen_size_y(s)); #endif for (yy = 0; yy < screen_size_y(s); yy++) { @@ -2580,7 +2600,7 @@ screen_write_collect_end(struct screen_write_ctx *ctx) } #ifdef ENABLE_IMAGES - image_redraw_area(ctx, s->cx, s->cy, ci->used, 1); + screen_write_image_damage(ctx, s->cx, s->cy, ci->used, 1); #endif grid_view_set_cells(s->grid, s->cx, s->cy, &ci->gc, cl->data + ci->x, @@ -2666,9 +2686,6 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) struct grid_line *gl; struct grid_cell_entry *gce; struct grid_cell tmp_gc, now_gc; -#ifdef ENABLE_IMAGES - struct grid_cell image_gc; -#endif struct tty_ctx ttyctx; u_int sx = screen_size_x(s), sy = screen_size_y(s); u_int width = ud->width, xx, not_wrap, i, n, vis; @@ -2714,18 +2731,8 @@ screen_write_cell(struct screen_write_ctx *ctx, const struct grid_cell *gc) screen_write_initctx(ctx, &ttyctx, 0, 0); #ifdef ENABLE_IMAGES - /* Update the text underlay without removing an image placement. */ - grid_view_get_cell(gd, s->cx, s->cy, &now_gc); - if ((now_gc.flags & GRID_FLAG_IMAGE) && - (~gc->flags & GRID_FLAG_IMAGE)) { - image_redraw_area(ctx, s->cx, s->cy, width, 1); - memcpy(&image_gc, gc, sizeof image_gc); - image_gc.flags |= GRID_FLAG_IMAGE|GRID_FLAG_IMAGE_DAMAGED; - image_gc.image_id = now_gc.image_id; - image_gc.image_x = now_gc.image_x; - image_gc.image_y = now_gc.image_y; - gc = &image_gc; - } + /* Input semantics decide which image layers this text damages. */ + screen_write_image_damage(ctx, s->cx, s->cy, width, 1); #endif /* Handle overwriting of UTF-8 characters. */ @@ -2955,6 +2962,9 @@ screen_write_combine(struct screen_write_ctx *ctx, const struct grid_cell *gc) force_wide = 0; /* Set the new cell. */ +#ifdef ENABLE_IMAGES + screen_write_image_damage(ctx, cx - n, cy, n, 1); +#endif grid_view_set_cell(gd, cx - n, cy, &last); if (force_wide) grid_view_set_padding(gd, cx - 1, cy, last.bg); @@ -3103,7 +3113,7 @@ screen_write_sixelimage(struct screen_write_ctx *ctx, struct sixel_image *si, sixel_free(si); return; } - image_write(ctx, im, bg); + image_write_sixel(ctx, im, bg); image_free(image_get_id(im)); } #endif diff --git a/style.c b/style.c index ed79f99c5..484b2a59c 100644 --- a/style.c +++ b/style.c @@ -31,9 +31,6 @@ /* Default style. */ static struct style style_default = { { { { ' ' }, 0, 1, 1 }, 0, 0, 8, 8, 0, 0 -#ifdef ENABLE_IMAGES - , 0, 0, 0 -#endif }, 0, 0, diff --git a/tmux.h b/tmux.h index f0c0a82ec..69583c50d 100644 --- a/tmux.h +++ b/tmux.h @@ -80,8 +80,8 @@ struct session; struct image; struct image_backend; struct image_rect; -#endif -#ifdef ENABLE_IMAGES +struct image_line; +struct image_store; struct sixel_image; #endif @@ -809,8 +809,6 @@ struct colour_palette { #define GRID_FLAG_NOPALETTE 0x20 #define GRID_FLAG_CLEARED 0x40 #define GRID_FLAG_TAB 0x80 -#define GRID_FLAG_IMAGE 0x100 -#define GRID_FLAG_IMAGE_DAMAGED 0x200 /* Grid line flags. */ #define GRID_LINE_WRAPPED 0x1 @@ -871,11 +869,6 @@ struct grid_cell { int bg; int us; u_int link; -#ifdef ENABLE_IMAGES - u_int image_id; - u_int image_x; - u_int image_y; -#endif }; /* Grid extended cell entry. */ @@ -887,11 +880,6 @@ struct grid_extd_entry { int bg; int us; u_int link; -#ifdef ENABLE_IMAGES - u_short image_id; - u_short image_x; - u_short image_y; -#endif } __packed; /* Grid cell entry. */ @@ -929,6 +917,9 @@ struct grid_line { u_int time; struct osc133_data osc133_data; u_short flags; +#ifdef ENABLE_IMAGES + struct image_line *images; +#endif }; /* Entire grid of cells. */ @@ -948,6 +939,9 @@ struct grid { u_int scroll_generation; struct grid_line *linedata; +#ifdef ENABLE_IMAGES + struct image_store *images; +#endif }; /* Virtual cursor in a grid. */ @@ -1116,6 +1110,7 @@ struct screen_write_ctx { #define SCREEN_WRITE_SYNC 0x1 #define SCREEN_WRITE_OBSCURED 0x2 #define SCREEN_WRITE_CHECKED_IF_OBSCURED 0x4 +#define SCREEN_WRITE_INPUT 0x8 screen_write_init_ctx_cb init_ctx_cb; void *arg; @@ -4238,7 +4233,6 @@ char *regsub(const char *, const char *, const char *, int); /* image.c */ #define IMAGE_BACKEND_GRAPHICAL 0x1 #define IMAGE_BACKEND_SCROLLS 0x2 -#define IMAGE_BACKEND_TEMPORAL 0x4 struct image *image_create(u_int, u_int, u_int, u_int, u_int, u_int, u_char *); @@ -4246,8 +4240,6 @@ struct image *image_create_view(struct image *, u_int, u_int, u_int, u_int, u_int, u_int, u_int, u_int); struct image *image_find(u_int); u_int image_get_id(const struct image *); -u_short image_get_grid_id(u_int); -u_int image_get_id_by_grid_id(u_short); void image_get_size(const struct image *, u_int *, u_int *); void image_get_canvas_size(const struct image *, u_int *, u_int *); void image_get_size_in_cells(const struct image *, u_int *, u_int *); @@ -4257,9 +4249,10 @@ struct sixel_image *image_get_sixel(const struct image *); void image_set_sixel(struct image *, struct sixel_image *); void image_ref(u_int); void image_free(u_int); -void image_set_cell(struct grid_cell *, struct image *, u_int, +void image_write_sixel(struct screen_write_ctx *, struct image *, u_int); -void image_write(struct screen_write_ctx *, struct image *, u_int); +void image_write_kitty(struct screen_write_ctx *, struct image *, + u_int, u_int, u_int, int32_t); void image_get_pixel_rect(const struct image *, u_int, u_int, u_int, u_int, u_int *, u_int *, u_int *, u_int *); void image_size_in_cells(u_int, u_int, u_int, u_int, u_int *, @@ -4273,8 +4266,6 @@ void image_redraw_all(struct screen_write_ctx *); void image_redraw_scroll(struct screen_write_ctx *, u_int); void image_redraw_start(struct tty *, u_int, u_int, u_int, u_int); int image_backend_flags(struct tty *); -int image_get_draw_cell(struct tty *, const struct grid_cell *, - struct grid_cell *, const struct tty_style_ctx *); void image_tty_update(struct tty *); void image_tty_geometry_changed(struct tty *); void image_tty_free(struct tty *, int); @@ -4283,20 +4274,44 @@ void image_draw_line(struct tty *, struct screen *, u_int, u_int, void image_get_fallback_cell(struct tty *, struct image *, u_int, u_int, const struct grid_cell *, struct grid_cell *, const struct tty_style_ctx *); +int image_get_fallback_at(struct tty *, struct screen *, u_int, + u_int, const struct grid_cell *, struct grid_cell *, + const struct tty_style_ctx *); struct image *image_rect_get_image(const struct image_rect *); const struct grid_cell *image_rect_get_cell( const struct image_rect *); void image_rect_get_coords(const struct image_rect *, u_int *, u_int *, u_int *, u_int *, u_int *, u_int *); +int32_t image_rect_get_z(const struct image_rect *); void image_clear(struct screen_write_ctx *, u_int); +void image_clear_kitty(struct screen_write_ctx *, char, u_int, + u_int, int32_t); +void image_grid_damage(struct grid *, u_int, u_int, u_int, u_int); +void image_grid_free_line(struct grid *, struct grid_line *); +void image_grid_free(struct grid *); +void image_grid_move_cells(struct grid *, u_int, u_int, u_int, + u_int); +void image_grid_duplicate_lines(struct grid *, u_int, struct grid *, + u_int, u_int); +void image_grid_copy_area(struct grid *, u_int, u_int, struct grid *, + u_int, u_int, u_int, u_int); +int image_grid_line_has_images(const struct grid_line *); +int image_grid_check_area(struct grid *, u_int, u_int, u_int, + u_int); +int image_grid_get_source(struct grid *, u_int, u_int, + struct image *, u_int *, u_int *); +void image_place_cell_kitty(struct screen_write_ctx *, struct image *, + u_int, u_int, u_int, u_int, u_int, u_int, int32_t); #define KITTY_PARSE_ERROR -1 #define KITTY_PARSE_OK 0 #define KITTY_PARSE_MORE 1 #define KITTY_PARSE_MISSING 2 struct image *kitty_parse_image(void **, const u_char *, size_t, u_int, - u_int, u_int *, u_int *, u_int *, char *, int *); -int kitty_placeholder_to_cell(void *, struct grid_cell *, - const struct grid_cell *); + u_int, u_int *, u_int *, u_int *, char *, char *, u_int *, + int32_t *, int *); +int kitty_placeholder_to_image(void *, struct grid *, + struct grid_cell *, u_int, u_int, struct image **, u_int *, + u_int *, u_int *, u_int *, int32_t *); void kitty_free_state(void *); void kitty_draw_rect(struct tty *, const struct image_rect *, const struct tty_style_ctx *); @@ -4309,7 +4324,8 @@ void kitty_geometry_changed(struct tty *); /* image-sixel.c */ #define SIXEL_COLOUR_REGISTERS 1024 void sixel_draw_rect(struct tty *, - const struct image_rect *, const struct tty_style_ctx *); + const struct image_rect *, const struct tty_style_ctx *); +void sixel_redraw_start(struct tty *, u_int, u_int, u_int, u_int); void sixel_free_output(struct tty *, int); void sixel_geometry_changed(struct tty *); struct sixel_image *sixel_parse(const char *, size_t, u_int, u_int, u_int, diff --git a/tty-draw.c b/tty-draw.c index e1cc2a517..9eb1c80e0 100644 --- a/tty-draw.c +++ b/tty-draw.c @@ -125,6 +125,7 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, struct grid_cell gc, ngc, last; #ifdef ENABLE_IMAGES struct grid_cell image_gc; + int image_status; #endif struct grid_line *gl; u_int i, j, last_i, cx, ex, width; @@ -250,21 +251,26 @@ tty_draw_line(struct tty *tty, struct screen *s, u_int px, u_int py, u_int nx, if (px >= ex || i >= ex - px) { /* Outside the area being drawn. */ - empty = nx - i; gcp = &grid_default_cell; +#ifdef ENABLE_IMAGES + image_status = image_get_fallback_at(tty, s, px + i, + py, gcp, &image_gc, style_ctx); + if (image_status == 1) { + gcp = &image_gc; + empty = 0; + } else if (image_status == -1) + empty = 1; + else +#endif + empty = nx - i; } else { /* Get the current cell. */ grid_view_get_cell(gd, px + i, py, &gc); - -#ifdef ENABLE_IMAGES - if (gc.flags & GRID_FLAG_IMAGE) { - (void)image_get_draw_cell(tty, &gc, &image_gc, - style_ctx); - gcp = &image_gc; - } else - gcp = &gc; -#else gcp = &gc; +#ifdef ENABLE_IMAGES + if (image_get_fallback_at(tty, s, px + i, py, &gc, + &image_gc, style_ctx) == 1) + gcp = &image_gc; #endif /* Work out empty cells. */ diff --git a/tty.c b/tty.c index f3d49110c..b46c43a33 100644 --- a/tty.c +++ b/tty.c @@ -2132,9 +2132,6 @@ tty_cell(struct tty *tty, const struct grid_cell *gc, const struct tty_style_ctx *style_ctx) { const struct grid_cell *gcp; -#ifdef ENABLE_IMAGES - struct grid_cell image_gc; -#endif /* Skip last character if terminal is stupid. */ if ((tty->term->flags & TERM_NOAM) && @@ -2150,14 +2147,6 @@ tty_cell(struct tty *tty, const struct grid_cell *gc, if (!tty_check_overlay(tty, tty->cx, tty->cy)) return; -#ifdef ENABLE_IMAGES - if (gc->flags & GRID_FLAG_IMAGE) { - if (image_get_draw_cell(tty, gc, &image_gc, style_ctx)) - return; - gc = &image_gc; - } -#endif - /* Check the output codeset and apply attributes. */ gcp = tty_check_codeset(tty, gc); tty_attributes(tty, gcp, style_ctx); diff --git a/window-copy.c b/window-copy.c index fc29c64fd..eba64d119 100644 --- a/window-copy.c +++ b/window-copy.c @@ -5408,6 +5408,14 @@ window_copy_write_line(struct window_mode_entry *wme, window_copy_write_one(wme, ctx, width, py, hsize - data->oy + py, content_sx, &mgc, &cgc, &mkgc, &clgc); +#ifdef ENABLE_IMAGES + /* Copy the backing line's image layers separately from its text cells. */ + image_grid_free_line(s->grid, + &s->grid->linedata[s->grid->hsize + py]); + image_grid_copy_area(s->grid, width, s->grid->hsize + py, + data->backing->grid, 0, hsize - data->oy + py, content_sx, 1); +#endif + if (py == 0 && s->rupper < s->rlower && !data->hide_position) { value = options_get_string(oo, "copy-mode-position-format"); if (*value != '\0') { @@ -6188,9 +6196,7 @@ window_copy_copy_line(struct window_mode_entry *wme, char **buf, size_t *off, grid_get_cell(gd, i, sy, &gc); if (gc.flags & GRID_FLAG_PADDING) continue; - if (gc.flags & GRID_FLAG_IMAGE) - utf8_set(&ud, ' '); - else if (gc.flags & GRID_FLAG_TAB) + if (gc.flags & GRID_FLAG_TAB) utf8_set(&ud, '\t'); else utf8_copy(&ud, &gc.data);