diff options
| author | user <user@clank> | 2026-08-18 16:10:10 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-18 16:10:10 +0200 |
| commit | 14dad660036e34a899a406f09195f790bb60709c (patch) | |
| tree | aa2f12c2d51011d2a2cd818c9a7c31345d55bc56 | |
| parent | sl0ppty: a horizontal handle gets heavier, not taller (diff) | |
| download | slosh-14dad660036e34a899a406f09195f790bb60709c.tar.gz slosh-14dad660036e34a899a406f09195f790bb60709c.tar.xz slosh-14dad660036e34a899a406f09195f790bb60709c.zip | |
sl0ppty: the split arrow belongs on the button
The arrow rode the middle of the dashed boundary line, which put the two halves
of one message in two places: the handle said *press here* and a glyph out in the
middle of the pane said *this is what happens*. It also meant the direction was
unreadable until you were already on the target, since the dashed line is stage
two of the hover.
So the arrow moves into the handle and the boundary line goes back to being a
plain boundary -- which is the honest shape for it anyway: it is the *edge* of the
pane you are about to make, and an edge has no middle worth marking. The handle
is now one object that explains itself, and because the handle is drawn from the
first stage, brushing any part of an edge tells you where its button is and which
way that button splits, without previewing a boundary nobody asked about.
Knocked out with ATTR_INVERSE rather than drawn in a second colour: the bar is
solid `guide`, so a `guide` glyph on top of it would have been invisible, and
inverting gets contrast that follows whatever colour the theme sets for no new
knob. Along the handle the arrow sits in the middle, where the pointer is aiming.
Across it -- a distinction only the upright pair has, being the two that grow a
second cell -- it sits on the edge's own cell rather than the one over the
content, because it marks a boundary and the boundary lands on the frame.
Verified: `make retest`, **1873 checks measured** (`test_split_ux.py` 100 of
them), all green. The test that pinned the arrow to the dashed line is now the
test that pins it to the handle, on all four sides, plus one that it is legible
from the first stage with no boundary drawn.
Checked live on a 116x36 pane: `◄█` at column 2 and `█►` at column 117 on the
middle row of their bars, `━━━███▼███━━━` on the bottom border's single row, and
-- the case that shows why this is worth it -- hovering the *far left* of the top
row draws `███▲███` at column 85, where the handle actually is, having dodged a
centred title.
A note on the two commits before this one: their messages quote 1801 and 1807
checks, which were carried over from earlier in the session rather than measured.
The figure was 1865 at that point. The code and the tests were right; the numbers
in those two messages were not.
| -rw-r--r-- | DESIGN.md | 23 | ||||
| -rw-r--r-- | config/config.kdl | 2 | ||||
| -rw-r--r-- | docs/panes.md | 10 | ||||
| -rw-r--r-- | src/app.c | 53 | ||||
| -rw-r--r-- | tests/test_split_ux.py | 43 |
5 files changed, 96 insertions, 35 deletions
@@ -298,12 +298,23 @@ 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 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. +solid **with the direction arrow knocked out of it**, which says *where the +button is and what it does*. **On the handle**: the dashed line where the new +boundary would land, which says *exactly where the result goes*. 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 button carries its own verb.** The arrow used to ride the middle of the +dashed line, which put the two halves of one message in two places — the handle +saying *press here* and a glyph out in the pane saying *this is what happens* — +and made the direction unreadable until you were already on the target. On the +handle it is one object that explains itself, the boundary line goes back to +being a plain boundary (an edge has no middle worth marking), and brushing an +edge now tells you which way that button splits. It is knocked out with +`ATTR_INVERSE` rather than drawn in a second colour, because the bar is solid +`guide` and a `guide` glyph on top of it would be invisible: inverting gets +contrast that follows whatever colour you set, for no new theme knob. **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 diff --git a/config/config.kdl b/config/config.kdl index c5df59f..878d97c 100644 --- a/config/config.kdl +++ b/config/config.kdl @@ -689,7 +689,7 @@ theme { button_bg_idle "#55555c" // buttons on a pane that is not focused // ── things that appear under the pointer ──────────────────────────────── - guide "#ff5fd7" // split guide: armed edge, dashed boundary, arrow + guide "#ff5fd7" // split guide: armed edge, handle, its arrow, boundary resize "#ff5fd7" // the handle in the gap between two panes drop_target "#ff5fd7" // frame of a pane a drag could land on diff --git a/docs/panes.md b/docs/panes.md index fc27020..b341821 100644 --- a/docs/panes.md +++ b/docs/panes.md @@ -39,11 +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 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. + on an edge fills the handle in solid, with an arrow in it for the way it will + split — 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 new boundary will + land. 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 @@ -3485,38 +3485,61 @@ static void draw_split_guide(app_t *a, screen_t *s, node_t *leaf) { if (in < h.x) h.x = in; h.w = 2; } + /* The button carries its own verb. The arrow used to ride the dashed line, + * which put the two halves of the message in two places: the handle said + * *press here* and something over in the middle of the pane said *this is + * what happens*. On the handle it is one object that explains itself, and it + * is legible from the first stage -- brushing an edge now tells you which way + * that button splits without drawing a boundary you did not ask about. + * + * Knocked out of the bar with ATTR_INVERSE rather than drawn in a second + * colour: the block is solid `guide`, so a `guide` glyph on top of it would + * be invisible, and inverting spends no new theme knob to get contrast that + * follows whatever colour you set. + * + * 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. */ + const char *arrow = side == 'l' ? "\u25c4" + : side == 'r' ? "\u25ba" + : side == 't' ? "\u25b2" + : "\u25bc"; + /* Along the handle it goes in the middle, which is where the pointer is + * aiming. Across it, on the edge's own cell rather than the one over the + * content: the arrow marks a boundary, and the boundary lands on the frame. + * That distinction only exists on the upright pair, since those are the two + * that grew a second cell. */ + bool vert = side == 'l' || side == 'r'; + uint16_t ax = vert ? (side == 'l' ? h.x : (uint16_t)(h.x + h.w - 1)) + : (uint16_t)(h.x + h.w / 2); + uint16_t ay = vert ? (uint16_t)(h.y + h.h / 2) : h.y; for (uint16_t y = h.y; y < h.y + h.h; y++) { for (uint16_t x = h.x; x < h.x + h.w; x++) { const char *own = hit_test(&s->hits, x, y); - if (own && strcmp(own, act) == 0) - screen_text(s, x, y, "\u2588", hi, NO_COLOR, ATTR_BOLD); + if (!own || strcmp(own, act) != 0) continue; + bool tip = x == ax && y == ay; + screen_text(s, x, y, tip ? arrow : "\u2588", hi, NO_COLOR, + tip ? (uint16_t)(ATTR_BOLD | ATTR_INVERSE) : ATTR_BOLD); } } } if (!on_handle) return; - /* Stage two: 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. */ + /* Stage two: where the new boundary lands, and nothing about direction -- the + * handle is holding that end of the message now. A plain dashed line is also + * the honest shape for it: it is the *edge* of the new pane, and an edge has + * no middle worth marking. */ if (side == 'l' || side == 'r') { uint16_t mid = (uint16_t)(r.x + r.w / 2); for (uint16_t y = (uint16_t)(r.y + 1); y < y1; y++) 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 mid = (uint16_t)(r.y + r.h / 2); for (uint16_t x = (uint16_t)(r.x + 1); x < x1; x++) 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); } } diff --git a/tests/test_split_ux.py b/tests/test_split_ux.py index d568c97..333ce07 100644 --- a/tests/test_split_ux.py +++ b/tests/test_split_ux.py @@ -585,18 +585,45 @@ def test_the_guide_says_which_way_it_goes(): ) -def test_the_arrow_sits_on_the_new_boundary(): +def test_the_arrow_rides_the_handle(): + """The arrow used to sit halfway along the dashed line, which split the + message in two: the handle said *press here* and something out in the middle + of the pane said *this is what happens*. It belongs on the button.""" with Session(SH, cols=120, rows=26, config=FAST) as s: s.settle() p = s.pane() - x, y = edge_of(s, p, "r") - rest(s, x, y) + for side in ("l", "r", "t", "b"): + hx, hy = edge_of(s, p, side) + rest(s, hx, hy) + snap = s.snapshot() + pos = snap.find(ARROWS[side]) + # Inside the handle: same row for an upright edge, same column for a + # horizontal one, and never out on the boundary line. + near = pos is not None and ( + (side in "lr" and pos[1] == hy and abs(pos[0] - hx) <= 1) + or (side in "tb" and pos[1] == hy and abs(pos[0] - hx) <= 3) + ) + check( + f"the {side} arrow is on the handle", + near, + f"{pos} vs handle ({hx},{hy})", + ) + check( + f"and the {side} boundary line carries none", + pos != (p["x"] + p["w"] // 2, p["y"] + p["h"] // 2), + str(pos), + ) + + # The arrow is part of the button, so it is legible from the first stage: + # brushing an edge says which way that handle splits, without previewing + # a boundary. + rx, ry = s.snapshot().rim(p["id"], "l") + rest(s, rx, ry) snap = s.snapshot() - pos = snap.find(ARROWS["r"]) check( - "the arrow is on the dashed line, not the border", - pos is not None and pos[0] == p["x"] + p["w"] // 2, - str(pos), + "and it shows on rim hover too, with no boundary", + ARROWS["l"] in snap.screen() and "╎" not in snap.screen(), + repr([l for l in snap.text if ARROWS["l"] in l][:1]), ) @@ -787,7 +814,7 @@ if __name__ == "__main__": test_the_floor_is_configurable_in_both_axes() test_the_keyboard_obeys_the_same_floor() test_the_guide_says_which_way_it_goes() - test_the_arrow_sits_on_the_new_boundary() + test_the_arrow_rides_the_handle() test_it_splits_across_the_longer_side() test_the_aspect_is_the_configured_one() test_it_falls_back_to_the_axis_that_fits() |
