Panelreels port #52 (#90)

Port of panelreels, unit tests, and panelreel-demo from outcurses #52.
Doesn't completely work yet, but is surprisingly close!
This commit is contained in:
Nick Black 2019-12-01 17:48:38 -05:00 committed by GitHub
parent 326e93fee1
commit 426f632300
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1471 additions and 65 deletions

View File

@ -13,6 +13,7 @@ configure_file(tools/version.h.in include/version.h)
include(GNUInstallDirs)
find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED)
pkg_check_modules(TERMINFO REQUIRED tinfo>=6.1)
pkg_check_modules(AVUTIL REQUIRED libavutil>=56.0)
pkg_check_modules(AVFORMAT REQUIRED libavformat>=57.0)
@ -62,6 +63,7 @@ target_include_directories(notcurses-demo PRIVATE include)
target_link_libraries(notcurses-demo
PRIVATE
notcurses
Threads::Threads
)
target_compile_options(notcurses-demo
PRIVATE

View File

@ -143,15 +143,16 @@ API int notcurses_render(struct notcurses* nc);
//
// In the case of a valid read, a positive value is returned corresponding to
// the number of bytes in the UTF-8 character, or '1' for all specials keys.
// 0 is returned only by notcurses_getc(), to indicate that no input was
// available. Otherwise (including on EOF) -1 is returned.
// 0 is returned to indicate that no input was available, but only by
// notcurses_getc(). Otherwise (including on EOF) -1 is returned.
typedef enum {
NCKEY_INVALID,
NCKEY_RESIZE,
NCKEY_RESIZE, // generated interally in response to SIGWINCH
NCKEY_UP,
NCKEY_RIGHT,
NCKEY_DOWN,
NCKEY_LEFT,
NCKEY_DC, // delete
// FIXME...
} ncspecial_key;
@ -660,13 +661,10 @@ typedef struct panelreel_options {
// focused and non-focused tablets can have different styles. you can instead
// draw your own borders, or forgo borders entirely.
unsigned bordermask; // bitfield; 1s will not be drawn (see bordermaskbits)
uint32_t borderattr; // attributes used for panelreel border, no color!
int borderpair; // extended color pair for panelreel border
cell borderattr; // attributes used for panelreel border
unsigned tabletmask; // bitfield; same as bordermask but for tablet borders
uint32_t tabletattr; // attributes used for tablet borders, no color!
int tabletpair; // extended color pair for tablet borders
uint32_t focusedattr;// attributes used for focused tablet borders, no color!
int focusedpair; // extended color pair for focused tablet borders
cell tabletattr; // attributes used for tablet borders
cell focusedattr; // attributes used for focused tablet borders, no color!
} panelreel_options;
struct tablet;
@ -679,9 +677,9 @@ struct panelreel;
// and columns can be enforced via popts. efd, if non-negative, is an eventfd
// that ought be written to whenever panelreel_touch() updates a tablet (this
// is useful in the case of nonblocking input).
struct panelreel* panelreel_create(struct ncplane* nc,
const panelreel_options* popts,
int efd);
API struct panelreel* panelreel_create(struct ncplane* nc,
const panelreel_options* popts,
int efd);
// Tablet draw callback, provided a ncplane the first column that may be used,
// the first row that may be used, the first column that may not be used, the
@ -698,7 +696,7 @@ struct panelreel* panelreel_create(struct ncplane* nc,
// Returns the number of lines of output, which ought be less than or equal to
// maxy - begy, and non-negative (negative values might be used in the future).
typedef int (*tabletcb)(struct ncplane* p, int begx, int begy, int maxx,
int maxy, bool cliptop);
int maxy, bool cliptop, void* curry);
// Add a new tablet to the provided panelreel, having the callback object
// opaque. Neither, either, or both of after and before may be specified. If
@ -707,44 +705,45 @@ typedef int (*tabletcb)(struct ncplane* p, int begx, int begy, int maxx,
// specified tablet. If both are specifid, the tablet will be added to the
// resulting location, assuming it is valid (after->next == before->prev); if
// it is not valid, or there is any other error, NULL will be returned.
struct tablet* panelreel_add(struct panelreel* pr, struct tablet* after,
struct tablet *before, tabletcb cb, void* opaque);
API struct tablet* panelreel_add(struct panelreel* pr, struct tablet* after,
struct tablet *before, tabletcb cb,
void* opaque);
// Return the number of tablets.
int panelreel_tabletcount(const struct panelreel* pr);
API int panelreel_tabletcount(const struct panelreel* pr);
// Indicate that the specified tablet has been updated in a way that would
// change its display. This will trigger some non-negative number of callbacks
// (though not in the caller's context).
int panelreel_touch(struct panelreel* pr, struct tablet* t);
API int panelreel_touch(struct panelreel* pr, struct tablet* t);
// Delete the tablet specified by t from the panelreel specified by pr. Returns
// -1 if the tablet cannot be found.
int panelreel_del(struct panelreel* pr, struct tablet* t);
API int panelreel_del(struct panelreel* pr, struct tablet* t);
// Delete the active tablet. Returns -1 if there are no tablets.
int panelreel_del_focused(struct panelreel* pr);
API int panelreel_del_focused(struct panelreel* pr);
// Move to the specified location within the containing WINDOW.
int panelreel_move(struct panelreel* pr, int x, int y);
API int panelreel_move(struct panelreel* pr, int x, int y);
// Redraw the panelreel in its entirety, for instance after
// clearing the screen due to external corruption, or a SIGWINCH.
int panelreel_redraw(struct panelreel* pr);
API int panelreel_redraw(struct panelreel* pr);
// Return the focused tablet, if any tablets are present. This is not a copy;
// be careful to use it only for the duration of a critical section.
struct tablet* panelreel_focused(struct panelreel* pr);
API struct tablet* panelreel_focused(struct panelreel* pr);
// Change focus to the next tablet, if one exists
struct tablet* panelreel_next(struct panelreel* pr);
API struct tablet* panelreel_next(struct panelreel* pr);
// Change focus to the previous tablet, if one exists
struct tablet* panelreel_prev(struct panelreel* pr);
API struct tablet* panelreel_prev(struct panelreel* pr);
// Destroy a panelreel allocated with panelreel_create(). Does not destroy the
// underlying WINDOW. Returns non-zero on failure.
int panelreel_destroy(struct panelreel* pr);
API int panelreel_destroy(struct panelreel* pr);
#undef API

View File

@ -24,12 +24,13 @@ usage(const char* exe, int status){
fprintf(out, " -d: delay multiplier (float)\n");
fprintf(out, " -f: render to file in addition to stdout\n");
fprintf(out, "all demos are run if no specification is provided\n");
fprintf(out, " i: run intro\n");
fprintf(out, " s: run shuffle\n");
fprintf(out, " u: run uniblock\n");
fprintf(out, " m: run maxcolor\n");
fprintf(out, " b: run box\n");
fprintf(out, " g: run grid\n");
fprintf(out, " i: run intro\n");
fprintf(out, " m: run maxcolor\n");
fprintf(out, " p: run panelreels\n");
fprintf(out, " s: run shuffle\n");
fprintf(out, " u: run uniblock\n");
fprintf(out, " v: run view\n");
fprintf(out, " w: run widecolors\n");
exit(status);
@ -142,6 +143,7 @@ ext_demos(struct notcurses* nc, const char* demos){
case 'g': ret = grid_demo(nc); break;
case 'v': ret = view_demo(nc); break;
case 'w': ret = widecolor_demo(nc); break;
case 'p': ret = panelreel_demo(nc); break;
}
if(ret){
return ret;
@ -209,7 +211,7 @@ int main(int argc, char** argv){
if(argv[optind] != NULL){
usage(*argv, EXIT_FAILURE);
}
demos = "isumbgwv";
demos = "isumbgwvp";
}
if((nc = notcurses_init(&nopts)) == NULL){
return EXIT_FAILURE;

View File

@ -20,6 +20,7 @@ int maxcolor_demo(struct notcurses* nc);
int grid_demo(struct notcurses* nc);
int sliding_puzzle_demo(struct notcurses* nc);
int view_demo(struct notcurses* nc);
int panelreel_demo(struct notcurses* nc);
#ifdef __cplusplus
}

349
src/demo/panelreel.c Normal file
View File

@ -0,0 +1,349 @@
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <pthread.h>
#include <sys/poll.h>
#include <notcurses.h>
#include <sys/eventfd.h>
#include "demo.h"
// FIXME ought just be an unordered_map
typedef struct tabletctx {
pthread_t tid;
struct panelreel* pr;
struct tablet* t;
int lines;
unsigned rgb;
unsigned id;
struct tabletctx* next;
pthread_mutex_t lock;
} tabletctx;
static void
kill_tablet(tabletctx** tctx){
tabletctx* t = *tctx;
if(t){
if(pthread_cancel(t->tid)){
fprintf(stderr, "Warning: error sending pthread_cancel (%s)\n", strerror(errno));
}
if(pthread_join(t->tid, NULL)){
fprintf(stderr, "Warning: error joining pthread (%s)\n", strerror(errno));
}
panelreel_del(t->pr, t->t);
*tctx = t->next;
pthread_mutex_destroy(&t->lock);
free(t);
}
}
static int
kill_active_tablet(struct panelreel* pr, tabletctx** tctx){
struct tablet* focused = panelreel_focused(pr);
tabletctx* t;
while( (t = *tctx) ){
if(t->t == focused){
*tctx = t->next; // pull it out of the list
t->next = NULL; // finish splicing it out
break;
}
tctx = &t->next;
}
if(t == NULL){
return -1; // wasn't present in our list, wacky
}
kill_tablet(&t);
return 0;
}
// We need write in reverse order (since only the bottom will be seen, if we're
// partially off-screen), but also leave unused space at the end (since
// wresize() only keeps the top and left on a shrink).
static int
tabletup(struct ncplane* w, int begx, int begy, int maxx, int maxy,
tabletctx* tctx, int rgb){
char cchbuf[2];
cell c = CELL_TRIVIAL_INITIALIZER;
int y, idx;
idx = tctx->lines;
if(maxy - begy > tctx->lines){
maxy -= (maxy - begy - tctx->lines);
}
/*fprintf(stderr, "-OFFSET BY %d (%d->%d)\n", maxy - begy - tctx->lines,
maxy, maxy - (maxy - begy - tctx->lines));*/
for(y = maxy ; y >= begy ; --y, rgb += 16){
ncplane_cursor_move_yx(w, y, begx);
snprintf(cchbuf, sizeof(cchbuf) / sizeof(*cchbuf), "%x", idx % 16);
cell_load(w, &c, cchbuf);
cell_set_fg(&c, (rgb >> 16u) % 0xffu, (rgb >> 8u) % 0xffu, rgb % 0xffu);
int x;
for(x = begx ; x <= maxx ; ++x){
// lower-right corner always returns an error unless scrollok() is used
ncplane_putc(w, &c);
}
if(--idx == 0){
break;
}
}
// fprintf(stderr, "tabletup done%s at %d (%d->%d)\n", idx == 0 ? " early" : "", y, begy, maxy);
return tctx->lines - idx;
}
static int
tabletdown(struct ncplane* w, int begx, int begy, int maxx, int maxy,
tabletctx* tctx, unsigned rgb){
char cchbuf[2];
cell c = CELL_TRIVIAL_INITIALIZER;
int y;
for(y = begy ; y <= maxy ; ++y, rgb += 16){
if(y - begy >= tctx->lines){
break;
}
ncplane_cursor_move_yx(w, y, begx);
snprintf(cchbuf, sizeof(cchbuf) / sizeof(*cchbuf), "%x", y % 16);
cell_load(w, &c, cchbuf);
cell_set_fg(&c, (rgb >> 16u) % 0xffu, (rgb >> 8u) % 0xffu, rgb % 0xffu);
int x;
for(x = begx ; x <= maxx ; ++x){
// lower-right corner always returns an error unless scrollok() is used
ncplane_putc(w, &c);
}
}
return y - begy;
}
static int
tabletdraw(struct ncplane* p, int begx, int begy, int maxx, int maxy,
bool cliptop, void* vtabletctx){
int err = 0;
tabletctx* tctx = vtabletctx;
pthread_mutex_lock(&tctx->lock);
unsigned rgb = tctx->rgb;
int ll;
if(cliptop){
ll = tabletup(p, begx, begy, maxx, maxy, tctx, rgb);
}else{
ll = tabletdown(p, begx, begy, maxx, maxy, tctx, rgb);
}
ncplane_fg_rgb8(p, 242, 242, 242);
if(ll){
int summaryy = begy;
if(cliptop){
if(ll == maxy - begy + 1){
summaryy = ll - 1;
}else{
summaryy = ll;
}
}
err |= ncplane_cursor_move_yx(p, summaryy, begx);
ncplane_printf(p, "[#%u %d line%s %u/%u] ", tctx->id, tctx->lines,
tctx->lines == 1 ? "" : "s", begy, maxy);
}
/*fprintf(stderr, " \\--> callback for %d, %d lines (%d/%d -> %d/%d) dir: %s wrote: %d ret: %d\n", tctx->id,
tctx->lines, begy, begx, maxy, maxx,
cliptop ? "up" : "down", ll, err);*/
pthread_mutex_unlock(&tctx->lock);
assert(0 == err);
return ll;
}
// Each tablet has an associated thread which will periodically send update
// events for its tablet.
static void*
tablet_thread(void* vtabletctx){
static int MINSECONDS = 0;
tabletctx* tctx = vtabletctx;
while(true){
struct timespec ts;
ts.tv_sec = random() % 3 + MINSECONDS;
ts.tv_nsec = random() % 1000000000;
nanosleep(&ts, NULL);
int action = random() % 5;
pthread_mutex_lock(&tctx->lock);
if(action < 2){
if((tctx->lines -= (action + 1)) < 1){
tctx->lines = 1;
}
panelreel_touch(tctx->pr, tctx->t);
}else if(action > 2){
if((tctx->lines += (action - 2)) < 1){
tctx->lines = 1;
}
panelreel_touch(tctx->pr, tctx->t);
}
pthread_mutex_unlock(&tctx->lock);
}
return tctx;
}
static tabletctx*
new_tabletctx(struct panelreel* pr, unsigned *id){
tabletctx* tctx = malloc(sizeof(*tctx));
if(tctx == NULL){
return NULL;
}
pthread_mutex_init(&tctx->lock, NULL);
tctx->pr = pr;
tctx->lines = random() % 10 + 1; // FIXME a nice gaussian would be swell
tctx->rgb = random() % (1u << 24u);
tctx->id = ++*id;
if((tctx->t = panelreel_add(pr, NULL, NULL, tabletdraw, tctx)) == NULL){
pthread_mutex_destroy(&tctx->lock);
free(tctx);
return NULL;
}
if(pthread_create(&tctx->tid, NULL, tablet_thread, tctx)){
pthread_mutex_destroy(&tctx->lock);
free(tctx);
return NULL;
}
return tctx;
}
static int
handle_input(struct notcurses* nc, struct panelreel* pr, int efd,
cell* c, ncspecial_key* special){
struct pollfd fds[2] = {
{ .fd = STDIN_FILENO, .events = POLLIN, .revents = 0, },
{ .fd = efd, .events = POLLIN, .revents = 0, },
};
int key = -1;
int pret;
notcurses_render(nc);
do{
pret = poll(fds, sizeof(fds) / sizeof(*fds), -1);
if(pret < 0){
fprintf(stderr, "Error polling on stdin/eventfd (%s)\n", strerror(errno));
}else{
if(fds[0].revents & POLLIN){
key = notcurses_getc(nc, c, special);
if(key < 0){
return -1;
}
}
if(fds[1].revents & POLLIN){
uint64_t val;
if(read(efd, &val, sizeof(val)) != sizeof(val)){
fprintf(stderr, "Error reading from eventfd %d (%s)\n", efd, strerror(errno)); }else if(key < 0){
panelreel_redraw(pr);
notcurses_render(nc);
}
}
}
}while(key < 0);
return key;
}
static struct panelreel*
panelreel_demo_core(struct notcurses* nc, int efd, tabletctx** tctxs){
bool done = false;
int x = 4, y = 4;
panelreel_options popts = {
.infinitescroll = true,
.circular = true,
.min_supported_cols = 8,
.min_supported_rows = 5,
.borderattr = CELL_TRIVIAL_INITIALIZER,
.tabletattr = CELL_TRIVIAL_INITIALIZER,
.focusedattr = CELL_TRIVIAL_INITIALIZER,
.toff = y,
.loff = x,
.roff = 0,
.boff = 0,
};
cell_set_fg(&popts.focusedattr, 58, 150, 221);
cell_set_fg(&popts.tabletattr, 19, 161, 14);
cell_set_fg(&popts.borderattr, 136, 23, 152);
struct ncplane* w = notcurses_stdplane(nc);
struct panelreel* pr = panelreel_create(w, &popts, efd);
if(pr == NULL){
fprintf(stderr, "Error creating panelreel\n");
return NULL;
}
// Press a for a new panel above the current, c for a new one below the
// current, and b for a new block at arbitrary placement. q quits.
ncplane_fg_rgb8(w, 58, 150, 221);
ncplane_cursor_move_yx(w, 1, 1);
ncplane_printf(w, "a, b, c create tablets, DEL deletes, q quits.");
// FIXME clrtoeol();
unsigned id = 0;
do{
ncplane_styles_set(w, 0);
ncplane_fg_rgb8(w, 197, 15, 31);
int count = panelreel_tabletcount(pr);
ncplane_cursor_move_yx(w, 2, 2);
ncplane_printf(w, "%d tablet%s", count, count == 1 ? "" : "s");
// FIXME wclrtoeol(w);
ncplane_fg_rgb8(w, 0, 55, 218);
ncspecial_key special = NCKEY_INVALID;
cell c = CELL_TRIVIAL_INITIALIZER;
if(handle_input(nc, pr, efd, &c, &special) < 0){
done = true;
break;
}
// FIXME clrtoeol();
struct tabletctx* newtablet = NULL;
if(cell_simple_p(&c)){
if(c.gcluster){
switch(c.gcluster){
case 'p': sleep(60); exit(EXIT_FAILURE); break;
case 'a': newtablet = new_tabletctx(pr, &id); break;
case 'b': newtablet = new_tabletctx(pr, &id); break;
case 'c': newtablet = new_tabletctx(pr, &id); break;
case 'h': --x; if(panelreel_move(pr, x, y)){ ++x; } break;
case 'l': ++x; if(panelreel_move(pr, x, y)){ --x; } break;
case 'k': panelreel_prev(pr); break;
case 'j': panelreel_next(pr); break;
case 'q': done = true; break;
default:
ncplane_cursor_move_yx(w, 3, 2);
ncplane_printf(w, "Unknown keycode (%d)\n", c.gcluster);
}
}else{
switch(special){
case NCKEY_LEFT: --x; if(panelreel_move(pr, x, y)){ ++x; } break;
case NCKEY_RIGHT: ++x; if(panelreel_move(pr, x, y)){ --x; } break;
case NCKEY_UP: panelreel_prev(pr); break;
case NCKEY_DOWN: panelreel_next(pr); break;
case NCKEY_DC: kill_active_tablet(pr, tctxs); break;
default:
ncplane_cursor_move_yx(w, 3, 2);
ncplane_printf(w, "Unknown special (%d)\n", special);
}
}
}
cell_release(w, &c);
if(newtablet){
newtablet->next = *tctxs;
*tctxs = newtablet;
}
//panelreel_validate(w, pr); // do what, if not assert()ing? FIXME
}while(!done);
return pr;
}
int panelreel_demo(struct notcurses* nc){
tabletctx* tctxs = NULL;
int efd = eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK);
if(efd < 0){
fprintf(stderr, "Error creating eventfd (%s)\n", strerror(errno));
return -1;
}
struct panelreel* pr;
if((pr = panelreel_demo_core(nc, efd, &tctxs)) == NULL){
close(efd);
return -1;
}
/*fadeout(w, FADE_MILLISECONDS);*/
while(tctxs){
kill_tablet(&tctxs);
}
close(efd);
if(panelreel_destroy(pr)){
fprintf(stderr, "Error destroying panelreel\n");
return -1;
}
close(efd);
return 0;
}

View File

@ -85,12 +85,12 @@ int widecolor_demo(struct notcurses* nc){
"⑥ И҆ речѐ гдⷭ҇ь: сѐ, ро́дъ є҆ди́нъ, и҆ ѹ҆стнѣ̀ є҆ди҄нѣ всѣ́хъ, и҆ сїѐ нача́ша твори́ти: и҆ нн҃ѣ не ѡ҆скꙋдѣ́ютъ ѿ ни́хъ всѧ҄, є҆ли҄ка а́҆ще восхотѧ́тъ твори́ти.",
"⑦ Ⱂⱃⰻⰻⰴⱑⱅⰵ ⰺ ⰺⰸⱎⰵⰴⱎⰵ ⱄⰿⱑⱄⰻⰿⱏ ⰺⰿⱏ ⱅⱆ ⱔⰸⱏⰹⰽⰻ ⰺⱈⱏ · ⰴⰰ ⱀⰵ ⱆⱄⰾⱏⰹⱎⰰⱅⱏ ⰽⱁⰶⰴⱁ ⰴⱃⱆⰳⰰ ⱄⰲⱁⰵⰳⱁ ⁖⸏",
"काचं शक्नोम्यत्तुम् । नोपहिनस्ति माम्",
*/
"kācaṃ śaknomyattum; nopahinasti mām",
"ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει",
"Μπορῶ νὰ φάω σπασμένα γυαλιὰ χωρὶς νὰ πάθω τίποτα",
"Vitrum edere possum; mihi non nocet",
"🚬🌿💉💊☢☣🔫💣⚔🤜🤛🧠🦹🤺🏋️,🦔🐧🐣🦆🦢🦜🦉🐊🐸🦕 🦖🐬🐙🦂🦠🦀",
*/
// "🚬🌿💉💊☢☣🔫💣⚔🤜🤛🧠🦹🤺🏋️,🦔🐧🐣🦆🦢🦜🦉🐊🐸🦕 🦖🐬🐙🦂🦠🦀",
"Je puis mangier del voirre. Ne me nuit",
"Je peux manger du verre, ça ne me fait pas mal",
"Pòdi manjar de veire, me nafrariá pas",

View File

@ -6,6 +6,7 @@
#include <stdarg.h>
#include <string.h>
#include <stdbool.h>
#include "egcpool.h"
#ifdef __cplusplus
extern "C" {
@ -18,7 +19,35 @@ struct AVCodec;
struct AVCodecParameters;
struct AVPacket;
struct SwsContext;
struct ncplane;
// A plane is memory for some rectilinear virtual window, plus current cursor
// state for that window. A notcurses context describes a single terminal, and
// has a z-order of planes (I see no advantage to maintaining a poset, and we
// instead just use a list, top-to-bottom). Every cell on the terminal is part
// of at least one plane, and at least one plane covers the entirety of the
// terminal (this plane is created during initialization).
//
// Functions update these virtual planes over a series of API calls. Eventually,
// notcurses_render() is called. We then do a depth buffer blit of updated
// cells. A cell is updated if the topmost plane including that cell updates it,
// not simply if any plane updates it.
//
// A plane may be partially or wholly offscreen--this might occur if the
// screen is resized, for example. Offscreen portions will not be rendered.
// Accesses beyond the borders of a panel, however, are errors.
typedef struct ncplane {
cell* fb; // "framebuffer" of character cells
int x, y; // current location within this plane
int absx, absy; // origin of the plane relative to the screen
int lenx, leny; // size of the plane, [0..len{x,y}) is addressable
struct ncplane* z; // plane below us
egcpool pool; // attached storage pool for UTF-8 EGCs
uint64_t channels; // works the same way as cells
uint32_t attrword; // same deal as in a cell
void* userptr; // slot for the user to stick some opaque pointer
cell background; // cell written anywhere that fb[i].gcluster == 0
struct notcurses* nc; // notcurses object of which we are a part
} ncplane;
typedef struct ncvisual {
struct AVFormatContext* fmtctx;
@ -31,7 +60,7 @@ typedef struct ncvisual {
struct SwsContext* swsctx;
int packet_outstanding;
int dstwidth, dstheight;
struct ncplane* ncp;
ncplane* ncp;
} ncvisual;
#ifdef __cplusplus

View File

@ -26,35 +26,6 @@
#define ESC "\x1b"
// A plane is memory for some rectilinear virtual window, plus current cursor
// state for that window. A notcurses context describes a single terminal, and
// has a z-order of planes (I see no advantage to maintaining a poset, and we
// instead just use a list, top-to-bottom). Every cell on the terminal is part
// of at least one plane, and at least one plane covers the entirety of the
// terminal (this plane is created during initialization).
//
// Functions update these virtual planes over a series of API calls. Eventually,
// notcurses_render() is called. We then do a depth buffer blit of updated
// cells. A cell is updated if the topmost plane including that cell updates it,
// not simply if any plane updates it.
//
// A plane may be partially or wholly offscreen--this might occur if the
// screen is resized, for example. Offscreen portions will not be rendered.
// Accesses beyond the borders of a panel, however, are errors.
typedef struct ncplane {
cell* fb; // "framebuffer" of character cells
int x, y; // current location within this plane
int absx, absy; // origin of the plane relative to the screen
int lenx, leny; // size of the plane, [0..len{x,y}) is addressable
struct ncplane* z; // plane below us
egcpool pool; // attached storage pool for UTF-8 EGCs
uint64_t channels; // works the same way as cells
uint32_t attrword; // same deal as in a cell
void* userptr; // slot for the user to stick some opaque pointer
cell background; // cell written anywhere that fb[i].gcluster == 0
struct notcurses* nc; // notcurses object of which we are a part
} ncplane;
typedef struct ncstats {
uint64_t renders; // number of notcurses_render() runs
uint64_t renders_ns; // number of nanoseconds spent in notcurses_render()
@ -1445,8 +1416,12 @@ void ncplane_move_yx(ncplane* n, int y, int x){
}
void ncplane_yx(const ncplane* n, int* y, int* x){
*y = n->absy;
*x = n->absx;
if(y){
*y = n->absy;
}
if(x){
*x = n->absx;
}
}
// copy the UTF8-encoded EGC out of the cell, whether simple or complex. the

816
src/lib/panelreel.c Normal file
View File

@ -0,0 +1,816 @@
#include <errno.h>
#include <unistd.h>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "notcurses.h"
#include "internal.h"
// Tablets are the toplevel entitites within a panelreel. Each corresponds to
// a single, distinct ncplane.
typedef struct tablet {
ncplane* p; // visible panel, NULL when offscreen
struct tablet* next;
struct tablet* prev;
tabletcb cbfxn; // application callback to draw tablet
void* curry; // application data provided to cbfxn
} tablet;
// The visible screen can be reconstructed from three things:
// * which tablet is focused (pointed at by tablets)
// * which row the focused tablet starts at (derived from focused window)
// * the list of tablets (available from the focused tablet)
typedef struct panelreel {
ncplane* p; // ncplane this panelreel occupies, under tablets
panelreel_options popts; // copied in panelreel_create()
// doubly-linked list, a circular one when infinity scrolling is in effect.
// points at the focused tablet (when at least one tablet exists, one must be
// focused), which might be anywhere on the screen (but is always visible).
int efd; // eventfd, signaled in panelreel_touch() if >= 0
tablet* tablets;
// these values could all be derived at any time, but keeping them computed
// makes other things easier, or saves us time (at the cost of complexity).
int tabletcount; // could be derived, but we keep it o(1)
// last direction in which we moved. positive if we moved down ("next"),
// negative if we moved up ("prev"), 0 for non-linear operation. we start
// drawing unfocused tablets opposite the direction of our last movement, so
// that movement in an unfilled reel doesn't reorient our tablets.
int last_traveled_direction;
// are all of our tablets currently visible? our arrangement algorithm works
// differently when the reel is not completely filled. ideally we'd unite the
// two modes, but for now, check this bool and take one of two paths.
bool all_visible;
} panelreel;
// Returns the starting coordinates (relative to the screen) of the specified
// window, and its length. End is (begx + lenx - 1, begy + leny - 1).
static inline void
window_coordinates(const ncplane* w, int* begy, int* begx, int* leny, int* lenx){
ncplane_yx(w, begy, begx);
ncplane_dim_yx(w, leny, lenx);
}
// FIXME compatability wrapper for libpanel
int wresize(ncplane* n, int leny, int lenx){
int y, x;
ncplane_yx(n, &y, &x);
int dimy, dimx;
ncplane_dim_yx(n, &dimy, &dimx);
return ncplane_resize(n, 0, 0, dimy, dimx, y, x, leny, lenx);
}
// bchrs: 6-element array of wide border characters + attributes FIXME
static int
draw_borders(ncplane* w, unsigned nobordermask, const cell* attr,
bool cliphead, bool clipfoot){
int begx, begy, lenx, leny;
int ret = 0;
window_coordinates(w, &begy, &begx, &leny, &lenx);
begx = 0;
begy = 0;
int maxx = begx + lenx - 1;
int maxy = begy + leny - 1;
cell ul, ur, ll, lr, hl, vl;
cell_init(&ul); cell_init(&ur); cell_init(&hl);
cell_init(&ll); cell_init(&lr); cell_init(&vl);
if(ncplane_rounded_box_cells(w, &ul, &ur, &ll, &lr, &hl, &vl)){
return -1;
}
fprintf(stderr, "drawing borders %d/%d->%d/%d, mask: %04x, clipping: %c%c\n",
begx, begy, maxx, maxy, nobordermask,
cliphead ? 'T' : 't', clipfoot ? 'F' : 'f');
ul.attrword = attr->attrword; ul.channels = attr->channels;
ur.attrword = attr->attrword; ur.channels = attr->channels;
ll.attrword = attr->attrword; ll.channels = attr->channels;
lr.attrword = attr->attrword; lr.channels = attr->channels;
hl.attrword = attr->attrword; hl.channels = attr->channels;
vl.attrword = attr->attrword; vl.channels = attr->channels;
if(!cliphead){
// lenx - begx + 1 is the number of columns we have, but drop 2 due to
// corners. we thus want lenx - begx - 1 horizontal lines.
if(!(nobordermask & BORDERMASK_TOP)){
ret |= ncplane_cursor_move_yx(w, begy, begx);
ncplane_putc(w, &ul);
ncplane_hline(w, &hl, lenx - 2);
ncplane_putc(w, &ur);
}else{
if(!(nobordermask & BORDERMASK_LEFT)){
ret |= ncplane_cursor_move_yx(w, begy, begx);
ncplane_putc(w, &ul);
}
if(!(nobordermask & BORDERMASK_RIGHT)){
ret |= ncplane_cursor_move_yx(w, begy, maxx);
ncplane_putc(w, &ur);
}
}
}
int y;
for(y = begy + !cliphead ; y < maxy + !!clipfoot ; ++y){
if(!(nobordermask & BORDERMASK_LEFT)){
ret |= ncplane_cursor_move_yx(w, y, begx);
ncplane_putc(w, &vl);
}
if(!(nobordermask & BORDERMASK_RIGHT)){
ret |= ncplane_cursor_move_yx(w, y, maxx);
ncplane_putc(w, &vl);
}
}
if(!clipfoot){
if(!(nobordermask & BORDERMASK_BOTTOM)){
ret |= ncplane_cursor_move_yx(w, maxy, begx);
ncplane_putc(w, &ll);
ncplane_hline(w, &hl, lenx - 2);
ncplane_putc(w, &lr);
}else{
if(!(nobordermask & BORDERMASK_LEFT)){
ret |= ncplane_cursor_move_yx(w, maxy, begx);
ret |= ncplane_putc(w, &ll);
}
if(!(nobordermask & BORDERMASK_RIGHT)){
// mvwadd_wch returns error if we print to the lowermost+rightmost
// character cell. maybe we can make this go away with scrolling controls
// at setup? until then, don't check for error here FIXME.
ret |= ncplane_cursor_move_yx(w, maxy, maxx);
ret |= ncplane_putc(w, &lr);
}
}
}
cell_release(w, &ul); cell_release(w, &ur); cell_release(w, &hl);
cell_release(w, &ll); cell_release(w, &lr); cell_release(w, &vl);
// fprintf(stderr, "||--borders %d %d %d %d clip: %c%c ret: %d\n",
// begx, begy, maxx, maxy, cliphead ? 'y' : 'n', clipfoot ? 'y' : 'n', ret);
return ret;
}
// Draws the border (if one should be drawn) around the panelreel, and enforces
// any provided restrictions on visible window size.
static int
draw_panelreel_borders(const panelreel* pr){
int begx, begy;
int maxx, maxy;
window_coordinates(pr->p, &begy, &begx, &maxy, &maxx);
assert(begy >= 0 && begx >= 0);
assert(maxy >= 0 && maxx >= 0);
--maxx; // last column we can safely write to
--maxy; // last line we can safely write to
if(begx >= maxx || maxx - begx + 1 < pr->popts.min_supported_rows){
return 0; // no room
}
if(begy >= maxy || maxy - begy + 1 < pr->popts.min_supported_cols){
return 0; // no room
}
return draw_borders(pr->p, pr->popts.bordermask, &pr->popts.borderattr, false, false);
}
// Calculate the starting and ending coordinates available for occupation by
// the tablet, relative to the panelreel's ncplane. Returns non-zero if the
// tablet cannot be made visible as specified. If this is the focused tablet
// (direction == 0), it can take the entire reel -- frontiery is only a
// suggestion in this case -- so give it the full breadth.
static int
tablet_columns(const panelreel* pr, int* begx, int* begy, int* lenx, int* leny,
int frontiery, int direction){
window_coordinates(pr->p, begy, begx, leny, lenx);
int maxy = *leny + *begy - 1;
int begindraw = *begy + !(pr->popts.bordermask & BORDERMASK_TOP);
// FIXME i think this fails to account for an absent panelreel bottom?
int enddraw = maxy - !(pr->popts.bordermask & BORDERMASK_TOP);
if(direction){
if(frontiery < begindraw){
return -1;
}
if(frontiery > enddraw){
// fprintf(stderr, "FRONTIER: %d ENDDRAW: %d\n", frontiery, enddraw);
return -1;
}
}
// account for the panelreel borders
if(direction <= 0 && !(pr->popts.bordermask & BORDERMASK_TOP)){
++*begy;
--*leny;
}
if(direction >= 0 && !(pr->popts.bordermask & BORDERMASK_BOTTOM)){
--*leny;
}
if(!(pr->popts.bordermask & BORDERMASK_LEFT)){
++*begx;
--*lenx;
}
if(!(pr->popts.bordermask & BORDERMASK_RIGHT)){
--*lenx;
}
// at this point, our coordinates describe the largest possible tablet for
// this panelreel. this is the correct solution for the focused tablet. other
// tablets can only grow in one of two directions, so tighten them up.
if(direction > 0){
*leny -= (frontiery - *begy);
*begy = frontiery;
}else if(direction < 0){
*leny = frontiery - *begy + 1;
}
return 0;
}
// Draw the specified tablet, if possible. A direction less than 0 means we're
// laying out towards the top. Greater than zero means towards the bottom. 0
// means this is the focused tablet, always the first one to be drawn.
// frontiery is the line on which we're placing the tablet (in the case of the
// focused window, this is only an ideal, subject to change). For direction
// greater than or equal to 0, it's the top line of the tablet. For direction
// less than 0, it's the bottom line. Gives the tablet all possible space to
// work with (i.e. up to the edge we're approaching, or the entire panel for
// the focused tablet). If the callback uses less space, shrinks the panel back
// down before displaying it. Destroys any panel if it ought be hidden.
// Returns 0 if the tablet was able to be wholly rendered, non-zero otherwise.
static int
panelreel_draw_tablet(const panelreel* pr, tablet* t, int frontiery,
int direction){
int lenx, leny, begy, begx;
ncplane* fp = t->p;
if(tablet_columns(pr, &begx, &begy, &lenx, &leny, frontiery, direction)){
//fprintf(stderr, "no room: %p:%p base %d/%d len %d/%d\n", t, fp, begx, begy, lenx, leny);
// fprintf(stderr, "FRONTIER DONE!!!!!!\n");
if(fp){
// fprintf(stderr, "HIDING %p at frontier %d (dir %d) with %d\n", t, frontiery, direction, leny);
ncplane_destroy(fp);
t->p = NULL;
}
return -1;
}
// fprintf(stderr, "tplacement: %p:%p base %d/%d len %d/%d\n", t, fp, begx, begy, lenx, leny);
// fprintf(stderr, "DRAWING %p at frontier %d (dir %d) with %d\n", t, frontiery, direction, leny);
if(fp == NULL){ // create a panel for the tablet
t->p = notcurses_newplane(pr->p->nc, leny + 1, lenx, begy, begx, NULL);
if((fp = t->p) == NULL){
return -1;
}
}else{
int trueby, truebx;
ncplane_yx(fp, &trueby, &truebx);
int truey, truex;
ncplane_dim_yx(fp, &truey, &truex);
if(truey != leny){
// fprintf(stderr, "RESIZE TRUEY: %d BEGY: %d LENY: %d\n", truey, begy, leny);
if(wresize(fp, leny, truex)){
return -1;
}
truey = leny;
}
if(begy != trueby){
ncplane_move_yx(fp, begy, begx);
}
}
if(wresize(fp, leny, lenx)){
return -1;
}
bool cliphead = false;
bool clipfoot = false;
// We pass the coordinates in which the callback may freely write. That's
// the full width (minus tablet borders), and the full range of open space
// in the direction we're moving. We're not passing *lenghts* to the callback,
// but *coordinates* within the window--everywhere save tabletborders.
int cby = 0, cbx = 0, cbmaxy = leny, cbmaxx = lenx;
--cbmaxy;
--cbmaxx;
// If we're drawing up, we'll always have a bottom border unless it's masked
if(direction < 0 && !(pr->popts.tabletmask & BORDERMASK_BOTTOM)){
--cbmaxy;
}
// If we're drawing down, we'll always have a top border unless it's masked
if(direction >= 0 && !(pr->popts.tabletmask & BORDERMASK_TOP)){
++cby;
}
// Adjust the x-bounds for side borders, which we always have if unmasked
cbmaxx -= !(pr->popts.tabletmask & BORDERMASK_RIGHT);
cbx += !(pr->popts.tabletmask & BORDERMASK_LEFT);
bool cbdir = direction < 0 ? true : false;
// fprintf(stderr, "calling! lenx/leny: %d/%d cbx/cby: %d/%d cbmaxx/cbmaxy: %d/%d dir: %d\n",
// lenx, leny, cbx, cby, cbmaxx, cbmaxy, direction);
int ll = t->cbfxn(fp, cbx, cby, cbmaxx, cbmaxy, cbdir, t->curry);
//fprintf(stderr, "RETURNRETURNRETURN %p %d (%d, %d, %d) DIR %d\n",
// t, ll, cby, cbmaxy, leny, direction);
if(ll != leny){
if(ll == leny - 1){ // only has one border visible (partially off-screen)
++ll; // account for that border
wresize(fp, ll, lenx);
if(direction < 0){
cliphead = true;
ncplane_move_yx(fp, begy + leny - ll, begx);
// fprintf(stderr, "MOVEDOWN CLIPPED RESIZED (-1) from %d to %d\n", leny, ll);
}else{
clipfoot = true;
// fprintf(stderr, "RESIZED (-1) from %d to %d\n", leny, ll);
}
}else{ // both borders are visible
ll += 2; // account for both borders
// fprintf(stderr, "RESIZING (-2) from %d to %d\n", leny, ll);
wresize(fp, ll, lenx);
if(direction < 0){
// fprintf(stderr, "MOVEDOWN UNCLIPPED (skip %d)\n", leny - ll);
ncplane_move_yx(fp, begy + leny - ll, begx);
}
}
// The focused tablet will have been resized properly above, but it might
// be out of position (the focused tablet ought move as little as possible).
// Move it back to the frontier, or the nearest line above if it has grown.
if(direction == 0){
if(leny - frontiery + 1 < ll){
//fprintf(stderr, "frontieryIZING ADJ %d %d %d %d NEW %d\n", cbmaxy, leny,
// frontiery, ll, frontiery - ll + 1);
int dontcarex;
ncplane_yx(pr->p, &frontiery, &dontcarex);
frontiery += (leny - ll + 1);
}
ncplane_move_yx(fp, frontiery, begx);
}
}
draw_borders(fp, pr->popts.tabletmask,
direction == 0 ? &pr->popts.focusedattr : &pr->popts.tabletattr,
cliphead, clipfoot);
return cliphead || clipfoot;
}
// draw and size the focused tablet, which must exist (pr->tablets may not be
// NULL). it can occupy the entire panelreel.
static int
draw_focused_tablet(const panelreel* pr){
int pbegy, pbegx, plenx, pleny; // panelreel window coordinates
window_coordinates(pr->p, &pbegy, &pbegx, &pleny, &plenx);
int fulcrum;
if(pr->tablets->p == NULL){
if(pr->last_traveled_direction >= 0){
fulcrum = pleny + pbegy - !(pr->popts.bordermask & BORDERMASK_BOTTOM);
}else{
fulcrum = pbegy + !(pr->popts.bordermask & BORDERMASK_TOP);
}
}else{ // focused was already present. want to stay where we are, if possible
int dontcarex;
ncplane_yx(pr->tablets->p, &fulcrum, &dontcarex);
// FIXME ugh can't we just remember the previous fulcrum?
int prevfulcrum;
ncplane_yx(pr->tablets->prev->p, &prevfulcrum, &dontcarex);
int nextfulcrum;
ncplane_yx(pr->tablets->next->p, &nextfulcrum, &dontcarex);
if(pr->last_traveled_direction > 0 && fulcrum < prevfulcrum){
fulcrum = pleny + pbegy - !(pr->popts.bordermask & BORDERMASK_BOTTOM);
}else if(pr->last_traveled_direction < 0 && fulcrum > nextfulcrum){
fulcrum = pbegy + !(pr->popts.bordermask & BORDERMASK_TOP);
}
}
//fprintf(stderr, "PR dims: %d/%d + %d/%d fulcrum: %d\n", pbegy, pbegx, pleny, plenx, fulcrum);
panelreel_draw_tablet(pr, pr->tablets, fulcrum, 0);
return 0;
}
// move down below the focused tablet, filling up the reel to the bottom.
// returns the last tablet drawn.
static tablet*
draw_following_tablets(const panelreel* pr, const tablet* otherend){
int wmaxy, wbegy, wbegx, wlenx, wleny; // working tablet window coordinates
tablet* working = pr->tablets;
int frontiery;
// move down past the focused tablet, filling up the reel to the bottom
while(working->next != otherend || otherend->p == NULL){
window_coordinates(working->p, &wbegy, &wbegx, &wleny, &wlenx);
wmaxy = wbegy + wleny - 1;
frontiery = wmaxy + 2;
//fprintf(stderr, "EASTBOUND AND DOWN: %d %d\n", frontiery, wmaxy + 2);
working = working->next;
panelreel_draw_tablet(pr, working, frontiery, 1);
if(working->p == NULL){ // FIXME might be more to hide
break;
}
}
// FIXME keep going forward, hiding those no longer visible
return working;
}
// move up above the focused tablet, filling up the reel to the top.
// returns the last tablet drawn.
static tablet*
draw_previous_tablets(const panelreel* pr, const tablet* otherend){
int wbegy, wbegx, wlenx, wleny; // working tablet window coordinates
tablet* upworking = pr->tablets;
int frontiery;
while(upworking->prev != otherend || otherend->p == NULL){
window_coordinates(upworking->p, &wbegy, &wbegx, &wleny, &wlenx);
//fprintf(stderr, "MOVIN' ON UP: %d %d\n", frontiery, wbegy - 2);
frontiery = wbegy - 2;
upworking = upworking->prev;
panelreel_draw_tablet(pr, upworking, frontiery, -1);
if(upworking->p){
window_coordinates(upworking->p, &wbegy, &wbegx, &wleny, &wlenx);
//fprintf(stderr, "new up coords: %d/%d + %d/%d, %d\n", wbegy, wbegx, wleny, wlenx, frontiery);
frontiery = wbegy - 2;
}
if(upworking == otherend){
otherend = otherend->prev;
}
}
// FIXME keep going backwards, hiding those no longer visible
return upworking;
}
// all tablets must be visible (valid ->p), and at least one tablet must exist
static tablet*
find_topmost(panelreel* pr){
tablet* t = pr->tablets;
int curline;
ncplane_yx(t->p, &curline, NULL);
int trialline;
ncplane_yx(t->prev->p, &trialline, NULL);
while(trialline < curline){
t = t->prev;
curline = trialline;
ncplane_yx(t->prev->p, &trialline, NULL);
}
// fprintf(stderr, "topmost: %p @ %d\n", t, curline);
return t;
}
// all the tablets are believed to be wholly visible. in this case, we only want
// to fill up the necessary top rows, even if it means moving everything up at
// the end. large gaps should always be at the bottom to avoid ui discontinuity.
// this must only be called if we actually have at least one tablet. note that
// as a result of this function, we might not longer all be wholly visible.
// good god almighty, this is some fucking garbage.
static int
panelreel_arrange_denormalized(panelreel* pr){
// we'll need the starting line of the tablet which just lost focus, and the
// starting line of the tablet which just gained focus.
int fromline, nowline;
ncplane_yx(pr->tablets->p, &nowline, NULL);
// we've moved to the next or previous tablet. either we were not at the end,
// in which case we can just move the focus, or we were at the end, in which
// case we need bring the target tablet to our end, and draw in the direction
// opposite travel (a single tablet is a trivial case of the latter case).
// how do we know whether we were at the end? if the new line is not in the
// direction of movement relative to the old one, of course!
tablet* topmost = find_topmost(pr);
int wbegy, wbegx, wleny, wlenx;
window_coordinates(pr->p, &wbegy, &wbegx, &wleny, &wlenx);
int frontiery = wbegy + !(pr->popts.bordermask & BORDERMASK_TOP);
if(pr->last_traveled_direction >= 0){
ncplane_yx(pr->tablets->prev->p, &fromline, NULL);
if(fromline > nowline){ // keep the order we had
topmost = topmost->next;
}
}else{
ncplane_yx(pr->tablets->next->p, &fromline, NULL);
if(fromline < nowline){ // keep the order we had
topmost = topmost->prev;
}
}
// fprintf(stderr, "gotta draw 'em all FROM: %d NOW: %d!\n", fromline, nowline);
tablet* t = topmost;
do{
int broken;
if(t == pr->tablets){
broken = panelreel_draw_tablet(pr, t, frontiery, 0);
}else{
broken = panelreel_draw_tablet(pr, t, frontiery, 1);
}
if(t->p == NULL || broken){
pr->all_visible = false;
break;
}
int dontcarex, basey;
ncplane_dim_yx(t->p, &frontiery, &dontcarex);
ncplane_yx(t->p, &basey, &dontcarex);
frontiery += basey + 1;
}while((t = t->next) != topmost);
return 0;
}
// Arrange the panels, starting with the focused window, wherever it may be.
// If necessary, resize it to the full size of the reel--focus has its
// privileges. We then work in the opposite direction of travel, filling out
// the reel above and below. If we moved down to get here, do the tablets above
// first. If we moved up, do the tablets below. This ensures tablets stay in
// place relative to the new focus; they could otherwise pivot around the new
// focus, if we're not filling out the reel.
//
// This can still leave a gap plus a partially-onscreen tablet FIXME
static int
panelreel_arrange(panelreel* pr){
tablet* focused = pr->tablets;
if(focused == NULL){
return 0; // if none are focused, none exist
}
// FIXME we special-cased this because i'm dumb and couldn't think of a more
// elegant way to do this. we keep 'all_visible' as boolean state to avoid
// having to do an o(n) iteration each round, but this is still grotesque, and
// feels fragile...
if(pr->all_visible){
return panelreel_arrange_denormalized(pr);
}
draw_focused_tablet(pr);
tablet* otherend = focused;
if(pr->last_traveled_direction >= 0){
otherend = draw_previous_tablets(pr, otherend);
otherend = draw_following_tablets(pr, otherend);
}else{
otherend = draw_following_tablets(pr, otherend);
otherend = draw_previous_tablets(pr, otherend);
}
// FIXME move them up to plug any holes in original direction?
fprintf(stderr, "DONE ARRANGING\n");
return 0;
}
int panelreel_redraw(panelreel* pr){
fprintf(stderr, "--------> BEGIN REDRAW <--------\n");
int ret = 0;
if(draw_panelreel_borders(pr)){
return -1; // enforces specified dimensional minima
}
ret |= panelreel_arrange(pr);
return ret;
}
static bool
validate_panelreel_opts(ncplane* w, const panelreel_options* popts){
if(w == NULL){
return false;
}
if(!popts->infinitescroll){
if(popts->circular){
return false; // can't set circular without infinitescroll
}
}
const unsigned fullmask = BORDERMASK_LEFT |
BORDERMASK_RIGHT |
BORDERMASK_TOP |
BORDERMASK_BOTTOM;
if(popts->bordermask > fullmask){
return false;
}
if(popts->tabletmask > fullmask){
return false;
}
return true;
}
panelreel* panelreel_create(ncplane* w, const panelreel_options* popts, int efd){
panelreel* pr;
if(!validate_panelreel_opts(w, popts)){
return NULL;
}
if((pr = malloc(sizeof(*pr))) == NULL){
return NULL;
}
pr->efd = efd;
pr->tablets = NULL;
pr->tabletcount = 0;
pr->all_visible = true;
pr->last_traveled_direction = -1; // draw down after the initial tablet
memcpy(&pr->popts, popts, sizeof(*popts));
int maxx, maxy, wx, wy;
window_coordinates(w, &wy, &wx, &maxy, &maxx);
--maxy;
--maxx;
int ylen, xlen;
ylen = maxy - popts->boff - popts->toff + 1;
if(ylen < 0){
ylen = maxy - popts->toff;
if(ylen < 0){
ylen = 0; // but this translates to a full-screen window...FIXME
}
}
xlen = maxx - popts->roff - popts->loff + 1;
if(xlen < 0){
xlen = maxx - popts->loff;
if(xlen < 0){
xlen = 0; // FIXME see above...
}
}
if((pr->p = notcurses_newplane(w->nc, ylen, xlen, popts->toff + wy, popts->loff + wx, NULL)) == NULL){
free(pr);
return NULL;
}
if(panelreel_redraw(pr)){
ncplane_destroy(pr->p);
free(pr);
return NULL;
}
return pr;
}
// we've just added a new tablet. it need be inserted at the correct place in
// the reel. this will naturally fall out of things if the panelreel is full; we
// can just call panelreel_redraw(). otherwise, we need make ourselves at least
// minimally visible, to satisfy the preconditions of
// panelreel_arrange_denormalized(). this function, and approach, is shit.
// FIXME get rid of nc param here
static tablet*
insert_new_panel(struct notcurses* nc, panelreel* pr, tablet* t){
if(!pr->all_visible){
return t;
}
int wbegy, wbegx, wleny, wlenx; // params of PR
window_coordinates(pr->p, &wbegy, &wbegx, &wleny, &wlenx);
// are we the only tablet?
int begx, begy, lenx, leny, frontiery;
if(t->prev == t){
frontiery = wbegy + !(pr->popts.bordermask & BORDERMASK_TOP);
if(tablet_columns(pr, &begx, &begy, &lenx, &leny, frontiery, 1)){
pr->all_visible = false;
return t;
}
fprintf(stderr, "newwin: %d/%d + %d/%d\n", begy, begx, leny, lenx);
if((t->p = notcurses_newplane(nc, leny, lenx, begy, begx, NULL)) == NULL){
pr->all_visible = false;
return t;
}
fprintf(stderr, "created first tablet!\n");
return t;
}
// we're not the only tablet, alas.
// our new window needs to be after our prev
int dontcarex;
ncplane_yx(t->prev->p, &frontiery, &dontcarex);
int dimprevy, dimprevx;
ncplane_dim_yx(t->prev->p, &dimprevy, &dimprevx);
frontiery += dimprevy + 2;
frontiery += 2;
if(tablet_columns(pr, &begx, &begy, &lenx, &leny, frontiery, 1)){
pr->all_visible = false;
return t;
}
if((t->p = notcurses_newplane(nc, 2, lenx, begy, begx, NULL)) == NULL){
pr->all_visible = false;
return t;
}
// FIXME push the other ones down by 4
return t;
}
tablet* panelreel_add(panelreel* pr, tablet* after, tablet *before,
tabletcb cbfxn, void* opaque){
tablet* t;
if(after && before){
if(after->prev != before || before->next != after){
return NULL;
}
}else if(!after && !before){
// This way, without user interaction or any specification, new tablets are
// inserted at the "end" relative to the focus. The first one to be added
// gets and keeps the focus. New ones will go on the bottom, until we run
// out of space. New tablets are then created off-screen.
before = pr->tablets;
}
if((t = malloc(sizeof(*t))) == NULL){
return NULL;
}
fprintf(stderr, "--------->NEW TABLET %p\n", t);
if(after){
t->next = after->next;
after->next = t;
t->prev = after;
t->next->prev = t;
}else if(before){
t->prev = before->prev;
before->prev = t;
t->next = before;
t->prev->next = t;
}else{ // we're the first tablet
t->prev = t->next = t;
pr->tablets = t;
}
t->cbfxn = cbfxn;
t->curry = opaque;
++pr->tabletcount;
t->p = NULL;
// if we have room, it needs become visible immediately, in the proper place,
// lest we invalidate the preconditions of panelreel_arrange_denormalized().
insert_new_panel(pr->p->nc, pr, t);
panelreel_redraw(pr); // don't return failure; tablet was still created...
return t;
}
int panelreel_del_focused(panelreel* pr){
return panelreel_del(pr, pr->tablets);
}
int panelreel_del(panelreel* pr, struct tablet* t){
if(pr == NULL || t == NULL){
return -1;
}
t->prev->next = t->next;
if(pr->tablets == t){
if((pr->tablets = t->next) == t){
pr->tablets = NULL;
}
}
t->next->prev = t->prev;
if(t->p){
ncplane_destroy(t->p);
}
free(t);
--pr->tabletcount;
panelreel_redraw(pr);
return 0;
}
int panelreel_destroy(panelreel* preel){
int ret = 0;
if(preel){
tablet* t = preel->tablets;
while(t){
t->prev->next = NULL;
tablet* tmp = t->next;
panelreel_del(preel, t);
t = tmp;
}
free(preel);
}
return ret;
}
int panelreel_tabletcount(const panelreel* preel){
return preel->tabletcount;
}
int panelreel_touch(panelreel* pr, tablet* t){
(void)t; // FIXME make these more granular eventually
int ret = 0;
if(pr->efd >= 0){
uint64_t val = 1;
if(write(pr->efd, &val, sizeof(val)) != sizeof(val)){
fprintf(stderr, "Error writing to eventfd %d (%s)\n",
pr->efd, strerror(errno));
ret = -1;
}
}
return ret;
}
// Move to some position relative to the current position
static int
move_tablet(ncplane* p, int deltax, int deltay){
int oldx, oldy;
ncplane_yx(p, &oldy, &oldx);
int x = oldx + deltax;
int y = oldy + deltay;
ncplane_move_yx(p, y, x);
return 0;
}
tablet* panelreel_focused(panelreel* pr){
return pr->tablets;
}
int panelreel_move(panelreel* preel, int x, int y){
ncplane* w = preel->p;
int oldx, oldy;
ncplane_yx(w, &oldy, &oldx);
const int deltax = x - oldx;
const int deltay = y - oldy;
if(move_tablet(preel->p, deltax, deltay)){
ncplane_move_yx(preel->p, oldy, oldx);
panelreel_redraw(preel);
return -1;
}
if(preel->tablets){
tablet* t = preel->tablets;
do{
if(t->p == NULL){
break;
}
move_tablet(t->p, deltax, deltay);
}while((t = t->prev) != preel->tablets);
if(t != preel->tablets){ // don't repeat if we covered all tablets
for(t = preel->tablets->next ; t != preel->tablets ; t = t->next){
if(t->p == NULL){
break;
}
move_tablet(t->p, deltax, deltay);
}
}
}
panelreel_redraw(preel);
return 0;
}
tablet* panelreel_next(panelreel* pr){
if(pr->tablets){
pr->tablets = pr->tablets->next;
fprintf(stderr, "---------------> moved to next, %p to %p <----------\n",
pr->tablets->prev, pr->tablets);
pr->last_traveled_direction = 1;
}
panelreel_redraw(pr);
return pr->tablets;
}
tablet* panelreel_prev(panelreel* pr){
if(pr->tablets){
pr->tablets = pr->tablets->prev;
fprintf(stderr, "----------------> moved to prev, %p to %p <----------\n",
pr->tablets->next, pr->tablets);
pr->last_traveled_direction = -1;
}
panelreel_redraw(pr);
return pr->tablets;
}

233
tests/panelreel.cpp Normal file
View File

@ -0,0 +1,233 @@
#include "main.h"
#include <iostream>
class PanelReelTest : public :: testing::Test {
protected:
void SetUp() override {
setlocale(LC_ALL, nullptr);
if(getenv("TERM") == nullptr){
GTEST_SKIP();
}
notcurses_options nopts{};
nopts.inhibit_alternate_screen = true;
nopts.retain_cursor = true;
nopts.pass_through_esc = true;
nopts.outfp = stdin;
nc_ = notcurses_init(&nopts);
ASSERT_NE(nullptr, nc_);
n_ = notcurses_stdplane(nc_);
ASSERT_NE(nullptr, n_);
ASSERT_EQ(0, ncplane_cursor_move_yx(n_, 0, 0));
}
void TearDown() override {
if(nc_){
EXPECT_EQ(0, notcurses_stop(nc_));
}
}
struct notcurses* nc_{};
struct ncplane* n_{};
};
TEST_F(PanelReelTest, InitLinear) {
panelreel_options p = { };
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
}
TEST_F(PanelReelTest, InitLinearInfinite) {
panelreel_options p{};
p.infinitescroll = true;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
}
TEST_F(PanelReelTest, InitCircular) {
panelreel_options p{};
p.infinitescroll = true;
p.circular = true;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
ASSERT_EQ(0, panelreel_destroy(pr));
}
// circular is not allowed to be true when infinitescroll is false
TEST_F(PanelReelTest, FiniteCircleRejected) {
panelreel_options p{};
p.infinitescroll = false;
p.circular = true;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_EQ(nullptr, pr);
}
// We ought be able to invoke panelreel_next() and panelreel_prev() safely,
// even if there are no tablets. They both ought return nullptr.
TEST_F(PanelReelTest, MovementWithoutTablets) {
panelreel_options p{};
p.infinitescroll = false;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
EXPECT_EQ(nullptr, panelreel_next(pr));
// EXPECT_EQ(0, panelreel_validate(n_, pr));
EXPECT_EQ(nullptr, panelreel_prev(pr));
// EXPECT_EQ(0, panelreel_validate(n_, pr));
}
int panelcb(struct ncplane* p, int begx, int begy, int maxx, int maxy,
bool cliptop, void* curry){
EXPECT_NE(nullptr, p);
EXPECT_LT(begx, maxx);
EXPECT_LT(begy, maxy);
EXPECT_EQ(nullptr, curry);
EXPECT_FALSE(cliptop);
// FIXME verify geometry is as expected
return 0;
}
TEST_F(PanelReelTest, OneTablet) {
panelreel_options p{};
p.infinitescroll = false;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
struct tablet* t = panelreel_add(pr, nullptr, nullptr, panelcb, nullptr);
ASSERT_NE(nullptr, t);
// EXPECT_EQ(0, panelreel_validate(n_, pr));
EXPECT_EQ(0, panelreel_del(pr, t));
// EXPECT_EQ(0, panelreel_validate(n_, pr));
}
TEST_F(PanelReelTest, MovementWithOneTablet) {
panelreel_options p{};
p.infinitescroll = false;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
struct tablet* t = panelreel_add(pr, nullptr, nullptr, panelcb, nullptr);
ASSERT_NE(nullptr, t);
// EXPECT_EQ(0, panelreel_validate(n_, pr));
EXPECT_NE(nullptr, panelreel_next(pr));
// EXPECT_EQ(0, panelreel_validate(n_, pr));
EXPECT_NE(nullptr, panelreel_prev(pr));
// EXPECT_EQ(0, panelreel_validate(n_, pr));
EXPECT_EQ(0, panelreel_del(pr, t));
// EXPECT_EQ(0, panelreel_validate(n_, pr));
}
TEST_F(PanelReelTest, DeleteActiveTablet) {
panelreel_options p{};
p.infinitescroll = false;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
struct tablet* t = panelreel_add(pr, nullptr, nullptr, panelcb, nullptr);
ASSERT_NE(nullptr, t);
EXPECT_EQ(0, panelreel_del_focused(pr));
}
TEST_F(PanelReelTest, NoBorder) {
panelreel_options p{};
p.bordermask = BORDERMASK_LEFT | BORDERMASK_RIGHT |
BORDERMASK_TOP | BORDERMASK_BOTTOM;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
}
TEST_F(PanelReelTest, BadBorderBitsRejected) {
panelreel_options p{};
p.bordermask = BORDERMASK_LEFT * 2;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_EQ(nullptr, pr);
}
TEST_F(PanelReelTest, NoTabletBorder) {
panelreel_options p{};
p.tabletmask = BORDERMASK_LEFT | BORDERMASK_RIGHT |
BORDERMASK_TOP | BORDERMASK_BOTTOM;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_NE(nullptr, pr);
}
TEST_F(PanelReelTest, BadTabletBorderBitsRejected) {
panelreel_options p{};
p.tabletmask = BORDERMASK_LEFT * 2;
struct panelreel* pr = panelreel_create(n_, &p, -1);
ASSERT_EQ(nullptr, pr);
}
/*
// Make a target window occupying all but a containing perimeter of the
// specified WINDOW (which will usually be n_).
struct ncpanel* make_targwin(struct ncpanel* w) {
cchar_t cc;
int cpair = COLOR_GREEN;
EXPECT_EQ(OK, setcchar(&cc, L"W", 0, 0, &cpair));
int x, y, xx, yy;
getbegyx(w, y, x);
getmaxyx(w, yy, xx);
yy -= 2;
xx -= 2;
++x;
++y;
WINDOW* ww = subwin(w, yy, xx, y, x);
EXPECT_NE(nullptr, ww);
PANEL* p = new_panel(ww);
EXPECT_NE(nullptr, p);
EXPECT_EQ(OK, wbkgrnd(ww, &cc));
return p;
}
TEST_F(PanelReelTest, InitWithinSubwin) {
panelreel_options p{};
p.loff = 1;
p.roff = 1;
p.toff = 1;
p.boff = 1;
EXPECT_EQ(0, clear());
PANEL* base = make_targwin(n_);
ASSERT_NE(nullptr, base);
WINDOW* basew = panel_window(base);
ASSERT_NE(nullptr, basew);
struct panelreel* pr = panelreel_create(basew, &p, -1);
ASSERT_NE(nullptr, pr);
EXPECT_EQ(0, panelreel_validate(basew, pr));
ASSERT_EQ(0, panelreel_destroy(pr));
EXPECT_EQ(OK, del_panel(base));
EXPECT_EQ(OK, delwin(basew));
}
TEST_F(PanelReelTest, SubwinNoPanelreelBorders) {
panelreel_options p{};
p.loff = 1;
p.roff = 1;
p.toff = 1;
p.boff = 1;
p.bordermask = BORDERMASK_LEFT | BORDERMASK_RIGHT |
BORDERMASK_TOP | BORDERMASK_BOTTOM;
EXPECT_EQ(0, clear());
PANEL* base = make_targwin(n_);
ASSERT_NE(nullptr, base);
WINDOW* basew = panel_window(base);
ASSERT_NE(nullptr, basew);
struct panelreel* pr = panelreel_create(basew, &p, -1);
ASSERT_NE(nullptr, pr);
EXPECT_EQ(0, panelreel_validate(basew, pr));
ASSERT_EQ(0, panelreel_destroy(pr));
EXPECT_EQ(OK, del_panel(base));
EXPECT_EQ(OK, delwin(basew));
}
TEST_F(PanelReelTest, SubwinNoOffsetGeom) {
panelreel_options p{};
EXPECT_EQ(0, clear());
PANEL* base = make_targwin(n_);
ASSERT_NE(nullptr, base);
WINDOW* basew = panel_window(base);
ASSERT_NE(nullptr, basew);
struct panelreel* pr = panelreel_create(basew, &p, -1);
ASSERT_NE(nullptr, pr);
EXPECT_EQ(0, panelreel_validate(basew, pr));
ASSERT_EQ(0, panelreel_destroy(pr));
EXPECT_EQ(OK, del_panel(base));
EXPECT_EQ(OK, delwin(basew));
}
*/