From 3821b66bc707188c463283b31ef95b56e9c421b6 Mon Sep 17 00:00:00 2001 From: nick black Date: Mon, 3 Feb 2020 21:32:04 -0500 Subject: [PATCH] menu: highlight (bold+uline) section shortcuts --- include/notcurses.h | 4 +--- src/lib/internal.h | 39 ++++++++++++++++++++++++++++++++++++++- src/lib/menu.c | 37 ++++++++++++++++++++++++++++--------- src/poc/menu.c | 6 +++--- tests/menu.cpp | 24 ++++++++---------------- 5 files changed, 78 insertions(+), 32 deletions(-) diff --git a/include/notcurses.h b/include/notcurses.h index d7d45cb6a..7721e387c 100644 --- a/include/notcurses.h +++ b/include/notcurses.h @@ -2219,9 +2219,7 @@ struct ncmenu_section { char* name; // utf-8 c string int itemcount; struct ncmenu_item* items; - int xoff; // used only in library copy, ignored in request - int bodycols; // used only in library copy, ignored in request - int itemselected; // used only in library copy, ignored in request + ncinput shortcut; // shortcut, will be underlined if present in name }; typedef struct ncmenu_options { diff --git a/src/lib/internal.h b/src/lib/internal.h index e62e37002..de0004060 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include "notcurses.h" @@ -123,9 +124,20 @@ typedef struct renderstate { bool defaultelidable; } renderstate; +typedef struct ncmenu_int_section { + char* name; // utf-8 c string + int itemcount; + struct ncmenu_item* items; + ncinput shortcut; // shortcut, will be underlined if present in name + int xoff; // column offset from beginning of menu bar + int bodycols; // column width of longest item + int itemselected; // current item selected, -1 for no selection + int shortcut_offset; // column offset within name of shortcut EGC +} ncmenu_int_section; + typedef struct ncmenu { ncplane* ncp; - struct ncmenu_section* sections; + ncmenu_int_section* sections; bool bottom; // are we on the bottom (vs top)? int sectioncount; // must be positive int unrolledsection; // currently unrolled section, -1 if none @@ -259,6 +271,31 @@ typedef struct notcurses { void sigwinch_handler(int signo); +// Search the provided multibyte (UTF8) string 's' for the provided unicode +// codepoint 'cp'. If found, return the column offset of the EGC in which the +// codepoint appears in 'col', and the byte offset as the return value. If not +// found, -1 is returned, and 'col' is meaningless. +static inline int +mbstr_find_codepoint(const char* s, char32_t cp, int* col){ + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + size_t bytes = 0; + size_t r; + wchar_t w; + *col = 0; + while((r = mbrtowc(&w, s + bytes, MB_CUR_MAX, &ps)) != (size_t)-1 && r != (size_t)-2){ + if(r == 0){ + break; + } + if(towlower(cp) == (char32_t)towlower(w)){ + return bytes; + } + *col += wcwidth(w); + bytes += r; + } + return -1; +} + // load all known special keys from terminfo, and build the input sequence trie int prep_special_keys(notcurses* nc); diff --git a/src/lib/menu.c b/src/lib/menu.c index a21b72ee8..647338659 100644 --- a/src/lib/menu.c +++ b/src/lib/menu.c @@ -1,7 +1,7 @@ #include "internal.h" static void -free_menu_section(struct ncmenu_section* ms){ +free_menu_section(ncmenu_int_section* ms){ for(int i = 0 ; i < ms->itemcount ; ++i){ free(ms->items[i].desc); } @@ -18,7 +18,7 @@ free_menu_sections(ncmenu* ncm){ } static int -dup_menu_section(struct ncmenu_section* dst, const struct ncmenu_section* src){ +dup_menu_section(ncmenu_int_section* dst, const struct ncmenu_section* src){ // we must reject any empty section if(src->itemcount == 0 || src->items == NULL){ return -1; @@ -70,8 +70,10 @@ dup_menu_sections(ncmenu* ncm, const ncmenu_options* opts, int* totalwidth, int* } int maxheight = 0; int maxwidth = *totalwidth; + int xoff = 2; for(int i = 0 ; i < opts->sectioncount ; ++i){ int cols = mbswidth(opts->sections[i].name); + ncm->sections[i].xoff = xoff; if(cols < 0 || (ncm->sections[i].name = strdup(opts->sections[i].name)) == NULL){ while(i--){ free_menu_section(&ncm->sections[i]); @@ -95,6 +97,13 @@ dup_menu_sections(ncmenu* ncm, const ncmenu_options* opts, int* totalwidth, int* maxwidth = *totalwidth + ncm->sections[i].bodycols + 2; } *totalwidth += cols + 2; + memcpy(&ncm->sections[i].shortcut, &opts->sections[i].shortcut, sizeof(ncm->sections[i].shortcut)); + if(mbstr_find_codepoint(ncm->sections[i].name, + ncm->sections[i].shortcut.id, + &ncm->sections[i].shortcut_offset) < 0){ + ncm->sections[i].shortcut_offset = -1; + } + xoff += cols + 2; } *totalwidth = maxwidth; *totalheight += maxheight + 2; // two rows of border @@ -119,8 +128,7 @@ write_header(ncmenu* ncm){ ncm->ncp->channels = ncm->headerchannels; return -1; } for(int i = 0 ; i < ncm->sectioncount ; ++i){ - ncm->sections[i].xoff = xoff; - if(ncplane_putstr(ncm->ncp, ncm->sections[i].name) < 0){ + if(ncplane_putstr_yx(ncm->ncp, ypos, xoff, ncm->sections[i].name) < 0){ return -1; } if(ncplane_putc(ncm->ncp, &c) < 0){ @@ -129,12 +137,24 @@ write_header(ncmenu* ncm){ ncm->ncp->channels = ncm->headerchannels; if(ncplane_putc(ncm->ncp, &c) < 0){ return -1; } + if(ncm->sections[i].shortcut_offset >= 0){ + cell cl = CELL_TRIVIAL_INITIALIZER; + if(ncplane_at_yx(ncm->ncp, ypos, xoff + ncm->sections[i].shortcut_offset, &cl) < 0){ + return -1; + } + cell_styles_on(&cl, NCSTYLE_UNDERLINE|NCSTYLE_BOLD); + if(ncplane_putc_yx(ncm->ncp, ypos, xoff + ncm->sections[i].shortcut_offset, &cl) < 0){ + return -1; + } + cell_release(ncm->ncp, &cl); + } xoff += mbswidth(ncm->sections[i].name) + 2; } - while(xoff++ < dimx){ - if(ncplane_putc(ncm->ncp, &c) < 0){ + while(xoff < dimx){ + if(ncplane_putc_yx(ncm->ncp, ypos, xoff, &c) < 0){ return -1; } + ++xoff; } return 0; } @@ -144,7 +164,7 @@ ncmenu* ncmenu_create(notcurses* nc, const ncmenu_options* opts){ return NULL; } int totalheight = 1; - int totalwidth = 1; // start with one character margin on the left + int totalwidth = 2; // start with two-character margin on the left ncmenu* ret = malloc(sizeof(*ret)); ret->sectioncount = opts->sectioncount; ret->sections = NULL; @@ -205,7 +225,7 @@ int ncmenu_unroll(ncmenu* n, int sectionidx){ if(ncplane_rounded_box_sized(n->ncp, 0, n->headerchannels, height, width, 0)){ return -1; } - const struct ncmenu_section* sec = &n->sections[sectionidx]; + const ncmenu_int_section* sec = &n->sections[sectionidx]; for(int i = 0 ; i < sec->itemcount ; ++i){ ++ypos; if(sec->items[i].desc){ @@ -282,7 +302,6 @@ int ncmenu_nextitem(ncmenu* n){ return -1; } } - // FIXME can't allow any section to be all NULLs or we'll infintitely loop do{ if(++n->sections[n->unrolledsection].itemselected == n->sections[n->unrolledsection].itemcount){ n->sections[n->unrolledsection].itemselected = 0; diff --git a/src/poc/menu.c b/src/poc/menu.c index 251da0a11..48d0ccd1f 100644 --- a/src/poc/menu.c +++ b/src/poc/menu.c @@ -67,15 +67,15 @@ int main(void){ { .desc = "Restart", }, }; struct ncmenu_item file_items[] = { - { .desc = "New", }, + { .desc = "New", .shortcut = { .id = 'n', .ctrl = true, }, }, { .desc = "Open", }, { .desc = "Close", }, { .desc = NULL, }, { .desc = "Quit", }, }; struct ncmenu_section sections[] = { - { .name = "Demo", .items = demo_items, }, - { .name = "File", .items = file_items, }, + { .name = "Demo", .items = demo_items, .shortcut = { .id = 'd', .alt = true, }, }, + { .name = "File", .items = file_items, .shortcut = { .id = 'f', .alt = true, }, }, }; sections[0].itemcount = sizeof(demo_items) / sizeof(*demo_items); sections[1].itemcount = sizeof(file_items) / sizeof(*file_items); diff --git a/tests/menu.cpp b/tests/menu.cpp index 70acdbbf6..4c6c767ee 100644 --- a/tests/menu.cpp +++ b/tests/menu.cpp @@ -39,8 +39,7 @@ TEST_CASE("MenuTest") { SUBCASE("EmptySectionReject") { struct ncmenu_options opts{}; struct ncmenu_section sections[] = { - { .name = strdup("Empty"), .itemcount = 0, .items = nullptr, - .xoff = -1, .bodycols = -1, .itemselected = -1, }, + { .name = strdup("Empty"), .itemcount = 0, .items = nullptr, .shortcut{}, }, }; opts.sections = sections; opts.sectioncount = sizeof(sections) / sizeof(*sections); @@ -56,8 +55,7 @@ TEST_CASE("MenuTest") { { .desc = nullptr, .shortcut = {}, }, }; struct ncmenu_section sections[] = { - { .name = strdup("Empty"), .itemcount = 1, .items = empty_items, - .xoff = -1, .bodycols = -1, .itemselected = -1, }, + { .name = strdup("Empty"), .itemcount = 1, .items = empty_items, .shortcut{}, }, }; struct ncmenu_options opts{}; opts.sections = sections; @@ -73,8 +71,7 @@ TEST_CASE("MenuTest") { { .desc = strdup("I would like a new file"), .shortcut = {}, }, }; struct ncmenu_section sections[] = { - { .name = strdup("File"), .itemcount = sizeof(file_items) / sizeof(*file_items), .items = file_items, - .xoff = -1, .bodycols = -1, .itemselected = -1, }, + { .name = strdup("File"), .itemcount = sizeof(file_items) / sizeof(*file_items), .items = file_items, .shortcut{}, }, }; struct ncmenu_options opts{}; opts.sections = sections; @@ -90,16 +87,11 @@ TEST_CASE("MenuTest") { { .desc = strdup("Generic menu entry"), .shortcut = {}, }, }; struct ncmenu_section sections[] = { - { .name = strdup("antidisestablishmentarianism"), .itemcount = sizeof(items) / sizeof(*items), .items = items, - .xoff = -1, .bodycols = -1, .itemselected = -1, }, - { .name = strdup("floccinaucinihilipilification"), .itemcount = sizeof(items) / sizeof(*items), .items = items, - .xoff = -1, .bodycols = -1, .itemselected = -1, }, - { .name = strdup("pneumonoultramicroscopicsilicovolcanoconiosis"), .itemcount = sizeof(items) / sizeof(*items), .items = items, - .xoff = -1, .bodycols = -1, .itemselected = -1, }, - { .name = strdup("supercalifragilisticexpialidocious"), .itemcount = sizeof(items) / sizeof(*items), .items = items, - .xoff = -1, .bodycols = -1, .itemselected = -1, }, - { .name = strdup("Incomprehensibilities"), .itemcount = sizeof(items) / sizeof(*items), .items = items, - .xoff = -1, .bodycols = -1, .itemselected = -1, }, + { .name = strdup("antidisestablishmentarianism"), .itemcount = sizeof(items) / sizeof(*items), .items = items, .shortcut{}, }, + { .name = strdup("floccinaucinihilipilification"), .itemcount = sizeof(items) / sizeof(*items), .items = items, .shortcut{}, }, + { .name = strdup("pneumonoultramicroscopicsilicovolcanoconiosis"), .itemcount = sizeof(items) / sizeof(*items), .items = items, .shortcut{}, }, + { .name = strdup("supercalifragilisticexpialidocious"), .itemcount = sizeof(items) / sizeof(*items), .items = items, .shortcut{}, }, + { .name = strdup("Incomprehensibilities"), .itemcount = sizeof(items) / sizeof(*items), .items = items, .shortcut{}, }, }; struct ncmenu_options opts{}; opts.sections = sections;