mirror of
https://codeberg.org/dnkl/foot.git
synced 2026-02-05 04:06:08 -05:00
955 lines
30 KiB
C
955 lines
30 KiB
C
#include "render.h"
|
|
|
|
#include <string.h>
|
|
|
|
#include <sys/ioctl.h>
|
|
#include <sys/time.h>
|
|
#include <sys/timerfd.h>
|
|
#include <sys/prctl.h>
|
|
|
|
#include <wayland-cursor.h>
|
|
#include <xdg-shell.h>
|
|
|
|
#include <fcft/fcft.h>
|
|
|
|
#define LOG_MODULE "render"
|
|
#define LOG_ENABLE_DBG 0
|
|
#include "log.h"
|
|
#include "shm.h"
|
|
#include "grid.h"
|
|
|
|
#define min(x, y) ((x) < (y) ? (x) : (y))
|
|
#define max(x, y) ((x) > (y) ? (x) : (y))
|
|
|
|
struct font *
|
|
attrs_to_font(const struct terminal *term, const struct attributes *attrs)
|
|
{
|
|
int idx = attrs->italic << 1 | attrs->bold;
|
|
return term->fonts[idx];
|
|
}
|
|
|
|
static inline struct rgb
|
|
color_hex_to_rgb(uint32_t color)
|
|
{
|
|
return (struct rgb){
|
|
((color >> 16) & 0xff) / 255.,
|
|
((color >> 8) & 0xff) / 255.,
|
|
((color >> 0) & 0xff) / 255.,
|
|
};
|
|
}
|
|
|
|
static inline pixman_color_t
|
|
color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha)
|
|
{
|
|
int alpha_div = 0xffff / alpha;
|
|
return (pixman_color_t){
|
|
.red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)) / alpha_div,
|
|
.green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)) / alpha_div,
|
|
.blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)) / alpha_div,
|
|
.alpha = alpha,
|
|
};
|
|
}
|
|
|
|
static inline pixman_color_t
|
|
color_hex_to_pixman(uint32_t color)
|
|
{
|
|
/* Count on the compiler optimizing this */
|
|
return color_hex_to_pixman_with_alpha(color, 0xffff);
|
|
}
|
|
|
|
static inline void
|
|
color_dim(struct rgb *rgb)
|
|
{
|
|
rgb->r /= 2.;
|
|
rgb->g /= 2.;
|
|
rgb->b /= 2.;
|
|
}
|
|
|
|
static inline void
|
|
pixman_color_dim(pixman_color_t *color)
|
|
{
|
|
color->red /= 2;
|
|
color->green /= 2;
|
|
color->blue /= 2;
|
|
}
|
|
|
|
static inline void
|
|
pixman_color_dim_for_search(pixman_color_t *color)
|
|
{
|
|
color->red /= 3;
|
|
color->green /= 3;
|
|
color->blue /= 3;
|
|
}
|
|
|
|
static inline int
|
|
font_baseline(const struct terminal *term)
|
|
{
|
|
return term->fonts[0]->ascent;
|
|
}
|
|
|
|
static void
|
|
draw_unfocused_block(const struct terminal *term, pixman_image_t *pix,
|
|
const pixman_color_t *color, int x, int y, int cell_cols)
|
|
{
|
|
pixman_image_fill_rectangles(
|
|
PIXMAN_OP_SRC, pix, color, 4,
|
|
(pixman_rectangle16_t []){
|
|
{x, y, cell_cols * term->cell_width, 1}, /* top */
|
|
{x, y, 1, term->cell_height}, /* left */
|
|
{x + cell_cols * term->cell_width - 1, y, 1, term->cell_height}, /* right */
|
|
{x, y + term->cell_height - 1, cell_cols * term->cell_width, 1}, /* bottom */
|
|
});
|
|
}
|
|
|
|
static void
|
|
draw_bar(const struct terminal *term, pixman_image_t *pix,
|
|
const struct font *font,
|
|
const pixman_color_t *color, int x, int y)
|
|
{
|
|
int baseline = y + font_baseline(term) - term->fonts[0]->ascent;
|
|
pixman_image_fill_rectangles(
|
|
PIXMAN_OP_SRC, pix, color,
|
|
1, &(pixman_rectangle16_t){
|
|
x, baseline,
|
|
font->underline.thickness, term->fonts[0]->ascent + term->fonts[0]->descent});
|
|
}
|
|
|
|
static void
|
|
draw_underline(const struct terminal *term, pixman_image_t *pix,
|
|
const struct font *font,
|
|
const pixman_color_t *color, int x, int y, int cols)
|
|
{
|
|
pixman_image_fill_rectangles(
|
|
PIXMAN_OP_SRC, pix, color,
|
|
1, &(pixman_rectangle16_t){
|
|
x, y + font_baseline(term) - font->underline.position,
|
|
cols * term->cell_width, font->underline.thickness});
|
|
}
|
|
|
|
static void
|
|
draw_strikeout(const struct terminal *term, pixman_image_t *pix,
|
|
const struct font *font,
|
|
const pixman_color_t *color, int x, int y, int cols)
|
|
{
|
|
pixman_image_fill_rectangles(
|
|
PIXMAN_OP_SRC, pix, color,
|
|
1, &(pixman_rectangle16_t){
|
|
x, y + font_baseline(term) - font->strikeout.position,
|
|
cols * term->cell_width, font->strikeout.thickness});
|
|
}
|
|
|
|
static bool
|
|
coord_is_selected(const struct terminal *term, int col, int row)
|
|
{
|
|
if (term->selection.start.col == -1 || term->selection.end.col == -1)
|
|
return false;
|
|
|
|
const struct coord *start = &term->selection.start;
|
|
const struct coord *end = &term->selection.end;
|
|
|
|
if (start->row > end->row || (start->row == end->row && start->col > end->col)) {
|
|
const struct coord *tmp = start;
|
|
start = end;
|
|
end = tmp;
|
|
}
|
|
|
|
assert(start->row <= end->row);
|
|
|
|
row += term->grid->view;
|
|
|
|
if (start->row == end->row) {
|
|
return row == start->row && col >= start->col && col <= end->col;
|
|
} else {
|
|
if (row == start->row)
|
|
return col >= start->col;
|
|
else if (row == end->row)
|
|
return col <= end->col;
|
|
else
|
|
return row >= start->row && row <= end->row;
|
|
}
|
|
}
|
|
|
|
static int
|
|
render_cell(struct terminal *term, pixman_image_t *pix,
|
|
struct cell *cell, int col, int row, bool has_cursor)
|
|
{
|
|
if (cell->attrs.clean)
|
|
return 0;
|
|
|
|
cell->attrs.clean = 1;
|
|
|
|
int width = term->cell_width;
|
|
int height = term->cell_height;
|
|
int x = term->x_margin + col * width;
|
|
int y = term->y_margin + row * height;
|
|
|
|
bool have_focus = term->wl->focused == term;
|
|
bool is_selected = coord_is_selected(term, col, row);
|
|
|
|
bool block_cursor =
|
|
has_cursor &&
|
|
have_focus &&
|
|
term->cursor_style == CURSOR_BLOCK &&
|
|
term->cursor_blink.state == CURSOR_BLINK_ON;
|
|
|
|
uint32_t _fg = 0;
|
|
uint32_t _bg = 0;
|
|
|
|
if (block_cursor && term->cursor_color.text >> 31) {
|
|
/* User configured cursor color overrides all attributes */
|
|
assert(term->cursor_color.cursor >> 31);
|
|
|
|
/* Slightly ugly... reverse here, since they'll be reversed again below */
|
|
_fg = term->cursor_color.cursor;
|
|
_bg = term->cursor_color.text;
|
|
} else {
|
|
/* Use cell specific color, if set, otherwise the default colors (possible reversed) */
|
|
_fg = cell->attrs.have_fg
|
|
? cell->attrs.fg
|
|
: !term->reverse ? term->colors.fg : term->colors.bg;
|
|
_bg = cell->attrs.have_bg
|
|
? cell->attrs.bg
|
|
: !term->reverse ? term->colors.bg : term->colors.fg;
|
|
}
|
|
|
|
/* If *one* is set, we reverse */
|
|
if (block_cursor ^ cell->attrs.reverse ^ is_selected) {
|
|
uint32_t swap = _fg;
|
|
_fg = _bg;
|
|
_bg = swap;
|
|
}
|
|
|
|
if (cell->attrs.blink && term->blink.state == BLINK_OFF)
|
|
_fg = _bg;
|
|
|
|
pixman_color_t fg = color_hex_to_pixman(_fg);
|
|
pixman_color_t bg = color_hex_to_pixman_with_alpha(
|
|
_bg, block_cursor ? 0xffff : term->colors.alpha);
|
|
|
|
if (cell->attrs.dim)
|
|
pixman_color_dim(&fg);
|
|
|
|
if (term->is_searching && !is_selected) {
|
|
pixman_color_dim_for_search(&fg);
|
|
pixman_color_dim_for_search(&bg);
|
|
}
|
|
|
|
struct font *font = attrs_to_font(term, &cell->attrs);
|
|
const struct glyph *glyph = font_glyph_for_wc(font, cell->wc);
|
|
|
|
int cell_cols = glyph != NULL ? max(1, glyph->cols) : 1;
|
|
|
|
/* Background */
|
|
pixman_image_fill_rectangles(
|
|
PIXMAN_OP_SRC, pix, &bg, 1,
|
|
&(pixman_rectangle16_t){x, y, cell_cols * width, height});
|
|
|
|
/* Non-block cursors (including the "unfocused" cursor) */
|
|
if (has_cursor &&
|
|
!block_cursor &&
|
|
(term->cursor_blink.state == CURSOR_BLINK_ON || !have_focus))
|
|
{
|
|
pixman_color_t cursor_color;
|
|
if (term->cursor_color.text >> 31) {
|
|
cursor_color = color_hex_to_pixman(
|
|
is_selected ? term->cursor_color.text : term->cursor_color.cursor);
|
|
|
|
if (term->is_searching)
|
|
pixman_color_dim(&cursor_color);
|
|
} else
|
|
cursor_color = fg;
|
|
|
|
switch (term->cursor_style) {
|
|
case CURSOR_BLOCK:
|
|
if (!have_focus)
|
|
draw_unfocused_block(term, pix, &cursor_color, x, y, cell_cols);
|
|
break;
|
|
|
|
case CURSOR_BAR:
|
|
draw_bar(term, pix, font, &cursor_color, x, y);
|
|
break;
|
|
|
|
case CURSOR_UNDERLINE:
|
|
draw_underline(
|
|
term, pix, attrs_to_font(term, &cell->attrs), &cursor_color,
|
|
x, y, cell_cols);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cell->attrs.blink)
|
|
term_arm_blink_timer(term);
|
|
|
|
if (cell->wc == 0 || cell->attrs.conceal)
|
|
return cell_cols;
|
|
|
|
if (glyph != NULL) {
|
|
if (unlikely(pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8)) {
|
|
/* Glyph surface is a pre-rendered image (typically a color emoji...) */
|
|
if (!(cell->attrs.blink && term->blink.state == BLINK_OFF)) {
|
|
pixman_image_composite32(
|
|
PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0,
|
|
x + glyph->x, y + font_baseline(term) - glyph->y,
|
|
glyph->width, glyph->height);
|
|
}
|
|
} else {
|
|
/* Glyph surface is an alpha mask */
|
|
pixman_image_t *src = pixman_image_create_solid_fill(&fg);
|
|
pixman_image_composite32(
|
|
PIXMAN_OP_OVER, src, glyph->pix, pix, 0, 0, 0, 0,
|
|
x + glyph->x, y + font_baseline(term) - glyph->y,
|
|
glyph->width, glyph->height);
|
|
pixman_image_unref(src);
|
|
}
|
|
}
|
|
|
|
/* Underline */
|
|
if (cell->attrs.underline) {
|
|
draw_underline(term, pix, attrs_to_font(term, &cell->attrs),
|
|
&fg, x, y, cell_cols);
|
|
}
|
|
|
|
if (cell->attrs.strikethrough) {
|
|
draw_strikeout(term, pix, attrs_to_font(term, &cell->attrs),
|
|
&fg, x, y, cell_cols);
|
|
}
|
|
|
|
return cell_cols;
|
|
}
|
|
|
|
static void
|
|
grid_render_scroll(struct terminal *term, struct buffer *buf,
|
|
const struct damage *dmg)
|
|
{
|
|
int dst_y = term->y_margin + (dmg->scroll.region.start + 0) * term->cell_height;
|
|
int src_y = term->y_margin + (dmg->scroll.region.start + dmg->scroll.lines) * term->cell_height;
|
|
int height = (dmg->scroll.region.end - dmg->scroll.region.start - dmg->scroll.lines) * term->cell_height;
|
|
|
|
LOG_DBG("damage: SCROLL: %d-%d by %d lines (dst-y: %d, src-y: %d, "
|
|
"height: %d, stride: %d, mmap-size: %zu)",
|
|
dmg->scroll.region.start, dmg->scroll.region.end,
|
|
dmg->scroll.lines,
|
|
dst_y, src_y, height, buf->stride,
|
|
buf->size);
|
|
|
|
if (height > 0) {
|
|
uint8_t *raw = buf->mmapped;
|
|
memmove(raw + dst_y * buf->stride,
|
|
raw + src_y * buf->stride,
|
|
height * buf->stride);
|
|
|
|
wl_surface_damage_buffer(
|
|
term->window->surface, term->x_margin, dst_y, term->width - term->x_margin, height);
|
|
}
|
|
}
|
|
|
|
static void
|
|
grid_render_scroll_reverse(struct terminal *term, struct buffer *buf,
|
|
const struct damage *dmg)
|
|
{
|
|
int src_y = term->y_margin + (dmg->scroll.region.start + 0) * term->cell_height;
|
|
int dst_y = term->y_margin + (dmg->scroll.region.start + dmg->scroll.lines) * term->cell_height;
|
|
int height = (dmg->scroll.region.end - dmg->scroll.region.start - dmg->scroll.lines) * term->cell_height;
|
|
|
|
LOG_DBG("damage: SCROLL REVERSE: %d-%d by %d lines (dst-y: %d, src-y: %d, "
|
|
"height: %d, stride: %d, mmap-size: %zu)",
|
|
dmg->scroll.region.start, dmg->scroll.region.end,
|
|
dmg->scroll.lines,
|
|
dst_y, src_y, height, buf->stride,
|
|
buf->size);
|
|
|
|
if (height > 0) {
|
|
uint8_t *raw = buf->mmapped;
|
|
memmove(raw + dst_y * buf->stride,
|
|
raw + src_y * buf->stride,
|
|
height * buf->stride);
|
|
|
|
wl_surface_damage_buffer(
|
|
term->window->surface, term->x_margin, dst_y, term->width - term->x_margin, height);
|
|
}
|
|
}
|
|
|
|
static void
|
|
render_row(struct terminal *term, pixman_image_t *pix, struct row *row, int row_no)
|
|
{
|
|
for (int col = term->cols - 1; col >= 0; col--)
|
|
render_cell(term, pix, &row->cells[col], col, row_no, false);
|
|
}
|
|
|
|
int
|
|
render_worker_thread(void *_ctx)
|
|
{
|
|
struct render_worker_context *ctx = _ctx;
|
|
struct terminal *term = ctx->term;
|
|
const int my_id = ctx->my_id;
|
|
free(ctx);
|
|
|
|
char proc_title[16];
|
|
snprintf(proc_title, sizeof(proc_title), "foot:render:%d", my_id);
|
|
|
|
if (prctl(PR_SET_NAME, proc_title, 0, 0, 0) < 0)
|
|
LOG_ERRNO("render worker %d: failed to set process title", my_id);
|
|
|
|
sem_t *start = &term->render.workers.start;
|
|
sem_t *done = &term->render.workers.done;
|
|
mtx_t *lock = &term->render.workers.lock;
|
|
cnd_t *cond = &term->render.workers.cond;
|
|
|
|
while (true) {
|
|
sem_wait(start);
|
|
|
|
struct buffer *buf = term->render.workers.buf;
|
|
bool frame_done = false;
|
|
|
|
while (!frame_done) {
|
|
mtx_lock(lock);
|
|
while (tll_length(term->render.workers.queue) == 0)
|
|
cnd_wait(cond, lock);
|
|
|
|
int row_no = tll_pop_front(term->render.workers.queue);
|
|
mtx_unlock(lock);
|
|
|
|
switch (row_no) {
|
|
default:
|
|
assert(buf != NULL);
|
|
render_row(term, buf->pix, grid_row_in_view(term->grid, row_no), row_no);
|
|
break;
|
|
|
|
case -1:
|
|
frame_done = true;
|
|
sem_post(done);
|
|
break;
|
|
|
|
case -2:
|
|
return 0;
|
|
}
|
|
}
|
|
};
|
|
|
|
return -1;
|
|
}
|
|
|
|
static void frame_callback(
|
|
void *data, struct wl_callback *wl_callback, uint32_t callback_data);
|
|
|
|
static const struct wl_callback_listener frame_listener = {
|
|
.done = &frame_callback,
|
|
};
|
|
|
|
void
|
|
grid_render(struct terminal *term)
|
|
{
|
|
if (term->is_shutting_down)
|
|
return;
|
|
|
|
#define TIME_FRAME_RENDERING 0
|
|
|
|
#if TIME_FRAME_RENDERING
|
|
struct timeval start_time;
|
|
gettimeofday(&start_time, NULL);
|
|
#endif
|
|
|
|
assert(term->width > 0);
|
|
assert(term->height > 0);
|
|
|
|
unsigned long cookie = (uintptr_t)term;
|
|
struct buffer *buf = shm_get_buffer(
|
|
term->wl->shm, term->width, term->height, cookie);
|
|
|
|
wl_surface_attach(term->window->surface, buf->wl_buf, 0, 0);
|
|
|
|
pixman_image_t *pix = buf->pix;
|
|
|
|
/* If we resized the window, or is flashing, or just stopped flashing */
|
|
if (term->render.last_buf != buf ||
|
|
term->flash.active || term->render.was_flashing ||
|
|
term->is_searching != term->render.was_searching)
|
|
{
|
|
if (term->render.last_buf != NULL &&
|
|
term->render.last_buf->width == buf->width &&
|
|
term->render.last_buf->height == buf->height &&
|
|
!term->flash.active &&
|
|
!term->render.was_flashing &&
|
|
term->is_searching == term->render.was_searching)
|
|
{
|
|
static bool has_warned = false;
|
|
if (!has_warned) {
|
|
LOG_WARN("it appears your Wayland compositor does not support buffer re-use for SHM clients; expect lower performance.");
|
|
has_warned = true;
|
|
}
|
|
|
|
assert(term->render.last_buf->size == buf->size);
|
|
memcpy(buf->mmapped, term->render.last_buf->mmapped, buf->size);
|
|
}
|
|
|
|
else {
|
|
/* Fill area outside the cell grid with the default background color */
|
|
int rmargin = term->x_margin + term->cols * term->cell_width;
|
|
int bmargin = term->y_margin + term->rows * term->cell_height;
|
|
int rmargin_width = term->width - rmargin;
|
|
int bmargin_height = term->height - bmargin;
|
|
|
|
uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg;
|
|
pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, term->colors.alpha);
|
|
if (term->is_searching)
|
|
pixman_color_dim(&bg);
|
|
|
|
pixman_image_fill_rectangles(
|
|
PIXMAN_OP_SRC, pix, &bg, 4,
|
|
(pixman_rectangle16_t[]){
|
|
{0, 0, term->width, term->y_margin}, /* Top */
|
|
{0, 0, term->x_margin, term->height}, /* Left */
|
|
{rmargin, 0, rmargin_width, term->height}, /* Right */
|
|
{0, bmargin, term->width, bmargin_height}}); /* Bottom */
|
|
|
|
wl_surface_damage_buffer(
|
|
term->window->surface, 0, 0, term->width, term->y_margin);
|
|
wl_surface_damage_buffer(
|
|
term->window->surface, 0, 0, term->x_margin, term->height);
|
|
wl_surface_damage_buffer(
|
|
term->window->surface, rmargin, 0, rmargin_width, term->height);
|
|
wl_surface_damage_buffer(
|
|
term->window->surface, 0, bmargin, term->width, bmargin_height);
|
|
|
|
/* Force a full grid refresh */
|
|
term_damage_view(term);
|
|
}
|
|
|
|
term->render.last_buf = buf;
|
|
term->render.was_flashing = term->flash.active;
|
|
term->render.was_searching = term->is_searching;
|
|
}
|
|
|
|
/* Erase old cursor (if we rendered a cursor last time) */
|
|
if (term->render.last_cursor.cell != NULL) {
|
|
|
|
struct cell *cell = term->render.last_cursor.cell;
|
|
struct coord at = term->render.last_cursor.in_view;
|
|
term->render.last_cursor.cell = NULL;
|
|
|
|
/* If cell already is dirty, it will be rendered anyway */
|
|
if (cell->attrs.clean) {
|
|
cell->attrs.clean = 0;
|
|
render_cell(term, pix, cell, at.col, at.row, false);
|
|
|
|
wl_surface_damage_buffer(
|
|
term->window->surface,
|
|
term->x_margin + at.col * term->cell_width,
|
|
term->y_margin + at.row * term->cell_height,
|
|
term->cell_width, term->cell_height);
|
|
}
|
|
}
|
|
|
|
tll_foreach(term->grid->scroll_damage, it) {
|
|
switch (it->item.type) {
|
|
case DAMAGE_SCROLL:
|
|
if (term->grid->view == term->grid->offset)
|
|
grid_render_scroll(term, buf, &it->item);
|
|
break;
|
|
|
|
case DAMAGE_SCROLL_REVERSE:
|
|
if (term->grid->view == term->grid->offset)
|
|
grid_render_scroll_reverse(term, buf, &it->item);
|
|
break;
|
|
|
|
case DAMAGE_SCROLL_IN_VIEW:
|
|
grid_render_scroll(term, buf, &it->item);
|
|
break;
|
|
|
|
case DAMAGE_SCROLL_REVERSE_IN_VIEW:
|
|
grid_render_scroll_reverse(term, buf, &it->item);
|
|
break;
|
|
}
|
|
|
|
tll_remove(term->grid->scroll_damage, it);
|
|
}
|
|
|
|
if (term->render.workers.count > 0) {
|
|
|
|
term->render.workers.buf = buf;
|
|
for (size_t i = 0; i < term->render.workers.count; i++)
|
|
sem_post(&term->render.workers.start);
|
|
|
|
assert(tll_length(term->render.workers.queue) == 0);
|
|
|
|
for (int r = 0; r < term->rows; r++) {
|
|
struct row *row = grid_row_in_view(term->grid, r);
|
|
|
|
if (!row->dirty)
|
|
continue;
|
|
|
|
mtx_lock(&term->render.workers.lock);
|
|
tll_push_back(term->render.workers.queue, r);
|
|
cnd_signal(&term->render.workers.cond);
|
|
mtx_unlock(&term->render.workers.lock);
|
|
|
|
row->dirty = false;
|
|
|
|
wl_surface_damage_buffer(
|
|
term->window->surface,
|
|
term->x_margin, term->y_margin + r * term->cell_height,
|
|
term->width - term->x_margin, term->cell_height);
|
|
}
|
|
|
|
mtx_lock(&term->render.workers.lock);
|
|
for (size_t i = 0; i < term->render.workers.count; i++)
|
|
tll_push_back(term->render.workers.queue, -1);
|
|
cnd_broadcast(&term->render.workers.cond);
|
|
mtx_unlock(&term->render.workers.lock);
|
|
} else {
|
|
for (int r = 0; r < term->rows; r++) {
|
|
struct row *row = grid_row_in_view(term->grid, r);
|
|
|
|
if (!row->dirty)
|
|
continue;
|
|
|
|
render_row(term, pix, row, r);
|
|
row->dirty = false;
|
|
|
|
wl_surface_damage_buffer(
|
|
term->window->surface,
|
|
term->x_margin, term->y_margin + r * term->cell_height,
|
|
term->width - term->x_margin, term->cell_height);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Determine if we need to render a cursor or not. The cursor
|
|
* could be hidden. Or it could have been scrolled out of view.
|
|
*/
|
|
bool cursor_is_visible = false;
|
|
int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1);
|
|
int cursor_row = (term->grid->offset + term->cursor.point.row) & (term->grid->num_rows - 1);
|
|
if (view_end >= term->grid->view) {
|
|
/* Not wrapped */
|
|
if (cursor_row >= term->grid->view && cursor_row <= view_end)
|
|
cursor_is_visible = true;
|
|
} else {
|
|
/* Wrapped */
|
|
if (cursor_row >= term->grid->view || cursor_row <= view_end)
|
|
cursor_is_visible = true;
|
|
}
|
|
|
|
/*
|
|
* Wait for workers to finish before we render the cursor. This is
|
|
* because the cursor cell might be dirty, in which case a worker
|
|
* will render it (but without the cursor).
|
|
*/
|
|
if (term->render.workers.count > 0) {
|
|
for (size_t i = 0; i < term->render.workers.count; i++)
|
|
sem_wait(&term->render.workers.done);
|
|
term->render.workers.buf = NULL;
|
|
}
|
|
|
|
if (cursor_is_visible && !term->hide_cursor) {
|
|
/* Remember cursor coordinates so that we can erase it next
|
|
* time. Note that we need to re-align it against the view. */
|
|
int view_aligned_row
|
|
= (cursor_row - term->grid->view + term->grid->num_rows) & (term->grid->num_rows - 1);
|
|
|
|
term->render.last_cursor.actual = term->cursor.point;
|
|
term->render.last_cursor.in_view = (struct coord) {
|
|
term->cursor.point.col, view_aligned_row};
|
|
|
|
struct row *row = grid_row_in_view(term->grid, view_aligned_row);
|
|
struct cell *cell = &row->cells[term->cursor.point.col];
|
|
|
|
cell->attrs.clean = 0;
|
|
term->render.last_cursor.cell = cell;
|
|
int cols_updated = render_cell(
|
|
term, pix, cell, term->cursor.point.col, view_aligned_row, true);
|
|
|
|
wl_surface_damage_buffer(
|
|
term->window->surface,
|
|
term->x_margin + term->cursor.point.col * term->cell_width,
|
|
term->y_margin + view_aligned_row * term->cell_height,
|
|
cols_updated * term->cell_width, term->cell_height);
|
|
}
|
|
|
|
if (term->flash.active) {
|
|
/* Note: alpha is pre-computed in each color component */
|
|
/* TODO: dim while searching */
|
|
pixman_image_fill_rectangles(
|
|
PIXMAN_OP_OVER, pix,
|
|
&(pixman_color_t){.red=0x7fff, .green=0x7fff, .blue=0, .alpha=0x7fff},
|
|
1, &(pixman_rectangle16_t){0, 0, term->width, term->height});
|
|
|
|
wl_surface_damage_buffer(
|
|
term->window->surface, 0, 0, term->width, term->height);
|
|
}
|
|
|
|
assert(term->grid->offset >= 0 && term->grid->offset < term->grid->num_rows);
|
|
assert(term->grid->view >= 0 && term->grid->view < term->grid->num_rows);
|
|
|
|
assert(term->window->frame_callback == NULL);
|
|
term->window->frame_callback = wl_surface_frame(term->window->surface);
|
|
wl_callback_add_listener(term->window->frame_callback, &frame_listener, term);
|
|
|
|
wl_surface_set_buffer_scale(term->window->surface, term->scale);
|
|
wl_surface_commit(term->window->surface);
|
|
|
|
#if TIME_FRAME_RENDERING
|
|
struct timeval end_time;
|
|
gettimeofday(&end_time, NULL);
|
|
|
|
struct timeval render_time;
|
|
timersub(&end_time, &start_time, &render_time);
|
|
LOG_INFO("frame rendered in %lds %ldus",
|
|
render_time.tv_sec, render_time.tv_usec);
|
|
#endif
|
|
wl_display_flush(term->wl->display);
|
|
}
|
|
|
|
static void
|
|
frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data)
|
|
{
|
|
struct terminal *term = data;
|
|
|
|
assert(term->window->frame_callback == wl_callback);
|
|
wl_callback_destroy(wl_callback);
|
|
term->window->frame_callback = NULL;
|
|
|
|
if (term->render.pending) {
|
|
term->render.pending = false;
|
|
grid_render(term);
|
|
}
|
|
}
|
|
|
|
void
|
|
render_search_box(struct terminal *term)
|
|
{
|
|
assert(term->window->search_sub_surface != NULL);
|
|
|
|
const int scale = term->scale >= 1 ? term->scale : 1;
|
|
const int margin = scale * 3;
|
|
const int width = min(term->width, 2 * margin + max(20, term->search.len) * term->cell_width);
|
|
const int height = min(term->height, 2 * margin + 1 * term->cell_height);
|
|
|
|
unsigned long cookie = (uintptr_t)term;
|
|
struct buffer *buf = shm_get_buffer(term->wl->shm, width, height, cookie);
|
|
|
|
/* Background - yellow on empty/match, red on mismatch */
|
|
pixman_color_t color = color_hex_to_pixman(
|
|
term->search.match_len == term->search.len
|
|
? term->colors.table[3] : term->colors.table[1]);
|
|
|
|
pixman_image_fill_rectangles(
|
|
PIXMAN_OP_SRC, buf->pix, &color,
|
|
1, &(pixman_rectangle16_t){0, 0, width, height});
|
|
|
|
struct font *font = term->fonts[0];
|
|
int x = margin;
|
|
int y = margin;
|
|
pixman_color_t fg = color_hex_to_pixman(term->colors.table[0]);
|
|
|
|
/* Text (what the user entered - *not* match(es)) */
|
|
for (size_t i = 0; i < term->search.len; i++) {
|
|
if (i == term->search.cursor)
|
|
draw_bar(term, buf->pix, font, &fg, x, y);
|
|
|
|
const struct glyph *glyph = font_glyph_for_wc(font, term->search.buf[i]);
|
|
if (glyph == NULL)
|
|
continue;
|
|
|
|
pixman_image_t *src = pixman_image_create_solid_fill(&fg);
|
|
pixman_image_composite32(
|
|
PIXMAN_OP_OVER, src, glyph->pix, buf->pix, 0, 0, 0, 0,
|
|
x + glyph->x, y + font_baseline(term) - glyph->y,
|
|
glyph->width, glyph->height);
|
|
pixman_image_unref(src);
|
|
|
|
x += term->cell_width;
|
|
}
|
|
|
|
if (term->search.cursor >= term->search.len)
|
|
draw_bar(term, buf->pix, font, &fg, x, y);
|
|
|
|
wl_subsurface_set_position(
|
|
term->window->search_sub_surface,
|
|
max(0, term->width - width - margin),
|
|
max(0, term->height - height - margin));
|
|
|
|
wl_surface_damage_buffer(term->window->search_surface, 0, 0, width, height);
|
|
wl_surface_attach(term->window->search_surface, buf->wl_buf, 0, 0);
|
|
wl_surface_set_buffer_scale(term->window->search_surface, scale);
|
|
wl_surface_commit(term->window->search_surface);
|
|
}
|
|
|
|
static void
|
|
reflow(struct row **new_grid, int new_cols, int new_rows,
|
|
struct row *const *old_grid, int old_cols, int old_rows)
|
|
{
|
|
/* TODO: actually reflow */
|
|
for (int r = 0; r < min(new_rows, old_rows); r++) {
|
|
size_t copy_cols = min(new_cols, old_cols);
|
|
size_t clear_cols = new_cols - copy_cols;
|
|
|
|
if (old_grid[r] == NULL)
|
|
continue;
|
|
|
|
if (new_grid[r] == NULL)
|
|
new_grid[r] = grid_row_alloc(new_cols, false);
|
|
|
|
struct cell *new_cells = new_grid[r]->cells;
|
|
const struct cell *old_cells = old_grid[r]->cells;
|
|
|
|
new_grid[r]->dirty = old_grid[r]->dirty;
|
|
memcpy(new_cells, old_cells, copy_cols * sizeof(new_cells[0]));
|
|
memset(&new_cells[copy_cols], 0, clear_cols * sizeof(new_cells[0]));
|
|
}
|
|
}
|
|
|
|
/* Move to terminal.c? */
|
|
void
|
|
render_resize(struct terminal *term, int width, int height)
|
|
{
|
|
int scale = -1;
|
|
tll_foreach(term->window->on_outputs, it) {
|
|
if (it->item->scale > scale)
|
|
scale = it->item->scale;
|
|
}
|
|
|
|
if (scale == -1) {
|
|
/* Haven't 'entered' an output yet? */
|
|
scale = 1;
|
|
}
|
|
|
|
width *= scale;
|
|
height *= scale;
|
|
|
|
if (width == 0 && height == 0) {
|
|
/* Assume we're not fully up and running yet */
|
|
return;
|
|
}
|
|
|
|
if (width == term->width && height == term->height && scale == term->scale)
|
|
return;
|
|
|
|
term->width = width;
|
|
term->height = height;
|
|
term->scale = scale;
|
|
|
|
const int scrollback_lines = term->render.scrollback_lines;
|
|
|
|
const int old_cols = term->cols;
|
|
const int old_rows = term->rows;
|
|
const int old_normal_grid_rows = term->normal.num_rows;
|
|
const int old_alt_grid_rows = term->alt.num_rows;
|
|
|
|
const int new_cols = term->width / term->cell_width;
|
|
const int new_rows = term->height / term->cell_height;
|
|
const int new_normal_grid_rows = 1 << (32 - __builtin_clz(new_rows + scrollback_lines - 1));
|
|
const int new_alt_grid_rows = 1 << (32 - __builtin_clz(new_rows));
|
|
|
|
term->x_margin = (term->width - new_cols * term->cell_width) / 2;
|
|
term->y_margin = (term->height - new_rows * term->cell_height) / 2;
|
|
|
|
term->normal.offset %= new_normal_grid_rows;
|
|
term->normal.view %= new_normal_grid_rows;
|
|
|
|
term->alt.offset %= new_alt_grid_rows;
|
|
term->alt.view %= new_alt_grid_rows;
|
|
|
|
/* Allocate new 'normal' grid */
|
|
struct row **normal = calloc(new_normal_grid_rows, sizeof(normal[0]));
|
|
for (int r = 0; r < new_rows; r++) {
|
|
size_t real_r = (term->normal.view + r) & (new_normal_grid_rows - 1);
|
|
normal[real_r] = grid_row_alloc(new_cols, true);
|
|
}
|
|
|
|
/* Allocate new 'alt' grid */
|
|
struct row **alt = calloc(new_alt_grid_rows, sizeof(alt[0]));
|
|
for (int r = 0; r < new_rows; r++) {
|
|
int real_r = (term->alt.view + r) & (new_alt_grid_rows - 1);
|
|
alt[real_r] = grid_row_alloc(new_cols, true);
|
|
}
|
|
|
|
/* Reflow content */
|
|
reflow(normal, new_cols, new_normal_grid_rows,
|
|
term->normal.rows, old_cols, old_normal_grid_rows);
|
|
reflow(alt, new_cols, new_alt_grid_rows,
|
|
term->alt.rows, old_cols, old_alt_grid_rows);
|
|
|
|
/* Free old 'normal' grid */
|
|
for (int r = 0; r < term->normal.num_rows; r++)
|
|
grid_row_free(term->normal.rows[r]);
|
|
free(term->normal.rows);
|
|
|
|
/* Free old 'alt' grid */
|
|
for (int r = 0; r < term->alt.num_rows; r++)
|
|
grid_row_free(term->alt.rows[r]);
|
|
free(term->alt.rows);
|
|
|
|
/* Reset tab stops */
|
|
tll_free(term->tab_stops);
|
|
for (int c = 0; c < new_cols; c += 8)
|
|
tll_push_back(term->tab_stops, c);
|
|
|
|
term->cols = new_cols;
|
|
term->rows = new_rows;
|
|
|
|
term->normal.rows = normal;
|
|
term->normal.num_rows = new_normal_grid_rows;
|
|
term->normal.num_cols = new_cols;
|
|
term->alt.rows = alt;
|
|
term->alt.num_rows = new_alt_grid_rows;
|
|
term->alt.num_cols = new_cols;
|
|
|
|
LOG_INFO("resize: %dx%d, grid: cols=%d, rows=%d (x-margin=%d, y-margin=%d)",
|
|
term->width, term->height, term->cols, term->rows,
|
|
term->x_margin, term->y_margin);
|
|
|
|
/* Signal TIOCSWINSZ */
|
|
if (ioctl(term->ptmx, TIOCSWINSZ,
|
|
&(struct winsize){
|
|
.ws_row = term->rows,
|
|
.ws_col = term->cols,
|
|
.ws_xpixel = term->width,
|
|
.ws_ypixel = term->height}) == -1)
|
|
{
|
|
LOG_ERRNO("TIOCSWINSZ");
|
|
}
|
|
|
|
if (term->scroll_region.start >= term->rows)
|
|
term->scroll_region.start = 0;
|
|
|
|
if (term->scroll_region.end >= old_rows)
|
|
term->scroll_region.end = term->rows;
|
|
|
|
term_cursor_to(
|
|
term,
|
|
min(term->cursor.point.row, term->rows - 1),
|
|
min(term->cursor.point.col, term->cols - 1));
|
|
|
|
term->render.last_cursor.cell = NULL;
|
|
|
|
term_damage_view(term);
|
|
render_refresh(term);
|
|
}
|
|
|
|
void
|
|
render_set_title(struct terminal *term, const char *_title)
|
|
{
|
|
/* TODO: figure out what the limit actually is */
|
|
static const size_t max_len = 100;
|
|
|
|
const char *title = _title;
|
|
char *copy = NULL;
|
|
|
|
if (strlen(title) > max_len) {
|
|
copy = strndup(_title, max_len);
|
|
title = copy;
|
|
}
|
|
|
|
xdg_toplevel_set_title(term->window->xdg_toplevel, title);
|
|
free(copy);
|
|
}
|
|
|
|
void
|
|
render_refresh(struct terminal *term)
|
|
{
|
|
if (term->window->frame_callback == NULL)
|
|
grid_render(term);
|
|
else
|
|
term->render.pending = true;
|
|
}
|