1 #include "internal.h"
2
3 // ncmenu_item and ncmenu_section have internal and (minimal) external forms
4 typedef struct ncmenu_int_item {
5 char* desc; // utf-8 menu item, NULL for horizontal separator
6 ncinput shortcut; // shortcut, all should be distinct
7 int shortcut_offset; // column offset with desc of shortcut EGC
8 char* shortdesc; // description of shortcut, can be NULL
9 int shortdesccols; // columns occupied by shortcut description
10 bool disabled; // disabled?
11 } ncmenu_int_item;
12
13 typedef struct ncmenu_int_section {
14 char* name; // utf-8 c string
15 unsigned itemcount;
16 ncmenu_int_item* items; // items, NULL iff itemcount == 0
17 ncinput shortcut; // shortcut, will be underlined if present in name
18 int xoff; // column offset from beginning of menu bar
19 int bodycols; // column width of longest item
20 int itemselected; // current item selected, -1 for no selection
21 int shortcut_offset; // column offset within name of shortcut EGC
22 int enabled_item_count; // number of enabled items: section is disabled iff 0
23 } ncmenu_int_section;
24
25 typedef struct ncmenu {
26 ncplane* ncp;
27 int sectioncount; // must be positive
28 ncmenu_int_section* sections; // NULL iff sectioncount == 0
29 int unrolledsection; // currently unrolled section, -1 if none
30 int headerwidth; // minimum space necessary to display all sections
31 uint64_t headerchannels; // styling for header
32 uint64_t dissectchannels; // styling for disabled section headers
33 uint64_t sectionchannels; // styling for sections
34 uint64_t disablechannels; // styling for disabled entries
35 bool bottom; // are we on the bottom (vs top)?
36 } ncmenu;
37
38 // Search the provided multibyte (UTF8) string 's' for the provided unicode
39 // codepoint 'cp'. If found, return the column offset of the EGC in which the
40 // codepoint appears in 'col', and the byte offset as the return value. If not
41 // found, -1 is returned, and 'col' is meaningless.
42 static int
mbstr_find_codepoint(const char * s,uint32_t cp,int * col)43 mbstr_find_codepoint(const char* s, uint32_t cp, int* col){
44 mbstate_t ps;
45 memset(&ps, 0, sizeof(ps));
46 size_t bytes = 0;
47 size_t r;
48 wchar_t w;
49 *col = 0;
50 while((r = mbrtowc(&w, s + bytes, MB_CUR_MAX, &ps)) != (size_t)-1 && r != (size_t)-2){
51 if(r == 0){
52 break;
53 }
54 if(towlower(cp) == towlower(w)){
55 return bytes;
56 }
57 *col += wcwidth(w);
58 bytes += r;
59 }
60 return -1;
61 }
62
63 static void
free_menu_section(ncmenu_int_section * ms)64 free_menu_section(ncmenu_int_section* ms){
65 for(unsigned i = 0 ; i < ms->itemcount ; ++i){
66 free(ms->items[i].desc);
67 free(ms->items[i].shortdesc);
68 }
69 free(ms->items);
70 free(ms->name);
71 }
72
73 static void
free_menu_sections(ncmenu * ncm)74 free_menu_sections(ncmenu* ncm){
75 for(int i = 0 ; i < ncm->sectioncount ; ++i){
76 free_menu_section(&ncm->sections[i]);
77 }
78 free(ncm->sections);
79 }
80
81 static int
dup_menu_item(ncmenu_int_item * dst,const struct ncmenu_item * src)82 dup_menu_item(ncmenu_int_item* dst, const struct ncmenu_item* src){
83 #define ALTMOD "Alt+"
84 #define CTLMOD "Ctrl+"
85 dst->disabled = false;
86 if((dst->desc = strdup(src->desc)) == NULL){
87 return -1;
88 }
89 if(!src->shortcut.id){
90 dst->shortdesccols = 0;
91 dst->shortdesc = NULL;
92 return 0;
93 }
94 size_t bytes = 1; // NUL terminator
95 if(src->shortcut.alt){
96 bytes += strlen(ALTMOD);
97 }
98 if(src->shortcut.ctrl){
99 bytes += strlen(CTLMOD);
100 }
101 mbstate_t ps;
102 memset(&ps, 0, sizeof(ps));
103 size_t shortsize = wcrtomb(NULL, src->shortcut.id, &ps);
104 if(shortsize == (size_t)-1){
105 free(dst->desc);
106 return -1;
107 }
108 bytes += shortsize + 1;
109 char* sdup = malloc(bytes);
110 int n = snprintf(sdup, bytes, "%s%s", src->shortcut.alt ? ALTMOD : "",
111 src->shortcut.ctrl ? CTLMOD : "");
112 if(n < 0 || (size_t)n >= bytes){
113 free(sdup);
114 free(dst->desc);
115 return -1;
116 }
117 memset(&ps, 0, sizeof(ps));
118 size_t mbbytes = wcrtomb(sdup + n, src->shortcut.id, &ps);
119 if(mbbytes == (size_t)-1){ // shouldn't happen
120 free(sdup);
121 free(dst->desc);
122 return -1;
123 }
124 sdup[n + mbbytes] = '\0';
125 dst->shortdesc = sdup;
126 dst->shortdesccols = ncstrwidth(dst->shortdesc, NULL, NULL);
127 return 0;
128 #undef CTLMOD
129 #undef ALTMOD
130 }
131
132 static int
dup_menu_section(ncmenu_int_section * dst,const struct ncmenu_section * src)133 dup_menu_section(ncmenu_int_section* dst, const struct ncmenu_section* src){
134 // we must reject any empty section
135 if(src->itemcount == 0 || src->items == NULL){
136 return -1;
137 }
138 dst->bodycols = 0;
139 dst->itemselected = 0;
140 dst->items = NULL;
141 // we must reject any section which is entirely separators
142 bool gotitem = false;
143 dst->itemcount = 0;
144 dst->enabled_item_count = 0;
145 dst->items = malloc(sizeof(*dst->items) * src->itemcount);
146 if(dst->items == NULL){
147 return -1;
148 }
149 for(int i = 0 ; i < src->itemcount ; ++i){
150 if(src->items[i].desc){
151 if(dup_menu_item(&dst->items[i], &src->items[i])){
152 while(i--){
153 free(dst->items[i].desc);
154 }
155 free(dst->items);
156 return -1;
157 }
158 gotitem = true;
159 int cols = ncstrwidth(dst->items[i].desc, NULL, NULL);
160 if(dst->items[i].shortdesc){
161 cols += 2 + dst->items[i].shortdesccols; // two spaces minimum
162 }
163 if(cols > dst->bodycols){
164 dst->bodycols = cols;
165 }
166 memcpy(&dst->items[i].shortcut, &src->items[i].shortcut, sizeof(dst->items[i].shortcut));
167 if(mbstr_find_codepoint(dst->items[i].desc,
168 dst->items[i].shortcut.id,
169 &dst->items[i].shortcut_offset) < 0){
170 dst->items[i].shortcut_offset = -1;
171 }
172 }else{
173 dst->items[i].desc = NULL;
174 dst->items[i].shortdesc = NULL;
175 }
176 ++dst->itemcount;
177 }
178 dst->enabled_item_count = dst->itemcount;
179 if(!gotitem){
180 while(dst->itemcount){
181 free(dst->items[--dst->itemcount].desc);
182 }
183 free(dst->items);
184 return -1;
185 }
186 return 0;
187 }
188
189 // Duplicates all menu sections in opts, adding their length to '*totalwidth'.
190 static int
dup_menu_sections(ncmenu * ncm,const ncmenu_options * opts,unsigned * totalwidth,unsigned * totalheight)191 dup_menu_sections(ncmenu* ncm, const ncmenu_options* opts, unsigned* totalwidth, unsigned* totalheight){
192 if(opts->sectioncount == 0){
193 return -1;
194 }
195 ncm->sections = malloc(sizeof(*ncm->sections) * opts->sectioncount);
196 if(ncm->sections == NULL){
197 return -1;
198 }
199 bool rightaligned = false; // can only right-align once. twice is error.
200 unsigned maxheight = 0;
201 unsigned maxwidth = *totalwidth;
202 unsigned xoff = 2;
203 int i;
204 for(i = 0 ; i < opts->sectioncount ; ++i){
205 if(opts->sections[i].name){
206 int cols = ncstrwidth(opts->sections[i].name, NULL, NULL);
207 if(rightaligned){ // FIXME handle more than one right-aligned section
208 ncm->sections[i].xoff = -(cols + 2);
209 }else{
210 ncm->sections[i].xoff = xoff;
211 }
212 if(cols < 0 || (ncm->sections[i].name = strdup(opts->sections[i].name)) == NULL){
213 goto err;
214 }
215 if(dup_menu_section(&ncm->sections[i], &opts->sections[i])){
216 free(ncm->sections[i].name);
217 goto err;
218 }
219 if(ncm->sections[i].itemcount > maxheight){
220 maxheight = ncm->sections[i].itemcount;
221 }
222 if(*totalwidth + cols + 2 > maxwidth){
223 maxwidth = *totalwidth + cols + 2;
224 }
225 if(*totalwidth + ncm->sections[i].bodycols + 2 > maxwidth){
226 maxwidth = *totalwidth + ncm->sections[i].bodycols + 2;
227 }
228 *totalwidth += cols + 2;
229 memcpy(&ncm->sections[i].shortcut, &opts->sections[i].shortcut, sizeof(ncm->sections[i].shortcut));
230 if(mbstr_find_codepoint(ncm->sections[i].name,
231 ncm->sections[i].shortcut.id,
232 &ncm->sections[i].shortcut_offset) < 0){
233 ncm->sections[i].shortcut_offset = -1;
234 }
235 xoff += cols + 2;
236 }else{ // divider; remaining sections are right-aligned
237 if(rightaligned){
238 goto err;
239 }
240 rightaligned = true;
241 ncm->sections[i].name = NULL;
242 ncm->sections[i].items = NULL;
243 ncm->sections[i].itemcount = 0;
244 ncm->sections[i].xoff = -1;
245 ncm->sections[i].bodycols = 0;
246 ncm->sections[i].itemselected = -1;
247 ncm->sections[i].shortcut_offset = -1;
248 ncm->sections[i].enabled_item_count = 0;
249 }
250 }
251 if(ncm->sectioncount == 1 && rightaligned){
252 goto err;
253 }
254 *totalwidth = maxwidth;
255 *totalheight += maxheight + 2; // two rows of border
256 return 0;
257
258 err:
259 while(i--){
260 free_menu_section(&ncm->sections[i]);
261 }
262 free(ncm->sections);
263 return -1;
264 }
265
266 // what section header, if any, is living at the provided x coordinate? solves
267 // by replaying the write_header() algorithm. returns -1 if no such section.
268 static int
section_x(const ncmenu * ncm,int x)269 section_x(const ncmenu* ncm, int x){
270 int dimx = ncplane_dim_x(ncm->ncp);
271 for(int i = 0 ; i < ncm->sectioncount ; ++i){
272 if(!ncm->sections[i].name){
273 continue;
274 }
275 if(ncm->sections[i].xoff < 0){ // right-aligned
276 int pos = dimx + ncm->sections[i].xoff;
277 if(x < pos){
278 break;
279 }
280 if(x < pos + ncstrwidth(ncm->sections[i].name, NULL, NULL)){
281 return i;
282 }
283 }else{
284 if(x < ncm->sections[i].xoff){
285 break;
286 }
287 if(x < ncm->sections[i].xoff + ncstrwidth(ncm->sections[i].name, NULL, NULL)){
288 return i;
289 }
290 }
291 }
292 return -1;
293 }
294
295 static int
write_header(ncmenu * ncm)296 write_header(ncmenu* ncm){
297 ncplane_set_channels(ncm->ncp, ncm->headerchannels);
298 unsigned dimy, dimx;
299 ncplane_dim_yx(ncm->ncp, &dimy, &dimx);
300 unsigned xoff = 0; // 2-column margin on left
301 int ypos = ncm->bottom ? dimy - 1 : 0;
302 if(ncplane_cursor_move_yx(ncm->ncp, ypos, 0)){
303 return -1;
304 }
305 nccell c = NCCELL_INITIALIZER(' ', 0, ncm->headerchannels);
306 ncplane_set_styles(ncm->ncp, 0);
307 if(ncplane_putc(ncm->ncp, &c) < 0){
308 return -1;
309 }
310 if(ncplane_putc(ncm->ncp, &c) < 0){
311 return -1;
312 }
313 for(int i = 0 ; i < ncm->sectioncount ; ++i){
314 if(ncm->sections[i].name){
315 ncplane_cursor_move_yx(ncm->ncp, ypos, xoff);
316 int spaces = ncm->sections[i].xoff - xoff;
317 if(ncm->sections[i].xoff < 0){ // right-aligned
318 spaces = dimx + ncm->sections[i].xoff - xoff;
319 if(spaces < 0){
320 spaces = 0;
321 }
322 }
323 xoff += spaces;
324 while(spaces--){
325 if(ncplane_putc(ncm->ncp, &c) < 0){
326 return -1;
327 }
328 }
329 if(ncm->sections[i].enabled_item_count <= 0){
330 ncplane_set_channels(ncm->ncp, ncm->dissectchannels);
331 }else{
332 ncplane_set_channels(ncm->ncp, ncm->headerchannels);
333 }
334 if(ncplane_putstr_yx(ncm->ncp, ypos, xoff, ncm->sections[i].name) < 0){
335 return -1;
336 }
337 if(ncm->sections[i].shortcut_offset >= 0){
338 nccell cl = NCCELL_TRIVIAL_INITIALIZER;
339 if(ncplane_at_yx_cell(ncm->ncp, ypos, xoff + ncm->sections[i].shortcut_offset, &cl) < 0){
340 return -1;
341 }
342 nccell_on_styles(&cl, NCSTYLE_UNDERLINE|NCSTYLE_BOLD);
343 if(ncplane_putc_yx(ncm->ncp, ypos, xoff + ncm->sections[i].shortcut_offset, &cl) < 0){
344 return -1;
345 }
346 nccell_release(ncm->ncp, &cl);
347 }
348 xoff += ncstrwidth(ncm->sections[i].name, NULL, NULL);
349 }
350 }
351 while(xoff < dimx){
352 if(ncplane_putc_yx(ncm->ncp, ypos, xoff, &c) < 0){
353 return -1;
354 }
355 ++xoff;
356 }
357 return 0;
358 }
359
360 static int
resize_menu(ncplane * n)361 resize_menu(ncplane* n){
362 const ncplane* parent = ncplane_parent_const(n);
363 int dimx = ncplane_dim_x(parent);
364 int dimy = ncplane_dim_y(n);
365 if(ncplane_resize_simple(n, dimy, dimx)){
366 return -1;
367 }
368 ncmenu* menu = ncplane_userptr(n);
369 int unrolled = menu->unrolledsection;
370 if(unrolled < 0){
371 return write_header(menu);
372 }
373 ncplane_erase(n); // "rolls up" section without resetting unrolledsection
374 return ncmenu_unroll(menu, unrolled);
375 }
376
ncmenu_create(ncplane * n,const ncmenu_options * opts)377 ncmenu* ncmenu_create(ncplane* n, const ncmenu_options* opts){
378 ncmenu_options zeroed = {};
379 if(!opts){
380 opts = &zeroed;
381 }
382 if(opts->sectioncount <= 0 || !opts->sections){
383 logerror("Invalid %d-ary section information\n", opts->sectioncount);
384 return NULL;
385 }
386 if(opts->flags >= (NCMENU_OPTION_HIDING << 1u)){
387 logwarn("Provided unsupported flags %016" PRIx64 "\n", opts->flags);
388 }
389 unsigned totalheight = 1;
390 unsigned totalwidth = 2; // start with two-character margin on the left
391 ncmenu* ret = malloc(sizeof(*ret));
392 ret->sectioncount = opts->sectioncount;
393 ret->sections = NULL;
394 unsigned dimy, dimx;
395 ncplane_dim_yx(n, &dimy, &dimx);
396 if(ret){
397 ret->bottom = !!(opts->flags & NCMENU_OPTION_BOTTOM);
398 if(dup_menu_sections(ret, opts, &totalwidth, &totalheight) == 0){
399 ret->headerwidth = totalwidth;
400 if(totalwidth < dimx){
401 totalwidth = dimx;
402 }
403 struct ncplane_options nopts = {
404 .y = ret->bottom ? dimy - totalheight : 0,
405 .x = 0,
406 .rows = totalheight,
407 .cols = totalwidth,
408 .userptr = ret,
409 .name = "menu",
410 .resizecb = resize_menu,
411 .flags = NCPLANE_OPTION_FIXED,
412 };
413 ret->ncp = ncplane_create(n, &nopts);
414 if(ret->ncp){
415 if(ncplane_set_widget(ret->ncp, ret, (void(*)(void*))ncmenu_destroy) == 0){
416 ret->unrolledsection = -1;
417 ret->headerchannels = opts->headerchannels;
418 ret->dissectchannels = opts->headerchannels;
419 ncchannels_set_fg_rgb(&ret->dissectchannels, 0xdddddd);
420 ret->sectionchannels = opts->sectionchannels;
421 ret->disablechannels = ret->sectionchannels;
422 ncchannels_set_fg_rgb(&ret->disablechannels, 0xdddddd);
423 nccell c = NCCELL_TRIVIAL_INITIALIZER;
424 nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT);
425 nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT);
426 ncplane_set_base_cell(ret->ncp, &c);
427 nccell_release(ret->ncp, &c);
428 if(write_header(ret) == 0){
429 return ret;
430 }
431 }
432 ncplane_destroy(ret->ncp);
433 }
434 free_menu_sections(ret);
435 }
436 free(ret);
437 }
438 logerror("Error creating ncmenu\n");
439 return NULL;
440 }
441
442 static inline int
section_height(const ncmenu * n,int sectionidx)443 section_height(const ncmenu* n, int sectionidx){
444 return n->sections[sectionidx].itemcount + 2;
445 }
446
447 static inline int
section_width(const ncmenu * n,int sectionidx)448 section_width(const ncmenu* n, int sectionidx){
449 return n->sections[sectionidx].bodycols + 2;
450 }
451
ncmenu_unroll(ncmenu * n,int sectionidx)452 int ncmenu_unroll(ncmenu* n, int sectionidx){
453 if(ncmenu_rollup(n)){ // roll up any unrolled section
454 return -1;
455 }
456 if(sectionidx < 0 || sectionidx >= n->sectioncount){
457 logerror("Unrolled invalid sectionidx %d\n", sectionidx);
458 return -1;
459 }
460 if(n->sections[sectionidx].enabled_item_count <= 0){
461 return 0;
462 }
463 if(n->sections[sectionidx].name == NULL){
464 return -1;
465 }
466 n->unrolledsection = sectionidx;
467 unsigned dimy, dimx;
468 ncplane_dim_yx(n->ncp, &dimy, &dimx);
469 const int height = section_height(n, sectionidx);
470 const int width = section_width(n, sectionidx);
471 int xpos = n->sections[sectionidx].xoff < 0 ?
472 (int)dimx + (n->sections[sectionidx].xoff - 2) : n->sections[sectionidx].xoff;
473 if(xpos + width >= (int)dimx){
474 xpos = dimx - (width + 2);
475 }
476 int ypos = n->bottom ? dimy - height - 1 : 1;
477 if(ncplane_cursor_move_yx(n->ncp, ypos, xpos)){
478 return -1;
479 }
480 if(ncplane_rounded_box_sized(n->ncp, 0, n->headerchannels, height, width, 0)){
481 return -1;
482 }
483 const ncmenu_int_section* sec = &n->sections[sectionidx];
484 for(unsigned i = 0 ; i < sec->itemcount ; ++i){
485 ++ypos;
486 if(sec->items[i].desc){
487 // FIXME the user ought be able to configure the disabled channel
488 if(!sec->items[i].disabled){
489 ncplane_set_channels(n->ncp, n->sectionchannels);
490 }else{
491 ncplane_set_channels(n->ncp, n->disablechannels);
492 }
493 if(sec->itemselected >= 0){
494 if(i == (unsigned)sec->itemselected){
495 ncplane_set_channels(n->ncp, ncchannels_reverse(ncplane_channels(n->ncp)));
496 }
497 }
498 ncplane_set_styles(n->ncp, 0);
499 int cols = ncplane_putstr_yx(n->ncp, ypos, xpos + 1, sec->items[i].desc);
500 if(cols < 0){
501 return -1;
502 }
503 // we need pad out the remaining columns of this line with spaces. if
504 // there's a shortcut description, we align it to the right, printing
505 // spaces only through the start of the aligned description.
506 int thiswidth = width;
507 if(sec->items[i].shortdesc){
508 thiswidth -= sec->items[i].shortdesccols;
509 }
510 // print any necessary padding spaces
511 for(int j = cols + 1 ; j < thiswidth - 1 ; ++j){
512 if(ncplane_putchar(n->ncp, ' ') < 0){
513 return -1;
514 }
515 }
516 if(sec->items[i].shortdesc){
517 if(ncplane_putstr(n->ncp, sec->items[i].shortdesc) < 0){
518 return -1;
519 }
520 }
521 if(sec->items[i].shortcut_offset >= 0){
522 nccell cl = NCCELL_TRIVIAL_INITIALIZER;
523 if(ncplane_at_yx_cell(n->ncp, ypos, xpos + 1 + sec->items[i].shortcut_offset, &cl) < 0){
524 return -1;
525 }
526 nccell_on_styles(&cl, NCSTYLE_UNDERLINE|NCSTYLE_BOLD);
527 if(ncplane_putc_yx(n->ncp, ypos, xpos + 1 + sec->items[i].shortcut_offset, &cl) < 0){
528 return -1;
529 }
530 nccell_release(n->ncp, &cl);
531 }
532 }else{
533 n->ncp->channels = n->headerchannels;
534 ncplane_set_styles(n->ncp, 0);
535 if(ncplane_putegc_yx(n->ncp, ypos, xpos, "├", NULL) < 0){
536 return -1;
537 }
538 for(int j = 1 ; j < width - 1 ; ++j){
539 if(ncplane_putegc(n->ncp, "─", NULL) < 0){
540 return -1;
541 }
542 }
543 if(ncplane_putegc(n->ncp, "┤", NULL) < 0){
544 return -1;
545 }
546 }
547 }
548 return 0;
549 }
550
ncmenu_rollup(ncmenu * n)551 int ncmenu_rollup(ncmenu* n){
552 if(n->unrolledsection < 0){
553 return 0;
554 }
555 n->unrolledsection = -1;
556 ncplane_erase(n->ncp);
557 return write_header(n);
558 }
559
ncmenu_nextsection(ncmenu * n)560 int ncmenu_nextsection(ncmenu* n){
561 int nextsection = n->unrolledsection;
562 // FIXME probably best to detect cycles
563 do{
564 if(++nextsection == n->sectioncount){
565 nextsection = 0;
566 }
567 }while(n->sections[nextsection].name == NULL ||
568 n->sections[nextsection].enabled_item_count == 0);
569 return ncmenu_unroll(n, nextsection);
570 }
571
ncmenu_prevsection(ncmenu * n)572 int ncmenu_prevsection(ncmenu* n){
573 int prevsection = n->unrolledsection;
574 // FIXME probably best to detect cycles
575 do{
576 if(--prevsection < 0){
577 prevsection = n->sectioncount - 1;
578 }
579 }while(n->sections[prevsection].name == NULL ||
580 n->sections[prevsection].enabled_item_count == 0);
581 return ncmenu_unroll(n, prevsection);
582 }
583
ncmenu_nextitem(ncmenu * n)584 int ncmenu_nextitem(ncmenu* n){
585 if(n->unrolledsection == -1){
586 if(ncmenu_unroll(n, 0)){
587 return -1;
588 }
589 }
590 ncmenu_int_section* sec = &n->sections[n->unrolledsection];
591 // FIXME probably best to detect cycles
592 do{
593 if((unsigned)++sec->itemselected == sec->itemcount){
594 sec->itemselected = 0;
595 }
596 }while(!sec->items[sec->itemselected].desc || sec->items[sec->itemselected].disabled);
597 return ncmenu_unroll(n, n->unrolledsection);
598 }
599
ncmenu_previtem(ncmenu * n)600 int ncmenu_previtem(ncmenu* n){
601 if(n->unrolledsection == -1){
602 if(ncmenu_unroll(n, 0)){
603 return -1;
604 }
605 }
606 ncmenu_int_section* sec = &n->sections[n->unrolledsection];
607 // FIXME probably best to detect cycles
608 do{
609 if(sec->itemselected-- == 0){
610 sec->itemselected = sec->itemcount - 1;
611 }
612 }while(!sec->items[sec->itemselected].desc || sec->items[sec->itemselected].disabled);
613 return ncmenu_unroll(n, n->unrolledsection);
614 }
615
ncmenu_selected(const ncmenu * n,ncinput * ni)616 const char* ncmenu_selected(const ncmenu* n, ncinput* ni){
617 if(n->unrolledsection < 0){
618 return NULL;
619 }
620 const struct ncmenu_int_section* sec = &n->sections[n->unrolledsection];
621 const int itemidx = sec->itemselected;
622 if(ni){
623 memcpy(ni, &sec->items[itemidx].shortcut, sizeof(*ni));
624 }
625 return sec->items[itemidx].desc;
626 }
627
ncmenu_mouse_selected(const ncmenu * n,const ncinput * click,ncinput * ni)628 const char* ncmenu_mouse_selected(const ncmenu* n, const ncinput* click,
629 ncinput* ni){
630 if(click->id != NCKEY_BUTTON1){
631 return NULL;
632 }
633 if(click->evtype != NCTYPE_RELEASE){
634 return NULL;
635 }
636 struct ncplane* nc = n->ncp;
637 int y = click->y;
638 int x = click->x;
639 unsigned dimy, dimx;
640 ncplane_dim_yx(nc, &dimy, &dimx);
641 if(!ncplane_translate_abs(nc, &y, &x)){
642 return NULL;
643 }
644 // FIXME section_x() works only off the section header lengths, meaning that
645 // if we click an item outside of those columns covered by the header, it will
646 // read as a -1 from section_x(). we want to instead get the unrolled section,
647 // find its boundaries, and verify that we are within them.
648 int i = section_x(n, x);
649 if(i < 0 || i != n->unrolledsection){
650 return NULL;
651 }
652 const struct ncmenu_int_section* sec = &n->sections[n->unrolledsection];
653 if(y < 2 || (unsigned)y - 2 >= sec->itemcount){
654 return NULL;
655 }
656 const int itemidx = y - 2;
657 if(ni){
658 memcpy(ni, &sec->items[itemidx].shortcut, sizeof(*ni));
659 }
660 return sec->items[itemidx].desc;
661 }
662
ncmenu_offer_input(ncmenu * n,const ncinput * nc)663 bool ncmenu_offer_input(ncmenu* n, const ncinput* nc){
664 // we can't actually select menu items in this function, since we need to
665 // invoke an arbitrary function as a result.
666 if(nc->id == NCKEY_BUTTON1 && nc->evtype == NCTYPE_RELEASE){
667 int y = nc->y;
668 int x = nc->x;
669 unsigned dimy, dimx;
670 ncplane_dim_yx(n->ncp, &dimy, &dimx);
671 if(!ncplane_translate_abs(n->ncp, &y, &x)){
672 return false;
673 }
674 if(y != (n->bottom ? (int)dimy - 1 : 0)){
675 return false;
676 }
677 int i = section_x(n, x);
678 if(i < 0 || i == n->unrolledsection){
679 ncmenu_rollup(n);
680 }else{
681 ncmenu_unroll(n, i);
682 }
683 return true;
684 }else if(nc->evtype == NCTYPE_RELEASE){
685 return false;
686 }
687 for(int si = 0 ; si < n->sectioncount ; ++si){
688 const ncmenu_int_section* sec = &n->sections[si];
689 if(sec->enabled_item_count == 0){
690 continue;
691 }
692 if(!ncinput_equal_p(&sec->shortcut, nc)){
693 continue;
694 }
695 ncmenu_unroll(n, si);
696 return true;
697 }
698 if(n->unrolledsection < 0){ // all following need an unrolled section
699 return false;
700 }
701 if(nc->id == NCKEY_LEFT){
702 if(ncmenu_prevsection(n)){
703 return false;
704 }
705 return true;
706 }else if(nc->id == NCKEY_RIGHT){
707 if(ncmenu_nextsection(n)){
708 return false;
709 }
710 return true;
711 }else if(nc->id == NCKEY_UP || nc->id == NCKEY_SCROLL_UP){
712 if(ncmenu_previtem(n)){
713 return false;
714 }
715 return true;
716 }else if(nc->id == NCKEY_DOWN || nc->id == NCKEY_SCROLL_DOWN){
717 if(ncmenu_nextitem(n)){
718 return false;
719 }
720 return true;
721 }else if(nc->id == NCKEY_ESC){
722 ncmenu_rollup(n);
723 return true;
724 }
725 return false;
726 }
727
728 // FIXME we probably ought implement this with a trie or something
ncmenu_item_set_status(ncmenu * n,const char * section,const char * item,bool enabled)729 int ncmenu_item_set_status(ncmenu* n, const char* section, const char* item,
730 bool enabled){
731 for(int si = 0 ; si < n->sectioncount ; ++si){
732 struct ncmenu_int_section* sec = &n->sections[si];
733 if(strcmp(sec->name, section) == 0){
734 for(unsigned ii = 0 ; ii < sec->itemcount ; ++ii){
735 struct ncmenu_int_item* i = &sec->items[ii];
736 if(strcmp(i->desc, item) == 0){
737 const bool changed = i->disabled == enabled;
738 i->disabled = !enabled;
739 if(changed){
740 if(i->disabled){
741 if(--sec->enabled_item_count == 0){
742 write_header(n);
743 }
744 }else{
745 if(++sec->enabled_item_count == 1){
746 write_header(n);
747 }
748 }
749 if(n->unrolledsection == si){
750 if(sec->enabled_item_count == 0){
751 ncmenu_rollup(n);
752 }else{
753 ncmenu_unroll(n, n->unrolledsection);
754 }
755 }
756 }
757 return 0;
758 }
759 }
760 break;
761 }
762 }
763 return -1;
764 }
765
ncmenu_plane(ncmenu * menu)766 ncplane* ncmenu_plane(ncmenu* menu){
767 return menu->ncp;
768 }
769
ncmenu_destroy(ncmenu * n)770 void ncmenu_destroy(ncmenu* n){
771 if(n){
772 free_menu_sections(n);
773 if(ncplane_set_widget(n->ncp, NULL, NULL) == 0){
774 ncplane_destroy(n->ncp);
775 }
776 free(n);
777 }
778 }
779