diff options
| author | Ingmar Steen <iksteen@gmail.com> | 2026-09-06 19:59:13 +0200 |
|---|---|---|
| committer | Ingmar Steen <iksteen@gmail.com> | 2026-09-06 19:59:13 +0200 |
| commit | af82aff3cac5370cc08b2e0d7358097bfb2d9e45 (patch) | |
| tree | f13ad9272e021d039a462a96ce49f9c6b1929d68 | |
| parent | graphics: state z=0 on a placement, so a mirror paints it over the text (diff) | |
| download | slosh-af82aff3cac5370cc08b2e0d7358097bfb2d9e45.tar.gz slosh-af82aff3cac5370cc08b2e0d7358097bfb2d9e45.tar.xz slosh-af82aff3cac5370cc08b2e0d7358097bfb2d9e45.zip | |
graphics: place what a float leaves of an image, not nothing
Occlusion for placements is clipping: the cell compositor gets it free
from paint order, but images are sent after the diff and drawn over it,
so what a float covers has to be taken out of the placement itself. A
clean edge came out as a smaller rectangle; anything else -- a float
across the middle, a float on a corner, a float wholly inside -- was
refused, on the grounds that the remainder is an L and a placement is a
rectangle. The image vanished, all of it, the moment a floating pane
crossed it, on the terminal and in the mirror alike, and came back when
the float moved off.
The remainder is not one rectangle, but it is always a few: the strips
above and below what is covered, and what is left beside it between
them. So rect_subtract() cuts the visible remainder into rectangles and
each is placed in its own right, with the source rectangle the same
arithmetic the pane edge already used -- generalised to a cut on any
side, and to a first piece that keeps the sub-cell offset while the
others start on a cell boundary. Eight pieces is the ceiling (two
floats' worth); past it the old refusal stands.
The pieces of one placement need an id each, and the program's own
placement id is one number that cannot be several, so the model now
hands out the placement ids the client is told and keeps the program's
as the identity it was. `graphics` reports the piece alongside it.
Checked against a float parked over an image: the picture is cut around
the float, the strips tile exactly the image minus what the float
covers, and their source rectangles land where their cells do (a float
inside a 40x10-cell natural-size image gives 320x32 above, 320x32
below, 56x96 left and 72x96 right).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRz3zwZJvBvzsR9k4cj6dd
Diffstat (limited to '')
| -rw-r--r-- | include/graphics.h | 15 | ||||
| -rw-r--r-- | src/app_draw.c | 260 | ||||
| -rw-r--r-- | src/graphics.c | 17 | ||||
| -rw-r--r-- | tests/test_graphics.py | 45 |
4 files changed, 209 insertions, 128 deletions
diff --git a/include/graphics.h b/include/graphics.h index b84b574..cebb53c 100644 --- a/include/graphics.h +++ b/include/graphics.h @@ -26,10 +26,20 @@ typedef struct { bool sent; } gfx_image_t; -/* One placement on screen this frame. */ +/* One placement on screen this frame -- or one visible piece of it, when + * something on top has cut it into more than one rectangle. */ typedef struct { uint32_t out_id; - uint32_t place_id; + uint32_t place_id; /* the id the program chose */ + /* Which visible rectangle of that placement this is. A placement nothing + * covers has one piece; a float across the middle of an image leaves two, + * and a float in a corner leaves two or three -- each of them a placement + * of its own on the wire, which is the only shape the protocol has. */ + uint16_t piece; + /* The placement id the client is told, ours to hand out: the pieces of one + * placement need an id each, and the program's own id is one number that + * cannot be several. */ + uint32_t wire_id; uint16_t col, row; /* screen cells */ uint16_t cols, rows; /* Where the image starts *inside* its first cell, in pixels. This is what @@ -63,6 +73,7 @@ typedef struct { 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 piece; /* which visible rectangle of it */ 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 */ diff --git a/src/app_draw.c b/src/app_draw.c index c3486fc..c47a253 100644 --- a/src/app_draw.c +++ b/src/app_draw.c @@ -2309,6 +2309,94 @@ static void gfx_place_view(graphics_t *out, gfx_req_t r, rect_t v) { gfx_place(out, &r); } +/* The most rectangles one image is cut into. One float leaves at most four + * pieces, so this is two floats' worth; past it the image is dropped rather + * than shredded, which is what the old emitter did for every cut. */ +#define GFX_MAX_PIECES 8 + +/* What is left of `r` once the occluders are taken out of it, as rectangles. + * Zero when nothing is left, or when saying it would take more than `max`. */ +static size_t rect_subtract(rect_t r, const rect_t *occ, size_t nocc, + rect_t *out, size_t max) { + if (!r.w || !r.h) return 0; + if (max > GFX_MAX_PIECES) max = GFX_MAX_PIECES; /* `next` below is that big */ + size_t n = 0; + out[n++] = r; + for (size_t i = 0; i < nocc; i++) { + rect_t next[GFX_MAX_PIECES]; + size_t m = 0; + for (size_t j = 0; j < n; j++) { + int px0 = out[j].x, py0 = out[j].y; + int px1 = px0 + out[j].w, py1 = py0 + out[j].h; + int ox0 = occ[i].x, oy0 = occ[i].y; + int ox1 = ox0 + occ[i].w, oy1 = oy0 + occ[i].h; + if (ox1 <= px0 || ox0 >= px1 || oy1 <= py0 || oy0 >= py1) { + if (m == max) return 0; + next[m++] = out[j]; + continue; + } + int cx0 = ox0 > px0 ? ox0 : px0, cx1 = ox1 < px1 ? ox1 : px1; + int cy0 = oy0 > py0 ? oy0 : py0, cy1 = oy1 < py1 ? oy1 : py1; + /* The strips around the covered part: the full width above and below + * it, and what is beside it between those two. Four at most, and each + * of them empty when the cover reaches that edge. */ + const int strips[4][4] = { + {px0, py0, px1, cy0}, + {px0, cy1, px1, py1}, + {px0, cy0, cx0, cy1}, + {cx1, cy0, px1, cy1}, + }; + for (size_t k = 0; k < 4; k++) { + int x0 = strips[k][0], y0 = strips[k][1]; + int x1 = strips[k][2], y1 = strips[k][3]; + if (x1 <= x0 || y1 <= y0) continue; + if (m == max) return 0; + next[m++] = (rect_t){(uint16_t)x0, (uint16_t)y0, (uint16_t)(x1 - x0), + (uint16_t)(y1 - y0)}; + } + } + memcpy(out, next, m * sizeof *out); + n = m; + if (!n) return 0; + } + return n; +} + +/* One axis of one piece: the source rectangle for the `n` cells that start + * `d` cells into a placement `total` cells wide, and the offset the piece + * begins at inside its own first cell. + * + * Scaled and natural are different sums, for the reason the pane-edge crop + * gives: with c=/r= a cell is worth source/cells pixels, and at natural size + * a cell is worth its own pixels -- measured from where the image starts, + * which is `off` into the first cell and nowhere else. */ +static void axis_crop(bool scaled, uint16_t total, uint16_t d, uint16_t n, + uint32_t cell_px, uint32_t *src, uint32_t *len, + uint32_t *off) { + if (!total || !n) return; + if (scaled) { + uint32_t s0 = (uint32_t)((uint64_t)*len * d / total); + uint32_t s1 = (uint32_t)((uint64_t)*len * (d + n) / total); + *src += s0; + *len = s1 - s0; + /* Scaling does not move where the image starts inside its first cell, + * so the first piece keeps that offset and a later one starts on a cell + * boundary by construction. */ + if (d) *off = 0; + return; + } + uint32_t edge = (uint32_t)d * cell_px; + uint32_t skip = d ? (edge > *off ? edge - *off : 0) : 0; + if (skip > *len) skip = *len; + uint32_t keep = d ? 0 : *off; + uint32_t avail = (uint32_t)n * cell_px; + avail = avail > keep ? avail - keep : 0; + *src += skip; + *len -= skip; + if (*len > avail) *len = avail; + *off = keep; +} + static void gfx_from_pane(pane_t *p, const pane_gfx_t *g, void *ud) { struct gfx_ctx *c = ud; node_t *leaf = c->leaf; @@ -2358,121 +2446,62 @@ static void gfx_from_pane(pane_t *p, const pane_gfx_t *g, void *ud) { /* The floats above this pane. The cell compositor gets occlusion free from * paint order; placements are sent after the diff and get it from this: - * what one clean edge can express is cropped — the same source-rectangle - * arithmetic as the pane-edge clipping, aimed at another clipper — and a - * float in the *middle* of an image is a shape one placement cannot - * express, so that placement is suppressed for the frame and returns when - * the float moves. A corner overlap leaves an L, which is two rects, which - * is the same refusal. */ - uint16_t col = g->col, row = g->row; - uint32_t sx = g->sx, sy = g->sy; - uint32_t xo = g->x_off, yo = g->y_off; - for (size_t i = 0; i < c->nocc; i++) { - int px0 = leaf->content.x + col, py0 = leaf->content.y + row; - int px1 = px0 + cols, py1 = py0 + rows; - int ox0 = c->occ[i].x, oy0 = c->occ[i].y; - int ox1 = ox0 + c->occ[i].w, oy1 = oy0 + c->occ[i].h; - if (ox1 <= px0 || ox0 >= px1 || oy1 <= py0 || oy0 >= py1) continue; - bool spans_x = ox0 <= px0 && ox1 >= px1; - bool spans_y = oy0 <= py0 && oy1 >= py1; - if (spans_x && spans_y) return; /* fully covered */ - if (spans_x) { - if (oy0 <= py0) { /* trimmed from the top: the source origin moves */ - uint16_t cut = (uint16_t)(oy1 - py0); - if (g->req_rows) { - uint32_t drop = rows ? (uint32_t)((uint64_t)sh * cut / rows) : 0; - sy += drop; - sh -= drop; - } else { - uint32_t px = (uint32_t)cut * g->cell_px_h; - px = px > yo ? px - yo : 0; - yo = 0; - sy += px < sh ? px : sh; - sh = px < sh ? sh - px : 0; - } - row = (uint16_t)(row + cut); - rows = (uint16_t)(rows - cut); - } else if (oy1 >= py1) { /* trimmed from the bottom, like the edge */ - uint16_t keep = (uint16_t)(oy0 - py0); - if (g->req_rows) { - sh = rows ? (uint32_t)((uint64_t)sh * keep / rows) : sh; - } else { - uint32_t px = (uint32_t)keep * g->cell_px_h; - px = px > yo ? px - yo : 0; - if (sh > px) sh = px; - } - rows = keep; - } else { - return; /* a strip across the middle */ - } - } else if (spans_y) { - if (ox0 <= px0) { - uint16_t cut = (uint16_t)(ox1 - px0); - if (g->req_cols) { - uint32_t drop = cols ? (uint32_t)((uint64_t)sw * cut / cols) : 0; - sx += drop; - sw -= drop; - } else { - uint32_t px = (uint32_t)cut * g->cell_px_w; - px = px > xo ? px - xo : 0; - xo = 0; - sx += px < sw ? px : sw; - sw = px < sw ? sw - px : 0; - } - col = (uint16_t)(col + cut); - cols = (uint16_t)(cols - cut); - } else if (ox1 >= px1) { - uint16_t keep = (uint16_t)(ox0 - px0); - if (g->req_cols) { - sw = cols ? (uint32_t)((uint64_t)sw * keep / cols) : sw; - } else { - uint32_t px = (uint32_t)keep * g->cell_px_w; - px = px > xo ? px - xo : 0; - if (sw > px) sw = px; - } - cols = keep; - } else { - return; - } - } else { - return; /* a corner: the remainder is an L, not a rect */ - } - if (!cols || !rows || !sw || !sh) return; + * what is left of the image once the floats above it are taken away. + * + * That remainder is not always one rectangle -- a float across the middle + * of an image leaves two, a float in a corner leaves two, a float wholly + * inside it leaves four -- and a placement is one rectangle. So the + * remainder is cut into rectangles and each one is placed in its own + * right, which is a shape the protocol does have. It used to be suppressed + * instead, on the grounds that an L is not a rect: the whole image vanished + * the moment a floating pane crossed it, on the terminal and in anything + * mirroring it, and came back when the float moved off. */ + rect_t whole = {(uint16_t)(leaf->content.x + g->col), + (uint16_t)(leaf->content.y + g->row), cols, rows}; + rect_t vis[GFX_MAX_PIECES]; + size_t nvis = rect_subtract(whole, c->occ, c->nocc, vis, GFX_MAX_PIECES); + for (size_t i = 0; i < nvis; i++) { + /* Where the piece starts inside the placement, in cells, is all the + * source rectangle needs -- the pane-edge arithmetic again, aimed at a + * cut that can be on any side now. */ + uint32_t psx = g->sx, psy = g->sy, psw = sw, psh = sh; + uint32_t pxo = g->x_off, pyo = g->y_off; + axis_crop(g->req_cols != 0, cols, (uint16_t)(vis[i].x - whole.x), vis[i].w, + g->cell_px_w, &psx, &psw, &pxo); + axis_crop(g->req_rows != 0, rows, (uint16_t)(vis[i].y - whole.y), vis[i].h, + g->cell_px_h, &psy, &psh, &pyo); + gfx_place_view(c->out, + (gfx_req_t){ + .pane = leaf->id, + .src_id = g->image_id, + .gen = g->generation, + .place_id = g->place_id, + .piece = (uint16_t)i, + .col = vis[i].x, + .row = vis[i].y, + .cols = vis[i].w, + .rows = vis[i].h, + .x_off = pxo, + .y_off = pyo, + /* Zero when the program never asked to scale; when it + * did, the piece scales into the cells it covers. */ + .scale_cols = g->req_cols ? vis[i].w : 0, + .scale_rows = g->req_rows ? vis[i].h : 0, + .sx = psx, + .sy = psy, + .sw = psw, + .sh = psh, + .px_w = g->src_w, + .px_h = g->src_h, + .cell_px_w = g->cell_px_w, + .cell_px_h = g->cell_px_h, + .format = g->format, + .compression = g->compression, + .data = g->data, + .data_len = g->data_len, + }, + c->viewport); } - - gfx_place_view( - c->out, - (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 + col), - .row = (uint16_t)(leaf->content.y + row), - .cols = cols, - .rows = rows, - /* The offsets survive the pane's own clipping untouched; an - * occlusion trim from the left or top zeroes the one it consumed. */ - .x_off = xo, - .y_off = yo, - /* 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 = sx, - .sy = sy, - .sw = sw, - .sh = sh, - .px_w = g->src_w, - .px_h = g->src_h, - .cell_px_w = g->cell_px_w, - .cell_px_h = g->cell_px_h, - .format = g->format, - .compression = g->compression, - .data = g->data, - .data_len = g->data_len, - }, - c->viewport); } /* One frame's worth of "what covers what": the visible floats, so every @@ -2554,6 +2583,9 @@ char *app_graphics_json(app_t *a) { json_obj_open(&j, NULL); json_int(&j, "image", places[i].out_id); json_int(&j, "placement", places[i].place_id); + /* Which rectangle of that placement: a float lying across an image + * leaves more than one, and they are only told apart by this. */ + json_int(&j, "piece", places[i].piece); json_int(&j, "x", places[i].col); json_int(&j, "y", places[i].row); json_int(&j, "cols", places[i].cols); diff --git a/src/graphics.c b/src/graphics.c index 3330ece..77b3b7f 100644 --- a/src/graphics.c +++ b/src/graphics.c @@ -18,7 +18,7 @@ * pinned to a spot on the screen no scrolling will move. The queue survives * until gfx_commit() says the client actually heard it. */ typedef struct { - uint32_t out_id, place_id; + uint32_t out_id, wire_id; } gfx_dead_t; struct graphics { @@ -27,6 +27,7 @@ struct graphics { gfx_place_t *places; size_t nplaces, placecap; uint32_t next_out_id; + uint32_t next_wire_id; /* placement ids we hand out; see gfx_place_t */ gfx_dead_t *dead; /* deletions owed to the client */ size_t ndead, deadcap; @@ -85,6 +86,7 @@ static void out_b64(graphics_t *g, const uint8_t *in, size_t len) { graphics_t *gfx_new(void) { graphics_t *g = calloc(1, sizeof *g); g->next_out_id = OUT_ID_BASE; + g->next_wire_id = 1; return g; } @@ -223,7 +225,8 @@ void gfx_place(graphics_t *g, const gfx_req_t *req) { 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) + if (g->places[i].out_id == img->out_id && g->places[i].place_id == pid && + g->places[i].piece == req->piece) slot = &g->places[i]; if (!slot) { if (g->nplaces == g->placecap) { @@ -234,6 +237,8 @@ void gfx_place(graphics_t *g, const gfx_req_t *req) { memset(slot, 0, sizeof *slot); slot->out_id = img->out_id; slot->place_id = pid; + slot->piece = req->piece; + slot->wire_id = g->next_wire_id++; } /* What the client is holding, against what it should be holding. A * placement that has not moved, resized or re-cropped is left alone: see @@ -299,14 +304,14 @@ char *gfx_flush(graphics_t *g, size_t *out_len) { } bool queued = false; for (size_t j = 0; j < g->ndead; j++) - if (g->dead[j].out_id == p->out_id && g->dead[j].place_id == p->place_id) + if (g->dead[j].out_id == p->out_id && g->dead[j].wire_id == p->wire_id) queued = true; if (!queued) { if (g->ndead == g->deadcap) { g->deadcap = g->deadcap ? g->deadcap * 2 : 8; g->dead = realloc(g->dead, g->deadcap * sizeof *g->dead); } - g->dead[g->ndead++] = (gfx_dead_t){p->out_id, p->place_id}; + g->dead[g->ndead++] = (gfx_dead_t){p->out_id, p->wire_id}; } } g->nplaces = keep; @@ -315,7 +320,7 @@ char *gfx_flush(graphics_t *g, size_t *out_len) { * came back is deleted and re-placed in the same stream, which nets out. */ for (size_t i = 0; i < g->ndead; i++) out_fmt(g, "\x1b_Ga=d,d=i,q=2,i=%u,p=%u\x1b\\", g->dead[i].out_id, - g->dead[i].place_id); + g->dead[i].wire_id); /* Then place what the client is not already holding. C=1 keeps the cursor * where the text renderer left it, which matters because we are @@ -350,7 +355,7 @@ char *gfx_flush(graphics_t *g, size_t *out_len) { * cells (dim_unfocused paints one over every cell it touches) hides the * picture completely -- on the mirrored copy only, while the terminal, * which took the default, still shows it. */ - out_fmt(g, "\x1b_Ga=p,q=2,C=1,z=0,i=%u,p=%u", p->out_id, p->place_id); + out_fmt(g, "\x1b_Ga=p,q=2,C=1,z=0,i=%u,p=%u", p->out_id, p->wire_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 diff --git a/tests/test_graphics.py b/tests/test_graphics.py index 50edf43..aabe87a 100644 --- a/tests/test_graphics.py +++ b/tests/test_graphics.py @@ -612,10 +612,12 @@ def test_scrolled_away_placements_are_dropped(): def test_a_float_occludes_a_placement(): """The cell compositor gets occlusion free from paint order; placements - are sent after the diff and get it from clipping (D22): a clean edge - crops -- the pane-edge arithmetic aimed at another clipper -- and a - float in the middle is a shape one placement cannot express, so that - placement is suppressed for the frame and returns when the float moves. + are sent after the diff and get it from clipping (D22): what is left of + the image once the float is taken out of it. A clean edge leaves one + rectangle, and anything else leaves several -- each placed in its own + right, because a placement is one rectangle and the remainder need not + be. Suppressing the image instead, which is what this used to do, meant + a floating pane crossing a picture erased all of it. Every pane here runs the same command, so the float shows the image too; the tiled pane's placement is told apart by the image id it had before @@ -642,10 +644,22 @@ def test_a_float_occludes_a_placement(): str(got), ) - # Across the middle: no single rect can say what remains. + # Across the middle: what is left is the columns on either side of it. s.api("float", x=img["x"] + 10, y=0, w=30, h=28) s.settle(20) - check("a middle strip suppresses it", mine() == [], str(places(s))) + got = sorted(mine(), key=lambda p: p["x"]) + check( + "a middle strip leaves the columns beside it", + len(got) == 2 + and (got[0]["x"], got[0]["cols"]) == (img["x"], 10) + and (got[1]["x"], got[1]["cols"]) == (img["x"] + 40, img["cols"] - 40), + str(got), + ) + check( + "and each piece crops to the cells it covers", + all(p["cols"] and p["rows"] == img["rows"] for p in got), + str(got), + ) # Moved clear: the placement returns whole. s.api("float", x=img["x"], y=14, w=30, h=12) @@ -657,6 +671,25 @@ def test_a_float_occludes_a_placement(): str(got), ) + # Landing inside it: what is left is the strips around the float, and + # they tile exactly the image minus what the float covers -- the + # float's rect being its frame's, not the content's. + s.api("float", x=img["x"] + 10, y=img["y"] + 1, w=20, h=3) + s.settle(20) + got = mine() + fl = [p for p in s.api("panes")["panes"] if p["floating"]][0] + ox0, oy0 = max(img["x"], fl["x"]), max(img["y"], fl["y"]) + ox1 = min(img["x"] + img["cols"], fl["x"] + fl["w"]) + oy1 = min(img["y"] + img["rows"], fl["y"] + fl["h"]) + hidden = max(0, ox1 - ox0) * max(0, oy1 - oy0) + covered = sum(p["cols"] * p["rows"] for p in got) + check( + "a float over the middle of an image leaves the strips around it", + len(got) > 1 and covered == img["cols"] * img["rows"] - hidden, + f"{len(got)} pieces covering {covered} of " + f"{img['cols'] * img['rows'] - hidden}: {got}", + ) + # Over the top rows: cropped from the top, the source origin moving. s.api("float", x=0, y=0, w=100, h=img["y"] + 3) s.settle(20) |
