aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-14 14:25:13 +0200
committeruser <user@clank>2026-08-14 14:25:13 +0200
commit858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7 (patch)
tree951d60c94949dc43c468252142a93006663b9f76 /src
parentsl0ppty: positional shaders, and a config block to ask for them (diff)
downloadslosh-858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7.tar.gz
slosh-858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7.tar.xz
slosh-858234c05031a7bffc3ffe2a9b2aa4530d3ff0d7.zip
sl0ppty: pane states are a table, not three hardcoded knobs
dim_unfocused, drag_grayscale and drag_dim were three knobs describing two situations, and there was no way to say anything about the third, fourth or fifth. They are replaced by a table: a state names a chain, and the chain is exactly as expressive as the global `shaders` block because it is the same parser. states { dragging { } drop_target { grayscale amount=200; dim amount=140 } suspended { grayscale amount=180; dim amount=90 } scrolled { tint amount=30 color="#ffcc88" } unfocused { dim amount=90 } } A pane is usually in several states at once, so exactly one wins: the first to match, in a fixed order running from the most deliberate to the most ambient. Fixed rather than read from the block, because it is a ranking by urgency rather than a preference — a pane you are holding should not be recoloured by anything, and a mode the whole screen is in outranks an ambient hint about one pane. They do not stack, which is the same call the drag policy already had to make by hand: two reasons to be grey compound into a muddy grey that reads as neither. Defaults are the old drag behaviour and nothing else, so out of the box this changes nothing. drop_hover is now separable from drop_target. The pane under the pointer was already distinguished by its border; it can now differ in colour too, and defaults to the same chain so it does not have to. Two states asked for did not survive contact with the code, and it is worth writing down why: `dead` cannot exist. app_reap() runs every iteration of the server loop, before the paint, and closing the last pane closes the tab — so a pane whose program exits is gone before a frame containing it is ever composed. A `dead` shader would name something that could never draw. Making it real means keeping dead panes until they are dismissed, which is a feature about lifetimes and not about colour. `suspended` could not be seen either, for a much smaller reason: draw_cb drew its "press a key to run" label and returned before reaching the shader pass. It falls through now. A pane that has not started is a state worth colouring, and its label is the only contents it has.
Diffstat (limited to 'src')
-rw-r--r--src/app.c55
-rw-r--r--src/config.c119
-rw-r--r--src/config.h35
3 files changed, 141 insertions, 68 deletions
diff --git a/src/app.c b/src/app.c
index 4a3c348..9996f0e 100644
--- a/src/app.c
+++ b/src/app.c
@@ -1468,31 +1468,34 @@ size_t app_shade_count(app_t *a, uint32_t pane_id) {
* that forgot to clear it is a bug that only shows up in front of someone.
* The rect and the collapsed flag are recomputed every pass for exactly this
* reason; this is the same rule applied to colour. */
-static size_t policy_shaders(app_t *a, node_t *n, shader_t *out, size_t cap) {
- size_t k = 0;
+/* Which state this pane is in, or PSTATE_COUNT for none.
+ *
+ * A pane is usually in several at once, so the order here is the answer: the
+ * first that matches wins and the rest are not asked. It runs from the most
+ * transient and deliberate to the most ambient, because a pane you are holding
+ * should not be recoloured by anything, a mode the whole screen is in outranks
+ * a hint about one pane, and "not focused" is the weakest thing that can be
+ * true of a pane. */
+static pane_state_t pane_state(app_t *a, node_t *n) {
/* Only once the pointer has actually moved: a press that turns out to be a
- * click would otherwise flash the whole session grey on its way to nothing. */
+ * click would otherwise flash the whole session on its way to nothing. */
if (a->drag.kind == DRAG_TITLE && a->drag.moved) {
- /* The pane being dragged is left alone and everything else is pushed back
- * — desaturated *and* darkened, because grey alone still competes for
- * attention at the same brightness. Two passes rather than one shader
- * that does both: they are independently tunable, and "colourless" and
- * "recessed" are different things to want.
- *
- * This replaces the focus dimming rather than stacking with it: two
- * reasons to be grey compound into a muddy grey that reads as neither. */
- if (n->id == a->drag.src) return 0;
- if (CFG.drag_grayscale && k < cap &&
- shader_make(&out[k], "grayscale", (color_t){0}, CFG.drag_grayscale))
- k++;
- if (CFG.drag_dim && k < cap &&
- shader_make(&out[k], "dim", (color_t){0}, CFG.drag_dim))
- k++;
- return k;
+ if (n->id == a->drag.src) return PSTATE_DRAGGING;
+ if (n->id == a->drag.target) return PSTATE_DROP_HOVER;
+ return PSTATE_DROP_TARGET;
}
- if (CFG.dim_unfocused && n != cur(a)->focus && k < cap &&
- shader_make(&out[k], "dim", (color_t){0}, CFG.dim_unfocused))
- k++;
+ if (pane_suspended(n->pane)) return PSTATE_SUSPENDED;
+ if (pane_scrolled(n->pane)) return PSTATE_SCROLLED;
+ if (n != cur(a)->focus) return PSTATE_UNFOCUSED;
+ return PSTATE_COUNT;
+}
+
+static size_t policy_shaders(app_t *a, node_t *n, shader_t *out, size_t cap) {
+ pane_state_t st = pane_state(a, n);
+ if (st >= PSTATE_COUNT) return 0;
+ size_t k = CFG.state_n[st];
+ if (k > cap) k = cap;
+ for (size_t i = 0; i < k; i++) out[i] = CFG.state_shaders[st][i];
return k;
}
@@ -1550,6 +1553,9 @@ static void draw_cb(node_t *n, void *ud) {
draw_frame(d->a, d->s, n);
if (pane_suspended(n->pane)) {
+ /* Falls through to the shader pass rather than returning: a pane that has
+ * not started is a state you can want to colour, and its label is the only
+ * content it has. */
/* The pane exists, is laid out, and has run nothing. Say what it would. */
const char *label = pane_label(n->pane);
char line[256];
@@ -1572,9 +1578,10 @@ static void draw_cb(node_t *n, void *ud) {
screen_text(d->s, (uint16_t)(n->content.x + (n->content.w - w) / 2),
(uint16_t)(n->content.y + n->content.h / 2), line, FRAME_IDLE,
NO_COLOR, 0);
- return;
+ } else {
+ pane_compose(n->pane, d->s, n->content.x, n->content.y,
+ n == cur(d->a)->focus);
}
- pane_compose(n->pane, d->s, n->content.x, n->content.y, n == cur(d->a)->focus);
/* Between the contents and the chrome that goes over them: the frame was
* painted before this and lies outside the content rect, and the split guide
* is painted after, so it stays legible on top of a shaded pane. */
diff --git a/src/config.c b/src/config.c
index 92ee560..fee7d54 100644
--- a/src/config.c
+++ b/src/config.c
@@ -106,6 +106,51 @@ static bool parse_color(const char *text, color_t *out) {
return true;
}
+static const char *const PSTATE_NAMES[PSTATE_COUNT] = {
+ "dragging", "drop_hover", "drop_target", "suspended", "scrolled",
+ "unfocused",
+};
+
+const char *pane_state_name(pane_state_t s) {
+ return s < PSTATE_COUNT ? PSTATE_NAMES[s] : "";
+}
+
+/* Reads the children of `node` as a shader chain into `out` (SHADE_MAX slots),
+ * returning how many were understood. */
+static size_t parse_shader_list(config_t *c, const kdl_node_t *node,
+ shader_t *out, char *err, size_t errcap) {
+ size_t n = 0;
+ for (size_t i = 0; i < node->nkids && n < SHADE_MAX; i++) {
+ const kdl_node_t *k = node->kids[i];
+ if (!k || !k->name) continue;
+
+ long amount = kdl_prop_int(k, "amount", 128);
+ if (amount < 0) amount = 0;
+ if (amount > 255) amount = 255;
+ /* Every shader that takes a number calls it something different, so accept
+ * each name rather than making you remember which belongs to which. */
+ long param = kdl_prop_int(k, "at", -1);
+ if (param < 0) param = kdl_prop_int(k, "radius", -1);
+ if (param < 0) param = kdl_prop_int(k, "band", -1);
+ if (param < 0) param = kdl_prop_int(k, "direction", -1);
+ if (param < 0) param = 0;
+ if (param > 65535) param = 65535;
+
+ color_t col = c->frame_focus; /* a sensible default for `ruler` */
+ const char *cs = kdl_prop(k, "color", NULL);
+ if (cs && !parse_color(cs, &col) && err && !err[0])
+ snprintf(err, errcap, "bad colour for shader %s: %s", k->name, cs);
+
+ if (!shader_make_p(&out[n], k->name, col, (uint8_t)amount,
+ (uint16_t)param)) {
+ if (err && !err[0]) snprintf(err, errcap, "unknown shader: %s", k->name);
+ continue;
+ }
+ n++;
+ }
+ return n;
+}
+
static void bind_add(config_t *c, int key, uint16_t mods, action_t action) {
for (size_t i = 0; i < c->nbinds; i++)
if (c->binds[i].key == key && c->binds[i].mods == mods) {
@@ -129,15 +174,26 @@ void config_defaults(config_t *c) {
c->toast_ms = 2500;
c->hover_delay_ms = 250;
c->double_click_ms = 400;
- c->dim_unfocused = 0;
- c->drag_grayscale = 200;
- c->drag_dim = 140;
c->status_bar = true;
c->focus_follows_mouse = true;
c->default_fg = rgb(0xff, 0xff, 0xff);
c->default_bg = rgb(0x00, 0x00, 0x00);
+ /* The one state with an opinion out of the box: while a pane is being
+ * dragged, everything it could be dropped onto is pushed back so the pane in
+ * your hand stands out. Everything else is empty until asked for. */
+ shader_make(&c->state_shaders[PSTATE_DROP_TARGET][0], "grayscale",
+ (color_t){0}, 200);
+ shader_make(&c->state_shaders[PSTATE_DROP_TARGET][1], "dim", (color_t){0},
+ 140);
+ c->state_n[PSTATE_DROP_TARGET] = 2;
+ shader_make(&c->state_shaders[PSTATE_DROP_HOVER][0], "grayscale",
+ (color_t){0}, 200);
+ shader_make(&c->state_shaders[PSTATE_DROP_HOVER][1], "dim", (color_t){0},
+ 140);
+ c->state_n[PSTATE_DROP_HOVER] = 2;
+
c->frame_focus = rgb(0xff, 0x5f, 0xd7);
c->frame_idle = rgb(0x45, 0x45, 0x4a);
c->title_focus = rgb(0xff, 0xff, 0xff);
@@ -257,12 +313,7 @@ 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);
- c->dim_unfocused = (uint8_t)kdl_arg_int(kdl_child(root, "dim_unfocused"), 0,
- c->dim_unfocused);
- c->drag_grayscale = (uint8_t)kdl_arg_int(kdl_child(root, "drag_grayscale"), 0,
- c->drag_grayscale);
- c->drag_dim =
- (uint8_t)kdl_arg_int(kdl_child(root, "drag_dim"), 0, c->drag_dim);
+
const char *sh = kdl_arg(kdl_child(root, "shell"), 0, NULL);
if (sh) {
@@ -270,41 +321,29 @@ bool config_load(config_t *c, const char *path, char *err, size_t errcap) {
c->shell = strdup(sh);
}
- /* `shaders { vignette amount=60; ruler amount=70 at=80 }` — one node per
- * pass, and the order they are written in is the order they run, because a
- * chain is a sequence. A block that names nothing known leaves the previous
- * set alone rather than silently clearing it. */
+ /* One node per pass, in the order written, because a chain is a sequence.
+ * Shared by `shaders { }` and by every state, so a state's chain is exactly
+ * as expressive as the global one and there is one parser to be wrong. */
const kdl_node_t *shaders = kdl_child(root, "shaders");
- if (shaders) {
- c->nshaders = 0;
- for (size_t i = 0; i < shaders->nkids && c->nshaders < SHADE_MAX; i++) {
- const kdl_node_t *k = shaders->kids[i];
- if (!k || !k->name) continue;
+ if (shaders) c->nshaders = parse_shader_list(c, shaders, c->shaders, err, errcap);
- long amount = kdl_prop_int(k, "amount", 128);
- if (amount < 0) amount = 0;
- if (amount > 255) amount = 255;
- /* Every shader that takes a number calls it something different, so
- * accept each name rather than making you remember which is which. */
- long param = kdl_prop_int(k, "at", -1);
- if (param < 0) param = kdl_prop_int(k, "radius", -1);
- if (param < 0) param = kdl_prop_int(k, "band", -1);
- if (param < 0) param = kdl_prop_int(k, "direction", -1);
- if (param < 0) param = 0;
- if (param > 65535) param = 65535;
-
- color_t col = c->frame_focus; /* a sensible default for `ruler` */
- const char *cs = kdl_prop(k, "color", NULL);
- if (cs && !parse_color(cs, &col) && err && !err[0])
- snprintf(err, errcap, "bad colour for shader %s: %s", k->name, cs);
-
- if (!shader_make_p(&c->shaders[c->nshaders], k->name, col,
- (uint8_t)amount, (uint16_t)param)) {
- if (err && !err[0])
- snprintf(err, errcap, "unknown shader: %s", k->name);
+ /* `states { drop_target { grayscale amount=200; dim amount=140 } }` — what a
+ * pane looks like while it is in a state. Naming a state at all replaces its
+ * default outright, including with nothing, which is how you turn one off. */
+ const kdl_node_t *states = kdl_child(root, "states");
+ if (states) {
+ for (size_t i = 0; i < states->nkids; i++) {
+ const kdl_node_t *k = states->kids[i];
+ if (!k || !k->name) continue;
+ pane_state_t st = PSTATE_COUNT;
+ for (size_t j = 0; j < PSTATE_COUNT; j++)
+ if (strcmp(pane_state_name((pane_state_t)j), k->name) == 0)
+ st = (pane_state_t)j;
+ if (st == PSTATE_COUNT) {
+ if (err && !err[0]) snprintf(err, errcap, "unknown pane state: %s", k->name);
continue;
}
- c->nshaders++;
+ c->state_n[st] = parse_shader_list(c, k, c->state_shaders[st], err, errcap);
}
}
diff --git a/src/config.h b/src/config.h
index a6bd2f2..098c9bc 100644
--- a/src/config.h
+++ b/src/config.h
@@ -14,6 +14,23 @@
#include "shader.h"
#include "sl0ppty.h"
+/* Pane states a shader can be hung off, in the order they are tested. Only
+ * states a pane can actually be seen in: a pane whose program exits is reaped
+ * before the next paint, so there is deliberately no "dead" here — it would
+ * name a shader that could never draw a frame. */
+typedef enum {
+ PSTATE_DRAGGING, /* the pane you have hold of */
+ PSTATE_DROP_HOVER, /* the one under the pointer, where it would land */
+ PSTATE_DROP_TARGET, /* the others, all of them somewhere it could go */
+ PSTATE_SUSPENDED, /* laid out, never started */
+ PSTATE_SCROLLED, /* looking at scrollback rather than the present */
+ PSTATE_UNFOCUSED,
+ PSTATE_COUNT,
+} pane_state_t;
+
+/* The config name for a state, e.g. "drop_target". */
+const char *pane_state_name(pane_state_t s);
+
typedef enum {
ACT_NONE = 0,
ACT_SPLIT_COLS,
@@ -64,15 +81,25 @@ typedef struct {
uint16_t double_click_ms; /* how close two clicks must be to be a double */
/* Shader strengths, 0..255, 0 being off. Ambient dimming is a taste and is
* off by default; the drag greying is transient and on. */
- uint8_t dim_unfocused;
- uint8_t drag_grayscale;
- uint8_t drag_dim;
-
/* Colour passes every pane gets, in the order they were written. Ordinary
* shaders rather than policy: the session has no opinion about these, you
* asked for them. */
shader_t shaders[SHADE_MAX];
size_t nshaders;
+
+ /* What a pane looks like when it is in a particular state. A pane is
+ * usually in several at once — unfocused *and* scrolled, suspended *and*
+ * something you could drop onto — so exactly one wins, the first that
+ * matches in the order below. That order is fixed rather than taken from
+ * the config, because it is a ranking by urgency and not a preference: a
+ * pane you are holding should not be recoloured by anything, and a mode the
+ * whole screen is in outranks an ambient hint about one pane.
+ *
+ * Stacking them instead would let two reasons to be grey compound into one
+ * muddy grey that reads as neither, which is the mistake the drag policy
+ * already had to avoid by hand. */
+ shader_t state_shaders[PSTATE_COUNT][SHADE_MAX];
+ size_t state_n[PSTATE_COUNT];
bool status_bar;
bool focus_follows_mouse;