diff options
| author | user <user@clank> | 2026-08-14 23:25:46 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-14 23:25:46 +0200 |
| commit | aa01b72770d83091a53a43722ecc9a527bc18931 (patch) | |
| tree | 04a35b6ba2566a154cc344ebc0e031941cbce873 /src | |
| parent | sl0ppty: carry the terminal's cell size, so images can have a size (diff) | |
| download | slosh-aa01b72770d83091a53a43722ecc9a527bc18931.tar.gz slosh-aa01b72770d83091a53a43722ecc9a527bc18931.tar.xz slosh-aa01b72770d83091a53a43722ecc9a527bc18931.zip | |
sl0ppty: re-emit what the program meant, not what it covers
Images rendered after the last commit, and were still wrong twice. Both
mistakes are invisible in a still picture, which is what every test was
looking at, and obvious the moment anything moves.
**The sub-cell offset was dropped.** A placement says where the image starts
*inside* its first cell (`X=`/`Y=`); that is what makes motion smooth rather
than a slideshow. We tracked the placement and re-emitted it without those,
so a bouncing logo moved in steps of a whole cell -- eight pixels across,
sixteen down.
**`c=`/`r=` were passed on unconditionally.** They mean *scale the image into
this many cells*, and the count a natural-size image happens to cover goes up
by one whenever it straddles one more boundary -- so the picture changed size
as it slid along. They are now sent only when the program asked for them,
which lib-vt reports as 0 when it did not.
Cropping splits the same way, and getting it wrong took a test failure to
notice: at natural size a lost cell costs the screen pixels the pane has left
(measured from the offset, not the cell edge, or an offset image hangs up to
a cell over the border); when the program asked to scale, a lost cell is
worth source/cells pixels and the crop is proportional. Cropping a 4-pixel
image scaled across 40 cells by screen pixels removes all of it.
gfx_place() took twenty positional arguments, half of them uint32_t pixel
quantities where a transposed pair compiles perfectly and lands the picture
slightly wrong -- the exact bug class this file exists for. It takes a struct
now.
`{"cmd":"graphics","format":"bytes"}` answers with the escape sequences the
client is actually sent. The model being right is not the same as the bytes
being right, and this whole episode was a lesson in asserting on the first
while the second was wrong. The new tests assert on the bytes.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 75 | ||||
| -rw-r--r-- | src/cmd.c | 15 | ||||
| -rw-r--r-- | src/graphics.c | 49 | ||||
| -rw-r--r-- | src/graphics.h | 36 | ||||
| -rw-r--r-- | src/pane.c | 33 | ||||
| -rw-r--r-- | src/sl0ppty.h | 10 |
6 files changed, 178 insertions, 40 deletions
@@ -3191,25 +3191,70 @@ static void gfx_from_pane(pane_t *p, const pane_gfx_t *g, void *ud) { if (g->col >= leaf->content.w || g->row >= leaf->content.h) return; uint16_t cols = g->cols, rows = g->rows; uint32_t sw = g->sw, sh = g->sh; + /* Cropping means moving the source rectangle, and how much source a lost + * cell is worth depends on whether the image is being scaled: + * + * natural size one source pixel is one screen pixel, so the crop is the + * screen pixels the pane has left -- measured from where + * the image starts, which is `x_off` into its first cell + * and not at the cell's edge. Cropping by whole cells alone + * let an offset image hang up to a cell over the border. + * scaled the program asked for c=/r=, so a cell is worth + * source/cells pixels and the crop is proportional. Taking + * screen pixels off a scaled image would crop far too much + * -- a 4px image across 40 cells loses its whole self. */ if (g->col + cols > leaf->content.w) { - uint16_t over = (uint16_t)(g->col + cols - leaf->content.w); - cols = (uint16_t)(cols - over); - uint32_t px = over * g->cell_px_w; - sw = sw > px ? sw - px : 0; + uint16_t avail = (uint16_t)(leaf->content.w - g->col); + if (g->req_cols) { + sw = cols ? (uint32_t)((uint64_t)sw * avail / cols) : sw; + } else { + uint32_t px = (uint32_t)avail * g->cell_px_w; + px = px > g->x_off ? px - g->x_off : 0; + if (sw > px) sw = px; + } + cols = avail; } if (g->row + rows > leaf->content.h) { - uint16_t over = (uint16_t)(g->row + rows - leaf->content.h); - rows = (uint16_t)(rows - over); - uint32_t px = over * g->cell_px_h; - sh = sh > px ? sh - px : 0; + uint16_t avail = (uint16_t)(leaf->content.h - g->row); + if (g->req_rows) { + sh = rows ? (uint32_t)((uint64_t)sh * avail / rows) : sh; + } else { + uint32_t px = (uint32_t)avail * g->cell_px_h; + px = px > g->y_off ? px - g->y_off : 0; + if (sh > px) sh = px; + } + rows = avail; } if (!cols || !rows) return; - gfx_place(c->a->gfx, leaf->id, g->image_id, g->generation, g->place_id, - (uint16_t)(leaf->content.x + g->col), - (uint16_t)(leaf->content.y + g->row), cols, rows, g->sx, g->sy, sw, - sh, g->src_w, g->src_h, g->format, g->compression, g->data, - g->data_len); + gfx_place(c->a->gfx, &(gfx_req_t){ + .pane = leaf->id, + .src_id = g->image_id, + .gen = g->generation, + .place_id = g->place_id, + .col = (uint16_t)(leaf->content.x + g->col), + .row = (uint16_t)(leaf->content.y + g->row), + .cols = cols, + .rows = rows, + /* Carried through untouched: the pane's own clipping already dealt with + * it, and the offset is relative to the first cell either way. */ + .x_off = g->x_off, + .y_off = g->y_off, + /* Clipped the same way the cell counts were, and zero when the program + * never asked to scale. */ + .scale_cols = g->req_cols ? cols : 0, + .scale_rows = g->req_rows ? rows : 0, + .sx = g->sx, + .sy = g->sy, + .sw = sw, + .sh = sh, + .px_w = g->src_w, + .px_h = g->src_h, + .format = g->format, + .compression = g->compression, + .data = g->data, + .data_len = g->data_len, + }); } static void gfx_leaf_cb(node_t *n, void *ud) { @@ -3245,6 +3290,10 @@ char *app_graphics_json(app_t *a) { json_int(&j, "y", places[i].row); json_int(&j, "cols", places[i].cols); json_int(&j, "rows", places[i].rows); + /* Reported so a test can see sub-cell motion, which is otherwise only + * visible as "the picture moves smoothly" on somebody's screen. */ + json_int(&j, "x_off", places[i].x_off); + json_int(&j, "y_off", places[i].y_off); json_obj_close(&j); } json_arr_close(&j); @@ -220,6 +220,21 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in, } if (strcmp(cmd, "graphics") == 0) { app_compose(a, s); + /* `format:"bytes"` answers with what the client's terminal is actually + * sent, escape sequences and all. The model and the bytes are two + * different things, and this session was one long lesson in asserting on + * the model while the bytes were wrong. */ + if (strcmp(jv_gets(req, "format", "placements"), "bytes") == 0) { + size_t len = 0; + const char *raw = app_graphics(a, &len); + json_t j; + json_init(&j); + json_obj_open(&j, NULL); + json_bool(&j, "ok", true); + json_str(&j, "bytes", raw ? raw : "", raw ? len : 0); + json_obj_close(&j); + return j.buf; + } return jok_raw("placements", app_graphics_json(a)); } if (strcmp(cmd, "clipboard") == 0) { diff --git a/src/graphics.c b/src/graphics.c index bf9aa1d..d3fc07c 100644 --- a/src/graphics.c +++ b/src/graphics.c @@ -127,11 +127,10 @@ static void transmit(graphics_t *g, gfx_image_t *img, uint32_t px_w, img->sent = true; } -void gfx_place(graphics_t *g, uint32_t pane, uint32_t src_id, uint64_t gen, - uint32_t place_id, uint16_t col, uint16_t row, uint16_t cols, - uint16_t rows, uint32_t sx, uint32_t sy, uint32_t sw, - uint32_t sh, uint32_t px_w, uint32_t px_h, int format, - int compression, const uint8_t *data, size_t data_len) { +void gfx_place(graphics_t *g, const gfx_req_t *req) { + if (!g || !req) return; + const uint32_t pane = req->pane, src_id = req->src_id; + const uint64_t gen = req->gen; gfx_image_t *img = img_find(g, pane, src_id); if (!img) { if (g->nimgs == g->imgcap) { @@ -147,9 +146,10 @@ void gfx_place(graphics_t *g, uint32_t pane, uint32_t src_id, uint64_t gen, img->sent = false; } if (!img->sent) - transmit(g, img, px_w, px_h, format, compression, data, data_len); + transmit(g, img, req->px_w, req->px_h, req->format, req->compression, + req->data, req->data_len); - uint32_t pid = place_id ? place_id : 1; + uint32_t pid = req->place_id ? req->place_id : 1; gfx_place_t *slot = NULL; for (size_t i = 0; i < g->nplaces; i++) if (g->places[i].out_id == img->out_id && g->places[i].place_id == pid) @@ -164,14 +164,18 @@ void gfx_place(graphics_t *g, uint32_t pane, uint32_t src_id, uint64_t gen, slot->out_id = img->out_id; slot->place_id = pid; } - slot->col = col; - slot->row = row; - slot->cols = cols; - slot->rows = rows; - slot->sx = sx; - slot->sy = sy; - slot->sw = sw; - slot->sh = sh; + slot->col = req->col; + slot->row = req->row; + slot->cols = req->cols; + slot->rows = req->rows; + slot->x_off = req->x_off; + slot->y_off = req->y_off; + slot->scale_cols = req->scale_cols; + slot->scale_rows = req->scale_rows; + slot->sx = req->sx; + slot->sy = req->sy; + slot->sw = req->sw; + slot->sh = req->sh; slot->live = true; } @@ -194,8 +198,19 @@ char *gfx_flush(graphics_t *g, size_t *out_len) { for (size_t i = 0; i < g->nplaces; i++) { gfx_place_t *p = &g->places[i]; out_fmt(g, "\x1b[%u;%uH", p->row + 1, p->col + 1); - out_fmt(g, "\x1b_Ga=p,q=2,C=1,i=%u,p=%u,c=%u,r=%u", p->out_id, p->place_id, - p->cols, p->rows); + out_fmt(g, "\x1b_Ga=p,q=2,C=1,i=%u,p=%u", p->out_id, p->place_id); + /* c=/r= mean *scale into this many cells*, so they are passed on only + * when the program asked for them. Sending the cell count a natural-size + * image happens to cover looks identical in a still picture and makes a + * moving one change size, because that count goes up by one whenever the + * image straddles one more cell boundary. */ + if (p->scale_cols || p->scale_rows) + out_fmt(g, ",c=%u,r=%u", p->scale_cols, p->scale_rows); + /* Where the image sits inside its first cell. Without these an image can + * only ever land on a cell boundary, so anything moving smoothly arrives + * in steps of a whole cell -- eight pixels across, sixteen down. */ + if (p->x_off) out_fmt(g, ",X=%u", p->x_off); + if (p->y_off) out_fmt(g, ",Y=%u", p->y_off); /* The source rectangle is what actually crops; c/r alone would scale. */ if (p->sw || p->sh) out_fmt(g, ",x=%u,y=%u,w=%u,h=%u", p->sx, p->sy, p->sw, p->sh); diff --git a/src/graphics.h b/src/graphics.h index e31b733..cc495ec 100644 --- a/src/graphics.h +++ b/src/graphics.h @@ -32,6 +32,13 @@ typedef struct { uint32_t place_id; uint16_t col, row; /* screen cells */ uint16_t cols, rows; + /* Where the image starts *inside* its first cell, in pixels. This is what + * makes motion smooth instead of snapping a cell at a time, and dropping it + * is invisible in a still picture and obvious the moment anything moves. */ + uint32_t x_off, y_off; + /* Cells to scale into, or 0 to draw at natural size. Only sent on when the + * program asked for it. */ + uint16_t scale_cols, scale_rows; uint32_t sx, sy, sw, sh; /* source rectangle, in image pixels */ bool live; } gfx_place_t; @@ -41,13 +48,32 @@ void gfx_free(graphics_t *g); /* Start a frame: everything placed last frame is marked stale. */ void gfx_begin(graphics_t *g); +/* Everything gfx_place() needs: where it goes, what it crops to, and the + * image itself for the first time it is seen. + * + * A struct rather than the twenty positional arguments this used to be. Half + * of them are uint32_t pixel quantities, so a transposed pair compiles + * perfectly and shows up as a picture in slightly the wrong place — which is + * exactly the bug class this file is for. */ +typedef struct { + uint32_t pane; /* which pane it belongs to */ + uint32_t src_id; /* the id the program chose */ + uint64_t gen; /* image generation, so a changed image is re-sent */ + uint32_t place_id; + uint16_t col, row; /* screen cells */ + uint16_t cols, rows; /* cells covered */ + uint16_t scale_cols, scale_rows; /* cells to scale into, 0 for natural */ + uint32_t x_off, y_off; /* offset within the first cell, in pixels */ + uint32_t sx, sy, sw, sh; /* source rectangle, in image pixels */ + uint32_t px_w, px_h; /* the image's own size */ + int format, compression; + const uint8_t *data; /* NULL once the client has it */ + size_t data_len; +} gfx_req_t; + /* Record a visible placement, transmitting the image if the client has not * seen it yet. `data` may be NULL when the image is already transmitted. */ -void gfx_place(graphics_t *g, uint32_t pane, uint32_t src_id, uint64_t gen, - uint32_t place_id, uint16_t col, uint16_t row, uint16_t cols, - uint16_t rows, uint32_t sx, uint32_t sy, uint32_t sw, - uint32_t sh, uint32_t px_w, uint32_t px_h, int format, - int compression, const uint8_t *data, size_t data_len); +void gfx_place(graphics_t *g, const gfx_req_t *req); /* End a frame: emit transmits, placements and deletions. Caller frees. */ char *gfx_flush(graphics_t *g, size_t *out_len); @@ -593,11 +593,20 @@ size_t pane_graphics(pane_t *p, pane_gfx_fn cb, void *ud) { size_t n = 0; while (ghostty_kitty_graphics_placement_next(it)) { - uint32_t image_id = 0, place_id = 0; + uint32_t image_id = 0, place_id = 0, x_off = 0, y_off = 0; ghostty_kitty_graphics_placement_get( it, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_IMAGE_ID, &image_id); ghostty_kitty_graphics_placement_get( it, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_PLACEMENT_ID, &place_id); + ghostty_kitty_graphics_placement_get( + it, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_X_OFFSET, &x_off); + ghostty_kitty_graphics_placement_get( + it, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_Y_OFFSET, &y_off); + uint32_t req_cols = 0, req_rows = 0; + ghostty_kitty_graphics_placement_get( + it, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_COLUMNS, &req_cols); + ghostty_kitty_graphics_placement_get( + it, GHOSTTY_KITTY_GRAPHICS_PLACEMENT_DATA_ROWS, &req_rows); GhosttyKittyGraphicsImage img = ghostty_kitty_graphics_image(gfx, image_id); GhosttyKittyGraphicsPlacementRenderInfo info = @@ -621,21 +630,31 @@ size_t pane_graphics(pane_t *p, pane_gfx_fn cb, void *ud) { uint32_t sw = info.source_width, sh = info.source_height; int32_t vc = info.viewport_col, vr = info.viewport_row; uint32_t cols = info.grid_cols, rows = info.grid_rows; + /* Clipping consumes the sub-cell offset first. The image starts `x_off` + * pixels into its origin cell, so dropping `off` whole cells from the + * left skips `off * cw - x_off` pixels of image, and what remains begins + * exactly on a cell boundary — offset zero from there on. */ if (vc < 0) { uint32_t off = (uint32_t)(-vc); if (off >= cols) continue; - sx += off * cw; - sw = sw > off * cw ? sw - off * cw : 0; + uint32_t skip = off * cw > x_off ? off * cw - x_off : 0; + sx += skip; + sw = sw > skip ? sw - skip : 0; cols -= off; + if (req_cols) req_cols = req_cols > off ? req_cols - off : 0; vc = 0; + x_off = 0; } if (vr < 0) { uint32_t off = (uint32_t)(-vr); if (off >= rows) continue; - sy += off * ch; - sh = sh > off * ch ? sh - off * ch : 0; + uint32_t skip = off * ch > y_off ? off * ch - y_off : 0; + sy += skip; + sh = sh > skip ? sh - skip : 0; rows -= off; + if (req_rows) req_rows = req_rows > off ? req_rows - off : 0; vr = 0; + y_off = 0; } if (!cols || !rows) continue; @@ -646,6 +665,10 @@ size_t pane_graphics(pane_t *p, pane_gfx_fn cb, void *ud) { .row = (uint16_t)vr, .cols = (uint16_t)cols, .rows = (uint16_t)rows, + .x_off = x_off, + .y_off = y_off, + .req_cols = (uint16_t)req_cols, + .req_rows = (uint16_t)req_rows, .px_w = info.pixel_width, .px_h = info.pixel_height, .sx = sx, diff --git a/src/sl0ppty.h b/src/sl0ppty.h index 2e56577..fb7ba2d 100644 --- a/src/sl0ppty.h +++ b/src/sl0ppty.h @@ -179,6 +179,16 @@ void pane_set_notify_handler(pane_t *p, pane_notify_fn fn, void *ud); typedef struct { uint32_t image_id, place_id; uint16_t col, row, cols, rows; + /* Where the image starts inside its first cell, in pixels. Sub-cell + * positioning: what a program moving something smoothly relies on, and the + * difference between motion and a slideshow. */ + uint32_t x_off, y_off; + /* What the program asked to scale into, or 0 for "draw it at its natural + * size". The difference matters on the way out: naming a cell count tells + * the terminal to *scale* the image into it, and the count a natural + * placement happens to cover changes by one as it slides across a cell + * boundary — so passing it on makes a moving image change size. */ + uint16_t req_cols, req_rows; uint32_t px_w, px_h; /* rendered size */ uint32_t src_w, src_h; /* the image's own size */ /* The part of the image to draw, in pixels. Clipping has to move this: |
