1 #include "internal.h"
2 
3 // internal ncselector item
4 struct ncselector_int {
5   char* option;
6   char* desc;
7   size_t opcolumns;   // filled in by library
8   size_t desccolumns; // filled in by library
9 };
10 
11 struct ncmselector_int {
12   char* option;
13   char* desc;
14   bool selected;
15 };
16 
17 typedef struct ncselector {
18   ncplane* ncp;                  // backing ncplane
19   unsigned selected;             // index of selection
20   unsigned startdisp;            // index of first option displayed
21   unsigned maxdisplay;           // max number of items to display, 0 -> no limit
22   unsigned longop;               // columns occupied by longest option
23   unsigned longdesc;             // columns occupied by longest description
24   struct ncselector_int* items;  // list of items and descriptions, heap-copied
25   unsigned itemcount;            // number of pairs in 'items'
26   char* title;                   // can be NULL, in which case there's no riser
27   unsigned titlecols;            // columns occupied by title
28   char* secondary;               // can be NULL
29   unsigned secondarycols;        // columns occupied by secondary
30   char* footer;                  // can be NULL
31   unsigned footercols;           // columns occupied by footer
32   uint64_t opchannels;           // option channels
33   uint64_t descchannels;         // description channels
34   uint64_t titlechannels;        // title channels
35   uint64_t footchannels;         // secondary and footer channels
36   uint64_t boxchannels;          // border channels
37   int uarrowy, darrowy, arrowx;// location of scrollarrows, even if not present
38 } ncselector;
39 
40 typedef struct ncmultiselector {
41   ncplane* ncp;                   // backing ncplane
42   unsigned current;               // index of highlighted item
43   unsigned startdisp;             // index of first option displayed
44   unsigned maxdisplay;            // max number of items to display, 0 -> no limit
45   unsigned longitem;              // columns occupied by longest item
46   struct ncmselector_int* items;  // items, descriptions, and statuses, heap-copied
47   unsigned itemcount;             // number of pairs in 'items'
48   char* title;                    // can be NULL, in which case there's no riser
49   unsigned titlecols;             // columns occupied by title
50   char* secondary;                // can be NULL
51   unsigned secondarycols;         // columns occupied by secondary
52   char* footer;                   // can be NULL
53   unsigned footercols;            // columns occupied by footer
54   uint64_t opchannels;            // option channels
55   uint64_t descchannels;          // description channels
56   uint64_t titlechannels;         // title channels
57   uint64_t footchannels;          // secondary and footer channels
58   uint64_t boxchannels;           // border channels
59   int uarrowy, darrowy, arrowx;   // location of scrollarrows, even if not present
60 } ncmultiselector;
61 
62 // ideal body width given the ncselector's items and secondary/footer
63 static int
ncselector_body_width(const ncselector * n)64 ncselector_body_width(const ncselector* n){
65   unsigned cols = 0;
66   // the body is the maximum of
67   //  * longop + longdesc + 5
68   //  * secondary + 2
69   //  * footer + 2
70   if(n->footercols + 2 > cols){
71     cols = n->footercols + 2;
72   }
73   if(n->secondarycols + 2 > cols){
74     cols = n->secondarycols + 2;
75   }
76   if(n->longop + n->longdesc + 5 > cols){
77     cols = n->longop + n->longdesc + 5;
78   }
79   return cols;
80 }
81 
82 // redraw the selector widget in its entirety
83 static int
ncselector_draw(ncselector * n)84 ncselector_draw(ncselector* n){
85   ncplane_erase(n->ncp);
86   nccell transchar = NCCELL_TRIVIAL_INITIALIZER;
87   nccell_set_fg_alpha(&transchar, NCALPHA_TRANSPARENT);
88   nccell_set_bg_alpha(&transchar, NCALPHA_TRANSPARENT);
89   // if we have a title, we'll draw a riser. the riser is two rows tall, and
90   // exactly four columns longer than the title, and aligned to the right. we
91   // draw a rounded box. the body will blow part or all of the bottom away.
92   int yoff = 0;
93   if(n->title){
94     size_t riserwidth = n->titlecols + 4;
95     int offx = ncplane_halign(n->ncp, NCALIGN_RIGHT, riserwidth);
96     ncplane_cursor_move_yx(n->ncp, 0, 0);
97     if(offx){
98       ncplane_hline(n->ncp, &transchar, offx);
99     }
100     ncplane_cursor_move_yx(n->ncp, 0, offx);
101     ncplane_rounded_box_sized(n->ncp, 0, n->boxchannels, 3, riserwidth, 0);
102     n->ncp->channels = n->titlechannels;
103     ncplane_printf_yx(n->ncp, 1, offx + 1, " %s ", n->title);
104     yoff += 2;
105     ncplane_cursor_move_yx(n->ncp, 1, 0);
106     if(offx){
107       ncplane_hline(n->ncp, &transchar, offx);
108     }
109   }
110   unsigned bodywidth = ncselector_body_width(n);
111   unsigned dimy, dimx;
112   ncplane_dim_yx(n->ncp, &dimy, &dimx);
113   int xoff = ncplane_halign(n->ncp, NCALIGN_RIGHT, bodywidth);
114   if(xoff){
115     for(unsigned y = yoff + 1 ; y < dimy ; ++y){
116       ncplane_cursor_move_yx(n->ncp, y, 0);
117       ncplane_hline(n->ncp, &transchar, xoff);
118     }
119   }
120   ncplane_cursor_move_yx(n->ncp, yoff, xoff);
121   ncplane_rounded_box_sized(n->ncp, 0, n->boxchannels, dimy - yoff, bodywidth, 0);
122   if(n->title){
123     n->ncp->channels = n->boxchannels;
124     if(notcurses_canutf8(ncplane_notcurses(n->ncp))){
125       ncplane_putegc_yx(n->ncp, 2, dimx - 1, "┤", NULL);
126     }else{
127       ncplane_putchar_yx(n->ncp, 2, dimx - 1, '|');
128     }
129     if(bodywidth < dimx){
130       if(notcurses_canutf8(ncplane_notcurses(n->ncp))){
131         ncplane_putegc_yx(n->ncp, 2, dimx - bodywidth, "┬", NULL);
132       }else{
133         ncplane_putchar_yx(n->ncp, 2, dimx - bodywidth, '-');
134       }
135     }
136     if((n->titlecols + 4 != dimx) && n->titlecols > n->secondarycols){
137       if(notcurses_canutf8(ncplane_notcurses(n->ncp))){
138         ncplane_putegc_yx(n->ncp, 2, dimx - (n->titlecols + 4), "┴", NULL);
139       }else{
140         ncplane_putchar_yx(n->ncp, 2, dimx - (n->titlecols + 4), '-');
141       }
142     }
143   }
144   // There is always at least one space available on the right for the
145   // secondary title and footer, but we'd prefer to use a few more if we can.
146   if(n->secondary){
147     int xloc = bodywidth - (n->secondarycols + 1) + xoff;
148     if(n->secondarycols < bodywidth - 2){
149       --xloc;
150     }
151     n->ncp->channels = n->footchannels;
152     ncplane_putstr_yx(n->ncp, yoff, xloc, n->secondary);
153   }
154   if(n->footer){
155     int xloc = bodywidth - (n->footercols + 1) + xoff;
156     if(n->footercols < bodywidth - 2){
157       --xloc;
158     }
159     n->ncp->channels = n->footchannels;
160     ncplane_putstr_yx(n->ncp, dimy - 1, xloc, n->footer);
161   }
162   // Top line of body (background and possibly up arrow)
163   ++yoff;
164   ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1);
165   for(unsigned i = xoff + 1 ; i < dimx - 1 ; ++i){
166     nccell transc = NCCELL_TRIVIAL_INITIALIZER; // fall back to base cell
167     ncplane_putc(n->ncp, &transc);
168   }
169   const int bodyoffset = dimx - bodywidth + 2;
170   if(n->maxdisplay && n->maxdisplay < n->itemcount){
171     n->ncp->channels = n->descchannels;
172     n->arrowx = bodyoffset + n->longop;
173     if(notcurses_canutf8(ncplane_notcurses(n->ncp))){
174       ncplane_putegc_yx(n->ncp, yoff, n->arrowx, "↑", NULL);
175     }else{
176       ncplane_putchar_yx(n->ncp, yoff, n->arrowx, '<');
177     }
178   }else{
179     n->arrowx = -1;
180   }
181   n->uarrowy = yoff;
182   unsigned printidx = n->startdisp;
183   unsigned printed = 0;
184   for(yoff += 1 ; yoff < (int)dimy - 2 ; ++yoff){
185     if(n->maxdisplay && printed == n->maxdisplay){
186       break;
187     }
188     ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1);
189     for(int i = xoff + 1 ; i < (int)dimx - 1 ; ++i){
190       nccell transc = NCCELL_TRIVIAL_INITIALIZER; // fall back to base cell
191       ncplane_putc(n->ncp, &transc);
192     }
193     n->ncp->channels = n->opchannels;
194     if(printidx == n->selected){
195       n->ncp->channels = (uint64_t)ncchannels_bchannel(n->opchannels) << 32u | ncchannels_fchannel(n->opchannels);
196     }
197     ncplane_printf_yx(n->ncp, yoff, bodyoffset + (n->longop - n->items[printidx].opcolumns), "%s", n->items[printidx].option);
198     n->ncp->channels = n->descchannels;
199     if(printidx == n->selected){
200       n->ncp->channels = (uint64_t)ncchannels_bchannel(n->descchannels) << 32u | ncchannels_fchannel(n->descchannels);
201     }
202     ncplane_printf_yx(n->ncp, yoff, bodyoffset + n->longop, " %s", n->items[printidx].desc);
203     if(++printidx == n->itemcount){
204       printidx = 0;
205     }
206     ++printed;
207   }
208   // Bottom line of body (background and possibly down arrow)
209   ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1);
210   for(int i = xoff + 1 ; i < (int)dimx - 1 ; ++i){
211     nccell transc = NCCELL_TRIVIAL_INITIALIZER; // fall back to base cell
212     ncplane_putc(n->ncp, &transc);
213   }
214   if(n->maxdisplay && n->maxdisplay < n->itemcount){
215     n->ncp->channels = n->descchannels;
216     if(notcurses_canutf8(ncplane_notcurses(n->ncp))){
217       ncplane_putegc_yx(n->ncp, yoff, n->arrowx, "↓", NULL);
218     }else{
219       ncplane_putchar_yx(n->ncp, yoff, n->arrowx, '>');
220     }
221   }
222   n->darrowy = yoff;
223   return 0;
224 }
225 
226 // calculate the necessary dimensions based off properties of the selector
227 static void
ncselector_dim_yx(const ncselector * n,unsigned * ncdimy,unsigned * ncdimx)228 ncselector_dim_yx(const ncselector* n, unsigned* ncdimy, unsigned* ncdimx){
229   unsigned rows = 0, cols = 0; // desired dimensions
230   const ncplane* parent = ncplane_parent(n->ncp);
231   unsigned dimy, dimx; // dimensions of containing plane
232   ncplane_dim_yx(parent, &dimy, &dimx);
233   if(n->title){ // header adds two rows for riser
234     rows += 2;
235   }
236   // we have a top line, a bottom line, two lines of margin, and must be able
237   // to display at least one row beyond that, so require five more
238   rows += 5;
239   rows += (!n->maxdisplay || n->maxdisplay > n->itemcount ? n->itemcount : n->maxdisplay) - 1; // rows necessary to display all options
240   if(rows > dimy){ // claw excess back
241     rows = dimy;
242   }
243   *ncdimy = rows;
244   cols = ncselector_body_width(n);
245   // the riser, if it exists, is header + 4. the cols are the max of these two.
246   if(n->titlecols + 4 > cols){
247     cols = n->titlecols + 4;
248   }
249   *ncdimx = cols;
250 }
251 
252 static void
ncselector_destroy_internal(ncselector * n)253 ncselector_destroy_internal(ncselector* n){
254   if(n){
255     while(n->itemcount--){
256       free(n->items[n->itemcount].option);
257       free(n->items[n->itemcount].desc);
258     }
259     if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
260       ncplane_destroy(n->ncp);
261     }
262     free(n->items);
263     free(n->title);
264     free(n->secondary);
265     free(n->footer);
266     free(n);
267   }
268 }
269 
ncselector_destroy(ncselector * n,char ** item)270 void ncselector_destroy(ncselector* n, char** item){
271   if(n){
272     if(item){
273       *item = n->items[n->selected].option;
274       n->items[n->selected].option = NULL;
275     }
276     ncselector_destroy_internal(n);
277   }
278 }
279 
ncselector_create(ncplane * n,const ncselector_options * opts)280 ncselector* ncselector_create(ncplane* n, const ncselector_options* opts){
281   if(n == notcurses_stdplane(ncplane_notcurses(n))){
282     logerror("won't use the standard plane\n"); // would fail later on resize
283     return NULL;
284   }
285   ncselector_options zeroed = {};
286   if(!opts){
287     opts = &zeroed;
288   }
289   unsigned itemcount = 0;
290   if(opts->flags > 0){
291     logwarn("Provided unsupported flags %016" PRIx64 "\n", opts->flags);
292   }
293   if(opts->items){
294     for(const struct ncselector_item* i = opts->items ; i->option ; ++i){
295       ++itemcount;
296     }
297   }
298   ncselector* ns = malloc(sizeof(*ns));
299   if(ns == NULL){
300     return NULL;
301   }
302   memset(ns, 0, sizeof(*ns));
303   if(opts->defidx && opts->defidx >= itemcount){
304     logerror("default index %u too large (%u items)\n", opts->defidx, itemcount);
305     goto freeitems;
306   }
307   ns->title = opts->title ? strdup(opts->title) : NULL;
308   ns->titlecols = opts->title ? ncstrwidth(opts->title, NULL, NULL) : 0;
309   ns->secondary = opts->secondary ? strdup(opts->secondary) : NULL;
310   ns->secondarycols = opts->secondary ? ncstrwidth(opts->secondary, NULL, NULL) : 0;
311   ns->footer = opts->footer ? strdup(opts->footer) : NULL;
312   ns->footercols = opts->footer ? ncstrwidth(opts->footer, NULL, NULL) : 0;
313   ns->selected = opts->defidx;
314   ns->longop = 0;
315   if( (ns->maxdisplay = opts->maxdisplay) ){
316     if(opts->defidx >= ns->maxdisplay){
317       ns->startdisp = opts->defidx - ns->maxdisplay + 1;
318     }else{
319       ns->startdisp = 0;
320     }
321   }else{
322     ns->startdisp = 0;
323   }
324   ns->longdesc = 0;
325   ns->opchannels = opts->opchannels;
326   ns->boxchannels = opts->boxchannels;
327   ns->descchannels = opts->descchannels;
328   ns->titlechannels = opts->titlechannels;
329   ns->footchannels = opts->footchannels;
330   ns->boxchannels = opts->boxchannels;
331   ns->darrowy = ns->uarrowy = ns->arrowx = -1;
332   if(itemcount){
333     if(!(ns->items = malloc(sizeof(*ns->items) * itemcount))){
334       goto freeitems;
335     }
336   }else{
337     ns->items = NULL;
338   }
339   for(ns->itemcount = 0 ; ns->itemcount < itemcount ; ++ns->itemcount){
340     const struct ncselector_item* src = &opts->items[ns->itemcount];
341     int unsafe = ncstrwidth(src->option, NULL, NULL);
342     if(unsafe < 0){
343       goto freeitems;
344     }
345     unsigned cols = unsafe;
346     ns->items[ns->itemcount].opcolumns = cols;
347     if(cols > ns->longop){
348       ns->longop = cols;
349     }
350     const char *desc = src->desc ? src->desc : "";
351     unsafe = ncstrwidth(desc, NULL, NULL);
352     if(unsafe < 0){
353       goto freeitems;
354     }
355     cols = unsafe;
356     ns->items[ns->itemcount].desccolumns = cols;
357     if(cols > ns->longdesc){
358       ns->longdesc = cols;
359     }
360     ns->items[ns->itemcount].option = strdup(src->option);
361     ns->items[ns->itemcount].desc = strdup(desc);
362     if(!(ns->items[ns->itemcount].desc && ns->items[ns->itemcount].option)){
363       free(ns->items[ns->itemcount].option);
364       free(ns->items[ns->itemcount].desc);
365       goto freeitems;
366     }
367   }
368   unsigned dimy, dimx;
369   ns->ncp = n;
370   ncselector_dim_yx(ns, &dimy, &dimx);
371   if(ncplane_resize_simple(n, dimy, dimx)){
372     goto freeitems;
373   }
374   if(ncplane_set_widget(ns->ncp, ns, (void(*)(void*))ncselector_destroy_internal)){
375     goto freeitems;
376   }
377   ncselector_draw(ns); // deal with error here?
378   return ns;
379 
380 freeitems:
381   while(ns->itemcount--){
382     free(ns->items[ns->itemcount].option);
383     free(ns->items[ns->itemcount].desc);
384   }
385   free(ns->items);
386   free(ns->title); free(ns->secondary); free(ns->footer);
387   free(ns);
388   ncplane_destroy(n);
389   return NULL;
390 }
391 
ncselector_additem(ncselector * n,const struct ncselector_item * item)392 int ncselector_additem(ncselector* n, const struct ncselector_item* item){
393   unsigned origdimy, origdimx;
394   ncselector_dim_yx(n, &origdimy, &origdimx);
395   size_t newsize = sizeof(*n->items) * (n->itemcount + 1);
396   struct ncselector_int* items = realloc(n->items, newsize);
397   if(!items){
398     return -1;
399   }
400   n->items = items;
401   n->items[n->itemcount].option = strdup(item->option);
402   const char *desc = item->desc ? item->desc : "";
403   n->items[n->itemcount].desc = strdup(desc);
404   int usafecols = ncstrwidth(item->option, NULL, NULL);
405   if(usafecols < 0){
406     return -1;
407   }
408   unsigned cols = usafecols;
409   n->items[n->itemcount].opcolumns = cols;
410   if(cols > n->longop){
411     n->longop = cols;
412   }
413   cols = ncstrwidth(desc, NULL, NULL);
414   n->items[n->itemcount].desccolumns = cols;
415   if(cols > n->longdesc){
416     n->longdesc = cols;
417   }
418   ++n->itemcount;
419   unsigned dimy, dimx;
420   ncselector_dim_yx(n, &dimy, &dimx);
421   if(origdimx < dimx || origdimy < dimy){ // resize if too small
422     ncplane_resize_simple(n->ncp, dimy, dimx);
423   }
424   return ncselector_draw(n);
425 }
426 
ncselector_delitem(ncselector * n,const char * item)427 int ncselector_delitem(ncselector* n, const char* item){
428   unsigned origdimy, origdimx;
429   ncselector_dim_yx(n, &origdimy, &origdimx);
430   bool found = false;
431   int maxop = 0, maxdesc = 0;
432   for(unsigned idx = 0 ; idx < n->itemcount ; ++idx){
433     if(strcmp(n->items[idx].option, item) == 0){ // found it
434       free(n->items[idx].option);
435       free(n->items[idx].desc);
436       if(idx < n->itemcount - 1){
437         memmove(n->items + idx, n->items + idx + 1, sizeof(*n->items) * (n->itemcount - idx - 1));
438       }else{
439         if(idx){
440           --n->selected;
441         }
442       }
443       --n->itemcount;
444       found = true;
445       --idx;
446     }else{
447       int cols = ncstrwidth(n->items[idx].option, NULL, NULL);
448       if(cols > maxop){
449         maxop = cols;
450       }
451       cols = ncstrwidth(n->items[idx].desc, NULL, NULL);
452       if(cols > maxdesc){
453         maxdesc = cols;
454       }
455     }
456   }
457   if(found){
458     n->longop = maxop;
459     n->longdesc = maxdesc;
460     unsigned dimy, dimx;
461     ncselector_dim_yx(n, &dimy, &dimx);
462     if(origdimx > dimx || origdimy > dimy){ // resize if too big
463       ncplane_resize_simple(n->ncp, dimy, dimx);
464     }
465     return ncselector_draw(n);
466   }
467   return -1; // wasn't found
468 }
469 
ncselector_plane(ncselector * n)470 ncplane* ncselector_plane(ncselector* n){
471   return n->ncp;
472 }
473 
ncselector_selected(const ncselector * n)474 const char* ncselector_selected(const ncselector* n){
475   if(n->itemcount == 0){
476     return NULL;
477   }
478   return n->items[n->selected].option;
479 }
480 
ncselector_previtem(ncselector * n)481 const char* ncselector_previtem(ncselector* n){
482   const char* ret = NULL;
483   if(n->itemcount == 0){
484     return ret;
485   }
486   if(n->selected == n->startdisp){
487     if(n->startdisp-- == 0){
488       n->startdisp = n->itemcount - 1;
489     }
490   }
491   if(n->selected == 0){
492     n->selected = n->itemcount;
493   }
494   --n->selected;
495   ret = n->items[n->selected].option;
496   ncselector_draw(n);
497   return ret;
498 }
499 
ncselector_nextitem(ncselector * n)500 const char* ncselector_nextitem(ncselector* n){
501   const char* ret = NULL;
502   if(n->itemcount == 0){
503     return NULL;
504   }
505   unsigned lastdisp = n->startdisp;
506   lastdisp += n->maxdisplay && n->maxdisplay < n->itemcount ? n->maxdisplay : n->itemcount;
507   --lastdisp;
508   lastdisp %= n->itemcount;
509   if(lastdisp == n->selected){
510     if(++n->startdisp == n->itemcount){
511       n->startdisp = 0;
512     }
513   }
514   ++n->selected;
515   if(n->selected == n->itemcount){
516     n->selected = 0;
517   }
518   ret = n->items[n->selected].option;
519   ncselector_draw(n);
520   return ret;
521 }
522 
ncselector_offer_input(ncselector * n,const ncinput * nc)523 bool ncselector_offer_input(ncselector* n, const ncinput* nc){
524   const int items_shown = ncplane_dim_y(n->ncp) - 4 - (n->title ? 2 : 0);
525   if(nc->id == NCKEY_BUTTON1 && nc->evtype == NCTYPE_RELEASE){
526     int y = nc->y, x = nc->x;
527     if(!ncplane_translate_abs(n->ncp, &y, &x)){
528       return false;
529     }
530     if(y == n->uarrowy && x == n->arrowx){
531       ncselector_previtem(n);
532       return true;
533     }else if(y == n->darrowy && x == n->arrowx){
534       ncselector_nextitem(n);
535       return true;
536     }else if(n->uarrowy < y && y < n->darrowy){
537       // FIXME we probably only want to consider it a click if both the release
538       // and the depress happened to be on us. for now, just check release.
539       // FIXME verify that we're within the body walls!
540       // FIXME verify we're on the left of the split?
541       // FIXME verify that we're on a visible glyph?
542       int cury = (n->selected + n->itemcount - n->startdisp) % n->itemcount;
543       int click = y - n->uarrowy - 1;
544       while(click > cury){
545         ncselector_nextitem(n);
546         ++cury;
547       }
548       while(click < cury){
549         ncselector_previtem(n);
550         --cury;
551       }
552       return true;
553     }
554   }else if(nc->evtype != NCTYPE_RELEASE){
555     if(nc->id == NCKEY_UP){
556       ncselector_previtem(n);
557       return true;
558     }else if(nc->id == NCKEY_DOWN){
559       ncselector_nextitem(n);
560       return true;
561     }else if(nc->id == NCKEY_SCROLL_UP){
562       ncselector_previtem(n);
563       return true;
564     }else if(nc->id == NCKEY_SCROLL_DOWN){
565       ncselector_nextitem(n);
566       return true;
567     }else if(nc->id == NCKEY_PGDOWN){
568       if(items_shown > 0){
569         for(int i = 0 ; i < items_shown ; ++i){
570           ncselector_nextitem(n);
571         }
572       }
573       return true;
574     }else if(nc->id == NCKEY_PGUP){
575       if(items_shown > 0){
576         for(int i = 0 ; i < items_shown ; ++i){
577           ncselector_previtem(n);
578         }
579       }
580       return true;
581     }
582   }
583   return false;
584 }
585 
ncmultiselector_plane(ncmultiselector * n)586 ncplane* ncmultiselector_plane(ncmultiselector* n){
587   return n->ncp;
588 }
589 
590 // ideal body width given the ncselector's items and secondary/footer
591 static unsigned
ncmultiselector_body_width(const ncmultiselector * n)592 ncmultiselector_body_width(const ncmultiselector* n){
593   unsigned cols = 0;
594   // the body is the maximum of
595   //  * longop + longdesc + 5
596   //  * secondary + 2
597   //  * footer + 2
598   if(n->footercols + 2 > cols){
599     cols = n->footercols + 2;
600   }
601   if(n->secondarycols + 2 > cols){
602     cols = n->secondarycols + 2;
603   }
604   if(n->longitem + 7 > cols){
605     cols = n->longitem + 7;
606   }
607   return cols;
608 }
609 
610 // redraw the multiselector widget in its entirety
611 static int
ncmultiselector_draw(ncmultiselector * n)612 ncmultiselector_draw(ncmultiselector* n){
613   ncplane_erase(n->ncp);
614   nccell transchar = NCCELL_TRIVIAL_INITIALIZER;
615   nccell_set_fg_alpha(&transchar, NCALPHA_TRANSPARENT);
616   nccell_set_bg_alpha(&transchar, NCALPHA_TRANSPARENT);
617   // if we have a title, we'll draw a riser. the riser is two rows tall, and
618   // exactly four columns longer than the title, and aligned to the right. we
619   // draw a rounded box. the body will blow part or all of the bottom away.
620   unsigned yoff = 0;
621   if(n->title){
622     size_t riserwidth = n->titlecols + 4;
623     int offx = ncplane_halign(n->ncp, NCALIGN_RIGHT, riserwidth);
624     ncplane_cursor_move_yx(n->ncp, 0, 0);
625     if(offx){
626       ncplane_hline(n->ncp, &transchar, offx);
627     }
628     ncplane_rounded_box_sized(n->ncp, 0, n->boxchannels, 3, riserwidth, 0);
629     n->ncp->channels = n->titlechannels;
630     ncplane_printf_yx(n->ncp, 1, offx + 1, " %s ", n->title);
631     yoff += 2;
632     ncplane_cursor_move_yx(n->ncp, 1, 0);
633     if(offx){
634       ncplane_hline(n->ncp, &transchar, offx);
635     }
636   }
637   unsigned bodywidth = ncmultiselector_body_width(n);
638   unsigned dimy, dimx;
639   ncplane_dim_yx(n->ncp, &dimy, &dimx);
640   int xoff = ncplane_halign(n->ncp, NCALIGN_RIGHT, bodywidth);
641   if(xoff){
642     for(unsigned y = yoff + 1 ; y < dimy ; ++y){
643       ncplane_cursor_move_yx(n->ncp, y, 0);
644       ncplane_hline(n->ncp, &transchar, xoff);
645     }
646   }
647   ncplane_cursor_move_yx(n->ncp, yoff, xoff);
648   ncplane_rounded_box_sized(n->ncp, 0, n->boxchannels, dimy - yoff, bodywidth, 0);
649   if(n->title){
650     n->ncp->channels = n->boxchannels;
651     ncplane_putegc_yx(n->ncp, 2, dimx - 1, "┤", NULL);
652     if(bodywidth < dimx){
653       ncplane_putegc_yx(n->ncp, 2, dimx - bodywidth, "┬", NULL);
654     }
655     if((n->titlecols + 4 != dimx) && n->titlecols > n->secondarycols){
656       ncplane_putegc_yx(n->ncp, 2, dimx - (n->titlecols + 4), "┴", NULL);
657     }
658   }
659   // There is always at least one space available on the right for the
660   // secondary title and footer, but we'd prefer to use a few more if we can.
661   if(n->secondary){
662     int xloc = bodywidth - (n->secondarycols + 1) + xoff;
663     if(n->secondarycols < bodywidth - 2){
664       --xloc;
665     }
666     n->ncp->channels = n->footchannels;
667     ncplane_putstr_yx(n->ncp, yoff, xloc, n->secondary);
668   }
669   if(n->footer){
670     int xloc = bodywidth - (n->footercols + 1) + xoff;
671     if(n->footercols < bodywidth - 2){
672       --xloc;
673     }
674     n->ncp->channels = n->footchannels;
675     ncplane_putstr_yx(n->ncp, dimy - 1, xloc, n->footer);
676   }
677   // Top line of body (background and possibly up arrow)
678   ++yoff;
679   ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1);
680   for(unsigned i = xoff + 1 ; i < dimx - 1 ; ++i){
681     nccell transc = NCCELL_TRIVIAL_INITIALIZER; // fall back to base cell
682     ncplane_putc(n->ncp, &transc);
683   }
684   const int bodyoffset = dimx - bodywidth + 2;
685   if(n->maxdisplay && n->maxdisplay < n->itemcount){
686     n->ncp->channels = n->descchannels;
687     n->arrowx = bodyoffset + 1;
688     ncplane_putegc_yx(n->ncp, yoff, n->arrowx, "↑", NULL);
689   }else{
690     n->arrowx = -1;
691   }
692   n->uarrowy = yoff;
693   unsigned printidx = n->startdisp;
694   unsigned printed = 0;
695   // visible option lines
696   for(yoff += 1 ; yoff < dimy - 2 ; ++yoff){
697     if(n->maxdisplay && printed == n->maxdisplay){
698       break;
699     }
700     ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1);
701     for(unsigned i = xoff + 1 ; i < dimx - 1 ; ++i){
702       nccell transc = NCCELL_TRIVIAL_INITIALIZER; // fall back to base cell
703       ncplane_putc(n->ncp, &transc);
704     }
705     n->ncp->channels = n->descchannels;
706     if(printidx == n->current){
707       n->ncp->channels = (uint64_t)ncchannels_bchannel(n->descchannels) << 32u | ncchannels_fchannel(n->descchannels);
708     }
709     if(notcurses_canutf8(ncplane_notcurses(n->ncp))){
710       ncplane_putegc_yx(n->ncp, yoff, bodyoffset, n->items[printidx].selected ? "☒" : "☐", NULL);
711     }else{
712       ncplane_putchar_yx(n->ncp, yoff, bodyoffset, n->items[printidx].selected ? 'X' : '-');
713     }
714     n->ncp->channels = n->opchannels;
715     if(printidx == n->current){
716       n->ncp->channels = (uint64_t)ncchannels_bchannel(n->opchannels) << 32u | ncchannels_fchannel(n->opchannels);
717     }
718     ncplane_printf(n->ncp, " %s ", n->items[printidx].option);
719     n->ncp->channels = n->descchannels;
720     if(printidx == n->current){
721       n->ncp->channels = (uint64_t)ncchannels_bchannel(n->descchannels) << 32u | ncchannels_fchannel(n->descchannels);
722     }
723     ncplane_printf(n->ncp, "%s", n->items[printidx].desc);
724     if(++printidx == n->itemcount){
725       printidx = 0;
726     }
727     ++printed;
728   }
729   // Bottom line of body (background and possibly down arrow)
730   ncplane_cursor_move_yx(n->ncp, yoff, xoff + 1);
731   for(unsigned i = xoff + 1 ; i < dimx - 1 ; ++i){
732     nccell transc = NCCELL_TRIVIAL_INITIALIZER; // fall back to base cell
733     ncplane_putc(n->ncp, &transc);
734   }
735   if(n->maxdisplay && n->maxdisplay < n->itemcount){
736     n->ncp->channels = n->descchannels;
737     ncplane_putegc_yx(n->ncp, yoff, n->arrowx, "↓", NULL);
738   }
739   n->darrowy = yoff;
740   return 0;
741 }
742 
ncmultiselector_previtem(ncmultiselector * n)743 const char* ncmultiselector_previtem(ncmultiselector* n){
744   const char* ret = NULL;
745   if(n->itemcount == 0){
746     return ret;
747   }
748   if(n->current == n->startdisp){
749     if(n->startdisp-- == 0){
750       n->startdisp = n->itemcount - 1;
751     }
752   }
753   if(n->current == 0){
754     n->current = n->itemcount;
755   }
756   --n->current;
757   ret = n->items[n->current].option;
758   ncmultiselector_draw(n);
759   return ret;
760 }
761 
ncmultiselector_nextitem(ncmultiselector * n)762 const char* ncmultiselector_nextitem(ncmultiselector* n){
763   const char* ret = NULL;
764   if(n->itemcount == 0){
765     return NULL;
766   }
767   unsigned lastdisp = n->startdisp;
768   lastdisp += n->maxdisplay && n->maxdisplay < n->itemcount ? n->maxdisplay : n->itemcount;
769   --lastdisp;
770   lastdisp %= n->itemcount;
771   if(lastdisp == n->current){
772     if(++n->startdisp == n->itemcount){
773       n->startdisp = 0;
774     }
775   }
776   ++n->current;
777   if(n->current == n->itemcount){
778     n->current = 0;
779   }
780   ret = n->items[n->current].option;
781   ncmultiselector_draw(n);
782   return ret;
783 }
784 
ncmultiselector_offer_input(ncmultiselector * n,const ncinput * nc)785 bool ncmultiselector_offer_input(ncmultiselector* n, const ncinput* nc){
786   const int items_shown = ncplane_dim_y(n->ncp) - 4 - (n->title ? 2 : 0);
787   if(nc->id == NCKEY_BUTTON1 && nc->evtype == NCTYPE_RELEASE){
788     int y = nc->y, x = nc->x;
789     if(!ncplane_translate_abs(n->ncp, &y, &x)){
790       return false;
791     }
792     if(y == n->uarrowy && x == n->arrowx){
793       ncmultiselector_previtem(n);
794       return true;
795     }else if(y == n->darrowy && x == n->arrowx){
796       ncmultiselector_nextitem(n);
797       return true;
798     }else if(n->uarrowy < y && y < n->darrowy){
799       // FIXME we probably only want to consider it a click if both the release
800       // and the depress happened to be on us. for now, just check release.
801       // FIXME verify that we're within the body walls!
802       // FIXME verify we're on the left of the split?
803       // FIXME verify that we're on a visible glyph?
804       int cury = (n->current + n->itemcount - n->startdisp) % n->itemcount;
805       int click = y - n->uarrowy - 1;
806       while(click > cury){
807         ncmultiselector_nextitem(n);
808         ++cury;
809       }
810       while(click < cury){
811         ncmultiselector_previtem(n);
812         --cury;
813       }
814       return true;
815     }
816   }else if(nc->evtype != NCTYPE_RELEASE){
817     if(nc->id == ' '){
818       n->items[n->current].selected = !n->items[n->current].selected;
819       ncmultiselector_draw(n);
820       return true;
821     }else if(nc->id == NCKEY_UP){
822       ncmultiselector_previtem(n);
823       return true;
824     }else if(nc->id == NCKEY_DOWN){
825       ncmultiselector_nextitem(n);
826       return true;
827     }else if(nc->id == NCKEY_PGDOWN){
828       if(items_shown > 0){
829         for(int i = 0 ; i < items_shown ; ++i){
830           ncmultiselector_nextitem(n);
831         }
832       }
833       return true;
834     }else if(nc->id == NCKEY_PGUP){
835       if(items_shown > 0){
836         for(int i = 0 ; i < items_shown ; ++i){
837           ncmultiselector_previtem(n);
838         }
839       }
840       return true;
841     }else if(nc->id == NCKEY_SCROLL_UP){
842       ncmultiselector_previtem(n);
843       return true;
844     }else if(nc->id == NCKEY_SCROLL_DOWN){
845       ncmultiselector_nextitem(n);
846       return true;
847     }
848   }
849   return false;
850 }
851 
852 // calculate the necessary dimensions based off properties of the selector and
853 // the containing plane
854 static int
ncmultiselector_dim_yx(const ncmultiselector * n,unsigned * ncdimy,unsigned * ncdimx)855 ncmultiselector_dim_yx(const ncmultiselector* n, unsigned* ncdimy, unsigned* ncdimx){
856   unsigned rows = 0, cols = 0; // desired dimensions
857   unsigned dimy, dimx; // dimensions of containing screen
858   ncplane_dim_yx(ncplane_parent(n->ncp), &dimy, &dimx);
859   if(n->title){ // header adds two rows for riser
860     rows += 2;
861   }
862   // we have a top line, a bottom line, two lines of margin, and must be able
863   // to display at least one row beyond that, so require five more
864   rows += 5;
865   if(rows > dimy){ // insufficient height to display selector
866     return -1;
867   }
868   rows += (!n->maxdisplay || n->maxdisplay > n->itemcount ? n->itemcount : n->maxdisplay) - 1; // rows necessary to display all options
869   if(rows > dimy){ // claw excess back
870     rows = dimy;
871   }
872   *ncdimy = rows;
873   cols = ncmultiselector_body_width(n);
874   // the riser, if it exists, is header + 4. the cols are the max of these two.
875   if(n->titlecols + 4 > cols){
876     cols = n->titlecols + 4;
877   }
878   if(cols > dimx){ // insufficient width to display selector
879     return -1;
880   }
881   *ncdimx = cols;
882   return 0;
883 }
884 
ncmultiselector_create(ncplane * n,const ncmultiselector_options * opts)885 ncmultiselector* ncmultiselector_create(ncplane* n, const ncmultiselector_options* opts){
886   if(n == notcurses_stdplane(ncplane_notcurses(n))){
887     logerror("won't use the standard plane\n"); // would fail later on resize
888     return NULL;
889   }
890   ncmultiselector_options zeroed = {};
891   if(!opts){
892     opts = &zeroed;
893   }
894   if(opts->flags > 0){
895     logwarn("Provided unsupported flags %016" PRIx64 "\n", opts->flags);
896   }
897   unsigned itemcount = 0;
898   if(opts->items){
899     for(const struct ncmselector_item* i = opts->items ; i->option ; ++i){
900       ++itemcount;
901     }
902   }
903   ncmultiselector* ns = malloc(sizeof(*ns));
904   if(ns == NULL){
905     return NULL;
906   }
907   memset(ns, 0, sizeof(*ns));
908   ns->title = opts->title ? strdup(opts->title) : NULL;
909   ns->titlecols = opts->title ? ncstrwidth(opts->title, NULL, NULL) : 0;
910   ns->secondary = opts->secondary ? strdup(opts->secondary) : NULL;
911   ns->secondarycols = opts->secondary ? ncstrwidth(opts->secondary, NULL, NULL) : 0;
912   ns->footer = opts->footer ? strdup(opts->footer) : NULL;
913   ns->footercols = opts->footer ? ncstrwidth(opts->footer, NULL, NULL) : 0;
914   ns->current = 0;
915   ns->startdisp = 0;
916   ns->longitem = 0;
917   ns->maxdisplay = opts->maxdisplay;
918   ns->opchannels = opts->opchannels;
919   ns->boxchannels = opts->boxchannels;
920   ns->descchannels = opts->descchannels;
921   ns->titlechannels = opts->titlechannels;
922   ns->footchannels = opts->footchannels;
923   ns->boxchannels = opts->boxchannels;
924   ns->darrowy = ns->uarrowy = ns->arrowx = -1;
925   if(itemcount){
926     if(!(ns->items = malloc(sizeof(*ns->items) * itemcount))){
927       goto freeitems;
928     }
929   }else{
930     ns->items = NULL;
931   }
932   for(ns->itemcount = 0 ; ns->itemcount < itemcount ; ++ns->itemcount){
933     const struct ncmselector_item* src = &opts->items[ns->itemcount];
934     int unsafe = ncstrwidth(src->option, NULL, NULL);
935     if(unsafe < 0){
936       goto freeitems;
937     }
938     unsigned cols = unsafe;
939     if(cols > ns->longitem){
940       ns->longitem = cols;
941     }
942     unsafe = ncstrwidth(src->desc, NULL, NULL);
943     if(unsafe < 0){
944       goto freeitems;
945     }
946     unsigned cols2 = unsafe;
947     if(cols + cols2 > ns->longitem){
948       ns->longitem = cols + cols2;
949     }
950     ns->items[ns->itemcount].option = strdup(src->option);
951     ns->items[ns->itemcount].desc = strdup(src->desc);
952     ns->items[ns->itemcount].selected = src->selected;
953     if(!(ns->items[ns->itemcount].desc && ns->items[ns->itemcount].option)){
954       free(ns->items[ns->itemcount].option);
955       free(ns->items[ns->itemcount].desc);
956       goto freeitems;
957     }
958   }
959   unsigned dimy, dimx;
960   ns->ncp = n;
961   if(ncmultiselector_dim_yx(ns, &dimy, &dimx)){
962     goto freeitems;
963   }
964   if(ncplane_resize_simple(ns->ncp, dimy, dimx)){
965     goto freeitems;
966   }
967   if(ncplane_set_widget(ns->ncp, ns, (void(*)(void*))ncmultiselector_destroy)){
968     goto freeitems;
969   }
970   ncmultiselector_draw(ns); // deal with error here?
971   return ns;
972 
973 freeitems:
974   while(ns->itemcount--){
975     free(ns->items[ns->itemcount].option);
976     free(ns->items[ns->itemcount].desc);
977   }
978   free(ns->items);
979   free(ns->title); free(ns->secondary); free(ns->footer);
980   free(ns);
981   ncplane_destroy(n);
982   return NULL;
983 }
984 
ncmultiselector_destroy(ncmultiselector * n)985 void ncmultiselector_destroy(ncmultiselector* n){
986   if(n){
987     while(n->itemcount--){
988       free(n->items[n->itemcount].option);
989       free(n->items[n->itemcount].desc);
990     }
991     if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
992       ncplane_destroy(n->ncp);
993     }
994     free(n->items);
995     free(n->title);
996     free(n->secondary);
997     free(n->footer);
998     free(n);
999   }
1000 }
1001 
ncmultiselector_selected(ncmultiselector * n,bool * selected,unsigned count)1002 int ncmultiselector_selected(ncmultiselector* n, bool* selected, unsigned count){
1003   if(n->itemcount != count || n->itemcount < 1){
1004     return -1;
1005   }
1006   while(--count){
1007     selected[count] = n->items[count].selected;
1008   }
1009   return 0;
1010 }
1011