aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-18 15:49:59 +0200
committeruser <user@clank>2026-08-18 15:49:59 +0200
commit0e48544210c58e55ebc17d399b390c6c3bba9492 (patch)
treea82ecbadc6601099c92cd3a2fa9a7fd6e381c444
parentsl0ppty: the middle of a border is the button, not all of it (diff)
downloadslosh-0e48544210c58e55ebc17d399b390c6c3bba9492.tar.gz
slosh-0e48544210c58e55ebc17d399b390c6c3bba9492.tar.xz
slosh-0e48544210c58e55ebc17d399b390c6c3bba9492.zip
sl0ppty: a horizontal handle gets heavier, not taller
The armed handle grew by one cell into the pane on all four sides, which is one change on two axes and therefore two different results. A cell is about twice as tall as it is wide -- the fact `gap_aspect` exists for, and the one the split picker measures its longer side by -- so the extra column on a left or right edge read as a thicker line, and the extra row on a top or bottom edge read as a wall. It also spent a row of somebody's output to say something the glyph was already saying: `█` is the heaviest thing one cell can draw, and against `━` it needs no help. So only the upright sides widen now. A horizontal handle stays on its own row. That also removes its claim on a content cell, which is why the inner-strip hit is registered on those two sides only -- the paint and the hit still describe the same rect, which is the whole reason the guide registers what it thickens. Verified: `make retest`, 1807 checks, all green -- six new, pinning the asymmetry in both directions: the top and bottom handles occupy exactly the row their border is on, the left and right ones exactly two columns, and the inner column of each upright handle hit-tests as that border rather than as the pane. Measured on a live session rather than argued: hovering the bottom border of a 116x36 pane puts seven block cells on row 37 and no others, inside the `━` rule; hovering the left border puts them on rows 16-22 across columns 2 and 3.
-rw-r--r--DESIGN.md29
-rw-r--r--docs/panes.md8
-rw-r--r--src/app.c18
-rw-r--r--tests/test_split_ux.py47
4 files changed, 82 insertions, 20 deletions
diff --git a/DESIGN.md b/DESIGN.md
index 2517df8..5c331d2 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -297,17 +297,26 @@ splits. The rest of the edge — the *rim* — arms the guide and does nothing w
clicked.
Hovering therefore answers two questions in two stages, because they are two
-questions. **Anywhere on the edge**: the edge goes heavy and the handle
-thickens by a cell into the pane, which says *where the button is* — the thing
-you can no longer infer now that the edge is not all button. **On the handle**:
-the dashed line where the new boundary would land and the arrow pointing at the
-half that will be new, which say *what the click will do*. The hint follows the
-same split: it is the caption on the second stage, so sweeping an edge neither
-draws a boundary nor claims a split you cannot get from where you are.
+questions. **Anywhere on the edge**: the edge goes heavy and the handle goes
+solid, which says *where the button is* — the thing you can no longer infer now
+that the edge is not all button. **On the handle**: the dashed line where the new
+boundary would land and the arrow pointing at the half that will be new, which
+say *what the click will do*. The hint follows the same split: it is the caption
+on the second stage, so sweeping an edge neither draws a boundary nor claims a
+split you cannot get from where you are.
-The handle is a rect in the hit list like everything else, so the thickened
-form is *registered* in the frame that paints it — a handle you can see and
-cannot click would be worse than either mistake alone.
+**Only the upright sides get wider.** A cell is about twice as tall as it is
+wide — the same fact `gap_aspect` exists for, and that the split picker measures
+its longer side by — so growing by one is not one thing on the two axes. A left
+or right handle takes a second column and reads as a thicker line. A top or
+bottom handle taking a second *row* read as a wall instead, and spent a row of
+somebody's output saying it, so those stay on their own row: the block glyph is
+the heaviest a single cell has, and against `━` it needs no help.
+
+The handle is a rect in the hit list like everything else, so where the heavier
+form claims a cell the pane was using, that cell is *registered* in the frame
+that paints it — a handle you can see and cannot click would be worse than
+either mistake alone.
The top border keeps its other job: **the row drags the pane, its handle also
splits upward**. A press that never moves is a click — the same distinction the
diff --git a/docs/panes.md b/docs/panes.md
index 7e8e745..fc27020 100644
--- a/docs/panes.md
+++ b/docs/panes.md
@@ -39,9 +39,11 @@ nothing you click can disagree with what is on screen.
is the side the new pane appears on. Only the middle of an edge splits: three
to seven cells of it, called the handle, because a whole edge is a long target
to brush past and a changed layout is an expensive accident. Resting anywhere
- on an edge thickens the handle so you can see where to click; resting *on* the
- handle adds a dashed line where the split would land and names the direction in
- the hint bar. The rest of the edge does nothing when clicked.
+ on an edge fills the handle in solid so you can see where to click — a cell
+ wider on the left and right edges, and no taller on the top and bottom ones,
+ since a row is about twice a column and a two-row bar reads as a wall. Resting
+ *on* the handle adds a dashed line where the split would land and names the
+ direction in the hint bar. The rest of the edge does nothing when clicked.
- **The top row is the drag handle, and its handle splits upward.** Clicking a
pane's header to focus it never splits it. On a titled pane the handle sits
wherever the title and the buttons left room, since the title keeps its own
diff --git a/src/app.c b/src/app.c
index ccb86ac..8754b35 100644
--- a/src/app.c
+++ b/src/app.c
@@ -3456,8 +3456,17 @@ 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);
}
- /* The handle, thickened by one cell into the pane, and the hit for that inner
- * strip registered here because *this* is the frame in which it is that size.
+ /* The handle, made heavier, and — where that means claiming a cell the pane
+ * was using — the hit for it registered here, because *this* is the frame in
+ * which it is that size.
+ *
+ * Only the upright sides grow. A cell is about twice as tall as it is wide
+ * (`gap_aspect`, and the split picker leans on the same fact), so one extra
+ * column is a nudge and one extra row is a slab: a two-row bar across a top or
+ * bottom border reads as a wall rather than as a thicker line, and eats a row
+ * of somebody's output to say so. The block glyph is already the heaviest
+ * thing a single cell can draw, and against `━` it is unmistakable, so a
+ * horizontal handle stays one row and says it with ink instead of size.
*
* The rect is read back from the list rather than recomputed: draw_frame
* placed it, and for the top row it places it wherever the title and the
@@ -3475,11 +3484,6 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) {
hit_add(&s->hits, in, h.y, 1, h.h, act);
if (in < h.x) h.x = in;
h.w = 2;
- } else {
- uint16_t in = side == 't' ? (uint16_t)(h.y + 1) : (uint16_t)(h.y - 1);
- hit_add(&s->hits, h.x, in, h.w, 1, act);
- if (in < h.y) h.y = in;
- h.h = 2;
}
for (uint16_t y = h.y; y < h.y + h.h; y++) {
for (uint16_t x = h.x; x < h.x + h.w; x++) {
diff --git a/tests/test_split_ux.py b/tests/test_split_ux.py
index 3bf8613..d568c97 100644
--- a/tests/test_split_ux.py
+++ b/tests/test_split_ux.py
@@ -203,6 +203,52 @@ def test_the_guide_escalates_from_edge_to_handle():
)
+def test_the_handle_thickens_by_a_column_never_a_row():
+ """A cell is about twice as tall as it is wide, so the two axes cannot both
+ grow by one and look the same. An upright handle takes a second column; a
+ horizontal one stays on its own row and gets heavier instead, because two
+ rows of block across a border reads as a wall and costs a row of output."""
+ with Session(SH, cols=80, rows=26, config=FAST) as s:
+ s.settle()
+ p = s.pane()
+
+ def blocks(snap):
+ return [
+ (x, y)
+ for y, row in enumerate(snap.text)
+ for x, ch in enumerate(row)
+ if ch == "█"
+ ]
+
+ for side in ("t", "b"):
+ hx, hy = s.snapshot().handle(p["id"], side)
+ rest(s, hx, hy)
+ cells = blocks(s.snapshot())
+ rows = {y for _, y in cells}
+ check(
+ f"the {side} handle is one row tall",
+ cells and rows == {hy},
+ f"rows {sorted(rows)}, handle row {hy}",
+ )
+
+ for side in ("l", "r"):
+ hx, hy = s.snapshot().handle(p["id"], side)
+ rest(s, hx, hy)
+ cells = blocks(s.snapshot())
+ cols = {x for x, _ in cells}
+ inward = hx + 1 if side == "l" else hx - 1
+ check(
+ f"the {side} handle is two columns wide",
+ cells and cols == {hx, inward},
+ f"columns {sorted(cols)}, wanted {sorted((hx, inward))}",
+ )
+ check(
+ f"and the {side} handle's inner column is clickable",
+ s.snapshot().hit_at(inward, hy) == f"border:{p['id']}:{side}",
+ str(s.snapshot().hit_at(inward, hy)),
+ )
+
+
def test_guide_follows_the_new_layout():
"""Regression: the guide described the layout from *before* the split.
@@ -729,6 +775,7 @@ if __name__ == "__main__":
test_no_more_plus()
test_each_border_splits_toward_itself()
test_the_guide_escalates_from_edge_to_handle()
+ test_the_handle_thickens_by_a_column_never_a_row()
test_guide_follows_the_new_layout()
test_the_guide_waits_for_a_rest()
test_press_skips_the_wait()