tui: Move screen state tracking to new "ugrid" module

The ugrid module implements a unicode "drawing" grid and is used to store TUI
screen state. Later this module will be reused in other layers.
This commit is contained in:
Thiago de Arruda 2015-08-26 09:07:04 -03:00
parent cb9ae4e373
commit f5c5cdb306
3 changed files with 243 additions and 140 deletions

View File

@ -21,6 +21,7 @@
#include "nvim/os/input.h"
#include "nvim/os/os.h"
#include "nvim/strings.h"
#include "nvim/ugrid.h"
// Space reserved in the output buffer to restore the cursor to normal when
// flushing. No existing terminal will require 32 bytes to do that.
@ -31,11 +32,6 @@ typedef struct {
int top, bot, left, right;
} Rect;
typedef struct {
char data[7];
HlAttrs attrs;
} Cell;
typedef struct {
unibi_var_t params[9];
char buf[OUTBUF_SIZE];
@ -45,17 +41,13 @@ typedef struct {
unibi_term *ut;
uv_tty_t output_handle;
SignalWatcher winch_handle;
Rect scroll_region;
UGrid grid;
kvec_t(Rect) invalid_regions;
int row, col;
int bg, fg;
int out_fd;
int old_height;
bool can_use_terminal_scroll;
bool mouse_enabled;
bool busy;
HlAttrs attrs, print_attrs;
Cell **screen;
HlAttrs print_attrs;
int showing_mode;
struct {
int enable_mouse, disable_mouse;
@ -71,32 +63,14 @@ static bool volatile got_winch = false;
# include "tui/tui.c.generated.h"
#endif
#define EMPTY_ATTRS ((HlAttrs){false, false, false, false, false, -1, -1})
#define FOREACH_CELL(ui, top, bot, left, right, go, code) \
do { \
TUIData *data = ui->data; \
for (int row = top; row <= bot; ++row) { \
Cell *cells = data->screen[row]; \
if (go) { \
unibi_goto(ui, row, left); \
} \
for (int col = left; col <= right; ++col) { \
Cell *cell = cells + col; \
(void)(cell); \
code; \
} \
} \
} while (0)
UI *tui_start(void)
{
TUIData *data = xcalloc(1, sizeof(TUIData));
UI *ui = xcalloc(1, sizeof(UI));
ui->data = data;
data->attrs = data->print_attrs = EMPTY_ATTRS;
data->fg = data->bg = -1;
data->print_attrs = EMPTY_ATTRS;
ugrid_init(&data->grid);
data->can_use_terminal_scroll = true;
data->bufpos = 0;
data->bufsize = sizeof(data->buf) - CNORM_COMMAND_MAX_SIZE;
@ -201,7 +175,7 @@ static void tui_stop(UI *ui)
}
xfree(data->write_loop);
unibi_destroy(data->ut);
destroy_screen(data);
ugrid_free(&data->grid);
xfree(data);
ui_detach(ui);
xfree(ui);
@ -233,9 +207,10 @@ static void update_attrs(UI *ui, HlAttrs attrs)
data->print_attrs = attrs;
unibi_out(ui, unibi_exit_attribute_mode);
UGrid *grid = &data->grid;
int fg = attrs.foreground != -1 ? attrs.foreground : data->fg;
int bg = attrs.background != -1 ? attrs.background : data->bg;
int fg = attrs.foreground != -1 ? attrs.foreground : grid->fg;
int bg = attrs.background != -1 ? attrs.background : grid->bg;
if (ui->rgb) {
if (fg != -1) {
@ -277,25 +252,25 @@ static void update_attrs(UI *ui, HlAttrs attrs)
}
}
static void print_cell(UI *ui, Cell *ptr)
static void print_cell(UI *ui, UCell *ptr)
{
update_attrs(ui, ptr->attrs);
out(ui, ptr->data, strlen(ptr->data));
}
static void clear_region(UI *ui, int top, int bot, int left, int right,
bool refresh)
static void clear_region(UI *ui, int top, int bot, int left, int right)
{
TUIData *data = ui->data;
HlAttrs clear_attrs = EMPTY_ATTRS;
clear_attrs.foreground = data->fg;
clear_attrs.background = data->bg;
update_attrs(ui, clear_attrs);
UGrid *grid = &data->grid;
bool cleared = false;
if (refresh && data->bg == -1 && right == ui->width -1) {
if (grid->bg == -1 && right == ui->width -1) {
// Background is set to the default color and the right edge matches the
// screen end, try to use terminal codes for clearing the requested area.
HlAttrs clear_attrs = EMPTY_ATTRS;
clear_attrs.foreground = grid->fg;
clear_attrs.background = grid->bg;
update_attrs(ui, clear_attrs);
if (left == 0) {
if (bot == ui->height - 1) {
if (top == 0) {
@ -318,36 +293,26 @@ static void clear_region(UI *ui, int top, int bot, int left, int right,
}
}
bool clear = refresh && !cleared;
FOREACH_CELL(ui, top, bot, left, right, clear, {
cell->data[0] = ' ';
cell->data[1] = 0;
cell->attrs = clear_attrs;
if (clear) {
if (!cleared) {
// could not clear using faster terminal codes, refresh the whole region
int currow = -1;
UGRID_FOREACH_CELL(grid, top, bot, left, right, {
if (currow != row) {
unibi_goto(ui, row, col);
currow = row;
}
print_cell(ui, cell);
}
});
});
}
// restore cursor
unibi_goto(ui, data->row, data->col);
unibi_goto(ui, grid->row, grid->col);
}
static void tui_resize(UI *ui, int width, int height)
{
TUIData *data = ui->data;
destroy_screen(data);
data->screen = xmalloc((size_t)height * sizeof(Cell *));
for (int i = 0; i < height; i++) {
data->screen[i] = xcalloc((size_t)width, sizeof(Cell));
}
data->old_height = height;
data->scroll_region.top = 0;
data->scroll_region.bot = height - 1;
data->scroll_region.left = 0;
data->scroll_region.right = width - 1;
data->row = data->col = 0;
ugrid_resize(&data->grid, width, height);
if (!got_winch) { // Try to resize the terminal window.
char r[16]; // enough for 9999x9999
@ -361,22 +326,23 @@ static void tui_resize(UI *ui, int width, int height)
static void tui_clear(UI *ui)
{
TUIData *data = ui->data;
clear_region(ui, data->scroll_region.top, data->scroll_region.bot,
data->scroll_region.left, data->scroll_region.right, true);
UGrid *grid = &data->grid;
ugrid_clear(grid);
clear_region(ui, grid->top, grid->bot, grid->left, grid->right);
}
static void tui_eol_clear(UI *ui)
{
TUIData *data = ui->data;
clear_region(ui, data->row, data->row, data->col,
data->scroll_region.right, true);
UGrid *grid = &data->grid;
ugrid_eol_clear(grid);
clear_region(ui, grid->row, grid->row, grid->col, grid->right);
}
static void tui_cursor_goto(UI *ui, int row, int col)
{
TUIData *data = ui->data;
data->row = row;
data->col = col;
ugrid_goto(&data->grid, row, col);
unibi_goto(ui, row, col);
}
@ -434,11 +400,7 @@ static void tui_set_scroll_region(UI *ui, int top, int bot, int left,
int right)
{
TUIData *data = ui->data;
data->scroll_region.top = top;
data->scroll_region.bot = bot;
data->scroll_region.left = left;
data->scroll_region.right = right;
ugrid_set_scroll_region(&data->grid, top, bot, left, right);
data->can_use_terminal_scroll =
left == 0 && right == ui->width - 1
&& ((top == 0 && bot == ui->height - 1)
@ -448,31 +410,24 @@ static void tui_set_scroll_region(UI *ui, int top, int bot, int left,
static void tui_scroll(UI *ui, int count)
{
TUIData *data = ui->data;
int top = data->scroll_region.top;
int bot = data->scroll_region.bot;
int left = data->scroll_region.left;
int right = data->scroll_region.right;
UGrid *grid = &data->grid;
int clear_top, clear_bot;
ugrid_scroll(grid, count, &clear_top, &clear_bot);
if (data->can_use_terminal_scroll) {
// Change terminal scroll region and move cursor to the top
data->params[0].i = top;
data->params[1].i = bot;
data->params[0].i = grid->top;
data->params[1].i = grid->bot;
unibi_out(ui, unibi_change_scroll_region);
unibi_goto(ui, top, left);
unibi_goto(ui, grid->top, grid->left);
// also set default color attributes or some terminals can become funny
HlAttrs clear_attrs = EMPTY_ATTRS;
clear_attrs.foreground = data->fg;
clear_attrs.background = data->bg;
clear_attrs.foreground = grid->fg;
clear_attrs.background = grid->bg;
update_attrs(ui, clear_attrs);
}
// Compute start/stop/step for the loop below, also use terminal scroll
// if possible
int start, stop, step;
if (count > 0) {
start = top;
stop = bot - count + 1;
step = 1;
if (data->can_use_terminal_scroll) {
if (count == 1) {
unibi_out(ui, unibi_delete_line);
@ -483,9 +438,6 @@ static void tui_scroll(UI *ui, int count)
}
} else {
start = bot;
stop = top - count - 1;
step = -1;
if (data->can_use_terminal_scroll) {
if (count == -1) {
unibi_out(ui, unibi_insert_line);
@ -501,52 +453,30 @@ static void tui_scroll(UI *ui, int count)
data->params[0].i = 0;
data->params[1].i = ui->height - 1;
unibi_out(ui, unibi_change_scroll_region);
unibi_goto(ui, data->row, data->col);
}
unibi_goto(ui, grid->row, grid->col);
int i;
// Scroll internal screen
for (i = start; i != stop; i += step) {
Cell *target_row = data->screen[i] + left;
Cell *source_row = data->screen[i + count] + left;
memcpy(target_row, source_row, sizeof(Cell) * (size_t)(right - left + 1));
}
// clear emptied region, updating the terminal if its builtin scrolling
// facility was used. This is done when the background color is not the
// default, since scrolling may leave wrong background in the cleared area.
bool update_clear = data->bg != -1 && data->can_use_terminal_scroll;
if (count > 0) {
clear_region(ui, stop, stop + count - 1, left, right, update_clear);
if (grid->bg != -1) {
// Update the cleared area of the terminal if its builtin scrolling
// facility was used and the background color is not the default. This is
// required because scrolling may leave wrong background in the cleared
// area.
clear_region(ui, clear_top, clear_bot, grid->left, grid->right);
}
} else {
clear_region(ui, stop + count + 1, stop, left, right, update_clear);
}
if (!data->can_use_terminal_scroll) {
// Mark the entire scroll region as invalid for redrawing later
invalidate(ui, data->scroll_region.top, data->scroll_region.bot,
data->scroll_region.left, data->scroll_region.right);
invalidate(ui, grid->top, grid->bot, grid->left, grid->right);
}
}
static void tui_highlight_set(UI *ui, HlAttrs attrs)
{
((TUIData *)ui->data)->attrs = attrs;
((TUIData *)ui->data)->grid.attrs = attrs;
}
static void tui_put(UI *ui, uint8_t *text, size_t size)
{
TUIData *data = ui->data;
Cell *cell = data->screen[data->row] + data->col;
cell->data[size] = 0;
cell->attrs = data->attrs;
if (text) {
memcpy(cell->data, text, size);
}
print_cell(ui, cell);
data->col += 1;
print_cell(ui, ugrid_put(&data->grid, text, size));
}
static void tui_bell(UI *ui)
@ -561,26 +491,32 @@ static void tui_visual_bell(UI *ui)
static void tui_update_fg(UI *ui, int fg)
{
((TUIData *)ui->data)->fg = fg;
((TUIData *)ui->data)->grid.fg = fg;
}
static void tui_update_bg(UI *ui, int bg)
{
((TUIData *)ui->data)->bg = bg;
((TUIData *)ui->data)->grid.bg = bg;
}
static void tui_flush(UI *ui)
{
TUIData *data = ui->data;
UGrid *grid = &data->grid;
while (kv_size(data->invalid_regions)) {
Rect r = kv_pop(data->invalid_regions);
FOREACH_CELL(ui, r.top, r.bot, r.left, r.right, true, {
int currow = -1;
UGRID_FOREACH_CELL(grid, r.top, r.bot, r.left, r.right, {
if (currow != row) {
unibi_goto(ui, row, col);
currow = row;
}
print_cell(ui, cell);
});
}
unibi_goto(ui, data->row, data->col);
unibi_goto(ui, grid->row, grid->col);
flush_buf(ui);
}
@ -890,13 +826,3 @@ static void flush_buf(UI *ui)
unibi_out(ui, unibi_cursor_invisible);
}
}
static void destroy_screen(TUIData *data)
{
if (data->screen) {
for (int i = 0; i < data->old_height; i++) {
xfree(data->screen[i]);
}
xfree(data->screen);
}
}

137
src/nvim/ugrid.c Normal file
View File

@ -0,0 +1,137 @@
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <limits.h>
#include "nvim/vim.h"
#include "nvim/ui.h"
#include "nvim/ugrid.h"
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ugrid.c.generated.h"
#endif
void ugrid_init(UGrid *grid)
{
grid->attrs = EMPTY_ATTRS;
grid->fg = grid->bg = -1;
grid->cells = NULL;
}
void ugrid_free(UGrid *grid)
{
destroy_cells(grid);
}
void ugrid_resize(UGrid *grid, int width, int height)
{
destroy_cells(grid);
grid->cells = xmalloc((size_t)height * sizeof(UCell *));
for (int i = 0; i < height; i++) {
grid->cells[i] = xcalloc((size_t)width, sizeof(UCell));
}
grid->top = 0;
grid->bot = height - 1;
grid->left = 0;
grid->right = width - 1;
grid->row = grid->col = 0;
grid->width = width;
grid->height = height;
}
void ugrid_clear(UGrid *grid)
{
clear_region(grid, grid->top, grid->bot, grid->left, grid->right);
}
void ugrid_eol_clear(UGrid *grid)
{
clear_region(grid, grid->row, grid->row, grid->col, grid->right);
}
void ugrid_goto(UGrid *grid, int row, int col)
{
grid->row = row;
grid->col = col;
}
void ugrid_set_scroll_region(UGrid *grid, int top, int bot, int left, int right)
{
grid->top = top;
grid->bot = bot;
grid->left = left;
grid->right = right;
}
void ugrid_scroll(UGrid *grid, int count, int *clear_top, int *clear_bot)
{
// Compute start/stop/step for the loop below
int start, stop, step;
if (count > 0) {
start = grid->top;
stop = grid->bot - count + 1;
step = 1;
} else {
start = grid->bot;
stop = grid->top - count - 1;
step = -1;
}
int i;
// Copy cell data
for (i = start; i != stop; i += step) {
UCell *target_row = grid->cells[i] + grid->left;
UCell *source_row = grid->cells[i + count] + grid->left;
memcpy(target_row, source_row,
sizeof(UCell) * (size_t)(grid->right - grid->left + 1));
}
// clear cells in the emptied region,
if (count > 0) {
*clear_top = stop;
*clear_bot = stop + count - 1;
} else {
*clear_bot = stop;
*clear_top = stop + count + 1;
}
clear_region(grid, *clear_top, *clear_bot, grid->left, grid->right);
}
UCell *ugrid_put(UGrid *grid, uint8_t *text, size_t size)
{
UCell *cell = grid->cells[grid->row] + grid->col;
cell->data[size] = 0;
cell->attrs = grid->attrs;
if (text) {
memcpy(cell->data, text, size);
}
grid->col += 1;
return cell;
}
static void clear_region(UGrid *grid, int top, int bot, int left, int right)
{
HlAttrs clear_attrs = EMPTY_ATTRS;
clear_attrs.foreground = grid->fg;
clear_attrs.background = grid->bg;
UGRID_FOREACH_CELL(grid, top, bot, left, right, {
cell->data[0] = ' ';
cell->data[1] = 0;
cell->attrs = clear_attrs;
});
}
static void destroy_cells(UGrid *grid)
{
if (grid->cells) {
for (int i = 0; i < grid->height; i++) {
xfree(grid->cells[i]);
}
xfree(grid->cells);
}
}

40
src/nvim/ugrid.h Normal file
View File

@ -0,0 +1,40 @@
#ifndef NVIM_UGRID_H
#define NVIM_UGRID_H
#include "nvim/ui.h"
typedef struct ucell UCell;
typedef struct ugrid UGrid;
struct ucell {
char data[7];
HlAttrs attrs;
};
struct ugrid {
int top, bot, left, right;
int row, col;
int bg, fg;
int width, height;
HlAttrs attrs;
UCell **cells;
};
#define EMPTY_ATTRS ((HlAttrs){false, false, false, false, false, -1, -1})
#define UGRID_FOREACH_CELL(grid, top, bot, left, right, code) \
do { \
for (int row = top; row <= bot; ++row) { \
UCell *row_cells = (grid)->cells[row]; \
for (int col = left; col <= right; ++col) { \
UCell *cell = row_cells + col; \
(void)(cell); \
code; \
} \
} \
} while (0)
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "ugrid.h.generated.h"
#endif
#endif // NVIM_UGRID_H