diff --git a/term_util.c b/term_util.c index 4cc282c..ee49034 100644 --- a/term_util.c +++ b/term_util.c @@ -2,7 +2,8 @@ #include "oso.h" #include #include -#include + +ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm); void term_util_init_colors() { if (has_colors()) { @@ -38,20 +39,18 @@ struct Qmsg { Qmsg_dismiss_mode dismiss_mode; }; -struct Qmenu_item_extra { - int user_id; +typedef struct Qmenu_item { + char const *text; + int id; U8 owns_string : 1, is_spacer : 1; -}; +} Qmenu_item; struct Qmenu { Qblock qblock; - MENU *ncurses_menu; - ITEM **ncurses_items; + Qmenu_item *items; Usz items_count, items_cap; - ITEM *initial_item; - int id; - // Flag for right-padding hack. Temp until we do our own menus - U8 has_submenu_item : 1; + int current_item, id; + U8 needs_reprint : 1, is_frontmost : 1; }; struct Qform { @@ -175,18 +174,26 @@ bool qnav_draw(void) { getmaxyx(stdscr, term_h, term_w); for (Usz i = 0; i < qnav_stack.count; ++i) { Qblock *qb = qnav_stack.blocks[i]; - if (qnav_stack.occlusion_dirty) { - bool is_frontmost = i == qnav_stack.count - 1; + bool is_frontmost = i == qnav_stack.count - 1; + if (qnav_stack.occlusion_dirty) qblock_print_frame(qb, is_frontmost); - switch (qb->tag) { - case Qblock_type_qmsg: - break; - case Qblock_type_qmenu: - qmenu_set_displayed_active(qmenu_of(qb), is_frontmost); - break; - case Qblock_type_qform: - break; + switch (qb->tag) { + case Qblock_type_qmsg: + break; + case Qblock_type_qmenu: { + Qmenu *qm = qmenu_of(qb); + if (qm->is_frontmost != is_frontmost) { + qm->is_frontmost = is_frontmost; + qm->needs_reprint = 1; + } + if (qm->needs_reprint) { + qmenu_reprint(qm); + qm->needs_reprint = 0; } + break; + } + case Qblock_type_qform: + break; } touchwin(qb->outer_window); // here? or after continue? if (term_h < 1 || term_w < 1) @@ -222,7 +229,7 @@ void qblock_print_title(Qblock *qb, char const *title, int attr) { wattr_get(qb->outer_window, &attrs, &pair, NULL); wattrset(qb->outer_window, attr); waddch(qb->outer_window, ' '); - wprintw(qb->outer_window, title); + waddstr(qb->outer_window, title); waddch(qb->outer_window, ' '); wattr_set(qb->outer_window, attrs, pair, NULL); } @@ -343,91 +350,65 @@ Qmsg *qmsg_of(Qblock *qb) { return ORCA_CONTAINER_OF(qb, Qmsg, qblock); } Qmenu *qmenu_create(int id) { Qmenu *qm = (Qmenu *)malloc(sizeof(Qmenu)); qblock_init(&qm->qblock, Qblock_type_qmenu); - qm->ncurses_menu = NULL; - qm->ncurses_items = NULL; + qm->items = NULL; qm->items_count = 0; qm->items_cap = 0; - qm->initial_item = NULL; + qm->current_item = 0; qm->id = id; - qm->has_submenu_item = 0; + qm->needs_reprint = 1; + qm->is_frontmost = 0; return qm; } void qmenu_destroy(Qmenu *qm) { qmenu_free(qm); } int qmenu_id(Qmenu const *qm) { return qm->id; } -static ORCA_NOINLINE void -qmenu_allocitems(Qmenu *qm, Usz count, Usz *out_idx, ITEM ***out_items, - struct Qmenu_item_extra **out_extras) { +static ORCA_NOINLINE Qmenu_item *qmenu_allocitems(Qmenu *qm, Usz count) { Usz old_count = qm->items_count; - // Add 1 for the extra null terminator guy - Usz new_count = old_count + count + 1; + if (old_count > SIZE_MAX - count) // overflow + exit(1); + Usz new_count = old_count + count; Usz items_cap = qm->items_cap; - ITEM **items = qm->ncurses_items; + Qmenu_item *items = qm->items; if (new_count > items_cap) { // todo overflow check, realloc fail check - Usz old_cap = items_cap; Usz new_cap = new_count < 32 ? 32 : orca_round_up_power2(new_count); - Usz new_size = new_cap * (sizeof(ITEM *) + sizeof(struct Qmenu_item_extra)); - ITEM **new_items = (ITEM **)realloc(items, new_size); + Usz new_size = new_cap * sizeof(Qmenu_item); + Qmenu_item *new_items = (Qmenu_item *)realloc(items, new_size); if (!new_items) exit(1); items = new_items; items_cap = new_cap; - // Move old extras data to new position - Usz old_extras_offset = sizeof(ITEM *) * old_cap; - Usz new_extras_offset = sizeof(ITEM *) * new_cap; - Usz old_extras_size = sizeof(struct Qmenu_item_extra) * old_count; - memmove((char *)items + new_extras_offset, - (char *)items + old_extras_offset, old_extras_size); - qm->ncurses_items = new_items; + qm->items = new_items; qm->items_cap = new_cap; } - // Not using new_count here in order to leave an extra 1 for the null - // terminator as required by ncurses. - qm->items_count = old_count + count; - Usz extras_offset = sizeof(ITEM *) * items_cap; - *out_idx = old_count; - *out_items = items + old_count; - *out_extras = - (struct Qmenu_item_extra *)((char *)items + extras_offset) + old_count; -} -ORCA_FORCEINLINE static struct Qmenu_item_extra * -qmenu_item_extras_ptr(Qmenu *qm) { - Usz offset = sizeof(ITEM *) * qm->items_cap; - return (struct Qmenu_item_extra *)((char *)qm->ncurses_items + offset); -} -// Get the curses menu item user pointer out, turn it to an int, and use it as -// an index into the 'extras' arrays. -ORCA_FORCEINLINE static struct Qmenu_item_extra * -qmenu_itemextra(struct Qmenu_item_extra *extras, ITEM *item) { - return extras + (int)(intptr_t)(item_userptr(item)); + qm->items_count = new_count; + return items + old_count; +} +ORCA_NOINLINE static void qmenu_reprint(Qmenu *qm) { + WINDOW *win = qm->qblock.content_window; + Qmenu_item *items = qm->items; + bool isfront = qm->is_frontmost; + werase(win); + for (Usz i = 0, n = qm->items_count; i < n; ++i) { + bool iscur = items[i].id == qm->current_item; + wattrset(win, isfront ? iscur ? A_BOLD : A_NORMAL : A_DIM); + wmove(win, (int)i, iscur ? 1 : 3); + if (iscur) + waddstr(win, "> "); + waddstr(win, items[i].text); + } } void qmenu_set_title(Qmenu *qm, char const *title) { qblock_set_title(&qm->qblock, title); } void qmenu_add_choice(Qmenu *qm, int id, char const *text) { assert(id != 0); - Usz idx; - ITEM **items; - struct Qmenu_item_extra *extras; - qmenu_allocitems(qm, 1, &idx, &items, &extras); - items[0] = new_item(text, NULL); - set_item_userptr(items[0], (void *)(uintptr_t)idx); - extras[0].user_id = id; - extras[0].owns_string = false; - extras[0].is_spacer = false; -} -void qmenu_add_submenu(Qmenu *qm, int id, char const *text) { - assert(id != 0); - qm->has_submenu_item = true; // don't add +1 right padding to subwindow - Usz idx; - ITEM **items; - struct Qmenu_item_extra *extras; - qmenu_allocitems(qm, 1, &idx, &items, &extras); - items[0] = new_item(text, ">"); - set_item_userptr(items[0], (void *)(uintptr_t)idx); - extras[0].user_id = id; - extras[0].owns_string = false; - extras[0].is_spacer = false; + Qmenu_item *item = qmenu_allocitems(qm, 1); + item->text = text; + item->id = id; + item->owns_string = false; + item->is_spacer = false; + if (!qm->current_item) + qm->current_item = id; } void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) { va_list ap; @@ -442,85 +423,42 @@ void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) { va_end(ap); if (printedsize != textsize) exit(1); // todo better handling? - Usz idx; - ITEM **items; - struct Qmenu_item_extra *extras; - qmenu_allocitems(qm, 1, &idx, &items, &extras); - items[0] = new_item(buffer, NULL); - set_item_userptr(items[0], (void *)(uintptr_t)idx); - extras[0].user_id = id; - extras[0].owns_string = true; - extras[0].is_spacer = false; + Qmenu_item *item = qmenu_allocitems(qm, 1); + item->text = buffer; + item->id = id; + item->owns_string = true; + item->is_spacer = false; + if (!qm->current_item) + qm->current_item = id; } void qmenu_add_spacer(Qmenu *qm) { - Usz idx; - ITEM **items; - struct Qmenu_item_extra *extras; - qmenu_allocitems(qm, 1, &idx, &items, &extras); - items[0] = new_item(" ", NULL); - item_opts_off(items[0], O_SELECTABLE); - set_item_userptr(items[0], (void *)(uintptr_t)idx); - extras[0].user_id = 0; - extras[0].owns_string = false; - extras[0].is_spacer = true; + Qmenu_item *item = qmenu_allocitems(qm, 1); + item->text = " "; + item->id = 0; + item->owns_string = false; + item->is_spacer = true; } void qmenu_set_current_item(Qmenu *qm, int id) { - ITEM **items = qm->ncurses_items; - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); - ITEM *found = NULL; - for (Usz i = 0, n = qm->items_count; i < n; i++) { - ITEM *item = items[i]; - if (qmenu_itemextra(extras, item)->user_id == id) { - found = item; - break; - } - } - if (!found) + if (qm->current_item == id) return; - if (qm->ncurses_menu) { - set_current_item(qm->ncurses_menu, found); - } else { - qm->initial_item = found; - } -} -int qmenu_current_item(Qmenu *qm) { - ITEM *item = NULL; - if (qm->ncurses_menu) - item = current_item(qm->ncurses_menu); - if (!item) - item = qm->initial_item; - if (!item) - return 0; - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); - return qmenu_itemextra(extras, item)->user_id; -} -void qmenu_set_displayed_active(Qmenu *qm, bool active) { - // Could add a flag in the Qmenu to avoid redundantly changing this stuff. - set_menu_fore(qm->ncurses_menu, active ? A_BOLD : A_DIM); - set_menu_back(qm->ncurses_menu, active ? A_NORMAL : A_DIM); - set_menu_grey(qm->ncurses_menu, active ? A_DIM : A_DIM); + qm->current_item = id; + qm->needs_reprint = 1; } +int qmenu_current_item(Qmenu *qm) { return qm->current_item; } void qmenu_push_to_nav(Qmenu *qm) { - // new_menu() will get angry if there are no items in the menu. We'll get a - // null pointer back, and our code will get angry. Instead, just add an empty - // spacer item. This will probably only ever occur as a programming error, - // but we should try to avoid having to deal with qmenu_push_to_nav() - // returning a non-ignorable error for now. + // Probably a programming error if there are no items. Make the menu visible + // so the programmer knows something went wrong. if (qm->items_count == 0) qmenu_add_spacer(qm); - // Allocating items always leaves an extra available item at the end. This is - // so we can assign a NULL to it here, since ncurses requires the array to be - // null terminated instead of using a count. - qm->ncurses_items[qm->items_count] = NULL; - qm->ncurses_menu = new_menu(qm->ncurses_items); - set_menu_mark(qm->ncurses_menu, " > "); - set_menu_fore(qm->ncurses_menu, A_BOLD); - set_menu_grey(qm->ncurses_menu, A_DIM); - set_menu_format(qm->ncurses_menu, 30, 1); // temp to allow large Y - int menu_min_h, menu_min_w; - scale_menu(qm->ncurses_menu, &menu_min_h, &menu_min_w); - if (!qm->has_submenu_item) - menu_min_w += 1; // temp hack + Usz n = qm->items_count; + Qmenu_item *items = qm->items; + int menu_min_h = (int)n, menu_min_w = 0; + for (Usz i = 0; i < n; ++i) { + int item_w = (int)strlen(items[i].text); + if (item_w > menu_min_w) + menu_min_w = item_w; + } + menu_min_w += 3 + 1; // left " > " plus 1 empty space on right if (qm->qblock.title) { // Stupid lack of wcswidth() means we can't know how wide this string is // actually displayed. Just fake it for now, until we have Unicode strings @@ -529,58 +467,50 @@ void qmenu_push_to_nav(Qmenu *qm) { if (title_w > menu_min_w) menu_min_w = title_w; } - if (qm->initial_item) - set_current_item(qm->ncurses_menu, qm->initial_item); qnav_stack_push(&qm->qblock, menu_min_h, menu_min_w); - set_menu_win(qm->ncurses_menu, qm->qblock.outer_window); - set_menu_sub(qm->ncurses_menu, qm->qblock.content_window); - // TODO use this to set how "big" the menu is, visually, for scrolling. - // (ncurses can't figure that out on its own, aparently...) - // We'll need to split apart some work chunks so that we calculate the size - // beforehand. - // set_menu_format(qm->ncurses_menu, 5, 1); - post_menu(qm->ncurses_menu); } void qmenu_free(Qmenu *qm) { - unpost_menu(qm->ncurses_menu); - free_menu(qm->ncurses_menu); - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); - for (Usz i = 0; i < qm->items_count; ++i) { - ITEM *item = qm->ncurses_items[i]; - struct Qmenu_item_extra *extra = qmenu_itemextra(extras, item); - char const *freed_str = NULL; - if (extra->owns_string) - freed_str = item_name(item); - free_item(qm->ncurses_items[i]); - if (freed_str) - free((void *)freed_str); + Qmenu_item *items = qm->items; + for (Usz i = 0, n = qm->items_count; i < n; ++i) { + if (items[i].owns_string) + free((void *)items[i].text); } - free(qm->ncurses_items); + free(qm->items); free(qm); } -ORCA_NOINLINE -static void qmenu_drive_upordown(Qmenu *qm, int req_up_or_down) { - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); - ITEM *starting = current_item(qm->ncurses_menu); - menu_driver(qm->ncurses_menu, req_up_or_down); - ITEM *cur = current_item(qm->ncurses_menu); +ORCA_NOINLINE static void qmenu_drive_upordown(Qmenu *qm, bool downwards) { + Qmenu_item *items = qm->items; + Usz n = qm->items_count; + if (n <= 1) + return; + int cur_id = qm->current_item; + Usz starting = 0; + for (; starting < n; ++starting) { + if (items[starting].id == cur_id) + goto found; + } + return; +found:; + Usz current = starting; for (;;) { - if (!cur || cur == starting) - break; - if (!qmenu_itemextra(extras, cur)->is_spacer) + if (downwards && current < n - 1) + current++; + else if (!downwards && current > 0) + current--; + if (current == starting) break; - ITEM *prev = cur; - menu_driver(qm->ncurses_menu, req_up_or_down); - cur = current_item(qm->ncurses_menu); - if (cur == prev) + if (!items[current].is_spacer) break; } + if (current != starting) { + qm->current_item = items[current].id; + qm->needs_reprint = 1; + } } bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) { - struct Qmenu_item_extra *extras = qmenu_item_extras_ptr(qm); switch (key) { case 27: { out_action->any.type = Qmenu_action_type_canceled; @@ -588,17 +518,15 @@ bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action) { } case ' ': case '\r': - case KEY_ENTER: { - ITEM *cur = current_item(qm->ncurses_menu); + case KEY_ENTER: out_action->picked.type = Qmenu_action_type_picked; - out_action->picked.id = cur ? qmenu_itemextra(extras, cur)->user_id : 0; + out_action->picked.id = qm->current_item; return true; - } case KEY_UP: - qmenu_drive_upordown(qm, REQ_UP_ITEM); + qmenu_drive_upordown(qm, false); return false; case KEY_DOWN: - qmenu_drive_upordown(qm, REQ_DOWN_ITEM); + qmenu_drive_upordown(qm, true); return false; } return false; diff --git a/term_util.h b/term_util.h index fdcc3e0..3f0d3dd 100644 --- a/term_util.h +++ b/term_util.h @@ -141,12 +141,10 @@ void qmenu_destroy(Qmenu *qm); int qmenu_id(Qmenu const *qm); void qmenu_set_title(Qmenu *qm, char const *title); void qmenu_add_choice(Qmenu *qm, int id, char const *text); -void qmenu_add_submenu(Qmenu *qm, int id, char const *text); void qmenu_add_printf(Qmenu *qm, int id, char const *fmt, ...) ORCA_TERM_UTIL_PRINTF(3, 4); void qmenu_add_spacer(Qmenu *qm); void qmenu_set_current_item(Qmenu *qm, int id); -void qmenu_set_displayed_active(Qmenu *qm, bool active); void qmenu_push_to_nav(Qmenu *qm); int qmenu_current_item(Qmenu *qm); bool qmenu_drive(Qmenu *qm, int key, Qmenu_action *out_action); diff --git a/tool b/tool index 48f43d5..877cd43 100755 --- a/tool +++ b/tool @@ -409,7 +409,7 @@ build_target() { add cc_flags -D_POSIX_C_SOURCE=200809L ;; esac - add libraries -lmenuw -lformw -lncursesw + add libraries -lformw -lncursesw if [[ $portmidi_enabled = 1 ]]; then add libraries -lportmidi add cc_flags -DFEAT_PORTMIDI