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