diff options
| author | user <user@clank> | 2026-08-14 14:45:49 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-14 14:45:49 +0200 |
| commit | 037c3550f075ff78e680e5a1f1508d4c292ffcb1 (patch) | |
| tree | 18c085806ac40d1e5ff9b49e6cee668732152fde /src | |
| parent | sl0ppty: pane states are a table, not three hardcoded knobs (diff) | |
| download | slosh-037c3550f075ff78e680e5a1f1508d4c292ffcb1.tar.gz slosh-037c3550f075ff78e680e5a1f1508d4c292ffcb1.tar.xz slosh-037c3550f075ff78e680e5a1f1508d4c292ffcb1.zip | |
sl0ppty: a floor under splitting, and an arrow saying which way
Two changes to the same affordance.
A border stops being a button below `min_split cols=32 rows=8`. This is a
different question from min_pane, which is where the layout gives up and
collapses a pane: two twenty-five column panes technically fit and are not
worth having, so the number that decides whether to *offer* a split is larger
than the one that decides whether a pane can survive. It is clamped to never
go below min_pane, because refusing to offer what the layout would collapse
anyway is the floor under the floor.
The test is the layout's own arithmetic rather than an approximation of it:
the same floor, the same gap, and the same child count the split would create
— which is one more sibling when the parent already splits that way and two
when it does not. The guide's standing claim is that drawing and the layout
cannot disagree, and a guide promising a split that immediately collapsed
would be exactly that.
The keyboard obeys the same floor, with a toast. A rule only the mouse
followed would mean the same session splits or refuses depending on how you
asked, and whether there is room cannot depend on that. The control API and
layout files are *not* bound by it: a script asking for a pane is declaring
what it wants rather than being offered something, and D6's responsive
collapse is what catches it there. That distinction was not free — app_split_pane
went through the same helper as the keybinding, so the API was silently bound
too until a test noticed, and the interactive path is now its own function.
The guide also gained an arrow on the dashed line, pointing at the half the
new pane will take. The line already said where the boundary lands and left
which side to be inferred from which border you happened to be touching.
Pointers for left and right rather than the matching triangles: U+25C0 and
U+25B6 carry emoji presentation and are widely rendered double-width, while
screen_text books every chrome glyph as exactly one cell — so one that drew as
two would shift the rest of the row. U+25B2 is already the scroll indicator,
so the vertical pair is known good here.
Raising a floor invalidates every test that quietly relied on the old one, and
nine of them did: sessions sized for a world where a sixty-column terminal
could be split into columns. They are sized for the rule now, and the two that
were really about *layout* collapse rather than the affordance now split over
the API, which is the path that still means "do it anyway".
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.c | 96 | ||||
| -rw-r--r-- | src/config.c | 10 | ||||
| -rw-r--r-- | src/config.h | 6 |
3 files changed, 104 insertions, 8 deletions
@@ -719,10 +719,65 @@ static void split_node(app_t *a, node_t *leaf, split_dir_t dir, bool before) { layout(a); } +/* Would splitting this pane produce two panes worth having? + * + * This is the layout's own arithmetic, not an approximation of it: the same + * floor, the same gap, and the same child count the split would actually + * create — which is one more sibling when the parent already splits this way, + * and two when it does not. Guessing here would let the guide promise a split + * that immediately collapsed, and the guide's whole claim is that drawing and + * the layout cannot disagree. */ +static bool split_fits(node_t *leaf, split_dir_t dir) { + /* Two different questions, and the larger answer wins: min_pane is where the + * layout gives up, min_split is where splitting stopped being worth + * offering. A pane can clear the first and still be too small to be worth + * halving — which is the whole point of having both. */ + uint16_t floor_ = dir == SPLIT_COLS ? CFG.min_split_cols : CFG.min_split_rows; + uint16_t hard = dir == SPLIT_COLS ? MIN_PANE_COLS : MIN_PANE_ROWS; + if (floor_ < hard) floor_ = hard; + uint16_t gap = + dir == SPLIT_COLS ? (uint16_t)(CFG.gap * CFG.gap_aspect) : CFG.gap; + + size_t k; + uint16_t total; + if (leaf->parent && leaf->parent->dir == dir) { + k = leaf->parent->nkids + 1; + total = dir == SPLIT_COLS ? leaf->parent->rect.w : leaf->parent->rect.h; + } else { + k = 2; + total = dir == SPLIT_COLS ? leaf->rect.w : leaf->rect.h; + } + return total >= (uint16_t)(k * floor_ + gap * (k - 1)); +} + +static split_dir_t side_dir(char side) { + return (side == 'l' || side == 'r') ? SPLIT_COLS : SPLIT_ROWS; +} + static void split_focus(app_t *a, split_dir_t dir) { split_node(a, cur(a)->focus, dir, false); } +/* Splitting because a person just asked for it, which is the only case the + * floor applies to. + * + * The keyboard obeys the same floor the border does: a rule only the mouse + * followed would mean the same session splits or refuses depending on how you + * asked, and whether there is room cannot depend on that. The control API and + * layout files go through split_focus() and are deliberately *not* bound by + * it — a script asking for a pane is declaring what it wants, not being + * offered something, and D6's responsive collapse is what catches it there. */ +static void split_focus_ui(app_t *a, split_dir_t dir) { + node_t *n = cur(a)->focus; + if (!n) return; + if (!split_fits(n, dir)) { + app_toast(a, dir == SPLIT_COLS ? "no room to split across" + : "no room to split down"); + return; + } + split_node(a, n, dir, false); +} + static node_t *first_leaf(node_t *n) { while (n && n->kind == NODE_SPLIT) n = n->kids[0]; return n; @@ -1247,11 +1302,24 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { } if (!side) return; + /* Nothing is offered that cannot be delivered: below the floor this pane + * would only collapse, so the border simply stops being a button. */ + if (!split_fits(leaf, side_dir(side))) return; + rect_t r = leaf->rect; if (r.w < 4 || r.h < 4) return; uint16_t x1 = (uint16_t)(r.x + r.w - 1), y1 = (uint16_t)(r.y + r.h - 1); color_t hi = FRAME_FOCUS; + /* An arrow on the new boundary, pointing at the side the new pane will take. + * The dashed line says where, and on its own leaves which half is the new + * one to be inferred from which border you happen to be touching. + * + * Pointers rather than the matching triangles for left and right: U+25C0 and + * U+25B6 carry emoji presentation and terminals widely render them + * double-width, and screen_text books every chrome glyph as one cell — so a + * cell that draws as two would shift the rest of the row. U+25B2 is already + * the scroll indicator, so the vertical pair is known good here. */ if (side == 'l' || side == 'r') { uint16_t bx = side == 'l' ? r.x : x1; uint16_t mid = (uint16_t)(r.x + r.w / 2); @@ -1259,6 +1327,8 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { screen_text(s, bx, y, "\u2503", hi, NO_COLOR, ATTR_BOLD); screen_text(s, mid, y, "\u254e", hi, NO_COLOR, 0); } + screen_text(s, mid, (uint16_t)(r.y + r.h / 2), + side == 'l' ? "\u25c4" : "\u25ba", hi, NO_COLOR, ATTR_BOLD); } else { uint16_t by = side == 't' ? r.y : y1; uint16_t mid = (uint16_t)(r.y + r.h / 2); @@ -1266,6 +1336,8 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { screen_text(s, x, by, "\u2501", hi, NO_COLOR, ATTR_BOLD); screen_text(s, x, mid, "\u254c", hi, NO_COLOR, 0); } + screen_text(s, (uint16_t)(r.x + r.w / 2), mid, + side == 't' ? "\u25b2" : "\u25bc", hi, NO_COLOR, ATTR_BOLD); } } @@ -2176,8 +2248,8 @@ static bool prefix_command(app_t *a, const input_event_t *ev) { return true; } switch (act) { - case ACT_SPLIT_COLS: split_focus(a, SPLIT_COLS); return true; - case ACT_SPLIT_ROWS: split_focus(a, SPLIT_ROWS); return true; + case ACT_SPLIT_COLS: split_focus_ui(a, SPLIT_COLS); return true; + case ACT_SPLIT_ROWS: split_focus_ui(a, SPLIT_ROWS); return true; case ACT_CLOSE_PANE: if (cur(a)->focus) close_leaf(a, cur(a)->focus); return true; @@ -2328,13 +2400,21 @@ void app_event(app_t *a, const input_event_t *ev) { node_t *n = pane_by_id(a, a->drag.src); if (n) { char side = a->drag.side; - bool rows = side == 't' || side == 'b'; bool before = side == 'l' || side == 't'; - split_node(a, n, rows ? SPLIT_ROWS : SPLIT_COLS, before); - app_toast(a, side == 'l' ? "split left" - : side == 'r' ? "split right" - : side == 't' ? "split up" - : "split down"); + split_dir_t dir = side_dir(side); + /* The guide already declined to offer this, so the click that + * the guide would have explained must decline too — otherwise + * the border silently does something it just said it would not. */ + if (!split_fits(n, dir)) { + app_toast(a, dir == SPLIT_COLS ? "no room to split across" + : "no room to split down"); + } else { + split_node(a, n, dir, before); + app_toast(a, side == 'l' ? "split left" + : side == 'r' ? "split right" + : side == 't' ? "split up" + : "split down"); + } } a->drag.kind = DRAG_NONE; a->drag.src = a->drag.target = 0; diff --git a/src/config.c b/src/config.c index fee7d54..56175eb 100644 --- a/src/config.c +++ b/src/config.c @@ -170,6 +170,8 @@ void config_defaults(config_t *c) { c->title_align = ALIGN_CENTER; c->min_pane_cols = 24; c->min_pane_rows = 6; + c->min_split_cols = 32; + c->min_split_rows = 8; c->scroll_lines = 3; c->toast_ms = 2500; c->hover_delay_ms = 250; @@ -301,6 +303,14 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) { else c->title_align = ALIGN_CENTER; } + const kdl_node_t *mins = kdl_child(root, "min_split"); + if (mins) { + c->min_split_cols = + (uint16_t)kdl_prop_int(mins, "cols", c->min_split_cols); + c->min_split_rows = + (uint16_t)kdl_prop_int(mins, "rows", c->min_split_rows); + } + const kdl_node_t *minp = kdl_child(root, "min_pane"); if (minp) { c->min_pane_cols = (uint16_t)kdl_prop_int(minp, "cols", c->min_pane_cols); diff --git a/src/config.h b/src/config.h index 098c9bc..cf10d74 100644 --- a/src/config.h +++ b/src/config.h @@ -75,6 +75,12 @@ typedef struct { bool rounded; align_t title_align; uint16_t min_pane_cols, min_pane_rows; + /* The smallest pane a split is allowed to *produce*. min_pane is the point + * below which the layout gives up and collapses a pane; this is the point + * below which splitting stopped being worth offering, which is a larger + * number and a different question. Never used below min_pane: refusing to + * offer something the layout would collapse anyway is the floor. */ + uint16_t min_split_cols, min_split_rows; uint16_t scroll_lines; uint16_t toast_ms; uint16_t hover_delay_ms; /* how long the pointer must rest to arm a guide */ /* how long an announcement stays up */ /* rows per wheel notch */ |
