]> git.draconx.ca Git - rrace.git/commitdiff
curses: Begin to implement a game menu system.
authorNick Bowler <nbowler@draconx.ca>
Sun, 12 Jun 2022 15:51:27 +0000 (11:51 -0400)
committerNick Bowler <nbowler@draconx.ca>
Sun, 12 Jun 2022 15:56:26 +0000 (11:56 -0400)
Add the skeleton for very simple control interface based on function
keys.  Show labels along the bottom row for up to 10 functions, with
each corresponding to the first 10 function keys (or esc+number).

For now, hardcode two functions: new game and exit.  More to come.

src/curses.c

index 06ec3519c87e530bcaa4837c7ff7276c882bab47..7497395cbaa4f4865f18faf3ea979113a1935bad 100644 (file)
@@ -45,11 +45,20 @@ enum {
        WINDOW_MAX,
 };
 
+/* Colour pair enumeration */
+enum {
+       /* Pairs 1-6 correspond to tile colours */
+       RR_COLOUR_CURSOR = TILE_MAX, // black on black, for the cursor
+       RR_COLOUR_TOOLBAR,           // cyan on black (use reverse video)
+       RR_COLOUR_MAX
+};
+
 static struct app_state {
        struct board board;
 
        WINDOW *gamewin[WINDOW_MAX], *goalwin[WINDOW_MAX];
-       WINDOW *timer;
+       WINDOW *toolbar, *timer;
+       int last_input;
 
        /* Most recently displayed timer value, for screen redraw. */
        uint_least32_t timer_ms;
@@ -114,7 +123,7 @@ static void draw_tile(WINDOW **win, unsigned colour, unsigned selected,
        case TILE_BLUE:   ch = 'o'; attr |= A_BOLD; break;
        case TILE_WHITE:  ch = '.'; attr |= A_BOLD; break;
 
-       case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(TILE_MAX);
+       case TILE_EMPTY: attr = A_BOLD|COLOR_PAIR(RR_COLOUR_CURSOR);
        }
 
        getmaxyx(border, h, w);
@@ -276,6 +285,58 @@ static void realloc_tiles(WINDOW **win, int h)
        win[WINDOW_TILEFILL] = derwin(border, h-2, w-2, 1, 1);
 }
 
+/*
+ * Given the toolbar function number (between 1 and 10, inclusive), and the
+ * total width of the screen, return the character position for the start of
+ * its label display.
+ *
+ * The intention is to divide the width of the screen into 10 roughly
+ * equally-sized areas, spreading out the remainder so that the width of
+ * each label is a monotone increasing function of the total width.
+ *
+ * The minimum size of a label is 6 characters (2 of which are used for the
+ * number indicator)
+ */
+static int toolbar_xpos(int i, int total_width)
+{
+       int button_width = MAX(6, total_width/10);
+       int rem = total_width - 10*button_width;
+       int pos = (i-1)*button_width;
+
+       switch (rem) {
+       case 9: pos += i > 6;
+       case 8: pos += i > 2;
+       case 7: pos += i > 7;
+       case 6: pos += i > 3;
+       case 5: pos += i > 8;
+       case 4: pos += i > 4;
+       case 3: pos += i > 9;
+       case 2: pos += i > 5;
+       }
+
+       return pos;
+}
+
+static void draw_toolbar(struct app_state *state)
+{
+       WINDOW *toolbar = state->toolbar;
+       int i, w, lw;
+
+       getmaxyx(toolbar, i, w);
+       werase(toolbar);
+
+       lw = MAX(6, w/10);
+       mvwprintw(toolbar, 0, toolbar_xpos( 1, w)+2, "%.*s", lw, "Help");
+       mvwprintw(toolbar, 0, toolbar_xpos( 2, w)+2, "%.*s", lw, "NewGame");
+       mvwprintw(toolbar, 0, toolbar_xpos(10, w)+2, "%.*s", lw, "Exit");
+
+       mvwchgat(toolbar, 0, 0, -1, A_REVERSE, RR_COLOUR_TOOLBAR, 0);
+       for (i = 1; i <= 10; i++)
+               mvwprintw(toolbar, 0, toolbar_xpos(i, w), "%2d", i);
+
+       wnoutrefresh(state->toolbar);
+}
+
 static void setup_mainwin(struct app_state *state)
 {
        int w, h, gamesz, goalsz, scr_w, scr_h, split;
@@ -312,6 +373,10 @@ static void setup_mainwin(struct app_state *state)
        /* Status area */
        w = MAX(0, scr_w-split-1);
        realloc_area(&state->timer, 1, w, GAME_YPOS+h, split+1);
+
+       /* Toolbar */
+       realloc_area(&state->toolbar, 1, scr_w, scr_h-1, 0);
+       draw_toolbar(state);
 }
 
 static void app_initialize(int argc, char **argv)
@@ -392,7 +457,8 @@ static void app_initialize(int argc, char **argv)
        init_pair(TILE_GREEN, COLOR_GREEN, COLOR_BLACK);
        init_pair(TILE_BLUE, COLOR_BLUE, COLOR_BLACK);
        init_pair(TILE_WHITE, COLOR_WHITE, COLOR_BLACK);
-       init_pair(TILE_MAX, COLOR_BLACK, COLOR_BLACK);
+       init_pair(RR_COLOUR_CURSOR, COLOR_BLACK, COLOR_BLACK);
+       init_pair(RR_COLOUR_TOOLBAR, COLOR_CYAN, COLOR_BLACK);
 
        setup_mainwin(&state);
        refresh();
@@ -457,7 +523,111 @@ static void do_new_game(struct app_state *state)
        game_begin(&state->board);
 }
 
+static void do_function(struct app_state *state, unsigned func)
+{
+       switch (func) {
+       case 2:
+               do_new_game(state);
+               break;
+       case 10:
+               endwin();
+               exit(0);
+       }
+}
+
 #if HAVE_CURSES_MOUSE_SUPPORT
+/*
+ * Returns the toolbar function (1 through 10) under the given x, y screen
+ * coordinates, or 0 if the coordinates are outside of the toolbar.
+ */
+static int mouse_toolbar_function(struct app_state *state, int x, int y)
+{
+       int toolbar_x, toolbar_y, toolbar_w, toolbar_h, i;
+
+       getbegyx(state->toolbar, toolbar_y, toolbar_x);
+       getmaxyx(state->toolbar, toolbar_h, toolbar_w);
+
+       if ((void)toolbar_x, y != toolbar_y)
+               return 0;
+
+       /* OK, selected a button, determine which one */
+       for (i = 10; i > 1; i--) {
+               if ((void)toolbar_h, x >= toolbar_xpos(i, toolbar_w))
+                       break;
+       }
+
+       return i;
+}
+
+/*
+ * Given x, y as screen coordinates, record which (if any) toolbar function
+ * label is at that position, to be performed later.
+ *
+ * If no function is indicated, returns zero.  Otherwise, returns non-zero.
+ */
+static int press_toolbar(struct app_state *state, int x, int y)
+{
+       return state->toolbar_click = mouse_toolbar_function(state, x, y);
+}
+
+/*
+ * Perform the action previously recorded by press_toolbar, if and only if
+ * the x, y screen coordinates correspond to the same function.
+ */
+static void release_toolbar(struct app_state *state, int x, int y)
+{
+       int func = mouse_toolbar_function(state, x, y);
+
+       if (func && state->toolbar_click == func) {
+               do_function(state, func);
+       }
+
+       state->toolbar_click = 0;
+}
+
+/*
+ * Given x, y as screen coordinates, determine which (if any) game tile is
+ * at that position.
+ *
+ * If there is no such tile, performs no action and returns 0.
+ *
+ * Otherwise, attempts to move that tile and returns non-zero.
+ */
+static int press_tile(struct app_state *state, int x, int y)
+{
+       int game_x, game_y, tile_w, tile_h;
+       uint_fast32_t cursor_mask, move_mask;
+
+       getbegyx(state->gamewin[WINDOW_AREA], game_y, game_x);
+       getmaxyx(state->gamewin[WINDOW_TILEBORDER], tile_h, tile_w);
+       tile_w += tile_w & 1;
+
+       /* special case the left spacer column */
+       if (x == game_x+1)
+               x++;
+
+       if (x < game_x+2 || (x -= game_x+2)/5 >= tile_w)
+               return 0;
+       if (y < game_y+1 || (y -= game_y+1)/5 >= tile_h)
+               return 0;
+
+       /* OK, selected a tile. */
+       x /= tile_w;
+       y /= tile_h;
+
+       /* Disable the keyboard cursor due to mouse action */
+       cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
+       state->cursor = -1;
+
+       move_mask = do_move(state, x, y);
+       if ((cursor_mask & move_mask) == 0) {
+               curs_redraw_game(state, cursor_mask);
+               doupdate();
+       }
+
+       return 1;
+}
+
 static void do_mouse(struct app_state *state)
 {
        unsigned long bstate;
@@ -500,29 +670,20 @@ static void do_mouse(struct app_state *state)
                doupdate();
        }
 
-       if (!state->view_goal_on_game && bstate & BUTTON1_PRESSED) {
-               uint_fast32_t cursor_mask, move_mask;
-               int w, h;
-
-               /* Determine size of the game area */
-               getmaxyx(state->gamewin[WINDOW_TILEBORDER], h, w);
-               w = 2*(w+1)/2;
-
-               if (x < 4 || (x -= 4)/5 >= w) return;
-               if (y <= GAME_YPOS || (y -= GAME_YPOS+1)/5 >= h) return;
+       /* Ignore button1 if holding button3 to view goal */
+       if (state->view_goal_on_game & 1)
+               return;
 
-               /* Turn off the keyboard cursor when using the mouse */
-               cursor_mask = state->cursor < 0 ? -1 : 1ul << state->cursor;
-               state->cursor = -1;
+       if (bstate & BUTTON1_PRESSED) {
+               if (press_toolbar(state, x, y));
+               else if (press_tile(state, x, y));
+       }
 
-               move_mask = do_move(state, x/w, y/h);
-               if ((cursor_mask & move_mask) == 0) {
-                       curs_redraw_game(state, cursor_mask);
-                       doupdate();
-               }
+       if (bstate & BUTTON1_RELEASED) {
+               release_toolbar(state, x, y);
        }
 }
-#endif
+#endif /* HAVE_CURSES_MOUSE_SUPPORT */
 
 static void do_move_cursor(struct app_state *state, int c)
 {
@@ -565,6 +726,9 @@ static void do_move_cursor(struct app_state *state, int c)
 
 static void do_keystroke(struct app_state *state, int c)
 {
+       int last_input = state->last_input;
+       state->last_input = c;
+
        switch (c) {
        case KEY_DOWN: case KEY_UP: case KEY_LEFT: case KEY_RIGHT:
                do_move_cursor(state, c);
@@ -579,40 +743,56 @@ static void do_keystroke(struct app_state *state, int c)
                        do_move(state, state->cursor%5, state->cursor/5);
                break;
        }
-}
 
-int main(int argc, char **argv)
-{
-       setlocale(LC_ALL, "");
-       app_initialize(argc, argv);
+       /* ESC+# keys */
+       if (last_input == '\33' && c >= '0' && c <= '9') {
+               do_function(state, (c -= '0') == 0 ? 10 : c);
+       }
 
-       do_new_game(&state);
+       /* F# keys */
+       if (c >= KEY_F(1) && c <= KEY_F(10)) {
+               do_function(state, c - KEY_F0);
+       }
+}
 
-       while (1) {
-               int c = getch();
+/* One iteration of main input loop */
+void do_mainloop(struct app_state *state)
+{
+       int c = getch();
 
-               switch (c) {
+       switch (c) {
 #ifdef KEY_RESIZE
-               case KEY_RESIZE:
-                       setup_mainwin(&state);
-                       clear();
-                       refresh();
-                       curs_redraw_game(&state, -1);
-                       curs_redraw_goal(&state, -1);
-                       update_timer(&state, state.timer_ms);
-                       break;
+       case KEY_RESIZE:
+               setup_mainwin(state);
+               clear();
+               refresh();
+               curs_redraw_game(state, -1);
+               curs_redraw_goal(state, -1);
+               draw_toolbar(state);
+               update_timer(state, state->timer_ms);
+               break;
 #endif
 #if HAVE_CURSES_MOUSE_SUPPORT
-               case KEY_MOUSE:
-                       do_mouse(&state);
-                       break;
+       case KEY_MOUSE:
+               do_mouse(state);
+               break;
 #endif
-               default:
-                       do_keystroke(&state, c);
-               case ERR:;
-               }
-
-               if (state.board.x <= 4)
-                       update_timer(&state, game_elapsed(&state.board));
+       default:
+               do_keystroke(state, c);
+       case ERR:;
        }
+
+       if (state->board.x <= 4)
+               update_timer(state, game_elapsed(&state->board));
+}
+
+int main(int argc, char **argv)
+{
+       setlocale(LC_ALL, "");
+       app_initialize(argc, argv);
+
+       do_new_game(&state);
+       while (1)
+               do_mainloop(&state);
+       abort();
 }