2 * Copyright Neil Brown ©2015-2020 <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
29 struct render_list *next;
30 const char *text_orig;
31 const char *text safe, *attr safe; // both are allocated
34 const char *xypos; /* location in text_orig where given x,y was found */
51 static int draw_some(struct pane *p safe, struct pane *focus safe,
52 struct render_list **rlp safe,
54 const char *start safe, const char **endp safe,
55 const char *attr safe, int margin, int cursorpos, int xpos,
58 /* Measure the text from 'start' to '*endp', expecting to
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
70 int len = *endp - start;
72 struct call_return cr;
75 int rmargin = p->w - margin;
76 struct render_list *rl;
80 if (len == 0 && cursorpos < 0)
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.
92 str = strndup(start, len);
94 /* TABs are only sent one at a time, and are rendered as space */
96 if (xpos >= 0 && xpos >= *x && xpos < rmargin) {
97 /* reduce marking to given position, and record that
98 * as xypos when we hit it.
104 rl = calloc(1, sizeof(*rl));
105 cr = home_call_ret(all, focus, "text-size", p, rmargin - *x, NULL, str,
108 if (max == 0 && ret == XYPOS) {
109 /* Must have already reported XY position, don't do it again */
112 rmargin = p->w - margin;
113 cr = home_call_ret(all, focus, "text-size", p,
114 rmargin - *x, NULL, str,
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..
124 cr = home_call_ret(all, focus, "text-size", p,
125 rmargin - *x, NULL, str,
129 rl->text_orig = start;
131 rl->attr = strdup(attr);
136 rl->xypos = start + strlen(str);
138 if (cursorpos >= 0 && cursorpos <= len && cursorpos <= max)
139 rl->cursorpos = cursorpos;
148 /* Didn't draw everything. */
153 static char *get_last_attr(const char *attrs safe, const char *attr safe)
155 const char *com = attrs + strlen(attrs);
156 int len = strlen(attr);
158 for (; com >= attrs ; com--) {
160 if (*com != ',' && com > attrs)
164 if (strncmp(com+i, attr, len) != 0)
166 if (com[i+len] != ':')
169 i = strcspn(com, ",");
170 return strndup(com, i);
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)
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.
187 struct render_list *last_wrap = NULL, *end_wrap = NULL, *last_rl = NULL;
189 int wrap_len = 0; /* length of text in final <wrap> section */
190 struct render_list *rl, *tofree;
196 for (rl = *rlp; wrap_pos && rl; rl = rl->next) {
197 if (strstr(rl->attr, "wrap,") && rl != *rlp) {
203 wrap_len += strlen(rl->text);
213 /* A wrap was found, so finish there */
216 for (rl = *rlp; rl && rl != last_wrap; rl = rl->next) {
217 int cp = rl->cursorpos;
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
229 home_call(focus, "Draw:text", p, cp, NULL, rl->text,
230 scale, NULL, rl->attr,
233 if (xypos && rl->xypos) {
236 *xyattr = strsave(p, rl->attr);
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))
245 if (cp >= 0 && dodraw)
246 home_call(focus, "Draw:text", p, cp, NULL, rl->text,
247 scale, NULL, rl->attr,
249 x = rl->x + rl->width;
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",
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,
269 scale, NULL, last_rl->attr);
270 rl = calloc(1, sizeof(*rl));
272 rl->attr = strdup(last_rl->attr); // FIXME underline,fg:blue ???
278 /* 'x' is how much to shift-left remaining rl entries,
279 * Don't want to shift them over the wrap-head
284 for (rl = tofree; rl && rl != end_wrap; rl = tofree) {
286 free((void*)rl->text);
287 free((void*)rl->attr);
291 /* Shift remaining rl to the left */
292 for (rl = end_wrap; rl; rl = rl->next)
297 static void update_line_height_attr(struct pane *p safe,
298 struct pane *focus safe,
300 int *a safe,int *w, char *attr safe,
301 char *str safe, int scale)
303 struct call_return cr = home_call_ret(all, focus, "text-size", p,
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,
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
326 const char *segstart = line;
327 int above = 0, below = 0;
330 buf_append(&attr, ',');
333 const char *st = line;
334 if (c == '<' && *line == '<') {
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);
347 while (*line && line[-1] != '>')
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,"))
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;
371 update_line_height_attr(p, focus, h, a, w, b, "",
374 /* strip back to ",," */
377 while (attr.len > 0 &&
378 (attr.b[attr.len] != ',' ||
379 attr.b[attr.len+1] != ','))
383 if (line > segstart && line[-1] == '\n')
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);
393 free(buf_final(&attr));
396 static void render_image(struct pane *p safe, struct pane *focus safe,
397 const char *line safe,
398 int dodraw, int scale)
401 short width = p->parent->w/2, height = p->parent->h/2;
406 while (*line && *line != '>') {
407 int len = strcspn(line, ",>");
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;
420 line += strspn(line, ",");
422 pane_resize(p, (p->parent->w - width)/2, p->y, width, height);
424 home_call(focus, "Draw:image", p, 0, NULL, fname, 5);
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)
435 rlst->x + rlst->width < posx)
440 *xypos = rlst->text_orig;
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;
448 *xyattr = strsave(p, rlst->attr);
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.
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;
470 short offset = ci->num2;
474 const char *line_start;
478 int wrap_offset = 0; /*number of columns displayed in earlier lines */
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");
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;
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.
508 start = line_start = line;
511 home_call(focus, "pane-clear", p);
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.
519 render_image(p, focus, line, dodraw, scale);
520 attr_set_int(&p->attrs, "line-height", p->h);
525 update_line_height(p, focus, &line_height, &ascent, &twidth, ¢er,
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",
539 rd->prefix_len = x + shift_left;
544 x += (p->w - x - twidth) / 2;
548 x = p->w - x - twidth + (center + 2);
549 /* tabs are measured against this margin */
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.
564 free((void*)rd->xyattr);
570 while (*line && y < p->h && !end_of_page) {
574 /* mwidth is recalculated whenever attrs change */
575 struct call_return cr = home_call_ret(all, focus,
584 rd->curs_width = mwidth;
588 /* Found the cursor, stop looking */
589 posy = -1; posx = -1;
591 if (y+line_height >= posy &&
592 y <= posy && x <= posx)
597 if (y > posy && want_xypos == 1 && xypos) {
598 rd->xyattr = xyattr ? strdup(xyattr) : NULL;
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
613 if (cstart != start || y != cy) {
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,
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,
645 rd->xyattr = xyattr ?
646 strdup(xyattr) : NULL;
652 /* truncate: skip over normal text, but
655 line += strcspn(line, "\n");
662 if (line == line_start + offset)
663 rd->curs_width = mwidth;
664 if (ch >= ' ' && ch != '<') {
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
671 if ((*line & 0xc0) == 0x80)
672 /* In the middle of a UTF-8 */
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,
681 offset - (start - line_start),
687 ret = draw_some(p, focus, &rlst, &x, start, &line,
690 in_tab ?:offset - (start - line_start),
693 if (ret != OK || !ch)
698 ret = draw_some(p, focus, &rlst, &x, start, &line,
701 in_tab ?:offset - (start - line_start),
708 const char *a = line;
710 while (*line && line[-1] != '>')
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,
725 atoi(tb+4) * scale / 1000;
727 /* strip back to ",," */
730 while (attr.len >=2 &&
731 (attr.b[attr.len-1] != ',' ||
732 attr.b[attr.len-2] != ','))
737 if (offset == start - line_start)
738 offset += line-start;
748 flush_line(p, focus, dodraw, &rlst, y+ascent, scale, 0,
754 if (xypos && want_xypos == 1) {
755 rd->xyattr = xyattr ? strdup(xyattr) : NULL;
759 } else if (ch == '\f') {
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,
771 offset == (start - line_start)
776 in_tab = -1; // suppress extra cursors
788 buf_concat(&attr, ",underline,fg:red");
789 ret = draw_some(p, focus, &rlst, &x, buf, &b,
792 offset - (start - line_start),
798 if (!*line && (line > start || offset == start - line_start)) {
799 /* Some more to draw */
800 draw_some(p, focus, &rlst, &x, start, &line,
802 wrap ? mwidth : 0, offset - (start - line_start),
806 flush_line(p, focus, dodraw, &rlst, y+ascent, scale, 0,
809 if (want_xypos == 1) {
810 rd->xyattr = xyattr ? strdup(xyattr) : NULL;
811 ret_xypos = xypos ?: line;
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) {
827 /* No newline at the end .. but we must render as whole lines */
829 free(buf_final(&attr));
835 /* Mustn't resize after clearing the pane, or we'll
836 * be out-of-sync with display manager.
838 pane_resize(p, p->x, p->y, p->w, y);
839 attr_set_int(&p->attrs, "line-height", line_height);
841 struct render_list *r = rlst;
843 free((void*)r->text);
844 free((void*)r->attr);
849 return ret_xypos - line_start + 1;
853 return end_of_page ? 2 : 1;
856 DEF_CMD(renderline_get)
858 struct rline_data *rd = ci->home->data;
860 const char *val = buf;
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)
870 else if (strcmp(ci->str, "render-line:valid") == 0)
871 snprintf(buf, sizeof(buf), "%d",rd->is_valid);
875 comm_call(ci->comm2, "attr", ci->focus, 0, NULL, val);
879 DEF_CMD(renderline_set)
881 struct rline_data *rd = ci->home->data;
883 free((void*)rd->line);
885 rd->line = strdup(ci->str);
888 rd->is_valid = !!ci->str;
892 DEF_CMD(renderline_invalidate)
894 struct rline_data *rd = ci->home->data;
900 DEF_CMD(renderline_close)
902 struct rline_data *rd = ci->home->data;
904 free((void*)rd->xyattr);
905 free((void*)rd->line);
910 static struct map *rl_map;
911 DEF_LOOKUP_CMD(renderline_handle, rl_map);
913 DEF_CMD(renderline_attach)
915 struct rline_data *rd;
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);
931 p = pane_register(ci->focus, -10, &renderline_handle.c, rd);
936 return comm_call(ci->comm2, "cb", p);
939 void edlib_init(struct pane *ed safe)
941 call_comm("global-set-command", ed, &renderline_attach, 0, NULL,
942 "attach-renderline");