diff --git a/CMakeLists.txt b/CMakeLists.txt index 9a82b783e..ff16ad955 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/include/notcurses.h b/include/notcurses.h index 44793f2a2..61d0a9dab 100644 --- a/include/notcurses.h +++ b/include/notcurses.h @@ -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 diff --git a/src/demo/demo.c b/src/demo/demo.c index c3be7a5f7..321a3fe23 100644 --- a/src/demo/demo.c +++ b/src/demo/demo.c @@ -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; diff --git a/src/demo/demo.h b/src/demo/demo.h index c85f5e14c..f81d02851 100644 --- a/src/demo/demo.h +++ b/src/demo/demo.h @@ -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 } diff --git a/src/demo/panelreel.c b/src/demo/panelreel.c new file mode 100644 index 000000000..4e48f3309 --- /dev/null +++ b/src/demo/panelreel.c @@ -0,0 +1,349 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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; +} diff --git a/src/demo/widecolor.c b/src/demo/widecolor.c index b3a15bceb..c1bf2e588 100644 --- a/src/demo/widecolor.c +++ b/src/demo/widecolor.c @@ -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", diff --git a/src/lib/internal.h b/src/lib/internal.h index 09cceea51..af4456951 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -6,6 +6,7 @@ #include #include #include +#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 diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index 63d77ffd4..d59c2cdf7 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -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 diff --git a/src/lib/panelreel.c b/src/lib/panelreel.c new file mode 100644 index 000000000..b014929a0 --- /dev/null +++ b/src/lib/panelreel.c @@ -0,0 +1,816 @@ +#include +#include +#include +#include +#include +#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; +} diff --git a/tests/panelreel.cpp b/tests/panelreel.cpp new file mode 100644 index 000000000..5294536d4 --- /dev/null +++ b/tests/panelreel.cpp @@ -0,0 +1,233 @@ +#include "main.h" +#include + +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)); +} +*/