aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-15 11:32:47 +0200
committeruser <user@clank>2026-08-15 11:32:47 +0200
commitd006c523de132dafa5748345ceb22f4a9f5d4cd4 (patch)
treeb81b8481a9a48bd770a0f30df66f1da8d2f7a7f7 /tests
parentsl0ppty: C-a ? shows every binding you have (diff)
downloadslosh-d006c523de132dafa5748345ceb22f4a9f5d4cd4.tar.gz
slosh-d006c523de132dafa5748345ceb22f4a9f5d4cd4.tar.xz
slosh-d006c523de132dafa5748345ceb22f4a9f5d4cd4.zip
sl0ppty: modals get their own surface, and dim what is behind them
Two things wrong with the first one, and both are about it being a modal rather than a pane that happens to float. The close button was invisible. It was drawn in the pane button colour on the finder background, and in the default theme those are the same value -- #45454a on #45454a -- so it only appeared when the pointer landed on it and the hover colour took over. That is the hazard of borrowing names from another surface: each one is right, and together they are nothing. A modal is its own surface now, with its own names (modal_bg, modal_fg, modal_border, modal_title, modal_button, modal_button_hover), a dark panel a shade lighter than the dimmed screen, a border in the accent, and a button chosen to be visible *without* the pointer on it. All six themes set them. And nothing behind it moved, so it read as another pane rather than as something in front. There is a scrim now, `modal_scrim` (0..255, 120 by default), and the pleasing part is that it is not a new mechanism: it is the shader pass aimed at the screen instead of at a pane. Same dim, same materialisation of default colours, no special case. It goes down after everything else is painted and before the modal, so the modal is the only thing at full brightness -- which is the whole idea, and is asserted rather than assumed. The shape is now reusable: modal_frame() draws the box, title, close button and hit, and returns the rect inside it; draw_scrim() is one line above it. The next modal is a function call and looks the same as this one. The finder is deliberately left alone. It is a picker rather than a modal -- it navigates, where a modal here dismisses on any key -- so unifying them would be making two things one because they are both rectangles.
Diffstat (limited to 'tests')
-rw-r--r--tests/test_help.py76
1 files changed, 76 insertions, 0 deletions
diff --git a/tests/test_help.py b/tests/test_help.py
index c070ac5..b121284 100644
--- a/tests/test_help.py
+++ b/tests/test_help.py
@@ -48,6 +48,82 @@ def test_it_opens_and_lists_the_bindings():
check("shows arrows as arrows", "←" in screen and "→" in screen, screen)
+def style(snap, x, y):
+ run = snap.style_at(x, y)
+ return (run or {}).get("fg"), (run or {}).get("bg")
+
+
+def modal_top(snap):
+ for y, line in enumerate(snap.text):
+ if "keys" in line and "\u256d" in line:
+ return y, line
+ return None, None
+
+
+def test_the_close_button_can_be_seen_without_hovering_it():
+ """It was drawn in the pane button colour on the finder background, which
+ are the same value: invisible until the pointer happened to land on it."""
+ with Session(SH, cols=92, rows=28) as s:
+ s.settle()
+ snap = open_help(s)
+ y, line = modal_top(snap)
+ check("the modal has a top border", y is not None, snap.screen())
+ x = line.rindex("x")
+ fg, bg = style(snap, x, y)
+ check("the close button is not its own background", fg != bg,
+ f"fg {fg} bg {bg}")
+ check("and it is registered as a target",
+ snap.hit_at(x, y) == "closehelp", str(snap.hit_at(x, y)))
+
+
+def test_the_close_button_closes_it():
+ with Session(SH, cols=92, rows=28) as s:
+ s.settle()
+ snap = open_help(s)
+ y, line = modal_top(snap)
+ s.click(line.rindex("x"), y)
+ s.settle()
+ check("clicking the button puts it away",
+ "split into columns" not in s.snapshot().screen())
+
+
+def test_what_is_behind_is_pushed_back():
+ with Session(SH, cols=100, rows=30) as s:
+ s.settle()
+ before = s.snapshot()
+ pos = before.find("pane 1/1")
+ check("the status line is there", pos is not None, before.screen())
+ x, y = pos[0] + 1, pos[1]
+ lit = style(before, x, y)
+
+ after = open_help(s)
+ dimmed = style(after, x, y)
+ check("the screen behind the modal is dimmed", dimmed != lit,
+ f"{lit} -> {dimmed}")
+
+ # And the modal itself is not: it is drawn after the scrim, which is
+ # the whole point of the ordering. Sampled on the title rather than on
+ # a corner, because the modal's top row can sit on a pane's border row
+ # and then the line has two of those.
+ my, line = modal_top(after)
+ tx = line.index("keys")
+ check("the modal is not dimmed with it",
+ style(after, tx, my)[0] == "#ffffff", str(style(after, tx, my)))
+
+
+def test_the_scrim_can_be_turned_off():
+ conf = cfg("modal_scrim 0\n")
+ with Session(SH, cols=100, rows=30, config=conf) as s:
+ s.settle()
+ before = s.snapshot()
+ x, y = before.find("pane 1/1")[0] + 1, before.find("pane 1/1")[1]
+ lit = style(before, x, y)
+ after = open_help(s)
+ check("nothing behind it changes", style(after, x, y) == lit,
+ f"{lit} -> {style(after, x, y)}")
+ check("but the modal is still up", "split into columns" in after.screen())
+
+
def test_any_key_dismisses_it_and_does_nothing_else():
with Session(SH, cols=92, rows=28) as s:
s.settle()