diff options
| author | user <user@clank> | 2026-09-29 14:26:28 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-09-29 14:26:28 +0200 |
| commit | ca1b5bdf5bf7b5ff01ab515846802b3487599d3a (patch) | |
| tree | a047fc09bccbe077b98acd0bf2648c2e02616b04 /src | |
| parent | panes: read and write a pane by id, without taking focus (diff) | |
| download | slosh-ca1b5bdf5bf7b5ff01ab515846802b3487599d3a.tar.gz slosh-ca1b5bdf5bf7b5ff01ab515846802b3487599d3a.tar.xz slosh-ca1b5bdf5bf7b5ff01ab515846802b3487599d3a.zip | |
layouts, moves: build panes without pulling the view to them
Applying a layout landed on the first tab it made, and moving a pane into a
tab took focus there. For a program building a pane in the background that
is two failures at once: it yanks the view off the human mid-sentence, and
it leaves the *next* caller to read focus as "where I am" and put its work
in a stranger's tab.
`apply-layout focus:false` builds the tabs and keeps looking at whatever was
on screen; a layout naming an `active` tab still wins, because that is a
statement about where to be. `move-pane` gains `beside` (which pane in the
destination to split, rather than whatever it has focused) and `focus:false`
-- the two things a caller placing a pane next to *itself* needs.
That uncovered a sizing bug that was unreachable before: a pane's size comes
from a layout pass, and a tab nobody has looked at never had one, so a pane
built in the background stayed 1x1 and its program drew into a single cell.
The jump is what used to size it. layout_tab() gives a background tab a real
pass, borrowing the tab cursor for its length -- nothing composes, reads
input or draws a frame in between.
Diffstat (limited to 'src')
| -rw-r--r-- | src/app_layout.c | 59 | ||||
| -rw-r--r-- | src/app_session.c | 17 | ||||
| -rw-r--r-- | src/cmd.c | 14 |
3 files changed, 85 insertions, 5 deletions
diff --git a/src/app_layout.c b/src/app_layout.c index 7979b08..dae183a 100644 --- a/src/app_layout.c +++ b/src/app_layout.c @@ -447,6 +447,32 @@ static rect_t tab_area(app_t *a) { * second opinion about where the layout put everything. */ rect_t app_tab_area(app_t *a) { return tab_area(a); } +/* Lay out a tab that is not the one on screen. + * + * `layout` is written around `cur(a)` -- zoom, focus, the minimised strip and + * the floats are all that tab's -- so this borrows the cursor for the length + * of one pass rather than growing a second copy of the arithmetic. Nothing + * observes `a->cur` in between: no compose, no input, no frame. + * + * It exists because a pane's size comes from a layout pass, and a tab nobody + * has looked at never had one: a pane built or moved into a background tab + * stayed 1x1, and a program in it drew into a single cell. That was invisible + * for as long as making a pane elsewhere *also* jumped the view to it -- the + * jump is what sized it. Panes can now be made without that jump, so the + * sizing has to be asked for on purpose. */ +void layout_tab(app_t *a, size_t ti) { + if (!a->ntabs || ti >= a->ntabs) return; + if (ti == a->cur) { + layout(a); + return; + } + size_t save = a->cur; + a->cur = ti; + layout(a); + a->cur = save; + layout(a); /* leave the viewed tab's geometry as it was found */ +} + void layout(app_t *a) { if (!a->ntabs) return; /* Re-derived below, like everything else about a layout pass: whether this @@ -1460,10 +1486,31 @@ static bool detach_leaf(app_t *a, node_t *leaf, size_t *from, bool *emptied) { * mechanism. */ bool app_move_pane_to_tab(app_t *a, uint32_t pane_id, uint32_t tab_id, bool rows) { + return app_move_pane_beside(a, pane_id, tab_id, 0, rows, true); +} + +/* The same move, told exactly where to land and whether to take focus there. + * + * `beside` is the pane in the destination to split, rather than "whatever that + * tab has focused": a program moving a pane in beside *itself* knows which + * pane that is, and the tab's focus is a fact about the last person who looked + * at it. `take_focus` false leaves focus where it was, which is what a caller + * doing background work wants -- focusing the moved pane also selects its tab + * (see app_focus_pane), so a move used to end with the view somewhere nobody + * asked for. `beside` 0 keeps the old behaviour. */ +bool app_move_pane_beside(app_t *a, uint32_t pane_id, uint32_t tab_id, + uint32_t beside_id, bool rows, bool take_focus) { node_t *leaf = pane_id ? pane_by_id(a, pane_id) : cur(a)->focus; if (!leaf || leaf->kind != NODE_LEAF) return false; tab_t *dest = tab_by_id(a, tab_id); if (!dest || !dest->root) return false; + /* A `beside` that is not in the destination tab is a caller with the wrong + * idea of where things are; refusing beats silently landing elsewhere. */ + node_t *want = beside_id ? pane_by_id(a, beside_id) : NULL; + if (beside_id && (!want || want->kind != NODE_LEAF || + tab_of(a, want) != (size_t)(dest - a->tabs))) + return false; + uint32_t want_id = beside_id; size_t src_ti = tab_of(a, leaf); if (src_ti == (size_t)-1 || &a->tabs[src_ti] == dest) return false; @@ -1479,9 +1526,17 @@ bool app_move_pane_to_tab(app_t *a, uint32_t pane_id, uint32_t tab_id, node_free(leaf); return false; } - node_t *beside = dest->focus ? dest->focus : first_leaf(dest->root); + /* Re-found by id for the same reason the tab was: the removal above can have + * moved the node array under us. */ + node_t *beside = want_id ? pane_by_id(a, want_id) : NULL; + if (!beside) beside = dest->focus ? dest->focus : first_leaf(dest->root); + node_t *had_focus = dest->focus; place_beside(a, dest, beside, leaf, rows ? SPLIT_ROWS : SPLIT_COLS, false); - dest->focus = leaf; + dest->focus = take_focus ? leaf : (had_focus ? had_focus : leaf); + /* The destination may not be the tab on screen, and the pane that just + * arrived needs a size either way: a program told it is 1x1 draws into one + * cell and has no reason to ask again. */ + layout_tab(a, (size_t)(dest - a->tabs)); /* Stay where we were looking, unless that tab is the one that just went. */ for (size_t i = 0; i < a->ntabs; i++) diff --git a/src/app_session.c b/src/app_session.c index 716b5d1..8900088 100644 --- a/src/app_session.c +++ b/src/app_session.c @@ -147,6 +147,10 @@ static node_t *build_pane(app_t *a, const kdl_node_t *node, const char *cwd, bool app_apply_layout(app_t *a, const kdl_node_t *root, bool replace, const char *base, char *err, size_t errcap) { + /* Taken and cleared up front, so one request's "keep my view" cannot leak + * into the next layout applied for any other reason. */ + bool keep_view = a->layout_keep_view; + a->layout_keep_view = false; a->restore_tab = (size_t)-1; const kdl_node_t *lay = kdl_child(root, "layout"); if (!lay) lay = root; /* allow a bare list of tabs */ @@ -227,13 +231,24 @@ bool app_apply_layout(app_t *a, const kdl_node_t *root, bool replace, a->restore_tab -= before; if (a->restore_tab != (size_t)-1 && a->restore_tab < a->ntabs) a->cur = a->restore_tab; - else + else if (!keep_view) a->cur = replace ? 0 : before; + else if (a->cur >= a->ntabs && a->ntabs) + a->cur = a->ntabs - 1; /* the view we kept is gone (replace): clamp */ a->restore_tab = (size_t)-1; layout(a); + /* Every tab this layout built gets a pass, on screen or not: a pane's size + * comes from a layout pass, and `focus:false` means nobody is going to look + * at these and give them one. Without it a background pane runs at 1x1 -- + * which used to be impossible to hit only because applying a layout always + * jumped the view to it. */ + if (keep_view && !replace) + for (size_t i = before; i < a->ntabs; i++) layout_tab(a, i); return true; } +void app_layout_keep_view(app_t *a) { a->layout_keep_view = true; } + bool app_apply_layout_text_at(app_t *a, const char *text, bool replace, const char *base, char *err, size_t errcap) { kdl_node_t *root = kdl_parse(text, err, errcap); @@ -271,8 +271,15 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in, if (!made) return jerr("cannot move it to a tab of its own"); return jok_int("tab", (long long)made); } - if (!app_move_pane_to_tab(a, pid, tid, rows)) - return jerr("cannot move it there"); + /* `beside` says which pane in the destination to land next to, and + * `focus:false` says not to take focus (and so not to pull the view) + * on arrival: the two things a caller placing a pane beside *itself* + * needs, neither of which can be expressed by "that tab's focus". */ + uint32_t beside = (uint32_t)jv_geti(req, "beside", 0); + bool take_focus = jv_getb(req, "focus", true); + if (!app_move_pane_beside(a, pid, tid, beside, rows, take_focus)) + return jerr(beside ? "cannot move it there (is `beside` in that tab?)" + : "cannot move it there"); return jok_int("tab", (long long)tid); } if (strcmp(cmd, "new-tab") == 0) { @@ -352,6 +359,9 @@ static char *cmd_json(app_t *a, screen_t *s, input_parser_t *in, const char *path = jv_gets(req, "path", NULL); const char *text = jv_gets(req, "kdl", NULL); bool replace = jv_getb(req, "replace", false); + /* `focus:false` builds the tabs without going to them. A layout that names + * an `active` tab still wins -- that is a statement about where to be. */ + if (!jv_getb(req, "focus", true)) app_layout_keep_view(a); char err[256] = {0}; bool ok = text ? app_apply_layout_text(a, text, replace, err, sizeof err) : path ? app_apply_layout_file(a, path, replace, err, sizeof err) |
