menu: highlight (bold+uline) section shortcuts

This commit is contained in:
nick black 2020-02-03 21:32:04 -05:00
parent 4c8adb0072
commit 3821b66bc7
No known key found for this signature in database
GPG Key ID: 5F43400C21CBFACC
5 changed files with 78 additions and 32 deletions

View File

@ -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 {

View File

@ -22,6 +22,7 @@
#include <stdarg.h>
#include <string.h>
#include <signal.h>
#include <wctype.h>
#include <stdbool.h>
#include <pthread.h>
#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);

View File

@ -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;

View File

@ -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);

View File

@ -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;