ncmultiselector links up

This commit is contained in:
nick black 2020-03-09 01:20:29 -04:00 committed by Nick Black
parent 635d6e5751
commit 078feca8e6
2 changed files with 370 additions and 1 deletions

View File

@ -182,6 +182,30 @@ typedef struct ncselector {
int uarrowy, darrowy, arrowx;// location of scrollarrows, even if not present int uarrowy, darrowy, arrowx;// location of scrollarrows, even if not present
} ncselector; } 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 { typedef struct ncdirect {
int attrword; // current styles int attrword; // current styles
int colors; // number of colors terminfo reported usable for this screen int colors; // number of colors terminfo reported usable for this screen

View File

@ -389,3 +389,348 @@ void ncselector_destroy(ncselector* n, char** item){
free(n); 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);
}
}