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) include(GNUInstallDirs)
find_package(PkgConfig REQUIRED) find_package(PkgConfig REQUIRED)
find_package(Threads REQUIRED)
pkg_check_modules(TERMINFO REQUIRED tinfo>=6.1) pkg_check_modules(TERMINFO REQUIRED tinfo>=6.1)
pkg_check_modules(AVUTIL REQUIRED libavutil>=56.0) pkg_check_modules(AVUTIL REQUIRED libavutil>=56.0)
pkg_check_modules(AVFORMAT REQUIRED libavformat>=57.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 target_link_libraries(notcurses-demo
PRIVATE PRIVATE
notcurses notcurses
Threads::Threads
) )
target_compile_options(notcurses-demo target_compile_options(notcurses-demo
PRIVATE 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 // 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. // 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 // 0 is returned to indicate that no input was available, but only by
// available. Otherwise (including on EOF) -1 is returned. // notcurses_getc(). Otherwise (including on EOF) -1 is returned.
typedef enum { typedef enum {
NCKEY_INVALID, NCKEY_INVALID,
NCKEY_RESIZE, NCKEY_RESIZE, // generated interally in response to SIGWINCH
NCKEY_UP, NCKEY_UP,
NCKEY_RIGHT, NCKEY_RIGHT,
NCKEY_DOWN, NCKEY_DOWN,
NCKEY_LEFT, NCKEY_LEFT,
NCKEY_DC, // delete
// FIXME... // FIXME...
} ncspecial_key; } ncspecial_key;
@ -660,13 +661,10 @@ typedef struct panelreel_options {
// focused and non-focused tablets can have different styles. you can instead // focused and non-focused tablets can have different styles. you can instead
// draw your own borders, or forgo borders entirely. // draw your own borders, or forgo borders entirely.
unsigned bordermask; // bitfield; 1s will not be drawn (see bordermaskbits) unsigned bordermask; // bitfield; 1s will not be drawn (see bordermaskbits)
uint32_t borderattr; // attributes used for panelreel border, no color! cell borderattr; // attributes used for panelreel border
int borderpair; // extended color pair for panelreel border
unsigned tabletmask; // bitfield; same as bordermask but for tablet borders unsigned tabletmask; // bitfield; same as bordermask but for tablet borders
uint32_t tabletattr; // attributes used for tablet borders, no color! cell tabletattr; // attributes used for tablet borders
int tabletpair; // extended color pair for tablet borders cell focusedattr; // attributes used for focused tablet borders, no color!
uint32_t focusedattr;// attributes used for focused tablet borders, no color!
int focusedpair; // extended color pair for focused tablet borders
} panelreel_options; } panelreel_options;
struct tablet; struct tablet;
@ -679,9 +677,9 @@ struct panelreel;
// and columns can be enforced via popts. efd, if non-negative, is an eventfd // 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 // that ought be written to whenever panelreel_touch() updates a tablet (this
// is useful in the case of nonblocking input). // is useful in the case of nonblocking input).
struct panelreel* panelreel_create(struct ncplane* nc, API struct panelreel* panelreel_create(struct ncplane* nc,
const panelreel_options* popts, const panelreel_options* popts,
int efd); int efd);
// Tablet draw callback, provided a ncplane the first column that may be used, // 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 // 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 // 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). // 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, 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 // 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 // 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 // specified tablet. If both are specifid, the tablet will be added to the
// resulting location, assuming it is valid (after->next == before->prev); if // 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. // it is not valid, or there is any other error, NULL will be returned.
struct tablet* panelreel_add(struct panelreel* pr, struct tablet* after, API struct tablet* panelreel_add(struct panelreel* pr, struct tablet* after,
struct tablet *before, tabletcb cb, void* opaque); struct tablet *before, tabletcb cb,
void* opaque);
// Return the number of tablets. // 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 // 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 // change its display. This will trigger some non-negative number of callbacks
// (though not in the caller's context). // (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 // Delete the tablet specified by t from the panelreel specified by pr. Returns
// -1 if the tablet cannot be found. // -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. // 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. // 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 // Redraw the panelreel in its entirety, for instance after
// clearing the screen due to external corruption, or a SIGWINCH. // 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; // 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. // 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 // 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 // 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 // Destroy a panelreel allocated with panelreel_create(). Does not destroy the
// underlying WINDOW. Returns non-zero on failure. // underlying WINDOW. Returns non-zero on failure.
int panelreel_destroy(struct panelreel* pr); API int panelreel_destroy(struct panelreel* pr);
#undef API #undef API

View File

@ -24,12 +24,13 @@ usage(const char* exe, int status){
fprintf(out, " -d: delay multiplier (float)\n"); fprintf(out, " -d: delay multiplier (float)\n");
fprintf(out, " -f: render to file in addition to stdout\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, "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, " b: run box\n");
fprintf(out, " g: run grid\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, " v: run view\n");
fprintf(out, " w: run widecolors\n"); fprintf(out, " w: run widecolors\n");
exit(status); exit(status);
@ -142,6 +143,7 @@ ext_demos(struct notcurses* nc, const char* demos){
case 'g': ret = grid_demo(nc); break; case 'g': ret = grid_demo(nc); break;
case 'v': ret = view_demo(nc); break; case 'v': ret = view_demo(nc); break;
case 'w': ret = widecolor_demo(nc); break; case 'w': ret = widecolor_demo(nc); break;
case 'p': ret = panelreel_demo(nc); break;
} }
if(ret){ if(ret){
return ret; return ret;
@ -209,7 +211,7 @@ int main(int argc, char** argv){
if(argv[optind] != NULL){ if(argv[optind] != NULL){
usage(*argv, EXIT_FAILURE); usage(*argv, EXIT_FAILURE);
} }
demos = "isumbgwv"; demos = "isumbgwvp";
} }
if((nc = notcurses_init(&nopts)) == NULL){ if((nc = notcurses_init(&nopts)) == NULL){
return EXIT_FAILURE; return EXIT_FAILURE;

View File

@ -20,6 +20,7 @@ int maxcolor_demo(struct notcurses* nc);
int grid_demo(struct notcurses* nc); int grid_demo(struct notcurses* nc);
int sliding_puzzle_demo(struct notcurses* nc); int sliding_puzzle_demo(struct notcurses* nc);
int view_demo(struct notcurses* nc); int view_demo(struct notcurses* nc);
int panelreel_demo(struct notcurses* nc);
#ifdef __cplusplus #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", "kācaṃ śaknomyattum; nopahinasti mām",
"ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει", "ὕαλον ϕαγεῖν δύναμαι· τοῦτο οὔ με βλάπτει",
"Μπορῶ νὰ φάω σπασμένα γυαλιὰ χωρὶς νὰ πάθω τίποτα", "Μπορῶ νὰ φάω σπασμένα γυαλιὰ χωρὶς νὰ πάθω τίποτα",
"Vitrum edere possum; mihi non nocet", "Vitrum edere possum; mihi non nocet",
"🚬🌿💉💊☢☣🔫💣⚔🤜🤛🧠🦹🤺🏋️,🦔🐧🐣🦆🦢🦜🦉🐊🐸🦕 🦖🐬🐙🦂🦠🦀", // "🚬🌿💉💊☢☣🔫💣⚔🤜🤛🧠🦹🤺🏋️,🦔🐧🐣🦆🦢🦜🦉🐊🐸🦕 🦖🐬🐙🦂🦠🦀",
*/
"Je puis mangier del voirre. Ne me nuit", "Je puis mangier del voirre. Ne me nuit",
"Je peux manger du verre, ça ne me fait pas mal", "Je peux manger du verre, ça ne me fait pas mal",
"Pòdi manjar de veire, me nafrariá pas", "Pòdi manjar de veire, me nafrariá pas",

View File

@ -6,6 +6,7 @@
#include <stdarg.h> #include <stdarg.h>
#include <string.h> #include <string.h>
#include <stdbool.h> #include <stdbool.h>
#include "egcpool.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
@ -18,7 +19,35 @@ struct AVCodec;
struct AVCodecParameters; struct AVCodecParameters;
struct AVPacket; struct AVPacket;
struct SwsContext; 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 { typedef struct ncvisual {
struct AVFormatContext* fmtctx; struct AVFormatContext* fmtctx;
@ -31,7 +60,7 @@ typedef struct ncvisual {
struct SwsContext* swsctx; struct SwsContext* swsctx;
int packet_outstanding; int packet_outstanding;
int dstwidth, dstheight; int dstwidth, dstheight;
struct ncplane* ncp; ncplane* ncp;
} ncvisual; } ncvisual;
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -26,35 +26,6 @@
#define ESC "\x1b" #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 { typedef struct ncstats {
uint64_t renders; // number of notcurses_render() runs uint64_t renders; // number of notcurses_render() runs
uint64_t renders_ns; // number of nanoseconds spent in notcurses_render() 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){ void ncplane_yx(const ncplane* n, int* y, int* x){
*y = n->absy; if(y){
*x = n->absx; *y = n->absy;
}
if(x){
*x = n->absx;
}
} }
// copy the UTF8-encoded EGC out of the cell, whether simple or complex. the // 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));
}
*/