aboutsummaryrefslogtreecommitdiffstats
path: root/src
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 /src
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 'src')
-rw-r--r--src/app.c175
-rw-r--r--src/config.c23
-rw-r--r--src/config.h14
3 files changed, 152 insertions, 60 deletions
diff --git a/src/app.c b/src/app.c
index ab2e425..213dc53 100644
--- a/src/app.c
+++ b/src/app.c
@@ -99,6 +99,12 @@ bool app_reload_config(char *err, size_t errcap) {
#define MINBAR_HOVER (CFG.minbar_hover)
#define HINT_C (CFG.hint)
#define DEAD_C (CFG.dead)
+#define MODAL_FG (CFG.modal_fg)
+#define MODAL_BG (CFG.modal_bg)
+#define MODAL_BORDER (CFG.modal_border)
+#define MODAL_TITLE (CFG.modal_title)
+#define MODAL_BTN (CFG.modal_button)
+#define MODAL_BTN_HOVER (CFG.modal_button_hover)
static const color_t NO_COLOR = {0};
@@ -3052,6 +3058,90 @@ static size_t help_rows(help_row_t *out, size_t cap) {
return n;
}
+/* ---- modals -------------------------------------------------------------
+ *
+ * A surface that floats over the layout: everything behind it is pushed back,
+ * it is opaque, it wears a pane's frame so it plainly belongs to this program,
+ * and it owns the topmost hits while it is up. The cheatsheet is the first
+ * one; the shape is here rather than inside it so the second one costs a
+ * function call and looks the same.
+ */
+
+/* Push the whole screen back, so what floats reads as being in front.
+ *
+ * This is the shader pass, aimed at the screen instead of a pane: the same
+ * dim, the same materialisation of "terminal default" colours (without which
+ * the chrome would not darken at all, since most of it has no colour of its
+ * own). Nothing about it is special-cased, which is the nice part -- a scrim
+ * is just a colour pass with everything in its rect. */
+static void draw_scrim(app_t *a, screen_t *s) {
+ if (!CFG.modal_scrim) return;
+ shader_t dim;
+ if (!shader_make(&dim, "dim", (color_t){0}, CFG.modal_scrim)) return;
+ shade_ctx_t base = {
+ .now_ms = now_ms_(),
+ .default_fg = CFG.default_fg,
+ .default_bg = CFG.default_bg,
+ };
+ shade_apply(s, &dim, 1, 0, 0, s->cols, s->rows, &base);
+}
+
+/* The frame every modal wears: an opaque box with a pane's corners, a title
+ * in the top rule and a close button where a pane keeps one. Returns the rect
+ * *inside* it, which is all a caller should care about. */
+static rect_t modal_frame(app_t *a, screen_t *s, uint16_t w, uint16_t h,
+ const char *title, const char *close_action) {
+ if (w > s->cols) w = s->cols;
+ if (h > s->rows) h = s->rows;
+ uint16_t x = (uint16_t)((s->cols - w) / 2), y = (uint16_t)((s->rows - h) / 2);
+ uint16_t x1 = (uint16_t)(x + w - 1), y1 = (uint16_t)(y + h - 1);
+
+ /* Opaque, or the panes underneath read through it: a modal you can see a
+ * shell prompt through is a modal nobody trusts. */
+ for (uint16_t yy = y; yy < y + h; yy++)
+ for (uint16_t xx = x; xx < x + w; xx++)
+ screen_text(s, xx, yy, " ", NO_COLOR, MODAL_BG, 0);
+
+ const char *tl = CFG.rounded ? "\u256d" : "\u250c", *tr = CFG.rounded ? "\u256e" : "\u2510";
+ const char *bl = CFG.rounded ? "\u2570" : "\u2514", *br = CFG.rounded ? "\u256f" : "\u2518";
+ screen_text(s, x, y, tl, MODAL_BORDER, MODAL_BG, 0);
+ screen_text(s, x1, y, tr, MODAL_BORDER, MODAL_BG, 0);
+ screen_text(s, x, y1, bl, MODAL_BORDER, MODAL_BG, 0);
+ screen_text(s, x1, y1, br, MODAL_BORDER, MODAL_BG, 0);
+ for (uint16_t xx = (uint16_t)(x + 1); xx < x1; xx++) {
+ screen_text(s, xx, y, "\u2500", MODAL_BORDER, MODAL_BG, 0);
+ screen_text(s, xx, y1, "\u2500", MODAL_BORDER, MODAL_BG, 0);
+ }
+ for (uint16_t yy = (uint16_t)(y + 1); yy < y1; yy++) {
+ screen_text(s, x, yy, "\u2502", MODAL_BORDER, MODAL_BG, 0);
+ screen_text(s, x1, yy, "\u2502", MODAL_BORDER, MODAL_BG, 0);
+ }
+
+ if (title && *title && w > cells(title) + 4) {
+ char buf[80];
+ snprintf(buf, sizeof buf, " %s ", title);
+ screen_text(s, (uint16_t)(x + (w - cells(buf)) / 2), y, buf, MODAL_TITLE,
+ MODAL_BG, ATTR_BOLD);
+ }
+
+ /* The close button a pane has, in the place a pane keeps it. Two cells of
+ * target for a one-cell mark, for the reason the frame's own buttons are
+ * three: a one-cell target is a thing you miss with a mouse. */
+ if (close_action && w > 10) {
+ uint16_t bw = (uint16_t)(cells(CFG.close_mark) + 1);
+ uint16_t bx = (uint16_t)(x1 - bw);
+ bool hot = ptr_on(a, bx, y, bw, 1);
+ char cell[24];
+ snprintf(cell, sizeof cell, "%s ", CFG.close_mark);
+ screen_text(s, bx, y, cell, hot ? MODAL_BTN_HOVER : MODAL_BTN, MODAL_BG,
+ hot ? ATTR_BOLD : 0);
+ hit_add(&s->hits, bx, y, bw, 1, close_action);
+ }
+
+ return (rect_t){(uint16_t)(x + 1), (uint16_t)(y + 1), (uint16_t)(w - 2),
+ (uint16_t)(h - 2)};
+}
+
/* Draw at most `limit` columns of `text`, cut on a character boundary.
*
* The box is clamped to the screen, so on a small terminal the rows are wider
@@ -3104,88 +3194,48 @@ static void draw_help(app_t *a, screen_t *s) {
}
uint16_t body = (uint16_t)(cw[0] + (two ? cw[1] + 4 : 0));
- uint16_t w = (uint16_t)(body + 6);
size_t left_n = two ? split : n, right_n = two ? n - split : 0;
uint16_t lines = (uint16_t)(left_n > right_n ? left_n : right_n);
- uint16_t h = (uint16_t)(lines + 6);
- if (w > s->cols) w = s->cols;
- if (h > s->rows) h = s->rows;
- uint16_t x = (uint16_t)((s->cols - w) / 2), y = (uint16_t)((s->rows - h) / 2);
- /* Opaque, or the panes underneath read through it: a modal that you can see
- * a shell prompt through is a modal nobody trusts. */
- for (uint16_t yy = y; yy < y + h; yy++)
- for (uint16_t xx = x; xx < x + w; xx++)
- screen_text(s, xx, yy, " ", NO_COLOR, FINDER_BG, 0);
-
- /* The same frame a pane wears, for the same reason a dialog wears the
- * window chrome of its platform: it belongs to this program. */
- const char *tl = CFG.rounded ? "\u256d" : "\u250c", *tr = CFG.rounded ? "\u256e" : "\u2510";
- const char *bl = CFG.rounded ? "\u2570" : "\u2514", *br = CFG.rounded ? "\u256f" : "\u2518";
- uint16_t x1 = (uint16_t)(x + w - 1), y1 = (uint16_t)(y + h - 1);
- screen_text(s, x, y, tl, FRAME_FOCUS, FINDER_BG, 0);
- screen_text(s, x1, y, tr, FRAME_FOCUS, FINDER_BG, 0);
- screen_text(s, x, y1, bl, FRAME_FOCUS, FINDER_BG, 0);
- screen_text(s, x1, y1, br, FRAME_FOCUS, FINDER_BG, 0);
- for (uint16_t xx = (uint16_t)(x + 1); xx < x1; xx++) {
- screen_text(s, xx, y, "\u2500", FRAME_FOCUS, FINDER_BG, 0);
- screen_text(s, xx, y1, "\u2500", FRAME_FOCUS, FINDER_BG, 0);
- }
- for (uint16_t yy = (uint16_t)(y + 1); yy < y1; yy++) {
- screen_text(s, x, yy, "\u2502", FRAME_FOCUS, FINDER_BG, 0);
- screen_text(s, x1, yy, "\u2502", FRAME_FOCUS, FINDER_BG, 0);
- }
-
- const char *title = " keys ";
- uint16_t tx = (uint16_t)(x + (w - cells(title)) / 2);
- screen_text(s, tx, y, title, TITLE_FOCUS, FINDER_BG, ATTR_BOLD);
-
- /* The close button a pane has, in the place a pane has it. */
- if (w > 10) {
- uint16_t bx = (uint16_t)(x1 - 2);
- bool hot = ptr_on(a, bx, y, 2, 1);
- screen_text(s, bx, y, CFG.close_mark, hot ? PANE_BTN_HOVER : PANE_BTN,
- FINDER_BG, hot ? ATTR_BOLD : 0);
- hit_add(&s->hits, bx, y, 2, 1, "closehelp");
- }
+ rect_t in = modal_frame(a, s, (uint16_t)(body + 6), (uint16_t)(lines + 6),
+ "keys", "closehelp");
+ uint16_t x1 = (uint16_t)(in.x + in.w); /* the right border */
/* Everything here is prefix-then-key, and saying so once beats repeating
* the prefix on thirty rows. */
- char lead[80];
- char pfx[24];
+ char lead[80], pfx[24];
config_chord_name(CFG.prefix_key, CFG.prefix_mods, pfx, sizeof pfx);
snprintf(lead, sizeof lead, "%s then:", pfx);
- help_text(s, (uint16_t)(x + 3), (uint16_t)(y + 2), lead,
- (uint16_t)(w > 6 ? w - 6 : 0), HINT_C, FINDER_BG, 0);
+ help_text(s, (uint16_t)(in.x + 2), (uint16_t)(in.y + 1), lead, in.w, HINT_C,
+ MODAL_BG, 0);
for (size_t i = 0; i < n; i++) {
bool right = two && i >= split;
size_t row = right ? i - split : i;
- if ((uint16_t)(row + 4) >= h - 1) continue;
- uint16_t rx = (uint16_t)(x + 3 + (right ? cw[0] + 4 : 0));
- uint16_t ry = (uint16_t)(y + 4 + row);
+ uint16_t ry = (uint16_t)(in.y + 3 + row);
+ if (ry >= in.y + in.h) continue;
+ uint16_t rx = (uint16_t)(in.x + 2 + (right ? cw[0] + 4 : 0));
/* Everything is cut at the frame, and the left column additionally at the
* right one, so a clamped box loses words rather than its border. */
- uint16_t room = (uint16_t)(x1 > rx + 1 ? x1 - rx - 1 : 0);
- if (two && !right && cw[0] + 4 < room) room = (uint16_t)(cw[0] + 2);
+ uint16_t room = (uint16_t)(x1 > rx ? x1 - rx : 0);
+ if (two && !right && cw[0] + 2 < room) room = (uint16_t)(cw[0] + 2);
if (!rows[i].group) {
- help_text(s, rx, ry, rows[i].label, room, TITLE_FOCUS, FINDER_BG,
+ help_text(s, rx, ry, rows[i].label, room, MODAL_TITLE, MODAL_BG,
ATTR_BOLD);
continue;
}
- help_text(s, rx, ry, rows[i].chord, room, TITLE_FOCUS, FINDER_BG, 0);
+ help_text(s, rx, ry, rows[i].chord, room, MODAL_TITLE, MODAL_BG, 0);
uint16_t lx = (uint16_t)(rx + kw[right ? 1 : 0] + 2);
- help_text(s, lx, ry, rows[i].label,
- (uint16_t)(x1 > lx + 1 ? x1 - lx - 1 : 0), FINDER_FG, FINDER_BG,
- 0);
+ help_text(s, lx, ry, rows[i].label, (uint16_t)(x1 > lx ? x1 - lx : 0),
+ MODAL_FG, MODAL_BG, 0);
}
const char *foot = " any key closes this ";
- if (w > cells(foot) + 4)
- screen_text(s, (uint16_t)(x + (w - cells(foot)) / 2), y1, foot, HINT_C,
- FINDER_BG, 0);
+ if (in.w > cells(foot))
+ screen_text(s, (uint16_t)(in.x + (in.w - cells(foot)) / 2),
+ (uint16_t)(in.y + in.h), foot, HINT_C, MODAL_BG, 0);
}
static void draw_finder(app_t *a, screen_t *s) {
@@ -3384,8 +3434,13 @@ void app_compose(app_t *a, screen_t *s) {
draw_status_line(a, s);
if (a->finder) draw_finder(a, s); /* painted last, so its hits win */
/* And the modal on top of even that: it is the only thing that can be
- * interacted with while it is up, so it owns the topmost hits. */
- if (a->help) draw_help(a, s);
+ * interacted with while it is up, so it owns the topmost hits. The scrim
+ * goes down first, over everything already painted — including the finder,
+ * if that happened to be open. */
+ if (a->help) {
+ draw_scrim(a, s);
+ draw_help(a, s);
+ }
draw_toasts(a, s); /* and above even that: it is transient */
}
diff --git a/src/config.c b/src/config.c
index 4406443..bdff0a6 100644
--- a/src/config.c
+++ b/src/config.c
@@ -222,6 +222,7 @@ void config_defaults(config_t *c) {
c->toast_ms = 2500;
c->hover_delay_ms = 250;
c->double_click_ms = 400;
+ c->modal_scrim = 120;
c->status_bar = true;
c->status_line = true;
/* Deliberately wider than the panes' own margin (gap * gap_aspect = 2), so
@@ -327,6 +328,18 @@ void config_defaults(config_t *c) {
c->bell = accent;
c->dead = rgb(0xff, 0x87, 0x5f);
+
+ /* A surface, not a hole: lighter than the dimmed screen behind it, with a
+ * border in the accent so the edge is never in doubt. The button is a grey
+ * that is *visible* on that surface — the first version used the pane
+ * button colour, which is the same value as the old background it was drawn
+ * on, so it only appeared when you hovered it. */
+ c->modal_bg = rgb(0x1c, 0x1c, 0x22);
+ c->modal_fg = rgb(0xe8, 0xe8, 0xea);
+ c->modal_border = accent;
+ c->modal_title = bright;
+ c->modal_button = rgb(0x8a, 0x8a, 0x95);
+ c->modal_button_hover = accent;
c->hint = bright;
c->minbar = dim;
c->minbar_hover = accent;
@@ -618,6 +631,10 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) {
c->hover_delay_ms);
c->double_click_ms = (uint16_t)kdl_arg_int(kdl_child(root, "double_click_ms"),
0, c->double_click_ms);
+ {
+ long v = kdl_arg_int(kdl_child(root, "modal_scrim"), 0, c->modal_scrim);
+ c->modal_scrim = (uint8_t)(v < 0 ? 0 : v > 255 ? 255 : v);
+ }
const char *sh = kdl_arg(kdl_child(root, "shell"), 0, NULL);
@@ -719,6 +736,12 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) {
{"finder_sel_fg", &c->finder_sel_fg},
{"finder_sel_bg", &c->finder_sel_bg},
{"bell", &c->bell},
+ {"modal_fg", &c->modal_fg},
+ {"modal_bg", &c->modal_bg},
+ {"modal_border", &c->modal_border},
+ {"modal_title", &c->modal_title},
+ {"modal_button", &c->modal_button},
+ {"modal_button_hover", &c->modal_button_hover},
{"dead", &c->dead},
{"hint", &c->hint},
{"minbar", &c->minbar},
diff --git a/src/config.h b/src/config.h
index 6dd470d..889c16b 100644
--- a/src/config.h
+++ b/src/config.h
@@ -126,6 +126,10 @@ typedef struct {
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 */
uint16_t double_click_ms; /* how close two clicks must be to be a double */
+ /* How far everything behind a modal is pushed back, 0..255, 0 being off.
+ * A modal that floats over a screen as bright as it is reads as another
+ * pane rather than as something in front. */
+ uint8_t modal_scrim;
/* Shader strengths, 0..255, 0 being off. Ambient dimming is a taste and is
* off by default; the drag greying is transient and on. */
/* Colour passes every pane gets, in the order they were written. Ordinary
@@ -222,6 +226,16 @@ typedef struct {
/* the hint in the middle of the status line */
color_t hint;
+ /* Modals: a surface that floats over the layout, with its own frame. Named
+ * separately from the pane colours it resembles because it is a different
+ * *surface* — it sits on top of everything, it is opaque, and it has to
+ * stay legible against a background that has been dimmed underneath it.
+ * Borrowing the pane button colour here is what made the close button
+ * invisible: dim on dim. */
+ color_t modal_fg, modal_bg;
+ color_t modal_border, modal_title;
+ color_t modal_button, modal_button_hover;
+
/* announcements */
color_t toast_fg, toast_bg;