From 36dd47c5a92410eed56d52619a3617e6ffbf12b2 Mon Sep 17 00:00:00 2001 From: Nick Bowler Date: Sun, 12 Jun 2022 11:51:27 -0400 Subject: [PATCH] curses: Begin to implement a game menu system. 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 | 278 ++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 229 insertions(+), 49 deletions(-) diff --git a/src/curses.c b/src/curses.c index 06ec351..7497395 100644 --- a/src/curses.c +++ b/src/curses.c @@ -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(); } -- 2.43.2