From 9803343f14f87a7f20b24f88add7e384f9d3eb56 Mon Sep 17 00:00:00 2001 From: cancel Date: Wed, 12 Dec 2018 21:55:54 +0900 Subject: [PATCH] Add start of new menu/nav-stack system --- term_util.c | 203 ++++++++++++++++++++++++++++++++++++++++++++++++++++ term_util.h | 102 ++++++++++++++++++++++++++ tool | 4 +- tui_main.c | 196 ++++++++++++++++++++++++++++++-------------------- 4 files changed, 426 insertions(+), 79 deletions(-) create mode 100644 term_util.c create mode 100644 term_util.h diff --git a/term_util.c b/term_util.c new file mode 100644 index 0000000..749bd50 --- /dev/null +++ b/term_util.c @@ -0,0 +1,203 @@ +#include "base.h" +#include "term_util.h" + +void term_util_init_colors() { + if (has_colors()) { + // Enable color + start_color(); + use_default_colors(); + for (int ifg = 0; ifg < Colors_count; ++ifg) { + for (int ibg = 0; ibg < Colors_count; ++ibg) { + int res = init_pair((short int)(1 + ifg * Colors_count + ibg), + (short int)(ifg - 1), (short int)(ibg - 1)); + (void)res; + // Might fail on Linux virtual console/terminal for a couple of colors. + // Just ignore. +#if 0 + if (res == ERR) { + endwin(); + fprintf(stderr, "Error initializing color pair: %d %d\n", ifg - 1, + ibg - 1); + exit(1); + } +#endif + } + } + } +} + +#define ORCA_CONTAINER_OF(ptr, type, member) \ + ((type*)((char*)(1 ? (ptr) : &((type*)0)->member) - offsetof(type, member))) + +Qnav_stack qnav_stack; + +static struct { int unused; } qmenu_spacer_user_unique; + +void qnav_init() { + qnav_stack.count = 0; + qnav_stack.stack_changed = false; + memset(qnav_stack.blocks, 0, sizeof(qnav_stack.blocks)); +} +void qnav_deinit() { + while (qnav_stack.count != 0) + qnav_stack_pop(); +} +void qnav_stack_push(Qnav_type_tag tag, int height, int width, + Qnav_block* out) { +#ifndef NDEBUG + for (Usz i = 0; i < qnav_stack.count; ++i) { + assert(qnav_stack.blocks[i] != out); + } +#endif + int left; + if (qnav_stack.count > 0) { + WINDOW* w = qnav_stack.blocks[qnav_stack.count - 1]->outer_window; + left = getbegx(w) + getmaxx(w) + 0; + } else { + left = 0; + } + qnav_stack.blocks[qnav_stack.count] = out; + ++qnav_stack.count; + out->outer_window = newwin(height + 2, width + 3, 0, left); + out->content_window = derwin(out->outer_window, height, width, 1, 1); + out->tag = tag; + qnav_stack.stack_changed = true; +} +Qnav_block* qnav_top_block() { + if (qnav_stack.count == 0) + return NULL; + return qnav_stack.blocks[qnav_stack.count - 1]; +} +void qnav_free_block(Qnav_block* qb); +void qnav_stack_pop() { + assert(qnav_stack.count > 0); + if (qnav_stack.count == 0) + return; + Qnav_block* qb = qnav_stack.blocks[qnav_stack.count - 1]; + WINDOW* content_window = qb->content_window; + WINDOW* outer_window = qb->outer_window; + qnav_free_block(qb); + delwin(content_window); + delwin(outer_window); + --qnav_stack.count; + qnav_stack.blocks[qnav_stack.count] = NULL; + qnav_stack.stack_changed = true; +} +void qnav_draw_box(Qnav_block* qb) { box(qb->outer_window, 0, 0); } +void qnav_draw_title(Qnav_block* qb, char const* title) { + wmove(qb->outer_window, 0, 2); + wprintw(qb->outer_window, title); +} + +Qnav_block* qnav_push_message(int height, int width) { + Qnav_block* qb = malloc(sizeof(Qnav_block)); + qnav_stack_push(Qnav_type_message, height, width, qb); + qnav_draw_box(qb); + return qb; +} + +bool qnav_drive_message(Qnav_block* qb, int key) { + (void)qb; + switch (key) { + case ' ': + case 27: + case '\r': + case KEY_ENTER: + return true; + } + return false; +} + +void qmenu_start(Qmenu* qm) { memset(qm, 0, sizeof(Qmenu)); } +void qmenu_add_text_item(Qmenu* qm, char const* text, int id) { + ITEM* item = new_item(text, NULL); + set_item_userptr(item, (void*)(intptr_t)(id)); + qm->ncurses_items[qm->items_count] = item; + ++qm->items_count; +} +void qmenu_add_spacer(Qmenu* qm) { + ITEM* item = new_item(" ", NULL); + item_opts_off(item, O_SELECTABLE); + set_item_userptr(item, &qmenu_spacer_user_unique); + qm->ncurses_items[qm->items_count] = item; + ++qm->items_count; +} +void qmenu_push_to_nav(Qmenu* qm) { + qm->ncurses_menu = new_menu(qm->ncurses_items); + set_menu_mark(qm->ncurses_menu, " > "); + set_menu_grey(qm->ncurses_menu, A_DIM); + int menu_min_h, menu_min_w; + scale_menu(qm->ncurses_menu, &menu_min_h, &menu_min_w); + qnav_stack_push(Qnav_type_qmenu, menu_min_h, menu_min_w, &qm->nav_block); + set_menu_win(qm->ncurses_menu, qm->nav_block.outer_window); + set_menu_sub(qm->ncurses_menu, qm->nav_block.content_window); + post_menu(qm->ncurses_menu); +} + +void qmenu_free(Qmenu* qm) { + unpost_menu(qm->ncurses_menu); + free_menu(qm->ncurses_menu); + for (Usz i = 0; i < qm->items_count; ++i) { + free_item(qm->ncurses_items[i]); + } +} + +void qnav_free_block(Qnav_block* qb) { + switch (qb->tag) { + case Qnav_type_message: { + free(qb); + } break; + case Qnav_type_qmenu: { + Qmenu* qm = ORCA_CONTAINER_OF(qb, Qmenu, nav_block); + qmenu_free(qm); + } break; + } +} + +bool qmenu_drive(Qmenu* qm, int key, Qmenu_action* out_action) { + switch (key) { + case 27: { + out_action->any.type = Qmenu_action_type_canceled; + return true; + } + case ' ': + case '\r': + case KEY_ENTER: { + ITEM* cur = current_item(qm->ncurses_menu); + out_action->picked.type = Qmenu_action_type_picked; + out_action->picked.id = cur ? (int)(intptr_t)item_userptr(cur) : 0; + return true; + } break; + case KEY_UP: { + ITEM* starting = current_item(qm->ncurses_menu); + menu_driver(qm->ncurses_menu, REQ_UP_ITEM); + for (;;) { + ITEM* cur = current_item(qm->ncurses_menu); + if (!cur || cur == starting) + break; + if (item_userptr(cur) != &qmenu_spacer_user_unique) + break; + menu_driver(qm->ncurses_menu, REQ_UP_ITEM); + } + return false; + } + case KEY_DOWN: { + ITEM* starting = current_item(qm->ncurses_menu); + menu_driver(qm->ncurses_menu, REQ_DOWN_ITEM); + for (;;) { + ITEM* cur = current_item(qm->ncurses_menu); + if (!cur || cur == starting) + break; + if (item_userptr(cur) != &qmenu_spacer_user_unique) + break; + menu_driver(qm->ncurses_menu, REQ_DOWN_ITEM); + } + return false; + } + } + return false; +} + +Qmenu* qmenu_of(Qnav_block* qb) { + return ORCA_CONTAINER_OF(qb, Qmenu, nav_block); +} diff --git a/term_util.h b/term_util.h new file mode 100644 index 0000000..457d731 --- /dev/null +++ b/term_util.h @@ -0,0 +1,102 @@ +#pragma once +#include +#include + +#define CTRL_PLUS(c) ((c)&037) + +typedef enum { + C_natural, + C_black, + C_red, + C_green, + C_yellow, + C_blue, + C_magenta, + C_cyan, + C_white, +} Color_name; + +enum { + Colors_count = C_white + 1, +}; + +enum { + Cdef_normal = COLOR_PAIR(1), +}; + +typedef enum { + A_normal = A_NORMAL, + A_bold = A_BOLD, + A_dim = A_DIM, + A_standout = A_STANDOUT, + A_reverse = A_REVERSE, +} Term_attr; + +ORCA_FORCE_INLINE +int fg_bg(Color_name fg, Color_name bg) { + return COLOR_PAIR(1 + fg * Colors_count + bg); +} + +void term_util_init_colors(); + +typedef enum { + Qnav_type_message, + Qnav_type_qmenu, +} Qnav_type_tag; + +typedef struct { + Qnav_type_tag tag; + WINDOW* outer_window; + WINDOW* content_window; +} Qnav_block; + +typedef struct { + Qnav_block* blocks[16]; + Usz count; + bool stack_changed; +} Qnav_stack; + +typedef struct { + Qnav_block nav_block; + MENU* ncurses_menu; + ITEM* ncurses_items[32]; + Usz items_count; +} Qmenu; + +typedef enum { + Qmenu_action_type_canceled, + Qmenu_action_type_picked, +} Qmenu_action_type; + +typedef struct { + Qmenu_action_type type; +} Qmenu_action_any; + +typedef struct { + Qmenu_action_type type; + int id; +} Qmenu_action_picked; + +typedef union { + Qmenu_action_any any; + Qmenu_action_picked picked; +} Qmenu_action; + +void qnav_init(); +void qnav_deinit(); +void qnav_draw_box(Qnav_block* qb); +void qnav_draw_title(Qnav_block* qb, char const* title); +Qnav_block* qnav_top_block(); +void qnav_stack_pop(); + +Qnav_block* qnav_push_message(int height, int width); +bool qnav_drive_message(Qnav_block* qb, int key); + +void qmenu_start(Qmenu* qm); +void qmenu_add_text_item(Qmenu* qm, char const* text, int id); +void qmenu_add_spacer(Qmenu* qm); +void qmenu_push_to_nav(Qmenu* qm); +bool qmenu_drive(Qmenu* qm, int key, Qmenu_action* out_action); +Qmenu* qmenu_of(Qnav_block* qb); + +extern Qnav_stack qnav_stack; diff --git a/tool b/tool index e3a85b5..b74396f 100755 --- a/tool +++ b/tool @@ -230,7 +230,7 @@ build_target() { out_exe=cli ;; orca|tui) - add source_files osc_out.c tui_main.c + add source_files osc_out.c term_util.c tui_main.c add cc_flags -D_XOPEN_SOURCE_EXTENDED=1 # thirdparty headers (like sokol_time.h) should get -isystem for their # include dir so that any warnings they generate with our warning flags @@ -254,7 +254,7 @@ build_target() { add cc_flags -D_POSIX_C_SOURCE=200809L ;; esac - add libraries -lncurses + add libraries -lmenu -lncurses # If we wanted wide chars, use -lncursesw on Linux, and still just # -lncurses on Mac. ;; diff --git a/tui_main.c b/tui_main.c index 589a544..4c2777c 100644 --- a/tui_main.c +++ b/tui_main.c @@ -5,16 +5,14 @@ #include "mark.h" #include "osc_out.h" #include "sim.h" +#include "term_util.h" #include #include -#include #define SOKOL_IMPL #include "sokol_time.h" #undef SOKOL_IMPL -#define CTRL_PLUS(c) ((c)&037) - static void usage() { // clang-format off fprintf(stderr, @@ -42,39 +40,6 @@ static void usage() { // clang-format on } -typedef enum { - C_natural, - C_black, - C_red, - C_green, - C_yellow, - C_blue, - C_magenta, - C_cyan, - C_white, -} Color_name; - -enum { - Colors_count = C_white + 1, -}; - -enum { - Cdef_normal = COLOR_PAIR(1), -}; - -typedef enum { - A_normal = A_NORMAL, - A_bold = A_BOLD, - A_dim = A_DIM, - A_standout = A_STANDOUT, - A_reverse = A_REVERSE, -} Term_attr; - -ORCA_FORCE_INLINE -int fg_bg(Color_name fg, Color_name bg) { - return COLOR_PAIR(1 + fg * Colors_count + bg); -} - typedef enum { Glyph_class_unknown, Glyph_class_grid, @@ -989,7 +954,6 @@ void ged_set_window_size(Ged* a, int win_h, int win_w) { } void ged_draw(Ged* a, WINDOW* win) { - werase(win); // We can predictavely step the next simulation tick and then use the // resulting markmap buffer for better UI visualization. If we don't do // this, after loading a fresh file or after the user performs some edit @@ -1036,7 +1000,6 @@ void ged_draw(Ged* a, WINDOW* win) { draw_oevent_list(win, &a->oevent_list); } a->is_draw_dirty = false; - wrefresh(win); } void ged_adjust_bpm(Ged* a, Isz delta_bpm) { @@ -1471,6 +1434,34 @@ bool hacky_try_save(Field* field, char const* filename) { return true; } +// +// menu stuff +// + +enum { + Menu_id_quit = 1, + Menu_id_save, +}; + +struct { + Qmenu qmenu; +} g_main_menu; + +void push_main_menu() { + Qmenu* qm = &g_main_menu.qmenu; + qmenu_start(qm); + qmenu_add_text_item(qm, "Save As...", Menu_id_save); + qmenu_add_spacer(qm); + qmenu_add_text_item(qm, "Quit", Menu_id_quit); + qmenu_push_to_nav(qm); + qnav_draw_box(&qm->nav_block); + qnav_draw_title(&qm->nav_block, "ORCA"); +} + +// +// main +// + enum { Argopt_margins = UCHAR_MAX + 1, Argopt_osc_server, @@ -1538,6 +1529,7 @@ int main(int argc, char** argv) { return 1; } + qnav_init(); Ged ged_state; ged_init(&ged_state); @@ -1631,29 +1623,7 @@ int main(int argc, char** argv) { // hasn't typed anything. That way we can mix other timers in our code, // instead of being a slave only to terminal input. // nodelay(stdscr, TRUE); - - if (has_colors()) { - // Enable color - start_color(); - use_default_colors(); - for (int ifg = 0; ifg < Colors_count; ++ifg) { - for (int ibg = 0; ibg < Colors_count; ++ibg) { - int res = init_pair((short int)(1 + ifg * Colors_count + ibg), - (short int)(ifg - 1), (short int)(ibg - 1)); - (void)res; - // Might fail on Linux virtual console/terminal for a couple of colors. - // Just ignore. -#if 0 - if (res == ERR) { - endwin(); - fprintf(stderr, "Error initializing color pair: %d %d\n", ifg - 1, - ibg - 1); - exit(1); - } -#endif - } - } - } + term_util_init_colors(); mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, NULL); if (has_mouse()) { @@ -1661,7 +1631,8 @@ int main(int argc, char** argv) { mouseinterval(0); } - WINDOW* cont_win = NULL; + WINDOW* cont_window = NULL; + int key = KEY_RESIZE; wtimeout(stdscr, 0); U64 last_time = 0; @@ -1673,9 +1644,26 @@ int main(int argc, char** argv) { U64 diff = stm_laptime(&last_time); ged_apply_delta_secs(&ged_state, stm_sec(diff)); ged_do_stuff(&ged_state); - if (ged_is_draw_dirty(&ged_state)) { - ged_draw(&ged_state, cont_win); + bool drew_any = false; + if (qnav_stack.stack_changed) { + werase(stdscr); + drew_any = true; + qnav_stack.stack_changed = false; + } + if (ged_is_draw_dirty(&ged_state) || drew_any) { + werase(cont_window); + ged_draw(&ged_state, cont_window); + wnoutrefresh(cont_window); + drew_any = true; + } + for (Usz i = 0; i < qnav_stack.count; ++i) { + Qnav_block* qb = qnav_stack.blocks[i]; + touchwin(qb->outer_window); + wnoutrefresh(qb->outer_window); + drew_any = true; } + if (drew_any) + doupdate(); diff = stm_laptime(&last_time); ged_apply_delta_secs(&ged_state, stm_sec(diff)); double secs_to_d = ged_secs_to_deadline(&ged_state); @@ -1696,7 +1684,8 @@ int main(int argc, char** argv) { wtimeout(stdscr, new_timeout); cur_timeout = new_timeout; } - } break; + goto next_getch; + } case KEY_RESIZE: { int term_height = getmaxy(stdscr); int term_width = getmaxx(stdscr); @@ -1713,23 +1702,25 @@ int main(int argc, char** argv) { content_h -= margins_2; content_w -= margins_2; } - if (cont_win == NULL || getmaxy(cont_win) != content_h || - getmaxx(cont_win) != content_w) { - if (cont_win) { - delwin(cont_win); + if (cont_window == NULL || getmaxy(cont_window) != content_h || + getmaxx(cont_window) != content_w) { + if (cont_window) { + delwin(cont_window); } wclear(stdscr); - cont_win = derwin(stdscr, content_h, content_w, content_y, content_x); + cont_window = + derwin(stdscr, content_h, content_w, content_y, content_x); ged_set_window_size(&ged_state, content_h, content_w); } - } break; + goto next_getch; + } case KEY_MOUSE: { MEVENT mevent; - if (cont_win && getmouse(&mevent) == OK) { + if (cont_window && getmouse(&mevent) == OK) { int win_y, win_x; int win_h, win_w; - getbegyx(cont_win, win_y, win_x); - getmaxyx(cont_win, win_h, win_w); + getbegyx(cont_window, win_y, win_x); + getmaxyx(cont_window, win_h, win_w); int inwin_y = mevent.y - win_y; int inwin_x = mevent.x - win_x; if (inwin_y >= win_h) @@ -1742,9 +1733,49 @@ int main(int argc, char** argv) { inwin_x = 0; ged_mouse_event(&ged_state, (Usz)inwin_y, (Usz)inwin_x, mevent.bstate); } - } break; + goto next_getch; + } case CTRL_PLUS('q'): goto quit; + } + + Qnav_block* qb = qnav_top_block(); + if (qb) { + switch (qb->tag) { + case Qnav_type_message: { + if (qnav_drive_message(qb, key)) + qnav_stack_pop(); + } break; + case Qnav_type_qmenu: { + Qmenu* qm = qmenu_of(qb); + Qmenu_action act; + if (qmenu_drive(qm, key, &act)) { + switch (act.any.type) { + case Qmenu_action_type_canceled: { + qnav_stack_pop(); + } break; + case Qmenu_action_type_picked: { + if (qm == &g_main_menu.qmenu) { + switch (act.picked.id) { + case Menu_id_quit: + goto quit; + case Menu_id_save: { + Qnav_block* msg = qnav_push_message(3, 30); + WINDOW* msgw = msg->content_window; + wmove(msgw, 0, 1); + wprintw(msgw, "Not yet implemented from this menu"); + } break; + } + } + } break; + } + } + } break; + } + goto next_getch; + } + + switch (key) { case KEY_UP: case CTRL_PLUS('k'): ged_dir_input(&ged_state, Ged_dir_up); @@ -1851,6 +1882,15 @@ int main(int argc, char** argv) { ged_input_character(&ged_state, '.'); break; + case CTRL_PLUS('d'): + case KEY_F(1): { + if (qnav_top_block()) { + qnav_stack_pop(); + } else { + push_main_menu(); + } + } break; + case KEY_F(2): { if (!ged_state.filename) break; @@ -1894,6 +1934,7 @@ int main(int argc, char** argv) { #endif break; } + next_getch: key = wgetch(stdscr); if (cur_timeout != 0) { wtimeout(stdscr, 0); @@ -1902,8 +1943,9 @@ int main(int argc, char** argv) { } quit: ged_stop_all_sustained_notes(&ged_state); - if (cont_win) { - delwin(cont_win); + qnav_deinit(); + if (cont_window) { + delwin(cont_window); } endwin(); ged_deinit(&ged_state);