]> git.neil.brown.name Git - edlib.git/blob - lib-view.c
TODO: clean out done items.
[edlib.git] / lib-view.c
1 /*
2  * Copyright Neil Brown ©2015-2023 <neil@brown.name>
3  * May be distributed under terms of GPLv2 - see file:COPYING
4  *
5  * A buffer can be viewed in a pane.
6  * The pane is (typically) a tile in a display.
7  * As well as content from the buffer, a 'view' provides
8  * a scroll bar and a status line.
9  * These serve to visually separate different views from each other.
10  *
11  */
12
13 #include <unistd.h>
14 #include <stdlib.h>
15 #include <string.h>
16 #include <wchar.h>
17 #include <wctype.h>
18 #include <stdio.h>
19
20 #define PANE_DATA_TYPE struct view_data
21 #include "core.h"
22 #include "misc.h"
23
24 struct view_data {
25         int             border;
26         int             old_border;
27         int             border_width, border_height;
28         int             line_height;
29         int             ascent;
30         int             scroll_bar_y;
31         struct mark     *viewpoint;
32         struct pane     *child;
33 };
34 #include "core-pane.h"
35
36 /* 0 to 4 borders are possible */
37 enum {
38         BORDER_LEFT     = 1,
39         BORDER_RIGHT    = 2,
40         BORDER_TOP      = 4,
41         BORDER_BOT      = 8,
42         BORDER_STATUS   = 16, // override
43 };
44
45 static struct map *view_map safe;
46 DEF_LOOKUP_CMD(view_handle, view_map);
47 static struct pane *do_view_attach(struct pane *par safe, int border);
48 static int calc_border(struct pane *p safe);
49
50 static const char default_status[] =
51         "{!CountLinesAsync}M:{doc-modified?,*,-}{doc-readonly?,%%,  } D:{doc-file-changed?,CHANGED:,}{doc-name%-15} L{^line}/{lines} {display-context}{render-default}/{view-default} {doc-status}";
52 static const char default_title[] =
53         "{doc-name}";
54
55 static char *format_status(const char *status safe,
56                            struct pane *focus safe,
57                            struct mark *pm)
58 {
59         struct buf b;
60         char *close;
61         char *f;
62         char type;
63         int width;
64         char *attr;
65         char sep;
66
67         buf_init(&b);
68         while (*status) {
69                 int point_attr = 0;
70                 int l;
71
72                 if (*status != '{') {
73                         buf_append(&b, *status);
74                         status++;
75                         continue;
76                 }
77                 status += 1;
78                 close = strchr(status, '}');
79                 if (!close)
80                         break;
81                 f = strnsave(focus, status, close-status);
82                 if (!f)
83                         break;
84                 status = close + 1;
85
86                 if (*f == '!' && pm) {
87                         /* This is a command to call */
88                         call(f+1, focus, 0, pm);
89                         continue;
90                 }
91                 if (*f == '^') {
92                         point_attr = 1;
93                         f += 1;
94                 }
95                 /* Some extras here for future expansion */
96                 l = strcspn(f, ":+?#!@$%^&*=<>");
97                 type = f[l];
98                 f[l] = 0;
99                 if (point_attr && pm)
100                         attr = attr_find(*mark_attr(pm), f);
101                 else
102                         attr = pane_attr_get(focus, f);
103                 if (!attr)
104                         attr = "";
105                 switch (type) {
106                 case '%': /* make spaces visible */
107                         if (attr[0] <= ' ' ||
108                             (attr[0] && attr[strlen(attr)-1] <= ' '))
109                                 attr = strconcat(focus, "\"", attr, "\"");
110                         /* fallthrough */
111                 case ':':
112                         /* Format in a field */
113                         width = atoi(f+l+1);
114                         if (width < 0) {
115                                 buf_concat(&b, attr);
116                                 width += strlen(attr);
117                                 while (width < 0) {
118                                         buf_append(&b, ' ');
119                                         width += 1;
120                                 }
121                         } else {
122                                 width -= strlen(attr);
123                                 while (width > 0) {
124                                         buf_append(&b, ' ');
125                                         width -= 1;
126                                 }
127                                 buf_concat(&b, attr);
128                         }
129                         break;
130                 case '?':
131                         /* flag - no, 0, empty, false are second option */
132                         f += l+1;
133                         sep = *f++;
134                         if (!sep)
135                                 break;
136                         if (strcasecmp(attr, "no") == 0 ||
137                             strcasecmp(attr, "false") == 0 ||
138                             strcmp(attr, "0") == 0 ||
139                             strlen(attr) == 0) {
140                                 f = strchr(f, sep);
141                                 if (f)
142                                         f += 1;
143                                 else
144                                         f = "";
145                         }
146                         while (*f && *f != sep) {
147                                 buf_append(&b, *f);
148                                 f += 1;
149                         }
150                         break;
151                 default:
152                         buf_concat(&b, attr);
153                 }
154         }
155         return buf_final(&b);
156 }
157
158 static void one_char(struct pane *p safe, const char *s, char *attr, int x, int y)
159 {
160         call("Draw:text", p, -1, NULL, s, 0, NULL, attr, x, y);
161 }
162
163 DEF_CMD(view_refresh)
164 {
165         struct pane *p = ci->home;
166         struct view_data *vd = p->data;
167         int i;
168         struct mark *pm;
169         const char *status;
170         const char *title;
171
172         if (vd->border <= 0)
173                 return 1;
174         if (vd->line_height <= 0)
175                 return 1;
176
177         call("Draw:clear", p, 0, NULL, "bg:white");
178         pm = call_ret(mark, "doc:point", ci->focus);
179         status = pane_attr_get(ci->focus, "status-line");
180         if (!status)
181                 status = default_status;
182         status = format_status(status, ci->focus, pm);
183         title = pane_attr_get(ci->focus, "pane-title");
184         if (!title)
185                 title = default_title;
186         title = format_status(title, ci->focus, pm);
187
188         mark_watch(pm);
189
190         if (vd->border & BORDER_LEFT) {
191                 /* Left border is (currently) always a scroll bar */
192                 for (i = 0; i < p->h; i += vd->line_height)
193                         one_char(p, "┃", "inverse", 0, i + vd->ascent);
194
195                 if (p->h > 4 * vd->line_height) {
196                         int l;
197                         int vpln = 0;
198                         int mid;
199
200                         if (vd->viewpoint) {
201                                 call("CountLinesAsync", ci->focus, 0, vd->viewpoint);
202                                 vpln = attr_find_int(*mark_attr(vd->viewpoint),
203                                                      "line");
204                         } else if (pm) {
205                                 call("CountLinesAsync", ci->focus, 0, pm);
206                                 vpln = attr_find_int(*mark_attr(pm), "line");
207                         }
208
209                         l = pane_attr_get_int(ci->focus, "lines", 1);
210                         if (l <= 0)
211                                 l = 1;
212                         mid = vd->line_height + (p->h - 4 * vd->line_height) * vpln / l;
213                         one_char(p, "^", NULL, 0, mid-vd->line_height + vd->ascent);
214                         one_char(p, "#", "inverse", 0, mid + vd->ascent);
215                         one_char(p, "v", NULL, 0, mid+vd->line_height + vd->ascent);
216                         one_char(p, "+", "inverse", 0, p->h
217                                  - vd->line_height + vd->ascent);
218                         vd->scroll_bar_y = mid;
219                 }
220         }
221         if (vd->border & BORDER_RIGHT) {
222                 for (i = 0; i < p->h; i += vd->line_height)
223                         one_char(p, "┃", "inverse", p->w-vd->border_width,
224                                  i + vd->ascent);
225         }
226         if (vd->border & BORDER_TOP) {
227                 int label;
228                 for (i = 0; i < p->w; i += vd->border_width)
229                         one_char(p, "━", "inverse", i, vd->ascent);
230                 label = (p->w - strlen(title?:"") * vd->border_width) / 2;
231                 if (label < vd->border_width)
232                         label = 1;
233                 one_char(p, title, "inverse",
234                          label, vd->ascent);
235         }
236         if (vd->border & BORDER_BOT) {
237                 for (i = 0; i < p->w; i+= vd->border_width)
238                         one_char(p, "═", "inverse", i,
239                                  p->h-vd->border_height+vd->ascent);
240
241                 if (!(vd->border & BORDER_TOP) ||
242                     (vd->border & BORDER_STATUS)) {
243                         one_char(p, status, "inverse",
244                                  4*vd->border_width,
245                                  p->h-vd->border_height + vd->ascent);
246                 }
247         }
248         if (!(~vd->border & (BORDER_LEFT|BORDER_BOT)))
249                 /* Both are set */
250                 one_char(p, "┗", "inverse", 0, p->h-vd->border_height+vd->ascent);
251         if (!(~vd->border & (BORDER_RIGHT|BORDER_TOP)))
252                 one_char(p, "╳", "inverse", p->w-vd->border_width, vd->ascent);
253         if (!(~vd->border & (BORDER_LEFT|BORDER_TOP)))
254                 one_char(p, "┏", "inverse", 0, vd->ascent);
255         if (!(~vd->border & (BORDER_RIGHT|BORDER_BOT)))
256                 one_char(p, "┛", "inverse", p->w-vd->border_width, p->h-vd->border_height+vd->ascent);
257
258         return 1;
259 }
260
261 DEF_CMD_CLOSED(view_close)
262 {
263         struct view_data *vd = ci->home->data;
264
265         mark_free(vd->viewpoint);
266         vd->viewpoint = NULL;
267         return 1;
268 }
269
270 DEF_CMD(view_clone)
271 {
272         struct view_data *vd = ci->home->data;
273         struct pane *parent = ci->focus;
274         struct pane *p2;
275
276         p2 = do_view_attach(parent, vd->old_border);
277         if (p2)
278                 pane_clone_children(ci->home, p2);
279         return 1;
280 }
281
282 DEF_CMD(view_child_notify)
283 {
284         struct pane *p = ci->home;
285         struct view_data *vd = p->data;
286
287         if (ci->focus->z)
288                 return 1;
289         if (vd->child && ci->num > 0)
290                 pane_close(vd->child);
291         if (ci->num > 0)
292                 vd->child = ci->focus;
293         else if (vd->child == ci->focus)
294                 vd->child = NULL;
295         p->focus = vd->child;
296         return 1;
297 }
298
299 DEF_CMD(view_refresh_size)
300 {
301         struct pane *p = ci->home;
302         struct view_data *vd = p->data;
303         int x = 0, y = 0;
304         int w = p->w;
305         int h = p->h;
306         int b;
307
308         if (vd->border >= 0)
309                 vd->border = calc_border(ci->focus);
310         b = vd->border < 0 ? 0 : vd->border;
311         if (vd->line_height < 0) {
312                 /* FIXME should use scale */
313                 struct call_return cr = call_ret(all, "Draw:text-size", ci->home,
314                                                  -1, NULL, "M",
315                                                  0, NULL, "bold");
316                 if (cr.ret == 0) {
317                         cr.x = cr.y =1;
318                         cr.i2 = 0;
319                 }
320                 vd->line_height = cr.y;
321                 vd->border_height = cr.y;
322                 vd->border_width = cr.x;
323                 vd->ascent = cr.i2;
324                 attr_set_int(&p->attrs, "border-width", cr.x);
325                 attr_set_int(&p->attrs, "border-height", cr.y);
326 #if 0
327                 if (h < vd->border_height * 3 &&
328                     (b & (BORDER_TOP|BORDER_BOT)) ==
329                     (BORDER_TOP|BORDER_BOT)) {
330                         b &= ~BORDER_TOP;
331                         b &= ~BORDER_BOT;
332                 }
333                 if (w < vd->border_width * 3 &&
334                     (b & (BORDER_LEFT|BORDER_RIGHT)) ==
335                     (BORDER_LEFT|BORDER_RIGHT)) {
336                         b &= ~BORDER_LEFT;
337                         b &= ~BORDER_RIGHT;
338                 }
339 #endif
340         }
341
342         if (b & BORDER_LEFT) {
343                 x += vd->border_width; w -= vd->border_width;
344         }
345         if (b & BORDER_RIGHT) {
346                 w -= vd->border_width;
347         }
348         if (b & BORDER_TOP) {
349                 y += vd->border_height; h -= vd->border_height;
350         }
351         if (b & BORDER_BOT) {
352                 h -= vd->border_height;
353         }
354         if (w <= 0)
355                 w = 1;
356         if (h <= 0)
357                 h = 1;
358         pane_damaged(p, DAMAGED_REFRESH);
359         if (vd->child)
360                 pane_resize(vd->child, x, y, w, h);
361
362         return 1;
363 }
364
365 DEF_CMD(view_status_changed)
366 {
367         if (strcmp(ci->key, "mark:moving") == 0) {
368                 struct mark *pt = call_ret(mark, "doc:point", ci->home);
369                 if (pt != ci->mark)
370                         return 1;
371         }
372         pane_damaged(ci->home, DAMAGED_VIEW);
373         pane_damaged(ci->home, DAMAGED_REFRESH);
374         if (strcmp(ci->key, "view:changed") == 0)
375                 return Efallthrough;
376         return 1;
377 }
378
379 DEF_CMD(view_reposition)
380 {
381         struct view_data *vd = ci->home->data;
382
383         if (!ci->mark)
384                 return Efallthrough;
385
386         if (!vd->viewpoint || !mark_same(vd->viewpoint, ci->mark)) {
387                 pane_damaged(ci->home, DAMAGED_REFRESH);
388                 if (vd->viewpoint)
389                         mark_free(vd->viewpoint);
390                 if (ci->mark)
391                         vd->viewpoint = mark_dup(ci->mark);
392                 else
393                         vd->viewpoint = NULL;
394         }
395         return Efallthrough;
396 }
397
398 static struct pane *do_view_attach(struct pane *par safe, int border)
399 {
400         struct view_data *vd;
401         struct pane *p;
402
403         p = pane_register(par, 0, &view_handle.c);
404         if (!p)
405                 return p;
406         vd = p->data;
407         vd->border = border;
408         vd->old_border = border;
409         vd->line_height = -1;
410         vd->border_width = vd->border_height = -1;
411         /* Capture status-changed notification so we can update 'changed' flag in
412          * status line */
413         call("doc:request:doc:status-changed", p);
414         call("doc:request:doc:replaced", p);
415         call("doc:request:mark:moving", p);
416         /* And update display-context */
417         call("Window:request:display-context", p);
418         return p;
419 }
420
421 static int calc_border(struct pane *p safe)
422 {
423         int borders = 0;
424         char *borderstr = pane_attr_get(p, "borders");
425         if (!borderstr)
426                 borderstr = "";
427         if (strchr(borderstr, 'T')) borders |= BORDER_TOP;
428         if (strchr(borderstr, 'B')) borders |= BORDER_BOT;
429         if (strchr(borderstr, 'L')) borders |= BORDER_LEFT;
430         if (strchr(borderstr, 'R')) borders |= BORDER_RIGHT;
431         if (strchr(borderstr, 's')) borders |= BORDER_STATUS;
432         return borders;
433 }
434
435 DEF_CMD(view_attach)
436 {
437         int borders = calc_border(ci->focus);
438         struct pane *p = do_view_attach(ci->focus, borders);
439
440         if (!p)
441                 return Efail;
442         return comm_call(ci->comm2, "callback:attach", p);
443 }
444
445 DEF_CMD(view_click)
446 {
447         struct pane *p = ci->home;
448         struct view_data *vd = p->data;
449         struct pane *c = vd->child;
450         int mid = vd->scroll_bar_y;
451         int lh = vd->line_height;
452         int num;
453         int scale;
454         struct xy cih;
455
456         cih = pane_mapxy(ci->focus, ci->home, ci->x, ci->y, False);
457
458         if (ci->focus != p)
459                 /* Event was in the child */
460                 return Efallthrough;
461         if (!c)
462                 return 1;
463         /* Ignore if not in scroll-bar, which it to left of child */
464         if (cih.y < c->y ||             // above child
465             cih.y >= c->y + c->h ||     // below child
466             cih.x >= c->x)              // Not to right of child
467                 return 1;
468
469         if (p->h <= 4)
470                 /* scroll bar too small to be useful */
471                 return 1;
472
473         scale = 100; /* 10% for small movements */
474         num = RPT_NUM(ci);
475
476         if (cih.y < mid - lh) {
477                 /* big scroll up */
478                 num = -num;
479                 scale = 900;
480         } else if (cih.y <= mid) {
481                 /* scroll up */
482                 num = -num;
483         } else if (cih.y <= mid + lh) {
484                 /* scroll down */
485         } else {
486                 /* big scroll down */
487                 scale = 900;
488         }
489         call("Move-View", pane_focus(ci->focus), num * scale);
490         return 1;
491 }
492
493 DEF_CMD(view_release)
494 {
495         /* Make sure release doesn't go to parent if not in child */
496
497         if (ci->focus != ci->home)
498                 /* Event was in the child */
499                 return Efallthrough;
500         return 1;
501 }
502
503 DEF_CMD(view_scroll)
504 {
505         if (strcmp(ci->key, "M:Press-4") == 0)
506                 call("Move-View", pane_focus(ci->focus), -200);
507         else
508                 call("Move-View", pane_focus(ci->focus), 200);
509         return 1;
510 }
511
512 DEF_CMD(view_refresh_view)
513 {
514         struct pane *p = ci->home;
515         struct view_data *vd = p->data;
516         int border;
517
518         border = calc_border(ci->focus);
519         if (vd->border >= 0 && border != vd->border) {
520                 vd->border = border;
521                 pane_damaged(p, DAMAGED_SIZE);
522         }
523         return 1;
524 }
525
526 DEF_CMD(view_clip)
527 {
528         struct view_data *vd = ci->home->data;
529
530         if (vd->viewpoint)
531                 mark_clip(vd->viewpoint, ci->mark, ci->mark2, !!ci->num);
532         return Efallthrough;
533 }
534
535 DEF_CMD(view_border)
536 {
537         struct pane *p = ci->home;
538         struct view_data *vd = p->data;
539
540         if (ci->num <= 0)
541                 vd->border = -1;
542         else
543                 vd->border = vd->old_border;
544
545         pane_damaged(p, DAMAGED_SIZE);
546         return Efallthrough; /* allow other handlers to change borders */
547 }
548
549 void edlib_init(struct pane *ed safe)
550 {
551         view_map = key_alloc();
552
553         key_add(view_map, "M:Click-1", &view_click);
554         key_add(view_map, "M:Press-1", &view_click);
555         key_add(view_map, "M:Release-1", &view_release);
556         key_add(view_map, "M:DPress-1", &view_click);
557         key_add(view_map, "M:TPress-1", &view_click);
558         key_add(view_map, "M:Press-4", &view_scroll);
559         key_add(view_map, "M:Press-5", &view_scroll);
560         key_add(view_map, "Tile:border", &view_border);
561         key_add(view_map, "Refresh:view", &view_refresh_view);
562         key_add(view_map, "Close", &view_close);
563         key_add(view_map, "Clone", &view_clone);
564         key_add(view_map, "Child-Notify", &view_child_notify);
565         key_add(view_map, "Refresh:size", &view_refresh_size);
566         key_add(view_map, "Refresh", &view_refresh);
567         key_add(view_map, "doc:status-changed", &view_status_changed);
568         key_add(view_map, "doc:replaced", &view_status_changed);
569         key_add(view_map, "mark:moving", &view_status_changed);
570         key_add(view_map, "view:changed", &view_status_changed);
571         key_add(view_map, "display-context", &view_status_changed);
572         key_add(view_map, "render:reposition", &view_reposition);
573         key_add(view_map, "Notify:clip", &view_clip);
574
575         call_comm("global-set-command", ed, &view_attach, 0, NULL, "attach-view");
576 }