From 078feca8e6807b33d38d938dd5682eac6c5ce6ca Mon Sep 17 00:00:00 2001 From: nick black Date: Mon, 9 Mar 2020 01:20:29 -0400 Subject: [PATCH] ncmultiselector links up --- src/lib/internal.h | 24 ++++ src/lib/selector.c | 347 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 370 insertions(+), 1 deletion(-) diff --git a/src/lib/internal.h b/src/lib/internal.h index 3aea9c2a7..1982a06dc 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -182,6 +182,30 @@ typedef struct ncselector { int uarrowy, darrowy, arrowx;// location of scrollarrows, even if not present } ncselector; +typedef struct ncmultiselector { + ncplane* ncp; // backing ncplane + unsigned current; // index of highlighted item + unsigned startdisp; // index of first option displayed + unsigned maxdisplay; // max number of items to display, 0 -> no limit + int longop; // columns occupied by longest option + int longdesc; // columns occupied by longest description + struct selector_item* items; // list of items and descriptions, heap-copied + unsigned itemcount; // number of pairs in 'items' + char* title; // can be NULL, in which case there's no riser + int titlecols; // columns occupied by title + char* secondary; // can be NULL + int secondarycols; // columns occupied by secondary + char* footer; // can be NULL + int footercols; // columns occupied by footer + cell background; // background, used in body only + uint64_t opchannels; // option channels + uint64_t descchannels; // description channels + uint64_t titlechannels; // title channels + uint64_t footchannels; // secondary and footer channels + uint64_t boxchannels; // border channels + int uarrowy, darrowy, arrowx;// location of scrollarrows, even if not present +} ncmultiselector; + typedef struct ncdirect { int attrword; // current styles int colors; // number of colors terminfo reported usable for this screen diff --git a/src/lib/selector.c b/src/lib/selector.c index 1edb7f61c..349cd5ff0 100644 --- a/src/lib/selector.c +++ b/src/lib/selector.c @@ -354,7 +354,7 @@ bool ncselector_offer_input(ncselector* n, const ncinput* nc){ // FIXME verify that we're within the body walls! // FIXME verify we're on the left of the split? // FIXME verify that we're on a visible glyph? - int cury = (n->selected + n->itemcount - n->startdisp) % n->itemcount; + int cury = (n->selected + n->itemcount - n->startdisp) % n->itemcount; int click = y - n->uarrowy - 1; while(click > cury){ ncselector_nextitem(n); @@ -389,3 +389,348 @@ void ncselector_destroy(ncselector* n, char** item){ free(n); } } + +// ideal body width given the ncselector's items and secondary/footer +static int +ncmultiselector_body_width(const ncmultiselector* n){ + int cols = 0; + // the body is the maximum of + // * longop + longdesc + 5 + // * secondary + 2 + // * footer + 2 + if(n->footercols + 2 > cols){ + cols = n->footercols + 2; + } + if(n->secondarycols + 2 > cols){ + cols = n->secondarycols + 2; + } + if(n->longop + n->longdesc + 5 > cols){ + cols = n->longop + n->longdesc + 5; + } + return cols; +} + +// redraw the multiselector widget in its entirety +static int +ncmultiselector_draw(ncmultiselector* n){ + ncplane_erase(n->ncp); + // if we have a title, we'll draw a riser. the riser is two rows tall, and + // exactly four columns longer than the title, and aligned to the right. we + // draw a rounded box. the body will blow part or all of the bottom away. + int yoff = 0; + if(n->title){ + size_t riserwidth = n->titlecols + 4; + int offx = ncplane_align(n->ncp, NCALIGN_RIGHT, riserwidth); + ncplane_cursor_move_yx(n->ncp, 0, offx); + ncplane_rounded_box_sized(n->ncp, 0, n->boxchannels, 3, riserwidth, 0); + n->ncp->channels = n->titlechannels; + ncplane_printf_yx(n->ncp, 1, offx + 1, " %s ", n->title); + yoff += 2; + } + int bodywidth = ncmultiselector_body_width(n); + int xoff = ncplane_align(n->ncp, NCALIGN_RIGHT, bodywidth); + ncplane_cursor_move_yx(n->ncp, yoff, xoff); + int dimy, dimx; + ncplane_dim_yx(n->ncp, &dimy, &dimx); + ncplane_rounded_box_sized(n->ncp, 0, n->boxchannels, dimy - yoff, bodywidth, 0); + if(n->title){ + n->ncp->channels = n->boxchannels; + ncplane_putegc_yx(n->ncp, 2, dimx - 1, "┤", NULL); + if(bodywidth < dimx){ + ncplane_putegc_yx(n->ncp, 2, dimx - bodywidth, "┬", NULL); + } + if((n->titlecols + 4 != dimx) && n->titlecols > n->secondarycols){ + ncplane_putegc_yx(n->ncp, 2, dimx - (n->titlecols + 4), "┴", NULL); + } + } + // There is always at least one space available on the right for the + // secondary title and footer, but we'd prefer to use a few more if we can. + if(n->secondary){ + int xloc = bodywidth - (n->secondarycols + 1) + xoff; + if(n->secondarycols < bodywidth - 2){ + --xloc; + } + n->ncp->channels = n->footchannels; + ncplane_putstr_yx(n->ncp, yoff, xloc, n->secondary); + } + if(n->footer){ + int xloc = bodywidth - (n->footercols + 1) + xoff; + if(n->footercols < bodywidth - 2){ + --xloc; + } + n->ncp->channels = n->footchannels; + ncplane_putstr_yx(n->ncp, dimy - 1, xloc, n->footer); + } + // Top line of body (background and possibly up arrow) + ++yoff; + ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1); + for(int i = xoff + 1 ; i < dimx - 1 ; ++i){ + ncplane_putc(n->ncp, &n->background); + } + const int bodyoffset = dimx - bodywidth + 2; + if(n->maxdisplay && n->maxdisplay < n->itemcount){ + n->ncp->channels = n->descchannels; + n->arrowx = bodyoffset + n->longop; + ncplane_putegc_yx(n->ncp, yoff, n->arrowx, "↑", NULL); + }else{ + n->arrowx = -1; + } + n->uarrowy = yoff; + unsigned printidx = n->startdisp; + unsigned printed = 0; + for(yoff += 1 ; yoff < dimy - 2 ; ++yoff){ + if(n->maxdisplay && printed == n->maxdisplay){ + break; + } + ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1); + for(int i = xoff + 1 ; i < dimx - 1 ; ++i){ + ncplane_putc(n->ncp, &n->background); + } + n->ncp->channels = n->opchannels; + if(printidx == n->current){ + n->ncp->channels = (uint64_t)channels_bchannel(n->opchannels) << 32u | channels_fchannel(n->opchannels); + } + ncplane_printf_yx(n->ncp, yoff, bodyoffset + (n->longop - n->items[printidx].opcolumns), "%s", n->items[printidx].option); + n->ncp->channels = n->descchannels; + if(printidx == n->current){ + n->ncp->channels = (uint64_t)channels_bchannel(n->descchannels) << 32u | channels_fchannel(n->descchannels); + } + ncplane_printf_yx(n->ncp, yoff, bodyoffset + n->longop, " %s", n->items[printidx].desc); + if(++printidx == n->itemcount){ + printidx = 0; + } + ++printed; + } + // Bottom line of body (background and possibly down arrow) + ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1); + for(int i = xoff + 1 ; i < dimx - 1 ; ++i){ + ncplane_putc(n->ncp, &n->background); + } + if(n->maxdisplay && n->maxdisplay < n->itemcount){ + n->ncp->channels = n->descchannels; + ncplane_putegc_yx(n->ncp, yoff, n->arrowx, "↓", NULL); + } + n->darrowy = yoff; + return notcurses_render(n->ncp->nc); +} + +const char* ncmultiselector_previtem(ncmultiselector* n){ + const char* ret = NULL; + if(n->itemcount == 0){ + return ret; + } + if(n->current == n->startdisp){ + if(n->startdisp-- == 0){ + n->startdisp = n->itemcount - 1; + } + } + if(n->current == 0){ + n->current = n->itemcount; + } + --n->current; + ret = n->items[n->current].option; + ncmultiselector_draw(n); + return ret; +} + +const char* ncmultiselector_nextitem(ncmultiselector* n){ + const char* ret = NULL; + if(n->itemcount == 0){ + return NULL; + } + unsigned lastdisp = n->startdisp; + lastdisp += n->maxdisplay && n->maxdisplay < n->itemcount ? n->maxdisplay : n->itemcount; + --lastdisp; + lastdisp %= n->itemcount; + if(lastdisp == n->current){ + if(++n->startdisp == n->itemcount){ + n->startdisp = 0; + } + } + ++n->current; + if(n->current == n->itemcount){ + n->current = 0; + } + ret = n->items[n->current].option; + ncmultiselector_draw(n); + return ret; +} + +bool ncmultiselector_offer_input(ncmultiselector* n, const ncinput* nc){ + // FIXME handle space to toggle selection + if(nc->id == NCKEY_UP){ + ncmultiselector_previtem(n); + return true; + }else if(nc->id == NCKEY_DOWN){ + ncmultiselector_nextitem(n); + return true; + }else if(nc->id == NCKEY_SCROLL_UP){ + ncmultiselector_previtem(n); + return true; + }else if(nc->id == NCKEY_SCROLL_DOWN){ + ncmultiselector_nextitem(n); + return true; + }else if(nc->id == NCKEY_RELEASE){ + int y = nc->y, x = nc->x; + if(!ncplane_translate_abs(n->ncp, &y, &x)){ + return false; + } + if(y == n->uarrowy && x == n->arrowx){ + ncmultiselector_previtem(n); + return true; + }else if(y == n->darrowy && x == n->arrowx){ + ncmultiselector_nextitem(n); + return true; + }else if(n->uarrowy < y && y < n->darrowy){ + // FIXME we probably only want to consider it a click if both the release + // and the depress happened to be on us. for now, just check release. + // FIXME verify that we're within the body walls! + // FIXME verify we're on the left of the split? + // FIXME verify that we're on a visible glyph? + int cury = (n->current + n->itemcount - n->startdisp) % n->itemcount; + int click = y - n->uarrowy - 1; + while(click > cury){ + ncmultiselector_nextitem(n); + ++cury; + } + while(click < cury){ + ncmultiselector_previtem(n); + --cury; + } + return true; + } + } + return false; +} + +// calculate the necessary dimensions based off properties of the selector and +// the containing screen FIXME should be based on containing ncplane +static int +ncmultiselector_dim_yx(notcurses* nc, const ncmultiselector* n, int* ncdimy, int* ncdimx){ + int rows = 0, cols = 0; // desired dimensions + int dimy, dimx; // dimensions of containing screen + notcurses_term_dim_yx(nc, &dimy, &dimx); + if(n->title){ // header adds two rows for riser + rows += 2; + } + // we have a top line, a bottom line, two lines of margin, and must be able + // to display at least one row beyond that, so require five more + rows += 5; + if(rows > dimy){ // insufficient height to display selector + return -1; + } + rows += (!n->maxdisplay || n->maxdisplay > n->itemcount ? n->itemcount : n->maxdisplay) - 1; // rows necessary to display all options + if(rows > dimy){ // claw excess back + rows = dimy; + } + *ncdimy = rows; + cols = ncmultiselector_body_width(n); + // the riser, if it exists, is header + 4. the cols are the max of these two. + if(n->titlecols + 4 > cols){ + cols = n->titlecols + 4; + } + if(cols > dimx){ // insufficient width to display selector + return -1; + } + *ncdimx = cols; + return 0; +} + +ncmultiselector* ncmultiselector_create(ncplane* n, int y, int x, const multiselector_options* opts){ + ncmultiselector* ns = malloc(sizeof(*ns)); + ns->title = opts->title ? strdup(opts->title) : NULL; + ns->titlecols = opts->title ? mbswidth(opts->title) : 0; + ns->secondary = opts->secondary ? strdup(opts->secondary) : NULL; + ns->secondarycols = opts->secondary ? mbswidth(opts->secondary) : 0; + ns->footer = opts->footer ? strdup(opts->footer) : NULL; + ns->footercols = opts->footer ? mbswidth(opts->footer) : 0; + ns->current = 0; + ns->startdisp = 0; + ns->longop = 0; + ns->maxdisplay = opts->maxdisplay; + ns->longdesc = 0; + ns->opchannels = opts->opchannels; + ns->boxchannels = opts->boxchannels; + ns->descchannels = opts->descchannels; + ns->titlechannels = opts->titlechannels; + ns->footchannels = opts->footchannels; + ns->boxchannels = opts->boxchannels; + ns->darrowy = ns->uarrowy = ns->arrowx = -1; + if(opts->itemcount){ + if(!(ns->items = malloc(sizeof(*ns->items) * opts->itemcount))){ + free(ns->title); free(ns->secondary); free(ns->footer); + free(n); + return NULL; + } + }else{ + ns->items = NULL; + } + for(ns->itemcount = 0 ; ns->itemcount < opts->itemcount ; ++ns->itemcount){ + const struct selector_item* src = &opts->items[ns->itemcount]; + int cols = mbswidth(src->option); + ns->items[ns->itemcount].opcolumns = cols; + if(cols > ns->longop){ + ns->longop = cols; + } + cols = mbswidth(src->desc); + ns->items[ns->itemcount].desccolumns = cols; + if(cols > ns->longdesc){ + ns->longdesc = cols; + } + ns->items[ns->itemcount].option = strdup(src->option); + ns->items[ns->itemcount].desc = strdup(src->desc); + if(!(ns->items[ns->itemcount].desc && ns->items[ns->itemcount].option)){ + free(ns->items[ns->itemcount].option); + free(ns->items[ns->itemcount].desc); + goto freeitems; + } + } + int dimy, dimx; + if(ncmultiselector_dim_yx(n->nc, ns, &dimy, &dimx)){ + goto freeitems; + } + if(!(ns->ncp = ncplane_new(n->nc, dimy, dimx, y, x, NULL))){ + goto freeitems; + } + cell_init(&ns->background); + uint64_t transchan = 0; + channels_set_fg_alpha(&transchan, CELL_ALPHA_TRANSPARENT); + channels_set_bg_alpha(&transchan, CELL_ALPHA_TRANSPARENT); + ncplane_set_base(ns->ncp, transchan, 0, ""); + if(cell_prime(ns->ncp, &ns->background, " ", 0, opts->bgchannels) < 0){ + ncplane_destroy(ns->ncp); + goto freeitems; + } + ncmultiselector_draw(ns); // deal with error here? + return ns; + +freeitems: + while(ns->itemcount--){ + free(ns->items[ns->itemcount].option); + free(ns->items[ns->itemcount].desc); + } + free(ns->items); + free(ns->title); free(ns->secondary); free(ns->footer); + free(ns); + return NULL; +} + +void ncmultiselector_destroy(ncmultiselector* n, char** item){ + if(n){ + if(item){ + *item = n->items[n->current].option; + n->items[n->current].option = NULL; + } + while(n->itemcount--){ + free(n->items[n->itemcount].option); + free(n->items[n->itemcount].desc); + } + cell_release(n->ncp, &n->background); + ncplane_destroy(n->ncp); + free(n->items); + free(n->title); + free(n->secondary); + free(n->footer); + free(n); + } +}