Most of notcurses-tetris, mergedown fixes (#420)

* ncplane_mergedown() fix for small planes #417
* tetris game needed for the book
pull/423/head
Nick Black 4 years ago committed by GitHub
parent c3feca6ded
commit 2b7b384e91
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

@ -958,7 +958,8 @@ API int ncplane_stain(struct ncplane* n, int ystop, int xstop, uint64_t ul,
// rendering region." Merging is independent of the position of 'src' viz 'dst'
// on the z-axis. If 'src' does not intersect with 'dst', 'dst' will not be
// changed, but it is not an error. The source plane still exists following
// this operation. Do not supply the same plane for both 'src' and 'dst'.
// this operation. If 'dst' is NULL, it will be interpreted as the standard
// plane. Do not supply the same plane for both 'src' and 'dst'.
API int ncplane_mergedown(struct ncplane* RESTRICT src, struct ncplane* RESTRICT dst);
// Erase every cell in the ncplane, resetting all attributes to normal, all
@ -1975,13 +1976,13 @@ ncvisual_simple_streamer(struct notcurses* nc, struct ncvisual* ncv, void* curry
// Stream the entirety of the media, according to its own timing. Blocking,
// obviously. streamer may be NULL; it is otherwise called for each frame, and
// its return value handled as outlined for stream cb. Pretty raw; beware.
// If streamer() returns non-zero, the stream is aborted, and that value is
// returned. By convention, return a positive number to indicate intentional
// abort from within streamer(). 'timescale' allows the frame duration time to
// be scaled. For a visual naturally running at 30FPS, a 'timescale' of 0.1
// will result in 300FPS, and a 'timescale' of 10 will result in 3FPS. It is an
// error to supply 'timescale' less than or equal to 0.
// its return value handled as outlined for stream cb. If streamer() returns
// non-zero, the stream is aborted, and that value is returned. By convention,
// return a positive number to indicate intentional abort from within
// streamer(). 'timescale' allows the frame duration time to be scaled. For a
// visual naturally running at 30FPS, a 'timescale' of 0.1 will result in
// 300FPS, and a 'timescale' of 10 will result in 3FPS. It is an error to
// supply 'timescale' less than or equal to 0.
API int ncvisual_stream(struct notcurses* nc, struct ncvisual* ncv,
int* averr, float timescale, streamcb streamer,
void* curry);

@ -544,6 +544,20 @@ cell_debug(const egcpool* p, const cell* c){
}
}
static inline void
plane_debug(const ncplane* n){
int dimy, dimx;
ncplane_dim_yx(n, &dimy, &dimx);
fprintf(stderr, "p: %p dim: %d/%d poolsize: %d\n", n, dimy, dimx, n->pool.poolsize);
for(int y = 0 ; y < 1 ; ++y){
for(int x = 0 ; x < 10 ; ++x){
const cell* c = &n->fb[fbcellidx(y, dimx, x)];
fprintf(stderr, "[%03d/%03d] ", y, x);
cell_debug(&n->pool, c);
}
}
}
// True if the cell does not generate background pixels. Only the FULL BLOCK
// glyph has this property, AFAIK.
// FIXME set a bit, doing this at load time
@ -571,7 +585,6 @@ cell_duplicate_far(egcpool* tpool, cell* targ, const ncplane* splane, const cell
return !!c->gcluster;
}
size_t ulen = strlen(extended_gcluster(splane, c));
//fprintf(stderr, "[%s] (%zu)\n", egcpool_extended_gcluster(&splane->pool, c), strlen(egcpool_extended_gcluster(&splane->pool, c)));
int eoffset = egcpool_stash(tpool, extended_gcluster(splane, c), ulen);
if(eoffset < 0){
return -1;

@ -203,19 +203,21 @@ lock_in_highcontrast(cell* targc, struct crender* crender){
}
}
// Paints a single ncplane into the provided framebuffer 'fb'. Whenever a cell
// is locked in, it is compared against the last frame. If it is different, the
// 'damagevec' bitmap is updated with a 1. 'pool' is typically nc->pool, but can
// Paints a single ncplane into the provided scratch framebuffer 'fb', and
// ultimately 'lastframe' (we can't always write directly into 'lastframe',
// because we need build state to solve certain cells, and need compare their
// solved result to the last frame). Whenever a cell is locked in, it is
// compared against the last frame. If it is different, the 'rvec' bitmap is updated with a 1. 'pool' is typically nc->pool, but can
// be whatever's backing fb.
static int
paint(notcurses* nc, ncplane* p, cell* lastframe, struct crender* rvec, cell* fb, egcpool* pool){
paint(ncplane* p, cell* lastframe, struct crender* rvec,
cell* fb, egcpool* pool, int dstleny, int dstlenx,
int dstabsy, int dstabsx, int lfdimx){
int y, x, dimy, dimx, offy, offx;
// don't use ncplane_dim_yx()/ncplane_yx() here, lest we deadlock
dimy = p->leny;
dimx = p->lenx;
offy = p->absy - nc->stdscr->absy;
offx = p->absx - nc->stdscr->absx;
//fprintf(stderr, "PLANE %p %d %d %d %d %d %d\n", p, dimy, dimx, offy, offx, nc->stdscr->leny, nc->stdscr->lenx);
ncplane_dim_yx(p, &dimy, &dimx);
offy = p->absy - dstabsy;
offx = p->absx - dstabsx;
//fprintf(stderr, "PLANE %p %d %d %d %d %d %d\n", p, dimy, dimx, offy, offx, dstleny, dstlenx);
// skip content above or to the left of the physical screen
int starty, startx;
if(offy < 0){
@ -231,19 +233,19 @@ paint(notcurses* nc, ncplane* p, cell* lastframe, struct crender* rvec, cell* fb
for(y = starty ; y < dimy ; ++y){
const int absy = y + offy;
// once we've passed the physical screen's bottom, we're done
if(absy >= nc->stdscr->leny){
if(absy >= dstleny){
break;
}
for(x = startx ; x < dimx ; ++x){
const int absx = x + offx;
if(absx >= nc->stdscr->lenx){
if(absx >= dstlenx){
break;
}
cell* targc = &fb[fbcellidx(absy, nc->stdscr->lenx, absx)];
cell* targc = &fb[fbcellidx(absy, dstlenx, absx)];
if(cell_locked_p(targc)){
continue;
}
struct crender* crender = &rvec[fbcellidx(absy, nc->stdscr->lenx, absx)];
struct crender* crender = &rvec[fbcellidx(absy, dstlenx, absx)];
const cell* vis = &p->fb[nfbcellidx(p, y, x)];
// if we never loaded any content into the cell (or obliterated it by
// writing in a zero), use the plane's base cell.
@ -263,7 +265,7 @@ paint(notcurses* nc, ncplane* p, cell* lastframe, struct crender* rvec, cell* fb
// screen, nor if we're bisected by a higher plane.
if(cell_double_wide_p(vis)){
// are we on the last column of the real screen? if so, 0x20 us
if(absx >= nc->stdscr->lenx - 1){
if(absx >= dstlenx - 1){
targc->gcluster = ' ';
// is the next cell occupied? if so, 0x20 us
}else if(targc[1].gcluster){
@ -313,13 +315,14 @@ paint(notcurses* nc, ncplane* p, cell* lastframe, struct crender* rvec, cell* fb
// which were already locked in were skipped at the top of the loop)?
if(cell_locked_p(targc)){
lock_in_highcontrast(targc, crender);
cell* prevcell = &lastframe[fbcellidx(absy, nc->lfdimx, absx)];
cell* prevcell = &lastframe[fbcellidx(absy, lfdimx, absx)];
/*if(cell_simple_p(targc)){
fprintf(stderr, "WROTE %u [%c] to %d/%d (%d/%d)\n", targc->gcluster, prevcell->gcluster, y, x, absy, absx);
}else{
fprintf(stderr, "WROTE %u [%s] to %d/%d (%d/%d)\n", targc->gcluster, extended_gcluster(crender->p, targc), y, x, absy, absx);
}
fprintf(stderr, "POOL: %p NC: %p SRC: %p\n", nc->pool.pool, nc, crender->p);*/
fprintf(stderr, "POOL: %p NC: %p SRC: %p\n", pool->pool, nc, crender->p);
}*/
if(cellcmp_and_dupfar(pool, prevcell, crender->p, targc)){
crender->damaged = true;
if(cell_double_wide_p(targc)){
@ -387,19 +390,30 @@ int ncplane_mergedown(ncplane* restrict src, ncplane* restrict dst){
}
int dimy, dimx;
ncplane_dim_yx(dst, &dimy, &dimx);
cell* fb = malloc(sizeof(*fb) * dimy * dimx);
cell* tmpfb = malloc(sizeof(*tmpfb) * dimy * dimx);
cell* rendfb = malloc(sizeof(*rendfb) * dimy * dimx);
const size_t crenderlen = sizeof(struct crender) * dimy * dimx;
struct crender* rvec = malloc(crenderlen);
memset(rvec, 0, crenderlen);
init_fb(fb, dimy, dimx);
if(paint(nc, src, dst->fb, rvec, fb, &dst->pool) || paint(nc, dst, dst->fb, rvec, fb, &dst->pool)){
init_fb(tmpfb, dimy, dimx);
init_fb(rendfb, dimy, dimx);
if(paint(src, rendfb, rvec, tmpfb, &dst->pool, dst->leny, dst->lenx,
dst->absy, dst->absx, dst->lenx)){
free(rvec);
free(rendfb);
free(tmpfb);
return -1;
}
if(paint(dst, rendfb, rvec, tmpfb, &dst->pool, dst->leny, dst->lenx,
dst->absy, dst->absx, dst->lenx)){
free(rvec);
free(fb);
free(rendfb);
free(tmpfb);
return -1;
}
postpaint(fb, dst->fb, dimy, dimx, rvec, &dst->pool);
postpaint(tmpfb, rendfb, dimy, dimx, rvec, &dst->pool);
free(dst->fb);
dst->fb = fb;
dst->fb = rendfb;
free(rvec);
return 0;
}
@ -420,7 +434,9 @@ notcurses_render_internal(notcurses* nc, struct crender* rvec){
init_fb(fb, dimy, dimx);
ncplane* p = nc->top;
while(p){
if(paint(nc, p, nc->lastframe, rvec, fb, &nc->pool)){
if(paint(p, nc->lastframe, rvec, fb, &nc->pool,
nc->stdscr->leny, nc->stdscr->lenx,
nc->stdscr->absy, nc->stdscr->absx, nc->lfdimx)){
free(fb);
return -1;
}
@ -790,9 +806,9 @@ update_palette(notcurses* nc, FILE* out){
// * refresh -- write the stream to the emulator
// Takes a rendered frame (a flat framebuffer, where each cell has the desired
// EGC, attribute, and channels) and the previously-rendered frame, and spits
// out an optimal sequence of terminal-appropriate escapes and EGCs. There
// should be an rvec entry for each cell; only the 'damaged' field is used.
// EGC, attribute, and channels), which has been written to nc->lastframe, and
// spits out an optimal sequence of terminal-appropriate escapes and EGCs. There
// should be an rvec entry for each cell, but only the 'damaged' field is used.
static int
notcurses_rasterize(notcurses* nc, const struct crender* rvec){
FILE* out = nc->rstate.mstreamfp;

@ -1,11 +1,13 @@
#include <iostream>
#include <mutex>
#include <atomic>
#include <thread>
#include <chrono>
#include <cstdlib>
#include <clocale>
#include <ncpp/NotCurses.hh>
const std::string BackgroundFile = "../data/tetris-background.jpeg";
using namespace std::chrono_literals;
// "North-facing" tetrimino forms (the form in which they are released from the
@ -22,26 +24,31 @@ static const struct tetrimino {
class TetrisNotcursesErr : public std::runtime_error {
public:
TetrisNotcursesErr(const std::string& s) throw()
: std::runtime_error(s) {
}
TetrisNotcursesErr(char const* const message) throw()
: std::runtime_error(message) {
}
virtual char const* what() const throw(){
virtual char const* what() const throw() {
return exception::what();
}
};
class Tetris {
public:
Tetris(ncpp::NotCurses& nc) :
Tetris(ncpp::NotCurses& nc, std::atomic_bool& gameover) :
nc_(nc),
score_(0),
msdelay_(10ms),
msdelay_(100ms),
curpiece_(nullptr),
stdplane_(nc_.get_stdplane())
board_(nullptr),
backg_(nullptr),
stdplane_(nc_.get_stdplane()),
gameover_(gameover)
{
curpiece_ = NewPiece();
DrawBoard();
curpiece_ = NewPiece();
}
// 0.5 cell aspect: One board height == one row. One board width == two columns.
@ -49,7 +56,7 @@ public:
static constexpr auto BOARD_HEIGHT = 20;
// FIXME ideally this would be called from constructor :/
void Ticker(){
void Ticker() {
std::chrono::milliseconds ms;
mtx_.lock();
do{
@ -57,26 +64,117 @@ public:
// FIXME loop and verify we didn't get a spurious wakeup
mtx_.unlock();
std::this_thread::sleep_for(ms);
mtx_.lock();
const std::lock_guard<std::mutex> lock(mtx_);
if(curpiece_){
int y, x;
curpiece_->get_yx(&y, &x);
++y;
if(PieceStuck()){
// FIXME lock it into place, get next piece
if(y <= board_top_y_ - 2){
gameover_ = true;
return;
}
curpiece_->mergedown(*board_);
curpiece_ = NewPiece();
}else{
++y;
if(!curpiece_->move(y, x) || !nc_.render()){
// FIXME
throw TetrisNotcursesErr("move() or render()");
}
}
}
}while(ms != std::chrono::milliseconds::zero());
}while(!gameover_);
}
void Stop(){
mtx_.lock();
msdelay_ = std::chrono::milliseconds::zero(); // FIXME wake it up?
mtx_.unlock();
void MoveLeft() {
const std::lock_guard<std::mutex> lock(mtx_);
int y, x;
if(!PrepForMove(&y, &x)){
return;
}
// For each line of the current piece, find the leftmost populated column.
// Check the game area to the immediate left. If something's there, we
// can't make this move.
ncpp::Cell c;
for(int ly = 0 ; ly < curpiece_->get_dim_y() ; ++ly){
int lx = 0;
while(lx < curpiece_->get_dim_x()){
if(curpiece_->get_at(ly, lx, &c)){
if(c.get().gcluster && c.get().gcluster != ' '){
break;
}
}
++lx;
}
if(lx < curpiece_->get_dim_x()){ // otherwise, nothing on this row
ncpp::Cell b;
int cmpy = ly, cmpx = lx - 1;
curpiece_->translate(*board_, &cmpy, &cmpx);
if(board_->get_at(cmpy, cmpx, &b)){
if(b.get().gcluster && b.get().gcluster != ' '){
return; // move is blocked
}
}
}
}
--x;
if(!curpiece_->move(y, x) || !nc_.render()){ // FIXME needs y?
throw TetrisNotcursesErr("move() or render()");
}
}
void MoveRight() {
const std::lock_guard<std::mutex> lock(mtx_);
int y, x;
if(!PrepForMove(&y, &x)){
return;
}
// For each line of the current piece, find the rightmost populated column.
// Check the game area to the immediate right. If something's there, we
// can't make this move.
ncpp::Cell c;
for(int ly = 0 ; ly < curpiece_->get_dim_y() ; ++ly){
int lx = curpiece_->get_dim_x() - 1;
while(lx >= 0){
if(curpiece_->get_at(ly, lx, &c)){
if(c.get().gcluster && c.get().gcluster != ' '){
break;
}
}
--lx;
}
if(lx >= 0){ // otherwise, nothing on this row
ncpp::Cell b;
int cmpy = ly, cmpx = lx + 1;
curpiece_->translate(*board_, &cmpy, &cmpx);
if(board_->get_at(cmpy, cmpx, &b)){
if(b.get().gcluster && b.get().gcluster != ' '){
return; // move is blocked
}
}
}
}
++x;
if(!curpiece_->move(y, x) || !nc_.render()){ // FIXME needs y?
throw TetrisNotcursesErr("move() or render()");
}
}
void RotateCcw() {
const std::lock_guard<std::mutex> lock(mtx_);
int y, x;
if(!PrepForMove(&y, &x)){
return;
}
// FIXME rotate that fucker ccw
}
void RotateCw() {
const std::lock_guard<std::mutex> lock(mtx_);
int y, x;
if(!PrepForMove(&y, &x)){
return;
}
// FIXME rotate that fucker cw
}
private:
@ -85,25 +183,60 @@ private:
std::mutex mtx_;
std::chrono::milliseconds msdelay_;
std::unique_ptr<ncpp::Plane> curpiece_;
std::unique_ptr<ncpp::Plane> board_;
std::unique_ptr<ncpp::Visual> backg_;
ncpp::Plane* stdplane_;
std::atomic_bool& gameover_;
int board_top_y_;
// Returns true if there's a current piece which can be moved
bool PrepForMove(int* y, int* x) {
if(!curpiece_){
return false;
}
curpiece_->get_yx(y, x);
return true;
}
void DrawBoard(){
// background is drawn to the standard plane, at the bottom.
void DrawBackground(const std::string& s) {
int averr;
try{
backg_ = std::make_unique<ncpp::Visual>(s.c_str(), &averr, 0, 0, ncpp::NCScale::Stretch);
}catch(std::exception& e){
throw TetrisNotcursesErr("visual(): " + s + ": " + e.what());
}
if(!backg_->decode(&averr)){
throw TetrisNotcursesErr("decode(): " + s);
}
if(!backg_->render(0, 0, 0, 0)){
throw TetrisNotcursesErr("render(): " + s);
}
}
// draw the background on the standard plane, then create a new plane for
// the play area.
void DrawBoard() {
DrawBackground(BackgroundFile);
int y, x;
stdplane_->get_dim(&y, &x);
board_top_y_ = y - (BOARD_HEIGHT + 2);
board_ = std::make_unique<ncpp::Plane>(BOARD_HEIGHT, BOARD_WIDTH * 2,
board_top_y_, x / 2 - (BOARD_WIDTH + 1));
uint64_t channels = 0;
channels_set_fg(&channels, 0x00b040);
if(!stdplane_->cursor_move(y - (BOARD_HEIGHT + 2), x / 2 - (BOARD_WIDTH + 1))){
throw TetrisNotcursesErr("cursor_move()");
}
if(!stdplane_->rounded_box(0, channels, y - 1, x / 2 + BOARD_WIDTH + 1, NCBOXMASK_TOP)){
channels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
if(!board_->rounded_box(0, channels, BOARD_HEIGHT - 1, BOARD_WIDTH * 2 - 1, NCBOXMASK_TOP)){
throw TetrisNotcursesErr("rounded_box()");
}
channels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
board_->set_base(channels, 0, "");
if(!nc_.render()){
throw TetrisNotcursesErr("render()");
}
}
bool PieceStuck(){
bool PieceStuck() {
if(!curpiece_){
return false;
}
@ -114,14 +247,13 @@ private:
curpiece_->get_dim(&y, &x);
--y;
while(x--){
int cmpy = y + 1, cmpx = x; // need absolute coordinates via translation
curpiece_->translate(nullptr, &cmpy, &cmpx);
int cmpy = y + 1, cmpx = x; // need game area coordinates via translation
curpiece_->translate(*board_, &cmpy, &cmpx);
ncpp::Cell c;
auto egc = nc_.get_at(cmpy, cmpx, c);
if(!egc){
return false; // FIXME is this not indicative of an error?
if(board_->get_at(cmpy, cmpx, &c) < 0){
throw TetrisNotcursesErr("get_at()");
}
if(*egc && *egc != ' '){
if(c.get().gcluster && c.get().gcluster != ' '){
return true;
}
}
@ -130,20 +262,20 @@ private:
// tidx is an index into tetriminos. yoff and xoff are relative to the
// terminal's origin. returns colored north-facing tetrimino on a plane.
std::unique_ptr<ncpp::Plane> NewPiece(){
std::unique_ptr<ncpp::Plane> NewPiece() {
const int tidx = random() % 7;
const struct tetrimino* t = &tetriminos[tidx];
const size_t cols = strlen(t->texture);
int y, x;
stdplane_->get_dim(&y, &x);
const int xoff = x / 2 - BOARD_WIDTH + (random() % BOARD_WIDTH - 3);
const int yoff = y - (BOARD_HEIGHT + 4);
std::unique_ptr<ncpp::Plane> n = std::make_unique<ncpp::Plane>(2, cols, yoff, xoff, nullptr);
const int xoff = x / 2 - BOARD_WIDTH + (random() % BOARD_WIDTH - 1);
std::unique_ptr<ncpp::Plane> n = std::make_unique<ncpp::Plane>(2, cols, board_top_y_ - 2, xoff, nullptr);
if(n){
uint64_t channels = 0;
channels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
channels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
n->set_fg(t->color);
n->set_bg_alpha(CELL_ALPHA_TRANSPARENT);
n->set_base(channels, 0, "");
y = 0;
for(size_t i = 0 ; i < strlen(t->texture) ; ++i){
@ -160,27 +292,36 @@ private:
};
int main(void){
int main(void) {
if(setlocale(LC_ALL, "") == nullptr){
return EXIT_FAILURE;
}
std::atomic_bool gameover = false;
notcurses_options ncopts{};
ncpp::NotCurses nc(ncopts);
Tetris t{nc};
Tetris t{nc, gameover};
std::thread tid(&Tetris::Ticker, &t);
ncpp::Plane* stdplane = nc.get_stdplane();
char32_t input;
ncinput ni;
while((input = nc.getc(true, &ni)) != (char32_t)-1){
while(!gameover && (input = nc.getc(true, &ni)) != (char32_t)-1){
if(input == 'q'){
break;
}
switch(input){
case NCKEY_LEFT: break;
case NCKEY_RIGHT: break;
case NCKEY_LEFT: t.MoveLeft(); break;
case NCKEY_RIGHT: t.MoveRight(); break;
case 'z': t.RotateCcw(); break;
case 'x': t.RotateCw(); break;
default:
stdplane->cursor_move(0, 0);
stdplane->printf("Got unknown input U+%06x", input);
nc.render();
break;
}
}
if(input == 'q'){
t.Stop();
if(gameover || input == 'q'){
gameover = true;
tid.join();
}else{
return EXIT_FAILURE;

@ -48,11 +48,17 @@ TEST_CASE("Fills") {
CHECK(0 == ncplane_destroy(pfn));
}
SUBCASE("PolyfillStandardPlane") {
cell c = CELL_SIMPLE_INITIALIZER('-');
CHECK(0 < ncplane_polyfill_yx(n_, 0, 0, &c));
CHECK(0 == notcurses_render(nc_));
}
SUBCASE("PolyfillEmptyPlane") {
cell c = CELL_SIMPLE_INITIALIZER('+');
struct ncplane* pfn = ncplane_new(nc_, 4, 4, 0, 0, nullptr);
struct ncplane* pfn = ncplane_new(nc_, 20, 20, 0, 0, nullptr);
REQUIRE(nullptr != pfn);
CHECK(16 == ncplane_polyfill_yx(pfn, 0, 0, &c));
CHECK(400 == ncplane_polyfill_yx(pfn, 0, 0, &c));
CHECK(0 == notcurses_render(nc_));
CHECK(0 == ncplane_destroy(pfn));
}
@ -319,7 +325,7 @@ TEST_CASE("Fills") {
CHECK(0 == notcurses_render(nc_));
}
SUBCASE("MergeDown") {
SUBCASE("MergeDownASCII") {
auto p1 = ncplane_new(nc_, 1, 10, 0, 0, nullptr);
REQUIRE(p1);
// make sure glyphs replace nulls
@ -353,6 +359,83 @@ TEST_CASE("Fills") {
ncplane_destroy(p1);
}
SUBCASE("MergeDownUni") {
auto p1 = ncplane_new(nc_, 1, 10, 0, 0, nullptr);
REQUIRE(p1);
// make sure glyphs replace nulls
CHECK(0 < ncplane_putstr(p1, "█▀▄▌▐🞵🞶🞷🞸🞹"));
CHECK(0 == ncplane_mergedown(p1, n_));
cell cbase = CELL_TRIVIAL_INITIALIZER;
cell cp = CELL_TRIVIAL_INITIALIZER;
for(int i = 0 ; i < 10 ; ++i){
CHECK(0 < ncplane_at_yx(n_, 0, i, &cbase));
CHECK(0 < ncplane_at_yx(p1, 0, i, &cp));
CHECK(0 == cellcmp(n_, &cbase, p1, &cp));
}
ncplane_destroy(p1);
CHECK(0 == notcurses_render(nc_));
auto p3 = ncplane_new(nc_, 1, 10, 0, 0, nullptr);
CHECK(0 == ncplane_cursor_move_yx(p3, 0, 0));
// make sure glyphs replace glyps
CHECK(0 < ncplane_putstr(p3, "🞵🞶🞷🞸🞹█▀▄▌▐"));
CHECK(0 == ncplane_mergedown(p3, NULL));
cell c3 = CELL_TRIVIAL_INITIALIZER;
for(int i = 0 ; i < 10 ; ++i){
CHECK(0 < ncplane_at_yx(n_, 0, i, &cbase));
CHECK(0 < ncplane_at_yx(p3, 0, i, &c3));
CHECK(0 == cellcmp(n_, &cbase, p3, &c3));
}
CHECK(0 == notcurses_render(nc_));
// make sure nulls do not replace glyphs
auto p2 = ncplane_new(nc_, 1, 10, 0, 0, nullptr);
CHECK(0 == ncplane_mergedown(p2, NULL));
ncplane_destroy(p2);
for(int i = 0 ; i < 10 ; ++i){
CHECK(0 < ncplane_at_yx(n_, 0, i, &cbase));
CHECK(0 < ncplane_at_yx(p3, 0, i, &c3));
CHECK(0 == cellcmp(n_, &cbase, p3, &c3));
}
ncplane_destroy(p3);
CHECK(0 == notcurses_render(nc_));
}
// test merging down one plane to another plane which is smaller than the
// standard plane
SUBCASE("MergeDownSmallPlane") {
constexpr int DIMX = 10;
constexpr int DIMY = 10;
auto p1 = ncplane_new(nc_, DIMY, DIMX, 2, 2, nullptr);
REQUIRE(p1);
cell c1 = CELL_TRIVIAL_INITIALIZER;
CHECK(0 < cell_load(p1, &c1, ""));
CHECK(0 == cell_set_bg(&c1, 0x00ff00));
CHECK(0 == cell_set_fg(&c1, 0x0000ff));
ncplane_polyfill_yx(p1, 0, 0, &c1);
CHECK(0 == notcurses_render(nc_));
auto p2 = ncplane_new(nc_, DIMY / 2, DIMX / 2, 3, 3, nullptr);
REQUIRE(p2);
cell c2 = CELL_TRIVIAL_INITIALIZER;
CHECK(0 < cell_load(p2, &c2, "🞶"));
CHECK(0 == cell_set_bg(&c2, 0x00ffff));
CHECK(0 == cell_set_fg(&c2, 0xff00ff));
ncplane_polyfill_yx(p2, 0, 0, &c2);
CHECK(0 == ncplane_mergedown(p2, p1));
CHECK(0 == notcurses_render(nc_));
for(int y = 0 ; y < DIMY ; ++y){
for(int x = 0 ; x < DIMX ; ++x){
CHECK(0 < ncplane_at_yx(p1, y, x, &c1));
if(y < 1 || y > 5 || x < 1 || x > 5){
CHECK(0 == strcmp(extended_gcluster(p1, &c1), ""));
}else{
CHECK(0 < ncplane_at_yx(p2, y - 1, x - 1, &c2));
CHECK(0 == cellcmp(p1, &c1, p2, &c2));
}
}
}
ncplane_destroy(p1);
ncplane_destroy(p2);
}
CHECK(0 == notcurses_stop(nc_));
CHECK(0 == fclose(outfp_));

Loading…
Cancel
Save