forked from dnkl/foot
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3850 lines
124 KiB
3850 lines
124 KiB
#include "render.h" |
|
|
|
#include <string.h> |
|
#include <wctype.h> |
|
#include <unistd.h> |
|
#include <signal.h> |
|
|
|
#include <sys/ioctl.h> |
|
#include <sys/time.h> |
|
#include <sys/timerfd.h> |
|
#include <sys/epoll.h> |
|
#include <pthread.h> |
|
|
|
#include "macros.h" |
|
#if HAS_INCLUDE(<pthread_np.h>) |
|
#include <pthread_np.h> |
|
#define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0) |
|
#elif defined(__NetBSD__) |
|
#define pthread_setname_np(thread, name) pthread_setname_np(thread, "%s", (void *)name) |
|
#endif |
|
|
|
#include <wayland-cursor.h> |
|
#include <xdg-shell.h> |
|
#include <presentation-time.h> |
|
|
|
#include <fcft/fcft.h> |
|
|
|
#define LOG_MODULE "render" |
|
#define LOG_ENABLE_DBG 0 |
|
#include "log.h" |
|
#include "box-drawing.h" |
|
#include "config.h" |
|
#include "grid.h" |
|
#include "hsl.h" |
|
#include "ime.h" |
|
#include "quirks.h" |
|
#include "selection.h" |
|
#include "shm.h" |
|
#include "sixel.h" |
|
#include "url-mode.h" |
|
#include "util.h" |
|
#include "xmalloc.h" |
|
|
|
#define TIME_SCROLL_DAMAGE 0 |
|
|
|
struct renderer { |
|
struct fdm *fdm; |
|
struct wayland *wayl; |
|
}; |
|
|
|
static struct { |
|
size_t total; |
|
size_t zero; /* commits presented in less than one frame interval */ |
|
size_t one; /* commits presented in one frame interval */ |
|
size_t two; /* commits presented in two or more frame intervals */ |
|
} presentation_statistics = {0}; |
|
|
|
static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data); |
|
|
|
struct renderer * |
|
render_init(struct fdm *fdm, struct wayland *wayl) |
|
{ |
|
struct renderer *renderer = malloc(sizeof(*renderer)); |
|
if (unlikely(renderer == NULL)) { |
|
LOG_ERRNO("malloc() failed"); |
|
return NULL; |
|
} |
|
|
|
*renderer = (struct renderer) { |
|
.fdm = fdm, |
|
.wayl = wayl, |
|
}; |
|
|
|
if (!fdm_hook_add(fdm, &fdm_hook_refresh_pending_terminals, renderer, |
|
FDM_HOOK_PRIORITY_NORMAL)) |
|
{ |
|
LOG_ERR("failed to register FDM hook"); |
|
free(renderer); |
|
return NULL; |
|
} |
|
|
|
return renderer; |
|
} |
|
|
|
void |
|
render_destroy(struct renderer *renderer) |
|
{ |
|
if (renderer == NULL) |
|
return; |
|
|
|
fdm_hook_del(renderer->fdm, &fdm_hook_refresh_pending_terminals, |
|
FDM_HOOK_PRIORITY_NORMAL); |
|
|
|
free(renderer); |
|
} |
|
|
|
static void DESTRUCTOR |
|
log_presentation_statistics(void) |
|
{ |
|
if (presentation_statistics.total == 0) |
|
return; |
|
|
|
const size_t total = presentation_statistics.total; |
|
LOG_INFO("presentation statistics: zero=%f%%, one=%f%%, two=%f%%", |
|
100. * presentation_statistics.zero / total, |
|
100. * presentation_statistics.one / total, |
|
100. * presentation_statistics.two / total); |
|
} |
|
|
|
static void |
|
sync_output(void *data, |
|
struct wp_presentation_feedback *wp_presentation_feedback, |
|
struct wl_output *output) |
|
{ |
|
} |
|
|
|
struct presentation_context { |
|
struct terminal *term; |
|
struct timeval input; |
|
struct timeval commit; |
|
}; |
|
|
|
static void |
|
presented(void *data, |
|
struct wp_presentation_feedback *wp_presentation_feedback, |
|
uint32_t tv_sec_hi, uint32_t tv_sec_lo, uint32_t tv_nsec, |
|
uint32_t refresh, uint32_t seq_hi, uint32_t seq_lo, uint32_t flags) |
|
{ |
|
struct presentation_context *ctx = data; |
|
struct terminal *term = ctx->term; |
|
const struct timeval *input = &ctx->input; |
|
const struct timeval *commit = &ctx->commit; |
|
|
|
const struct timeval presented = { |
|
.tv_sec = (uint64_t)tv_sec_hi << 32 | tv_sec_lo, |
|
.tv_usec = tv_nsec / 1000, |
|
}; |
|
|
|
bool use_input = (input->tv_sec > 0 || input->tv_usec > 0) && |
|
timercmp(&presented, input, >); |
|
char msg[1024]; |
|
int chars = 0; |
|
|
|
if (use_input && timercmp(&presented, input, <)) |
|
return; |
|
else if (timercmp(&presented, commit, <)) |
|
return; |
|
|
|
LOG_DBG("commit: %lu s %lu µs, presented: %lu s %lu µs", |
|
commit->tv_sec, commit->tv_usec, presented.tv_sec, presented.tv_usec); |
|
|
|
if (use_input) { |
|
struct timeval diff; |
|
timersub(commit, input, &diff); |
|
chars += snprintf( |
|
&msg[chars], sizeof(msg) - chars, |
|
"input - %llu µs -> ", (unsigned long long)diff.tv_usec); |
|
} |
|
|
|
struct timeval diff; |
|
timersub(&presented, commit, &diff); |
|
chars += snprintf( |
|
&msg[chars], sizeof(msg) - chars, |
|
"commit - %llu µs -> ", (unsigned long long)diff.tv_usec); |
|
|
|
if (use_input) { |
|
xassert(timercmp(&presented, input, >)); |
|
timersub(&presented, input, &diff); |
|
} else { |
|
xassert(timercmp(&presented, commit, >)); |
|
timersub(&presented, commit, &diff); |
|
} |
|
|
|
chars += snprintf( |
|
&msg[chars], sizeof(msg) - chars, |
|
"presented (total: %llu µs)", (unsigned long long)diff.tv_usec); |
|
|
|
unsigned frame_count = 0; |
|
if (tll_length(term->window->on_outputs) > 0) { |
|
const struct monitor *mon = tll_front(term->window->on_outputs); |
|
frame_count = (diff.tv_sec * 1000000. + diff.tv_usec) / (1000000. / mon->refresh); |
|
} |
|
|
|
presentation_statistics.total++; |
|
if (frame_count >= 2) |
|
presentation_statistics.two++; |
|
else if (frame_count >= 1) |
|
presentation_statistics.one++; |
|
else |
|
presentation_statistics.zero++; |
|
|
|
#define _log_fmt "%s (more than %u frames)" |
|
|
|
if (frame_count >= 2) |
|
LOG_ERR(_log_fmt, msg, frame_count); |
|
else if (frame_count >= 1) |
|
LOG_WARN(_log_fmt, msg, frame_count); |
|
else |
|
LOG_INFO(_log_fmt, msg, frame_count); |
|
|
|
#undef _log_fmt |
|
|
|
wp_presentation_feedback_destroy(wp_presentation_feedback); |
|
free(ctx); |
|
} |
|
|
|
static void |
|
discarded(void *data, struct wp_presentation_feedback *wp_presentation_feedback) |
|
{ |
|
struct presentation_context *ctx = data; |
|
wp_presentation_feedback_destroy(wp_presentation_feedback); |
|
free(ctx); |
|
} |
|
|
|
static const struct wp_presentation_feedback_listener presentation_feedback_listener = { |
|
.sync_output = &sync_output, |
|
.presented = &presented, |
|
.discarded = &discarded, |
|
}; |
|
|
|
static struct fcft_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 pixman_color_t |
|
color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha) |
|
{ |
|
return (pixman_color_t){ |
|
.red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)) * alpha / 0xffff, |
|
.green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)) * alpha / 0xffff, |
|
.blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)) * alpha / 0xffff, |
|
.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 uint32_t |
|
color_dim(uint32_t color) |
|
{ |
|
uint32_t alpha = color & 0xff000000; |
|
int hue, sat, lum; |
|
rgb_to_hsl(color, &hue, &sat, &lum); |
|
return alpha | hsl_to_rgb(hue, sat, lum / 1.5); |
|
} |
|
|
|
static inline uint32_t |
|
color_brighten(const struct terminal *term, uint32_t color) |
|
{ |
|
/* |
|
* First try to match the color against the base 8 colors. If we |
|
* find a match, return the corresponding bright color. |
|
*/ |
|
if (term->conf->bold_in_bright.palette_based) { |
|
for (size_t i = 0; i < 8; i++) { |
|
if (term->colors.table[i] == color) |
|
return term->colors.table[i + 8]; |
|
} |
|
return color; |
|
} |
|
|
|
int hue, sat, lum; |
|
rgb_to_hsl(color, &hue, &sat, &lum); |
|
return hsl_to_rgb(hue, sat, min(100, lum * 1.3)); |
|
} |
|
|
|
static inline void |
|
color_dim_for_search(pixman_color_t *color) |
|
{ |
|
color->red /= 2; |
|
color->green /= 2; |
|
color->blue /= 2; |
|
} |
|
|
|
static inline int |
|
font_baseline(const struct terminal *term) |
|
{ |
|
return term->font_y_ofs + 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_beam_cursor(const struct terminal *term, pixman_image_t *pix, |
|
const struct fcft_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, |
|
term_pt_or_px_as_pixels(term, &term->conf->cursor.beam_thickness), |
|
term->fonts[0]->ascent + term->fonts[0]->descent}); |
|
} |
|
|
|
static void |
|
draw_underline_with_thickness( |
|
const struct terminal *term, pixman_image_t *pix, |
|
const struct fcft_font *font, |
|
const pixman_color_t *color, int x, int y, int cols, int thickness) |
|
{ |
|
/* Make sure the line isn't positioned below the cell */ |
|
int y_ofs = font_baseline(term) - |
|
(term->conf->use_custom_underline_offset |
|
? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) |
|
: font->underline.position); |
|
|
|
y_ofs = min(y_ofs, term->cell_height - thickness); |
|
|
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, pix, color, |
|
1, &(pixman_rectangle16_t){ |
|
x, y + y_ofs, cols * term->cell_width, thickness}); |
|
} |
|
|
|
static void |
|
draw_underline_cursor(const struct terminal *term, pixman_image_t *pix, |
|
const struct fcft_font *font, |
|
const pixman_color_t *color, int x, int y, int cols) |
|
{ |
|
int thickness = term->conf->cursor.underline_thickness.px >= 0 |
|
? term_pt_or_px_as_pixels( |
|
term, &term->conf->cursor.underline_thickness) |
|
: font->underline.thickness; |
|
|
|
draw_underline_with_thickness( |
|
term, pix, font, color, x, y + font->underline.thickness, cols, |
|
thickness); |
|
} |
|
|
|
static void |
|
draw_underline(const struct terminal *term, pixman_image_t *pix, |
|
const struct fcft_font *font, |
|
const pixman_color_t *color, int x, int y, int cols) |
|
{ |
|
draw_underline_with_thickness( |
|
term, pix, font, color, x, y, cols, font->underline.thickness); |
|
} |
|
|
|
static void |
|
draw_strikeout(const struct terminal *term, pixman_image_t *pix, |
|
const struct fcft_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 void |
|
cursor_colors_for_cell(const struct terminal *term, const struct cell *cell, |
|
const pixman_color_t *fg, const pixman_color_t *bg, |
|
pixman_color_t *cursor_color, pixman_color_t *text_color) |
|
{ |
|
bool is_selected = cell->attrs.selected; |
|
|
|
if (term->cursor_color.cursor >> 31) { |
|
*cursor_color = color_hex_to_pixman(term->cursor_color.cursor); |
|
*text_color = color_hex_to_pixman( |
|
term->cursor_color.text >> 31 |
|
? term->cursor_color.text : term->colors.bg); |
|
|
|
if (cell->attrs.reverse ^ is_selected) { |
|
pixman_color_t swap = *cursor_color; |
|
*cursor_color = *text_color; |
|
*text_color = swap; |
|
} |
|
|
|
if (term->is_searching && !is_selected) { |
|
color_dim_for_search(cursor_color); |
|
color_dim_for_search(text_color); |
|
} |
|
} else { |
|
*cursor_color = *fg; |
|
*text_color = *bg; |
|
} |
|
} |
|
|
|
static void |
|
draw_cursor(const struct terminal *term, const struct cell *cell, |
|
const struct fcft_font *font, pixman_image_t *pix, pixman_color_t *fg, |
|
const pixman_color_t *bg, int x, int y, int cols) |
|
{ |
|
pixman_color_t cursor_color; |
|
pixman_color_t text_color; |
|
cursor_colors_for_cell(term, cell, fg, bg, &cursor_color, &text_color); |
|
|
|
switch (term->cursor_style) { |
|
case CURSOR_BLOCK: |
|
if (unlikely(!term->kbd_focus)) |
|
draw_unfocused_block(term, pix, &cursor_color, x, y, cols); |
|
|
|
else if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) { |
|
*fg = text_color; |
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, pix, &cursor_color, 1, |
|
&(pixman_rectangle16_t){x, y, cols * term->cell_width, term->cell_height}); |
|
} |
|
break; |
|
|
|
case CURSOR_BEAM: |
|
if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || |
|
!term->kbd_focus)) |
|
{ |
|
draw_beam_cursor(term, pix, font, &cursor_color, x, y); |
|
} |
|
break; |
|
|
|
case CURSOR_UNDERLINE: |
|
if (likely(term->cursor_blink.state == CURSOR_BLINK_ON || |
|
!term->kbd_focus)) |
|
{ |
|
draw_underline_cursor(term, pix, font, &cursor_color, x, y, cols); |
|
} |
|
break; |
|
} |
|
} |
|
|
|
static int |
|
render_cell(struct terminal *term, pixman_image_t *pix, |
|
struct row *row, int col, int row_no, bool has_cursor) |
|
{ |
|
struct cell *cell = &row->cells[col]; |
|
if (cell->attrs.clean) |
|
return 0; |
|
|
|
cell->attrs.clean = 1; |
|
cell->attrs.confined = true; |
|
|
|
int width = term->cell_width; |
|
int height = term->cell_height; |
|
const int x = term->margins.left + col * width; |
|
const int y = term->margins.top + row_no * height; |
|
|
|
bool is_selected = cell->attrs.selected; |
|
|
|
uint32_t _fg = 0; |
|
uint32_t _bg = 0; |
|
|
|
uint16_t alpha = 0xffff; |
|
|
|
if (is_selected && term->colors.use_custom_selection) { |
|
_fg = term->colors.selection_fg; |
|
_bg = term->colors.selection_bg; |
|
} 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.bg : term->colors.fg; |
|
_bg = cell->attrs.have_bg ? cell->attrs.bg : term->reverse ? term->colors.fg : term->colors.bg; |
|
|
|
if (cell->attrs.reverse ^ is_selected) { |
|
uint32_t swap = _fg; |
|
_fg = _bg; |
|
_bg = swap; |
|
} else if (!cell->attrs.have_bg) |
|
alpha = term->colors.alpha; |
|
} |
|
|
|
if (unlikely(is_selected && _fg == _bg)) { |
|
/* Invert bg when selected/highlighted text has same fg/bg */ |
|
_bg = ~_bg; |
|
alpha = 0xffff; |
|
} |
|
|
|
if (cell->attrs.dim) |
|
_fg = color_dim(_fg); |
|
if (term->conf->bold_in_bright.enabled && cell->attrs.bold) |
|
_fg = color_brighten(term, _fg); |
|
|
|
if (cell->attrs.blink && term->blink.state == BLINK_OFF) |
|
_fg = color_dim(_fg); |
|
|
|
pixman_color_t fg = color_hex_to_pixman(_fg); |
|
pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); |
|
|
|
if (term->is_searching && !is_selected) { |
|
color_dim_for_search(&fg); |
|
color_dim_for_search(&bg); |
|
} |
|
|
|
struct fcft_font *font = attrs_to_font(term, &cell->attrs); |
|
const struct composed *composed = NULL; |
|
const struct fcft_grapheme *grapheme = NULL; |
|
const struct fcft_glyph *single = NULL; |
|
const struct fcft_glyph **glyphs = NULL; |
|
unsigned glyph_count = 0; |
|
|
|
wchar_t base = cell->wc; |
|
int cell_cols = 1; |
|
|
|
if (base != 0) { |
|
if (unlikely( |
|
/* Classic box drawings */ |
|
(base >= GLYPH_BOX_DRAWING_FIRST && |
|
base <= GLYPH_BOX_DRAWING_LAST) || |
|
|
|
/* Braille */ |
|
(base >= GLYPH_BRAILLE_FIRST && |
|
base <= GLYPH_BRAILLE_LAST) || |
|
|
|
/* |
|
* Unicode 13 "Symbols for Legacy Computing" |
|
* sub-ranges below. |
|
* |
|
* Note, the full range is U+1FB00 - U+1FBF9 |
|
*/ |
|
(base >= GLYPH_LEGACY_FIRST && |
|
base <= GLYPH_LEGACY_LAST)) && |
|
|
|
likely(!term->conf->box_drawings_uses_font_glyphs)) |
|
{ |
|
struct fcft_glyph ***arr; |
|
size_t count; |
|
size_t idx; |
|
|
|
if (base >= GLYPH_LEGACY_FIRST) { |
|
arr = &term->custom_glyphs.legacy; |
|
count = GLYPH_LEGACY_COUNT; |
|
idx = base - GLYPH_LEGACY_FIRST; |
|
} else if (base >= GLYPH_BRAILLE_FIRST) { |
|
arr = &term->custom_glyphs.braille; |
|
count = GLYPH_BRAILLE_COUNT; |
|
idx = base - GLYPH_BRAILLE_FIRST; |
|
} else { |
|
arr = &term->custom_glyphs.box_drawing; |
|
count = GLYPH_BOX_DRAWING_COUNT; |
|
idx = base - GLYPH_BOX_DRAWING_FIRST; |
|
} |
|
|
|
if (unlikely(*arr == NULL)) |
|
*arr = xcalloc(count, sizeof((*arr)[0])); |
|
|
|
if (likely((*arr)[idx] != NULL)) |
|
single = (*arr)[idx]; |
|
else { |
|
mtx_lock(&term->render.workers.lock); |
|
|
|
/* Other thread may have instantiated it while we |
|
* acquired the lock */ |
|
single = (*arr)[idx]; |
|
if (likely(single == NULL)) |
|
single = (*arr)[idx] = box_drawing(term, base); |
|
mtx_unlock(&term->render.workers.lock); |
|
} |
|
|
|
if (single != NULL) { |
|
glyph_count = 1; |
|
glyphs = &single; |
|
cell_cols = single->cols; |
|
} |
|
} |
|
|
|
else if (base >= CELL_COMB_CHARS_LO && base <= CELL_COMB_CHARS_HI) |
|
{ |
|
composed = composed_lookup(term->composed, base - CELL_COMB_CHARS_LO); |
|
base = composed->chars[0]; |
|
|
|
if (term->conf->can_shape_grapheme && term->conf->tweak.grapheme_shaping) { |
|
grapheme = fcft_grapheme_rasterize( |
|
font, composed->count, composed->chars, |
|
0, NULL, term->font_subpixel); |
|
} |
|
|
|
if (grapheme != NULL) { |
|
cell_cols = composed->width; |
|
|
|
composed = NULL; |
|
glyphs = grapheme->glyphs; |
|
glyph_count = grapheme->count; |
|
} |
|
} |
|
|
|
|
|
if (single == NULL && grapheme == NULL) { |
|
xassert(base != 0); |
|
single = fcft_glyph_rasterize(font, base, term->font_subpixel); |
|
if (single == NULL) { |
|
glyph_count = 0; |
|
cell_cols = 1; |
|
} else { |
|
glyph_count = 1; |
|
glyphs = &single; |
|
cell_cols = single->cols; |
|
} |
|
} |
|
} |
|
|
|
assert(glyph_count == 0 || glyphs != NULL); |
|
|
|
const int cols_left = term->cols - col; |
|
cell_cols = max(1, min(cell_cols, cols_left)); |
|
|
|
/* |
|
* Determine cells that will bleed into their right neighbor and remember |
|
* them for cleanup in the next frame. |
|
*/ |
|
int render_width = cell_cols * width; |
|
if (term->conf->tweak.overflowing_glyphs && |
|
glyph_count > 0 && |
|
cols_left > cell_cols) |
|
{ |
|
int glyph_width = 0, advance = 0; |
|
for (size_t i = 0; i < glyph_count; i++) { |
|
glyph_width = max(glyph_width, |
|
advance + glyphs[i]->x + glyphs[i]->width); |
|
advance += glyphs[i]->advance.x; |
|
} |
|
|
|
if (glyph_width > render_width) { |
|
render_width = min(glyph_width, render_width + width); |
|
|
|
for (int i = 0; i < cell_cols; i++) |
|
row->cells[col + i].attrs.confined = false; |
|
} |
|
} |
|
|
|
pixman_region32_t clip; |
|
pixman_region32_init_rect( |
|
&clip, x, y, |
|
render_width, term->cell_height); |
|
pixman_image_set_clip_region32(pix, &clip); |
|
pixman_region32_fini(&clip); |
|
|
|
/* Background */ |
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, pix, &bg, 1, |
|
&(pixman_rectangle16_t){x, y, cell_cols * width, height}); |
|
|
|
if (cell->attrs.blink && term->blink.fd < 0) { |
|
/* TODO: use a custom lock for this? */ |
|
mtx_lock(&term->render.workers.lock); |
|
term_arm_blink_timer(term); |
|
mtx_unlock(&term->render.workers.lock); |
|
} |
|
|
|
if (has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus) |
|
draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); |
|
|
|
if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == L'\t' || |
|
(unlikely(cell->attrs.conceal) && !is_selected)) |
|
{ |
|
goto draw_cursor; |
|
} |
|
|
|
pixman_image_t *clr_pix = pixman_image_create_solid_fill(&fg); |
|
|
|
int pen_x = x; |
|
for (unsigned i = 0; i < glyph_count; i++) { |
|
const int letter_x_ofs = i == 0 ? term->font_x_ofs : 0; |
|
|
|
const struct fcft_glyph *glyph = glyphs[i]; |
|
if (glyph == NULL) |
|
continue; |
|
|
|
int g_x = glyph->x; |
|
int g_y = glyph->y; |
|
|
|
if (i > 0 && glyph->x >= 0) |
|
g_x -= term->cell_width; |
|
|
|
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, |
|
pen_x + letter_x_ofs + g_x, y + font_baseline(term) - g_y, |
|
glyph->width, glyph->height); |
|
} |
|
} else { |
|
pixman_image_composite32( |
|
PIXMAN_OP_OVER, clr_pix, glyph->pix, pix, 0, 0, 0, 0, |
|
pen_x + letter_x_ofs + g_x, y + font_baseline(term) - g_y, |
|
glyph->width, glyph->height); |
|
|
|
/* Combining characters */ |
|
if (composed != NULL) { |
|
assert(glyph_count == 1); |
|
|
|
for (size_t i = 1; i < composed->count; i++) { |
|
const struct fcft_glyph *g = fcft_glyph_rasterize( |
|
font, composed->chars[i], term->font_subpixel); |
|
|
|
if (g == NULL) |
|
continue; |
|
|
|
/* |
|
* Fonts _should_ assume the pen position is now |
|
* *after* the base glyph, and thus use negative |
|
* offsets for combining glyphs. |
|
* |
|
* Not all fonts behave like this however, and we |
|
* try to accommodate both variants. |
|
* |
|
* Since we haven't moved our pen position yet, we |
|
* add a full cell width to the offset (or two, in |
|
* case of double-width characters). |
|
* |
|
* If the font does *not* use negative offsets, |
|
* we'd normally use an offset of 0. However, to |
|
* somewhat deal with double-width glyphs we use |
|
* an offset of *one* cell. |
|
*/ |
|
int x_ofs = g->x < 0 |
|
? cell_cols * term->cell_width |
|
: (cell_cols - 1) * term->cell_width; |
|
|
|
pixman_image_composite32( |
|
PIXMAN_OP_OVER, clr_pix, g->pix, pix, 0, 0, 0, 0, |
|
/* Some fonts use a negative offset, while others use a |
|
* "normal" offset */ |
|
pen_x + x_ofs + g->x, |
|
y + font_baseline(term) - g->y, |
|
g->width, g->height); |
|
} |
|
} |
|
} |
|
|
|
pen_x += glyph->advance.x; |
|
} |
|
|
|
pixman_image_unref(clr_pix); |
|
|
|
/* Underline */ |
|
if (cell->attrs.underline) |
|
draw_underline(term, pix, font, &fg, x, y, cell_cols); |
|
|
|
if (cell->attrs.strikethrough) |
|
draw_strikeout(term, pix, font, &fg, x, y, cell_cols); |
|
|
|
if (unlikely(cell->attrs.url)) { |
|
pixman_color_t url_color = color_hex_to_pixman( |
|
term->conf->colors.use_custom.url |
|
? term->conf->colors.url |
|
: term->colors.table[3] |
|
); |
|
draw_underline(term, pix, font, &url_color, x, y, cell_cols); |
|
} |
|
|
|
draw_cursor: |
|
if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) |
|
draw_cursor(term, cell, font, pix, &fg, &bg, x, y, cell_cols); |
|
|
|
pixman_image_set_clip_region32(pix, NULL); |
|
return cell_cols; |
|
} |
|
|
|
static void |
|
render_row(struct terminal *term, pixman_image_t *pix, struct row *row, |
|
int row_no, int cursor_col) |
|
{ |
|
for (int col = term->cols - 1; col >= 0; col--) |
|
render_cell(term, pix, row, col, row_no, cursor_col == col); |
|
} |
|
|
|
static void |
|
render_urgency(struct terminal *term, struct buffer *buf) |
|
{ |
|
uint32_t red = term->colors.table[1]; |
|
if (term->is_searching) |
|
red = color_dim(red); |
|
|
|
pixman_color_t bg = color_hex_to_pixman(red); |
|
|
|
|
|
int width = min(min(term->margins.left, term->margins.right), |
|
min(term->margins.top, term->margins.bottom)); |
|
|
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, buf->pix[0], &bg, 4, |
|
(pixman_rectangle16_t[]){ |
|
/* Top */ |
|
{0, 0, term->width, width}, |
|
|
|
/* Bottom */ |
|
{0, term->height - width, term->width, width}, |
|
|
|
/* Left */ |
|
{0, width, width, term->height - 2 * width}, |
|
|
|
/* Right */ |
|
{term->width - width, width, width, term->height - 2 * width}, |
|
}); |
|
} |
|
|
|
static void |
|
render_margin(struct terminal *term, struct buffer *buf, |
|
int start_line, int end_line, bool apply_damage) |
|
{ |
|
/* Fill area outside the cell grid with the default background color */ |
|
const int rmargin = term->width - term->margins.right; |
|
const int bmargin = term->height - term->margins.bottom; |
|
const int line_count = end_line - start_line; |
|
|
|
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) |
|
color_dim_for_search(&bg); |
|
|
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, buf->pix[0], &bg, 4, |
|
(pixman_rectangle16_t[]){ |
|
/* Top */ |
|
{0, 0, term->width, term->margins.top}, |
|
|
|
/* Bottom */ |
|
{0, bmargin, term->width, term->margins.bottom}, |
|
|
|
/* Left */ |
|
{0, term->margins.top + start_line * term->cell_height, |
|
term->margins.left, line_count * term->cell_height}, |
|
|
|
/* Right */ |
|
{rmargin, term->margins.top + start_line * term->cell_height, |
|
term->margins.right, line_count * term->cell_height}, |
|
}); |
|
|
|
if (term->render.urgency) |
|
render_urgency(term, buf); |
|
|
|
/* Ensure the updated regions are copied to the next frame's |
|
* buffer when we're double buffering */ |
|
pixman_region32_union_rect( |
|
&buf->dirty, &buf->dirty, 0, 0, term->width, term->margins.top); |
|
pixman_region32_union_rect( |
|
&buf->dirty, &buf->dirty, 0, bmargin, term->width, term->margins.bottom); |
|
pixman_region32_union_rect( |
|
&buf->dirty, &buf->dirty, 0, 0, term->margins.left, term->height); |
|
pixman_region32_union_rect( |
|
&buf->dirty, &buf->dirty, |
|
rmargin, 0, term->margins.right, term->height); |
|
|
|
if (apply_damage) { |
|
/* Top */ |
|
wl_surface_damage_buffer( |
|
term->window->surface, 0, 0, term->width, term->margins.top); |
|
|
|
/* Bottom */ |
|
wl_surface_damage_buffer( |
|
term->window->surface, 0, bmargin, term->width, term->margins.bottom); |
|
|
|
/* Left */ |
|
wl_surface_damage_buffer( |
|
term->window->surface, |
|
0, term->margins.top + start_line * term->cell_height, |
|
term->margins.left, line_count * term->cell_height); |
|
|
|
/* Right */ |
|
wl_surface_damage_buffer( |
|
term->window->surface, |
|
rmargin, term->margins.top + start_line * term->cell_height, |
|
term->margins.right, line_count * term->cell_height); |
|
} |
|
} |
|
|
|
static void |
|
grid_render_scroll(struct terminal *term, struct buffer *buf, |
|
const struct damage *dmg) |
|
{ |
|
int height = (dmg->region.end - dmg->region.start - dmg->lines) * term->cell_height; |
|
|
|
LOG_DBG( |
|
"damage: SCROLL: %d-%d by %d lines", |
|
dmg->region.start, dmg->region.end, dmg->lines); |
|
|
|
if (height <= 0) |
|
return; |
|
|
|
#if TIME_SCROLL_DAMAGE |
|
struct timeval start_time; |
|
gettimeofday(&start_time, NULL); |
|
#endif |
|
|
|
int dst_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; |
|
int src_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; |
|
|
|
/* |
|
* SHM scrolling can be *much* faster, but it depends on how many |
|
* lines we're scrolling, and how much repairing we need to do. |
|
* |
|
* In short, scrolling a *large* number of rows is faster with a |
|
* memmove, while scrolling a *small* number of lines is faster |
|
* with SHM scrolling. |
|
* |
|
* However, since we need to restore the scrolling regions when |
|
* SHM scrolling, we also need to take this into account. |
|
* |
|
* Finally, we also have to restore the window margins, and this |
|
* is a *huge* performance hit when scrolling a large number of |
|
* lines (in addition to the sloweness of SHM scrolling as |
|
* method). |
|
* |
|
* So, we need to figure out when to SHM scroll, and when to |
|
* memmove. |
|
* |
|
* For now, assume that the both methods perform roughly the same, |
|
* given an equal number of bytes to move/allocate, and use the |
|
* method that results in the least amount of bytes to touch. |
|
* |
|
* Since number of lines directly translates to bytes, we can |
|
* simply count lines. |
|
* |
|
* SHM scrolling needs to first "move" (punch hole + allocate) |
|
* dmg->lines number of lines, and then we need to restore |
|
* the bottom scroll region. |
|
* |
|
* If the total number of lines is less than half the screen - use |
|
* SHM. Otherwise use memmove. |
|
*/ |
|
bool try_shm_scroll = |
|
shm_can_scroll(buf) && ( |
|
dmg->lines + |
|
dmg->region.start + |
|
(term->rows - dmg->region.end)) < term->rows / 2; |
|
|
|
bool did_shm_scroll = false; |
|
|
|
//try_shm_scroll = false; |
|
//try_shm_scroll = true; |
|
|
|
if (try_shm_scroll) { |
|
did_shm_scroll = shm_scroll( |
|
buf, dmg->lines * term->cell_height, |
|
term->margins.top, dmg->region.start * term->cell_height, |
|
term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); |
|
} |
|
|
|
if (did_shm_scroll) { |
|
/* Restore margins */ |
|
render_margin( |
|
term, buf, dmg->region.end - dmg->lines, term->rows, false); |
|
} else { |
|
/* Fallback for when we either cannot do SHM scrolling, or it failed */ |
|
uint8_t *raw = buf->data; |
|
memmove(raw + dst_y * buf->stride, |
|
raw + src_y * buf->stride, |
|
height * buf->stride); |
|
} |
|
|
|
#if TIME_SCROLL_DAMAGE |
|
struct timeval end_time; |
|
gettimeofday(&end_time, NULL); |
|
|
|
struct timeval memmove_time; |
|
timersub(&end_time, &start_time, &memmove_time); |
|
LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldus", |
|
height * buf->stride / 1024, dmg->lines, |
|
did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", |
|
memmove_time.tv_sec, memmove_time.tv_usec); |
|
#endif |
|
|
|
wl_surface_damage_buffer( |
|
term->window->surface, term->margins.left, dst_y, |
|
term->width - term->margins.left - term->margins.right, height); |
|
} |
|
|
|
static void |
|
grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, |
|
const struct damage *dmg) |
|
{ |
|
int height = (dmg->region.end - dmg->region.start - dmg->lines) * term->cell_height; |
|
|
|
LOG_DBG( |
|
"damage: SCROLL REVERSE: %d-%d by %d lines", |
|
dmg->region.start, dmg->region.end, dmg->lines); |
|
|
|
if (height <= 0) |
|
return; |
|
|
|
#if TIME_SCROLL_DAMAGE |
|
struct timeval start_time; |
|
gettimeofday(&start_time, NULL); |
|
#endif |
|
|
|
int src_y = term->margins.top + (dmg->region.start + 0) * term->cell_height; |
|
int dst_y = term->margins.top + (dmg->region.start + dmg->lines) * term->cell_height; |
|
|
|
bool try_shm_scroll = |
|
shm_can_scroll(buf) && ( |
|
dmg->lines + |
|
dmg->region.start + |
|
(term->rows - dmg->region.end)) < term->rows / 2; |
|
|
|
bool did_shm_scroll = false; |
|
|
|
if (try_shm_scroll) { |
|
did_shm_scroll = shm_scroll( |
|
buf, -dmg->lines * term->cell_height, |
|
term->margins.top, dmg->region.start * term->cell_height, |
|
term->margins.bottom, (term->rows - dmg->region.end) * term->cell_height); |
|
} |
|
|
|
if (did_shm_scroll) { |
|
/* Restore margins */ |
|
render_margin( |
|
term, buf, dmg->region.start, dmg->region.start + dmg->lines, false); |
|
} else { |
|
/* Fallback for when we either cannot do SHM scrolling, or it failed */ |
|
uint8_t *raw = buf->data; |
|
memmove(raw + dst_y * buf->stride, |
|
raw + src_y * buf->stride, |
|
height * buf->stride); |
|
} |
|
|
|
#if TIME_SCROLL_DAMAGE |
|
struct timeval end_time; |
|
gettimeofday(&end_time, NULL); |
|
|
|
struct timeval memmove_time; |
|
timersub(&end_time, &start_time, &memmove_time); |
|
LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldus", |
|
height * buf->stride / 1024, dmg->lines, |
|
did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", |
|
memmove_time.tv_sec, memmove_time.tv_usec); |
|
#endif |
|
|
|
wl_surface_damage_buffer( |
|
term->window->surface, term->margins.left, dst_y, |
|
term->width - term->margins.left - term->margins.right, height); |
|
} |
|
|
|
static void |
|
render_sixel_chunk(struct terminal *term, pixman_image_t *pix, const struct sixel *sixel, |
|
int term_start_row, int img_start_row, int count) |
|
{ |
|
/* Translate row/column to x/y pixel values */ |
|
const int x = term->margins.left + sixel->pos.col * term->cell_width; |
|
const int y = term->margins.top + term_start_row * term->cell_height; |
|
|
|
/* Width/height, in pixels - and don't touch the window margins */ |
|
const int width = max( |
|
0, |
|
min(sixel->width, |
|
term->width - x - term->margins.right)); |
|
const int height = max( |
|
0, |
|
min( |
|
min(count * term->cell_height, /* 'count' number of rows */ |
|
sixel->height - img_start_row * term->cell_height), /* What remains of the sixel */ |
|
term->height - y - term->margins.bottom)); |
|
|
|
/* Verify we're not stepping outside the grid */ |
|
xassert(x >= term->margins.left); |
|
xassert(y >= term->margins.top); |
|
xassert(width == 0 || x + width <= term->width - term->margins.right); |
|
xassert(height == 0 || y + height <= term->height - term->margins.bottom); |
|
|
|
//LOG_DBG("sixel chunk: %dx%d %dx%d", x, y, width, height); |
|
|
|
pixman_image_composite32( |
|
sixel->opaque ? PIXMAN_OP_SRC : PIXMAN_OP_OVER, |
|
sixel->pix, |
|
NULL, |
|
pix, |
|
0, img_start_row * term->cell_height, |
|
0, 0, |
|
x, y, |
|
width, height); |
|
|
|
wl_surface_damage_buffer(term->window->surface, x, y, width, height); |
|
} |
|
|
|
static void |
|
render_sixel(struct terminal *term, pixman_image_t *pix, |
|
const struct coord *cursor, const struct sixel *sixel) |
|
{ |
|
const int view_end = (term->grid->view + term->rows - 1) & (term->grid->num_rows - 1); |
|
const bool last_row_needs_erase = sixel->height % term->cell_height != 0; |
|
const bool last_col_needs_erase = sixel->width % term->cell_width != 0; |
|
|
|
int chunk_img_start = -1; /* Image-relative start row of chunk */ |
|
int chunk_term_start = -1; /* Viewport relative start row of chunk */ |
|
int chunk_row_count = 0; /* Number of rows to emit */ |
|
|
|
#define maybe_emit_sixel_chunk_then_reset() \ |
|
if (chunk_row_count != 0) { \ |
|
render_sixel_chunk( \ |
|
term, pix, sixel, \ |
|
chunk_term_start, chunk_img_start, chunk_row_count); \ |
|
chunk_term_start = chunk_img_start = -1; \ |
|
chunk_row_count = 0; \ |
|
} |
|
|
|
/* |
|
* Iterate all sixel rows: |
|
* |
|
* - ignore rows that aren't visible on-screen |
|
* - ignore rows that aren't dirty (they have already been rendered) |
|
* - chunk consecutive dirty rows into a 'chunk' |
|
* - emit (render) chunk as soon as a row isn't visible, or is clean |
|
* - emit final chunk after we've iterated all rows |
|
* |
|
* The purpose of this is to reduce the amount of pixels that |
|
* needs to be composited and marked as damaged for the |
|
* compositor. |
|
* |
|
* Since we do CPU based composition, rendering is a slow and |
|
* heavy task for foot, and thus it is important to not re-render |
|
* things unnecessarily. |
|
*/ |
|
|
|
for (int _abs_row_no = sixel->pos.row; |
|
_abs_row_no < sixel->pos.row + sixel->rows; |
|
_abs_row_no++) |
|
{ |
|
const int abs_row_no = _abs_row_no & (term->grid->num_rows - 1); |
|
const int term_row_no = |
|
(abs_row_no - term->grid->view + term->grid->num_rows) & |
|
(term->grid->num_rows - 1); |
|
|
|
/* Check if row is in the visible viewport */ |
|
if (view_end >= term->grid->view) { |
|
/* Not wrapped */ |
|
if (!(abs_row_no >= term->grid->view && abs_row_no <= view_end)) { |
|
/* Not visible */ |
|
maybe_emit_sixel_chunk_then_reset(); |
|
continue; |
|
} |
|
} else { |
|
/* Wrapped */ |
|
if (!(abs_row_no >= term->grid->view || abs_row_no <= view_end)) { |
|
/* Not visible */ |
|
maybe_emit_sixel_chunk_then_reset(); |
|
continue; |
|
} |
|
} |
|
|
|
/* Is the row dirty? */ |
|
struct row *row = term->grid->rows[abs_row_no]; |
|
xassert(row != NULL); /* Should be visible */ |
|
|
|
if (!row->dirty) { |
|
maybe_emit_sixel_chunk_then_reset(); |
|
continue; |
|
} |
|
|
|
int cursor_col = cursor->row == term_row_no ? cursor->col : -1; |
|
|
|
/* |
|
* If image contains transparent parts, render all (dirty) |
|
* cells beneath it. |
|
* |
|
* If image is opaque, loop cells and set their 'clean' bit, |
|
* to prevent the grid rendered from overwriting the sixel |
|
* |
|
* If the last sixel row only partially covers the cell row, |
|
* 'erase' the cell by rendering them. |
|
* |
|
* In all cases, do *not* clear the ‘dirty’ bit on the row, to |
|
* ensure the regular renderer includes them in the damage |
|
* rect. |
|
*/ |
|
if (!sixel->opaque) { |
|
/* TODO: multithreading */ |
|
int cursor_col = cursor->row == term_row_no ? cursor->col : -1; |
|
render_row(term, pix, row, term_row_no, cursor_col); |
|
} else { |
|
for (int col = sixel->pos.col; |
|
col < min(sixel->pos.col + sixel->cols, term->cols); |
|
col++) |
|
{ |
|
struct cell *cell = &row->cells[col]; |
|
|
|
if (!cell->attrs.clean) { |
|
bool last_row = abs_row_no == sixel->pos.row + sixel->rows - 1; |
|
bool last_col = col == sixel->pos.col + sixel->cols - 1; |
|
|
|
if ((last_row_needs_erase && last_row) || |
|
(last_col_needs_erase && last_col)) |
|
{ |
|
render_cell(term, pix, row, col, term_row_no, cursor_col == col); |
|
} else { |
|
cell->attrs.clean = 1; |
|
cell->attrs.confined = 1; |
|
} |
|
} |
|
} |
|
} |
|
|
|
if (chunk_term_start == -1) { |
|
xassert(chunk_img_start == -1); |
|
chunk_term_start = term_row_no; |
|
chunk_img_start = _abs_row_no - sixel->pos.row; |
|
chunk_row_count = 1; |
|
} else |
|
chunk_row_count++; |
|
} |
|
|
|
maybe_emit_sixel_chunk_then_reset(); |
|
#undef maybe_emit_sixel_chunk_then_reset |
|
} |
|
|
|
static void |
|
render_sixel_images(struct terminal *term, pixman_image_t *pix, |
|
const struct coord *cursor) |
|
{ |
|
if (likely(tll_length(term->grid->sixel_images)) == 0) |
|
return; |
|
|
|
const int scrollback_end |
|
= (term->grid->offset + term->rows) & (term->grid->num_rows - 1); |
|
|
|
const int view_start |
|
= (term->grid->view |
|
- scrollback_end |
|
+ term->grid->num_rows) & (term->grid->num_rows - 1); |
|
|
|
const int view_end = view_start + term->rows - 1; |
|
|
|
//LOG_DBG("SIXELS: %zu images, view=%d-%d", |
|
// tll_length(term->grid->sixel_images), view_start, view_end); |
|
|
|
tll_foreach(term->grid->sixel_images, it) { |
|
const struct sixel *six = &it->item; |
|
const int start |
|
= (six->pos.row |
|
- scrollback_end |
|
+ term->grid->num_rows) & (term->grid->num_rows - 1); |
|
const int end = start + six->rows - 1; |
|
|
|
//LOG_DBG(" sixel: %d-%d", start, end); |
|
if (start > view_end) { |
|
/* Sixel starts after view ends, no need to try to render it */ |
|
continue; |
|
} else if (end < view_start) { |
|
/* Image ends before view starts. Since the image list is |
|
* sorted, we can safely stop here */ |
|
break; |
|
} |
|
|
|
render_sixel(term, pix, cursor, &it->item); |
|
} |
|
} |
|
|
|
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED |
|
static void |
|
render_ime_preedit_for_seat(struct terminal *term, struct seat *seat, |
|
struct buffer *buf) |
|
{ |
|
if (likely(seat->ime.preedit.cells == NULL)) |
|
return; |
|
|
|
if (unlikely(term->is_searching)) |
|
return; |
|
|
|
/* Adjust cursor position to viewport */ |
|
struct coord cursor; |
|
cursor = term->grid->cursor.point; |
|
cursor.row += term->grid->offset; |
|
cursor.row -= term->grid->view; |
|
cursor.row &= term->grid->num_rows - 1; |
|
|
|
if (cursor.row < 0 || cursor.row >= term->rows) |
|
return; |
|
|
|
int cells_needed = seat->ime.preedit.count; |
|
|
|
if (seat->ime.preedit.cursor.start == cells_needed && |
|
seat->ime.preedit.cursor.end == cells_needed) |
|
{ |
|
/* Cursor will be drawn *after* the pre-edit string, i.e. in |
|
* the cell *after*. This means we need to copy, and dirty, |
|
* one extra cell from the original grid, or we’ll leave |
|
* trailing “cursors” after us if the user deletes text while |
|
* pre-editing */ |
|
cells_needed++; |
|
} |
|
|
|
int row_idx = cursor.row; |
|
int col_idx = cursor.col; |
|
int ime_ofs = 0; /* Offset into pre-edit string to start rendering at */ |
|
|
|
int cells_left = term->cols - cursor.col; |
|
int cells_used = min(cells_needed, term->cols); |
|
|
|
/* Adjust start of pre-edit text to the left if string doesn't fit on row */ |
|
if (cells_left < cells_used) |
|
col_idx -= cells_used - cells_left; |
|
|
|
if (cells_needed > cells_used) { |
|
int start = seat->ime.preedit.cursor.start; |
|
int end = seat->ime.preedit.cursor.end; |
|
|
|
if (start == end) { |
|
/* Ensure *end* of pre-edit string is visible */ |
|
ime_ofs = cells_needed - cells_used; |
|
} else { |
|
/* Ensure the *beginning* of the cursor-area is visible */ |
|
ime_ofs = start; |
|
|
|
/* Display as much as possible of the pre-edit string */ |
|
if (cells_needed - ime_ofs < cells_used) |
|
ime_ofs = cells_needed - cells_used; |
|
} |
|
|
|
/* Make sure we don't start in the middle of a character */ |
|
while (ime_ofs < cells_needed && |
|
seat->ime.preedit.cells[ime_ofs].wc >= CELL_SPACER) |
|
{ |
|
ime_ofs++; |
|
} |
|
} |
|
|
|
xassert(col_idx >= 0); |
|
xassert(col_idx < term->cols); |
|
|
|
struct row *row = grid_row_in_view(term->grid, row_idx); |
|
|
|
/* Don't start pre-edit text in the middle of a double-width character */ |
|
while (col_idx > 0 && row->cells[col_idx].wc >= CELL_SPACER) { |
|
cells_used++; |
|
col_idx--; |
|
} |
|
|
|
/* |
|
* Copy original content (render_cell() reads cell data directly |
|
* from grid), and mark all cells as dirty. This ensures they are |
|
* re-rendered when the pre-edit text is modified or removed. |
|
*/ |
|
struct cell *real_cells = malloc(cells_used * sizeof(real_cells[0])); |
|
for (int i = 0; i < cells_used; i++) { |
|
xassert(col_idx + i < term->cols); |
|
real_cells[i] = row->cells[col_idx + i]; |
|
real_cells[i].attrs.clean = 0; |
|
} |
|
row->dirty = true; |
|
|
|
/* Render pre-edit text */ |
|
xassert(seat->ime.preedit.cells[ime_ofs].wc < CELL_SPACER); |
|
for (int i = 0, idx = ime_ofs; idx < seat->ime.preedit.count; i++, idx++) { |
|
const struct cell *cell = &seat->ime.preedit.cells[idx]; |
|
|
|
if (cell->wc >= CELL_SPACER) |
|
continue; |
|
|
|
int width = max(1, wcwidth(cell->wc)); |
|
if (col_idx + i + width > term->cols) |
|
break; |
|
|
|
row->cells[col_idx + i] = *cell; |
|
render_cell(term, buf->pix[0], row, col_idx + i, row_idx, false); |
|
} |
|
|
|
int start = seat->ime.preedit.cursor.start - ime_ofs; |
|
int end = seat->ime.preedit.cursor.end - ime_ofs; |
|
|
|
if (!seat->ime.preedit.cursor.hidden) { |
|
const struct cell *start_cell = &seat->ime.preedit.cells[0]; |
|
|
|
pixman_color_t fg = color_hex_to_pixman(term->colors.fg); |
|
pixman_color_t bg = color_hex_to_pixman(term->colors.bg); |
|
|
|
pixman_color_t cursor_color, text_color; |
|
cursor_colors_for_cell( |
|
term, start_cell, &fg, &bg, &cursor_color, &text_color); |
|
|
|
int x = term->margins.left + (col_idx + start) * term->cell_width; |
|
int y = term->margins.top + row_idx * term->cell_height; |
|
|
|
if (end == start) { |
|
/* Bar */ |
|
if (start >= 0) { |
|
struct fcft_font *font = attrs_to_font(term, &start_cell->attrs); |
|
draw_beam_cursor(term, buf->pix[0], font, &cursor_color, x, y); |
|
} |
|
term_ime_set_cursor_rect(term, x, y, 1, term->cell_height); |
|
} |
|
|
|
else if (end > start) { |
|
/* Hollow cursor */ |
|
if (start >= 0 && end <= term->cols) { |
|
int cols = end - start; |
|
draw_unfocused_block(term, buf->pix[0], &cursor_color, x, y, cols); |
|
} |
|
|
|
term_ime_set_cursor_rect( |
|
term, x, y, (end - start) * term->cell_width, term->cell_height); |
|
} |
|
} |
|
|
|
/* Restore original content (but do not render) */ |
|
for (int i = 0; i < cells_used; i++) |
|
row->cells[col_idx + i] = real_cells[i]; |
|
free(real_cells); |
|
|
|
wl_surface_damage_buffer( |
|
term->window->surface, |
|
term->margins.left, |
|
term->margins.top + row_idx * term->cell_height, |
|
term->width - term->margins.left - term->margins.right, |
|
1 * term->cell_height); |
|
} |
|
#endif |
|
|
|
static void |
|
render_ime_preedit(struct terminal *term, struct buffer *buf) |
|
{ |
|
#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED |
|
tll_foreach(term->wl->seats, it) { |
|
if (it->item.kbd_focus == term) |
|
render_ime_preedit_for_seat(term, &it->item, buf); |
|
} |
|
#endif |
|
} |
|
|
|
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); |
|
|
|
sigset_t mask; |
|
sigfillset(&mask); |
|
pthread_sigmask(SIG_SETMASK, &mask, NULL); |
|
|
|
char proc_title[16]; |
|
snprintf(proc_title, sizeof(proc_title), "foot:render:%d", my_id); |
|
|
|
if (pthread_setname_np(pthread_self(), proc_title) < 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; |
|
|
|
while (true) { |
|
sem_wait(start); |
|
|
|
struct buffer *buf = term->render.workers.buf; |
|
bool frame_done = false; |
|
|
|
/* Translate offset-relative cursor row to view-relative */ |
|
struct coord cursor = {-1, -1}; |
|
if (!term->hide_cursor) { |
|
cursor = term->grid->cursor.point; |
|
cursor.row += term->grid->offset; |
|
cursor.row -= term->grid->view; |
|
cursor.row &= term->grid->num_rows - 1; |
|
} |
|
|
|
while (!frame_done) { |
|
mtx_lock(lock); |
|
xassert(tll_length(term->render.workers.queue) > 0); |
|
|
|
int row_no = tll_pop_front(term->render.workers.queue); |
|
mtx_unlock(lock); |
|
|
|
switch (row_no) { |
|
default: { |
|
xassert(buf != NULL); |
|
|
|
struct row *row = grid_row_in_view(term->grid, row_no); |
|
int cursor_col = cursor.row == row_no ? cursor.col : -1; |
|
|
|
render_row(term, buf->pix[my_id], row, row_no, cursor_col); |
|
break; |
|
} |
|
|
|
case -1: |
|
frame_done = true; |
|
sem_post(done); |
|
break; |
|
|
|
case -2: |
|
return 0; |
|
} |
|
} |
|
}; |
|
|
|
return -1; |
|
} |
|
|
|
struct csd_data { |
|
int x; |
|
int y; |
|
int width; |
|
int height; |
|
}; |
|
|
|
static struct csd_data |
|
get_csd_data(const struct terminal *term, enum csd_surface surf_idx) |
|
{ |
|
xassert(term->window->csd_mode == CSD_YES); |
|
|
|
/* Only title bar is rendered in maximized mode */ |
|
const int border_width = !term->window->is_maximized |
|
? term->conf->csd.border_width * term->scale : 0; |
|
|
|
const int title_height = term->window->is_fullscreen |
|
? 0 |
|
: term->conf->csd.title_height * term->scale; |
|
|
|
const int button_width = !term->window->is_fullscreen |
|
? term->conf->csd.button_width * term->scale : 0; |
|
|
|
const int button_close_width = term->width >= 1 * button_width |
|
? button_width : 0; |
|
|
|
const int button_maximize_width = term->width >= 2 * button_width |
|
? button_width : 0; |
|
|
|
const int button_minimize_width = term->width >= 3 * button_width |
|
? button_width : 0; |
|
|
|
switch (surf_idx) { |
|
case CSD_SURF_TITLE: return (struct csd_data){ 0, -title_height, term->width, title_height}; |
|
case CSD_SURF_LEFT: return (struct csd_data){-border_width, -title_height, border_width, title_height + term->height}; |
|
case CSD_SURF_RIGHT: return (struct csd_data){ term->width, -title_height, border_width, title_height + term->height}; |
|
case CSD_SURF_TOP: return (struct csd_data){-border_width, -title_height - border_width, term->width + 2 * border_width, border_width}; |
|
case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, term->width + 2 * border_width, border_width}; |
|
|
|
/* Positioned relative to CSD_SURF_TITLE */ |
|
case CSD_SURF_MINIMIZE: return (struct csd_data){term->width - 3 * button_width, 0, button_minimize_width, title_height}; |
|
case CSD_SURF_MAXIMIZE: return (struct csd_data){term->width - 2 * button_width, 0, button_maximize_width, title_height}; |
|
case CSD_SURF_CLOSE: return (struct csd_data){term->width - 1 * button_width, 0, button_close_width, title_height}; |
|
|
|
case CSD_SURF_COUNT: |
|
break; |
|
} |
|
|
|
BUG("Invalid csd_surface type"); |
|
return (struct csd_data){0}; |
|
} |
|
|
|
static void |
|
csd_commit(struct terminal *term, struct wl_surface *surf, struct buffer *buf) |
|
{ |
|
xassert(buf->width % term->scale == 0); |
|
xassert(buf->height % term->scale == 0); |
|
|
|
wl_surface_attach(surf, buf->wl_buf, 0, 0); |
|
wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); |
|
wl_surface_set_buffer_scale(surf, term->scale); |
|
wl_surface_commit(surf); |
|
} |
|
|
|
static void |
|
render_csd_part(struct terminal *term, |
|
struct wl_surface *surf, struct buffer *buf, |
|
int width, int height, pixman_color_t *color) |
|
{ |
|
xassert(term->window->csd_mode == CSD_YES); |
|
|
|
pixman_image_t *src = pixman_image_create_solid_fill(color); |
|
|
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, buf->pix[0], color, 1, |
|
&(pixman_rectangle16_t){0, 0, buf->width, buf->height}); |
|
pixman_image_unref(src); |
|
} |
|
|
|
static void |
|
render_osd(struct terminal *term, |
|
struct wl_surface *surf, struct wl_subsurface *sub_surf, |
|
struct fcft_font *font, struct buffer *buf, |
|
const wchar_t *text, uint32_t _fg, uint32_t _bg, |
|
unsigned x, unsigned y) |
|
{ |
|
pixman_region32_t clip; |
|
pixman_region32_init_rect(&clip, 0, 0, buf->width, buf->height); |
|
pixman_image_set_clip_region32(buf->pix[0], &clip); |
|
pixman_region32_fini(&clip); |
|
|
|
uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); |
|
pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha); |
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, buf->pix[0], &bg, 1, |
|
&(pixman_rectangle16_t){0, 0, buf->width, buf->height}); |
|
|
|
pixman_color_t fg = color_hex_to_pixman(_fg); |
|
const int x_ofs = term->font_x_ofs; |
|
|
|
const size_t len = wcslen(text); |
|
struct fcft_text_run *text_run = NULL; |
|
const struct fcft_glyph **glyphs = NULL; |
|
const struct fcft_glyph *_glyphs[len]; |
|
size_t glyph_count = 0; |
|
|
|
if (fcft_capabilities() & FCFT_CAPABILITY_TEXT_RUN_SHAPING) { |
|
text_run = fcft_text_run_rasterize(font, len, text, term->font_subpixel); |
|
|
|
if (text_run != NULL) { |
|
glyphs = text_run->glyphs; |
|
glyph_count = text_run->count; |
|
} |
|
} |
|
|
|
if (glyphs == NULL) { |
|
for (size_t i = 0; i < len; i++) { |
|
const struct fcft_glyph *glyph = fcft_glyph_rasterize( |
|
font, text[i], term->font_subpixel); |
|
|
|
if (glyph == NULL) |
|
continue; |
|
|
|
_glyphs[glyph_count++] = glyph; |
|
} |
|
|
|
glyphs = _glyphs; |
|
} |
|
|
|
pixman_image_t *src = pixman_image_create_solid_fill(&fg); |
|
|
|
for (size_t i = 0; i < glyph_count; i++) { |
|
const struct fcft_glyph *glyph = glyphs[i]; |
|
|
|
if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { |
|
pixman_image_composite32( |
|
PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, |
|
x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent - glyph->y, |
|
glyph->width, glyph->height); |
|
} else { |
|
pixman_image_composite32( |
|
PIXMAN_OP_OVER, src, glyph->pix, buf->pix[0], 0, 0, 0, 0, |
|
x + x_ofs + glyph->x, y + term->font_y_ofs + font->ascent - glyph->y, |
|
glyph->width, glyph->height); |
|
} |
|
|
|
x += glyph->advance.x; |
|
} |
|
|
|
fcft_text_run_destroy(text_run); |
|
pixman_image_unref(src); |
|
pixman_image_set_clip_region32(buf->pix[0], NULL); |
|
|
|
xassert(buf->width % term->scale == 0); |
|
xassert(buf->height % term->scale == 0); |
|
|
|
quirk_weston_subsurface_desync_on(sub_surf); |
|
wl_surface_attach(surf, buf->wl_buf, 0, 0); |
|
wl_surface_damage_buffer(surf, 0, 0, buf->width, buf->height); |
|
wl_surface_set_buffer_scale(surf, term->scale); |
|
|
|
struct wl_region *region = wl_compositor_create_region(term->wl->compositor); |
|
if (region != NULL) { |
|
wl_region_add(region, 0, 0, buf->width, buf->height); |
|
wl_surface_set_opaque_region(surf, region); |
|
wl_region_destroy(region); |
|
} |
|
|
|
wl_surface_commit(surf); |
|
quirk_weston_subsurface_desync_off(sub_surf); |
|
} |
|
|
|
static void |
|
render_csd_title(struct terminal *term, const struct csd_data *info, |
|
struct buffer *buf) |
|
{ |
|
xassert(term->window->csd_mode == CSD_YES); |
|
|
|
struct wl_surf_subsurf *surf = &term->window->csd.surface[CSD_SURF_TITLE]; |
|
if (info->width == 0 || info->height == 0) |
|
return; |
|
|
|
xassert(info->width % term->scale == 0); |
|
xassert(info->height % term->scale == 0); |
|
|
|
uint32_t bg = term->conf->csd.color.title_set |
|
? term->conf->csd.color.title |
|
: 0xffu << 24 | term->conf->colors.fg; |
|
uint32_t fg = term->conf->csd.color.buttons_set |
|
? term->conf->csd.color.buttons |
|
: term->conf->colors.bg; |
|
|
|
if (!term->visual_focus) { |
|
bg = color_dim(bg); |
|
fg = color_dim(fg); |
|
} |
|
|
|
const wchar_t *title_text = L""; |
|
wchar_t *_title_text = NULL; |
|
|
|
int chars = mbstowcs(NULL, term->window_title, 0); |
|
if (chars >= 0) { |
|
_title_text = xmalloc((chars + 1) * sizeof(wchar_t)); |
|
mbstowcs(_title_text, term->window_title, chars + 1); |
|
title_text = _title_text; |
|
} |
|
|
|
struct wl_window *win = term->window; |
|
const int margin = win->csd.font->space_advance.x > 0 |
|
? win->csd.font->space_advance.x |
|
: win->csd.font->max_advance.x; |
|
|
|
render_osd(term, surf->surf, surf->sub, win->csd.font, |
|
buf, title_text, fg, bg, margin, |
|
(buf->height - win->csd.font->height) / 2); |
|
|
|
csd_commit(term, surf->surf, buf); |
|
free(_title_text); |
|
} |
|
|
|
static void |
|
render_csd_border(struct terminal *term, enum csd_surface surf_idx, |
|
const struct csd_data *info, struct buffer *buf) |
|
{ |
|
xassert(term->window->csd_mode == CSD_YES); |
|
xassert(surf_idx >= CSD_SURF_LEFT && surf_idx <= CSD_SURF_BOTTOM); |
|
|
|
struct wl_surface *surf = term->window->csd.surface[surf_idx].surf; |
|
|
|
if (info->width == 0 || info->height == 0) |
|
return; |
|
|
|
xassert(info->width % term->scale == 0); |
|
xassert(info->height % term->scale == 0); |
|
|
|
pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0); |
|
render_csd_part(term, surf, buf, info->width, info->height, &color); |
|
csd_commit(term, surf, buf); |
|
} |
|
|
|
static pixman_color_t |
|
get_csd_button_fg_color(const struct config *conf) |
|
{ |
|
uint32_t _color = conf->colors.bg; |
|
uint16_t alpha = 0xffff; |
|
|
|
if (conf->csd.color.buttons_set) { |
|
_color = conf->csd.color.buttons; |
|
alpha = _color >> 24 | (_color >> 24 << 8); |
|
} |
|
|
|
return color_hex_to_pixman_with_alpha(_color, alpha); |
|
} |
|
|
|
static void |
|
render_csd_button_minimize(struct terminal *term, struct buffer *buf) |
|
{ |
|
pixman_color_t color = get_csd_button_fg_color(term->conf); |
|
pixman_image_t *src = pixman_image_create_solid_fill(&color); |
|
|
|
const int max_height = buf->height / 2; |
|
const int max_width = buf->width / 2; |
|
|
|
int width = max_width; |
|
int height = max_width / 2; |
|
|
|
if (height > max_height) { |
|
height = max_height; |
|
width = height * 2; |
|
} |
|
|
|
xassert(width <= max_width); |
|
xassert(height <= max_height); |
|
|
|
int x_margin = (buf->width - width) / 2.; |
|
int y_margin = (buf->height - height) / 2.; |
|
|
|
pixman_triangle_t tri = { |
|
.p1 = { |
|
.x = pixman_int_to_fixed(x_margin), |
|
.y = pixman_int_to_fixed(y_margin), |
|
}, |
|
.p2 = { |
|
.x = pixman_int_to_fixed(x_margin + width), |
|
.y = pixman_int_to_fixed(y_margin), |
|
}, |
|
.p3 = { |
|
.x = pixman_int_to_fixed(buf->width / 2), |
|
.y = pixman_int_to_fixed(y_margin + height), |
|
}, |
|
}; |
|
|
|
pixman_composite_triangles( |
|
PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, |
|
0, 0, 0, 0, 1, &tri); |
|
pixman_image_unref(src); |
|
} |
|
|
|
static void |
|
render_csd_button_maximize_maximized( |
|
struct terminal *term, struct buffer *buf) |
|
{ |
|
pixman_color_t color = get_csd_button_fg_color(term->conf); |
|
pixman_image_t *src = pixman_image_create_solid_fill(&color); |
|
|
|
const int max_height = buf->height / 3; |
|
const int max_width = buf->width / 3; |
|
|
|
int width = min(max_height, max_width); |
|
int thick = 1 * term->scale; |
|
|
|
const int x_margin = (buf->width - width) / 2; |
|
const int y_margin = (buf->height - width) / 2; |
|
|
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, buf->pix[0], &color, 4, |
|
(pixman_rectangle16_t[]){ |
|
{x_margin, y_margin, width, thick}, |
|
{x_margin, y_margin + thick, thick, width - 2 * thick}, |
|
{x_margin + width - thick, y_margin + thick, thick, width - 2 * thick}, |
|
{x_margin, y_margin + width - thick, width, thick}}); |
|
|
|
pixman_image_unref(src); |
|
|
|
} |
|
|
|
static void |
|
render_csd_button_maximize_window( |
|
struct terminal *term, struct buffer *buf) |
|
{ |
|
pixman_color_t color = get_csd_button_fg_color(term->conf); |
|
pixman_image_t *src = pixman_image_create_solid_fill(&color); |
|
|
|
const int max_height = buf->height / 2; |
|
const int max_width = buf->width / 2; |
|
|
|
int width = max_width; |
|
int height = max_width / 2; |
|
|
|
if (height > max_height) { |
|
height = max_height; |
|
width = height * 2; |
|
} |
|
|
|
xassert(width <= max_width); |
|
xassert(height <= max_height); |
|
|
|
int x_margin = (buf->width - width) / 2.; |
|
int y_margin = (buf->height - height) / 2.; |
|
|
|
pixman_triangle_t tri = { |
|
.p1 = { |
|
.x = pixman_int_to_fixed(buf->width / 2), |
|
.y = pixman_int_to_fixed(y_margin), |
|
}, |
|
.p2 = { |
|
.x = pixman_int_to_fixed(x_margin), |
|
.y = pixman_int_to_fixed(y_margin + height), |
|
}, |
|
.p3 = { |
|
.x = pixman_int_to_fixed(x_margin + width), |
|
.y = pixman_int_to_fixed(y_margin + height), |
|
}, |
|
}; |
|
|
|
pixman_composite_triangles( |
|
PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, |
|
0, 0, 0, 0, 1, &tri); |
|
|
|
pixman_image_unref(src); |
|
} |
|
|
|
static void |
|
render_csd_button_maximize(struct terminal *term, struct buffer *buf) |
|
{ |
|
if (term->window->is_maximized) |
|
render_csd_button_maximize_maximized(term, buf); |
|
else |
|
render_csd_button_maximize_window(term, buf); |
|
} |
|
|
|
static void |
|
render_csd_button_close(struct terminal *term, struct buffer *buf) |
|
{ |
|
pixman_color_t color = get_csd_button_fg_color(term->conf); |
|
pixman_image_t *src = pixman_image_create_solid_fill(&color); |
|
|
|
const int max_height = buf->height / 3; |
|
const int max_width = buf->width / 3; |
|
|
|
int width = min(max_height, max_width); |
|
|
|
const int x_margin = (buf->width - width) / 2; |
|
const int y_margin = (buf->height - width) / 2; |
|
|
|
pixman_image_fill_rectangles( |
|
PIXMAN_OP_SRC, buf->pix[0], &color, 1, |
|
&(pixman_rectangle16_t){x_margin, y_margin, width, width}); |
|
|
|
pixman_image_unref(src); |
|
} |
|
|
|
static void |
|
render_csd_button(struct terminal *term, enum csd_surface surf_idx, |
|
const struct csd_data *info, struct buffer *buf) |
|
{ |
|
xassert(term->window->csd_mode == CSD_YES); |
|
xassert(surf_idx >= CSD_SURF_MINIMIZE && surf_idx <= CSD_SURF_CLOSE); |
|
|
|
struct wl_surface *surf = term->window->csd.surface[surf_idx].surf; |
|
|
|
if (info->width == 0 || info->height == 0) |
|
return; |
|
|
|
xassert(info->width % term->scale == 0); |
|
xassert(info->height % term->scale == 0); |
|
|
|
uint32_t _color; |
|
uint16_t alpha = 0xffff; |
|
bool is_active = false; |
|
bool is_set = false; |
|
const uint32_t *conf_color = NULL; |
|
|
|
switch (surf_idx) { |
|
case CSD_SURF_MINIMIZE: |
|
_color = term->conf->colors.table[4]; /* blue */ |
|
is_set = term->conf->csd.color.minimize_set; |
|
conf_color = &term->conf->csd.color.minimize; |
|
is_active = term->active_surface == TERM_SURF_BUTTON_MINIMIZE; |
|
break; |
|
|
|
case CSD_SURF_MAXIMIZE: |
|
_color = term->conf->colors.table[2]; /* green */ |
|
is_set = term->conf->csd.color.maximize_set; |
|
conf_color = &term->conf->csd.color.maximize; |
|
is_active = term->active_surface == TERM_SURF_BUTTON_MAXIMIZE; |
|
break; |
|
|
|
case CSD_SURF_CLOSE: |
|
_color = term->conf->colors.table[1]; /* red */ |
|
is_set = term->conf->csd.color.close_set; |
|
conf_color = &term->conf->csd.color.close; |
|
is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE; |
|
break; |
|
|
|
default: |
|
BUG("unhandled surface type: %u", (unsigned)surf_idx); |
|
break; |
|
} |
|
|
|
if (is_active) { |
|
if (is_set) { |
|
_color = *conf_color; |
|
alpha = _color >> 24 | (_color >> 24 << 8); |
|
} |
|
} else { |
|
_color = 0; |
|
alpha = 0; |
|
} |
|
|
|
if (!term->visual_focus) |
|
_color = color_dim(_color); |
|
|
|
pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha); |
|
render_csd_part(term, surf, buf, info->width, info->height, &color); |
|
|
|
switch (surf_idx) { |
|
case CSD_SURF_MINIMIZE: render_csd_button_minimize(term, buf); break; |
|
case CSD_SURF_MAXIMIZE: render_csd_button_maximize(term, buf); break; |
|
case CSD_SURF_CLOSE: render_csd_button_close(term, buf); break; |
|
break; |
|
|
|
default: |
|
BUG("unhandled surface type: %u", (unsigned)surf_idx); |
|
break; |
|
} |
|
|
|
csd_commit(term, surf, buf); |
|
} |
|
|
|
static void |
|
render_csd(struct terminal *term) |
|
{ |
|
xassert(term->window->csd_mode == CSD_YES); |
|
|
|
if (term->window->is_fullscreen) |
|
return; |
|
|
|
struct csd_data infos[CSD_SURF_COUNT]; |
|
int widths[CSD_SURF_COUNT]; |
|
int heights[CSD_SURF_COUNT]; |
|
|
|
for (size_t i = 0; i < CSD_SURF_COUNT; i++) { |
|
infos[i] = get_csd_data(term, i); |
|
const int x = infos[i].x; |
|
const int y = infos[i].y; |
|
const int width = infos[i].width; |
|
const int height = infos[i].height; |
|
|
|
struct wl_surface *surf = term->window->csd.surface[i].surf; |
|
struct wl_subsurface *sub = term->window->csd.surface[i].sub; |
|
|
|
xassert(surf != NULL); |
|
xassert(sub != NULL); |
|
|
|
if (width == 0 || height == 0) { |
|
widths[i] = heights[i] = 0; |
|
wl_subsurface_set_position(sub, 0, 0); |
|
wl_surface_attach(surf, NULL, 0, 0); |
|
wl_surface_commit(surf); |
|
continue; |
|
} |
|
|
|
widths[i] = width; |
|
heights[i] = height; |
|
|
|
wl_subsurface_set_position(sub, x / term->scale, y / term->scale); |
|
} |
|
|
|
struct buffer *bufs[CSD_SURF_COUNT]; |
|
shm_get_many(term->render.chains.csd, CSD_SURF_COUNT, widths, heights, bufs); |
|
|
|
for (size_t i = CSD_SURF_LEFT; i <= CSD_SURF_BOTTOM; i++) |
|
render_csd_border(term, i, &infos[i], bufs[i]); |
|
for (size_t i = CSD_SURF_MINIMIZE; i <= CSD_SURF_CLOSE; i++) |
|
render_csd_button(term, i, &infos[i], bufs[i]); |
|
render_csd_title(term, &infos[CSD_SURF_TITLE], bufs[CSD_SURF_TITLE]); |
|
} |
|
|
|
static void |
|
render_scrollback_position(struct terminal *term) |
|
{ |
|
if (term->conf->scrollback.indicator.position == SCROLLBACK_INDICATOR_POSITION_NONE) |
|
return; |
|
|
|
struct wl_window *win = term->window; |
|
|
|
if (term->grid->view == term->grid->offset) { |
|
if (win->scrollback_indicator.surf != NULL) |
|
wayl_win_subsurface_destroy(&win->scrollback_indicator); |
|
return; |
|
} |
|
|
|
if (win->scrollback_indicator.surf == NULL) { |
|
if (!wayl_win_subsurface_new(win, &win->scrollback_indicator)) { |
|
LOG_ERR("failed to create scrollback indicator surface"); |
|
return; |
|
} |
|
} |
|
|
|
xassert(win->scrollback_indicator.surf != NULL); |
|
xassert(win->scrollback_indicator.sub != NULL); |
|
|
|
/* Find absolute row number of the scrollback start */ |
|
int scrollback_start = term->grid->offset + term->rows; |
|
int empty_rows = 0; |
|
while (term->grid->rows[scrollback_start & (term->grid->num_rows - 1)] == NULL) { |
|
scrollback_start++; |
|
empty_rows++; |
|
} |
|
|
|
/* Rebase viewport against scrollback start (so that 0 is at |
|
* the beginning of the scrollback) */ |
|
int rebased_view = term->grid->view - scrollback_start + term->grid->num_rows; |
|
rebased_view &= term->grid->num_rows - 1; |
|
|
|
/* How much of the scrollback is actually used? */ |
|
int populated_rows = term->grid->num_rows - empty_rows; |
|
xassert(populated_rows > 0); |
|
xassert(populated_rows <= term->grid->num_rows); |
|
|
|
/* |
|
* How far down in the scrollback we are. |
|
* |
|
* 0% -> at the beginning of the scrollback |
|
* 100% -> at the bottom, i.e. where new lines are inserted |
|
*/ |
|
double percent = |
|
rebased_view + term->rows == populated_rows |
|
? 1.0 |
|
: (double)rebased_view / (populated_rows - term->rows); |
|
|
|
wchar_t _text[64]; |
|
const wchar_t *text = _text; |
|
int cell_count = 0; |
|
|
|
/* *What* to render */ |
|
switch (term->conf->scrollback.indicator.format) { |
|
case SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE: |
|
swprintf(_text, sizeof(_text) / sizeof(_text[0]), L"%u%%", (int)(100 * percent)); |
|
cell_count = 3; |
|
break; |
|
|
|
case SCROLLBACK_INDICATOR_FORMAT_LINENO: |
|
swprintf(_text, sizeof(_text) / sizeof(_text[0]), L"%d", rebased_view + 1); |
|
cell_count = 1 + (int)log10(term->grid->num_rows); |
|
break; |
|
|
|
case SCROLLBACK_INDICATOR_FORMAT_TEXT: |
|
text = term->conf->scrollback.indicator.text; |
|
cell_count = wcslen(text); |
|
break; |
|
} |
|
|
|
const int scale = term->scale; |
|
const int margin = 3 * scale; |
|
|
|
const int width = |
|
(2 * margin + cell_count * term->cell_width + scale - 1) / scale * scale; |
|
const int height = |
|
(2 * margin + term->cell_height + scale - 1) / scale * scale; |
|
|
|
/* *Where* to render - parent relative coordinates */ |
|
int surf_top = 0; |
|
switch (term->conf->scrollback.indicator.position) { |
|
case SCROLLBACK_INDICATOR_POSITION_NONE: |
|
BUG("Invalid scrollback indicator position type"); |
|
return; |
|
|
|
case SCROLLBACK_INDICATOR_POSITION_FIXED: |
|
surf_top = term->cell_height - margin; |
|
break; |
|
|
|
case SCROLLBACK_INDICATOR_POSITION_RELATIVE: { |
|
int lines = term->rows - 2; /* Avoid using first and last rows */ |
|
if (term->is_searching) { |
|
/* Make sure we don't collide with the scrollback search box */ |
|
lines--; |
|
} |
|
|
|
lines = max(lines, 0); |
|
|
|
int pixels = max(lines * term->cell_height - height + 2 * margin, 0); |
|
surf_top = term->cell_height - margin + (int)(percent * pixels); |
|
break; |
|
} |
|
} |
|
|
|
const int x = (term->width - margin - width) / scale * scale; |
|
const int y = (term->margins.top + surf_top) / scale * scale; |
|
|
|
if (y + height > term->height) { |
|
wl_surface_attach(win->scrollback_indicator.surf, NULL, 0, 0); |
|
wl_surface_commit(win->scrollback_indicator.surf); |
|
return; |
|
} |
|
|
|
struct buffer_chain *chain = term->render.chains.scrollback_indicator; |
|
struct buffer *buf = shm_get_buffer(chain, width, height); |
|
|
|
wl_subsurface_set_position( |
|
win->scrollback_indicator.sub, x / scale, y / scale); |
|
|
|
render_osd( |
|
term, |
|
win->scrollback_indicator.surf, |
|
win->scrollback_indicator.sub, |
|
term->fonts[0], buf, text, |
|
term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 4], |
|
width - margin - wcslen(text) * term->cell_width, margin); |
|
} |
|
|
|
static void |
|
render_render_timer(struct terminal *term, struct timeval render_time) |
|
{ |
|
struct wl_window *win = term->window; |
|
|
|
wchar_t text[256]; |
|
double usecs = render_time.tv_sec * 1000000 + render_time.tv_usec; |
|
swprintf(text, sizeof(text) / sizeof(text[0]), L"%.2f µs", usecs); |
|
|
|
const int scale = term->scale; |
|
const int cell_count = wcslen(text); |
|
const int margin = 3 * scale; |
|
const int width = |
|
(2 * margin + cell_count * term->cell_width + scale - 1) / scale * scale; |
|
const int height = |
|
(2 * margin + term->cell_height + scale - 1) / scale * scale; |
|
|
|
struct buffer_chain *chain = term->render.chains.render_timer; |
|
struct buffer *buf = shm_get_buffer(chain, width, height); |
|
|
|
wl_subsurface_set_position( |
|
win->render_timer.sub, |
|
margin / term->scale, |
|
(term->margins.top + term->cell_height - margin) / term->scale); |
|
|
|
render_osd( |
|
term, |
|
win->render_timer.surf, |
|
win->render_timer.sub, |
|
term->fonts[0], buf, text, |
|
term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], |
|
margin, margin); |
|
} |
|
|
|
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, |
|
}; |
|
|
|
static void |
|
force_full_repaint(struct terminal *term, struct buffer *buf) |
|
{ |
|
tll_free(term->grid->scroll_damage); |
|
render_margin(term, buf, 0, term->rows, true); |
|
term_damage_view(term); |
|
} |
|
|
|
static void |
|
reapply_old_damage(struct terminal *term, struct buffer *new, struct buffer *old) |
|
{ |
|
static int counter = 0; |
|
static bool have_warned = false; |
|
if (!have_warned && ++counter > 5) { |
|
LOG_WARN("compositor is not releasing buffers immediately; " |
|
"expect lower rendering performance"); |
|
have_warned = true; |
|
} |
|
|
|
if (new->age > 1) { |
|
memcpy(new->data, old->data, new->height * new->stride); |
|
return; |
|
} |
|
|
|
/* |
|
* TODO: remove this frame’s damage from the region we copy from |
|
* the old frame. |
|
* |
|
* - this frame’s dirty region is only valid *after* we’ve applied |
|
* its scroll damage. |
|
* - last frame’s dirty region is only valid *before* we’ve |
|
* applied this frame’s scroll damage. |
|
* |
|
* Can we transform one of the regions? It’s not trivial, since |
|
* scroll damage isn’t just about counting lines; there may be |
|
* multiple damage records, each with different scrolling regions. |
|
*/ |
|
pixman_region32_t dirty; |
|
pixman_region32_init(&dirty); |
|
|
|
bool full_repaint_needed = true; |
|
|
|
for (int r = 0; r < term->rows; r++) { |
|
const struct row *row = grid_row_in_view(term->grid, r); |
|
|
|
bool row_all_dirty = true; |
|
for (int c = 0; c < term->cols; c++) { |
|
if (row->cells[c].attrs.clean) { |
|
row_all_dirty = false; |
|
full_repaint_needed = false; |
|
break; |
|
} |
|
} |
|
|
|
if (row_all_dirty) { |
|
pixman_region32_union_rect( |
|
&dirty, &dirty, |
|
term->margins.left, |
|
term->margins.top + r * term->cell_height, |
|
term->width - term->margins.left - term->margins.right, |
|
term->cell_height); |
|
} |
|
} |
|
|
|
if (full_repaint_needed) { |
|
force_full_repaint(term, new); |
|
return; |
|
} |
|
|