shithub: puzzles

Download patch

ref: a3310ab857f088489b35ebf10733ba284a24d27f
parent: 1d91522babe41fcf7cbfb06633ae6bc5606db367
author: Ben Harris <bjh21@bjh21.me.uk>
date: Sun Dec 4 20:13:26 EST 2022

New backend function: current_key_label()

This provides a way for the front end to ask how a particular key should
be labelled right now (specifically, for a given game_state and
game_ui).  This is useful on feature phones where it's conventional to
put a small caption above each soft key indicating what it currently
does.

The function currently provides labels only for CURSOR_SELECT and
CURSOR_SELECT2.  This is because these are the only keys that need
labelling on KaiOS.

The concept of labelling keys also turns up in the request_keys() call,
but there are quite a few differences.  The labels returned by
current_key_label() are dynamic and likely to vary with each move, while
the labels provided by request_keys() are constant for a given
game_params.  Also, the keys returned by request_keys() don't generally
include CURSOR_SELECT and CURSOR_SELECT2, because those aren't necessary
on platforms with pointing devices.  It might be possible to provide a
unified API covering both of this, but I think it would be quite
difficult to work with.

Where a key is to be unlabelled, current_key_label() is expected to
return an empty string.  This leaves open the possibility of NULL
indicating a fallback to button2label or the label specified by
request_keys() in the future.

It's tempting to try to implement current_key_label() by calling
interpret_move() and parsing its output.  This doesn't work for two
reasons.  One is that interpret_move() is entitled to modify the
game_ui, and there isn't really a practical way to back those changes
out.  The other is that the information returned by interpret_move()
isn't sufficient to generate a label.  For instance, in many puzzles it
generates moves that toggle the state of a square, but we want the label
to reflect which state the square will be toggled to.  The result is
that I've generally ended up pulling bits of code from interpret_move()
and execute_move() together to implement current_key_label().

Alongside the back-end function, there's a midend_current_key_label()
that's a thin wrapper around the back-end function.  It just adds an
assertion about which key's being requested and a default null
implementation so that back-ends can avoid defining the function if it
will do nothing useful.

--- a/blackbox.c
+++ b/blackbox.c
@@ -532,6 +532,41 @@
     ui->newmove = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cur_visible && !state->reveal) {
+        int gx = ui->cur_x, gy = ui->cur_y, rangeno = -1;
+        if (gx == 0 && gy == 0 && button == CURSOR_SELECT) return "Check";
+        if (gx >= 1 && gx <= state->w && gy >= 1 && gy <= state->h) {
+            /* Cursor somewhere in the arena. */
+            if (button == CURSOR_SELECT && !(GRID(state, gx,gy) & BALL_LOCK))
+                return (GRID(state, gx, gy) & BALL_GUESS) ? "Clear" : "Ball";
+            if (button == CURSOR_SELECT2)
+                return (GRID(state, gx, gy) & BALL_LOCK) ? "Unlock" : "Lock";
+        }
+        if (grid2range(state, gx, gy, &rangeno)) {
+            if (button == CURSOR_SELECT &&
+                state->exits[rangeno] == LASER_EMPTY)
+                return "Fire";
+            if (button == CURSOR_SELECT2) {
+                int n = 0;
+                /* Row or column lock or unlock. */
+                if (gy == 0 || gy > state->h) { /* Column lock */
+                    for (gy = 1; gy <= state->h; gy++)
+                        n += !!(GRID(state, gx, gy) & BALL_LOCK);
+                    return n > state->h/2 ? "Unlock" : "Lock";
+                } else { /* Row lock */
+                    for (gx = 1; gx <= state->w; gx++)
+                        n += !!(GRID(state, gx, gy) & BALL_LOCK);
+                    return n > state->w/2 ? "Unlock" : "Lock";
+                }
+            }
+        }
+    }
+    return "";
+}
+
 #define OFFSET(gx,gy,o) do {                                    \
     int off = (4 + (o) % 4) % 4;                                \
     (gx) += offsets[off].x;                                     \
@@ -1543,6 +1578,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/bridges.c
+++ b/bridges.c
@@ -2138,6 +2138,20 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button)) {
+        if (!ui->cur_visible)
+            return ""; /* Actually shows cursor. */
+        if (ui->dragging || button == CURSOR_SELECT2)
+            return "Finished";
+        if (GRID(state, ui->cur_x, ui->cur_y) & G_ISLAND)
+            return "Select";
+    }
+    return "";
+}
+
 struct game_drawstate {
     int tilesize;
     int w, h;
@@ -3269,6 +3283,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/cube.c
+++ b/cube.c
@@ -1774,6 +1774,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     PREFERRED_GRID_SCALE, game_compute_size, game_set_size,
--- a/devel.but
+++ b/devel.but
@@ -1692,6 +1692,43 @@
 frontends should use \cw{midend_request_keys()}
 (\k{midend-request-keys}).
 
+\S{backend-current-key-label} \cw{current_key_label()}
+
+\c const char *(*current_key_label)(const game_ui *ui,
+\c                                  const game_state *state, int button);
+
+This function is called to ask the back-end how certain keys should be
+labelled on platforms (such a feature phones) where this is
+conventional.
+These labels are expected to reflect what the keys will do right now,
+so they can change depending on the game and UI state.
+
+The \c{ui} and \c{state} arguments describe the state of the game for
+which key labels are required.
+The \c{button} argument is the same as the one passed to
+\cw{interpret_move()}.
+At present, the only values of \c{button} that can be passed to
+\cw{current_key_label()} are \cw{CURSOR_SELECT} and \cw{CURSOR_SELECT2}.
+The return value is a short string describing what the requested key
+will do if pressed.
+Usually the string should be a static string constant.
+If it's really necessary to use a dynamically-allocated string, it
+should remain valid until the next call to \cw{current_key_label()} or
+\cw{free_ui()} with the same \cw{game_ui} (so it can be referenced from
+the \cw{game_ui} and freed at the next one of those calls).
+
+There's no fixed upper limit on the length of string that this
+function can return, but more than about 12 characters is likely to
+cause problems for front-ends.  If two buttons have the same effect,
+their labels should be identical so that the front end can detect
+this.  Similarly, keys that do different things should have different
+labels.  The label should be an empty string (\cw{""}) if the key does
+nothing.
+
+Like \cw{request_keys()}, the \cw{current_key_label} pointer in the
+\c{game} structure is allowed to be \cw{NULL}, in which case the
+mid-end will treat it as though it always returned \cw{""}.
+
 \S{backend-flags} \c{flags}
 
 \c int flags;
@@ -3231,6 +3268,20 @@
 labels (i.e. the \cw{key_label} items that have their \cw{label}
 fields set to \cw{NULL}) by using \cw{button2label()}
 (\k{utils-button2label}).
+
+\H{midend-current-key-label} \cw{midend_current_key_label()}
+
+\c const char *midend_current_key_label(midend *me, int button);
+
+This is a thin wrapper around the backend's \cw{current_key_label()}
+function (\k{backend-current-key-label}).  Front ends that need to
+label \cw{CURSOR_SELECT} or \cw{CURSOR_SELECT2} should call this
+function after each move (at least after each call to
+\cw{midend_process_key()}) to get the current labels.  The front end
+should arrange to copy the returned string somewhere before the next
+call to the mid-end, just in case it's dynamically allocated.  If the
+button supplied does nothing, the label returned will be an empty
+string.
 
 \H{midend-colours} \cw{midend_colours()}
 
--- a/dominosa.c
+++ b/dominosa.c
@@ -2734,6 +2734,33 @@
         ui->cur_visible = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button)) {
+        int d1, d2, w = state->w;
+
+	if (!((ui->cur_x ^ ui->cur_y) & 1))
+	    return "";	       /* must have exactly one dimension odd */
+	d1 = (ui->cur_y / 2) * w + (ui->cur_x / 2);
+	d2 = ((ui->cur_y+1) / 2) * w + ((ui->cur_x+1) / 2);
+
+        /* We can't mark an edge next to any domino. */
+        if (button == CURSOR_SELECT2 &&
+            (state->grid[d1] != d1 || state->grid[d2] != d2))
+            return "";
+        if (button == CURSOR_SELECT) {
+            if (state->grid[d1] == d2) return "Remove";
+            return "Place";
+        } else {
+            int edge = d2 == d1 + 1 ? EDGE_R : EDGE_B;
+            if (state->edges[d1] & edge) return "Remove";
+            return "Line";
+        }
+    }
+    return "";
+}
+
 #define PREFERRED_TILESIZE 32
 #define TILESIZE (ds->tilesize)
 #define BORDER (TILESIZE * 3 / 4)
@@ -3417,6 +3444,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/fifteen.c
+++ b/fifteen.c
@@ -1110,6 +1110,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/filling.c
+++ b/filling.c
@@ -1429,6 +1429,23 @@
     ui->keydragging = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    const int w = state->shared->params.w;
+
+    if (IS_CURSOR_SELECT(button) && ui->cur_visible) {
+        if (button == CURSOR_SELECT) {
+            if (ui->keydragging) return "Stop";
+            return "Multiselect";
+        }
+        if (button == CURSOR_SELECT2 &&
+            !state->shared->clues[w*ui->cur_y + ui->cur_x])
+	    return (ui->sel[w*ui->cur_y + ui->cur_x]) ? "Deselect" : "Select";
+    }
+    return "";
+}
+
 #define PREFERRED_TILE_SIZE 32
 #define TILE_SIZE (ds->tilesize)
 #define BORDER (TILE_SIZE / 2)
@@ -2166,6 +2183,7 @@
     decode_ui,
     game_request_keys,
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/flip.c
+++ b/flip.c
@@ -928,6 +928,13 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button)) return "Flip";
+    return "";
+}
+
 struct game_drawstate {
     int w, h;
     bool started;
@@ -1346,6 +1353,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/flood.c
+++ b/flood.c
@@ -793,6 +793,18 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (button == CURSOR_SELECT &&
+        state->grid[0] != state->grid[ui->cy*state->w+ui->cx])
+        return "Fill";
+    if (button == CURSOR_SELECT2 &&
+        state->soln && state->solnpos < state->soln->nmoves)
+        return "Advance";
+    return "";
+}
+
 struct game_drawstate {
     bool started;
     int tilesize;
@@ -1357,6 +1369,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/galaxies.c
+++ b/galaxies.c
@@ -2539,6 +2539,33 @@
     return true;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    space *sp;
+
+    if (IS_CURSOR_SELECT(button) && ui->cur_visible) {
+        sp = &SPACE(state, ui->cur_x, ui->cur_y);
+        if (ui->dragging) {
+            if (ui->cur_x == ui->srcx && ui->cur_y == ui->srcy)
+                return "Cancel";
+            if (ok_to_add_assoc_with_opposite(
+                    state, &SPACE(state, ui->cur_x, ui->cur_y),
+                    &SPACE(state, ui->dotx, ui->doty)))
+                return "Place";
+            return (ui->srcx == ui->dotx && ui->srcy == ui->doty) ?
+                "Cancel" : "Remove";
+        } else if (sp->flags & F_DOT)
+            return "New arrow";
+        else if (sp->flags & F_TILE_ASSOC)
+            return "Move arrow";
+        else if (sp->type == s_edge &&
+                 edge_placement_legal(state, ui->cur_x, ui->cur_y))
+            return (sp->flags & F_EDGE_SET) ? "Clear" : "Edge";
+    }
+    return "";
+}
+
 static char *interpret_move(const game_state *state, game_ui *ui,
                             const game_drawstate *ds,
                             int x, int y, int button)
@@ -3813,6 +3840,11 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+#ifdef EDITOR
+    NULL,
+#else
+    current_key_label,
+#endif
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/guess.c
+++ b/guess.c
@@ -510,6 +510,19 @@
 	ui->peg_cur = 0;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (state->solved) return "";
+    if (button == CURSOR_SELECT) {
+        if (ui->peg_cur == state->params.npegs) return "Submit";
+        return "Place";
+    }
+    if (button == CURSOR_SELECT2 && ui->peg_cur != state->params.npegs)
+        return "Hold";
+    return "";
+}
+
 #define PEGSZ   (ds->pegsz)
 #define PEGOFF  (ds->pegsz + ds->gapsz)
 #define HINTSZ  (ds->hintsz)
@@ -1522,6 +1535,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PEG_PREFER_SZ, game_compute_size, game_set_size,
--- a/inertia.c
+++ b/inertia.c
@@ -1545,6 +1545,15 @@
     ui->just_made_move = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) &&
+        state->soln && state->solnpos < state->soln->len)
+        return "Advance";
+    return "";
+}
+
 struct game_drawstate {
     game_params p;
     int tilesize;
@@ -2233,6 +2242,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/keen.c
+++ b/keen.c
@@ -1569,6 +1569,14 @@
     }
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (ui->hshow && (button == CURSOR_SELECT))
+        return ui->hpencil ? "Ink" : "Pencil";
+    return "";
+}
+
 #define PREFERRED_TILESIZE 48
 #define TILESIZE (ds->tilesize)
 #define BORDER (TILESIZE / 2)
@@ -2477,6 +2485,7 @@
     decode_ui,
     game_request_keys,
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/lightup.c
+++ b/lightup.c
@@ -1855,6 +1855,26 @@
         ui->cur_visible = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int cx = ui->cur_x, cy = ui->cur_y;
+    unsigned int flags = GRID(state, flags, cx, cy);
+
+    if (!ui->cur_visible) return "";
+    if (button == CURSOR_SELECT) {
+        if (flags & (F_BLACK | F_IMPOSSIBLE)) return "";
+        if (flags & F_LIGHT) return "Clear";
+        return "Light";
+    }
+    if (button == CURSOR_SELECT2) {
+        if (flags & (F_BLACK | F_LIGHT)) return "";
+        if (flags & F_IMPOSSIBLE) return "Clear";
+        return "Mark";
+    }
+    return "";
+}
+
 #define DF_BLACK        1       /* black square */
 #define DF_NUMBERED     2       /* black square with number */
 #define DF_LIT          4       /* display (white) square lit up */
@@ -2324,6 +2344,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/loopy.c
+++ b/loopy.c
@@ -3677,6 +3677,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/magnets.c
+++ b/magnets.c
@@ -1745,6 +1745,36 @@
         ui->cur_visible = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int idx;
+
+    if (IS_CURSOR_SELECT(button)) {
+        if (!ui->cur_visible) return "";
+        idx = ui->cur_y * state->w + ui->cur_x;
+        if (button == CURSOR_SELECT) {
+            if (state->grid[idx] == NEUTRAL && state->flags[idx] & GS_SET)
+                return "";
+            switch (state->grid[idx]) {
+              case EMPTY: return "+";
+              case POSITIVE: return "-";
+              case NEGATIVE: return "Clear";
+            }
+        }
+        if (button == CURSOR_SELECT2) {
+            if (state->grid[idx] != NEUTRAL) return "";
+            if (state->flags[idx] & GS_SET) /* neutral */
+                return "?";
+            if (state->flags[idx] & GS_NOTNEUTRAL) /* !neutral */
+                return "Clear";
+            else
+                return "X";
+        }
+    }
+    return "";
+}
+    
 struct game_drawstate {
     int tilesize;
     bool started, solved;
@@ -2431,6 +2461,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/map.c
+++ b/map.c
@@ -2392,6 +2392,26 @@
                                       EPSILON_Y(ui->cur_lastmove));
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int r;
+
+    if (IS_CURSOR_SELECT(button) && ui->cur_visible) {
+        if (ui->drag_colour == -2) return "Pick";
+        r = region_from_ui_cursor(state, ui);
+        if (state->map->immutable[r]) return "Cancel";
+        if (!ui->cur_moved) return ui->drag_pencil ? "Cancel" : "Clear";
+        if (button == CURSOR_SELECT2) {
+            if (state->colouring[r] >= 0) return "Cancel";
+            if (ui->drag_colour >= 0) return "Stipple";
+        }
+        if (ui->drag_pencil) return "Stipple";
+        return ui->drag_colour >= 0 ? "Fill" : "Clear";
+    }
+    return "";
+}
+
 static char *interpret_move(const game_state *state, game_ui *ui,
                             const game_drawstate *ds,
                             int x, int y, int button)
@@ -3257,6 +3277,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     20, game_compute_size, game_set_size,
--- a/midend.c
+++ b/midend.c
@@ -1220,6 +1220,15 @@
     return keys;
 }
 
+/* Return a good label to show next to a key right now. */
+const char *midend_current_key_label(midend *me, int button)
+{
+    assert(IS_CURSOR_SELECT(button));
+    if (!me->ourgame->current_key_label) return "";
+    return me->ourgame->current_key_label(
+        me->ui, me->states[me->statepos-1].state, button);
+}
+
 void midend_redraw(midend *me)
 {
     assert(me->drawing);
--- a/mines.c
+++ b/mines.c
@@ -2400,6 +2400,35 @@
 	ui->completed = true;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int cx = ui->cur_x, cy = ui->cur_y;
+    int v = state->grid[cy * state->w + cx];
+
+    if (state->dead || state->won || !ui->cur_visible) return "";
+    if (button == CURSOR_SELECT2) {
+        if (v == -2) return "Mark";
+        if (v == -1) return "Unmark";
+        return "";
+    }
+    if (button == CURSOR_SELECT) {
+        int dy, dx, n = 0;
+        if (v == -2 || v == -3) return "Uncover";
+        if (v == 0) return "";
+        /* Count mine markers. */
+        for (dy = -1; dy <= +1; dy++)
+            for (dx = -1; dx <= +1; dx++)
+                if (cx+dx >= 0 && cx+dx < state->w &&
+			cy+dy >= 0 && cy+dy < state->h) {
+			if (state->grid[(cy+dy)*state->w+(cx+dx)] == -1)
+			    n++;
+		    }
+        if (n == v) return "Clear";
+    }
+    return "";
+}
+
 struct game_drawstate {
     int w, h, tilesize, bg;
     bool started;
@@ -3208,6 +3237,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/mosaic.c
+++ b/mosaic.c
@@ -1033,6 +1033,26 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    char *cell;
+
+    if (IS_CURSOR_SELECT(button)) {
+        if (!ui->cur_visible || state->not_completed_clues == 0) return "";
+        cell = get_coords(state, state->cells_contents, ui->cur_x, ui->cur_y);
+        switch (*cell & STATE_OK_NUM) {
+          case STATE_UNMARKED:
+            return button == CURSOR_SELECT ? "Black" : "White";
+          case STATE_MARKED:
+            return button == CURSOR_SELECT ? "White" : "Empty";
+          case STATE_BLANK:
+            return button == CURSOR_SELECT ? "Empty" : "Black";
+        }
+    }
+    return "";
+}
+
 static char *interpret_move(const game_state *state, game_ui *ui,
                             const game_drawstate *ds, int x, int y,
                             int button)
@@ -1598,6 +1618,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     DEFAULT_TILE_SIZE, game_compute_size, game_set_size,
--- a/net.c
+++ b/net.c
@@ -2053,6 +2053,18 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (tile(state, ui->cur_x, ui->cur_y) & LOCKED) {
+        if (button == CURSOR_SELECT2) return "Unlock";
+    } else {
+        if (button == CURSOR_SELECT) return "Rotate";
+        if (button == CURSOR_SELECT2) return "Lock";
+    }
+    return "";
+}
+
 struct game_drawstate {
     int width, height;
     int tilesize;
@@ -3254,6 +3266,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/netslide.c
+++ b/netslide.c
@@ -1050,6 +1050,14 @@
     int cur_x, cur_y;
 };
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cur_visible)
+        return "Slide";
+    return "";
+}
+
 static char *interpret_move(const game_state *state, game_ui *ui,
                             const game_drawstate *ds,
                             int x, int y, int button)
@@ -1875,6 +1883,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/nullgame.c
+++ b/nullgame.c
@@ -287,6 +287,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     20 /* FIXME */, game_compute_size, game_set_size,
--- a/palisade.c
+++ b/palisade.c
@@ -1392,6 +1392,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     48, game_compute_size, game_set_size,
--- a/pattern.c
+++ b/pattern.c
@@ -1241,6 +1241,23 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button)) {
+        if (!ui->cur_visible) return "";
+        switch (state->grid[ui->cur_y * state->common->w + ui->cur_x]) {
+          case GRID_UNKNOWN:
+            return button == CURSOR_SELECT ? "Black" : "White";
+          case GRID_FULL:
+            return button == CURSOR_SELECT ? "White" : "Grey";
+          case GRID_EMPTY:
+            return button == CURSOR_SELECT ? "Grey" : "Black";
+        }
+    }
+    return "";
+}
+
 struct game_drawstate {
     bool started;
     int w, h;
@@ -2025,6 +2042,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/pearl.c
+++ b/pearl.c
@@ -1847,6 +1847,20 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cursor_active) {
+        if (button == CURSOR_SELECT) {
+            if (ui->ndragcoords == -1) return "Start";
+            return "Stop";
+        }
+        if (button == CURSOR_SELECT2 && ui->ndragcoords >= 0)
+            return "Cancel";
+    }
+    return "";
+}
+
 #define PREFERRED_TILE_SIZE 31
 #define HALFSZ (ds->halfsz)
 #define TILE_SIZE (ds->halfsz*2 + 1)
@@ -2661,6 +2675,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/pegs.c
+++ b/pegs.c
@@ -800,6 +800,19 @@
     ui->cur_jumping = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int w = state->w;
+
+    if (IS_CURSOR_SELECT(button)) {
+        if (!ui->cur_visible) return "";
+        if (ui->cur_jumping) return "Cancel";
+        if (state->grid[ui->cur_y*w+ui->cur_x] == GRID_PEG) return "Select";
+    }
+    return "";
+}
+
 #define PREFERRED_TILE_SIZE 33
 #define TILESIZE (ds->tilesize)
 #define BORDER (TILESIZE / 2)
@@ -1338,6 +1351,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/puzzles.h
+++ b/puzzles.h
@@ -307,6 +307,7 @@
 void midend_stop_anim(midend *me);
 bool midend_process_key(midend *me, int x, int y, int button, bool *handled);
 key_label *midend_request_keys(midend *me, int *nkeys);
+const char *midend_current_key_label(midend *me, int button);
 void midend_force_redraw(midend *me);
 void midend_redraw(midend *me);
 float *midend_colours(midend *me, int *ncolours);
@@ -657,6 +658,8 @@
     key_label *(*request_keys)(const game_params *params, int *nkeys);
     void (*changed_state)(game_ui *ui, const game_state *oldstate,
                           const game_state *newstate);
+    const char *(*current_key_label)(const game_ui *ui,
+                                     const game_state *state, int button);
     char *(*interpret_move)(const game_state *state, game_ui *ui,
                             const game_drawstate *ds, int x, int y, int button);
     game_state *(*execute_move)(const game_state *state, const char *move);
--- a/range.c
+++ b/range.c
@@ -1245,6 +1245,27 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int cell;
+
+    if (IS_CURSOR_SELECT(button)) {
+        cell = state->grid[idx(ui->r, ui->c, state->params.w)];
+        if (!ui->cursor_show || cell > 0) return "";
+        switch (cell) {
+          case EMPTY:
+            return button == CURSOR_SELECT ? "Fill" : "Dot";
+          case WHITE:
+            return button == CURSOR_SELECT ? "Empty" : "Fill";
+          case BLACK:
+            return button == CURSOR_SELECT ? "Dot" : "Empty";
+        }
+    }
+    return "";
+
+}
+
 typedef struct drawcell {
     puzzle_size value;
     bool error, cursor, flash;
@@ -1818,6 +1839,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/rect.c
+++ b/rect.c
@@ -2374,6 +2374,21 @@
     unsigned long *visible;
 };
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cur_visible &&
+        !(ui->drag_start_x >= 0 && !ui->cur_dragging)) {
+        if (ui->cur_dragging) {
+            if (!ui->dragged) return "Cancel";
+            if ((button == CURSOR_SELECT2) == ui->erasing) return "Done";
+            return "Cancel";
+        }
+        return button == CURSOR_SELECT ? "Mark" : "Erase";
+    }
+    return "";
+}
+
 static char *interpret_move(const game_state *from, game_ui *ui,
                             const game_drawstate *ds,
                             int x, int y, int button)
@@ -2992,6 +3007,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/samegame.c
+++ b/samegame.c
@@ -1109,6 +1109,25 @@
 	ui->displaysel = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button)) {
+        int x = ui->xsel, y = ui->ysel, c = COL(state,x,y);
+        if (c == 0) return "";
+        if (ISSEL(ui, x, y))
+            return button == CURSOR_SELECT2 ? "Unselect" : "Remove";
+        if ((x > 0 && COL(state,x-1,y) == c) ||
+            (x+1 < state->params.w && COL(state,x+1,y) == c) ||
+            (y > 0 && COL(state,x,y-1) == c) ||
+            (y+1 < state->params.h && COL(state,x,y+1) == c))
+            return "Select";
+        /* Cursor is over a lone square, so we can't select it. */
+        if (ui->nselected) return "Unselect";
+    }
+    return "";
+}
+
 static char *sel_movedesc(game_ui *ui, const game_state *state)
 {
     int i;
@@ -1670,6 +1689,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/signpost.c
+++ b/signpost.c
@@ -1421,6 +1421,26 @@
     }
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cshow) {
+        if (ui->dragging) {
+            if (ui->drag_is_from) {
+                if (isvalidmove(state, false, ui->sx, ui->sy, ui->cx, ui->cy))
+                    return "To here";
+            } else {
+                if (isvalidmove(state, false, ui->cx, ui->cy, ui->sx, ui->sy))
+                    return "From here";
+            }
+            return "Cancel";
+        } else {
+            return button == CURSOR_SELECT ? "From here" : "To here";
+        }
+    }
+    return "";
+}
+
 struct game_drawstate {
     int tilesize;
     bool started, solved;
@@ -2277,6 +2297,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/singles.c
+++ b/singles.c
@@ -1473,6 +1473,18 @@
         ui->cshow = false;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cshow) {
+        unsigned int f = state->flags[ui->cy * state->w + ui->cx];
+        if (f & F_BLACK) return "Restore";
+        if (f & F_CIRCLE) return "Remove";
+        return button == CURSOR_SELECT ? "Black" : "Circle";
+    }
+    return "";
+}
+
 #define DS_BLACK        0x1
 #define DS_CIRCLE       0x2
 #define DS_CURSOR       0x4
@@ -1853,6 +1865,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/sixteen.c
+++ b/sixteen.c
@@ -591,6 +591,21 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cur_visible) {
+        if (ui->cur_x == -1 || ui->cur_x == state->w ||
+                ui->cur_y == -1 || ui->cur_y == state->h)
+            return button == CURSOR_SELECT2 ? "Back" : "Slide";
+        if (button == CURSOR_SELECT)
+            return ui->cur_mode == lock_tile ? "Unlock" : "Lock tile";
+        if (button == CURSOR_SELECT2)
+            return ui->cur_mode == lock_position ? "Unlock" : "Lock pos";
+    }
+    return "";
+}
+
 struct game_drawstate {
     bool started;
     int w, h, bgcolour;
@@ -1197,6 +1212,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/slant.c
+++ b/slant.c
@@ -1603,6 +1603,22 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cur_visible) {
+        switch (state->soln[ui->cur_y*state->p.w+ui->cur_x]) {
+          case 0:
+            return button == CURSOR_SELECT ? "\\" : "/";
+          case -1:
+            return button == CURSOR_SELECT ? "/" : "Blank";
+          case +1:
+            return button == CURSOR_SELECT ? "Blank" : "\\";
+        }
+    }
+    return "";
+}
+
 #define PREFERRED_TILESIZE 32
 #define TILESIZE (ds->tilesize)
 #define BORDER TILESIZE
@@ -2175,6 +2191,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/solo.c
+++ b/solo.c
@@ -4594,6 +4594,14 @@
     }
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (ui->hshow && (button == CURSOR_SELECT))
+        return ui->hpencil ? "Ink" : "Pencil";
+    return "";
+}
+
 struct game_drawstate {
     bool started, xtype;
     int cr;
@@ -5618,6 +5626,7 @@
     decode_ui,
     game_request_keys,
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/tents.c
+++ b/tents.c
@@ -1459,6 +1459,22 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int w = state->p.w;
+    int v = state->grid[ui->cy*w+ui->cx];
+
+    if (IS_CURSOR_SELECT(button) && ui->cdisp) {
+        switch (v) {
+          case BLANK:
+            return button == CURSOR_SELECT ? "Tent" : "Green";
+          case TENT: case NONTENT: return "Clear";
+        }
+    }
+    return "";
+}
+
 struct game_drawstate {
     int tilesize;
     bool started;
@@ -2626,6 +2642,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/towers.c
+++ b/towers.c
@@ -1198,6 +1198,14 @@
     }
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (ui->hshow && (button == CURSOR_SELECT))
+        return ui->hpencil ? "Ink" : "Pencil";
+    return "";
+}
+
 #define PREFERRED_TILESIZE 48
 #define TILESIZE (ds->tilesize)
 #define BORDER (TILESIZE * 9 / 8)
@@ -2048,6 +2056,7 @@
     decode_ui,
     game_request_keys,
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/tracks.c
+++ b/tracks.c
@@ -2121,6 +2121,35 @@
     return true;
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (IS_CURSOR_SELECT(button) && ui->cursor_active) {
+        int gx = ui->curx / 2, gy = ui->cury / 2;
+        int w = state->p.w;
+        int direction =
+            ((ui->curx % 2) == 0) ? L : ((ui->cury % 2) == 0) ? U : 0;
+        if (direction &&
+            ui_can_flip_edge(state, gx, gy, direction,
+                             button == CURSOR_SELECT2)) {
+            unsigned ef = S_E_FLAGS(state, gx, gy, direction);
+            switch (button) {
+              case CURSOR_SELECT: return (ef & E_TRACK) ? "Clear" : "Track";
+              case CURSOR_SELECT2: return (ef & E_NOTRACK) ? "Clear" : "X";
+            }
+        }
+        if (!direction &&
+            ui_can_flip_square(state, gx, gy, button == CURSOR_SELECT2)) {
+            unsigned sf = state->sflags[gy*w+gx];
+            switch (button) {
+              case CURSOR_SELECT: return (sf & S_TRACK) ? "Clear" : "Track";
+              case CURSOR_SELECT2: return (sf & S_NOTRACK) ? "Clear" : "X";
+            }
+        }
+    }
+    return "";
+}
+
 static char *edge_flip_str(const game_state *state, int x, int y, int dir, bool notrack, char *buf) {
     unsigned ef = S_E_FLAGS(state, x, y, dir);
     char c;
@@ -2973,6 +3002,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/twiddle.c
+++ b/twiddle.c
@@ -637,6 +637,17 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (!ui->cur_visible) return "";
+    switch (button) {
+      case CURSOR_SELECT: return "Turn left";
+      case CURSOR_SELECT2: return "Turn right";
+    }
+    return "";
+}
+
 struct game_drawstate {
     bool started;
     int w, h, bgcolour;
@@ -1313,6 +1324,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/undead.c
+++ b/undead.c
@@ -1681,6 +1681,20 @@
     }
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int xi;
+
+    if (ui->hshow && button == CURSOR_SELECT)
+        return ui->hpencil ? "Ink" : "Pencil";
+    if (button == CURSOR_SELECT2) {
+        xi = state->common->xinfo[ui->hx + ui->hy*(state->common->params.w+2)];
+        if (xi >= 0 && !state->common->fixed[xi]) return "Clear";
+    }
+    return "";
+}
+
 struct game_drawstate {
     int tilesize;
     bool started, solved;
@@ -2783,6 +2797,7 @@
     decode_ui,
     game_request_keys,
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/unequal.c
+++ b/unequal.c
@@ -1470,6 +1470,14 @@
     }
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (ui->hshow && IS_CURSOR_SELECT(button))
+        return ui->hpencil ? "Ink" : "Pencil";
+    return "";
+}
+
 struct game_drawstate {
     int tilesize, order;
     bool started;
@@ -2139,6 +2147,7 @@
     decode_ui,
     game_request_keys,
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILE_SIZE, game_compute_size, game_set_size,
--- a/unfinished/group.c
+++ b/unfinished/group.c
@@ -1326,6 +1326,26 @@
     }
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    if (ui->hshow && button == CURSOR_SELECT)
+        return ui->hpencil ? "Ink" : "Pencil";
+    if (ui->hshow && button == CURSOR_SELECT2) {
+        int w = state->par.w;
+        int i;
+        for (i = 0; i < ui->odn; i++) {
+            int x = state->sequence[ui->ohx + i*ui->odx];
+            int y = state->sequence[ui->ohy + i*ui->ody];
+            int index = y*w+x;
+            if (ui->hpencil && state->grid[index]) return "";
+            if (state->common->immutable[index]) return "";
+        }
+        return "Clear";
+    }
+    return "";
+}
+
 #define PREFERRED_TILESIZE 48
 #define TILESIZE (ds->tilesize)
 #define BORDER (TILESIZE / 2)
@@ -2311,6 +2331,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/unfinished/separate.c
+++ b/unfinished/separate.c
@@ -845,6 +845,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     20 /* FIXME */, game_compute_size, game_set_size,
--- a/unfinished/slide.c
+++ b/unfinished/slide.c
@@ -2337,6 +2337,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/unfinished/sokoban.c
+++ b/unfinished/sokoban.c
@@ -1459,6 +1459,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,
--- a/unruly.c
+++ b/unruly.c
@@ -1535,6 +1535,27 @@
 {
 }
 
+static const char *current_key_label(const game_ui *ui,
+                                     const game_state *state, int button)
+{
+    int hx = ui->cx, hy = ui->cy;
+    int w2 = state->w2;
+    char i = state->grid[hy * w2 + hx];
+
+    if (ui->cursor && IS_CURSOR_SELECT(button)) {
+        if (state->common->immutable[hy * w2 + hx]) return "";
+        switch (i) {
+          case EMPTY:
+            return button == CURSOR_SELECT ? "Black" : "White";
+          case N_ONE:
+            return button == CURSOR_SELECT ? "White" : "Empty";
+          case N_ZERO:
+            return button == CURSOR_SELECT ? "Empty" : "Black";
+        }
+    }
+    return "";
+}
+
 struct game_drawstate {
     int tilesize;
     int w2, h2;
@@ -2021,6 +2042,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    current_key_label,
     interpret_move,
     execute_move,
     DEFAULT_TILE_SIZE, game_compute_size, game_set_size,
--- a/untangle.c
+++ b/untangle.c
@@ -1481,6 +1481,7 @@
     decode_ui,
     NULL, /* game_request_keys */
     game_changed_state,
+    NULL, /* current_key_label */
     interpret_move,
     execute_move,
     PREFERRED_TILESIZE, game_compute_size, game_set_size,