The nctree widget, PoC, and unitt rs #1164.
pull/1365/head
Nick Black 3 years ago committed by GitHub
parent 11dbf7701e
commit e30b12a480
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -41,6 +41,9 @@ might surprise NCURSES programmers:
* Notcurses has no support for soft labels (`slk_init()`, etc.), subwindows
which share memory with their parents, nor the NCURSES tracing functionality
(`trace(3NCURSES)`).
* Notcurses doesn't implement any of the `curs_util(3x)` functions, including
window serialization/deserialization via `putwin()`/`getwin()`.
* Notcurses doesn't interact with `LINES` nor `COLUMNS` environment variables.
## Adapting NCURSES programs
@ -80,3 +83,19 @@ compat_mvwprintw(struct ncplane* nc, int y, int x, const char* fmt, ...){
These are pretty obvious, implementation-wise.
### Some details
* `cbreak()`/`nocbreak()`/`echo()`/`noecho()`/`nl()`/`nonl()`: termios
properties are not exposed as granularly by Notcurses. Rendered mode
always enters cbreak mode. Direct mode enters cbreak mode by default,
but `NCDIRECT_OPTION_INHIBIT_CBREAK` will inhibit this.
* `raw()`/`noraw()`: The line discipline conversions (e.g. Ctrl+C) can be
disabled at any time with `notcurses_linesigs_disable()`, and turned back on
with `notcurses_linesigs_enable()`.
* `keypad()`: The keypad is always enabled, if the `smkx`
capability is advertised.
* `halfdelay()`/`nodelay()`/`timeout()`/`wtimeout()`: No such global controls
are supported. Use `notcurses_getc()` with a timeout if you want a timeout.
Use `notcurses_getc_nblock()` if you want an immediate return.
* `intrflush()`/`qiflush()`/`noqiflush()`: No such functionality is supported.
* `typeahead()`: No such functionality is supported.

@ -0,0 +1,96 @@
% notcurses_tree(3)
% nick black <nickblack@linux.com>
% v2.2.1
# NAME
notcurses_tree - high-level hierarchical line-based data
# SYNOPSIS
**#include <notcurses/notcurses.h>**
```c
struct nctree;
struct ncplane;
typedef struct nctree_item {
void* curry;
struct nctree_item* subs;
unsigned subcount;
} nctree_item;
typedef struct nctree_options {
const nctree_item* items; // top-level nctree_item array
unsigned count; // size of |items|
uint64_t bchannels; // base channels
int (*nctreecb)(struct ncplane*, void*, int); // item callback
int indentcols; // columns to indent per hierarchy
uint64_t flags; // bitfield of NCTREE_OPTION_*
} nctree_options;
```
**struct nctree* nctree_create(struct ncplane* ***n***, const nctree_options* ***opts***);**
**struct ncplane* nctree_plane(struct nctree* ***n***);**
**int nctree_redraw(struct nctree* ***n***);**
**bool nctree_offer_input(struct nctree* ***n***, const ncinput* ***ni***);**
**void* nctree_focused(struct nctree* ***n***);**
**void* nctree_next(struct nctree* ***n***);**
**void* nctree_prev(struct nctree* ***n***);**
**void* nctree_goto(struct nctree* ***n***, const int* ***spec***, size_t ***specdepth***, int* ***failspec***);**
**void nctree_destroy(struct nctree* ***n***);**
# DESCRIPTION
**nctree**s organize static hierarchical items, and allow them to be browsed.
Each item can have arbitrary subitems. Items can be collapsed and expanded.
The display supports scrolling and searching. Items cannot be added or removed,
however; they must be provided in their entirety at creation time.
An **nctree** cannot be empty. **count** must be non-zero, and **items** must
not be **NULL**. The callback function **nctreecb** must also be non-**NULL**.
The callback function **nctreecb** is called on tree items when the tree is
redrawn. It will be called on each visible item, and any item which has become
hidden. If the item is newly hidden, the **ncplane** argument will be **NULL**.
If the item is newly visible, the **ncplane** argument will be an empty plane.
If the item was already visible, the **ncplane** argument will be the same
plane passed before. If the item has not changed since the last time the
callback was invoked, there is no need to change the plane, and the callback
can return immediately. Otherwise, the plane ought be drawn by the callback.
Any unused rows ought be trimmed away using **ncplane_resize**. If the plane
is expanded in the callback, it will be shrunk back down by the widget. The
**int** parameter indicates distance from the focused item. If the parameter
is negative, the item is before the focused item; a positive parameter implies
that the item follows the focused item; the focused item itself is passed zero.
# RETURN VALUES
**nctree_create** returns **NULL** for invalid options. This includes a **NULL**
**items** or **nctreecb** field, or a zero **count** field.
**nctree_next** and **nctree_prev** both return the **curry** pointer from the
newly-focused item. **nctree_focused** returns the **curry** pointer from the
already-focused item.
# NOTES
**nctree** shares many properties with **notcurses_reel**. Unlike the latter,
**nctree**s support arbitrary hierarchical levels, but they do not allow
elements to come and go across the lifetime of the widget.
# SEE ALSO
**notcurses(3)**,
**notcurses_input(3)**,
**notcurses_plane(3)**,
**notcurses_reel(3)**

@ -2997,6 +2997,73 @@ API bool ncmultiselector_offer_input(struct ncmultiselector* n, const ncinput* n
// Destroy the ncmultiselector.
API void ncmultiselector_destroy(struct ncmultiselector* n);
// nctree widget -- a vertical browser supporting line-based hierarchies.
//
// each item can have subitems, and has a curry. there is one callback for the
// entirety of the nctree. visible items have the callback invoked upon their
// curry and an ncplane. the ncplane can be reused across multiple invocations
// of the callback.
// each item has a curry, and zero or more subitems.
typedef struct nctree_item {
void* curry;
struct nctree_item* subs;
unsigned subcount;
} nctree_item;
typedef struct nctree_options {
const nctree_item* items; // top-level nctree_item array
unsigned count; // size of |items|
uint64_t bchannels; // base channels
int (*nctreecb)(struct ncplane*, void*, int); // item callback function
int indentcols; // columns to indent per level of hierarchy
uint64_t flags; // bitfield of NCTREE_OPTION_*
} nctree_options;
// |opts| may *not* be NULL, since it is necessary to define a callback
// function.
API ALLOC struct nctree* nctree_create(struct ncplane* n, const nctree_options* opts)
__attribute__ ((nonnull (1, 2)));
// Returns the ncplane on which this nctree lives.
API struct ncplane* nctree_plane(struct nctree* n)
__attribute__ ((nonnull (1)));
// Redraw the nctree 'n' in its entirety. The tree will be cleared, and items
// will be lain out, using the focused item as a fulcrum. Item-drawing
// callbacks will be invoked for each visible item.
API int nctree_redraw(struct nctree* n)
__attribute__ ((nonnull (1)));
// Offer input 'ni' to the nctree 'n'. If it's relevant, this function returns
// true, and the input ought not be processed further. If it's irrelevant to
// the tree, false is returned. Relevant inputs include:
// * a mouse click on an item (focuses item)
// * a mouse scrollwheel event (srolls tree)
// * up, down, pgup, or pgdown (navigates among items)
API bool nctree_offer_input(struct nctree* n, const ncinput* ni)
__attribute__ ((nonnull (1, 2)));
// Return the focused item, if any items are present. This is not a copy;
// be careful to use it only for the duration of a critical section.
API void* nctree_focused(struct nctree* n) __attribute__ ((nonnull (1)));
// Change focus to the next item.
API void* nctree_next(struct nctree* n) __attribute__ ((nonnull (1)));
// Change focus to the previous item.
API void* nctree_prev(struct nctree* n) __attribute__ ((nonnull (1)));
// Go to the item specified by the array |spec|, terminated by UINT_MAX. If
// the spec is invalid, NULL is returned, and the depth of the first invalid
// spec is written to *|failspec|. Otherwise, the true depth is written to
// *|failspec|, and the curry is returned (|failspec| is necessary because the
// curry could itself be NULL).
API void* nctree_goto(struct nctree* n, const unsigned* spec, int* failspec);
// Destroy the nctree.
API void nctree_destroy(struct nctree* n);
// Menus. Horizontal menu bars are supported, on the top and/or bottom rows.
// If the menu bar is longer than the screen, it will be only partially
// visible. Menus may be either visible or invisible by default. In the event of

@ -0,0 +1,388 @@
#include "internal.h"
// these are never allocated themselves, but always as arrays of object
typedef struct nctree_int_item {
void* curry;
ncplane* ncp;
unsigned subcount;
struct nctree_int_item* subs;
} nctree_int_item;
typedef struct nctree {
int (*cbfxn)(ncplane*, void*, int);
nctree_int_item items; // topmost set of items, holds widget plane
nctree_int_item* curitem; // item addressed by the path
unsigned maxdepth; // maximum hierarchy level
unsigned* currentpath; // array of |maxdepth|+1 elements, ended by UINT_MAX
int activerow; // active row 0 <= activerow < dimy
int indentcols; // cols to indent per level
uint64_t bchannels; // border glyph channels
} nctree;
// recursively free an array of nctree_int_item; nctree_int_item structs are
// never individually free()d, just their innards
static void
free_tree_items(nctree_int_item* iarray){
for(unsigned c = 0 ; c < iarray->subcount ; ++c){
free_tree_items(&iarray->subs[c]);
}
ncplane_destroy(iarray->ncp);
free(iarray->subs);
}
// allocates a |count|-sized array of nctree_int_items, and fills |fill| in,
// using |items|. updates |*maxdepth| when appropriate.
static int
dup_tree_items(nctree_int_item* fill, const nctree_item* items, unsigned count, unsigned depth,
unsigned* maxdepth){
fill->subcount = count;
// FIXME perhaps better to alloc a page at a time and take all items from
// there, for better TLB performance?
fill->subs = malloc(sizeof(*fill->subs) * count);
if(fill->subs == NULL){
return -1;
}
for(unsigned c = 0 ; c < fill->subcount ; ++c){
nctree_int_item* nii = &fill->subs[c];
nii->curry = items[c].curry;
if(nii->curry == NULL){
while(c--){
free_tree_items(&fill->subs[c]);
}
free(fill->subs);
return -1;
}
nii->ncp = NULL;
if(dup_tree_items(nii, items[c].subs, items[c].subcount, depth + 1, maxdepth)){
while(c--){
free_tree_items(&fill->subs[c]);
}
free(fill->subs);
return -1;
}
}
if(depth > *maxdepth){
*maxdepth = depth;
}
return 0;
}
static void
goto_first_item(nctree* n){
n->currentpath[0] = 0;
n->currentpath[1] = UINT_MAX;
n->curitem = &n->items.subs[0];
n->activerow = 0;
}
// the initial path ought point to the first item.
static int
prep_initial_path(nctree* n, unsigned maxdepth){
n->currentpath = malloc(sizeof(*n->currentpath) * (maxdepth + 1));
if(n->currentpath == NULL){
return -1;
}
goto_first_item(n);
return 0;
}
static nctree*
nctree_inner_create(ncplane* n, const struct nctree_options* opts){
nctree* ret = malloc(sizeof(*ret));
if(ret){
ret->bchannels = opts->bchannels;
ret->cbfxn = opts->nctreecb;
ret->indentcols = opts->indentcols;
ret->maxdepth = 0;
if(dup_tree_items(&ret->items, opts->items, opts->count, 0, &ret->maxdepth)){
free(ret);
return NULL;
}
//fprintf(stderr, "MAXDEPTH: %u\n", ret->maxdepth);
if(prep_initial_path(ret, ret->maxdepth)){
free_tree_items(&ret->items);
free(ret);
return NULL;
}
ret->items.ncp = n;
ret->items.curry = NULL;
nctree_redraw(ret);
}
return ret;
}
nctree* nctree_create(ncplane* n, const struct nctree_options* opts){
notcurses* nc = ncplane_notcurses(n);
if(opts->flags){
logwarn(nc, "Passed invalid flags 0x%016jx\n", (uint64_t)opts->flags);
}
if(opts->count == 0 || opts->items == NULL){
logerror(nc, "Can't create empty tree\n");
goto error;
}
if(opts->nctreecb == NULL){
logerror(nc, "Can't use NULL callback\n");
goto error;
}
if(opts->indentcols < 0){
logerror(nc, "Can't indent negative columns\n");
goto error;
}
nctree* ret = nctree_inner_create(n, opts);
if(ret == NULL){
logerror(nc, "Couldn't prepare nctree\n");
goto error;
}
return ret;
error:
ncplane_destroy(n);
return NULL;
}
void nctree_destroy(nctree* n){
if(n){
free_tree_items(&n->items);
free(n);
}
}
// Returns the ncplane on which this nctree lives.
ncplane* nctree_plane(nctree* n){
return n->items.ncp;
}
// the prev is either:
// the item to the left, if the last path component is 0, or
// a drop from the rightmost non-zero path component, extended out to the right, or
// the current item
// so we can always just go to the last path component, act there, and possibly
// extend it out to the maximal topright.
static nctree_int_item*
nctree_prev_internal(nctree* n, unsigned* newpath){
nctree_int_item* nii = &n->items;
nctree_int_item* wedge = NULL; // tracks the rightmost non-zero path
int idx = 0;
while(newpath[idx] != UINT_MAX){
nii = &nii->subs[newpath[idx]];
if(idx == 0){
wedge = &n->items;
}else{// if(idx > 1){
wedge = &wedge->subs[newpath[idx - 1]];
}
++idx;
}
--idx;
if(newpath[idx]){
--newpath[idx];
nii = &wedge->subs[newpath[idx]];
++idx;
//fprintf(stderr, "nii->subcount: %u idx: %d\n", nii->subcount, idx);
while(nii->subcount){
newpath[idx] = nii->subcount - 1;
nii = &nii->subs[newpath[idx]];
++idx;
//fprintf(stderr, "nii->subcount: %u idx: %d\n", nii->subcount, idx);
}
newpath[idx] = UINT_MAX;
return nii;
}
if(wedge == &n->items){
return nii; // no change
}
newpath[idx] = UINT_MAX;
return wedge;
}
void* nctree_prev(nctree* n){
n->curitem = nctree_prev_internal(n, n->currentpath);
return n->curitem->curry;
}
// the next is either:
// - an extension to the right, if subs are available, or
// - a bump to the rightmost path component with subcount available, or
// - the current item
static nctree_int_item*
nctree_next_internal(nctree* n, unsigned* newpath){
nctree_int_item* nii = &n->items;
nctree_int_item* wedge = NULL; // tracks the rightmost with room in subs
int idx = 0;
int wedidx = 0;
while(newpath[idx] != UINT_MAX){
if(newpath[idx] < nii->subcount - 1){
wedge = nii;
wedidx = idx;
}
nii = &nii->subs[newpath[idx]];
++idx;
}
if(nii->subcount){
newpath[idx] = 0;
newpath[idx + 1] = UINT_MAX;
return &nii->subs[newpath[idx]];
}
if(wedge){
++newpath[wedidx];
newpath[wedidx + 1] = UINT_MAX;
return &wedge->subs[newpath[wedidx]];
}
return nii;
}
void* nctree_next(nctree* n){
// FIXME update n->activerow, redraw
n->curitem = nctree_next_internal(n, n->currentpath);
return n->curitem->curry;
}
static int
tree_path_length(const unsigned* path){
int len = 0;
while(path[len] != UINT_MAX){
++len;
}
return len;
}
// draw the item. if *|frontiert| == *|frontierb|, we're the current item, and
// can use all the available space. if *|frontiert| < 0, draw down from
// *|frontierb|. otherwise, draw up from *|frontiert|.
static int
draw_tree_item(nctree* n, nctree_int_item* nii, const unsigned* path,
int* frontiert, int* frontierb){
//fprintf(stderr, "drawing item ft: %d fb: %d %p\n", *frontiert, *frontierb, nii->ncp);
if(!nii->ncp){
const int startx = (tree_path_length(path) - 1) * n->indentcols;
int ymin, ymax;
if(*frontiert == *frontierb){
ymin = 0;
ymax = ncplane_dim_y(n->items.ncp) - 1;
}else if(*frontiert < 0){
ymin = *frontierb;
ymax = ncplane_dim_y(n->items.ncp) - 1;
}else{
ymin = 0;
ymax = *frontiert;
}
//fprintf(stderr, "x: %d y: %d\n", startx, ymin);
struct ncplane_options nopts = {
.x = startx,
.y = ymin,
.cols = ncplane_dim_x(n->items.ncp) - startx,
.rows = ymax - ymin + 1,
.userptr = NULL,
.name = NULL,
.resizecb = NULL,
.flags = 0,
};
nii->ncp = ncplane_create(n->items.ncp, &nopts);
if(nii->ncp == NULL){
return -1;
}
}else{
// FIXME move and possibly enlarge nii->ncp
}
int ret = n->cbfxn(nii->ncp, nii->curry, 0); // FIXME third param
if(ret < 0){
return -1;
}
// FIXME shrink plane if it was enlarged
//fprintf(stderr, "ft: %d fb: %d %p ncplane_y: %d\n", *frontiert, *frontierb, nii->ncp, ncplane_y(nii->ncp));
if(ncplane_y(nii->ncp) <= *frontiert){
*frontiert = ncplane_y(nii->ncp) - 1;
}
if(ncplane_y(nii->ncp) + ncplane_dim_y(nii->ncp) > *frontierb){
*frontierb = ncplane_y(nii->ncp) + ncplane_dim_y(nii->ncp);
}
return 0;
}
// tmppath ought be initialized with currentpath, but having size sufficient
// to hold n->maxdepth + 1 unsigneds.
static int
nctree_inner_redraw(nctree* n, unsigned* tmppath){
ncplane* ncp = n->items.ncp;
if(ncplane_cursor_move_yx(ncp, n->activerow, 0)){
return -1;
}
int frontiert = n->activerow;
int frontierb = n->activerow;
nctree_int_item* nii = n->curitem;
if(draw_tree_item(n, nii, tmppath, &frontiert, &frontierb)){
return -1;
}
nctree_int_item* tmpnii;
// draw items above the current one FIXME
while(frontiert >= 0){
if((tmpnii = nctree_prev_internal(n, tmppath)) == nii){
break;
}
nii = tmpnii;
if(draw_tree_item(n, nii, tmppath, &frontiert, &frontierb)){
return -1;
}
}
// FIXME destroy any drawn ones before us
// move items up if there is a gap at the top FIXME
if(frontiert >= 0){
}
n->activerow = ncplane_y(n->curitem->ncp);
// draw items below the current one FIME
while(frontierb < ncplane_dim_y(n->items.ncp)){
if((tmpnii = nctree_next_internal(n, tmppath)) == nii){
break;
}
nii = tmpnii;
if(draw_tree_item(n, nii, tmppath, &frontiert, &frontierb)){
return -1;
}
}
// FIXME destroy any drawn ones after us
return 0;
}
int nctree_redraw(nctree* n){
unsigned* tmppath = malloc(sizeof(*tmppath) * (n->maxdepth + 1));
if(tmppath == NULL){
return -1;
}
memcpy(tmppath, n->currentpath, sizeof(*tmppath) * (n->maxdepth + 1));
int ret = nctree_inner_redraw(n, tmppath);
free(tmppath);
return ret;
}
bool nctree_offer_input(nctree* n, const ncinput* ni){
if(ni->id == NCKEY_UP){
nctree_prev(n);
return true;
}else if(ni->id == NCKEY_DOWN){
nctree_next(n);
return true;
}else if(ni->id == NCKEY_PGUP){
nctree_prev(n); // more FIXME
return true;
}else if(ni->id == NCKEY_PGDOWN){
nctree_next(n); // more FIXME
return true;
}else if(ni->id == NCKEY_HOME){
goto_first_item(n);
return true;
}else if(ni->id == NCKEY_END){
nctree_next(n); // more FIXME
return true;
}
// FIXME implement left, right, +, - (expand/collapse)
return false;
}
void* nctree_focused(nctree* n){
return n->curitem->curry;
}
/*
void* nctree_goto(nctree* n, const unsigned* spec, int* failspec){
// FIXME
}
*/

@ -1,49 +0,0 @@
#include <locale.h>
#include <notcurses/notcurses.h>
#include "compat/compat.h"
int main(void){
setlocale(LC_ALL, "");
struct notcurses_options opts = {
.flags = NCOPTION_INHIBIT_SETLOCALE,
};
struct timespec ts = {
.tv_sec = 1,
.tv_nsec = 500000000,
};
struct notcurses* nc = notcurses_core_init(&opts, NULL);
int dimy, dimx;
struct ncplane* std = notcurses_stddim_yx(nc, &dimy, &dimx);
struct ncvisual* ncv = ncvisual_from_plane(std, NCBLIT_2x1, 0, 0, dimy / 2, dimx / 2);
if(ncv == NULL){
notcurses_stop(nc);
return EXIT_FAILURE;
}
struct ncvisual_options vopts = {
.n = std,
};
if(ncvisual_set_yx(ncv, dimy / 2 - 1, dimx / 2 - 1, 0xffffffff) < 0){
notcurses_stop(nc);
return EXIT_FAILURE;
}
if(std != ncvisual_render(nc, ncv, &vopts)){
notcurses_stop(nc);
return EXIT_FAILURE;
}
notcurses_render(nc);
clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL);
if(ncvisual_polyfill_yx(ncv, 0, 0, ncpixel(0x00, 0x80, 0x80)) <= 0){
notcurses_stop(nc);
return EXIT_FAILURE;
}
if(std != ncvisual_render(nc, ncv, &vopts)){
notcurses_stop(nc);
return EXIT_FAILURE;
}
notcurses_render(nc);
clock_nanosleep(CLOCK_MONOTONIC, 0, &ts, NULL);
notcurses_stop(nc);
return EXIT_SUCCESS;
}

@ -0,0 +1,428 @@
#include "notcurses/notcurses.h"
static nctree_item alphaUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²¹⁴U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²¹⁵U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²¹⁶U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²¹⁷U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²¹⁸U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²¹⁹U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²²¹U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²²²U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²²⁸U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²²⁹U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁰U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³¹U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³²U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³³U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁴U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁵U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁶U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁸U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²⁴⁰U",
}
};
static nctree_item alphaU = {
.subs = alphaUs,
.subcount = sizeof(alphaUs) / sizeof(*alphaUs),
.curry = "ɑ-emitting U",
};
static nctree_item doubleUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²³⁰U",
}
};
static nctree_item doubleU = {
.subs = doubleUs,
.subcount = sizeof(doubleUs) / sizeof(*doubleUs),
.curry = "ββ-emitting U",
};
static nctree_item doubleminusUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²³⁸U",
}
};
static nctree_item doubleminusU = {
.subs = doubleminusUs,
.subcount = sizeof(doubleminusUs) / sizeof(*doubleminusUs),
.curry = "β−β−-emitting U",
};
static nctree_item betaminusUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²³⁹U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²⁴⁰U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²⁴²U",
}
};
static nctree_item betaminusPus[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²⁴¹Pu",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²⁴³Pu",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²⁴⁵Pu",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²⁴⁶Pu",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²⁴⁷Pu",
}
};
static nctree_item betaminus[] = {
{
.subs = betaminusUs,
.subcount = sizeof(betaminusUs) / sizeof(*betaminusUs),
.curry = "β−-emitting U",
}, {
.subs = betaminusPus,
.subcount = sizeof(betaminusPus) / sizeof(*betaminusPus),
.curry = "β−-emitting Pu",
},
};
static nctree_item betaplusUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²²²U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²²⁷U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²²⁹U",
},
};
static nctree_item betaplus = {
.subs = betaplusUs,
.subcount = sizeof(betaplusUs) / sizeof(*betaplusUs),
.curry = "β-emitting U",
};
static nctree_item gammaUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²¹⁶Uᵐ",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁴Uᵐ",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁵Uᵐ",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁶Uᵐ¹",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁶Uᵐ²",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁸Uᵐ",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁹Uᵐ¹",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁹Uᵐ²",
},
};
static nctree_item gammas = {
.subs = gammaUs,
.subcount = sizeof(gammaUs) / sizeof(*gammaUs),
.curry = "γ-emitting U",
};
static nctree_item sfissionUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²³⁰U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³²U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³³U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁴U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁵U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁶U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁸U",
},
};
static nctree_item sfissions = {
.subs = sfissionUs,
.subcount = sizeof(sfissionUs) / sizeof(*sfissionUs),
.curry = "spontaneously fissioning U",
};
static nctree_item ecaptureUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²²⁸U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³¹U",
},
};
static nctree_item ecaptures = {
.subs = ecaptureUs,
.subcount = sizeof(ecaptureUs) / sizeof(*ecaptureUs),
.curry = "electron capturing U",
};
static nctree_item cdecayUs[] = {
{
.subs = NULL,
.subcount = 0,
.curry = "²³²U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³³U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁴U",
}, {
.subs = NULL,
.subcount = 0,
.curry = "²³⁵U",
},
};
static nctree_item cdecays = {
.subs = cdecayUs,
.subcount = sizeof(cdecayUs) / sizeof(*cdecayUs),
.curry = "cluster decaying U",
};
static nctree_item radUs[] = {
{
.subs = &alphaU,
.subcount = 1,
.curry = "ɑ emitters",
}, {
.subs = betaminus,
.subcount = sizeof(betaminus) / sizeof(*betaminus),
.curry = "β− emitters",
}, {
.subs = &doubleminusU,
.subcount = 1,
.curry = "β−β− emitters",
}, {
.subs = &doubleU,
.subcount = 1,
.curry = "ββ emitters",
}, {
.subs = &betaplus,
.subcount = 1,
.curry = "β emitters",
}, {
.subs = &gammas,
.subcount = 1,
.curry = "γ emitters",
}, {
.subs = &sfissions,
.subcount = 1,
.curry = "spontaneous fissions",
}, {
.subs = &cdecays,
.subcount = 1,
.curry = "cluster decays",
}, {
.subs = &ecaptures,
.subcount = 1,
.curry = "electron captures",
},
};
static nctree_item rads = {
.subs = radUs,
.subcount = sizeof(radUs) / sizeof(*radUs),
.curry = "radiating isotopes",
};
static int
callback(struct ncplane* ncp, void* curry, int dizzy){
if(ncplane_dim_y(ncp) > 1){
if(ncplane_resize_simple(ncp, 1, ncplane_dim_x(ncp))){
return -1;
}
}
ncplane_cursor_move_yx(ncp, 0, 0);
ncplane_putstr(ncp, curry);
// FIXME
(void)dizzy;
return 0;
}
static int
tree_ui(struct notcurses* nc, struct nctree* tree){
ncinput ni;
while(notcurses_getc_blocking(nc, &ni) != (char32_t)-1){
if(nctree_offer_input(tree, &ni)){
if(nctree_redraw(tree)){
return -1;
}
continue;
}
if(ni.id == 'q'){
return 0;
}
}
return -1;
}
static struct nctree*
create_tree(struct notcurses* nc){
struct nctree_options topts = {
.items = &rads,
.count = 1,
.nctreecb = callback,
.indentcols = 2,
.flags = 0,
};
struct nctree* tree = nctree_create(notcurses_stdplane(nc), &topts);
if(tree){
notcurses_render(nc);
}
return tree;
}
int main(void){
struct notcurses_options nopts = {
.loglevel = NCLOGLEVEL_WARNING,
};
struct notcurses* nc = notcurses_init(&nopts, NULL);
if(nc == NULL){
return EXIT_FAILURE;
}
struct nctree* tree = create_tree(nc);
if(tree == NULL){
notcurses_stop(nc);
return EXIT_FAILURE;
}
int r = tree_ui(nc, tree);
nctree_destroy(tree);
notcurses_stop(nc);
if(r){
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

@ -0,0 +1,567 @@
#include "main.h"
#include <iostream>
int treecb(struct ncplane* n, void* curry, int pos){
ncplane_printf_yx(n, 0, 0, "item: %s pos: %d",
static_cast<const char*>(curry), pos);
return 0;
}
TEST_CASE("Tree") {
auto nc_ = testing_notcurses();
if(!nc_){
return;
}
struct ncplane* n_ = notcurses_stdplane(nc_);
REQUIRE(n_);
REQUIRE(0 == ncplane_cursor_move_yx(n_, 0, 0));
// should be refused with a null items
SUBCASE("BadTreeNoItems") {
struct nctree_options opts = {
.items = nullptr,
.count = 2,
.bchannels = 0,
.nctreecb = treecb,
.indentcols = 0,
.flags = 0,
};
auto treen = nctree_create(n_, &opts);
REQUIRE(nullptr == treen);
}
// should be refused with a zero count
SUBCASE("BadTreeNoCount") {
struct nctree_options opts = {
.items = {},
.count = 0,
.bchannels = 0,
.nctreecb = treecb,
.indentcols = 1,
.flags = 0,
};
auto treen = nctree_create(n_, &opts);
REQUIRE(nullptr == treen);
}
nctree_item subs[] = {
{
.curry = strdup("sub1-0"),
.subs = nullptr,
.subcount = 0,
},{
.curry = strdup("sub1-1"),
.subs = nullptr,
.subcount = 0,
}
};
nctree_item items[] = {
{
.curry = strdup("item0"),
.subs = nullptr,
.subcount = 0,
}, {
.curry = strdup("item1"),
.subs = subs,
.subcount = 2,
},
};
// should be refused with a null callback
SUBCASE("BadTreeNoCallback") {
struct nctree_options opts = {
.items = items,
.count = sizeof(items) / sizeof(*items),
.bchannels = 0,
.nctreecb = nullptr,
.indentcols = 1,
.flags = 0,
};
auto treen = nctree_create(n_, &opts);
REQUIRE(nullptr == treen);
}
// should be refused with negative indentcols
SUBCASE("BadTreeNoCallback") {
struct nctree_options opts = {
.items = items,
.count = sizeof(items) / sizeof(*items),
.bchannels = 0,
.nctreecb = treecb,
.indentcols = -1,
.flags = 0,
};
auto treen = nctree_create(n_, &opts);
REQUIRE(nullptr == treen);
}
SUBCASE("Create") {
struct nctree_options opts = {
.items = items,
.count = sizeof(items) / sizeof(*items),
.bchannels = 0,
.nctreecb = treecb,
.indentcols = 1,
.flags = 0,
};
const ncplane_options nopts = {
.y = 0, .x = 0, .rows = 3, .cols = ncplane_dim_y(n_),
.userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0,
};
auto treen = ncplane_create(n_, &nopts);
REQUIRE(nullptr != treen);
auto tree = nctree_create(treen, &opts);
REQUIRE(nullptr != tree);
CHECK(0 == notcurses_render(nc_));
CHECK(treen == nctree_plane(tree));
CHECK(items[0].curry == nctree_focused(tree));
nctree_destroy(tree);
}
SUBCASE("Traverse") {
struct nctree_options opts = {
.items = items,
.count = sizeof(items) / sizeof(*items),
.bchannels = 0,
.nctreecb = treecb,
.indentcols = 2,
.flags = 0,
};
const ncplane_options nopts = {
.y = 0, .x = 0, .rows = 3, .cols = ncplane_dim_y(n_),
.userptr = nullptr, .name = nullptr, .resizecb = nullptr, .flags = 0,
};
auto treen = ncplane_create(n_, &nopts);
REQUIRE(nullptr != treen);
auto tree = nctree_create(treen, &opts);
REQUIRE(nullptr != tree);
CHECK(0 == notcurses_render(nc_));
CHECK(treen == nctree_plane(tree));
CHECK(items[0].curry == nctree_focused(tree));
CHECK(items[0].curry == nctree_prev(tree));
CHECK(items[0].curry == nctree_focused(tree));
CHECK(items[1].curry == nctree_next(tree));
CHECK(items[1].curry == nctree_focused(tree));
CHECK(items[1].subs[0].curry == nctree_next(tree));
CHECK(items[1].subs[0].curry == nctree_focused(tree));
CHECK(items[1].subs[1].curry == nctree_next(tree));
CHECK(items[1].subs[1].curry == nctree_focused(tree));
CHECK(items[1].subs[1].curry == nctree_next(tree));
CHECK(items[1].subs[1].curry == nctree_focused(tree));
CHECK(items[1].subs[0].curry == nctree_prev(tree));
CHECK(items[1].subs[0].curry == nctree_focused(tree));
CHECK(items[1].curry == nctree_prev(tree));
CHECK(items[1].curry == nctree_focused(tree));
CHECK(items[0].curry == nctree_prev(tree));
CHECK(items[0].curry == nctree_focused(tree));
CHECK(items[0].curry == nctree_prev(tree));
CHECK(items[0].curry == nctree_focused(tree));
nctree_destroy(tree);
}
nctree_item alphaUs[] = {
{
.curry = const_cast<char*>("²¹⁴U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²¹⁵U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²¹⁶U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²¹⁷U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²¹⁸U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²¹⁹U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²²¹U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²²²U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²²⁸U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²²⁹U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁰U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³¹U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³²U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³³U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁴U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁵U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁶U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁸U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²⁴⁰U"),
.subs = NULL,
.subcount = 0,
},
};
nctree_item alphaU = {
.curry = const_cast<char*>("ɑ-emitting U"),
.subs = alphaUs,
.subcount = sizeof(alphaUs) / sizeof(*alphaUs),
};
nctree_item doubleUs[] = {
{
.curry = const_cast<char*>("²³⁰U"),
.subs = NULL,
.subcount = 0,
}
};
nctree_item doubleU = {
.curry = const_cast<char*>("ββ-emitting U"),
.subs = doubleUs,
.subcount = sizeof(doubleUs) / sizeof(*doubleUs),
};
nctree_item doubleminusUs[] = {
{
.curry = const_cast<char*>("²³⁸U"),
.subs = NULL,
.subcount = 0,
}
};
nctree_item doubleminusU = {
.curry = const_cast<char*>("β−β−-emitting U"),
.subs = doubleminusUs,
.subcount = sizeof(doubleminusUs) / sizeof(*doubleminusUs),
};
nctree_item betaminusUs[] = {
{
.curry = const_cast<char*>("²³⁹U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²⁴⁰U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²⁴²U"),
.subs = NULL,
.subcount = 0,
}
};
nctree_item betaminusPus[] = {
{
.curry = const_cast<char*>("²⁴¹Pu"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²⁴³Pu"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²⁴⁵Pu"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²⁴⁶Pu"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²⁴⁷Pu"),
.subs = NULL,
.subcount = 0,
}
};
nctree_item betaminus[] = {
{
.curry = const_cast<char*>("β−-emitting U"),
.subs = betaminusUs,
.subcount = sizeof(betaminusUs) / sizeof(*betaminusUs),
}, {
.curry = const_cast<char*>("β−-emitting Pu"),
.subs = betaminusPus,
.subcount = sizeof(betaminusPus) / sizeof(*betaminusPus),
},
};
nctree_item betaplusUs[] = {
{
.curry = const_cast<char*>("²²²U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²²⁷U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²²⁹U"),
.subs = NULL,
.subcount = 0,
},
};
nctree_item betaplus = {
.curry = const_cast<char*>("β-emitting U"),
.subs = betaplusUs,
.subcount = sizeof(betaplusUs) / sizeof(*betaplusUs),
};
nctree_item gammaUs[] = {
{
.curry = const_cast<char*>("²¹⁶Uᵐ"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁴Uᵐ"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁵Uᵐ"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁶Uᵐ¹"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁶Uᵐ²"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁸Uᵐ"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁹Uᵐ¹"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁹Uᵐ²"),
.subs = NULL,
.subcount = 0,
},
};
nctree_item gammas = {
.curry = const_cast<char*>("γ-emitting U"),
.subs = gammaUs,
.subcount = sizeof(gammaUs) / sizeof(*gammaUs),
};
nctree_item sfissionUs[] = {
{
.curry = const_cast<char*>("²³⁰U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³²U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³³U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁴U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁵U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁶U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁸U"),
.subs = NULL,
.subcount = 0,
},
};
nctree_item sfissions = {
.curry = const_cast<char*>("spontaneously fissioning U"),
.subs = sfissionUs,
.subcount = sizeof(sfissionUs) / sizeof(*sfissionUs),
};
nctree_item ecaptureUs[] = {
{
.curry = const_cast<char*>("²²⁸U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³¹U"),
.subs = NULL,
.subcount = 0,
},
};
nctree_item ecaptures = {
.curry = const_cast<char*>("electron capturing U"),
.subs = ecaptureUs,
.subcount = sizeof(ecaptureUs) / sizeof(*ecaptureUs),
};
nctree_item cdecayUs[] = {
{
.curry = const_cast<char*>("²³²U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³³U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁴U"),
.subs = NULL,
.subcount = 0,
}, {
.curry = const_cast<char*>("²³⁵U"),
.subs = NULL,
.subcount = 0,
},
};
nctree_item cdecays = {
.curry = const_cast<char*>("cluster decaying U"),
.subs = cdecayUs,
.subcount = sizeof(cdecayUs) / sizeof(*cdecayUs),
};
nctree_item radUs[] = {
{
.curry = const_cast<char*>("ɑ emitters"),
.subs = &alphaU,
.subcount = 1,
}, {
.curry = const_cast<char*>("β− emitters"),
.subs = betaminus,
.subcount = sizeof(betaminus) / sizeof(*betaminus),
}, {
.curry = const_cast<char*>("β−β− emitters"),
.subs = &doubleminusU,
.subcount = 1,
}, {
.curry = const_cast<char*>("ββ emitters"),
.subs = &doubleU,
.subcount = 1,
}, {
.curry = const_cast<char*>("β emitters"),
.subs = &betaplus,
.subcount = 1,
}, {
.curry = const_cast<char*>("γ emitters"),
.subs = &gammas,
.subcount = 1,
}, {
.curry = const_cast<char*>("spontaneous fissions"),
.subs = &sfissions,
.subcount = 1,
}, {
.curry = const_cast<char*>("cluster decays"),
.subs = &cdecays,
.subcount = 1,
}, {
.curry = const_cast<char*>("electron captures"),
.subs = &ecaptures,
.subcount = 1,
},
};
nctree_item rads = {
.curry = const_cast<char*>("radiating isotopes"),
.subs = radUs,
.subcount = sizeof(radUs) / sizeof(*radUs),
};
SUBCASE("TraverseLongList") {
struct nctree_options topts = {
.items = &rads,
.count = 1,
.bchannels = 0,
.nctreecb = treecb,
.indentcols = 2,
.flags = 0,
};
struct nctree* tree = nctree_create(notcurses_stdplane(nc_), &topts);
REQUIRE(nullptr != tree);
CHECK(0 == notcurses_render(nc_));
void* curry = nctree_focused(tree);
void* first = curry;
CHECK(nullptr != curry);
void* tmpcurry;
while( (tmpcurry = nctree_next(tree)) ){
if(tmpcurry == curry){
break;
}
curry = tmpcurry;
}
CHECK(nullptr != tmpcurry);
while( (tmpcurry = nctree_prev(tree)) ){
if(tmpcurry == curry){
break;
}
curry = tmpcurry;
}
CHECK(nullptr != tmpcurry);
CHECK(curry == first);
nctree_destroy(tree);
}
CHECK(0 == notcurses_stop(nc_));
}
Loading…
Cancel
Save