1// This is an open source non-commercial project. Dear PVS-Studio, please check
2// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
3
4// Compositor: merge floating grids with the main grid for display in
5// TUI and non-multigrid UIs.
6//
7// Layer-based compositing: https://en.wikipedia.org/wiki/Digital_compositing
8
9#include <assert.h>
10#include <stdbool.h>
11#include <stdio.h>
12#include <limits.h>
13
14#include "nvim/lib/kvec.h"
15#include "nvim/log.h"
16#include "nvim/main.h"
17#include "nvim/ascii.h"
18#include "nvim/vim.h"
19#include "nvim/ui.h"
20#include "nvim/highlight.h"
21#include "nvim/memory.h"
22#include "nvim/message.h"
23#include "nvim/popupmnu.h"
24#include "nvim/ui_compositor.h"
25#include "nvim/ugrid.h"
26#include "nvim/screen.h"
27#include "nvim/syntax.h"
28#include "nvim/api/private/helpers.h"
29#include "nvim/os/os.h"
30
31#ifdef INCLUDE_GENERATED_DECLARATIONS
32# include "ui_compositor.c.generated.h"
33#endif
34
35static UI *compositor = NULL;
36static int composed_uis = 0;
37kvec_t(ScreenGrid *) layers = KV_INITIAL_VALUE;
38
39static size_t bufsize = 0;
40static schar_T *linebuf;
41static sattr_T *attrbuf;
42
43#ifndef NDEBUG
44static int chk_width = 0, chk_height = 0;
45#endif
46
47static ScreenGrid *curgrid;
48
49static bool valid_screen = true;
50static int msg_current_row = INT_MAX;
51static bool msg_was_scrolled = false;
52
53static int msg_sep_row = -1;
54static schar_T msg_sep_char = { ' ', NUL };
55
56static int dbghl_normal, dbghl_clear, dbghl_composed, dbghl_recompose;
57
58void ui_comp_init(void)
59{
60 if (compositor != NULL) {
61 return;
62 }
63 compositor = xcalloc(1, sizeof(UI));
64
65 compositor->rgb = true;
66 compositor->grid_resize = ui_comp_grid_resize;
67 compositor->grid_scroll = ui_comp_grid_scroll;
68 compositor->grid_cursor_goto = ui_comp_grid_cursor_goto;
69 compositor->raw_line = ui_comp_raw_line;
70 compositor->msg_set_pos = ui_comp_msg_set_pos;
71
72 // Be unopinionated: will be attached together with a "real" ui anyway
73 compositor->width = INT_MAX;
74 compositor->height = INT_MAX;
75 for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
76 compositor->ui_ext[i] = true;
77 }
78
79 // TODO(bfredl): this will be more complicated if we implement
80 // hlstate per UI (i e reduce hl ids for non-hlstate UIs)
81 compositor->ui_ext[kUIHlState] = false;
82
83 kv_push(layers, &default_grid);
84 curgrid = &default_grid;
85
86 ui_attach_impl(compositor, 0);
87}
88
89void ui_comp_syn_init(void)
90{
91 dbghl_normal = syn_check_group((char_u *)S_LEN("RedrawDebugNormal"));
92 dbghl_clear = syn_check_group((char_u *)S_LEN("RedrawDebugClear"));
93 dbghl_composed = syn_check_group((char_u *)S_LEN("RedrawDebugComposed"));
94 dbghl_recompose = syn_check_group((char_u *)S_LEN("RedrawDebugRecompose"));
95}
96
97void ui_comp_attach(UI *ui)
98{
99 composed_uis++;
100 ui->composed = true;
101}
102
103void ui_comp_detach(UI *ui)
104{
105 composed_uis--;
106 if (composed_uis == 0) {
107 XFREE_CLEAR(linebuf);
108 XFREE_CLEAR(attrbuf);
109 bufsize = 0;
110 }
111 ui->composed = false;
112}
113
114bool ui_comp_should_draw(void)
115{
116 return composed_uis != 0 && valid_screen;
117}
118
119/// Places `grid` at (col,row) position with (width * height) size.
120/// Adds `grid` as the top layer if it is a new layer.
121///
122/// TODO(bfredl): later on the compositor should just use win_float_pos events,
123/// though that will require slight event order adjustment: emit the win_pos
124/// events in the beginning of update_screen(0), rather than in ui_flush()
125bool ui_comp_put_grid(ScreenGrid *grid, int row, int col, int height, int width,
126 bool valid, bool on_top)
127{
128 bool moved;
129 if (grid->comp_index != 0) {
130 moved = (row != grid->comp_row) || (col != grid->comp_col);
131 if (ui_comp_should_draw()) {
132 // Redraw the area covered by the old position, and is not covered
133 // by the new position. Disable the grid so that compose_area() will not
134 // use it.
135 grid->comp_disabled = true;
136 compose_area(grid->comp_row, row,
137 grid->comp_col, grid->comp_col + grid->Columns);
138 if (grid->comp_col < col) {
139 compose_area(MAX(row, grid->comp_row),
140 MIN(row+height, grid->comp_row+grid->Rows),
141 grid->comp_col, col);
142 }
143 if (col+width < grid->comp_col+grid->Columns) {
144 compose_area(MAX(row, grid->comp_row),
145 MIN(row+height, grid->comp_row+grid->Rows),
146 col+width, grid->comp_col+grid->Columns);
147 }
148 compose_area(row+height, grid->comp_row+grid->Rows,
149 grid->comp_col, grid->comp_col + grid->Columns);
150 grid->comp_disabled = false;
151 }
152 grid->comp_row = row;
153 grid->comp_col = col;
154 } else {
155 moved = true;
156#ifndef NDEBUG
157 for (size_t i = 0; i < kv_size(layers); i++) {
158 if (kv_A(layers, i) == grid) {
159 assert(false);
160 }
161 }
162#endif
163
164 // TODO(bfredl): this is pretty ad-hoc, add a proper z-order/priority
165 // scheme. For now:
166 // - msg_grid is always on top.
167 // - pum_grid is on top of all windows but not msg_grid. Except for when
168 // wildoptions=pum, and completing the cmdline with scrolled messages,
169 // then the pum has to be drawn over the scrolled messages.
170 size_t insert_at = kv_size(layers);
171 bool cmd_completion = (grid == &pum_grid && (State & CMDLINE)
172 && (wop_flags & WOP_PUM));
173 if (kv_A(layers, insert_at-1) == &msg_grid && !cmd_completion) {
174 insert_at--;
175 }
176 if (kv_A(layers, insert_at-1) == &pum_grid && (grid != &msg_grid)) {
177 insert_at--;
178 }
179 if (insert_at > 1 && !on_top) {
180 insert_at--;
181 }
182 // not found: new grid
183 kv_push(layers, grid);
184 if (insert_at < kv_size(layers)-1) {
185 for (size_t i = kv_size(layers)-1; i > insert_at; i--) {
186 kv_A(layers, i) = kv_A(layers, i-1);
187 kv_A(layers, i)->comp_index = i;
188 }
189 kv_A(layers, insert_at) = grid;
190 }
191
192 grid->comp_row = row;
193 grid->comp_col = col;
194 grid->comp_index = insert_at;
195 }
196 if (moved && valid && ui_comp_should_draw()) {
197 compose_area(grid->comp_row, grid->comp_row+grid->Rows,
198 grid->comp_col, grid->comp_col+grid->Columns);
199 }
200 return moved;
201}
202
203void ui_comp_remove_grid(ScreenGrid *grid)
204{
205 assert(grid != &default_grid);
206 if (grid->comp_index == 0) {
207 // grid wasn't present
208 return;
209 }
210
211 if (curgrid == grid) {
212 curgrid = &default_grid;
213 }
214
215 for (size_t i = grid->comp_index; i < kv_size(layers)-1; i++) {
216 kv_A(layers, i) = kv_A(layers, i+1);
217 kv_A(layers, i)->comp_index = i;
218 }
219 (void)kv_pop(layers);
220 grid->comp_index = 0;
221
222 // recompose the area under the grid
223 // inefficent when being overlapped: only draw up to grid->comp_index
224 ui_comp_compose_grid(grid);
225}
226
227bool ui_comp_set_grid(handle_T handle)
228{
229 if (curgrid->handle == handle) {
230 return true;
231 }
232 ScreenGrid *grid = NULL;
233 for (size_t i = 0; i < kv_size(layers); i++) {
234 if (kv_A(layers, i)->handle == handle) {
235 grid = kv_A(layers, i);
236 break;
237 }
238 }
239 if (grid != NULL) {
240 curgrid = grid;
241 return true;
242 }
243 return false;
244}
245
246static void ui_comp_raise_grid(ScreenGrid *grid, size_t new_index)
247{
248 size_t old_index = grid->comp_index;
249 for (size_t i = old_index; i < new_index; i++) {
250 kv_A(layers, i) = kv_A(layers, i+1);
251 kv_A(layers, i)->comp_index = i;
252 }
253 kv_A(layers, new_index) = grid;
254 grid->comp_index = new_index;
255 for (size_t i = old_index; i < new_index; i++) {
256 ScreenGrid *grid2 = kv_A(layers, i);
257 int startcol = MAX(grid->comp_col, grid2->comp_col);
258 int endcol = MIN(grid->comp_col+grid->Columns,
259 grid2->comp_col+grid2->Columns);
260 compose_area(MAX(grid->comp_row, grid2->comp_row),
261 MIN(grid->comp_row+grid->Rows, grid2->comp_row+grid2->Rows),
262 startcol, endcol);
263 }
264}
265
266static void ui_comp_grid_cursor_goto(UI *ui, Integer grid_handle,
267 Integer r, Integer c)
268{
269 if (!ui_comp_should_draw() || !ui_comp_set_grid((int)grid_handle)) {
270 return;
271 }
272 int cursor_row = curgrid->comp_row+(int)r;
273 int cursor_col = curgrid->comp_col+(int)c;
274
275 // TODO(bfredl): maybe not the best time to do this, for efficiency we
276 // should configure all grids before entering win_update()
277 if (curgrid != &default_grid) {
278 size_t new_index = kv_size(layers)-1;
279 if (kv_A(layers, new_index) == &pum_grid) {
280 new_index--;
281 }
282 if (curgrid->comp_index < new_index) {
283 ui_comp_raise_grid(curgrid, new_index);
284 }
285 }
286
287 if (cursor_col >= default_grid.Columns || cursor_row >= default_grid.Rows) {
288 // TODO(bfredl): this happens with 'writedelay', refactor?
289 // abort();
290 return;
291 }
292 ui_composed_call_grid_cursor_goto(1, cursor_row, cursor_col);
293}
294
295ScreenGrid *ui_comp_mouse_focus(int row, int col)
296{
297 for (ssize_t i = (ssize_t)kv_size(layers)-1; i > 0; i--) {
298 ScreenGrid *grid = kv_A(layers, i);
299 if (grid->focusable
300 && row >= grid->comp_row && row < grid->comp_row+grid->Rows
301 && col >= grid->comp_col && col < grid->comp_col+grid->Columns) {
302 return grid;
303 }
304 }
305 return NULL;
306}
307
308/// Baseline implementation. This is always correct, but we can sometimes
309/// do something more efficient (where efficiency means smaller deltas to
310/// the downstream UI.)
311static void compose_line(Integer row, Integer startcol, Integer endcol,
312 LineFlags flags)
313{
314 // in case we start on the right half of a double-width char, we need to
315 // check the left half. But skip it in output if it wasn't doublewidth.
316 int skipstart = 0, skipend = 0;
317 if (startcol > 0 && (flags & kLineFlagInvalid)) {
318 startcol--;
319 skipstart = 1;
320 }
321 if (endcol < default_grid.Columns && (flags & kLineFlagInvalid)) {
322 endcol++;
323 skipend = 1;
324 }
325
326 int col = (int)startcol;
327 ScreenGrid *grid = NULL;
328 schar_T *bg_line = &default_grid.chars[default_grid.line_offset[row]
329 +(size_t)startcol];
330 sattr_T *bg_attrs = &default_grid.attrs[default_grid.line_offset[row]
331 +(size_t)startcol];
332
333 while (col < endcol) {
334 int until = 0;
335 for (size_t i = 0; i < kv_size(layers); i++) {
336 ScreenGrid *g = kv_A(layers, i);
337 if (g->comp_row > row || row >= g->comp_row + g->Rows
338 || g->comp_disabled) {
339 continue;
340 }
341 if (g->comp_col <= col && col < g->comp_col+g->Columns) {
342 grid = g;
343 until = g->comp_col+g->Columns;
344 } else if (g->comp_col > col) {
345 until = MIN(until, g->comp_col);
346 }
347 }
348 until = MIN(until, (int)endcol);
349
350 assert(grid != NULL);
351 assert(until > col);
352 assert(until <= default_grid.Columns);
353 size_t n = (size_t)(until-col);
354
355 if (row == msg_sep_row && grid->comp_index <= msg_grid.comp_index) {
356 // TODO(bfredl): when we implement borders around floating windows, then
357 // msgsep can just be a border "around" the message grid.
358 grid = &msg_grid;
359 sattr_T msg_sep_attr = (sattr_T)HL_ATTR(HLF_MSGSEP);
360 for (int i = col; i < until; i++) {
361 memcpy(linebuf[i-startcol], msg_sep_char, sizeof(*linebuf));
362 attrbuf[i-startcol] = msg_sep_attr;
363 }
364 } else {
365 size_t off = grid->line_offset[row-grid->comp_row]
366 + (size_t)(col-grid->comp_col);
367 memcpy(linebuf+(col-startcol), grid->chars+off, n * sizeof(*linebuf));
368 memcpy(attrbuf+(col-startcol), grid->attrs+off, n * sizeof(*attrbuf));
369 if (grid->comp_col+grid->Columns > until
370 && grid->chars[off+n][0] == NUL) {
371 linebuf[until-1-startcol][0] = ' ';
372 linebuf[until-1-startcol][1] = '\0';
373 if (col == startcol && n == 1) {
374 skipstart = 0;
375 }
376 }
377 }
378
379 // 'pumblend' and 'winblend'
380 if (grid->blending) {
381 int width;
382 for (int i = col-(int)startcol; i < until-startcol; i += width) {
383 width = 1;
384 // negative space
385 bool thru = strequal((char *)linebuf[i], " ") && bg_line[i][0] != NUL;
386 if (i+1 < endcol-startcol && bg_line[i+1][0] == NUL) {
387 width = 2;
388 thru &= strequal((char *)linebuf[i+1], " ");
389 }
390 attrbuf[i] = (sattr_T)hl_blend_attrs(bg_attrs[i], attrbuf[i], &thru);
391 if (width == 2) {
392 attrbuf[i+1] = (sattr_T)hl_blend_attrs(bg_attrs[i+1],
393 attrbuf[i+1], &thru);
394 }
395 if (thru) {
396 memcpy(linebuf + i, bg_line + i, (size_t)width * sizeof(linebuf[i]));
397 }
398 }
399 }
400
401 // Tricky: if overlap caused a doublewidth char to get cut-off, must
402 // replace the visible half with a space.
403 if (linebuf[col-startcol][0] == NUL) {
404 linebuf[col-startcol][0] = ' ';
405 linebuf[col-startcol][1] = NUL;
406 if (col == endcol-1) {
407 skipend = 0;
408 }
409 } else if (n > 1 && linebuf[col-startcol+1][0] == NUL) {
410 skipstart = 0;
411 }
412
413 col = until;
414 }
415 if (linebuf[endcol-startcol-1][0] == NUL) {
416 skipend = 0;
417 }
418
419 assert(endcol <= chk_width);
420 assert(row < chk_height);
421
422 if (!(grid && grid == &default_grid)) {
423 // TODO(bfredl): too conservative, need check
424 // grid->line_wraps if grid->Width == Width
425 flags = flags & ~kLineFlagWrap;
426 }
427
428 ui_composed_call_raw_line(1, row, startcol+skipstart,
429 endcol-skipend, endcol-skipend, 0, flags,
430 (const schar_T *)linebuf+skipstart,
431 (const sattr_T *)attrbuf+skipstart);
432}
433
434static void compose_debug(Integer startrow, Integer endrow, Integer startcol,
435 Integer endcol, int syn_id, bool delay)
436{
437 if (!(rdb_flags & RDB_COMPOSITOR)) {
438 return;
439 }
440
441 endrow = MIN(endrow, default_grid.Rows);
442 endcol = MIN(endcol, default_grid.Columns);
443 int attr = syn_id2attr(syn_id);
444
445 for (int row = (int)startrow; row < endrow; row++) {
446 ui_composed_call_raw_line(1, row, startcol, startcol, endcol, attr, false,
447 (const schar_T *)linebuf,
448 (const sattr_T *)attrbuf);
449 }
450
451
452 if (delay) {
453 debug_delay(endrow-startrow);
454 }
455}
456
457static void debug_delay(Integer lines)
458{
459 ui_call_flush();
460 uint64_t wd = (uint64_t)labs(p_wd);
461 uint64_t factor = (uint64_t)MAX(MIN(lines, 5), 1);
462 os_microdelay(factor * wd * 1000u, true);
463}
464
465
466static void compose_area(Integer startrow, Integer endrow,
467 Integer startcol, Integer endcol)
468{
469 compose_debug(startrow, endrow, startcol, endcol, dbghl_recompose, true);
470 endrow = MIN(endrow, default_grid.Rows);
471 endcol = MIN(endcol, default_grid.Columns);
472 if (endcol <= startcol) {
473 return;
474 }
475 for (int r = (int)startrow; r < endrow; r++) {
476 compose_line(r, startcol, endcol, kLineFlagInvalid);
477 }
478}
479
480/// compose the area under the grid.
481///
482/// This is needed when some option affecting composition is changed,
483/// such as 'pumblend' for popupmenu grid.
484void ui_comp_compose_grid(ScreenGrid *grid)
485{
486 if (ui_comp_should_draw()) {
487 compose_area(grid->comp_row, grid->comp_row+grid->Rows,
488 grid->comp_col, grid->comp_col+grid->Columns);
489 }
490}
491
492static void ui_comp_raw_line(UI *ui, Integer grid, Integer row,
493 Integer startcol, Integer endcol,
494 Integer clearcol, Integer clearattr,
495 LineFlags flags, const schar_T *chunk,
496 const sattr_T *attrs)
497{
498 if (!ui_comp_should_draw() || !ui_comp_set_grid((int)grid)) {
499 return;
500 }
501
502 row += curgrid->comp_row;
503 startcol += curgrid->comp_col;
504 endcol += curgrid->comp_col;
505 clearcol += curgrid->comp_col;
506 if (curgrid != &default_grid) {
507 flags = flags & ~kLineFlagWrap;
508 }
509
510 assert(endcol <= clearcol);
511
512 // TODO(bfredl): this should not really be necessary. But on some condition
513 // when resizing nvim, a window will be attempted to be drawn on the older
514 // and possibly larger global screen size.
515 if (row >= default_grid.Rows) {
516 DLOG("compositor: invalid row %"PRId64" on grid %"PRId64, row, grid);
517 return;
518 }
519 if (clearcol > default_grid.Columns) {
520 DLOG("compositor: invalid last column %"PRId64" on grid %"PRId64,
521 clearcol, grid);
522 if (startcol >= default_grid.Columns) {
523 return;
524 }
525 clearcol = default_grid.Columns;
526 endcol = MIN(endcol, clearcol);
527 }
528
529 bool above_msg = (kv_A(layers, kv_size(layers)-1) == &msg_grid
530 && row < msg_current_row-(msg_was_scrolled?1:0));
531 bool covered = kv_size(layers)-(above_msg?1:0) > curgrid->comp_index+1;
532 // TODO(bfredl): eventually should just fix compose_line to respect clearing
533 // and optimize it for uncovered lines.
534 if (flags & kLineFlagInvalid || covered || curgrid->blending) {
535 compose_debug(row, row+1, startcol, clearcol, dbghl_composed, true);
536 compose_line(row, startcol, clearcol, flags);
537 } else {
538 compose_debug(row, row+1, startcol, endcol, dbghl_normal, false);
539 compose_debug(row, row+1, endcol, clearcol, dbghl_clear, true);
540 ui_composed_call_raw_line(1, row, startcol, endcol, clearcol, clearattr,
541 flags, chunk, attrs);
542 }
543}
544
545/// The screen is invalid and will soon be cleared
546///
547/// Don't redraw floats until screen is cleared
548void ui_comp_set_screen_valid(bool valid)
549{
550 valid_screen = valid;
551 if (!valid) {
552 msg_sep_row = -1;
553 }
554}
555
556static void ui_comp_msg_set_pos(UI *ui, Integer grid, Integer row,
557 Boolean scrolled, String sep_char)
558{
559 msg_grid.comp_row = (int)row;
560 if (scrolled && row > 0) {
561 msg_sep_row = (int)row-1;
562 if (sep_char.data) {
563 STRLCPY(msg_sep_char, sep_char.data, sizeof(msg_sep_char));
564 }
565 } else {
566 msg_sep_row = -1;
567 }
568
569 if (row > msg_current_row && ui_comp_should_draw()) {
570 compose_area(MAX(msg_current_row-1, 0), row, 0, default_grid.Columns);
571 } else if (row < msg_current_row && ui_comp_should_draw()
572 && msg_current_row < Rows) {
573 int delta = msg_current_row - (int)row;
574 if (msg_grid.blending) {
575 int first_row = MAX((int)row-(scrolled?1:0), 0);
576 compose_area(first_row, Rows-delta, 0, Columns);
577 } else {
578 // scroll separator togheter with message text
579 int first_row = MAX((int)row-(msg_was_scrolled?1:0), 0);
580 ui_composed_call_grid_scroll(1, first_row, Rows, 0, Columns, delta, 0);
581 if (scrolled && !msg_was_scrolled && row > 0) {
582 compose_area(row-1, row, 0, Columns);
583 }
584 }
585 }
586
587 msg_current_row = (int)row;
588 msg_was_scrolled = scrolled;
589}
590
591static void ui_comp_grid_scroll(UI *ui, Integer grid, Integer top,
592 Integer bot, Integer left, Integer right,
593 Integer rows, Integer cols)
594{
595 if (!ui_comp_should_draw() || !ui_comp_set_grid((int)grid)) {
596 return;
597 }
598 top += curgrid->comp_row;
599 bot += curgrid->comp_row;
600 left += curgrid->comp_col;
601 right += curgrid->comp_col;
602 bool covered = kv_size(layers) > curgrid->comp_index+1 || curgrid->blending;
603
604 if (covered) {
605 // TODO(bfredl):
606 // 1. check if rectangles actually overlap
607 // 2. calulate subareas that can scroll.
608 if (rows > 0) {
609 bot -= rows;
610 } else {
611 top += (-rows);
612 }
613 compose_area(top, bot, left, right);
614 } else {
615 ui_composed_call_grid_scroll(1, top, bot, left, right, rows, cols);
616 if (rdb_flags & RDB_COMPOSITOR) {
617 debug_delay(2);
618 }
619 }
620}
621
622static void ui_comp_grid_resize(UI *ui, Integer grid,
623 Integer width, Integer height)
624{
625 if (grid == 1) {
626 ui_composed_call_grid_resize(1, width, height);
627#ifndef NDEBUG
628 chk_width = (int)width;
629 chk_height = (int)height;
630#endif
631 size_t new_bufsize = (size_t)width;
632 if (bufsize != new_bufsize) {
633 xfree(linebuf);
634 xfree(attrbuf);
635 linebuf = xmalloc(new_bufsize * sizeof(*linebuf));
636 attrbuf = xmalloc(new_bufsize * sizeof(*attrbuf));
637 bufsize = new_bufsize;
638 }
639 }
640}
641
642