diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h index f92050479..2f5642eb1 100644 --- a/include/notcurses/notcurses.h +++ b/include/notcurses/notcurses.h @@ -1531,7 +1531,9 @@ ncplane_printf_stainable(struct ncplane* n, const char* format, ...){ // to '*bytes' if it is not NULL. Cleared columns are included in the return // value, but *not* included in the number of bytes written. Leaves the cursor // at the end of output. A partial write will be accomplished as far as it can; -// determine whether the write completed by inspecting '*bytes'. +// determine whether the write completed by inspecting '*bytes'. Can output to +// multiple rows even in the absence of scrolling, but not more rows than are +// available. With scrolling enabled, arbitrary amounts of data can be emitted. API int ncplane_puttext(struct ncplane* n, int y, ncalign_e align, const char* text, size_t* bytes); diff --git a/src/demo/zoo.c b/src/demo/zoo.c index 0e38a55d1..e3cfc44ca 100644 --- a/src/demo/zoo.c +++ b/src/demo/zoo.c @@ -265,7 +265,6 @@ typedef struct read_marshal { static void* reader_thread(void* vmarsh){ - // FIXME use ncplane_puttext() to handle word breaking; this is ugly const char text[] = "Notcurses provides several widgets to quickly build vivid TUIs.\n\n" "This NCReader widget facilitates free-form text entry complete with readline-style bindings. " @@ -291,9 +290,9 @@ reader_thread(void* vmarsh){ timespec_div(&demodelay, (y - targrow) / 3, &rowdelay); // we usually won't be done rendering the text before reaching our target row size_t textpos = 0; - const int TOWRITEMAX = 4; // FIXME throw in some jitter! int ret; - while(y > targrow){ + const int MAXTOWRITE = 8; + while(textpos < textlen || y > targrow){ pthread_mutex_lock(lock); if( (ret = demo_render(nc)) ){ pthread_mutex_unlock(lock); @@ -303,34 +302,22 @@ reader_thread(void* vmarsh){ return THREAD_RETURN_POSITIVE; } } - ncplane_move_yx(rplane, --y, x); - size_t towrite = textlen - textpos; - if(towrite > TOWRITEMAX){ - towrite = TOWRITEMAX; + if(y > targrow){ + --y; } - if(towrite){ - ncplane_putnstr(rplane, towrite, text + textpos); - textpos += towrite; + ncplane_move_yx(rplane, y, x); + size_t towrite = strcspn(text + textpos, " \t\n") + 1; + if(towrite > MAXTOWRITE){ + towrite = MAXTOWRITE; } - pthread_mutex_unlock(lock); - clock_nanosleep(CLOCK_MONOTONIC, 0, &rowdelay, NULL); - } - while(textpos < textlen){ - pthread_mutex_lock(lock); - if( (ret = demo_render(nc)) ){ - pthread_mutex_unlock(lock); - if(ret < 0){ + if(towrite){ + char* duped = strndup(text + textpos, towrite); + size_t bytes; + if(ncplane_puttext(rplane, -1, NCALIGN_LEFT, duped, &bytes) < 0 || bytes != strlen(duped)){ + free(duped); return THREAD_RETURN_NEGATIVE; - }else if(ret > 0){ - return THREAD_RETURN_POSITIVE; } - } - size_t towrite = textlen - textpos; - if(towrite > TOWRITEMAX){ - towrite = TOWRITEMAX; - } - if(towrite){ - ncplane_putnstr(rplane, towrite, text + textpos); + free(duped); textpos += towrite; } pthread_mutex_unlock(lock); diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c index 34ca2581a..a239a5af3 100644 --- a/src/lib/notcurses.c +++ b/src/lib/notcurses.c @@ -1417,7 +1417,7 @@ int ncplane_putegc_yx(ncplane* n, int y, int x, const char* gclust, int* sbytes) bool wide = cols > 1; if(x == -1 && y == -1 && n->x + wide >= n->lenx){ if(!n->scrolling){ - logerror(n->nc, "No room to output [%s]\n", gclust); + logerror(n->nc, "No room to output [%s] %d/%d\n", gclust, n->y, n->x); return -1; } scroll_down(n); @@ -1713,8 +1713,66 @@ overlong_word(const char* text, int dimx){ return false; } +// Determine if we need to drop down to the next line before trying to print +// anything. We do if both (1) there is not enough room to print an entire word +// on the line, and (2) we did not start on the left-hand side. If #2 is not +// true, it's just a very long word, and we print the portion we can. Performs +// the move, if it is determined to be necessary. +static int +puttext_premove(ncplane* n, const char* text){ +//fprintf(stderr, "CHECKING %d/%d %s\n", n->y, n->x, text); + if(n->x == 0){ // never move down when starting on the left hand origin + return 0; + } + const char* breaker = NULL; // where the last wordbreaker starts + if(n->x > 0 && n->x < n->lenx){ + int x = n->x; + const char* beginning = text; + while(*text && x <= ncplane_dim_x(n)){ + mbstate_t mbstate = {}; + wchar_t w; + size_t consumed = mbrtowc(&w, text, MB_CUR_MAX, &mbstate); + if(consumed == (size_t)-2 || consumed == (size_t)-1){ + logerror(n->nc, "Invalid UTF-8 after %zu bytes\n", text - beginning); + return -1; + } + if(iswordbreak(w)){ + //fprintf(stderr, "wordbreak [%lc] at %d\n", w, x); + if(x == n->x){ + text += consumed; + continue; // don't emit leading whitespace, or count it + }else{ + return 0; + } + } + int width = wcwidth(w); + //fprintf(stderr, "have char %lc (%d) (%zu)\n", w, width, text - linestart); + if(width < 0){ + width = 0; + } + if(x + width > n->lenx){ + break; + } + x += width; + text += consumed; + } + } + if(*text && breaker == NULL){ +//fprintf(stderr, "ADVANCING DA FOKKER, JA\n"); + if(n->scrolling){ + scroll_down(n); + }else{ + return ncplane_cursor_move_yx(n, n->y + 1, 0); + } + } + return 0; +} + // FIXME probably best to use u8_wordbreaks() and get all wordbreaks at once... int ncplane_puttext(ncplane* n, int y, ncalign_e align, const char* text, size_t* bytes){ + if(bytes){ + *bytes = 0; + } int totalcols = 0; // save the beginning for diagnostic const char* beginning = text; @@ -1725,8 +1783,19 @@ int ncplane_puttext(ncplane* n, int y, ncalign_e align, const char* text, size_t const int dimx = ncplane_dim_x(n); const int dimy = ncplane_dim_y(n); const char* linestart = text; - int x = 0; // number of columns consumed for this line + // if we're using NCALIGN_LEFT, we'll be printing with x==-1, i.e. wherever + // the cursor is. if there's insufficient room to print anything, we need to + // try moving to the next line first. FIXME this ought actually apply to all + // alignments, which ought be taken relative to n->x. no change for + // NCALIGN_RIGHT, but NCALIGN_CENTER needs explicitly handle it... do{ + //if(align == NCALIGN_LEFT){ + if(puttext_premove(n, text)){ + return -1; + } + //} +//fprintf(stderr, "**************STARTING AT %d/%d of %d/%d\n", n->y, n->x, n->leny, n->lenx); + int x = n->x; // number of columns consumed for this line const char* breaker = NULL; // where the last wordbreaker starts int breakercol = 0; // column of the last wordbreaker // figure how much text to output on this line @@ -1747,8 +1816,8 @@ int ncplane_puttext(ncplane* n, int y, ncalign_e align, const char* text, size_t } return -1; } -//fprintf(stderr, "have possible wordbreak %lc\n", w); if(iswordbreak(w)){ +//fprintf(stderr, "wordbreak [%lc] at %d\n", w, x); if(x == 0){ text += consumed; linestart = text; @@ -1769,7 +1838,7 @@ int ncplane_puttext(ncplane* n, int y, ncalign_e align, const char* text, size_t x += width; text += consumed; } -//fprintf(stderr, "OUT! %s %zu %d\n", linestart, text - linestart, x); +//fprintf(stderr, "%d/%d OUT! %s %zu %d\n", n->y, n->x, linestart, text - linestart, x); bool overlong = false; // ugh // if we have no breaker, we got a single word that was longer than our // line. print what we can and move along. if *text is nul, we're done. @@ -1796,7 +1865,7 @@ int ncplane_puttext(ncplane* n, int y, ncalign_e align, const char* text, size_t //fprintf(stderr, "exited at %d (%d) %zu looking at [%.*s]\n", x, dimx, breaker - linestart, (int)(breaker - linestart), linestart); if(breaker != linestart){ totalcols += breakercol; - const int xpos = ncplane_align(n, align, breakercol); + const int xpos = (align == NCALIGN_LEFT) ? -1 : ncplane_align(n, align, breakercol); // blows out if we supply a y beyond leny //fprintf(stderr, "y: %d %ld %.*s\n", y, breaker - linestart, (int)(breaker - linestart), linestart); if(ncplane_putnstr_yx(n, y, xpos, breaker - linestart, linestart) <= 0){ @@ -1826,6 +1895,7 @@ int ncplane_puttext(ncplane* n, int y, ncalign_e align, const char* text, size_t y = -1; } } +//fprintf(stderr, "new cursor: %d/%d\n", n->y, n->x); //fprintf(stderr, "LOOKING AT: [%c] [%s]\n", *text, linestart); }while(*text); if(bytes){ diff --git a/tests/layout.cpp b/tests/layout.cpp index e420e10ed..88a36ac5c 100644 --- a/tests/layout.cpp +++ b/tests/layout.cpp @@ -284,6 +284,80 @@ TEST_CASE("TextLayout") { ncplane_destroy(sp); } + SUBCASE("LayoutLongLines") { + // straight from the zoo demo, which didn't work at first + const int READER_COLS = 71; // equal to longest line + const int READER_ROWS = 4; + const char text[] = + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin ornare " + "neque ac ipsum viverra, vestibulum hendrerit leo consequat. Integer " + "velit, pharetra sed nisl quis, porttitor ornare purus. Cras ac " + "sollicitudin dolor, eget elementum dolor. Quisque lobortis sagittis."; + auto sp = ncplane_new(nc_, READER_ROWS, READER_COLS, 0, 0, nullptr); + REQUIRE(sp); + size_t bytes; + ncplane_home(sp); + CHECK(0 < ncplane_puttext(sp, 0, NCALIGN_LEFT, text, &bytes)); + CHECK(bytes == strlen(text)); + CHECK(0 == notcurses_render(nc_)); + char* line = ncplane_contents(sp, 0, 0, -1, -1); + REQUIRE(line); + // FIXME check line + free(line); + ncplane_destroy(sp); + } + + SUBCASE("LayoutZooText") { + // straight from the zoo demo, which didn't work at first + const int READER_COLS = 64; + const int READER_ROWS = 8; + const char text[] = + "Notcurses provides several widgets to quickly build vivid TUIs.\n\n" + "This NCReader widget facilitates free-form text entry complete with readline-style bindings. " "NCSelector allows a single option to be selected from a list. " "NCMultiselector allows 0..n options to be selected from a list of n items. " + "NCFdplane streams a file descriptor, while NCSubproc spawns a subprocess and streams its output. " + "A variety of plots are supported, and menus can be placed along the top and/or bottom of any plane.\n\n" + "Widgets can be controlled with the keyboard and/or mouse. They are implemented atop ncplanes, and these planes can be manipulated like all others."; + auto sp = ncplane_new(nc_, READER_ROWS, READER_COLS, 0, 0, nullptr); + REQUIRE(sp); + ncplane_set_scrolling(sp, true); + size_t bytes; + ncplane_home(sp); + CHECK(0 < ncplane_puttext(sp, 0, NCALIGN_LEFT, text, &bytes)); + CHECK(bytes == strlen(text)); + CHECK(0 == notcurses_render(nc_)); + char* line = ncplane_contents(sp, 0, 0, -1, -1); + REQUIRE(line); + // FIXME check line + free(line); + ncplane_destroy(sp); + } + + SUBCASE("LayoutZooTextNoScroll") { + // straight from the zoo demo, which didn't work at first + const int READER_COLS = 64; + const int READER_ROWS = 15; + const char text[] = + "Notcurses provides several widgets to quickly build vivid TUIs.\n\n" + "This NCReader widget facilitates free-form text entry complete with readline-style bindings. " + "NCSelector allows a single option to be selected from a list. " + "NCMultiselector allows 0..n options to be selected from a list of n items. " + "NCFdplane streams a file descriptor, while NCSubproc spawns a subprocess and streams its output. " + "A variety of plots are supported, and menus can be placed along the top and/or bottom of any plane.\n\n" + "Widgets can be controlled with the keyboard and/or mouse. They are implemented atop ncplanes, and these planes can be manipulated like all others."; + auto sp = ncplane_new(nc_, READER_ROWS, READER_COLS, 0, 0, nullptr); + REQUIRE(sp); + size_t bytes; + ncplane_home(sp); + CHECK(0 < ncplane_puttext(sp, 0, NCALIGN_LEFT, text, &bytes)); + CHECK(bytes == strlen(text)); + CHECK(0 == notcurses_render(nc_)); + char* line = ncplane_contents(sp, 0, 0, -1, -1); + REQUIRE(line); + // FIXME check line + free(line); + ncplane_destroy(sp); + } + CHECK(0 == notcurses_stop(nc_)); }