]> git.neil.brown.name Git - edlib.git/blob - lib-renderline.c
renderline: update comments and minor code improvements.
[edlib.git] / lib-renderline.c
1 /*
2  * Copyright Neil Brown ©2015-2020 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * A renderline pane will take a single line of marked-up text
6  * and draw it.  The "line" may well be longer that the width
7  * of the pane, and it might then be wrapped generatinging
8  * multiple display lines.
9  *
10  * The render-lines pane will place multiple renderline panes and use
11  * them to do the drawing - resizing and moving them as necessary to fit
12  * the size of the text.
13  *
14  * A renderline normally is only active when the render-lines (or other)
15  * parent pane is being refreshed - that pane hands over some of the
16  * task to the renderline pane.
17  * Specifically a "draw-markup" command provides a marked-up line of
18  * text, a scale, and other details.  The resulting image in measured
19  * and possibly drawn
20  *
21  *
22  */
23
24 #include <stdio.h>
25 #include "core.h"
26 #include "misc.h"
27
28 struct render_list {
29         struct render_list *next;
30         const char      *text_orig;
31         const char      *text safe, *attr safe; // both are allocated
32         short           x, width;
33         short           cursorpos;
34         const char      *xypos; /* location in text_orig where given x,y was found */
35 };
36
37 struct rline_data {
38         short           prefix_len;
39         const char      *xyattr;
40         short           curs_width;
41         const char      *line;
42         bool            is_valid;
43 };
44
45 enum {
46         OK = 0,
47         WRAP,
48         XYPOS,
49 };
50
51 static int draw_some(struct pane *p safe, struct pane *focus safe,
52                      struct render_list **rlp safe,
53                      int *x safe,
54                      const char *start safe, const char **endp safe,
55                      const char *attr safe, int margin, int cursorpos, int xpos,
56                      int scale)
57 {
58         /* Measure the text from 'start' to '*endp', expecting to
59          * draw to p[x,?].
60          * Update 'x' and 'endp' past what was drawn.
61          * Everything will be drawn with the same attributes: attr.
62          * If the text would get closer to right end than 'margin',
63          * we stop drawing before then.  If this happens, WRAP is returned.
64          * If drawing would pass xpos: stop there, record pointer
65          * into 'endp', and return XYPOS.
66          * If cursorpos is between 0 and len inclusive, a cursor is drawn there.
67          * Don't actually draw anything, just append a new entry to the
68          * render_list rlp.
69          */
70         int len = *endp - start;
71         char *str;
72         struct call_return cr;
73         int max;
74         int ret = WRAP;
75         int rmargin = p->w - margin;
76         struct render_list *rl;
77
78         if (cursorpos > len)
79                 cursorpos = -1;
80         if (len == 0 && cursorpos < 0)
81                 /* Nothing to do */
82                 return OK;
83         if ((*rlp == NULL ||
84              ((*rlp)->next == NULL && (*rlp)->text_orig == NULL)) &&
85             strstr(attr, "wrap,") && cursorpos < 0)
86                 /* The text is a <wrap> marker that causes a wrap is
87                  * suppressed unless the cursor is in it.
88                  * This will only ever be at start of line.  <wrap> text
89                  * elsewhere is not active.
90                  */
91                 return OK;
92         str = strndup(start, len);
93         if (*str == '\t')
94                 /* TABs are only sent one at a time, and are rendered as space */
95                 *str = ' ';
96         if (xpos >= 0 && xpos >= *x && xpos < rmargin) {
97                 /* reduce marking to given position, and record that
98                  * as xypos when we hit it.
99                  */
100                 rmargin = xpos;
101                 ret = XYPOS;
102         }
103
104         rl = calloc(1, sizeof(*rl));
105         cr = home_call_ret(all, focus, "text-size", p, rmargin - *x, NULL, str,
106                            scale, NULL, attr);
107         max = cr.i;
108         if (max == 0 && ret == XYPOS) {
109                 /* Must have already reported XY position, don't do it again */
110                 rl->xypos = start;
111                 ret = WRAP;
112                 rmargin = p->w - margin;
113                 cr = home_call_ret(all, focus, "text-size", p,
114                                    rmargin - *x, NULL, str,
115                                    scale, NULL, attr);
116                 max = cr.i;
117         }
118         if (max < len) {
119                 /* It didn't all fit, so trim the string and
120                  * try again.  It must fit this time.
121                  * I don't know what we expect to be different the second time..
122                  */
123                 str[max] = 0;
124                 cr = home_call_ret(all, focus, "text-size", p,
125                                    rmargin - *x, NULL, str,
126                                    scale, NULL, attr);
127         }
128
129         rl->text_orig = start;
130         rl->text = str;
131         rl->attr = strdup(attr);
132         rl->width = cr.x;
133         rl->x = *x;
134         *x += rl->width;
135         if (ret == XYPOS)
136                 rl->xypos = start + strlen(str);
137
138         if (cursorpos >= 0 && cursorpos <= len && cursorpos <= max)
139                 rl->cursorpos = cursorpos;
140         else
141                 rl->cursorpos = -1;
142         while (*rlp)
143                 rlp = &(*rlp)->next;
144         *rlp = rl;
145
146         if (max >= len)
147                 return OK;
148         /* Didn't draw everything. */
149         *endp = start + max;
150         return ret;
151 }
152
153 static char *get_last_attr(const char *attrs safe, const char *attr safe)
154 {
155         const char *com = attrs + strlen(attrs);
156         int len = strlen(attr);
157
158         for (; com >= attrs ; com--) {
159                 int i = 1;
160                 if (*com != ',' && com > attrs)
161                         continue;
162                 if (com == attrs)
163                         i = 0;
164                 if (strncmp(com+i, attr, len) != 0)
165                         continue;
166                 if (com[i+len] != ':')
167                         continue;
168                 com += i+len+1;
169                 i = strcspn(com, ",");
170                 return strndup(com, i);
171         }
172         return NULL;
173 }
174
175 static int flush_line(struct pane *p safe, struct pane *focus safe, int dodraw,
176                       struct render_list **rlp safe,
177                       int y, int scale, int wrap_pos,
178                       const char **xypos, const char **xyattr)
179 {
180         /* Flush a render_list returning x-space used.
181          * If wrap_pos is > 0, stop rendering before last entry with the
182          * "wrap" attribute; draw the wrap-tail at wrap_pos, and insert
183          * wrap_head into the render_list before the next line.
184          * If any render_list entry reports xypos, store that in xypos with
185          * matching attributes in xyattr.
186          */
187         struct render_list *last_wrap = NULL, *end_wrap = NULL, *last_rl = NULL;
188         int in_wrap = 0;
189         int wrap_len = 0; /* length of text in final <wrap> section */
190         struct render_list *rl, *tofree;
191         int x = 0;
192         char *head;
193
194         if (!*rlp)
195                 return 0;
196         for (rl = *rlp; wrap_pos && rl; rl = rl->next) {
197                 if (strstr(rl->attr, "wrap,") && rl != *rlp) {
198                         if (!in_wrap) {
199                                 last_wrap = rl;
200                                 in_wrap = 1;
201                                 wrap_len = 0;
202                         }
203                         wrap_len += strlen(rl->text);
204                         end_wrap = rl->next;
205                 } else {
206                         if (in_wrap)
207                                 end_wrap = rl;
208                         in_wrap = 0;
209                 }
210                 last_rl = rl;
211         }
212         if (last_wrap)
213                 /* A wrap was found, so finish there */
214                 last_rl = last_wrap;
215
216         for (rl = *rlp; rl && rl != last_wrap; rl = rl->next) {
217                 int cp = rl->cursorpos;
218
219                 if (wrap_pos &&
220                     cp >= (int)strlen(rl->text) + wrap_len)
221                         /* Don't want to place cursor at end of line before
222                          * the wrap, only on the next line after the
223                          * wrap.
224                          */
225                         cp = -1;
226
227                 x = rl->x;
228                 if (dodraw)
229                         home_call(focus, "Draw:text", p, cp, NULL, rl->text,
230                                   scale, NULL, rl->attr,
231                                   x, y);
232                 x += rl->width;
233                 if (xypos && rl->xypos) {
234                         *xypos = rl->xypos;
235                         if (xyattr)
236                                 *xyattr = strsave(p, rl->attr);
237                 }
238         }
239         /* Draw the wrap text if it contains the cursor */
240         for (; rl && rl != end_wrap; rl = rl->next) {
241                 int cp = rl->cursorpos;
242                 if (cp >= (int)strlen(rl->text))
243                         cp = -1;
244
245                 if (cp >= 0 && dodraw)
246                         home_call(focus, "Draw:text", p, cp, NULL, rl->text,
247                                   scale, NULL, rl->attr,
248                                   rl->x, y);
249                 x = rl->x + rl->width;
250         }
251         /* Draw the wrap-tail */
252         if (wrap_pos && last_rl && dodraw) {
253                 char *e = get_last_attr(last_rl->attr, "wrap-tail");
254                 home_call(focus, "Draw:text", p, -1, NULL, e ?: "\\",
255                           scale, NULL, "underline,fg:blue",
256                           wrap_pos, y);
257                 free(e);
258         }
259
260         tofree = *rlp;
261         *rlp = end_wrap;
262
263         /* Queue the wrap-head for the next flush */
264         if (wrap_pos && last_rl &&
265             (head = get_last_attr(last_rl->attr, "wrap-head"))) {
266                 struct call_return cr =
267                         home_call_ret(all, focus, "text-size", p,
268                                       p->w, NULL, head,
269                                       scale, NULL, last_rl->attr);
270                 rl = calloc(1, sizeof(*rl));
271                 rl->text = head;
272                 rl->attr = strdup(last_rl->attr); // FIXME underline,fg:blue ???
273                 rl->width = cr.x;
274                 rl->x = 0;
275                 rl->cursorpos = -1;
276                 rl->next = *rlp;
277                 *rlp = rl;
278                 /* 'x' is how much to shift-left remaining rl entries,
279                  * Don't want to shift them over the wrap-head
280                  */
281                 x -= cr.x;
282         }
283
284         for (rl = tofree; rl && rl != end_wrap; rl = tofree) {
285                 tofree = rl->next;
286                 free((void*)rl->text);
287                 free((void*)rl->attr);
288                 free(rl);
289         }
290
291         /* Shift remaining rl to the left */
292         for (rl = end_wrap; rl; rl = rl->next)
293                 rl->x -= x;
294         return x;
295 }
296
297 static void update_line_height_attr(struct pane *p safe,
298                                     struct pane *focus safe,
299                                     int *h safe,
300                                     int *a safe,int *w, char *attr safe,
301                                     char *str safe, int scale)
302 {
303         struct call_return cr = home_call_ret(all, focus, "text-size", p,
304                                               -1, NULL, str,
305                                               scale, NULL, attr);
306         if (cr.y > *h)
307                 *h = cr.y;
308         if (cr.i2 > *a)
309                 *a = cr.i2;
310         if (w)
311                 *w += cr.x;
312 }
313
314 static void update_line_height(struct pane *p safe, struct pane *focus safe,
315                                int *h safe, int *a safe,
316                                int *w safe, int *center, const char *line safe,
317                                int scale)
318 {
319         /* Extract general geometry information from the line.
320          * Maximum height and ascent are extracted together with total
321          * with and h-alignment info:
322          * 0 - left, 1 = centered, >=2 = space-on-left, <=-2 = space from right
323          */
324         struct buf attr;
325         int attr_found = 0;
326         const char *segstart = line;
327         int above = 0, below = 0;
328
329         buf_init(&attr);
330         buf_append(&attr, ',');
331         while (*line) {
332                 char c = *line++;
333                 const char *st = line;
334                 if (c == '<' && *line == '<') {
335                         line += 1;
336                         continue;
337                 }
338                 if (c != '<')
339                         continue;
340
341                 if (line - 1 > segstart) {
342                         char *l = strndup(segstart, line - 1 - segstart);
343                         update_line_height_attr(p, focus, h, a, w,
344                                                 buf_final(&attr), l, scale);
345                         free(l);
346                 }
347                 while (*line && line[-1] != '>')
348                         line += 1;
349                 segstart = line;
350                 if (st[0] != '/') {
351                         char *c2;
352                         char *b;
353
354                         buf_concat_len(&attr, st, line-st);
355                         attr.b[attr.len-1] = ',';
356                         buf_append(&attr, ',');
357                         b = buf_final(&attr);
358                         if (center && strstr(b, ",center,"))
359                                 *center = 1;
360                         if (center && (c2=strstr(b, ",left:")) != NULL)
361                                 *center = 2 + atoi(c2+6) * scale / 1000;
362                         if (center && (c2=strstr(b, ",right:")) != NULL)
363                                 *center = -2 - atoi(c2+7) * scale / 1000;
364                         if ((c2=strstr(b, ",space-above:")) != NULL)
365                                 above = atoi(c2+13) * scale / 1000;
366                         if ((c2=strstr(b, ",space-below:")) != NULL)
367                                 below = atoi(c2+13) * scale / 1000;
368                         if ((c2=strstr(b, ",tab:")) != NULL)
369                                 *w = atoi(c2+5) * scale / 1000;
370                         attr_found = 1;
371                         update_line_height_attr(p, focus, h, a, w, b, "",
372                                                 scale);
373                 } else {
374                         /* strip back to ",," */
375                         if (attr.len >= 2)
376                                 attr.len -= 2;
377                         while (attr.len > 0 &&
378                                (attr.b[attr.len] != ',' ||
379                                 attr.b[attr.len+1] != ','))
380                                 attr.len -= 1;
381                 }
382         }
383         if (line > segstart && line[-1] == '\n')
384                 line -= 1;
385         if (line > segstart || !attr_found) {
386                 char *l = strndup(segstart, line - segstart);
387                 update_line_height_attr(p, focus, h, a, w,
388                                         buf_final(&attr), l, scale);
389                 free(l);
390         }
391         *h += above + below;
392         *a += above;
393         free(buf_final(&attr));
394 }
395
396 static void render_image(struct pane *p safe, struct pane *focus safe,
397                         const char *line safe,
398                         int dodraw, int scale)
399 {
400         char *fname = NULL;
401         short width = p->parent->w/2, height = p->parent->h/2;
402
403         while (*line == '<')
404                 line += 1;
405
406         while (*line && *line != '>') {
407                 int len = strcspn(line, ",>");
408
409                 if (strncmp(line, "image:", 6) == 0) {
410                         const char *cp = line + 6;
411                         fname = strndup(cp, len-6);
412                 } else if (strncmp(line, "width:", 6) == 0) {
413                         width = atoi(line + 6);
414                         width = width * scale / 1000;
415                 } else if (strncmp(line, "height:", 7) == 0) {
416                         height = atoi(line + 7);
417                         height = height * scale / 1000;
418                 }
419                 line += len;
420                 line += strspn(line, ",");
421         }
422         pane_resize(p, (p->parent->w - width)/2, p->y, width, height);
423         if (fname && dodraw)
424                 home_call(focus, "Draw:image", p, 0, NULL, fname, 5);
425
426         free(fname);
427 }
428
429 static void find_xypos(struct render_list *rlst,
430                        struct pane *p safe, struct pane *focus safe, int posx,
431                        int scale, const char **xypos safe,
432                        const char **xyattr safe)
433 {
434         while (rlst &&
435                rlst->x + rlst->width < posx)
436                 rlst = rlst->next;
437         if (!rlst)
438                 return;
439         if (rlst->x > posx)
440                 *xypos = rlst->text_orig;
441         else {
442                 struct call_return cr = home_call_ret(
443                         all, focus, "text-size", p,
444                         posx - rlst->x, NULL, rlst->text,
445                         scale, NULL, rlst->attr);
446                 *xypos = rlst->text_orig + cr.i;
447         }
448         *xyattr = strsave(p, rlst->attr);
449 }
450 /* Render a line, with attributes and wrapping.
451  * The marked-up text to be processed has already been provided with
452  *   render-line:set.  It is in rd->line;
453  * ->num2 is <0, or an index into ->str where the cursor is,
454  *   and the x,y co-ords will be stored in p->cx,p->cy
455  * If key is "render-line:draw", then send drawing commands, otherwise
456  * just perform measurements.
457  * If key is "render-line:findxy", then only measure until the position
458  *   in x,y is reached, then return an index into ->str of where we reached.
459  *   Store the attributes active at the time so they can be fetched later.
460  */
461 DEF_CMD(renderline)
462 {
463         struct pane *p = ci->home;
464         struct rline_data *rd = p->data;
465         struct pane *focus = ci->focus;
466         const char *line = rd->line;
467         int dodraw = strcmp(ci->key, "render-line:draw") == 0;
468         short posx = ci->x;
469         short posy = ci->y;
470         short offset = ci->num2;
471
472         int x = 0;
473         int y = 0;
474         const char *line_start;
475         const char *start;
476         struct buf attr;
477         unsigned char ch;
478         int wrap_offset = 0; /*number of columns displayed in earlier lines */
479         int in_tab = 0;
480         int shift_left = atoi(pane_attr_get(focus, "shift_left") ?:"0");
481         int wrap = shift_left < 0;
482         char *prefix = pane_attr_get(focus, "prefix");
483         int line_height = 0;
484         int ascent = -1;
485         int mwidth = -1;
486         int ret = OK;
487         int twidth = 0;
488         int center = 0;
489         int margin;
490         int end_of_page = 0;
491         struct render_list *rlst = NULL;
492         const char *xypos = NULL;
493         const char *ret_xypos = NULL;
494         const char *xyattr = NULL;
495         int want_xypos = strcmp(ci->key, "render-line:findxy") == 0;
496         const char *cstart = NULL;
497         struct xy xyscale = pane_scale(focus);
498         int scale = xyscale.x;
499         short cx = -1, cy = -1;
500
501         if (!line)
502                 return Enoarg;
503         /* line_start doesn't change
504          * start is the start of the current segment(since attr)
505          *    is update after we call draw_some()
506          * line is where we are now up to.
507          */
508         start = line_start = line;
509
510         if (dodraw)
511                 home_call(focus, "pane-clear", p);
512
513         if (strncmp(line, "<image:",7) == 0) {
514                 /* For now an <image> must be on a line by itself.
515                  * Maybe this can be changed later if I decide on
516                  * something that makes sense.
517                  * The cursor is not on the image.
518                  */
519                 render_image(p, focus, line, dodraw, scale);
520                 attr_set_int(&p->attrs, "line-height", p->h);
521                 p->cx = p->cy = -1;
522                 return 1;
523         }
524
525         update_line_height(p, focus, &line_height, &ascent, &twidth, &center,
526                            line, scale);
527
528         if (!wrap)
529                 x -= shift_left;
530         else
531                 shift_left = 0;
532
533         if (prefix) {
534                 const char *s = prefix + strlen(prefix);
535                 update_line_height_attr(p, focus, &line_height, &ascent, NULL,
536                                         "bold", prefix, scale);
537                 draw_some(p, focus, &rlst, &x, prefix, &s, "bold",
538                           0, -1, -1, scale);
539                 rd->prefix_len = x + shift_left;
540         } else
541                 rd->prefix_len = 0;
542
543         if (center == 1)
544                 x += (p->w - x - twidth) / 2;
545         if (center >= 2)
546                 x += center - 2;
547         if (center <= -2)
548                 x = p->w - x - twidth + (center + 2);
549         /* tabs are measured against this margin */
550         margin = x;
551
552         buf_init(&attr);
553
554         rd->curs_width = 0;
555
556         /* If findxy was requested, posx and posy tells us
557          * what to look for, and we return index into line where this
558          * co-ordinate was reached.
559          * want_xypos will be set to 2 when we pass the co-ordinate
560          * At that time ret_xypos is set, to be used to provide return value.
561          * This might happen when y exceeds ypos, or we hit end-of-page.
562          */
563         if (want_xypos) {
564                 free((void*)rd->xyattr);
565                 rd->xyattr = NULL;
566         } else {
567                 posx = posy = -1;
568         }
569
570         while (*line && y < p->h && !end_of_page) {
571                 int XPOS;
572
573                 if (mwidth <= 0) {
574                         /* mwidth is recalculated whenever attrs change */
575                         struct call_return cr = home_call_ret(all, focus,
576                                                               "text-size", p,
577                                                               -1, NULL, "M",
578                                                               scale, NULL,
579                                                               buf_final(&attr));
580                         mwidth = cr.x;
581                         if (mwidth <= 0)
582                                 mwidth = 1;
583                         if (!rd->curs_width)
584                                 rd->curs_width = mwidth;
585                 }
586
587                 if (ret == XYPOS) {
588                         /* Found the cursor, stop looking */
589                         posy = -1; posx = -1;
590                 }
591                 if (y+line_height >= posy &&
592                     y <= posy && x <= posx)
593                         XPOS = posx;
594                 else
595                         XPOS = -1;
596
597                 if (y > posy && want_xypos == 1 && xypos) {
598                         rd->xyattr = xyattr ? strdup(xyattr) : NULL;
599                         ret_xypos = xypos;
600                         want_xypos = 2;
601                 }
602
603                 if (offset >= 0 && start - line_start <= offset) {
604                         if (y >= 0 && (y == 0 || y + line_height <= p->h)) {
605                                 /* Some chars result in multiple chars
606                                  * being drawn.  If they are on the same
607                                  * line, like spaces in a TAB, we leave
608                                  * cursor at the start.  If on different
609                                  * lines like "\" are e-o-l and char on
610                                  * next line, then leave cursor at first
611                                  * char on next line.
612                                  */
613                                 if (cstart != start || y != cy) {
614                                         cy = y;
615                                         cx = x;
616                                         cstart = start;
617                                 }
618                         } else {
619                                 cy = cx = -1;
620                         }
621                 }
622
623                 if ((ret == WRAP || x >= p->w - mwidth) &&
624                     (line[0] != '<' || line[1] == '<')) {
625                         /* No room for more text */
626                         if (wrap && *line && *line != '\n') {
627                                 int len = flush_line(p, focus, dodraw, &rlst,
628                                                      y+ascent, scale,
629                                                      p->w - mwidth,
630                                                      &xypos, &xyattr);
631                                 wrap_offset += len;
632                                 x -= len;
633                                 if (x < 0)
634                                         x = 0;
635                                 y += line_height;
636                                 if (want_xypos == 1) {
637                                         if (y+line_height >= posy &&
638                                             y <= posy && x > posx) {
639                                                 /* cursor is in field move down */
640                                                 find_xypos(rlst, p, focus,
641                                                            posx, scale,
642                                                            &xypos, &xyattr);
643                                         }
644                                         if (xypos) {
645                                                 rd->xyattr = xyattr ?
646                                                         strdup(xyattr) : NULL;
647                                                 ret_xypos = xypos;
648                                                 want_xypos = 2;
649                                         }
650                                 }
651                         } else {
652                                 /* truncate: skip over normal text, but
653                                  * stop at newline.
654                                  */
655                                 line += strcspn(line, "\n");
656                                 start = line;
657                         }
658                 }
659
660                 ret = OK;
661                 ch = *line;
662                 if (line == line_start + offset)
663                         rd->curs_width = mwidth;
664                 if (ch >= ' ' && ch != '<') {
665                         line += 1;
666                         /* Only flush out if string is getting a bit long.
667                          * i.e.  if we have reached the offset we are
668                          * measuring to, or if we could have reached the
669                          * right margin.
670                          */
671                         if ((*line & 0xc0) == 0x80)
672                                 /* In the middle of a UTF-8 */
673                                 continue;
674                         if (offset == (line - line_start) ||
675                             (line-start) * mwidth >= p->w - x ||
676                             (XPOS > x && (line - start)*mwidth > XPOS - x)) {
677                                 ret = draw_some(p, focus, &rlst, &x, start,
678                                                 &line,
679                                                 buf_final(&attr),
680                                                 wrap ? mwidth : 0,
681                                                 offset - (start - line_start),
682                                                 XPOS, scale);
683                                 start = line;
684                         }
685                         continue;
686                 }
687                 ret = draw_some(p, focus, &rlst, &x, start, &line,
688                                 buf_final(&attr),
689                                 wrap ? mwidth : 0,
690                                 in_tab ?:offset - (start - line_start),
691                                 XPOS, scale);
692                 start = line;
693                 if (ret != OK || !ch)
694                         continue;
695                 if (ch == '<') {
696                         line += 1;
697                         if (*line == '<') {
698                                 ret = draw_some(p, focus, &rlst, &x, start, &line,
699                                                 buf_final(&attr),
700                                                 wrap ? mwidth : 0,
701                                                 in_tab ?:offset - (start - line_start),
702                                                 XPOS, scale);
703                                 if (ret != OK)
704                                         continue;
705                                 start += 2;
706                                 line = start;
707                         } else {
708                                 const char *a = line;
709
710                                 while (*line && line[-1] != '>')
711                                         line += 1;
712
713                                 if (a[0] != '/') {
714                                         int ln = attr.len;
715                                         char *tb;
716
717                                         buf_concat_len(&attr, a, line-a);
718                                         /* mark location with ",," */
719                                         attr.b[attr.len-1] = ',';
720                                         buf_append(&attr, ',');
721                                         tb = strstr(buf_final(&attr)+ln,
722                                                     "tab:");
723                                         if (tb)
724                                                 x = margin +
725                                                         atoi(tb+4) * scale / 1000;
726                                 } else {
727                                         /* strip back to ",," */
728                                         if (attr.len > 0)
729                                                 attr.len -= 2;
730                                         while (attr.len >=2 &&
731                                                (attr.b[attr.len-1] != ',' ||
732                                                 attr.b[attr.len-2] != ','))
733                                                 attr.len -= 1;
734                                         if (attr.len == 1)
735                                                 attr.len = 0;
736                                 }
737                                 if (offset == start - line_start)
738                                         offset += line-start;
739                                 start = line;
740                                 mwidth = -1;
741                         }
742                         continue;
743                 }
744
745                 line += 1;
746                 if (ch == '\n') {
747                         xypos = line-1;
748                         flush_line(p, focus, dodraw, &rlst, y+ascent, scale, 0,
749                                    &xypos, &xyattr);
750                         y += line_height;
751                         x = 0;
752                         wrap_offset = 0;
753                         start = line;
754                         if (xypos && want_xypos == 1) {
755                                 rd->xyattr = xyattr ? strdup(xyattr) : NULL;
756                                 ret_xypos = xypos;
757                                 want_xypos = 2;
758                         }
759                 } else if (ch == '\f') {
760                         x = 0;
761                         start = line;
762                         wrap_offset = 0;
763                         end_of_page = 1;
764                 } else if (ch == '\t') {
765                         int xc = (wrap_offset + x) / mwidth;
766                         /* Note xc might be negative, so "xc % 8" won't work here */
767                         int w = 8 - (xc & 7);
768                         ret = draw_some(p, focus, &rlst, &x, start, &line,
769                                         buf_final(&attr),
770                                         wrap ? mwidth*2: 0,
771                                         offset == (start - line_start)
772                                         ? in_tab : -1,
773                                         XPOS, scale);
774                         if (w > 1) {
775                                 line -= 1;
776                                 in_tab = -1; // suppress extra cursors
777                         } else
778                                 in_tab = 0;
779                         start = line;
780                 } else {
781                         char buf[4];
782                         const char *b;
783                         int l = attr.len;
784                         buf[0] = '^';
785                         buf[1] = ch + '@';
786                         buf[2] = 0;
787                         b = buf+2;
788                         buf_concat(&attr, ",underline,fg:red");
789                         ret = draw_some(p, focus, &rlst, &x, buf, &b,
790                                         buf_final(&attr),
791                                         wrap ? mwidth*2: 0,
792                                         offset - (start - line_start),
793                                         XPOS, scale);
794                         attr.len = l;
795                         start = line;
796                 }
797         }
798         if (!*line && (line > start || offset == start - line_start)) {
799                 /* Some more to draw */
800                 draw_some(p, focus, &rlst, &x, start, &line,
801                           buf_final(&attr),
802                           wrap ? mwidth : 0, offset - (start - line_start),
803                           posx, scale);
804         }
805
806         flush_line(p, focus, dodraw, &rlst, y+ascent, scale, 0,
807                    &xypos, &xyattr);
808
809         if (want_xypos == 1) {
810                 rd->xyattr = xyattr ? strdup(xyattr) : NULL;
811                 ret_xypos = xypos ?: line;
812                 want_xypos = 2;
813         }
814
815         if (offset >= 0 && line - line_start <= offset) {
816                 if (y >= 0 && (y == 0 || y + line_height <= p->h)) {
817                         if (cstart != start || cy != y) {
818                                 cy = y;
819                                 cx = x;
820                                 cstart = start;
821                         }
822                 } else {
823                         cy = cx = -1;
824                 }
825         }
826         if (x > 0 || y == 0)
827                 /* No newline at the end .. but we must render as whole lines */
828                 y += line_height;
829         free(buf_final(&attr));
830         if (offset >= 0) {
831                 p->cx = cx;
832                 p->cy = cy;
833         }
834         if (!dodraw)
835                 /* Mustn't resize after clearing the pane, or we'll
836                  * be out-of-sync with display manager.
837                  */
838                 pane_resize(p, p->x, p->y, p->w, y);
839         attr_set_int(&p->attrs, "line-height", line_height);
840         while (rlst) {
841                 struct render_list *r = rlst;
842                 rlst = r->next;
843                 free((void*)r->text);
844                 free((void*)r->attr);
845                 free(r);
846         }
847         if (want_xypos) {
848                 if (ret_xypos)
849                         return ret_xypos - line_start + 1;
850                 else
851                         return 1;
852         } else
853                 return end_of_page ? 2 : 1;
854 }
855
856 DEF_CMD(renderline_get)
857 {
858         struct rline_data *rd = ci->home->data;
859         char buf[20];
860         const char *val = buf;
861
862         if (!ci->str)
863                 return Enoarg;
864         if (strcmp(ci->str, "prefix_len") == 0)
865                 snprintf(buf, sizeof(buf), "%d", rd->prefix_len);
866         else if (strcmp(ci->str, "curs_width") == 0)
867                 snprintf(buf, sizeof(buf), "%d", rd->curs_width);
868         else if (strcmp(ci->str, "xyattr") == 0)
869                 val = rd->xyattr;
870         else if (strcmp(ci->str, "render-line:valid") == 0)
871                 snprintf(buf, sizeof(buf), "%d",rd->is_valid);
872         else
873                 return Einval;
874
875         comm_call(ci->comm2, "attr", ci->focus, 0, NULL, val);
876         return 1;
877 }
878
879 DEF_CMD(renderline_set)
880 {
881         struct rline_data *rd = ci->home->data;
882
883         free((void*)rd->line);
884         if (ci->str)
885                 rd->line = strdup(ci->str);
886         else
887                 rd->line = NULL;
888         rd->is_valid = !!ci->str;
889         return 1;
890 }
891
892 DEF_CMD(renderline_invalidate)
893 {
894         struct rline_data *rd = ci->home->data;
895
896         rd->is_valid = 0;
897         return 1;
898 }
899
900 DEF_CMD(renderline_close)
901 {
902         struct rline_data *rd = ci->home->data;
903
904         free((void*)rd->xyattr);
905         free((void*)rd->line);
906         rd->xyattr = NULL;
907         return 1;
908 }
909
910 static struct map *rl_map;
911 DEF_LOOKUP_CMD(renderline_handle, rl_map);
912
913 DEF_CMD(renderline_attach)
914 {
915         struct rline_data *rd;
916         struct pane *p;
917
918         if (!rl_map) {
919                 rl_map = key_alloc();
920                 key_add(rl_map, "render-line:draw", &renderline);
921                 key_add(rl_map, "render-line:measure", &renderline);
922                 key_add(rl_map, "render-line:findxy", &renderline);
923                 key_add(rl_map, "render-line:invalidate", &renderline_invalidate);
924                 key_add(rl_map, "get-attr", &renderline_get);
925                 key_add(rl_map, "render-line:set", &renderline_set);
926                 key_add(rl_map, "Close", &renderline_close);
927                 key_add(rl_map, "Free", &edlib_do_free);
928         }
929
930         alloc(rd, pane);
931         p = pane_register(ci->focus, -10, &renderline_handle.c, rd);
932         if (!p) {
933                 unalloc(rd, pane);
934                 return Efail;
935         }
936         return comm_call(ci->comm2, "cb", p);
937 }
938
939 void edlib_init(struct pane *ed safe)
940 {
941         call_comm("global-set-command", ed, &renderline_attach, 0, NULL,
942                   "attach-renderline");
943 }