1 #include <errno.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 #include <string.h>
5 #include "internal.h"
6 
7 typedef enum {
8   DIRECTION_UP,
9   DIRECTION_DOWN,
10 } direction_e; // current direction of travel
11 
12 // A UNIFIED THEORY OF NCREELS
13 // (which are more complex than they may seem)
14 //
15 // We only redraw when ncreel_redraw() is explicitly called (and at creation).
16 // Redrawing consists of arranging the tablets, and calling the user's
17 // callbacks to fill them in. In general, drawing a tablet involves creating a
18 // plane having all available size, calling the callback, and then trimming the
19 // plane if it was not filled.
20 //
21 // Things which can happen between ncreel_redraw() calls:
22 //
23 //  * new focused tablet added (only when no tablets exist)
24 //  * new unfocused tablet added
25 //  * tablet removed (may be the focused tablet)
26 //  * tablets may grow or shrink
27 //  * focus can change due to new selection
28 //
29 // Any number and mix of these events can occur between draws.
30 //
31 // First rule: there must not be 2+ consecutive lines of blank space if there is
32 //             data which could be presented there (always fill the reel).
33 // Second rule: if there must be 2+ consecutive lines of blank space, they must
34 //             all be at the bottom of the reel (connect and anchor the reel).
35 // Third rule: the focused tablet gets all the space it can use.
36 // Fourth rule: thou shalt never wrap a tablet [across a border]
37 // Fifth rule: the focused tablet should remain where it is across redraws,
38 //             except as necessary to accommodate the prior rules.
39 //
40 // At any ncreel_redraw(), you can make three types of moves:
41 //
42 //  - i. moving up and replacing the topmost tablet (spinning operation)
43 //  - ii. moving down and replacing the bottommost tablet (spinning operation)
44 //  - iii. don't move / move otherwise (no necessary spin)
45 //
46 // The first two are simple -- draw the focused tablet next to the appropriate
47 // border of the reel, and then draw what we can in the other direction until
48 // running out of space (and then shift up if there is more than one line of
49 // gap at the top, or if we were moving up from the topmost tablet). This can
50 // be done independently of all other tablets; it is immaterial if some were
51 // removed, added, etc. We can detect these two cases thus:
52 //
53 //  - store a direction_e, written to by ncreel_next(), ncreel_prev(), and
54 //     ncreel_del() (when the focused tablet is deleted), defaulting to DOWN.
55 //  - store a pointer to the "visibly focused" tablet, written and read only by
56 //     ncreel_redraw(). this identifies the focused tablet upon last redraw. if
57 //     the visibly-focused tablet is removed, it instead takes the next tablet,
58 //     iff that tablet is visible. it otherwise takes the prev tablet, iff that
59 //     tablet is visible. it otherwise takes NULL.
60 //  - with these, in ncreel_redraw(), we have:
61 //    - case i iff the last direction was UP, and either the focused tablet is
62 //       not visible, or below the visibly-focused tablet, or there is no
63 //       visibly-focused tablet.
64 //    - case ii iff the last direction was DOWN, and either the focused tablet
65 //       is not visible, or above the visibly-focused tablet, or there is no
66 //       visibly-focused tablet.
67 //
68 // We otherwise have case iii. The focused tablet must be on-screen (if it was
69 // off-screen, we matched one of case i or case ii). We want to draw it as near
70 // to its current position as possible, subject to the first four Rules.
71 //
72 // ncreel_redraw() thus starts by determining the case. This must be done
73 // before any changes are made to the arrangement. It then clears the reel.
74 // The focused tablet is drawn as close to its desired line as possible. For
75 // case i, then draw the tablets below the focused tablet. For case ii, then
76 // draw the tablets above the focused tablet (and move them up, if necessary).
77 // Both of these cases are then handled.
78 //
79 // For case iii, see below...
80 //
81 // On tablet remove:
82 //  * destroy plane, remove from list
83 //  * if tablet was focused, change focus, update travel direction
84 //  * if tablet was visibly focused, change visible focus
85 // On tablet add:
86 //  * add to list (do not create plane)
87 //  * if no tablet existed, change focus to new tablet
88 // On focus change:
89 //  * change focus, update travel direction
90 // On redraw:
91 //  * if no tablets, we're done (deleted planes are already gone)
92 //  * resize focused tablet to maximum, preserving nothing
93 //  * call back for focused tablet redraw
94 //  * shrink focused tablet if applicable
95 //  * place focused tablet according to:
96 //    * the focused tablet should be wholly visible, or if not, use all space
97 //    * the focused tablet should be as close to its old position as possible.
98 //    * if focused tablet was not wholly on screen, it goes to the side
99 //       corresponding to the direction of movement.
100 //  * if out of space or tablets, we're done
101 //  FIXME *maybe* just draw up followed by down, rather than direction of travel?
102 //  * walk the list in the direction of travel, foc->focw
103 //    * designate tablet against the walk as 'focagainst', might be NULL
104 //    * if focagainst || focagainst, focw only through edge, otherwise
105 //    * focw can be as large as all remaining space
106 //    * if there is space, draw what we can of next tablet
107 //    * move the focused tablet againt the direction of travel if necessary
108 //    * prefer the space in the direction of walking
109 //    * last tablet drawn is 'backstop'
110 //  * if out of space or tablets, we're done
111 //  * walk the list in the direction against travel, foc->focw
112 //    * if focw == backstop, we're done
113 //    * draw through edge
114 
115 static int
draw_borders(ncplane * n,unsigned mask,uint64_t channel,direction_e direction)116 draw_borders(ncplane* n, unsigned mask, uint64_t channel, direction_e direction){
117   unsigned lenx, leny;
118   ncplane_dim_yx(n, &leny, &lenx);
119   int maxx = lenx - 1;
120   int maxy = leny - 1;
121   nccell ul, ur, ll, lr, hl, vl;
122   nccell_init(&ul); nccell_init(&ur); nccell_init(&hl);
123   nccell_init(&ll); nccell_init(&lr); nccell_init(&vl);
124   if(nccells_rounded_box(n, 0, channel, &ul, &ur, &ll, &lr, &hl, &vl)){
125     return -1;
126   }
127 //fprintf(stderr, "drawing borders %p ->%d/%d, mask: %04x\n", w, maxx, maxy, mask);
128   // lenx is the number of columns we have, but drop 2 due to corners. we thus
129   // want lenx - 2 horizontal lines in a top or bottom border.
130   int y = 0;
131   if(y < maxy || direction == DIRECTION_DOWN || (mask & NCBOXMASK_BOTTOM)){
132     if(!(mask & NCBOXMASK_TOP)){
133       ncplane_home(n);
134       ncplane_putc(n, &ul);
135       ncplane_hline(n, &hl, lenx - 2);
136       ncplane_putc(n, &ur);
137       ++y;
138     }
139   }
140   // draw the vertical sides, assuming they're not masked out. start wherever
141   // we're left following the previous stanza, end based on maxhorizy.
142   const bool candrawbottom = y <= maxy || direction == DIRECTION_UP || (mask & NCBOXMASK_TOP);
143   const int maxhorizy = maxy - (candrawbottom && !(mask & NCBOXMASK_BOTTOM));
144   int ret = 0;
145   while(y <= maxhorizy){
146     if(!(mask & NCBOXMASK_LEFT)){
147       ret |= ncplane_cursor_move_yx(n, y, 0);
148       ncplane_putc(n, &vl);
149     }
150     if(!(mask & NCBOXMASK_RIGHT)){
151       ret |= ncplane_cursor_move_yx(n, y, maxx);
152       ncplane_putc(n, &vl);
153     }
154     ++y;
155   }
156   if(candrawbottom){
157     if(!(mask & NCBOXMASK_BOTTOM)){
158       ret |= ncplane_cursor_move_yx(n, maxy, 0);
159       ncplane_putc(n, &ll);
160       ncplane_hline(n, &hl, lenx - 2);
161       ncplane_putc(n, &lr);
162     }
163   }
164   nccell_release(n, &ul); nccell_release(n, &ur); nccell_release(n, &hl);
165   nccell_release(n, &ll); nccell_release(n, &lr); nccell_release(n, &vl);
166   return ret;
167 }
168 
169 // Draws the border (if one should be drawn) around the ncreel, and enforces
170 // any provided restrictions on visible window size.
171 static int
draw_ncreel_borders(const ncreel * nr)172 draw_ncreel_borders(const ncreel* nr){
173   return draw_borders(nr->p, nr->ropts.bordermask, nr->ropts.borderchan,
174                       DIRECTION_UP); // direction shouldn't matter for reel
175 }
176 
177 // Calculate the starting and ending coordinates available for occupation by
178 // the tablet, relative to the ncreel's ncplane. Returns non-zero if the
179 // tablet cannot be made visible as specified. If this is the focused tablet
180 // (nr->tablets == t), it can take the entire reel -- frontiery is only a
181 // suggestion in this case -- so give it the full breadth.
182 static int
tablet_geom(const ncreel * nr,nctablet * t,int * begx,int * begy,int * lenx,int * leny,int frontiertop,int frontierbottom,direction_e direction)183 tablet_geom(const ncreel* nr, nctablet* t, int* begx, int* begy,
184             int* lenx, int* leny, int frontiertop, int frontierbottom,
185             direction_e direction){
186 //fprintf(stderr, "jigsawing %p with %d/%d dir %d\n", t, frontiertop, frontierbottom, direction);
187   *begy = 0;
188   *begx = 0;
189   unsigned uleny, ulenx;
190   ncplane_dim_yx(nr->p, &uleny, &ulenx);
191   *leny = uleny;
192   *lenx = ulenx;
193   if(frontiertop < 0){
194     if(direction == DIRECTION_UP){
195       return -1;
196     }
197     frontiertop = 0;
198   }
199   if(frontierbottom >= *leny){
200     if(direction == DIRECTION_DOWN){
201       return -1;
202     }
203     frontierbottom = *leny - 1;
204   }
205   // account for the ncreel borders on the sides
206   if(!(nr->ropts.bordermask & NCBOXMASK_LEFT)){
207     ++*begx;
208     --*lenx;
209   }
210   if(!(nr->ropts.bordermask & NCBOXMASK_RIGHT)){
211     --*lenx;
212   }
213   if(!(nr->ropts.bordermask & NCBOXMASK_TOP)){
214     ++*begy;
215     --*leny;
216   }
217   if(!(nr->ropts.bordermask & NCBOXMASK_BOTTOM)){
218     --*leny;
219   }
220   // at this point, our coordinates describe the largest possible tablet for
221   // this ncreel. this is the correct solution for the focused tablet. other
222   // tablets can only grow in one of two directions, so tighten them up.
223   if(nr->tablets != t){
224     *leny -= (frontierbottom - (frontiertop + 1));
225     if(direction == DIRECTION_DOWN){
226       *begy = frontierbottom;
227     }else{
228       *begy = frontiertop - *leny;
229     }
230   }
231   if(*leny <= 0 || *lenx <= 0){
232     return -1;
233   }
234   return 0;
235 }
236 
237 // kill the planes associated with |t|
238 static void
nctablet_wipeout(nctablet * t)239 nctablet_wipeout(nctablet* t){
240   if(t){
241     if(ncplane_set_widget(t->p, NULL, NULL) == 0){
242       ncplane_destroy_family(t->p);
243     }
244     t->p = NULL;
245     t->cbp = NULL;
246   }
247 }
248 
249 // destroy all existing tablet planes pursuant to redraw
250 static void
clean_reel(ncreel * r)251 clean_reel(ncreel* r){
252   nctablet* vft = r->vft;
253   if(vft){
254     for(nctablet* n = vft->next ; n->p && n != vft ; n = n->next){
255 //fprintf(stderr, "CLEANING NEXT: %p (%p)\n", n, n->p);
256       nctablet_wipeout(n);
257     }
258     for(nctablet* n = vft->prev ; n->p && n != vft ; n = n->prev){
259 //fprintf(stderr, "CLEANING PREV: %p (%p)\n", n, n->p);
260       nctablet_wipeout(n);
261     }
262 //fprintf(stderr, "CLEANING VFT: %p (%p)\n", vft, vft->p);
263     nctablet_wipeout(vft);
264     r->vft = NULL;
265   }
266 }
267 
268 // used as ncplane widget destructor callback, and likewise the heart of
269 // ncreel_del(). without this, at context collapse time we'd wipe out all the
270 // planes (without wiping out their subsidiaries), and ncreel_destroy() would
271 // walk freed memory. as we have no handle on the ncreel, we do not update its
272 // metadata (tabletcount), nor redraw, but who cares? we're shutting down.
273 static void
nctablet_delete_internal(struct nctablet * t)274 nctablet_delete_internal(struct nctablet* t){
275   t->prev->next = t->next;
276   t->next->prev = t->prev;
277   if(t->p){
278     if(ncplane_set_widget(t->p, NULL, NULL) == 0){
279       ncplane_destroy_family(t->p);
280     }
281   }
282   free(t);
283 }
284 
ncreel_del(ncreel * nr,struct nctablet * t)285 int ncreel_del(ncreel* nr, struct nctablet* t){
286   if(t == NULL){
287     return -1;
288   }
289   if(nr->tablets == t){
290     if((nr->tablets = t->next) == t){
291       nr->tablets = NULL;
292     }
293     // FIXME ought set direction based on actual location of replacement tablet
294     nr->direction = LASTDIRECTION_DOWN;
295   }
296   if(nr->vft == t){
297     clean_reel(nr); // maybe just make nr->tablets the vft?
298   }
299   nctablet_delete_internal(t);
300   --nr->tabletcount;
301   ncreel_redraw(nr);
302   return 0;
303 }
304 
305 // Draw the specified tablet, if possible. DIRECTION_UP means we're laying out
306 // bottom-to-top. DIRECTION_DOWN means top-to-bottom. 'frontier{top, bottom}'
307 // are the lines to which we'll be fitting the tablet ('frontiertop' to our
308 // last row for DIRECTION_UP, and 'frontierbottom' to our first row for
309 // DIRECTION_DOWN). Gives the tablet all possible space to work with (i.e.
310 // everything beyond the frontiers, or the entire reel for the focused tablet).
311 // If the callback uses less space, shrinks the plane to that size.
312 static int
ncreel_draw_tablet(const ncreel * nr,nctablet * t,int frontiertop,int frontierbottom,direction_e direction)313 ncreel_draw_tablet(const ncreel* nr, nctablet* t, int frontiertop,
314                    int frontierbottom, direction_e direction){
315   if(t->p || t->cbp){
316 //fprintf(stderr, "already drew %p: %p %p\n", t, t->p, t->cbp);
317     return -1;
318   }
319   int lenx, leny, begy, begx;
320   if(tablet_geom(nr, t, &begx, &begy, &lenx, &leny, frontiertop, frontierbottom, direction)){
321 //fprintf(stderr, "no room: %p base %d/%d len %d/%d dir %d\n", t, begy, begx, leny, lenx, direction);
322     return -1;
323   }
324 //fprintf(stderr, "p tplacement: %p base %d/%d len %d/%d frontiery %d %d dir %d\n", t, begy, begx, leny, lenx, frontiertop, frontierbottom, direction);
325   struct ncplane_options nopts = {
326     .y = begy,
327     .x = begx,
328     .rows = leny,
329     .cols = lenx,
330     .name = "tab",
331   };
332   ncplane* fp = ncplane_create(nr->p, &nopts);
333   if((t->p = fp) == NULL){
334 //fprintf(stderr, "failure creating border plane %d %d %d %d\n", leny, lenx, begy, begx);
335     return -1;
336   }
337   ncplane_set_widget(t->p, t, (void(*)(void*))nctablet_delete_internal);
338   // we allow the callback to use a bound plane that lives above our border
339   // plane, thus preventing the callback from spilling over the tablet border.
340   int cby = 0, cbx = 0, cbleny = leny, cblenx = lenx;
341   //cbleny -= !(nr->ropts.tabletmask & NCBOXMASK_BOTTOM);
342   // FIXME shouldn't this be instead "drop 1 if either is unmasked"?
343   if(!(nr->ropts.tabletmask & NCBOXMASK_TOP)){
344     --cbleny;
345     ++cby;
346   }
347   cblenx -= !(nr->ropts.tabletmask & NCBOXMASK_RIGHT);
348   if(!(nr->ropts.tabletmask & NCBOXMASK_LEFT)){
349     --cblenx;
350     ++cbx;
351   }
352   if(cbleny - cby + 1 > 0){
353 //fprintf(stderr, "cbp placement %dx%d @ %dx%d\n", cbleny, cblenx, cby, cbx);
354     struct ncplane_options dnopts = {
355       .y = cby,
356       .x = cbx,
357       .rows = cbleny,
358       .cols = cblenx,
359       .name = "tdat",
360     };
361     t->cbp = ncplane_create(t->p, &dnopts);
362     if(t->cbp == NULL){
363 //fprintf(stderr, "failure creating data plane %d %d %d %d\n", cbleny, cblenx, cby, cbx);
364       ncplane_destroy(t->p);
365       t->p = NULL;
366       return -1;
367     }
368     ncplane_move_above(t->cbp, t->p);
369 //fprintf(stderr, "calling! lenx/leny: %d/%d cbx/cby: %d/%d cblenx/cbleny: %d/%d dir: %d\n", lenx, leny, cbx, cby, cblenx, cbleny, direction);
370     int ll = t->cbfxn(t, direction == DIRECTION_DOWN);
371 //fprintf(stderr, "RETURNRETURNRETURN %p %d (%d, %d, %d) DIR %d\n", t, ll, cby, cbleny, leny, direction);
372     if(ll > cbleny){
373       logwarn("Tablet callback returned %d lines, %d allowed\n", ll, cbleny);
374       ll = cbleny;
375     }
376     if(ll != cbleny){
377       int diff = cbleny - ll;
378 //fprintf(stderr, "resizing data plane %d->%d\n", cbleny, ll);
379       if(ll){ // must be smaller than the space we provided; add back bottom
380         ncplane_resize_simple(t->cbp, ll, cblenx);
381       }else{
382         ncplane_destroy_family(t->cbp);
383         t->cbp = NULL;
384       }
385       // resize the borderplane iff we got smaller
386       if(!(nr->ropts.tabletmask & NCBOXMASK_BOTTOM)){
387         ncplane_resize_simple(t->p, leny - diff + 1, lenx);
388       }else{
389         ncplane_resize_simple(t->p, leny - diff, lenx);
390       }
391       // We needn't move the resized plane if drawing down, or the focused plane.
392       // The focused tablet will have been resized properly above, but it might
393       // be out of position (the focused tablet ought move as little as possible).
394       // Move it back to the frontier, or the nearest line above if it has grown.
395       if(nr->tablets == t){
396         if(leny - frontiertop + 1 < ll){
397           ncplane_yx(fp, &frontiertop, NULL);
398           frontiertop += (leny - ll);
399         }
400         ncplane_move_yx(fp, frontiertop, begx);
401 //fprintf(stderr, "moved to frontiertop %d\n", frontiertop);
402       }else if(direction == DIRECTION_UP){
403         ncplane_move_yx(fp, begy + diff, begx);
404 //fprintf(stderr, "MOVEDOWN from %d to %d\n", begy, begy + diff);
405       }
406       cbleny = ll;
407     }
408   }
409   // we can't push the border plane beyond its true boundaries, or we'll mess
410   // up layout later. instead, add a bottommask iff leny <= cbleny + 1
411   unsigned mask = nr->ropts.tabletmask;
412   if(leny <= cbleny + !(mask & NCBOXMASK_TOP)){
413     mask |= NCBOXMASK_BOTTOM;
414   }
415   uint64_t channels = nr->tablets == t ? nr->ropts.focusedchan : nr->ropts.tabletchan;
416   draw_borders(fp, mask, channels, direction);
417   return 0;
418 }
419 
420 // move down below the focused tablet, filling up the reel to the bottom.
421 // returns the last tablet drawn.
422 static nctablet*
draw_following_tablets(const ncreel * nr,nctablet * otherend,int frontiertop,int * frontierbottom)423 draw_following_tablets(const ncreel* nr, nctablet* otherend,
424                        int frontiertop, int* frontierbottom){
425   const bool botborder = !(nr->ropts.bordermask & NCBOXMASK_BOTTOM);
426 //fprintf(stderr, "following otherend: %p ->p: %p %d/%d\n", otherend, otherend->p, frontiertop, *frontierbottom);
427   nctablet* working = nr->tablets->next;
428   const int maxx = ncplane_dim_y(nr->p) - 1 - botborder;
429   // move down past the focused tablet, filling up the reel to the bottom
430   while(*frontierbottom <= maxx && (working != otherend || !otherend->p)){
431     if(working->p){
432       break;
433     }
434 //fprintf(stderr, "following otherend: %p ->p: %p\n", otherend, otherend->p);
435     // modify frontier based off the one we're at
436 //fprintf(stderr, "EASTBOUND AND DOWN: %p->%p %d %d\n", working, working->next, frontiertop, *frontierbottom);
437     if(ncreel_draw_tablet(nr, working, frontiertop, *frontierbottom, DIRECTION_DOWN)){
438       return NULL;
439     }
440     if(working == otherend){
441       otherend = otherend->next;
442     }
443     *frontierbottom += ncplane_dim_y(working->p) + 1;
444     working = working->next;
445   }
446 //fprintf(stderr, "done with prevs: %p->%p %d %d\n", working, working->prev, frontiertop, *frontierbottom);
447   return working;
448 }
449 
450 // move up above the focused tablet, filling up the reel to the top.
451 // returns the last tablet drawn.
452 static nctablet*
draw_previous_tablets(const ncreel * nr,nctablet * otherend,int * frontiertop,int frontierbottom)453 draw_previous_tablets(const ncreel* nr, nctablet* otherend,
454                       int* frontiertop, int frontierbottom){
455   const bool topborder = !(nr->ropts.bordermask & NCBOXMASK_TOP);
456   nctablet* upworking = nr->tablets->prev;
457 //fprintf(stderr, "preceding %p otherend: %p ->p: %p frontiers: %d %d\n", upworking, otherend, otherend->p, *frontiertop, frontierbottom);
458   // modify frontier based off the one we're at
459   while(*frontiertop >= topborder && (upworking != otherend || !otherend->p)){
460     if(upworking->p){
461       break;
462     }
463 //fprintf(stderr, "MOVIN' ON UP: %p->%p %d %d\n", upworking, upworking->prev, *frontiertop, frontierbottom);
464     if(ncreel_draw_tablet(nr, upworking, *frontiertop, frontierbottom, DIRECTION_UP)){
465       return NULL;
466     }
467     if(upworking == otherend){
468       otherend = otherend->prev;
469     }
470     *frontiertop -= ncplane_dim_y(upworking->p) + 1;
471     upworking = upworking->prev;
472   }
473 //fprintf(stderr, "done with prevs: %p->%p %d %d\n", upworking, upworking->prev, *frontiertop, frontierbottom);
474   return upworking;
475 }
476 
477 // Tablets are initially drawn assuming more space to be available than may
478 // actually exist. We do a pass at the end trimming any overhang.
479 static int
trim_reel_overhang(ncreel * r,nctablet * top,nctablet * bottom)480 trim_reel_overhang(ncreel* r, nctablet* top, nctablet* bottom){
481   int y;
482   if(!top || !top->p || !bottom || !bottom->p){
483     return -1;
484   }
485 //fprintf(stderr, "trimming: top %p bottom %p\n", top->p, bottom->p);
486   ncplane_yx(top->p, &y, NULL);
487   unsigned ylen, xlen;
488   ncplane_dim_yx(top->p, &ylen, &xlen);
489   const int miny = !(r->ropts.bordermask & NCBOXMASK_TOP);
490   int boty = y + ylen - 1;
491 //fprintf(stderr, "top: %dx%d @ %d, miny: %d\n", ylen, xlen, y, miny);
492   if(boty < miny){
493 //fprintf(stderr, "NUKING top!\n");
494     ncplane_destroy_family(top->p);
495     top->p = NULL;
496     top->cbp = NULL;
497     top = top->next;
498     return trim_reel_overhang(r, top, bottom);
499   }else if(y < miny){
500     int ynew = ylen - (miny - y);
501     if(ynew <= 0){
502       ncplane_destroy_family(top->p);
503       top->p = NULL;
504       top->cbp = NULL;
505     }else{
506       if(ncplane_resize(top->p, miny - y, 0, ynew, xlen, 0, 0, ynew, xlen)){
507         return -1;
508       }
509       if(top->cbp){
510         if(ynew == !(r->ropts.tabletmask & NCBOXMASK_TOP)){
511           ncplane_destroy_family(top->cbp);
512           top->cbp = NULL;
513         }else{
514           ncplane_dim_yx(top->cbp, &ylen, &xlen);
515           ynew -= !(r->ropts.tabletmask & NCBOXMASK_TOP);
516           if(ncplane_resize(top->cbp, miny - y, 0, ynew, xlen, 0, 0, ynew, xlen)){
517             return -1;
518           }
519           int x;
520           ncplane_yx(top->cbp, &y, &x);
521           ncplane_move_yx(top->cbp, y - 1, x);
522         }
523       }
524     }
525   }
526   if(bottom->p){
527     ncplane_dim_yx(bottom->p, &ylen, &xlen);
528     ncplane_yx(bottom->p, &y, NULL);
529     const int maxy = ncplane_dim_y(r->p) - (1 + !(r->ropts.bordermask & NCBOXMASK_BOTTOM));
530     boty = y + ylen - 1;
531   //fprintf(stderr, "bot: %dx%d @ %d, maxy: %d\n", ylen, xlen, y, maxy);
532     if(maxy < y){
533   //fprintf(stderr, "NUKING bottom!\n");
534       if(ncplane_set_widget(bottom->p, NULL, NULL) == 0){
535         ncplane_destroy_family(bottom->p);
536       }
537       bottom->p = NULL;
538       bottom->cbp = NULL;
539       bottom = bottom->prev;
540       return trim_reel_overhang(r, top, bottom);
541     }if(maxy < boty){
542       int ynew = ylen - (boty - maxy);
543       if(ynew <= 0){
544         ncplane_destroy_family(bottom->p);
545         bottom->p = NULL;
546         bottom->cbp = NULL;
547       }else{
548         if(ncplane_resize(bottom->p, 0, 0, ynew, xlen, 0, 0, ynew, xlen)){
549           return -1;
550         }
551   //fprintf(stderr, "TRIMMED bottom %p from %d to %d (%d)\n", bottom->p, ylen, ynew, maxy - boty);
552         if(bottom->cbp){
553           if(ynew == !(r->ropts.tabletmask & NCBOXMASK_BOTTOM)){
554             ncplane_destroy_family(bottom->cbp);
555             bottom->cbp = NULL;
556           }else{
557             ncplane_dim_yx(bottom->cbp, &ylen, &xlen);
558             ynew -= !(r->ropts.tabletmask & NCBOXMASK_BOTTOM);
559             if(ncplane_resize(bottom->cbp, 0, 0, ynew, xlen, 0, 0, ynew, xlen)){
560               return -1;
561             }
562           }
563         }
564       }
565     }
566   }
567 //fprintf(stderr, "finished trimming\n");
568   return 0;
569 }
570 
571 static int
tighten_reel_down(ncreel * r,int ybot)572 tighten_reel_down(ncreel* r, int ybot){
573   nctablet* cur = r->tablets;
574   while(cur){
575     if(cur->p == NULL){
576       break;
577     }
578     int cury, curx;
579     unsigned ylen;
580     ncplane_yx(cur->p, &cury, &curx);
581     ncplane_dim_yx(cur->p, &ylen, NULL);
582     if(cury <= ybot - (int)ylen - 1){
583       break;
584     }
585 //fprintf(stderr, "tightening %p down to %d from %d\n", cur, ybot - ylen, cury);
586     cury = ybot - (int)ylen;
587     ncplane_move_yx(cur->p, cury, curx);
588     ybot = cury - 1;
589     if((cur = cur->prev) == r->tablets){
590       break;
591     }
592   }
593   return 0;
594 }
595 
596 // run at the end of redraw, this aligns the top tablet with the top of the
597 // reel. we prefer empty space at the bottom (FIXME but not really -- we ought
598 // prefer space away from the last direction of movement. rather than this
599 // postprocessing, draw things to the right places!). we then trim any tablet
600 // overhang. FIXME could pass top/bottom in directly, available as otherend
601 static int
tighten_reel(ncreel * r)602 tighten_reel(ncreel* r){
603 //fprintf(stderr, "tightening it up\n");
604   nctablet* top = r->tablets;
605   nctablet* cur = top;
606   int ytop = INT_MAX;
607   // find the top tablet
608   while(cur){
609     if(cur->p == NULL){
610       break;
611     }
612     int cury;
613     ncplane_yx(cur->p, &cury, NULL);
614     if(cury >= ytop){
615       break;
616     }
617     ytop = cury;
618     top = cur;
619     cur = cur->prev;
620   }
621   int expected = !(r->ropts.bordermask & NCBOXMASK_TOP);
622   cur = top;
623   nctablet* bottom = r->tablets;
624   // find the bottom tablet, moving tablets up as we go along
625   while(cur){
626     if(cur->p == NULL){
627       break;
628     }
629     int cury, curx;
630     ncplane_yx(cur->p, &cury, &curx);
631     if(cury != expected){
632       if(ncplane_move_yx(cur->p, expected, curx)){
633 //fprintf(stderr, "tightened %p up to %d\n", cur, expected);
634         return -1;
635       }
636     }
637     unsigned ylen;
638     ncplane_dim_yx(cur->p, &ylen, NULL);
639     expected += ylen + 1;
640 //fprintf(stderr, "bottom (%p) gets %p\n", bottom, cur);
641     bottom = cur;
642     cur = cur->next;
643     if(cur == top){
644       break;
645     }
646   }
647   cur = r->tablets;
648   if(cur){
649     const ncplane* n = cur->p;
650     int yoff;
651     unsigned ylen, rylen;
652     ncplane_dim_yx(r->p, &rylen, NULL);
653     ncplane_yx(n, &yoff, NULL);
654     ncplane_dim_yx(n, &ylen, NULL);
655     const int ybot = rylen - 1 + !!(r->ropts.bordermask & NCBOXMASK_BOTTOM);
656     // FIXME want to tighten down whenever we're at the bottom, and the reel
657     // is full, not just in this case (this can leave a gap of more than 1 row)
658     if(yoff + (int)ylen + 1 >= ybot){
659       if(tighten_reel_down(r, ybot)){
660         return -1;
661       }
662     }
663   }
664   if(!top || !bottom){
665     return 0;
666   }
667   return trim_reel_overhang(r, top, bottom);
668 }
669 
670 // Arrange the panels, starting with the focused window, wherever it may be.
671 // If necessary, resize it to the full size of the reel--focus has its
672 // privileges. We then work in the opposite direction of travel, filling out
673 // the reel above and below. If we moved down to get here, do the tablets above
674 // first. If we moved up, do the tablets below. This ensures tablets stay in
675 // place relative to the new focus; they could otherwise pivot around the new
676 // focus, if we're not filling out the reel.
ncreel_redraw(ncreel * nr)677 int ncreel_redraw(ncreel* nr){
678 //fprintf(stderr, "\n--------> BEGIN REDRAW <--------\n");
679   nctablet* focused = nr->tablets;
680   int fulcrum; // target line
681   if(nr->direction == LASTDIRECTION_UP){
682     // case i iff the last direction was UP, and either the focused tablet is
683     // not visible, or below the visibly-focused tablet, or there is no
684     // visibly-focused tablet.
685     if(!focused || !focused->p || !nr->vft){
686       fulcrum = 0;
687 //fprintf(stderr, "case i fulcrum %d\n", fulcrum);
688     }else{
689       int focy, vfty;
690       ncplane_yx(focused->p, &focy, NULL);
691       ncplane_yx(nr->vft->p, &vfty, NULL);
692       if(focy > vfty){
693         fulcrum = 0;
694 //fprintf(stderr, "case i fulcrum %d (%d %d) %p %p\n", fulcrum, focy, vfty, focused, nr->vft);
695       }else{
696         ncplane_yx(focused->p, &fulcrum, NULL); // case iii
697 //fprintf(stderr, "case iii fulcrum %d (%d %d) %p %p lastdir: %d\n", fulcrum, focy, vfty, focused, nr->vft, nr->direction);
698       }
699     }
700   }else{
701     // case ii iff the last direction was DOWN, and either the focused tablet
702     // is not visible, or above the visibly-focused tablet, or there is no
703     // visibly-focused tablet.
704     if(!focused || !focused->p || !nr->vft){
705       fulcrum = ncplane_dim_y(nr->p) - 1;
706 //fprintf(stderr, "case ii fulcrum %d\n", fulcrum);
707     }else{
708       int focy, vfty;
709 //fprintf(stderr, "focused: %p\n", focused);
710       ncplane_yx(focused->p, &focy, NULL);
711       ncplane_yx(nr->vft->p, &vfty, NULL);
712       if(focy < vfty){
713         fulcrum = ncplane_dim_y(nr->p) - 1;
714 //fprintf(stderr, "case ii fulcrum %d (%d %d) %p %p\n", fulcrum, focy, vfty, focused, nr->vft);
715       }else{
716         ncplane_yx(focused->p, &fulcrum, NULL); // case iii
717 //fprintf(stderr, "case iiib fulcrum %d (%d %d) %p %p lastdir: %d\n", fulcrum, focy, vfty, focused, nr->vft, nr->direction);
718       }
719     }
720   }
721   clean_reel(nr);
722   if(focused){
723 //fprintf(stderr, "drawing focused tablet %p dir: %d fulcrum: %d!\n", focused, nr->direction, fulcrum);
724     if(ncreel_draw_tablet(nr, focused, fulcrum, fulcrum, DIRECTION_DOWN)){
725       logerror("Error drawing tablet\n");
726       return -1;
727     }
728 //fprintf(stderr, "drew focused tablet %p -> %p lastdir: %d!\n", focused, focused->p, nr->direction);
729     nctablet* otherend = focused;
730     int frontiertop, frontierbottom;
731     ncplane_yx(nr->tablets->p, &frontiertop, NULL);
732     frontierbottom = frontiertop + ncplane_dim_y(nr->tablets->p) + 1;
733     frontiertop -= 2;
734     if(nr->direction == LASTDIRECTION_DOWN){
735       otherend = draw_previous_tablets(nr, otherend, &frontiertop, frontierbottom);
736       if(otherend == NULL){
737         logerror("Error drawing higher tablets\n");
738         return -1;
739       }
740       otherend = draw_following_tablets(nr, otherend, frontiertop, &frontierbottom);
741     }else{ // DIRECTION_UP
742       otherend = draw_previous_tablets(nr, otherend, &frontiertop, frontierbottom);
743       if(otherend == NULL){
744         logerror("Error drawing higher tablets\n");
745         return -1;
746       }
747       otherend = draw_following_tablets(nr, otherend, frontiertop, &frontierbottom);
748     }
749     if(otherend == NULL){
750       logerror("Error drawing following tablets\n");
751       return -1;
752     }
753 //notcurses_debug(ncplane_notcurses(nr->p), stderr);
754     if(tighten_reel(nr)){
755       logerror("Error tightening reel\n");
756       return -1;
757     }
758 //notcurses_debug(ncplane_notcurses(nr->p), stderr);
759   }
760   nr->vft = nr->tablets; // update the visually-focused tablet pointer
761 //fprintf(stderr, "DONE ARRANGING\n");
762   if(draw_ncreel_borders(nr)){
763     logerror("Error drawing reel borders\n");
764     return -1; // enforces specified dimensional minima
765   }
766   return 0;
767 }
768 
769 static bool
validate_ncreel_opts(ncplane * n,const ncreel_options * ropts)770 validate_ncreel_opts(ncplane* n, const ncreel_options* ropts){
771   if(n == NULL){
772     return false;
773   }
774   if(ropts->flags >= (NCREEL_OPTION_CIRCULAR << 1u)){
775     logwarn("Provided unsupported flags 0x%016" PRIx64 "\n", ropts->flags);
776   }
777   if(ropts->flags & NCREEL_OPTION_CIRCULAR){
778     if(!(ropts->flags & NCREEL_OPTION_INFINITESCROLL)){
779       logerror("Can't set circular without infinitescroll\n");
780       return false; // can't set circular without infinitescroll
781     }
782   }
783   // there exist higher NCBOX defines, but they're not valid in this context
784   const unsigned fullmask = NCBOXMASK_LEFT |
785                             NCBOXMASK_RIGHT |
786                             NCBOXMASK_TOP |
787                             NCBOXMASK_BOTTOM;
788   if(ropts->bordermask > fullmask){
789     logerror("Bad bordermask: 0x%016x\n", ropts->bordermask);
790     return false;
791   }
792   if(ropts->tabletmask > fullmask){
793     logerror("Bad tabletmask: 0x%016x\n", ropts->bordermask);
794     return false;
795   }
796   return true;
797 }
798 
nctablet_plane(nctablet * t)799 ncplane* nctablet_plane(nctablet* t){
800   return t->cbp;
801 }
802 
ncreel_plane(ncreel * nr)803 ncplane* ncreel_plane(ncreel* nr){
804   return nr->p;
805 }
806 
ncreel_create(ncplane * n,const ncreel_options * ropts)807 ncreel* ncreel_create(ncplane* n, const ncreel_options* ropts){
808   ncreel_options zeroed = {};
809   ncreel* nr;
810   if(!ropts){
811     ropts = &zeroed;
812   }
813   if(!validate_ncreel_opts(n, ropts)){
814     return NULL;
815   }
816   if((nr = malloc(sizeof(*nr))) == NULL){
817     return NULL;
818   }
819   nr->tablets = NULL;
820   nr->tabletcount = 0;
821   nr->direction = LASTDIRECTION_DOWN; // draw down after the initial tablet
822   memcpy(&nr->ropts, ropts, sizeof(*ropts));
823   nr->p = n;
824   nr->vft = NULL;
825   if(ncplane_set_widget(nr->p, nr, (void(*)(void*))ncreel_destroy)){
826     ncplane_destroy(nr->p);
827     free(nr);
828     return NULL;
829   }
830   if(ncreel_redraw(nr)){
831     ncplane_destroy(nr->p);
832     free(nr);
833     return NULL;
834   }
835   return nr;
836 }
837 
ncreel_add(ncreel * nr,nctablet * after,nctablet * before,tabletcb cbfxn,void * opaque)838 nctablet* ncreel_add(ncreel* nr, nctablet* after, nctablet *before,
839                      tabletcb cbfxn, void* opaque){
840   nctablet* t;
841   if(after && before){
842     if(after->next != before || before->prev != after){
843       logerror("bad before (%p) / after (%p) spec\n", before, after);
844       return NULL;
845     }
846   }else if(!after && !before){
847     // This way, without user interaction or any specification, new tablets are
848     // inserted at the "end" relative to the focus. The first one to be added
849     // gets and keeps the focus. New ones will go on the bottom, until we run
850     // out of space. New tablets are then created off-screen.
851     before = nr->tablets;
852   }
853   if((t = malloc(sizeof(*t))) == NULL){
854     return NULL;
855   }
856 //fprintf(stderr, "--------->NEW TABLET %p\n", t);
857   if(after){
858     t->next = after->next;
859     after->next = t;
860     t->prev = after;
861     t->next->prev = t;
862   }else if(before){
863     t->prev = before->prev;
864     before->prev = t;
865     t->next = before;
866     t->prev->next = t;
867   }else{ // we're the first tablet
868     t->prev = t->next = t;
869     nr->tablets = t;
870   }
871   t->cbfxn = cbfxn;
872   t->curry = opaque;
873   ++nr->tabletcount;
874   t->p = NULL;
875   t->cbp = NULL;
876   ncreel_redraw(nr);
877   return t;
878 }
879 
ncreel_destroy(ncreel * nreel)880 void ncreel_destroy(ncreel* nreel){
881   if(nreel){
882     if(ncplane_set_widget(nreel->p, NULL, NULL) == 0){
883       nctablet* t;
884       while( (t = nreel->tablets) ){
885         ncreel_del(nreel, t);
886       }
887       ncplane_destroy(nreel->p);
888     }
889     free(nreel);
890   }
891 }
892 
nctablet_userptr(nctablet * t)893 void* nctablet_userptr(nctablet* t){
894   return t->curry;
895 }
896 
ncreel_tabletcount(const ncreel * nreel)897 int ncreel_tabletcount(const ncreel* nreel){
898   return nreel->tabletcount;
899 }
900 
ncreel_focused(ncreel * nr)901 nctablet* ncreel_focused(ncreel* nr){
902   return nr->tablets;
903 }
904 
ncreel_next(ncreel * nr)905 nctablet* ncreel_next(ncreel* nr){
906   if(nr->tablets){
907     nr->tablets = nr->tablets->next;
908 //fprintf(stderr, "---------------> moved to next, %p to %p <----------\n", nr->tablets->prev, nr->tablets);
909     nr->direction = LASTDIRECTION_DOWN;
910     ncreel_redraw(nr);
911   }
912   return nr->tablets;
913 }
914 
ncreel_prev(ncreel * nr)915 nctablet* ncreel_prev(ncreel* nr){
916   if(nr->tablets){
917     nr->tablets = nr->tablets->prev;
918 //fprintf(stderr, "----------------> moved to prev, %p to %p <----------\n", nr->tablets->next, nr->tablets);
919     nr->direction = LASTDIRECTION_UP;
920     ncreel_redraw(nr);
921   }
922   return nr->tablets;
923 }
924 
ncreel_offer_input(ncreel * n,const ncinput * nc)925 bool ncreel_offer_input(ncreel* n, const ncinput* nc){
926   if(nc->evtype == NCTYPE_RELEASE){
927     return false;
928   }
929   if(nc->id == NCKEY_UP){
930     ncreel_prev(n);
931     return true;
932   }else if(nc->id == NCKEY_DOWN){
933     ncreel_next(n);
934     return true;
935   }else if(nc->id == NCKEY_SCROLL_UP){
936     ncreel_prev(n);
937     return true;
938   }else if(nc->id == NCKEY_SCROLL_DOWN){
939     ncreel_next(n);
940     return true;
941   }
942   // FIXME page up/page down
943   // FIXME end/home (when not using infinite scrolling)
944   return false;
945 }
946