Repository: dankamongmen/notcurses (language: python)

Issue:
generalize ncplane_erase_region()
Quite often, I find myself needing to apply some pattern to the start or end of a row, within a plane, from my cursor. Most often, I want to clear. We ought expose this functionality in the API. For now, just write whatever crap. We might in the future want to infer some optimizations from this, but I'd rather a full general solution in the rendering stack.
generalize ncplane_erase_region()
Quite often, I find myself needing to apply some pattern to the start or end of a row, within a plane, from my cursor. Most often, I want to clear. We ought expose this functionality in the API. For now, just write whatever crap. We might in the future want to infer some optimizations from this, but I'd rather a full general solution in the rendering stack.
generalize ncplane_erase_region()
Quite often, I find myself needing to apply some pattern to the start or end of a row, within a plane, from my cursor. Most often, I want to clear. We ought expose this functionality in the API. For now, just write whatever crap. We might in the future want to infer some optimizations from this, but I'd rather a full general solution in the rendering stack.


Discussion / hints:
we already have this with `ncplane_erase_region()`. we ought just allow `x` and/or `y` to be specified as -1 to indicate "from current cursor location on that axis." we should also alllow negative lengths to indicate moving up and left from the specified position.
oooooh, we had a big ol' cut-and-paste bug sitting in `ncplane_erase_region()` =[
need some unit tests on this for sure
we already have this with `ncplane_erase_region()`. we ought just allow `x` and/or `y` to be specified as -1 to indicate "from current cursor location on that axis." we should also alllow negative lengths to indicate moving up and left from the specified position.
oooooh, we had a big ol' cut-and-paste bug sitting in `ncplane_erase_region()` =[
need some unit tests on this for sure
we already have this with `ncplane_erase_region()`. we ought just allow `x` and/or `y` to be specified as -1 to indicate "from current cursor location on that axis." we should also alllow negative lengths to indicate moving up and left from the specified position.
oooooh, we had a big ol' cut-and-paste bug sitting in `ncplane_erase_region()` =[
need some unit tests on this for sure

Fix patch:
diff --git a/USAGE.md b/USAGE.md
index c0c412a0c9..64a59b3f97 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -900,10 +900,26 @@ int ncplane_mergedown_simple(struct ncplane* restrict src,
 void ncplane_erase(struct ncplane* n);
 
 // Erase every cell in the region starting at {ystart, xstart} and having size
-// {ylen, xlen}. It is an error if any of ystart, xstart, ylen, or xlen is
-// negative. A value of 0 may be provided for ylen and/or xlen, meaning to
-// erase everything along that dimension. It is an error if ystart + ylen
-// or xstart + xlen is not in the plane.
+// {|ylen|x|xlen|} for non-zero lengths. If ystart and/or xstart are -1, the current
+// cursor position along that axis is used; other negative values are an error. A
+// negative ylen means to move up from the origin, and a negative xlen means to move
+// left from the origin. A positive ylen moves down, and a positive xlen moves right.
+// A value of 0 for the length erases everything along that dimension. It is an error
+// if the starting coordinate is not in the plane, but the ending coordinate may be
+// outside the plane.
+//
+// For example, on a plane of 20 rows and 10 columns, with the cursor at row 10 and
+// column 5, the following would hold:
+//
+//  (-1, -1, 0, 1): clears the column to the right of the cursor (column 6)
+//  (-1, -1, 0, -1): clears the column to the left of the cursor (column 4)
+//  (-1, -1, INT_MAX, 0): clears all rows with or below the cursor (rows 10--19)
+//  (-1, -1, -INT_MAX, 0): clears all rows with or above the cursor (rows 0--10)
+//  (-1, 4, 3, 3): clears from row 10, column 4 through row 12, column 6
+//  (-1, 4, 3, 3): clears from row 10, column 4 through row 8, column 2
+//  (4, -1, 0, 3): clears columns 5, 6, and 7
+//  (-1, -1, 0, 0): clears the plane *if the cursor is in a legal position*
+//  (0, 0, 0, 0): clears the plane in all cases
 int ncplane_erase_region(struct ncplane* n, int ystart, int xstart,
                          int ylen, int xlen);
 ```
diff --git a/doc/man/man3/notcurses_plane.3.md b/doc/man/man3/notcurses_plane.3.md
index 7fc658dd95..9672d0cf6b 100644
--- a/doc/man/man3/notcurses_plane.3.md
+++ b/doc/man/man3/notcurses_plane.3.md
@@ -320,7 +320,11 @@ might see changes. It is an error to merge a plane onto itself.
 homes the cursor. The base cell is preserved, as are the active attributes.
 **ncplane_erase_region** does the same for a subregion of the plane. For the
 latter, supply 0 for ***ylen*** and/or ***xlen*** to erase through that
-dimension, starting at the specified point. See [BUGS][] below.
+dimension, starting at the specified point. Supply -1 for ***ystart*** and/or
+***xstart*** to use the cursor's current position along that axis for a starting
+point. Negative ***ylen*** and ***xlen*** move up and to the left from the starting
+coordinate; positive ***ylen*** and ***xlen*** move down and to the right from same.
+See [BUGS][] below.
 
 When a plane is resized (whether by **ncplane_resize**, **SIGWINCH**, or any
 other mechanism), a depth-first recursion is performed on its children.
@@ -444,8 +448,8 @@ plane is destroyed. The caller should release this **nccell** with
 **ncplane_as_rgba** returns a heap-allocated array of **uint32_t** values,
 each representing a single RGBA pixel, or **NULL** on failure.
 
-**ncplane_erase_region** returns -1 if any of its parameters are negative, or
-if they specify any area beyond the plane.
+**ncplane_erase_region** returns -1 if **ystart** or **xstart** are less than -1,
+or outside the plane.
 
 **ncplane_cursor_move_yx** returns -1 if the coordinates are beyond the
 dimensions of the specified plane (except for the special value -1).
@@ -480,6 +484,12 @@ ncplane_set_base(notcurses_stdplane(nc), " ", 0, 0);
 notcurses_render(nc);
 ```
 
+or simply:
+
+```c
+notcurses_refresh(nc);
+```
+
 # SEE ALSO
 
 **notcurses(3)**,
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index 89d557f3fb..39f065d1ab 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -2231,10 +2231,26 @@ API int ncplane_mergedown(struct ncplane* RESTRICT src,
 API void ncplane_erase(struct ncplane* n);
 
 // Erase every cell in the region starting at {ystart, xstart} and having size
-// {ylen, xlen}. It is an error if any of ystart, xstart, ylen, or xlen is
-// negative. A value of 0 may be provided for ylen and/or xlen, meaning to
-// erase everything along that dimension. It is an error if ystart + ylen
-// or xstart + xlen is not in the plane.
+// {|ylen|x|xlen|} for non-zero lengths. If ystart and/or xstart are -1, the current
+// cursor position along that axis is used; other negative values are an error. A
+// negative ylen means to move up from the origin, and a negative xlen means to move
+// left from the origin. A positive ylen moves down, and a positive xlen moves right.
+// A value of 0 for the length erases everything along that dimension. It is an error
+// if the starting coordinate is not in the plane, but the ending coordinate may be
+// outside the plane.
+//
+// For example, on a plane of 20 rows and 10 columns, with the cursor at row 10 and
+// column 5, the following would hold:
+//
+//  (-1, -1, 0, 1): clears the column to the right of the cursor (column 6)
+//  (-1, -1, 0, -1): clears the column to the left of the cursor (column 4)
+//  (-1, -1, INT_MAX, 0): clears all rows with or below the cursor (rows 10--19)
+//  (-1, -1, -INT_MAX, 0): clears all rows with or above the cursor (rows 0--10)
+//  (-1, 4, 3, 3): clears from row 10, column 4 through row 12, column 6
+//  (-1, 4, 3, 3): clears from row 10, column 4 through row 8, column 2
+//  (4, -1, 0, 3): clears columns 5, 6, and 7
+//  (-1, -1, 0, 0): clears the plane *if the cursor is in a legal position*
+//  (0, 0, 0, 0): clears the plane in all cases
 API int ncplane_erase_region(struct ncplane* n, int ystart, int xstart,
                              int ylen, int xlen)
   __attribute__ ((nonnull (1)));
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index c3bb05eb86..c20783ce62 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -2103,6 +2103,7 @@ void ncplane_yx(const ncplane* n, int* y, int* x){
 }
 
 void ncplane_erase(ncplane* n){
+  loginfo("erasing plane\n");
   if(n->sprite){
     sprixel_hide(n->sprite);
   }
@@ -2122,28 +2123,57 @@ void ncplane_erase(ncplane* n){
 }
 
 int ncplane_erase_region(ncplane* n, int ystart, int xstart, int ylen, int xlen){
-  if(ylen < 0 || xlen < 0){
-    logerror("Won't erase section of negative length (%d, %d)\n", ylen, xlen);
-    return -1;
+  if(ystart == -1){
+    ystart = n->y;
+  }
+  if(xstart == -1){
+    xstart = n->x;
   }
   if(ystart < 0 || xstart < 0){
     logerror("Illegal start of erase (%d, %d)\n", ystart, xstart);
     return -1;
   }
-  if(ystart >= ncplane_dim_y(n) || ystart + ylen > ncplane_dim_y(n)){
-    logerror("Illegal y spec for erase (%d, %d)\n", ystart, ylen);
+  if(ystart >= ncplane_dim_y(n) || xstart >= ncplane_dim_x(n)){
+    logerror("Illegal start of erase (%d, %d)\n", ystart, xstart);
     return -1;
   }
-  if(ylen == 0){
-    ylen = ncplane_dim_y(n) - ystart;
-  }
-  if(xstart >= ncplane_dim_x(n) || xstart + xlen > ncplane_dim_x(n)){
-    logerror("Illegal x spec for erase (%d, %d)\n", xstart, xlen);
-    return -1;
+  if(xlen < 0){
+    if(xlen + 1 < -xstart){
+      xlen = -xstart - 1;
+    }
+    xstart = xstart + xlen + 1;
+    xlen = -xlen;
+  }else if(xlen == 0){
+    xstart = 0;
+    xlen = ncplane_dim_x(n);
   }
-  if(xlen == 0){
+  if(xlen > ncplane_dim_x(n) || xstart + xlen > ncplane_dim_x(n)){
     xlen = ncplane_dim_x(n) - xstart;
   }
+  if(ylen < 0){
+    if(ylen + 1 < -ystart){
+      ylen = -ystart - 1;
+    }
+    ystart = ystart + ylen + 1;
+    ylen = -ylen;
+  }else if(ylen == 0){
+    ystart = 0;
+    ylen = ncplane_dim_y(n);
+  }
+  if(ylen > ncplane_dim_y(n) || ystart + ylen > ncplane_dim_y(n)){
+    ylen = ncplane_dim_y(n) - ystart;
+  }
+  // special-case the full plane erasure, as it's powerfully optimized (O(1))
+  if(ystart == 0 && xstart == 0 &&
+      ylen == ncplane_dim_y(n) && xlen == ncplane_dim_x(n)){
+    int tmpy = n->y; // preserve cursor location
+    int tmpx = n->x;
+    ncplane_erase(n);
+    n->y = tmpy;
+    n->x = tmpx;
+    return 0;
+  }
+  loginfo("erasing %d/%d - %d/%d\n", ystart, xstart, ystart + ylen, xstart + xlen);
   for(int y = ystart ; y < ystart + ylen ; ++y){
     for(int x = xstart ; x < xstart + xlen ; ++x){
       nccell_release(n, &n->fb[nfbcellidx(n, y, x)]);
diff --git a/src/poc/bitmapstates.c b/src/poc/bitmapstates.c
index 12fbef756b..775c53cb5e 100644
--- a/src/poc/bitmapstates.c
+++ b/src/poc/bitmapstates.c
@@ -4,7 +4,7 @@
 static void
 emit(struct ncplane* n, const char* str){
   fprintf(stderr, "\n\n\n%s\n", str);
-  ncplane_erase_region(n, 6, 0, 0, 0);
+  ncplane_erase_region(n, 6, 0, INT_MAX, 0);
   ncplane_putstr_yx(n, 6, 0, str);
 }
 
diff --git a/src/poc/grid.c b/src/poc/grid.c
new file mode 100644
index 0000000000..024ff51281
--- /dev/null
+++ b/src/poc/grid.c
@@ -0,0 +1,83 @@
+#include <notcurses/notcurses.h>
+
+struct ncvisual*
+draw_grid(struct ncplane* stdn){
+  int maxby, maxbx;
+  int cellpxy, cellpxx;
+  ncplane_pixelgeom(stdn, NULL, NULL, &cellpxy, &cellpxx, &maxby, &maxbx);
+  if(cellpxy <= 1 || cellpxx <= 1){
+    fprintf(stderr, "cell-pixel geometry: %d %d\n", cellpxy, cellpxx);
+    return NULL;
+  }
+  uint32_t* rgba = malloc(maxby * maxbx * sizeof(*rgba));
+  if(rgba == NULL){
+    return NULL;
+  }
+  // we want an inlay on each cell, so if they're 10 wide, we want pixels
+  // at 0, 9, 10, 19, 20, 29, etc.
+  for(int y = 0 ; y < maxby ; ++y){
+    uint32_t* row = rgba + y * maxbx;
+    memset(row, 0xff, maxbx * sizeof(*rgba));
+    for(int yi = 0 ; yi < cellpxy - 2 ; ++yi){
+      ++y;
+      if(y < maxby){
+        row = rgba + y * maxbx;
+        for(int x = 0 ; x < maxbx ; ++x){
+          uint32_t* px = row + x;
+          ncpixel_set_a(px, 255);
+          ncpixel_set_r(px, 255);
+          ncpixel_set_g(px, 255);
+          ncpixel_set_b(px, 255);
+          if((x += cellpxx - 1) < maxbx){
+            px = row + x;
+            ncpixel_set_a(px, 255);
+            ncpixel_set_r(px, 255);
+            ncpixel_set_g(px, 255);
+            ncpixel_set_b(px, 255);
+          }
+        }
+      }
+    }
+    ++y;
+    row = rgba + y * maxbx;
+    memset(row, 0xff, maxbx * sizeof(*rgba));
+  }
+  struct ncvisual* ncv = ncvisual_from_rgba(rgba, maxby, maxbx * sizeof(*rgba), maxbx);
+  return ncv;
+}
+
+int main(void){
+  struct notcurses_options opts = {
+    .flags = NCOPTION_NO_ALTERNATE_SCREEN |
+             NCOPTION_DRAIN_INPUT |
+             NCOPTION_NO_CLEAR_BITMAPS |
+             NCOPTION_SUPPRESS_BANNERS |
+             NCOPTION_PRESERVE_CURSOR,
+  //  .loglevel = NCLOGLEVEL_TRACE,
+  };
+  struct notcurses* nc = notcurses_init(&opts, NULL);
+  if(nc == NULL){
+    return EXIT_FAILURE;
+  }
+  int dimy, dimx;
+  struct ncplane* stdn = notcurses_stddim_yx(nc, &dimy, &dimx);
+  struct ncvisual* ncv = draw_grid(stdn);
+  if(ncv == NULL){
+    notcurses_stop(nc);
+    return EXIT_FAILURE;
+  }
+  struct ncvisual_options vopts = {
+    .blitter = NCBLIT_PIXEL,
+    .flags = NCVISUAL_OPTION_NODEGRADE,
+  };
+  struct ncplane* bitn = ncvisual_render(nc, ncv, &vopts);
+  ncvisual_destroy(ncv);
+  if(bitn == NULL){
+    notcurses_stop(nc);
+    return EXIT_FAILURE;
+  }
+  // FIXME render it behind some text
+  notcurses_render(nc);
+  notcurses_stop(nc);
+  return EXIT_SUCCESS;
+}


Test patch:
diff --git a/src/tests/erase.cpp b/src/tests/erase.cpp
new file mode 100644
index 0000000000..86e7260127
--- /dev/null
+++ b/src/tests/erase.cpp
@@ -0,0 +1,177 @@
+#include <array>
+#include <cstdlib>
+#include "main.h"
+
+TEST_CASE("Erase") {
+  auto nc_ = testing_notcurses();
+  if(!nc_){
+    return;
+  }
+  int dimy, dimx;
+  struct ncplane* n_ = notcurses_stddim_yx(nc_, &dimy, &dimx);
+  REQUIRE(n_);
+
+  // fill the standard plane with 'x's
+  nccell nc = CELL_CHAR_INITIALIZER('x');
+  CHECK(0 < ncplane_polyfill_yx(n_, 0, 0, &nc));
+  nccell_release(n_, &nc);
+  CHECK(0 == notcurses_render(nc_));
+  CHECK(0 == ncplane_cursor_move_yx(n_, dimy / 2, dimx / 2));
+
+  // clear all columns to the left of cursor, inclusive
+  SUBCASE("EraseColumnsLeft") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, 0, -INT_MAX));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(x <= dimx / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  // clear all columns to the right of cursor, inclusive
+  SUBCASE("EraseColumnsRight") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, 0, INT_MAX));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(x >= dimx / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  // clear all rows above cursor, inclusive
+  SUBCASE("EraseRowsAbove") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, -INT_MAX, 0));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(y <= dimy / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  // clear all rows below cursor, inclusive
+  SUBCASE("EraseRowsBelow") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, INT_MAX, 0));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(y >= dimy / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  // current cursor to the end of the line
+  SUBCASE("EraseToEOL") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, 1, INT_MAX));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(y == dimy / 2 && x >= dimx / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  // current cursor to the start of the line
+  SUBCASE("EraseToSOL") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, 1, -INT_MAX));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(y == dimy / 2 && x <= dimx / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  // current cursor to the end of the line using -1 len
+  SUBCASE("EraseToEOLNeg") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, -1, INT_MAX));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(y == dimy / 2 && x >= dimx / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  // current cursor to the start of the line using -1 len
+  SUBCASE("EraseToSOLNeg") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, -1, -INT_MAX));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(y == dimy / 2 && x <= dimx / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  // current cursor only
+  SUBCASE("EraseCurrentCursor") {
+    CHECK(0 == ncplane_erase_region(n_, -1, -1, 1, 1));
+    for(int y = 0 ; y < dimy ; ++y){
+      for(int x = 0 ; x < dimx ; ++x){
+        char* c = ncplane_at_yx(n_, y, x, nullptr, nullptr);
+        REQUIRE(nullptr != c);
+        if(y == dimy / 2 && x == dimx / 2){
+          CHECK(0 == strcmp(c, ""));
+        }else{
+          CHECK(0 == strcmp(c, "x"));
+        }
+        free(c);
+      }
+    }
+  }
+
+  CHECK(0 == notcurses_render(nc_));
+  CHECK(0 == notcurses_stop(nc_));
+
+}
diff --git a/src/tests/main.cpp b/src/tests/main.cpp
index d011fc5857..693e8cf916 100644
--- a/src/tests/main.cpp
+++ b/src/tests/main.cpp
@@ -164,7 +164,9 @@ auto main(int argc, const char **argv) -> int {
   handle_opts(argv);
   int res = context.run(); // run
   reset_terminal();
-  check_data_dir();
+  if(res){
+    check_data_dir();
+  }
   if(context.shouldExit()){ // important - query flags (and --exit) rely on the user doing this
     return res;             // propagate the result of the tests
   }