2 * Copyright Neil Brown ©2015-2021 <neil@brown.name>
3 * May be distributed under terms of GPLv2 - see file:COPYING
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.
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.
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
24 #define _GNU_SOURCE /* for asprintf */
30 struct render_list *next;
31 const char *text_orig;
32 const char *text safe, *attr safe; // both are allocated
35 const char *xypos; /* location in text_orig where given x,y was found */
52 static int draw_some(struct pane *p safe, struct pane *focus safe,
53 struct render_list **rlp safe,
55 const char *start safe, const char **endp safe,
56 const char *attr safe, int margin, int cursorpos, int xpos,
59 /* Measure the text from 'start' to '*endp', expecting to
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
71 int len = *endp - start;
73 struct call_return cr;
76 int rmargin = p->w - margin;
77 struct render_list *rl;
81 if (len == 0 && cursorpos < 0)
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.
93 str = strndup(start, len);
95 /* TABs are only sent one at a time, and are rendered as space */
97 if (xpos >= 0 && xpos >= *x && xpos < rmargin) {
98 /* reduce marking to given position, and record that
99 * as xypos when we hit it.
105 rl = calloc(1, sizeof(*rl));
106 cr = home_call_ret(all, focus, "Draw:text-size", p,
107 rmargin - *x, NULL, str,
110 if (max == 0 && ret == XYPOS) {
111 /* Must have already reported XY position, don't do it again */
114 rmargin = p->w - margin;
115 cr = home_call_ret(all, focus, "Draw:text-size", p,
116 rmargin - *x, NULL, str,
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..
126 cr = home_call_ret(all, focus, "Draw:text-size", p,
127 rmargin - *x, NULL, str,
131 rl->text_orig = start;
133 rl->attr = strdup(attr);
138 rl->xypos = start + strlen(str);
140 if (cursorpos >= 0 && cursorpos <= len && cursorpos <= max)
141 rl->cursorpos = cursorpos;
150 /* Didn't draw everything. */
155 static char *get_last_attr(const char *attrs safe, const char *attr safe)
157 const char *com = attrs + strlen(attrs);
158 int len = strlen(attr);
160 for (; com >= attrs ; com--) {
162 if (*com != ',' && com > attrs)
166 if (strncmp(com+i, attr, len) != 0)
168 if (com[i+len] != ':')
171 i = strcspn(com, ",");
172 return strndup(com, i);
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)
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.
189 struct render_list *last_wrap = NULL, *end_wrap = NULL, *last_rl = NULL;
191 int wrap_len = 0; /* length of text in final <wrap> section */
192 struct render_list *rl, *tofree;
198 for (rl = *rlp; wrap_pos && rl; rl = rl->next) {
199 if (strstr(rl->attr, "wrap,") && rl != *rlp) {
205 wrap_len += strlen(rl->text);
215 /* A wrap was found, so finish there */
218 for (rl = *rlp; rl && rl != last_wrap; rl = rl->next) {
219 int cp = rl->cursorpos;
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
231 home_call(focus, "Draw:text", p, cp, NULL, rl->text,
232 scale, NULL, rl->attr,
235 if (xypos && rl->xypos) {
238 *xyattr = strsave(p, rl->attr);
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))
247 if (cp >= 0 && dodraw)
248 home_call(focus, "Draw:text", p, cp, NULL, rl->text,
249 scale, NULL, rl->attr,
251 x = rl->x + rl->width;
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",
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,
271 scale, NULL, last_rl->attr);
272 rl = calloc(1, sizeof(*rl));
274 rl->attr = strdup(last_rl->attr); // FIXME underline,fg:blue ???
280 /* 'x' is how much to shift-left remaining rl entries,
281 * Don't want to shift them over the wrap-head
286 for (rl = tofree; rl && rl != end_wrap; rl = tofree) {
288 free((void*)rl->text);
289 free((void*)rl->attr);
293 /* Shift remaining rl to the left */
294 for (rl = end_wrap; rl; rl = rl->next)
299 static void update_line_height_attr(struct pane *p safe,
300 struct pane *focus safe,
302 int *a safe,int *w, char *attr safe,
303 char *str safe, int scale)
305 struct call_return cr = home_call_ret(all, focus, "Draw:text-size", p,
316 static void strip_ctrl(char *s safe)
319 if (*s < ' ' || ((unsigned)*s >= 128 && (unsigned)*s < 128+' '))
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,
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
337 const char *segstart = line;
338 int above = 0, below = 0;
341 buf_append(&attr, ',');
344 const char *st = line;
345 if (c == '<' && *line == '<') {
352 if (line - 1 > segstart) {
353 char *l = strndup(segstart, line - 1 - segstart);
355 update_line_height_attr(p, focus, h, a, w,
356 buf_final(&attr), l, scale);
359 while (*line && line[-1] != '>')
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,"))
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;
383 update_line_height_attr(p, focus, h, a, w, b, "",
386 /* strip back to ",," */
389 while (attr.len > 0 &&
390 (attr.b[attr.len] != ',' ||
391 attr.b[attr.len+1] != ','))
395 if (line > segstart && line[-1] == '\n')
397 if (line > segstart || !attr_found) {
398 char *l = strndup(segstart, line - segstart);
400 update_line_height_attr(p, focus, h, a, w,
401 buf_final(&attr), l, scale);
406 free(buf_final(&attr));
409 static void render_image(struct pane *p safe, struct pane *focus safe,
410 const char *line safe,
411 int dodraw, int scale)
415 char *size = attr_find(p->attrs, "cached-size");
417 if (!size || sscanf(size, "%hdx%hd", &width, &height) != 2) {
419 width = p->parent->w/2;
420 height = p->parent->h/2;
425 while (*line && *line != '>') {
426 int len = strcspn(line, ",>");
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",
440 if (cr.x > 0 && cr.x < p->parent->w)
442 if (cr.y > 0 && cr.y < p->parent->h)
444 asprintf(&size, "%hdx%hd", width, height);
445 attr_set_str(&p->attrs, "cached-size", size);
448 line += strspn(line, ",");
450 pane_resize(p, (p->parent->w - width)/2, p->y, width, height);
452 home_call(focus, "Draw:image", p, 0, NULL, fname, 5);
457 static void set_xypos(struct render_list *rlst,
458 struct pane *p safe, struct pane *focus safe, int posx,
461 /* Find the text postition in the rlst which corresponds to
462 * the screen position posx, and report attribtes there too.
464 for (; rlst && rlst->x <= posx; rlst = rlst->next) {
465 if (rlst->x + rlst->width < posx)
469 rlst->xypos = rlst->text_orig;
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;
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.
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;
499 short offset = ci->num;
502 const char *line_start;
506 int wrap_offset = 0; /*number of columns displayed in earlier lines */
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");
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;
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.
536 start = line_start = line;
541 home_call(focus, "Draw:clear", p);
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.
549 render_image(p, focus, line, dodraw, scale);
550 attr_set_int(&p->attrs, "line-height", p->h);
555 update_line_height(p, focus, &line_height, &ascent, &twidth, ¢er,
558 if (line_height <= 0)
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",
572 rd->prefix_len = x + shift_left;
577 x += (p->w - x - twidth) / 2;
581 x = p->w - x - twidth + (center + 2);
582 /* tabs are measured against this margin */
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.
597 free((void*)rd->xyattr);
601 while (*line && y < p->h && !end_of_page) {
603 /* mwidth is recalculated whenever attrs change */
604 struct call_return cr = home_call_ret(all, focus,
613 rd->curs_width = mwidth;
616 if (want_xypos == 1 &&
617 y > ci->y - line_height &&
623 if (want_xypos == 1 && xypos) {
624 rd->xyattr = xyattr ? strdup(xyattr) : NULL;
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.
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,
656 if (want_xypos == 1 &&
657 y >= ci->y - line_height &&
659 /* cursor is in the tail of rlst that
660 * was relocated - reassess xypos
662 set_xypos(rlst, p, focus, ci->x, scale);
664 /* truncate: skip over normal text, but
667 line += strcspn(line, "\n");
674 if (line == line_start + offset)
675 rd->curs_width = mwidth;
676 if (ch >= ' ' && ch != '<') {
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
683 if ((*line & 0xc0) == 0x80)
684 /* In the middle of a UTF-8 */
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,
693 offset - (start - line_start),
699 ret = draw_some(p, focus, &rlst, &x, start, &line,
702 in_tab ?:offset - (start - line_start),
705 if (ret != OK || !ch)
710 ret = draw_some(p, focus, &rlst, &x, start, &line,
713 in_tab ?:offset - (start - line_start),
720 const char *a = line;
722 while (*line && line[-1] != '>')
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,
737 atoi(tb+4) * scale / 1000;
739 /* strip back to ",," */
742 while (attr.len >=2 &&
743 (attr.b[attr.len-1] != ',' ||
744 attr.b[attr.len-2] != ','))
749 if (offset == start - line_start)
750 offset += line-start;
760 flush_line(p, focus, dodraw, &rlst, y+ascent, scale, 0,
766 } else if (ch == '\f') {
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,
778 offset == (start - line_start)
783 in_tab = -1; // suppress extra cursors
795 buf_concat(&attr, ",underline,fg:red");
796 ret = draw_some(p, focus, &rlst, &x, buf, &b,
799 offset - (start - line_start),
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 &&
814 draw_some(p, focus, &rlst, &x, start, &line,
816 wrap ? mwidth : 0, offset - (start - line_start),
820 flush_line(p, focus, dodraw, &rlst, y+ascent, scale, 0,
823 if (want_xypos == 1) {
824 rd->xyattr = xyattr ? strdup(xyattr) : NULL;
825 ret_xypos = xypos ?: line;
829 if (offset >= 0 && line - line_start <= offset) {
830 if (y >= 0 && (y == 0 || y + line_height <= p->h)) {
838 /* No newline at the end .. but we must render as whole lines */
840 free(buf_final(&attr));
846 /* Mustn't resize after clearing the pane, or we'll
847 * be out-of-sync with display manager.
849 pane_resize(p, p->x, p->y, p->w, y);
850 attr_set_int(&p->attrs, "line-height", line_height);
852 struct render_list *r = rlst;
854 free((void*)r->text);
855 free((void*)r->attr);
860 return ret_xypos - line_start + 1;
864 return end_of_page ? 2 : 1;
867 DEF_CMD(renderline_get)
869 struct rline_data *rd = ci->home->data;
871 const char *val = buf;
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)
884 comm_call(ci->comm2, "attr", ci->focus, 0, NULL, val);
888 DEF_CMD(renderline_set)
890 struct rline_data *rd = ci->home->data;
891 const char *old = rd->line;
892 struct xy xyscale = pane_scale(ci->focus);
895 rd->line = strdup(ci->str);
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);
904 ci->home->damaged &= ~DAMAGED_VIEW;
908 DEF_CMD(renderline_close)
910 struct rline_data *rd = ci->home->data;
912 free((void*)rd->xyattr);
913 free((void*)rd->line);
918 static struct map *rl_map;
919 DEF_LOOKUP_CMD(renderline_handle, rl_map);
921 DEF_CMD(renderline_attach)
923 struct rline_data *rd;
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);
938 p = pane_register(ci->focus, -10, &renderline_handle.c, rd);
943 return comm_call(ci->comm2, "cb", p);
946 void edlib_init(struct pane *ed safe)
948 call_comm("global-set-command", ed, &renderline_attach, 0, NULL,
949 "attach-renderline");