diff options
| author | user <user@clank> | 2026-08-20 23:53:43 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-20 23:53:43 +0200 |
| commit | 64db2051192a6a6f5a9333f503d14b1ccccd699d (patch) | |
| tree | f14bdeee8ccbe6f413e20cf2b746ec16f4414ee1 /src | |
| parent | slosh: fold floating panes and the drop grammar into DESIGN.md (diff) | |
| download | slosh-64db2051192a6a6f5a9333f503d14b1ccccd699d.tar.gz slosh-64db2051192a6a6f5a9333f503d14b1ccccd699d.tar.xz slosh-64db2051192a6a6f5a9333f503d14b1ccccd699d.zip | |
slosh: M10c, the memory -- floats survive being written down
A dumped session quietly re-tiled its floats, which was the one data-loss
gap left in D22. dump-layout now records `floating=true x= y= w= h=` -- the
*wanted* rect, because intent is what survives a session and the next
screen's layout clamps it like any frame -- layout files declare the same
(linted: floating is a bool, x/y/w/h mean nothing without it), and a file
whose every pane floats self-heals through ensure_tiled: the top float
lands, the same rule closing the last tiled pane follows, no lint required.
save-workspace inherits all of it from the shared dumper.
`float` over the API takes x/y/w/h and then *places* rather than toggles --
a placement is a statement about where, not about whether -- floating the
pane first when it is tiled, re-placing when it already floats, with
omitted fields meaning "keep". The intent is stored exactly as asked: the
write-back rule is for hands, whose drags need the clamp to agree with the
eye; a script means what it typed.
And kitty graphics respect the stack. The cell compositor gets occlusion
free from paint order, but placements are sent after the diff and would
paint an image straight over a float covering it. Each pane's placements
are now clipped by the float rects above it -- every float for a tiled
pane, the higher-raised for a float -- cropping what one clean edge can
express (the pane-edge source-rectangle arithmetic aimed at another
clipper, including from the top and left, where the source origin moves and
the sub-cell offset is consumed) and suppressing what it cannot: a strip
across the middle or a corner overlap leaves a shape one placement cannot
say, and the image returns whole when the float moves. No z= needed --
clipping is the z-order, since nothing is ever allowed to overlap.
D22's "still owed" paragraph is paid and rewritten; M10 is fully checked.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app_draw.c | 141 | ||||
| -rw-r--r-- | src/app_layout.c | 21 | ||||
| -rw-r--r-- | src/app_session.c | 41 | ||||
| -rw-r--r-- | src/cmd.c | 13 |
4 files changed, 199 insertions, 17 deletions
diff --git a/src/app_draw.c b/src/app_draw.c index d96ceab..24cc50d 100644 --- a/src/app_draw.c +++ b/src/app_draw.c @@ -1873,6 +1873,13 @@ void draw_floats(app_t *a, screen_t *s) { struct gfx_ctx { app_t *a; node_t *leaf; + /* The float rects drawn over this leaf's cells: every visible float for a + * tiled pane, the higher-raised ones for a float. A placement is clipped + * against them below, because kitty images are drawn by the client's + * terminal *after* the cell diff — left alone, a picture in a tiled pane + * paints straight over the float covering it. */ + const rect_t *occ; + size_t nocc; }; static void gfx_from_pane(pane_t *p, const pane_gfx_t *g, void *ud) { @@ -1922,6 +1929,90 @@ static void gfx_from_pane(pane_t *p, const pane_gfx_t *g, void *ud) { } if (!cols || !rows) return; + /* 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; + } + gfx_place( c->a->gfx, &(gfx_req_t){ @@ -1929,20 +2020,20 @@ static void gfx_from_pane(pane_t *p, const pane_gfx_t *g, void *ud) { .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), + .col = (uint16_t)(leaf->content.x + col), + .row = (uint16_t)(leaf->content.y + 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, + /* 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 = g->sx, - .sy = g->sy, + .sx = sx, + .sy = sy, .sw = sw, .sh = sh, .px_w = g->src_w, @@ -1954,10 +2045,26 @@ static void gfx_from_pane(pane_t *p, const pane_gfx_t *g, void *ud) { }); } +/* One frame's worth of "what covers what": the visible floats, so every + * leaf's placements can be clipped by the ones above it. */ +struct gfx_walk { + app_t *a; + rect_t occ[64]; + uint32_t raised[64]; + size_t n; +}; + static void gfx_leaf_cb(node_t *n, void *ud) { - app_t *a = ud; - if (n->hidden) return; /* a collapsed pane draws nothing, images included */ - struct gfx_ctx ctx = {a, n}; + struct gfx_walk *w = ud; + if (n->hidden || n->collapsed) return; /* not drawn: images included */ + /* Above this leaf: every float for a tiled pane, the higher-raised for a + * float — the same order draw_floats paints in, so an image is clipped by + * exactly what its cells are covered by. */ + rect_t occ[64]; + size_t k = 0; + for (size_t i = 0; i < w->n; i++) + if (!n->floating || w->raised[i] > n->raised) occ[k++] = w->occ[i]; + struct gfx_ctx ctx = {w->a, n, occ, k}; pane_graphics(n->pane, gfx_from_pane, &ctx); } @@ -1965,7 +2072,17 @@ static void gfx_leaf_cb(node_t *n, void *ud) { * needs this frame. Borrowed until the next call. */ const char *app_graphics(app_t *a, size_t *len) { gfx_begin(a->gfx); - if (a->ntabs && cur(a)->root) walk(cur(a)->root, gfx_leaf_cb, a); + if (a->ntabs && cur(a)->root) { + struct gfx_walk w = {.a = a}; + node_t *fl[64]; + size_t nf = collect_floating(cur(a)->root, fl, 64, 0); + for (size_t i = 0; i < nf && w.n < 64; i++) { + if (fl[i]->hidden || fl[i]->collapsed) continue; + w.occ[w.n] = fl[i]->rect; + w.raised[w.n++] = fl[i]->raised; + } + walk(cur(a)->root, gfx_leaf_cb, &w); + } return gfx_flush(a->gfx, len); } diff --git a/src/app_layout.c b/src/app_layout.c index d2fc0b1..266775f 100644 --- a/src/app_layout.c +++ b/src/app_layout.c @@ -1083,6 +1083,27 @@ bool app_pane_floating(app_t *a, uint32_t id) { return n && n->floating; } +/* Float a pane *at* a rect: the script's version of the drag. Already + * floating, it is re-placed without a toggle — a placement is a statement + * about where, not about whether — and the intent is kept exactly as asked + * rather than written back clamped: a script that places at 500,500 on an + * 80-column session means "the far corner", and the layout clamps the drawn + * rect every frame the way it always does. Zero width or height means "keep + * what it has", so a caller can move without resizing. */ +bool app_float_place(app_t *a, uint32_t id, long x, long y, long w, long h) { + node_t *n = id ? pane_by_id(a, id) : (a->ntabs ? cur(a)->focus : NULL); + if (!n || n->kind != NODE_LEAF) return false; + if (!n->floating && !app_toggle_float(a, id)) return false; + rect_t want = n->float_rect; + if (w > 0) want.w = (uint16_t)w; + if (h > 0) want.h = (uint16_t)h; + if (x >= 0) want.x = (uint16_t)x; + if (y >= 0) want.y = (uint16_t)y; + n->float_rect = want; + layout(a); + return true; +} + /* An explicit move or resize is the user authoring the intent, so unlike a * terminal resize it *does* write back: the delta is applied to the rect as * drawn — what you grabbed is what moves, even when an older intent is diff --git a/src/app_session.c b/src/app_session.c index 0b228ce..634239f 100644 --- a/src/app_session.c +++ b/src/app_session.c @@ -110,6 +110,21 @@ static node_t *build_pane(app_t *a, const kdl_node_t *node, const char *cwd, if (!leaf) return NULL; leaf->weight = (int)kdl_prop_int(node, "weight", WEIGHT_UNIT); if (leaf->weight < WEIGHT_MIN) leaf->weight = WEIGHT_UNIT; + /* A float restores as a float (D22): the wanted rect is the intent that + * was dumped, and the layout clamps it into whatever screen this session + * has — absent, or nonsense, the centred default answers. A file whose + * every pane floats self-heals through ensure_tiled: the top float lands, + * which is the same rule closing the last tiled pane follows. */ + if (kdl_prop_bool(node, "floating", false)) { + leaf->floating = true; + leaf->raised = ++a->raise_seq; + long fx = kdl_prop_int(node, "x", 0), fy = kdl_prop_int(node, "y", 0); + long fw = kdl_prop_int(node, "w", 0), fh = kdl_prop_int(node, "h", 0); + if (fw > 0 && fh > 0) + leaf->float_rect = + (rect_t){(uint16_t)(fx < 0 ? 0 : fx), (uint16_t)(fy < 0 ? 0 : fy), + (uint16_t)fw, (uint16_t)fh}; + } /* `focus=true` restores which pane you were in. Recorded on the node and * resolved once the tab exists, because focus belongs to the tab. */ if (kdl_prop_bool(node, "focus", false)) a->restore_focus = leaf; @@ -257,7 +272,8 @@ bool app_apply_layout_file(app_t *a, const char *path, bool replace, char *err, /* Read by build_pane, on a `pane` node or on a tab acting as its own root. */ static const char *const PANE_PROPS[] = { - "split", "weight", "cwd", "command", "focus", "purpose", "suspended"}; + "split", "weight", "cwd", "command", "focus", "purpose", + "suspended", "floating", "x", "y", "w", "h"}; /* Read by app_apply_layout off the tab itself. */ static const char *const TAB_PROPS[] = {"name", "active"}; @@ -314,13 +330,22 @@ static void lc_props(lcheck_t *c, const kdl_node_t *n, bool is_tab) { * comes to tag nothing at all. A tab reads its own name and purpose either * way, so those are never the ignored ones. */ bool leaf_only = strcmp(k, "command") == 0 || strcmp(k, "suspended") == 0 || - strcmp(k, "focus") == 0 || + strcmp(k, "focus") == 0 || strcmp(k, "floating") == 0 || + strcmp(k, "x") == 0 || strcmp(k, "y") == 0 || + strcmp(k, "w") == 0 || strcmp(k, "h") == 0 || (!is_tab && strcmp(k, "purpose") == 0); if (kids && leaf_only) lc_say(c, n->line, "%s is ignored on a %s with panes in it", k, is_tab ? "tab" : "pane"); if (!kids && strcmp(k, "split") == 0) lc_say(c, n->line, "split is ignored on a pane with nothing to split"); + /* The wanted rect belongs to a float; on a tiled pane it would be read + * and mean nothing, which is the quiet kind of wrong this pass exists + * to say out loud. */ + bool rectp = strcmp(k, "x") == 0 || strcmp(k, "y") == 0 || + strcmp(k, "w") == 0 || strcmp(k, "h") == 0; + if (rectp && !kids && !kdl_prop_bool(n, "floating", false)) + lc_say(c, n->line, "%s is ignored without floating=true", k); } const char *dir = kdl_prop(n, "split", NULL); @@ -338,6 +363,7 @@ static void lc_props(lcheck_t *c, const kdl_node_t *n, bool is_tab) { lc_bool(c, n, "suspended"); lc_bool(c, n, "focus"); + lc_bool(c, n, "floating"); if (is_tab) lc_bool(c, n, "active"); for (size_t i = 0; i < n->nkids; i++) { @@ -836,6 +862,17 @@ static void dump_node(node_t *n, strbuf_t *b, int depth, dumpctx_t *ctx) { sb_add(b, " suspended=true"); ctx->suspended++; } + /* The wanted rect, not the drawn one: intent is what survives a session, + * and the layout of whatever screen this file is opened on will clamp it + * the way it clamps every frame. Written in tree order; the raise order is + * not kept, because focus re-raises and a stack of floats worth restoring + * exactly is a session, not a shape. */ + if (n->floating) { + sb_add(b, " floating=true"); + if (n->float_rect.w) + sb_add(b, " x=%u y=%u w=%u h=%u", n->float_rect.x, n->float_rect.y, + n->float_rect.w, n->float_rect.h); + } if (n == ctx->tab->focus) sb_add(b, " focus=true"); sb_add(b, "\n"); } @@ -191,13 +191,20 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in, if (!id) return jerr("cannot open a pane"); return jok_int("id", id); } - /* Toggle, like the button and the palette entry: M10c grows placement - * arguments here, this is the same one verb everything else runs. */ + /* Bare, a toggle — the same one verb the key and the palette run. With + * any of x/y/w/h it *places*: floats the pane first when it is tiled, + * re-places it when it already floats, and never un-floats — a placement + * is a statement about where, not about whether. */ if (strcmp(cmd, "float") == 0) { uint32_t id = (uint32_t)jv_geti(req, "id", 0); + long x = jv_geti(req, "x", -1), y = jv_geti(req, "y", -1); + long w = jv_geti(req, "w", 0), h = jv_geti(req, "h", 0); + bool place = x >= 0 || y >= 0 || w > 0 || h > 0; /* Two refusals share the word: no such pane, and no tiled pane left to * float over. */ - if (!app_toggle_float(a, id)) return jerr("cannot float that"); + bool ok = + place ? app_float_place(a, id, x, y, w, h) : app_toggle_float(a, id); + if (!ok) return jerr("cannot float that"); return jok_int("floating", app_pane_floating(a, id) ? 1 : 0); } if (strcmp(cmd, "edit-config") == 0) { |
