mirror of
https://github.com/neovim/neovim.git
synced 2025-09-17 08:48:16 +00:00
507 lines
15 KiB
C
507 lines
15 KiB
C
// This is an open source non-commercial project. Dear PVS-Studio, please check
|
|
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
|
|
|
|
// Compositor: merge floating grids with the main grid for display in
|
|
// TUI and non-multigrid UIs.
|
|
//
|
|
// Layer-based compositing: https://en.wikipedia.org/wiki/Digital_compositing
|
|
|
|
#include <assert.h>
|
|
#include <stdbool.h>
|
|
#include <stdio.h>
|
|
#include <limits.h>
|
|
|
|
#include "nvim/lib/kvec.h"
|
|
#include "nvim/log.h"
|
|
#include "nvim/main.h"
|
|
#include "nvim/ascii.h"
|
|
#include "nvim/vim.h"
|
|
#include "nvim/ui.h"
|
|
#include "nvim/highlight.h"
|
|
#include "nvim/memory.h"
|
|
#include "nvim/popupmnu.h"
|
|
#include "nvim/ui_compositor.h"
|
|
#include "nvim/ugrid.h"
|
|
#include "nvim/screen.h"
|
|
#include "nvim/syntax.h"
|
|
#include "nvim/api/private/helpers.h"
|
|
#include "nvim/os/os.h"
|
|
|
|
#ifdef INCLUDE_GENERATED_DECLARATIONS
|
|
# include "ui_compositor.c.generated.h"
|
|
#endif
|
|
|
|
static UI *compositor = NULL;
|
|
static int composed_uis = 0;
|
|
kvec_t(ScreenGrid *) layers = KV_INITIAL_VALUE;
|
|
|
|
static size_t bufsize = 0;
|
|
static schar_T *linebuf;
|
|
static sattr_T *attrbuf;
|
|
|
|
#ifndef NDEBUG
|
|
static int chk_width = 0, chk_height = 0;
|
|
#endif
|
|
|
|
static ScreenGrid *curgrid;
|
|
|
|
static bool valid_screen = true;
|
|
static bool msg_scroll_mode = false;
|
|
static int msg_first_invalid = 0;
|
|
|
|
void ui_comp_init(void)
|
|
{
|
|
if (compositor != NULL) {
|
|
return;
|
|
}
|
|
compositor = xcalloc(1, sizeof(UI));
|
|
|
|
compositor->rgb = true;
|
|
compositor->grid_resize = ui_comp_grid_resize;
|
|
compositor->grid_scroll = ui_comp_grid_scroll;
|
|
compositor->grid_cursor_goto = ui_comp_grid_cursor_goto;
|
|
compositor->raw_line = ui_comp_raw_line;
|
|
compositor->win_scroll_over_start = ui_comp_win_scroll_over_start;
|
|
compositor->win_scroll_over_reset = ui_comp_win_scroll_over_reset;
|
|
|
|
// Be unopinionated: will be attached together with a "real" ui anyway
|
|
compositor->width = INT_MAX;
|
|
compositor->height = INT_MAX;
|
|
for (UIExtension i = 0; (int)i < kUIExtCount; i++) {
|
|
compositor->ui_ext[i] = true;
|
|
}
|
|
|
|
// TODO(bfredl): this will be more complicated if we implement
|
|
// hlstate per UI (i e reduce hl ids for non-hlstate UIs)
|
|
compositor->ui_ext[kUIHlState] = false;
|
|
|
|
kv_push(layers, &default_grid);
|
|
curgrid = &default_grid;
|
|
|
|
ui_attach_impl(compositor);
|
|
}
|
|
|
|
|
|
void ui_comp_attach(UI *ui)
|
|
{
|
|
composed_uis++;
|
|
ui->composed = true;
|
|
}
|
|
|
|
void ui_comp_detach(UI *ui)
|
|
{
|
|
composed_uis--;
|
|
if (composed_uis == 0) {
|
|
XFREE_CLEAR(linebuf);
|
|
XFREE_CLEAR(attrbuf);
|
|
bufsize = 0;
|
|
}
|
|
ui->composed = false;
|
|
}
|
|
|
|
bool ui_comp_should_draw(void)
|
|
{
|
|
return composed_uis != 0 && valid_screen;
|
|
}
|
|
|
|
/// Places `grid` at (col,row) position with (width * height) size.
|
|
/// Adds `grid` as the top layer if it is a new layer.
|
|
///
|
|
/// TODO(bfredl): later on the compositor should just use win_float_pos events,
|
|
/// though that will require slight event order adjustment: emit the win_pos
|
|
/// events in the beginning of update_screen(0), rather than in ui_flush()
|
|
bool ui_comp_put_grid(ScreenGrid *grid, int row, int col, int height, int width,
|
|
bool valid, bool on_top)
|
|
{
|
|
bool moved;
|
|
if (grid->comp_index != 0) {
|
|
moved = (row != grid->comp_row) || (col != grid->comp_col);
|
|
if (ui_comp_should_draw()) {
|
|
// Redraw the area covered by the old position, and is not covered
|
|
// by the new position. Disable the grid so that compose_area() will not
|
|
// use it.
|
|
grid->comp_disabled = true;
|
|
compose_area(grid->comp_row, row,
|
|
grid->comp_col, grid->comp_col + grid->Columns);
|
|
if (grid->comp_col < col) {
|
|
compose_area(MAX(row, grid->comp_row),
|
|
MIN(row+height, grid->comp_row+grid->Rows),
|
|
grid->comp_col, col);
|
|
}
|
|
if (col+width < grid->comp_col+grid->Columns) {
|
|
compose_area(MAX(row, grid->comp_row),
|
|
MIN(row+height, grid->comp_row+grid->Rows),
|
|
col+width, grid->comp_col+grid->Columns);
|
|
}
|
|
compose_area(row+height, grid->comp_row+grid->Rows,
|
|
grid->comp_col, grid->comp_col + grid->Columns);
|
|
grid->comp_disabled = false;
|
|
}
|
|
grid->comp_row = row;
|
|
grid->comp_col = col;
|
|
} else {
|
|
moved = true;
|
|
#ifndef NDEBUG
|
|
for (size_t i = 0; i < kv_size(layers); i++) {
|
|
if (kv_A(layers, i) == grid) {
|
|
assert(false);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
size_t insert_at = kv_size(layers);
|
|
if (kv_A(layers, insert_at-1) == &pum_grid) {
|
|
insert_at--;
|
|
}
|
|
if (insert_at > 1 && !on_top) {
|
|
insert_at--;
|
|
}
|
|
// not found: new grid
|
|
kv_push(layers, grid);
|
|
if (insert_at < kv_size(layers)-1) {
|
|
for (size_t i = kv_size(layers)-1; i > insert_at; i--) {
|
|
kv_A(layers, i) = kv_A(layers, i-1);
|
|
kv_A(layers, i)->comp_index = i;
|
|
}
|
|
kv_A(layers, insert_at) = grid;
|
|
}
|
|
|
|
grid->comp_row = row;
|
|
grid->comp_col = col;
|
|
grid->comp_index = insert_at;
|
|
}
|
|
if (moved && valid && ui_comp_should_draw()) {
|
|
compose_area(grid->comp_row, grid->comp_row+grid->Rows,
|
|
grid->comp_col, grid->comp_col+grid->Columns);
|
|
}
|
|
return moved;
|
|
}
|
|
|
|
void ui_comp_remove_grid(ScreenGrid *grid)
|
|
{
|
|
assert(grid != &default_grid);
|
|
if (grid->comp_index == 0) {
|
|
// grid wasn't present
|
|
return;
|
|
}
|
|
|
|
if (curgrid == grid) {
|
|
curgrid = &default_grid;
|
|
}
|
|
|
|
for (size_t i = grid->comp_index; i < kv_size(layers)-1; i++) {
|
|
kv_A(layers, i) = kv_A(layers, i+1);
|
|
kv_A(layers, i)->comp_index = i;
|
|
}
|
|
(void)kv_pop(layers);
|
|
grid->comp_index = 0;
|
|
|
|
// recompose the area under the grid
|
|
// inefficent when being overlapped: only draw up to grid->comp_index
|
|
ui_comp_compose_grid(grid);
|
|
}
|
|
|
|
bool ui_comp_set_grid(handle_T handle)
|
|
{
|
|
if (curgrid->handle == handle) {
|
|
return true;
|
|
}
|
|
ScreenGrid *grid = NULL;
|
|
for (size_t i = 0; i < kv_size(layers); i++) {
|
|
if (kv_A(layers, i)->handle == handle) {
|
|
grid = kv_A(layers, i);
|
|
break;
|
|
}
|
|
}
|
|
if (grid != NULL) {
|
|
curgrid = grid;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void ui_comp_raise_grid(ScreenGrid *grid, size_t new_index)
|
|
{
|
|
size_t old_index = grid->comp_index;
|
|
for (size_t i = old_index; i < new_index; i++) {
|
|
kv_A(layers, i) = kv_A(layers, i+1);
|
|
kv_A(layers, i)->comp_index = i;
|
|
}
|
|
kv_A(layers, new_index) = grid;
|
|
grid->comp_index = new_index;
|
|
for (size_t i = old_index; i < new_index; i++) {
|
|
ScreenGrid *grid2 = kv_A(layers, i);
|
|
int startcol = MAX(grid->comp_col, grid2->comp_col);
|
|
int endcol = MIN(grid->comp_col+grid->Columns,
|
|
grid2->comp_col+grid2->Columns);
|
|
compose_area(MAX(grid->comp_row, grid2->comp_row),
|
|
MIN(grid->comp_row+grid->Rows, grid2->comp_row+grid2->Rows),
|
|
startcol, endcol);
|
|
}
|
|
}
|
|
|
|
static void ui_comp_grid_cursor_goto(UI *ui, Integer grid_handle,
|
|
Integer r, Integer c)
|
|
{
|
|
if (!ui_comp_should_draw() || !ui_comp_set_grid((int)grid_handle)) {
|
|
return;
|
|
}
|
|
int cursor_row = curgrid->comp_row+(int)r;
|
|
int cursor_col = curgrid->comp_col+(int)c;
|
|
|
|
// TODO(bfredl): maybe not the best time to do this, for efficiency we
|
|
// should configure all grids before entering win_update()
|
|
if (curgrid != &default_grid) {
|
|
size_t new_index = kv_size(layers)-1;
|
|
if (kv_A(layers, new_index) == &pum_grid) {
|
|
new_index--;
|
|
}
|
|
if (curgrid->comp_index < new_index) {
|
|
ui_comp_raise_grid(curgrid, new_index);
|
|
}
|
|
}
|
|
|
|
if (cursor_col >= default_grid.Columns || cursor_row >= default_grid.Rows) {
|
|
// TODO(bfredl): this happens with 'writedelay', refactor?
|
|
// abort();
|
|
return;
|
|
}
|
|
ui_composed_call_grid_cursor_goto(1, cursor_row, cursor_col);
|
|
}
|
|
|
|
ScreenGrid *ui_comp_mouse_focus(int row, int col)
|
|
{
|
|
// TODO(bfredl): click "through" unfocusable grids?
|
|
for (ssize_t i = (ssize_t)kv_size(layers)-1; i > 0; i--) {
|
|
ScreenGrid *grid = kv_A(layers, i);
|
|
if (row >= grid->comp_row && row < grid->comp_row+grid->Rows
|
|
&& col >= grid->comp_col && col < grid->comp_col+grid->Columns) {
|
|
return grid;
|
|
}
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/// Baseline implementation. This is always correct, but we can sometimes
|
|
/// do something more efficient (where efficiency means smaller deltas to
|
|
/// the downstream UI.)
|
|
static void compose_line(Integer row, Integer startcol, Integer endcol,
|
|
LineFlags flags)
|
|
{
|
|
// in case we start on the right half of a double-width char, we need to
|
|
// check the left half. But skip it in output if it wasn't doublewidth.
|
|
int skip = 0;
|
|
if (startcol > 0 && (flags & kLineFlagInvalid)) {
|
|
startcol--;
|
|
skip = 1;
|
|
}
|
|
|
|
int col = (int)startcol;
|
|
ScreenGrid *grid = NULL;
|
|
schar_T *bg_line = &default_grid.chars[default_grid.line_offset[row]
|
|
+(size_t)startcol];
|
|
sattr_T *bg_attrs = &default_grid.attrs[default_grid.line_offset[row]
|
|
+(size_t)startcol];
|
|
|
|
while (col < endcol) {
|
|
int until = 0;
|
|
for (size_t i = 0; i < kv_size(layers); i++) {
|
|
ScreenGrid *g = kv_A(layers, i);
|
|
if (g->comp_row > row || row >= g->comp_row + g->Rows
|
|
|| g->comp_disabled) {
|
|
continue;
|
|
}
|
|
if (g->comp_col <= col && col < g->comp_col+g->Columns) {
|
|
grid = g;
|
|
until = g->comp_col+g->Columns;
|
|
} else if (g->comp_col > col) {
|
|
until = MIN(until, g->comp_col);
|
|
}
|
|
}
|
|
until = MIN(until, (int)endcol);
|
|
|
|
assert(grid != NULL);
|
|
assert(until > col);
|
|
assert(until <= default_grid.Columns);
|
|
size_t n = (size_t)(until-col);
|
|
size_t off = grid->line_offset[row-grid->comp_row]
|
|
+ (size_t)(col-grid->comp_col);
|
|
memcpy(linebuf+(col-startcol), grid->chars+off, n * sizeof(*linebuf));
|
|
memcpy(attrbuf+(col-startcol), grid->attrs+off, n * sizeof(*attrbuf));
|
|
|
|
// 'pumblend' and 'winblend'
|
|
if (grid->blending) {
|
|
for (int i = col-(int)startcol; i < until-startcol; i++) {
|
|
bool thru = strequal((char *)linebuf[i], " "); // negative space
|
|
attrbuf[i] = (sattr_T)hl_blend_attrs(bg_attrs[i], attrbuf[i], &thru);
|
|
if (thru) {
|
|
memcpy(linebuf[i], bg_line[i], sizeof(linebuf[i]));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Tricky: if overlap caused a doublewidth char to get cut-off, must
|
|
// replace the visible half with a space.
|
|
if (linebuf[col-startcol][0] == NUL) {
|
|
linebuf[col-startcol][0] = ' ';
|
|
linebuf[col-startcol][1] = NUL;
|
|
} else if (n > 1 && linebuf[col-startcol+1][0] == NUL) {
|
|
skip = 0;
|
|
}
|
|
if (grid->comp_col+grid->Columns > until
|
|
&& grid->chars[off+n][0] == NUL) {
|
|
linebuf[until-1-startcol][0] = ' ';
|
|
linebuf[until-1-startcol][1] = '\0';
|
|
if (col == startcol && n == 1) {
|
|
skip = 0;
|
|
}
|
|
}
|
|
|
|
col = until;
|
|
}
|
|
assert(endcol <= chk_width);
|
|
assert(row < chk_height);
|
|
|
|
if (!(grid && grid == &default_grid)) {
|
|
// TODO(bfredl): too conservative, need check
|
|
// grid->line_wraps if grid->Width == Width
|
|
flags = flags & ~kLineFlagWrap;
|
|
}
|
|
|
|
ui_composed_call_raw_line(1, row, startcol+skip, endcol, endcol, 0, flags,
|
|
(const schar_T *)linebuf+skip,
|
|
(const sattr_T *)attrbuf+skip);
|
|
}
|
|
|
|
static void compose_area(Integer startrow, Integer endrow,
|
|
Integer startcol, Integer endcol)
|
|
{
|
|
endrow = MIN(endrow, default_grid.Rows);
|
|
endcol = MIN(endcol, default_grid.Columns);
|
|
if (endcol <= startcol) {
|
|
return;
|
|
}
|
|
for (int r = (int)startrow; r < endrow; r++) {
|
|
compose_line(r, startcol, endcol, kLineFlagInvalid);
|
|
}
|
|
}
|
|
|
|
/// compose the area under the grid.
|
|
///
|
|
/// This is needed when some option affecting composition is changed,
|
|
/// such as 'pumblend' for popupmenu grid.
|
|
void ui_comp_compose_grid(ScreenGrid *grid)
|
|
{
|
|
if (ui_comp_should_draw()) {
|
|
compose_area(grid->comp_row, grid->comp_row+grid->Rows,
|
|
grid->comp_col, grid->comp_col+grid->Columns);
|
|
}
|
|
}
|
|
|
|
static void ui_comp_raw_line(UI *ui, Integer grid, Integer row,
|
|
Integer startcol, Integer endcol,
|
|
Integer clearcol, Integer clearattr,
|
|
LineFlags flags, const schar_T *chunk,
|
|
const sattr_T *attrs)
|
|
{
|
|
if (!ui_comp_should_draw() || !ui_comp_set_grid((int)grid)) {
|
|
return;
|
|
}
|
|
|
|
row += curgrid->comp_row;
|
|
startcol += curgrid->comp_col;
|
|
endcol += curgrid->comp_col;
|
|
clearcol += curgrid->comp_col;
|
|
if (curgrid != &default_grid) {
|
|
flags = flags & ~kLineFlagWrap;
|
|
}
|
|
assert(row < default_grid.Rows);
|
|
assert(clearcol <= default_grid.Columns);
|
|
if (flags & kLineFlagInvalid
|
|
|| kv_size(layers) > curgrid->comp_index+1
|
|
|| curgrid->blending) {
|
|
compose_line(row, startcol, clearcol, flags);
|
|
} else {
|
|
ui_composed_call_raw_line(1, row, startcol, endcol, clearcol, clearattr,
|
|
flags, chunk, attrs);
|
|
}
|
|
}
|
|
|
|
/// The screen is invalid and will soon be cleared
|
|
///
|
|
/// Don't redraw floats until screen is cleared
|
|
void ui_comp_set_screen_valid(bool valid)
|
|
{
|
|
valid_screen = valid;
|
|
}
|
|
|
|
// TODO(bfredl): These events are somewhat of a hack. multiline messages
|
|
// should later on be a separate grid, then this would just be ordinary
|
|
// ui_comp_put_grid and ui_comp_remove_grid calls.
|
|
static void ui_comp_win_scroll_over_start(UI *ui)
|
|
{
|
|
msg_scroll_mode = true;
|
|
msg_first_invalid = ui->height;
|
|
}
|
|
|
|
static void ui_comp_win_scroll_over_reset(UI *ui)
|
|
{
|
|
msg_scroll_mode = false;
|
|
for (size_t i = 1; i < kv_size(layers); i++) {
|
|
ScreenGrid *grid = kv_A(layers, i);
|
|
if (grid->comp_row+grid->Rows > msg_first_invalid) {
|
|
compose_area(msg_first_invalid, grid->comp_row+grid->Rows,
|
|
grid->comp_col, grid->comp_col+grid->Columns);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ui_comp_grid_scroll(UI *ui, Integer grid, Integer top,
|
|
Integer bot, Integer left, Integer right,
|
|
Integer rows, Integer cols)
|
|
{
|
|
if (!ui_comp_should_draw() || !ui_comp_set_grid((int)grid)) {
|
|
return;
|
|
}
|
|
top += curgrid->comp_row;
|
|
bot += curgrid->comp_row;
|
|
left += curgrid->comp_col;
|
|
right += curgrid->comp_col;
|
|
bool covered = kv_size(layers) > curgrid->comp_index+1 || curgrid->blending;
|
|
if (!msg_scroll_mode && covered) {
|
|
// TODO(bfredl):
|
|
// 1. check if rectangles actually overlap
|
|
// 2. calulate subareas that can scroll.
|
|
if (rows > 0) {
|
|
bot -= rows;
|
|
} else {
|
|
top += (-rows);
|
|
}
|
|
compose_area(top, bot, left, right);
|
|
} else {
|
|
msg_first_invalid = MIN(msg_first_invalid, (int)top);
|
|
ui_composed_call_grid_scroll(1, top, bot, left, right, rows, cols);
|
|
}
|
|
}
|
|
|
|
static void ui_comp_grid_resize(UI *ui, Integer grid,
|
|
Integer width, Integer height)
|
|
{
|
|
if (grid == 1) {
|
|
ui_composed_call_grid_resize(1, width, height);
|
|
#ifndef NDEBUG
|
|
chk_width = (int)width;
|
|
chk_height = (int)height;
|
|
#endif
|
|
size_t new_bufsize = (size_t)width;
|
|
if (bufsize != new_bufsize) {
|
|
xfree(linebuf);
|
|
xfree(attrbuf);
|
|
linebuf = xmalloc(new_bufsize * sizeof(*linebuf));
|
|
attrbuf = xmalloc(new_bufsize * sizeof(*attrbuf));
|
|
bufsize = new_bufsize;
|
|
}
|
|
}
|
|
}
|
|
|