diff --git a/README.md b/README.md index 0bc12ec58..03f184d14 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ by [nick black](https://nick-black.com/dankwiki/index.php/Hack_on) (gcluster < 0x80; } +static inline int +cell_load_simple(struct ncplane* n, cell* c, char ch){ + cell_release(n, c); + c->channels &= ~CELL_WIDEASIAN_MASK; + c->gcluster = ch; + if(cell_simple_p(c)){ + return 1; + } + return -1; +} + // get the offset into the egcpool for this cell's EGC. returns meaningless and // unsafe results if called on a simple cell. static inline uint32_t diff --git a/doc/man/man1/notcurses-demo.1 b/doc/man/man1/notcurses-demo.1 index b4cb87a72..545e0c9df 100644 --- a/doc/man/man1/notcurses-demo.1 +++ b/doc/man/man1/notcurses-demo.1 @@ -27,25 +27,25 @@ contains a set of text-based demonstrations of capabilities from the notcurses l .P (i)ntro—a setting of tone .P -(s)liders—a missing-piece puzzle made up of colorful blocks -.P -(u)niblocks—a series of blocks detailing Unicode pages -.P (m)axcolors—smoothly changing colors .P (l)uigi-a dashing plumber of Apennine persuasion .P +(u)niblocks—a series of blocks detailing Unicode pages +.P (b)oxes—pulsating boxes with a transparent center .P (g)rid—a gradient of color lain atop a great grid .P -(w)idecolors—letters of many languages in many colors +(s)liders—a missing-piece puzzle made up of colorful blocks .P -(v)iew—in which PNGs are rendered as text, and a video, too +(w)idechomper—a gremlin feasts upon wide characters +.P +(v)iew—images and a video are rendered as text .P (p)anelreels—demonstration of the panelreel high-level widget .P -(o)utro—a message of hope from the library author +(o)utro—a message of hope from the library's author .SH NOTES Proper display requires a terminal advertising the RGB terminfo(5) capability (necessary for specification of arbitrary 24bpp colors), and a monospaced font with good Unicode support. .SH SEE ALSO diff --git a/include/notcurses.h b/include/notcurses.h index a1d193898..e92052624 100644 --- a/include/notcurses.h +++ b/include/notcurses.h @@ -379,6 +379,10 @@ API struct ncplane* ncplane_below(struct ncplane* n); // it in 'c'. This copy is safe to use until the ncplane is destroyed/erased. API int ncplane_at_cursor(struct ncplane* n, cell* c); +// Retrieve the cell at the specified location on the specified plane, returning +// it in 'c'. This copy is safe to use until the ncplane is destroyed/erased. +API int ncplane_at_yx(struct ncplane* n, int y, int x, cell* c); + // Manipulate the opaque user pointer associated with this plane. // ncplane_set_userptr() returns the previous userptr after replacing // it with 'opaque'. the others simply return the userptr. @@ -1101,7 +1105,8 @@ API int ncplane_fadein(struct ncplane* n, const struct timespec* ts); // Working with cells #define CELL_TRIVIAL_INITIALIZER { .gcluster = '\0', .attrword = 0, .channels = 0, } -#define CELL_SIMPLE_INITIALIZER(c) { .gcluster = c, .attrword = 0, .channels = 0, } +#define CELL_SIMPLE_INITIALIZER(c) { .gcluster = (c), .attrword = 0, .channels = 0, } +#define CELL_INITIALIZER(c, a, chan) { .gcluster = (c), .attrword = (a), .channels = (chan), } static inline void cell_init(cell* c){ @@ -1201,6 +1206,17 @@ cell_simple_p(const cell* c){ return c->gcluster < 0x80; } +static inline int +cell_load_simple(struct ncplane* n, cell* c, char ch){ + cell_release(n, c); + c->channels &= ~CELL_WIDEASIAN_MASK; + c->gcluster = ch; + if(cell_simple_p(c)){ + return 1; + } + return -1; +} + // get the offset into the egcpool for this cell's EGC. returns meaningless and // unsafe results if called on a simple cell. static inline uint32_t diff --git a/src/demo/demo.c b/src/demo/demo.c index 65e62bcce..f868dc283 100644 --- a/src/demo/demo.c +++ b/src/demo/demo.c @@ -52,7 +52,7 @@ usage(const char* exe, int status){ fprintf(out, " s: run shuffle\n"); fprintf(out, " u: run uniblock\n"); fprintf(out, " v: run view\n"); - fprintf(out, " w: run widecolors\n"); + fprintf(out, " w: run widechomper\n"); exit(status); } @@ -156,7 +156,7 @@ ext_demos(struct notcurses* nc, const char* demos){ case 'g': ret = grid_demo(nc); break; case 'l': ret = luigi_demo(nc); break; case 'v': ret = view_demo(nc); break; - case 'w': ret = widecolor_demo(nc); break; + case 'w': ret = widechomper_demo(nc); break; case 'p': ret = panelreel_demo(nc); break; default: fprintf(stderr, "Unknown demo specification: %c\n", *demos); diff --git a/src/demo/demo.h b/src/demo/demo.h index 48b2c74e0..e1f1e87f5 100644 --- a/src/demo/demo.h +++ b/src/demo/demo.h @@ -12,7 +12,7 @@ extern "C" { extern struct timespec demodelay; int unicodeblocks_demo(struct notcurses* nc); -int widecolor_demo(struct notcurses* nc); +int widechomper_demo(struct notcurses* nc); int box_demo(struct notcurses* nc); int maxcolor_demo(struct notcurses* nc); int grid_demo(struct notcurses* nc); diff --git a/src/demo/grid.c b/src/demo/grid.c index 0e3f209e5..4979ea6dc 100644 --- a/src/demo/grid.c +++ b/src/demo/grid.c @@ -102,6 +102,9 @@ gridswitch_demo(struct notcurses* nc, struct ncplane *n){ // center for(y = 1 ; y < maxy - 1 ; ++y){ x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_fg_rgb(&cl, 255 - rs * y, 255 - gs * (x + y), 255 - bs * x); cell_set_bg_rgb(&cl, 255 - rs * x, 255 - gs * (x + y), 255 - bs * y); ncplane_putc(n, &cl); @@ -117,6 +120,9 @@ gridswitch_demo(struct notcurses* nc, struct ncplane *n){ // bottom line x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_fg_rgb(&ll, 255 - rs * y, 255 - gs * (x + y), 255 - bs * x); cell_set_bg_rgb(&ll, 255 - rs * x, 255 - gs * (x + y), 255 - bs * y); ncplane_putc(n, &ll); @@ -167,6 +173,9 @@ gridinv_demo(struct notcurses* nc, struct ncplane *n){ // center for(y = 1 ; y < maxy - 1 ; ++y){ x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_fg_rgb(&cl, 0, 0, 0); cell_set_bg_rgb(&cl, 255 - rs * x, 255 - gs * (x + y), 255 - bs * y); ncplane_putc(n, &cl); @@ -182,6 +191,9 @@ gridinv_demo(struct notcurses* nc, struct ncplane *n){ // bottom line x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_fg_rgb(&ll, 0, 0, 0); cell_set_bg_rgb(&ll, 255 - rs * x, 255 - gs * (x + y), 255 - bs * y); ncplane_putc(n, &ll); @@ -233,6 +245,9 @@ int grid_demo(struct notcurses* nc){ // center for(y = 1 ; y < maxy - 1 ; ++y){ x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_bg_rgb(&cl, y, y, y); cell_set_bg_rgb(&cc, y, y, y); cell_set_bg_rgb(&cr, y, y, y); @@ -248,6 +263,9 @@ int grid_demo(struct notcurses* nc){ // bottom line x = 0; + if(ncplane_cursor_move_yx(n, y, x)){ + return -1; + } cell_set_bg_rgb(&ll, y, y, y); cell_set_bg_rgb(&lc, y, y, y); cell_set_bg_rgb(&lr, y, y, y); diff --git a/src/demo/widecolor.c b/src/demo/widechomper.c similarity index 97% rename from src/demo/widecolor.c rename to src/demo/widechomper.c index cda04a369..42636c971 100644 --- a/src/demo/widecolor.c +++ b/src/demo/widechomper.c @@ -7,6 +7,9 @@ #include #include "demo.h" +// Fill up the screen with as much crazy Unicode as we can, and then set a +// gremlin loose, looking to eat up all the wide characters. + // FIXME throw this in there somehow // ∮ E⋅da = Q, n → ∞, ∑ f(i) = ∏ g(i), ⎧⎡⎛┌─────┐⎞⎤⎫ // ⎪⎢⎜│a²+b³ ⎟⎥⎪ @@ -283,6 +286,7 @@ message(struct ncplane* n, int maxy, int maxx, int num, int total, int bytes_out, int egs_out, int cols_out){ cell c = CELL_TRIVIAL_INITIALIZER; cell_load(n, &c, " "); + cell_set_fg_alpha(&c, CELL_ALPHA_TRANS); cell_set_bg_alpha(&c, CELL_ALPHA_TRANS); ncplane_set_default(n, &c); cell_release(n, &c); @@ -297,7 +301,7 @@ message(struct ncplane* n, int maxy, int maxx, int num, int total, } // bottom handle ncplane_cursor_move_yx(n, 4, 17); - ncplane_putegc(n, "┬", 0, 0, NULL); + ncplane_putegc(n, "┬", 0, channels, NULL); ncplane_cursor_move_yx(n, 5, 17); ncplane_putegc(n, "│", 0, channels, NULL); ncplane_cursor_move_yx(n, 6, 17); @@ -344,7 +348,7 @@ message(struct ncplane* n, int maxy, int maxx, int num, int total, } // Much of this text comes from http://kermitproject.org/utf8.html -int widecolor_demo(struct notcurses* nc){ +int widechomper_demo(struct notcurses* nc){ static const char* strs[] = { "Война и мир", "Бра́тья Карама́зовы", @@ -575,9 +579,8 @@ int widecolor_demo(struct notcurses* nc){ NULL }; const char** s; - int count = notcurses_palette_size(nc); - const int steps[] = { 1, 0x100, 0x40000, 0x10001, }; - const int starts[] = { 0x4000, 0x40, 0x10000, 0x400040, }; + const int steps[] = { 0x100, 0x100, 0x40000, 0x10001, }; + const int starts[] = { 0x004000, 0x000040, 0x010101, 0x400040, }; struct ncplane* n = notcurses_stdplane(nc); size_t i; @@ -588,13 +591,10 @@ int widecolor_demo(struct notcurses* nc){ cell c; struct timespec screenend; clock_gettime(CLOCK_MONOTONIC, &screenend); - ns_to_timespec(timespec_to_ns(&screenend) + timespec_to_ns(&demodelay), &screenend); + ns_to_timespec(timespec_to_ns(&screenend) + 2 * timespec_to_ns(&demodelay), &screenend); do{ // (re)draw a screen const int start = starts[i]; int step = steps[i]; - const int rollover = 256 / ((step & 0xff) | ((step & 0xff00) >> 8u) - | ((step & 0xff0000) >> 16u)); - int rollcount = 0; // number of times we've added this step cell_init(&c); int y, x, maxy, maxx; ncplane_dim_yx(n, &maxy, &maxx); @@ -634,11 +634,18 @@ int widecolor_demo(struct notcurses* nc){ } int ulen = 0; int r; - if((r = ncplane_putegc(n, &(*s)[idx], 0, channels, &ulen)) < 0){ - if(ulen < 0){ + if(wcwidth(wcs) <= maxx - x){ + if((r = ncplane_putegc(n, &(*s)[idx], 0, channels, &ulen)) < 0){ + if(ulen < 0){ + return -1; + } + } + }else{ + cell octo = CELL_INITIALIZER('#', 0, channels); + if((r = ncplane_putc(n, &octo)) < 1){ return -1; } - break; + cell_release(n, &octo); } ncplane_cursor_yx(n, &y, &x); idx += ulen; @@ -646,19 +653,7 @@ int widecolor_demo(struct notcurses* nc){ cols_out += r; ++egcs_out; } - if(++rollcount % rollover == 0){ - step *= 256; - } - if((unsigned)step >= 1ul << 24){ - step >>= 24u; - } - if(step == 0){ - step = 1; - } - if((rgb += step) >= count){ - rgb = 0; - step *= 256; - } + rgb += step; } }while(y < maxy && x < maxx); struct ncplane* mess = notcurses_newplane(nc, 7, 57, 1, 4, NULL); @@ -697,7 +692,7 @@ int widecolor_demo(struct notcurses* nc){ pthread_join(tid, NULL); ncplane_destroy(mess); if(key == NCKEY_RESIZE){ - notcurses_resize(nc, NULL, NULL); + notcurses_resize(nc, &maxy, &maxx); } }while(key == NCKEY_RESIZE); } diff --git a/src/lib/internal.h b/src/lib/internal.h index 6c0087184..e238aea8b 100644 --- a/src/lib/internal.h +++ b/src/lib/internal.h @@ -197,6 +197,31 @@ rgb_quantize_256(unsigned r, unsigned g, unsigned b){ return r * 36 + g * 6 + b + 16; } +static inline int +term_emit(const char* name __attribute__ ((unused)), const char* seq, + FILE* out, bool flush){ + int ret = fprintf(out, "%s", seq); + if(ret < 0){ +// fprintf(stderr, "Error emitting %zub %s escape (%s)\n", strlen(seq), name, strerror(errno)); + return -1; + } + if((size_t)ret != strlen(seq)){ +// fprintf(stderr, "Short write (%db) for %zub %s sequence\n", ret, strlen(seq), name); + return -1; + } + if(flush && fflush(out)){ +// fprintf(stderr, "Error flushing after %db %s sequence (%s)\n", ret, name, strerror(errno)); + return -1; + } + return 0; +} + +static inline const char* +extended_gcluster(const ncplane* n, const cell* c){ + uint32_t idx = cell_egc_idx(c); + return n->pool.pool + idx; +} + #define NANOSECS_IN_SEC 1000000000 #ifdef __cplusplus diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index ba068fbab..407a04974 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -2,7 +2,6 @@ #include #include #include -#include #include #include #include @@ -157,40 +156,6 @@ int ncplane_putwstr_aligned(struct ncplane* n, int y, const wchar_t* gclustarr, return ncplane_putwstr(n, gclustarr); } -static inline uint64_t -timespec_to_ns(const struct timespec* t){ - return t->tv_sec * NANOSECS_IN_SEC + t->tv_nsec; -} - -static void -update_render_stats(const struct timespec* time1, const struct timespec* time0, - ncstats* stats, int bytes){ - int64_t elapsed = timespec_to_ns(time1) - timespec_to_ns(time0); - //fprintf(stderr, "Rendering took %ld.%03lds\n", elapsed / NANOSECS_IN_SEC, - // (elapsed % NANOSECS_IN_SEC) / 1000000); - if(bytes >= 0){ - stats->render_bytes += bytes; - if(bytes > stats->render_max_bytes){ - stats->render_max_bytes = bytes; - } - if(bytes < stats->render_min_bytes){ - stats->render_min_bytes = bytes; - } - }else{ - ++stats->failed_renders; - } - if(elapsed > 0){ // don't count clearly incorrect information, egads - ++stats->renders; - stats->render_ns += elapsed; - if(elapsed > stats->render_max_ns){ - stats->render_max_ns = elapsed; - } - if(elapsed < stats->render_min_ns){ - stats->render_min_ns = elapsed; - } - } -} - static const char NOTCURSES_VERSION[] = notcurses_VERSION_MAJOR "." notcurses_VERSION_MINOR "." @@ -238,6 +203,16 @@ int ncplane_at_cursor(ncplane* n, cell* c){ return cell_duplicate(n, c, &n->fb[fbcellidx(n, n->y, n->x)]); } +int ncplane_at_yx(ncplane* n, int y, int x, cell* c){ + if(y >= n->leny || x >= n->lenx){ + return true; + } + if(y < 0 || x < 0){ + return true; + } + return cell_duplicate(n, c, &n->fb[fbcellidx(n, y, x)]); +} + void ncplane_dim_yx(const ncplane* n, int* rows, int* cols){ if(rows){ *rows = n->leny; @@ -295,24 +270,6 @@ free_plane(ncplane* p){ } } -static int -term_emit(const char* name, const char* seq, FILE* out, bool flush){ - int ret = fprintf(out, "%s", seq); - if(ret < 0){ - fprintf(stderr, "Error emitting %zub %s escape (%s)\n", strlen(seq), name, strerror(errno)); - return -1; - } - if((size_t)ret != strlen(seq)){ - fprintf(stderr, "Short write (%db) for %zub %s sequence\n", ret, strlen(seq), name); - return -1; - } - if(flush && fflush(out)){ - fprintf(stderr, "Error flushing after %db %s sequence (%s)\n", ret, name, strerror(errno)); - return -1; - } - return 0; -} - // create a new ncplane at the specified location (relative to the true screen, // having origin at 0,0), having the specified size, and put it at the top of // the planestack. its cursor starts at its origin; its style starts as null. @@ -931,139 +888,10 @@ int ncplane_default(ncplane* ncp, cell* c){ return cell_duplicate(ncp, c, &ncp->defcell); } -// 3 for foreground, 4 for background, ugh FIXME -static int -term_esc_rgb(notcurses* nc __attribute__ ((unused)), FILE* out, int esc, - unsigned r, unsigned g, unsigned b){ - // The correct way to do this is using tiparm+tputs, but doing so (at least - // as of terminfo 6.1.20191019) both emits ~3% more bytes for a run of 'rgb' - // and gives rise to some corrupted cells (possibly due to special handling of - // values < 256; I'm not at this time sure). So we just cons up our own. - /*if(esc == 4){ - return term_emit("setab", tiparm(nc->setab, (int)((r << 16u) | (g << 8u) | b)), out, false); - }else if(esc == 3){ - return term_emit("setaf", tiparm(nc->setaf, (int)((r << 16u) | (g << 8u) | b)), out, false); - }else{ - return -1; - }*/ - #define RGBESC1 "\x1b" "[" - #define RGBESC2 "8;2;" - // rrr;ggg;bbbm - char rgbesc[] = RGBESC1 " " RGBESC2 " "; - int len = strlen(RGBESC1); - rgbesc[len++] = esc; - len += strlen(RGBESC2); - if(r > 99){ - rgbesc[len++] = r / 100 + '0'; - } - if(r > 9){ - rgbesc[len++] = (r % 100) / 10 + '0'; - } - rgbesc[len++] = (r % 10) + '0'; - rgbesc[len++] = ';'; - if(g > 99){ rgbesc[len++] = g / 100 + '0'; } - if(g > 9){ rgbesc[len++] = (g % 100) / 10 + '0'; } - rgbesc[len++] = g % 10 + '0'; - rgbesc[len++] = ';'; - if(b > 99){ rgbesc[len++] = b / 100 + '0'; } - if(b > 9){ rgbesc[len++] = (b % 100) / 10 + '0'; } - rgbesc[len++] = b % 10 + '0'; - rgbesc[len++] = 'm'; - rgbesc[len] = '\0'; - int w; - if((w = fputs_unlocked(rgbesc, out)) < len){ - return -1; - } - return 0; -} - -static int -term_bg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ - // We typically want to use tputs() and tiperm() to acquire and write the - // escapes, as these take into account terminal-specific delays, padding, - // etc. For the case of DirectColor, there is no suitable terminfo entry, but - // we're also in that case working with hopefully more robust terminals. - // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself. - if(nc->RGBflag){ - return term_esc_rgb(nc, out, '4', r, g, b); - }else{ - if(nc->setab == NULL){ - return -1; - } - // For 256-color indexed mode, start constructing a palette based off - // the inputs *if we can change the palette*. If more than 256 are used on - // a single screen, start... combining close ones? For 8-color mode, simple - // interpolation. I have no idea what to do for 88 colors. FIXME - if(nc->colors >= 256){ - term_emit("setab", tiparm(nc->setab, rgb_quantize_256(r, g, b)), out, false); - } - return -1; - } - return 0; -} - -static int -term_fg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ - // We typically want to use tputs() and tiperm() to acquire and write the - // escapes, as these take into account terminal-specific delays, padding, - // etc. For the case of DirectColor, there is no suitable terminfo entry, but - // we're also in that case working with hopefully more robust terminals. - // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself. - if(nc->RGBflag){ - return term_esc_rgb(nc, out, '3', r, g, b); - }else{ - if(nc->setaf == NULL){ - return -1; - } - if(nc->colors >= 256){ - term_emit("setaf", tiparm(nc->setaf, rgb_quantize_256(r, g, b)), out, false); - } - // For 256-color indexed mode, start constructing a palette based off - // the inputs *if we can change the palette*. If more than 256 are used on - // a single screen, start... combining close ones? For 8-color mode, simple - // interpolation. I have no idea what to do for 88 colors. FIXME - return -1; - } - return 0; -} - -static inline const char* -extended_gcluster(const ncplane* n, const cell* c){ - uint32_t idx = cell_egc_idx(c); - return n->pool.pool + idx; -} - const char* cell_extended_gcluster(const struct ncplane* n, const cell* c){ return extended_gcluster(n, c); } -// write the cell's UTF-8 grapheme cluster to the provided FILE*. returns the -// number of columns occupied by this EGC (only an approximation; it's actually -// a property of the font being used). -static int -term_putc(FILE* out, const ncplane* n, const cell* c){ - if(cell_simple_p(c)){ - if(c->gcluster == 0 || iscntrl(c->gcluster)){ -// fprintf(stderr, "[ ]\n"); - if(fputc_unlocked(' ', out) == EOF){ - return -1; - } - }else{ -// fprintf(stderr, "[%c]\n", c->gcluster); - if(fputc_unlocked(c->gcluster, out) == EOF){ - return -1; - } - } - }else{ - const char* ext = extended_gcluster(n, c); -// fprintf(stderr, "[%s]\n", ext); - if(fputs_unlocked(ext, out) < 0){ // FIXME check for short write? - return -1; - } - } - return 0; -} - static void advance_cursor(ncplane* n, int cols){ if(cursor_invalid_p(n)){ @@ -1071,156 +899,10 @@ advance_cursor(ncplane* n, int cols){ } if((n->x += cols) >= n->lenx){ ++n->y; - n->x -= n->lenx; + n->x = 0; } } -// check the current and target style bitmasks against the specified 'stylebit'. -// if they are different, and we have the necessary capability, write the -// applicable terminfo entry to 'out'. returns -1 only on a true error. -static int -term_setstyle(FILE* out, unsigned cur, unsigned targ, unsigned stylebit, - const char* ton, const char* toff){ - int ret = 0; - unsigned curon = cur & stylebit; - unsigned targon = targ & stylebit; - if(curon != targon){ - if(targon){ - if(ton){ - ret = term_emit("ton", ton, out, false); - } - }else{ - if(toff){ // how did this happen? we can turn it on, but not off? - ret = term_emit("toff", toff, out, false); - } - } - } - if(ret < 0){ - return -1; - } - return 0; -} - -// write any escape sequences necessary to set the desired style -static int -term_setstyles(const notcurses* nc, FILE* out, uint32_t* curattr, const cell* c, - bool* normalized){ - *normalized = false; - uint32_t cellattr = cell_styles(c); - if(cellattr == *curattr){ - return 0; // happy agreement, change nothing - } - int ret = 0; - // if only italics changed, don't emit any sgr escapes. xor of current and - // target ought have all 0s in the lower 8 bits if only italics changed. - if((cellattr ^ *curattr) & 0x00ff0000ul){ - *normalized = true; // FIXME this is pretty conservative - // if everything's 0, emit the shorter sgr0 - if(nc->sgr0 && ((cellattr & CELL_STYLE_MASK) == 0)){ - if(term_emit("sgr0", nc->sgr0, out, false) < 0){ - ret = -1; - } - }else if(term_emit("sgr", tiparm(nc->sgr, cellattr & CELL_STYLE_STANDOUT, - cellattr & CELL_STYLE_UNDERLINE, - cellattr & CELL_STYLE_REVERSE, - cellattr & CELL_STYLE_BLINK, - cellattr & CELL_STYLE_DIM, - cellattr & CELL_STYLE_BOLD, - cellattr & CELL_STYLE_INVIS, - cellattr & CELL_STYLE_PROTECT, 0), - out, false) < 0){ - ret = -1; - } - } - // sgr will blow away italics if they were set beforehand - ret |= term_setstyle(out, *curattr, cellattr, CELL_STYLE_ITALIC, nc->italics, nc->italoff); - *curattr = cellattr; - return ret; -} - -// Find the topmost cell for this coordinate by walking down the z-buffer, -// looking for an intersecting ncplane. Once we've found one, check it for -// transparency in either the back- or foreground. If the alpha channel is -// active, keep descending and blending until we hit opacity, or bedrock. We -// recurse to find opacity, and blend the result into what we have. The -// 'findfore' and 'findback' bools control our recursion--there's no point in -// going further down when a color is locked in, so don't (for instance) recurse -// further when we have a transparent foreground and opaque background atop an -// opaque foreground and transparent background. The cell we ultimately return -// (a const ref to 'c') is backed by '*retp' via rawdog copy; the caller must -// not call cell_release() upon it, nor use it beyond the scope of the render. -// -// So, as we go down, we find planes which can have impact on the result. Once -// we've locked the result in (base case), write the deep values we have to 'c'. -// Then, as we come back up, blend them as appropriate. The actual glyph is -// whichever one occurs at the top with a non-transparent α (α < 3). To effect -// tail recursion, though, we instead write first, and then recurse, blending -// as we descend. α <= 0 is opaque. α >= 3 is fully transparent. -static ncplane* -dig_visible_cell(cell* c, int y, int x, ncplane* p, int falpha, int balpha, - bool* damage){ - while(p){ - // where in the plane this coordinate would be, based off absy/absx. the - // true origin is 0,0, so abs=2,2 means coordinate 3,3 would be 1,1, while - // abs=-2,-2 would make coordinate 3,3 relative 5, 5. - int poffx, poffy; - poffy = y - p->absy; - poffx = x - p->absx; - if(poffy < p->leny && poffy >= 0){ - if(poffx < p->lenx && poffx >= 0){ // p is valid for this y, x - const cell* vis = &p->fb[fbcellidx(p, poffy, poffx)]; - // if we never loaded any content into the cell (or obliterated it by - // writing in a zero), use the plane's background cell. - if(vis->gcluster == 0){ - vis = &p->defcell; - } - bool lockedglyph = false; - int nalpha; - if(falpha > 0 && (nalpha = cell_get_fg_alpha(vis)) < CELL_ALPHA_TRANS){ - if(c->gcluster == 0){ // never write fully trans glyphs, never replace - if( (c->gcluster = vis->gcluster) ){ // index copy only - lockedglyph = true; // must return this ncplane for this glyph - c->attrword = vis->attrword; - cell_set_fchannel(c, cell_get_fchannel(vis)); // FIXME blend it in - falpha -= (CELL_ALPHA_TRANS - nalpha); // FIXME blend it in - if(p->damage[poffy]){ - *damage = true; - p->damage[poffy] = false; - } - } - } - } - if(balpha > 0 && (nalpha = cell_get_bg_alpha(vis)) < CELL_ALPHA_TRANS){ - cell_set_bchannel(c, cell_get_bchannel(vis)); // FIXME blend it in - balpha -= (CELL_ALPHA_TRANS - nalpha); - if(p->damage[poffy]){ - *damage = true; - p->damage[poffy] = false; - } - } - if((falpha > 0 || balpha > 0) && p->z){ // we must go further! - ncplane* cand = dig_visible_cell(c, y, x, p->z, falpha, balpha, damage); - if(!lockedglyph && cand){ - p = cand; - } - } - return p; - } - } - p = p->z; - } - // should never happen for valid y, x thanks to the stdplane. you fucked up! - return NULL; -} - -static inline ncplane* -visible_cell(cell* c, int y, int x, ncplane* n, bool* damage){ - cell_init(c); - return dig_visible_cell(c, y, x, n, CELL_ALPHA_TRANS, CELL_ALPHA_TRANS, damage); -} - -// Call with c->gcluster == 3, falpha == 3, balpha == 0, *retp == topplane. - // 'n' ends up above 'above' int ncplane_move_above_unsafe(ncplane* restrict n, ncplane* restrict above){ ncplane** an = find_above_ncplane(n); @@ -1279,232 +961,6 @@ int ncplane_move_bottom(ncplane* n){ return 0; } -static int -blocking_write(int fd, const char* buf, size_t buflen){ - size_t written = 0; - do{ - ssize_t w = write(fd, buf + written, buflen - written); - if(w < 0){ - if(errno != EAGAIN && errno != EWOULDBLOCK){ - return -1; - } - }else{ - written += w; - } - if(written < buflen){ - struct pollfd pfd = { - .fd = fd, - .events = POLLOUT, - .revents = 0, - }; - poll(&pfd, 1, -1); - } - }while(written < buflen); - return 0; -} - -// determine the best palette for the current frame, and write the necessary -// escape sequences to 'out'. for now, we just assume the ANSI palettes. at -// 256 colors, this is the 16 normal ones, 6x6x6 color cubes, and 32 greys. -// it's probably better to sample the darker regions rather than cover so much -// chroma, but whatever....FIXME -static int -prep_optimized_palette(notcurses* nc, FILE* out __attribute__ ((unused))){ - if(nc->RGBflag){ - return 0; // DirectColor, no need to write palette - } - if(!nc->CCCflag){ - return 0; // can't change palette - } - // FIXME - return 0; -} - -/* -// reshape the shadow framebuffer to match the stdplane's dimensions, throwing -// away the old one. -static int -reshape_shadow_fb(notcurses* nc){ - const size_t size = sizeof(nc->shadowbuf) * nc->stdscr->leny * nc->stdscr->lenx; - cell* fb = malloc(size); - if(fb == NULL){ - return -1; - } - free(nc->shadowbuf); - nc->shadowbuf = fb; - nc->shadowy = nc->stdscr->leny; - nc->shadowx = nc->stdscr->lenx; - memset(fb, 0, size); - return 0; -} -*/ - -static inline int -notcurses_render_internal(notcurses* nc){ - int ret = 0; - int y, x; - FILE* out = nc->mstreamfp; - fseeko(out, 0, SEEK_SET); - // don't write a clearscreen. we only update things that have been changed. - // we explicitly move the cursor at the beginning of each output line, so no - // need to home it expliticly. - prep_optimized_palette(nc, out); // FIXME do what on failure? - uint32_t curattr = 0; // current attributes set (does not include colors) - // FIXME as of at least gcc 9.2.1, we get a false -Wmaybe-uninitialized below - // when using these without explicit initializations. for the life of me, i - // can't see any such path, and valgrind is cool with it, so what ya gonna do? - unsigned lastr = 0, lastg = 0, lastb = 0; - unsigned lastbr = 0, lastbg = 0, lastbb = 0; - // we can elide a color escape iff the color has not changed between the two - // cells and the current cell uses no defaults, or if both the current and - // the last used both defaults. - bool fgelidable = false, bgelidable = false, defaultelidable = false; - /*if(nc->stdscr->leny != nc->shadowy || nc->stdscr->lenx != nc->shadowx){ - reshape_shadow_fb(nc); - }*/ - for(y = 0 ; y < nc->stdscr->leny ; ++y){ - bool linedamaged = false; // have we repositioned the cursor to start line? - bool newdamage = nc->damage[y]; -// fprintf(stderr, "nc->damage[%d] (%p) = %u\n", y, nc->damage + y, nc->damage[y]); - if(newdamage){ - nc->damage[y] = 0; - } - // move to the beginning of the line, in case our accounting was befouled - // by wider- (or narrower-) than-reported characters - for(x = 0 ; x < nc->stdscr->lenx ; ++x){ - unsigned r, g, b, br, bg, bb; - ncplane* p; - cell c; // no need to initialize - p = visible_cell(&c, y, x, nc->top, &newdamage); - assert(p); - // don't try to print a wide character on the last column; it'll instead - // be printed on the next line. they probably shouldn't be admitted, but - // we can end up with one due to a resize. - if((x + 1 >= nc->stdscr->lenx && cell_double_wide_p(&c))){ - continue; - } - if(!linedamaged){ - if(newdamage){ - term_emit("cup", tiparm(nc->cup, y, x), out, false); - nc->stats.cellelisions += x; - nc->stats.cellemissions += (nc->stdscr->lenx - x); - linedamaged = true; - }else{ - continue; - } - } - // set the style. this can change the color back to the default; if it - // does, we need update our elision possibilities. - bool normalized; - term_setstyles(nc, out, &curattr, &c, &normalized); - if(normalized){ - defaultelidable = true; - bgelidable = false; - fgelidable = false; - } - // we allow these to be set distinctly, but terminfo only supports using - // them both via the 'op' capability. unless we want to generate the 'op' - // escapes ourselves, if either is set to default, we first send op, and - // then a turnon for whichever aren't default. - - // we can elide the default set iff the previous used both defaults - if(cell_fg_default_p(&c) || cell_bg_default_p(&c)){ - if(!defaultelidable){ - ++nc->stats.defaultemissions; - term_emit("op", nc->op, out, false); - }else{ - ++nc->stats.defaultelisions; - } - // if either is not default, this will get turned off - defaultelidable = true; - fgelidable = false; - bgelidable = false; - } - - // we can elide the foreground set iff the previous used fg and matched - if(!cell_fg_default_p(&c)){ - cell_get_fg_rgb(&c, &r, &g, &b); - if(fgelidable && lastr == r && lastg == g && lastb == b){ - ++nc->stats.fgelisions; - }else{ - term_fg_rgb8(nc, out, r, g, b); - ++nc->stats.fgemissions; - fgelidable = true; - } - lastr = r; lastg = g; lastb = b; - defaultelidable = false; - } - if(!cell_bg_default_p(&c)){ - cell_get_bg_rgb(&c, &br, &bg, &bb); - if(bgelidable && lastbr == br && lastbg == bg && lastbb == bb){ - ++nc->stats.bgelisions; - }else{ - term_bg_rgb8(nc, out, br, bg, bb); - ++nc->stats.bgemissions; - bgelidable = true; - } - lastbr = br; lastbg = bg; lastbb = bb; - defaultelidable = false; - } -// fprintf(stderr, "[%02d/%02d] 0x%02x 0x%02x 0x%02x %p\n", y, x, r, g, b, p); - term_putc(out, p, &c); - if(cell_double_wide_p(&c)){ - ++x; - } - } - if(linedamaged == false){ - nc->stats.cellelisions += x; - } - } - ret |= fflush(out); - fflush(nc->ttyfp); - if(blocking_write(nc->ttyfd, nc->mstream, nc->mstrsize)){ - ret = -1; - } -/*fprintf(stderr, "%lu/%lu %lu/%lu %lu/%lu\n", defaultelisions, defaultemissions, - fgelisions, fgemissions, bgelisions, bgemissions);*/ - if(nc->renderfp){ - fprintf(nc->renderfp, "%s\n", nc->mstream); - } - return nc->mstrsize; -} - -static void -mutex_unlock(void* vlock){ - pthread_mutex_unlock(vlock); -} - -int notcurses_refresh(notcurses* nc){ - int ret = 0; - pthread_mutex_lock(&nc->lock); - pthread_cleanup_push(mutex_unlock, &nc->lock); - if(nc->mstream == NULL){ - ret = -1; // haven't rendered yet, and thus don't know what should be there - }else if(blocking_write(nc->ttyfd, nc->mstream, nc->mstrsize)){ - ret = -1; - } - pthread_cleanup_pop(1); - return ret; -} - -int notcurses_render(notcurses* nc){ - int ret = 0; - struct timespec start, done; - clock_gettime(CLOCK_MONOTONIC_RAW, &start); - pthread_mutex_lock(&nc->lock); - pthread_cleanup_push(mutex_unlock, &nc->lock); - int bytes = notcurses_render_internal(nc); - int dimy, dimx; - notcurses_resize(nc, &dimy, &dimx); - clock_gettime(CLOCK_MONOTONIC_RAW, &done); - update_render_stats(&done, &start, &nc->stats, bytes); - if(bytes < 0){ - ret = -1; - } - pthread_cleanup_pop(1); - return ret; -} - int ncplane_cursor_move_yx(ncplane* n, int y, int x){ if(x >= n->lenx || x < 0){ return -1; @@ -1549,18 +1005,54 @@ int cell_duplicate(ncplane* n, cell* targ, const cell* c){ return ulen; } +static inline void +cell_set_wide(cell* c){ + c->channels |= CELL_WIDEASIAN_MASK; +} + +static inline void +cell_obliterate(ncplane* n, cell* c){ + cell_release(n, c); + cell_init(c); +} + int ncplane_putc(ncplane* n, const cell* c){ ncplane_lock(n); if(cursor_invalid_p(n)){ ncplane_unlock(n); return -1; } + bool wide = cell_double_wide_p(c); + // A wide character obliterates anything to its immediate right (and marks + // that cell as wide). Any character placed atop one half of a wide character + // obliterates the other half. Note that a wide char can thus obliterate two + // wide chars, totalling four columns. cell* targ = &n->fb[fbcellidx(n, n->y, n->x)]; + if(n->x > 0){ + if(cell_double_wide_p(targ)){ // replaced cell is half of a wide char + if(targ->gcluster == 0){ // we're the right half + cell_obliterate(n, &n->fb[fbcellidx(n, n->y, n->x - 1)]); + }else{ + cell_obliterate(n, &n->fb[fbcellidx(n, n->y, n->x + 1)]); + } + } + } if(cell_duplicate(n, targ, c) < 0){ ncplane_unlock(n); return -1; } - int cols = 1 + cell_double_wide_p(targ); + int cols = 1 + wide; + if(wide){ // must set our right wide, and check for further damage + if(n->x < n->lenx - 1){ // check to our right + cell* candidate = &n->fb[fbcellidx(n, n->y, n->x + 1)]; + if(n->x < n->lenx - 2){ + if(cell_double_wide_p(candidate) && targ->gcluster){ // left half + cell_obliterate(n, &n->fb[fbcellidx(n, n->y, n->x + 2)]); + } + } + cell_set_wide(candidate); + } + } n->damage[n->y] = true; advance_cursor(n, cols); ncplane_unlock(n); @@ -1568,11 +1060,7 @@ int ncplane_putc(ncplane* n, const cell* c){ } int ncplane_putsimple(struct ncplane* n, char c){ - cell ce = { - .gcluster = c, - .attrword = ncplane_get_attr(n), - .channels = ncplane_get_channels(n), - }; + cell ce = CELL_INITIALIZER(c, ncplane_get_attr(n), ncplane_get_channels(n)); if(!cell_simple_p(&ce)){ return -1; } @@ -1622,6 +1110,7 @@ int cell_load(ncplane* n, cell* c, const char* gcluster){ int bytes; int cols; if((bytes = utf8_egc_len(gcluster, &cols)) >= 0 && bytes <= 1){ + c->channels &= ~CELL_WIDEASIAN_MASK; c->gcluster = *gcluster; return !!c->gcluster; } @@ -2053,6 +1542,9 @@ int ncvisual_render(const ncvisual* ncv){ const int linesize = f->linesize[0]; const unsigned char* data = f->data[0]; for(y = 0 ; y < f->height / 2 && y < dimy ; ++y){ + if(ncplane_cursor_move_yx(ncv->ncp, y, 0)){ + return -1; + } for(x = 0 ; x < f->width && x < dimx ; ++x){ int bpp = av_get_bits_per_pixel(av_pix_fmt_desc_get(f->format)); const unsigned char* rgbbase_up = data + (linesize * (y * 2)) + (x * bpp / CHAR_BIT); diff --git a/src/lib/render.c b/src/lib/render.c new file mode 100644 index 000000000..8c6dd017f --- /dev/null +++ b/src/lib/render.c @@ -0,0 +1,532 @@ +#include +#include +#include +#include "internal.h" + +static inline uint64_t +timespec_to_ns(const struct timespec* t){ + return t->tv_sec * NANOSECS_IN_SEC + t->tv_nsec; +} + +static void +mutex_unlock(void* vlock){ + pthread_mutex_unlock(vlock); +} + +static int +blocking_write(int fd, const char* buf, size_t buflen){ + size_t written = 0; + do{ + ssize_t w = write(fd, buf + written, buflen - written); + if(w < 0){ + if(errno != EAGAIN && errno != EWOULDBLOCK){ + return -1; + } + }else{ + written += w; + } + if(written < buflen){ + struct pollfd pfd = { + .fd = fd, + .events = POLLOUT, + .revents = 0, + }; + poll(&pfd, 1, -1); + } + }while(written < buflen); + return 0; +} + +int notcurses_refresh(notcurses* nc){ + int ret = 0; + pthread_mutex_lock(&nc->lock); + pthread_cleanup_push(mutex_unlock, &nc->lock); + if(nc->mstream == NULL){ + ret = -1; // haven't rendered yet, and thus don't know what should be there + }else if(blocking_write(nc->ttyfd, nc->mstream, nc->mstrsize)){ + ret = -1; + } + pthread_cleanup_pop(1); + return ret; +} + +static void +update_render_stats(const struct timespec* time1, const struct timespec* time0, + ncstats* stats, int bytes){ + int64_t elapsed = timespec_to_ns(time1) - timespec_to_ns(time0); + //fprintf(stderr, "Rendering took %ld.%03lds\n", elapsed / NANOSECS_IN_SEC, + // (elapsed % NANOSECS_IN_SEC) / 1000000); + if(bytes >= 0){ + stats->render_bytes += bytes; + if(bytes > stats->render_max_bytes){ + stats->render_max_bytes = bytes; + } + if(bytes < stats->render_min_bytes){ + stats->render_min_bytes = bytes; + } + }else{ + ++stats->failed_renders; + } + if(elapsed > 0){ // don't count clearly incorrect information, egads + ++stats->renders; + stats->render_ns += elapsed; + if(elapsed > stats->render_max_ns){ + stats->render_max_ns = elapsed; + } + if(elapsed < stats->render_min_ns){ + stats->render_min_ns = elapsed; + } + } +} + +// determine the best palette for the current frame, and write the necessary +// escape sequences to 'out'. for now, we just assume the ANSI palettes. at +// 256 colors, this is the 16 normal ones, 6x6x6 color cubes, and 32 greys. +// it's probably better to sample the darker regions rather than cover so much +// chroma, but whatever....FIXME +static int +prep_optimized_palette(notcurses* nc, FILE* out __attribute__ ((unused))){ + if(nc->RGBflag){ + return 0; // DirectColor, no need to write palette + } + if(!nc->CCCflag){ + return 0; // can't change palette + } + // FIXME + return 0; +} + +/* +// reshape the shadow framebuffer to match the stdplane's dimensions, throwing +// away the old one. +static int +reshape_shadow_fb(notcurses* nc){ + const size_t size = sizeof(nc->shadowbuf) * nc->stdscr->leny * nc->stdscr->lenx; + cell* fb = malloc(size); + if(fb == NULL){ + return -1; + } + free(nc->shadowbuf); + nc->shadowbuf = fb; + nc->shadowy = nc->stdscr->leny; + nc->shadowx = nc->stdscr->lenx; + memset(fb, 0, size); + return 0; +} +*/ + +// Find the topmost cell for this coordinate by walking down the z-buffer, +// looking for an intersecting ncplane. Once we've found one, check it for +// transparency in either the back- or foreground. If the alpha channel is +// active, keep descending and blending until we hit opacity, or bedrock. We +// recurse to find opacity, and blend the result into what we have. The +// 'findfore' and 'findback' bools control our recursion--there's no point in +// going further down when a color is locked in, so don't (for instance) recurse +// further when we have a transparent foreground and opaque background atop an +// opaque foreground and transparent background. The cell we ultimately return +// (a const ref to 'c') is backed by '*retp' via rawdog copy; the caller must +// not call cell_release() upon it, nor use it beyond the scope of the render. +// +// So, as we go down, we find planes which can have impact on the result. Once +// we've locked the result in (base case), write the deep values we have to 'c'. +// Then, as we come back up, blend them as appropriate. The actual glyph is +// whichever one occurs at the top with a non-transparent α (α < 3). To effect +// tail recursion, though, we instead write first, and then recurse, blending +// as we descend. α <= 0 is opaque. α >= 3 is fully transparent. +static ncplane* +dig_visible_cell(cell* c, int y, int x, ncplane* p, int falpha, int balpha, + bool* damage){ + while(p){ + // where in the plane this coordinate would be, based off absy/absx. the + // true origin is 0,0, so abs=2,2 means coordinate 3,3 would be 1,1, while + // abs=-2,-2 would make coordinate 3,3 relative 5, 5. + int poffx, poffy; + poffy = y - p->absy; + poffx = x - p->absx; + if(poffy < p->leny && poffy >= 0){ + if(poffx < p->lenx && poffx >= 0){ // p is valid for this y, x + const cell* vis = &p->fb[fbcellidx(p, poffy, poffx)]; + // if we never loaded any content into the cell (or obliterated it by + // writing in a zero), use the plane's background cell. + if(vis->gcluster == 0){ + vis = &p->defcell; + } + bool lockedglyph = false; + int nalpha; + if(falpha > 0 && (nalpha = cell_get_fg_alpha(vis)) < CELL_ALPHA_TRANS){ + if(c->gcluster == 0){ // never write fully trans glyphs, never replace + if( (c->gcluster = vis->gcluster) ){ // index copy only + lockedglyph = true; // must return this ncplane for this glyph + c->attrword = vis->attrword; + cell_set_fchannel(c, cell_get_fchannel(vis)); // FIXME blend it in + falpha -= (CELL_ALPHA_TRANS - nalpha); // FIXME blend it in + if(p->damage[poffy]){ + *damage = true; + p->damage[poffy] = false; + } + } + } + } + if(balpha > 0 && (nalpha = cell_get_bg_alpha(vis)) < CELL_ALPHA_TRANS){ + cell_set_bchannel(c, cell_get_bchannel(vis)); // FIXME blend it in + balpha -= (CELL_ALPHA_TRANS - nalpha); + if(p->damage[poffy]){ + *damage = true; + p->damage[poffy] = false; + } + } + if((falpha > 0 || balpha > 0) && p->z){ // we must go further! + ncplane* cand = dig_visible_cell(c, y, x, p->z, falpha, balpha, damage); + if(!lockedglyph && cand){ + p = cand; + } + } + return p; + } + } + p = p->z; + } + // should never happen for valid y, x thanks to the stdplane. you fucked up! + return NULL; +} + +static inline ncplane* +visible_cell(cell* c, int y, int x, ncplane* n, bool* damage){ + cell_init(c); + return dig_visible_cell(c, y, x, n, CELL_ALPHA_TRANS, CELL_ALPHA_TRANS, damage); +} + +// write the cell's UTF-8 grapheme cluster to the provided FILE*. returns the +// number of columns occupied by this EGC (only an approximation; it's actually +// a property of the font being used). +static int +term_putc(FILE* out, const ncplane* n, const cell* c){ + if(cell_simple_p(c)){ + if(c->gcluster == 0 || iscntrl(c->gcluster)){ +// fprintf(stderr, "[ ]\n"); + if(fputc_unlocked(' ', out) == EOF){ + return -1; + } + }else{ +// fprintf(stderr, "[%c]\n", c->gcluster); + if(fputc_unlocked(c->gcluster, out) == EOF){ + return -1; + } + } + }else{ + const char* ext = extended_gcluster(n, c); +// fprintf(stderr, "[%s]\n", ext); + if(fputs_unlocked(ext, out) < 0){ // FIXME check for short write? + return -1; + } + } + return 0; +} + +// check the current and target style bitmasks against the specified 'stylebit'. +// if they are different, and we have the necessary capability, write the +// applicable terminfo entry to 'out'. returns -1 only on a true error. +static int +term_setstyle(FILE* out, unsigned cur, unsigned targ, unsigned stylebit, + const char* ton, const char* toff){ + int ret = 0; + unsigned curon = cur & stylebit; + unsigned targon = targ & stylebit; + if(curon != targon){ + if(targon){ + if(ton){ + ret = term_emit("ton", ton, out, false); + } + }else{ + if(toff){ // how did this happen? we can turn it on, but not off? + ret = term_emit("toff", toff, out, false); + } + } + } + if(ret < 0){ + return -1; + } + return 0; +} + +// write any escape sequences necessary to set the desired style +static int +term_setstyles(const notcurses* nc, FILE* out, uint32_t* curattr, const cell* c, + bool* normalized){ + *normalized = false; + uint32_t cellattr = cell_styles(c); + if(cellattr == *curattr){ + return 0; // happy agreement, change nothing + } + int ret = 0; + // if only italics changed, don't emit any sgr escapes. xor of current and + // target ought have all 0s in the lower 8 bits if only italics changed. + if((cellattr ^ *curattr) & 0x00ff0000ul){ + *normalized = true; // FIXME this is pretty conservative + // if everything's 0, emit the shorter sgr0 + if(nc->sgr0 && ((cellattr & CELL_STYLE_MASK) == 0)){ + if(term_emit("sgr0", nc->sgr0, out, false) < 0){ + ret = -1; + } + }else if(term_emit("sgr", tiparm(nc->sgr, cellattr & CELL_STYLE_STANDOUT, + cellattr & CELL_STYLE_UNDERLINE, + cellattr & CELL_STYLE_REVERSE, + cellattr & CELL_STYLE_BLINK, + cellattr & CELL_STYLE_DIM, + cellattr & CELL_STYLE_BOLD, + cellattr & CELL_STYLE_INVIS, + cellattr & CELL_STYLE_PROTECT, 0), + out, false) < 0){ + ret = -1; + } + } + // sgr will blow away italics if they were set beforehand + ret |= term_setstyle(out, *curattr, cellattr, CELL_STYLE_ITALIC, nc->italics, nc->italoff); + *curattr = cellattr; + return ret; +} + +// 3 for foreground, 4 for background, ugh FIXME +static int +term_esc_rgb(notcurses* nc __attribute__ ((unused)), FILE* out, int esc, + unsigned r, unsigned g, unsigned b){ + // The correct way to do this is using tiparm+tputs, but doing so (at least + // as of terminfo 6.1.20191019) both emits ~3% more bytes for a run of 'rgb' + // and gives rise to some corrupted cells (possibly due to special handling of + // values < 256; I'm not at this time sure). So we just cons up our own. + /*if(esc == 4){ + return term_emit("setab", tiparm(nc->setab, (int)((r << 16u) | (g << 8u) | b)), out, false); + }else if(esc == 3){ + return term_emit("setaf", tiparm(nc->setaf, (int)((r << 16u) | (g << 8u) | b)), out, false); + }else{ + return -1; + }*/ + #define RGBESC1 "\x1b" "[" + #define RGBESC2 "8;2;" + // rrr;ggg;bbbm + char rgbesc[] = RGBESC1 " " RGBESC2 " "; + int len = strlen(RGBESC1); + rgbesc[len++] = esc; + len += strlen(RGBESC2); + if(r > 99){ + rgbesc[len++] = r / 100 + '0'; + } + if(r > 9){ + rgbesc[len++] = (r % 100) / 10 + '0'; + } + rgbesc[len++] = (r % 10) + '0'; + rgbesc[len++] = ';'; + if(g > 99){ rgbesc[len++] = g / 100 + '0'; } + if(g > 9){ rgbesc[len++] = (g % 100) / 10 + '0'; } + rgbesc[len++] = g % 10 + '0'; + rgbesc[len++] = ';'; + if(b > 99){ rgbesc[len++] = b / 100 + '0'; } + if(b > 9){ rgbesc[len++] = (b % 100) / 10 + '0'; } + rgbesc[len++] = b % 10 + '0'; + rgbesc[len++] = 'm'; + rgbesc[len] = '\0'; + int w; + if((w = fputs_unlocked(rgbesc, out)) < len){ + return -1; + } + return 0; +} + +static int +term_bg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ + // We typically want to use tputs() and tiperm() to acquire and write the + // escapes, as these take into account terminal-specific delays, padding, + // etc. For the case of DirectColor, there is no suitable terminfo entry, but + // we're also in that case working with hopefully more robust terminals. + // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself. + if(nc->RGBflag){ + return term_esc_rgb(nc, out, '4', r, g, b); + }else{ + if(nc->setab == NULL){ + return -1; + } + // For 256-color indexed mode, start constructing a palette based off + // the inputs *if we can change the palette*. If more than 256 are used on + // a single screen, start... combining close ones? For 8-color mode, simple + // interpolation. I have no idea what to do for 88 colors. FIXME + if(nc->colors >= 256){ + term_emit("setab", tiparm(nc->setab, rgb_quantize_256(r, g, b)), out, false); + } + return -1; + } + return 0; +} + +static int +term_fg_rgb8(notcurses* nc, FILE* out, unsigned r, unsigned g, unsigned b){ + // We typically want to use tputs() and tiperm() to acquire and write the + // escapes, as these take into account terminal-specific delays, padding, + // etc. For the case of DirectColor, there is no suitable terminfo entry, but + // we're also in that case working with hopefully more robust terminals. + // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself. + if(nc->RGBflag){ + return term_esc_rgb(nc, out, '3', r, g, b); + }else{ + if(nc->setaf == NULL){ + return -1; + } + if(nc->colors >= 256){ + term_emit("setaf", tiparm(nc->setaf, rgb_quantize_256(r, g, b)), out, false); + } + // For 256-color indexed mode, start constructing a palette based off + // the inputs *if we can change the palette*. If more than 256 are used on + // a single screen, start... combining close ones? For 8-color mode, simple + // interpolation. I have no idea what to do for 88 colors. FIXME + return -1; + } + return 0; +} + +static inline int +notcurses_render_internal(notcurses* nc){ + int ret = 0; + int y, x; + FILE* out = nc->mstreamfp; + fseeko(out, 0, SEEK_SET); + // don't write a clearscreen. we only update things that have been changed. + // we explicitly move the cursor at the beginning of each output line, so no + // need to home it expliticly. + prep_optimized_palette(nc, out); // FIXME do what on failure? + uint32_t curattr = 0; // current attributes set (does not include colors) + // FIXME as of at least gcc 9.2.1, we get a false -Wmaybe-uninitialized below + // when using these without explicit initializations. for the life of me, i + // can't see any such path, and valgrind is cool with it, so what ya gonna do? + unsigned lastr = 0, lastg = 0, lastb = 0; + unsigned lastbr = 0, lastbg = 0, lastbb = 0; + // we can elide a color escape iff the color has not changed between the two + // cells and the current cell uses no defaults, or if both the current and + // the last used both defaults. + bool fgelidable = false, bgelidable = false, defaultelidable = false; + /*if(nc->stdscr->leny != nc->shadowy || nc->stdscr->lenx != nc->shadowx){ + reshape_shadow_fb(nc); + }*/ + for(y = 0 ; y < nc->stdscr->leny ; ++y){ + bool linedamaged = false; // have we repositioned the cursor to start line? + bool newdamage = nc->damage[y]; +// fprintf(stderr, "nc->damage[%d] (%p) = %u\n", y, nc->damage + y, nc->damage[y]); + if(newdamage){ + nc->damage[y] = 0; + } + // move to the beginning of the line, in case our accounting was befouled + // by wider- (or narrower-) than-reported characters + for(x = 0 ; x < nc->stdscr->lenx ; ++x){ + unsigned r, g, b, br, bg, bb; + ncplane* p; + cell c; // no need to initialize + p = visible_cell(&c, y, x, nc->top, &newdamage); + assert(p); + // don't try to print a wide character on the last column; it'll instead + // be printed on the next line. they probably shouldn't be admitted, but + // we can end up with one due to a resize. + if((x + 1 >= nc->stdscr->lenx && cell_double_wide_p(&c))){ + continue; + } + if(!linedamaged){ + if(newdamage){ + term_emit("cup", tiparm(nc->cup, y, x), out, false); + nc->stats.cellelisions += x; + nc->stats.cellemissions += (nc->stdscr->lenx - x); + linedamaged = true; + }else{ + continue; + } + } + // set the style. this can change the color back to the default; if it + // does, we need update our elision possibilities. + bool normalized; + term_setstyles(nc, out, &curattr, &c, &normalized); + if(normalized){ + defaultelidable = true; + bgelidable = false; + fgelidable = false; + } + // we allow these to be set distinctly, but terminfo only supports using + // them both via the 'op' capability. unless we want to generate the 'op' + // escapes ourselves, if either is set to default, we first send op, and + // then a turnon for whichever aren't default. + + // we can elide the default set iff the previous used both defaults + if(cell_fg_default_p(&c) || cell_bg_default_p(&c)){ + if(!defaultelidable){ + ++nc->stats.defaultemissions; + term_emit("op", nc->op, out, false); + }else{ + ++nc->stats.defaultelisions; + } + // if either is not default, this will get turned off + defaultelidable = true; + fgelidable = false; + bgelidable = false; + } + + // we can elide the foreground set iff the previous used fg and matched + if(!cell_fg_default_p(&c)){ + cell_get_fg_rgb(&c, &r, &g, &b); + if(fgelidable && lastr == r && lastg == g && lastb == b){ + ++nc->stats.fgelisions; + }else{ + term_fg_rgb8(nc, out, r, g, b); + ++nc->stats.fgemissions; + fgelidable = true; + } + lastr = r; lastg = g; lastb = b; + defaultelidable = false; + } + if(!cell_bg_default_p(&c)){ + cell_get_bg_rgb(&c, &br, &bg, &bb); + if(bgelidable && lastbr == br && lastbg == bg && lastbb == bb){ + ++nc->stats.bgelisions; + }else{ + term_bg_rgb8(nc, out, br, bg, bb); + ++nc->stats.bgemissions; + bgelidable = true; + } + lastbr = br; lastbg = bg; lastbb = bb; + defaultelidable = false; + } +// fprintf(stderr, "[%02d/%02d] 0x%02x 0x%02x 0x%02x %p\n", y, x, r, g, b, p); + term_putc(out, p, &c); + if(cell_double_wide_p(&c)){ + ++x; + } + } + if(linedamaged == false){ + nc->stats.cellelisions += x; + } + } + ret |= fflush(out); + fflush(nc->ttyfp); + if(blocking_write(nc->ttyfd, nc->mstream, nc->mstrsize)){ + ret = -1; + } +/*fprintf(stderr, "%lu/%lu %lu/%lu %lu/%lu\n", defaultelisions, defaultemissions, + fgelisions, fgemissions, bgelisions, bgemissions);*/ + if(nc->renderfp){ + fprintf(nc->renderfp, "%s\n", nc->mstream); + } + return nc->mstrsize; +} + +int notcurses_render(notcurses* nc){ + int ret = 0; + struct timespec start, done; + clock_gettime(CLOCK_MONOTONIC_RAW, &start); + pthread_mutex_lock(&nc->lock); + pthread_cleanup_push(mutex_unlock, &nc->lock); + int bytes = notcurses_render_internal(nc); + int dimy, dimx; + notcurses_resize(nc, &dimy, &dimx); + clock_gettime(CLOCK_MONOTONIC_RAW, &done); + update_render_stats(&done, &start, &nc->stats, bytes); + if(bytes < 0){ + ret = -1; + } + pthread_cleanup_pop(1); + return ret; +} + diff --git a/src/poc/unidamage.cpp b/src/poc/unidamage.cpp index 2e7da5a09..3cf4c9bf5 100644 --- a/src/poc/unidamage.cpp +++ b/src/poc/unidamage.cpp @@ -18,19 +18,38 @@ int main(int argc, char** argv){ int dimx, dimy; ncplane_dim_yx(n, &dimy, &dimx); cell c = CELL_TRIVIAL_INITIALIZER; + cell_set_bg_rgb(&c, 0, 0x80, 0); + //ncplane_set_default(n, &c); if(cell_load(n, &c, "🐳") < 0){ goto err; } - if(ncplane_set_default(n, &c) < 0){ - goto err; - } - cell_release(n, &c); - if(cell_load(n, &c, "x") < 0){ - goto err; + if(dimy > 5){ + dimy = 5; } for(int i = 0 ; i < dimy ; ++i){ - for(int j = 0 ; j < dimx / 2 ; j += 2){ - if(ncplane_putc_yx(n, i, j, &c) < 0){ + for(int j = 8 ; j < dimx / 2 ; ++j){ // leave some empty spaces + if(ncplane_putc_yx(n, i, j * 2, &c) < 0){ + goto err; + } + } + } + ncplane_putc_yx(n, dimy, dimx - 3, &c); + ncplane_putc_yx(n, dimy, dimx - 1, &c); + ncplane_putc_yx(n, dimy + 1, dimx - 2, &c); + ncplane_putc_yx(n, dimy + 1, dimx - 4, &c); + cell_release(n, &c); + // put these on the right side of the wide glyphs + for(int i = 0 ; i < dimy / 2 ; ++i){ + for(int j = 5 ; j < dimx / 2 ; j += 2){ + if(ncplane_putsimple_yx(n, i, j, (j % 10) + '0') < 0){ + goto err; + } + } + } + // put these on the left side of the wide glyphs + for(int i = dimy / 2 ; i < dimy ; ++i){ + for(int j = 4 ; j < dimx / 2 ; j += 2){ + if(ncplane_putsimple_yx(n, i, j, (j % 10) + '0') < 0){ goto err; } } @@ -38,6 +57,7 @@ int main(int argc, char** argv){ if(notcurses_render(nc)){ goto err; } + printf("\n"); return notcurses_stop(nc) ? EXIT_FAILURE : EXIT_SUCCESS; err: diff --git a/tests/ncplane.cpp b/tests/ncplane.cpp index 886f0972e..ee954b73b 100644 --- a/tests/ncplane.cpp +++ b/tests/ncplane.cpp @@ -762,3 +762,116 @@ TEST_F(NcplaneTest, MoveToLowerRight) { EXPECT_EQ(0, ncplane_destroy(ncp)); // FIXME verify with ncplane_at_cursor() } + +// Placing a wide char to the immediate left of any other char ought obliterate +// that cell. +TEST_F(NcplaneTest, WideCharAnnihilatesRight) { + const wchar_t* w = L"🐸"; + const wchar_t* wbashed = L"🦂"; + const char bashed = 'X'; + int sbytes = 0; + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 1, wbashed, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putsimple_yx(n_, 1, 1, bashed)); + int x, y; + ncplane_cursor_yx(n_, &y, &x); + EXPECT_EQ(1, y); + EXPECT_EQ(2, x); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, w, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 1, 0, w, 0, 0, &sbytes)); + cell c = CELL_TRIVIAL_INITIALIZER; + ncplane_at_yx(n_, 0, 0, &c); + const char* wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🐸")); // should be frog + ncplane_at_yx(n_, 0, 1, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); // should be wide + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_EQ(0, c.gcluster); // should be nothing + ncplane_at_yx(n_, 1, 0, &c); + wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🐸")); // should be frog + ncplane_at_yx(n_, 1, 1, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); //should be wide + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_EQ(0, c.gcluster); + EXPECT_EQ(0, notcurses_render(nc_)); // should be nothing +} + +// Placing a wide char on the right half of a wide char ought obliterate the +// original wide char. +TEST_F(NcplaneTest, WideCharAnnihilatesWideLeft) { + const wchar_t* w = L"🐍"; + const wchar_t* wbashed = L"🦂"; + int sbytes = 0; + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wbashed, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 1, w, 0, 0, &sbytes)); + int x, y; + ncplane_cursor_yx(n_, &y, &x); + EXPECT_EQ(0, y); + EXPECT_EQ(3, x); + cell c = CELL_TRIVIAL_INITIALIZER; + ncplane_at_yx(n_, 0, 0, &c); + EXPECT_EQ(0, c.gcluster); // should be nothing + ncplane_at_yx(n_, 0, 1, &c); + const char* wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🐍")); // should be snake + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); // should be wide + EXPECT_EQ(0, notcurses_render(nc_)); +} + +// Placing a normal char on either half of a wide char ought obliterate +// the original wide char. +TEST_F(NcplaneTest, WideCharsAnnihilated) { + const char cc = 'X'; + const wchar_t* wbashedl = L"🐍"; + const wchar_t* wbashedr = L"🦂"; + int sbytes = 0; + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wbashedl, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 2, wbashedr, 0, 0, &sbytes)); + EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 1, cc)); + EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 2, cc)); + int x, y; + ncplane_cursor_yx(n_, &y, &x); + EXPECT_EQ(0, y); + EXPECT_EQ(3, x); + cell c = CELL_TRIVIAL_INITIALIZER; + ncplane_at_yx(n_, 0, 0, &c); + EXPECT_EQ(0, c.gcluster); // should be nothing + ncplane_at_yx(n_, 0, 1, &c); + EXPECT_EQ(cc, c.gcluster); // should be 'X' + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_EQ(cc, c.gcluster); // should be 'X" + ncplane_at_yx(n_, 0, 3, &c); + EXPECT_EQ(0, c.gcluster); // should be nothing + EXPECT_EQ(0, notcurses_render(nc_)); +} + +// But placing something to the immediate right of any glyph, that is not a +// problem. Ensure it is so. +TEST_F(NcplaneTest, AdjacentCharsSafe) { + const char cc = 'X'; + const wchar_t* wsafel = L"🐍"; + const wchar_t* wsafer = L"🦂"; + int sbytes = 0; + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 0, wsafel, 0, 0, &sbytes)); + EXPECT_LT(0, ncplane_putwegc_yx(n_, 0, 3, wsafer, 0, 0, &sbytes)); + EXPECT_EQ(1, ncplane_putsimple_yx(n_, 0, 2, cc)); + int x, y; + ncplane_cursor_yx(n_, &y, &x); + EXPECT_EQ(0, y); + EXPECT_EQ(3, x); + cell c = CELL_TRIVIAL_INITIALIZER; + ncplane_at_yx(n_, 0, 0, &c); + const char* wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🐍")); // should be snake + ncplane_at_yx(n_, 0, 1, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); // should be snake + ncplane_at_yx(n_, 0, 2, &c); + EXPECT_EQ(cc, c.gcluster); // should be 'X' + ncplane_at_yx(n_, 0, 3, &c); + wres = extended_gcluster(n_, &c); + EXPECT_EQ(0, strcmp(wres, "🦂")); // should be scorpion + ncplane_at_yx(n_, 0, 4, &c); + EXPECT_TRUE(cell_double_wide_p(&c)); // should be scorpion + EXPECT_EQ(0, notcurses_render(nc_)); +}