#include "render.h" #include #include #include #include #include #include #include #include #include #include "macros.h" #if HAS_INCLUDE() #include #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 #include #include #include #include #define LOG_MODULE "render" #define LOG_ENABLE_DBG 0 #include "box-drawing.h" #include "char32.h" #include "config.h" #include "cursor-shape.h" #include "grid.h" #include "ime.h" #include "log.h" #include "quirks.h" #include "search.h" #include "selection.h" #include "shm.h" #include "sixel.h" #include "srgb.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); static void render_tab_bar(struct terminal *term); static void render_tab_overview(struct terminal *term); 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 pixman_color_t color_hex_to_pixman_srgb(uint32_t color, uint16_t alpha) { return (pixman_color_t){ .alpha = alpha, /* Consider alpha linear already? */ .red = srgb_decode_8_to_16((color >> 16) & 0xff), .green = srgb_decode_8_to_16((color >> 8) & 0xff), .blue = srgb_decode_8_to_16((color >> 0) & 0xff), }; } static inline pixman_color_t color_hex_to_pixman_with_alpha(uint32_t color, uint16_t alpha, bool srgb) { pixman_color_t ret; if (srgb) ret = color_hex_to_pixman_srgb(color, alpha); else { ret = (pixman_color_t){ .red = ((color >> 16 & 0xff) | (color >> 8 & 0xff00)), .green = ((color >> 8 & 0xff) | (color >> 0 & 0xff00)), .blue = ((color >> 0 & 0xff) | (color << 8 & 0xff00)), .alpha = alpha, }; } ret.red = (uint32_t)ret.red * alpha / 0xffff; ret.green = (uint32_t)ret.green * alpha / 0xffff; ret.blue = (uint32_t)ret.blue * alpha / 0xffff; return ret; } static inline pixman_color_t color_hex_to_pixman(uint32_t color, bool srgb) { /* Count on the compiler optimizing this */ return color_hex_to_pixman_with_alpha(color, 0xffff, srgb); } static inline int i_lerp(int from, int to, float t) { return from + (to - from) * t; } static inline uint32_t color_blend_towards(uint32_t from, uint32_t to, float amount) { if (unlikely(amount == 0)) return from; float t = 1 - 1 / amount; uint32_t alpha = from & 0xff000000; uint8_t r = i_lerp((from >> 16) & 0xff, (to >> 16) & 0xff, t); uint8_t g = i_lerp((from >> 8) & 0xff, (to >> 8) & 0xff, t); uint8_t b = i_lerp((from >> 0) & 0xff, (to >> 0) & 0xff, t); return alpha | (r << 16) | (g << 8) | (b << 0); } static inline uint32_t color_dim(const struct terminal *term, uint32_t color) { const struct config *conf = term->conf; const uint8_t custom_dim = conf->colors_dark.use_custom.dim; if (unlikely(custom_dim != 0)) { for (size_t i = 0; i < 8; i++) { if (((custom_dim >> i) & 1) == 0) continue; if (term->colors.table[0 + i] == color) { /* "Regular" color, return the corresponding "dim" */ return conf->colors_dark.dim[i]; } else if (term->colors.table[8 + i] == color) { /* "Bright" color, return the corresponding "regular" */ return term->colors.table[i]; } } } const struct color_theme *theme = term_theme_get(term); return color_blend_towards(color, theme->dim_blend_towards == DIM_BLEND_TOWARDS_BLACK ? 0x00000000 : 0x00ffffff, conf->dim.amount); } 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; } return color_blend_towards(color, 0x00ffffff, term->conf->bold_in_bright.amount); } static void draw_hollow_block(const struct terminal *term, pixman_image_t *pix, const pixman_color_t *color, int x, int y, int cell_cols) { const int scale = (int)roundf(term->scale); const int width = min(min(scale, term->cell_width), term->cell_height); pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 4, (pixman_rectangle16_t[]){ {x, y, cell_cols * term->cell_width, width}, /* top */ {x, y, width, term->cell_height}, /* left */ {x + cell_cols * term->cell_width - width, y, width, term->cell_height}, /* right */ {x, y + term->cell_height - width, cell_cols * term->cell_width, width}, /* 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 + term->font_baseline - 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 int underline_offset(const struct terminal *term, const struct fcft_font *font) { return term->font_baseline - (term->conf->use_custom_underline_offset ? -term_pt_or_px_as_pixels(term, &term->conf->underline_offset) : font->underline.position); } 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; /* Make sure the line isn't positioned below the cell */ const int y_ofs = min(underline_offset(term, font) + thickness, 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(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, int x, int y, int cols) { const int thickness = term->conf->underline_thickness.px >= 0 ? term_pt_or_px_as_pixels(term, &term->conf->underline_thickness) : font->underline.thickness; /* Make sure the line isn't positioned below the cell */ const int y_ofs = min(underline_offset(term, font), 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_styled_underline(const struct terminal *term, pixman_image_t *pix, const struct fcft_font *font, const pixman_color_t *color, enum underline_style style, int x, int y, int cols) { xassert(style != UNDERLINE_NONE); if (style == UNDERLINE_SINGLE) { draw_underline(term, pix, font, color, x, y, cols); return; } const int thickness = term->conf->underline_thickness.px >= 0 ? term_pt_or_px_as_pixels(term, &term->conf->underline_thickness) : font->underline.thickness; int y_ofs; /* Make sure the line isn't positioned below the cell */ switch (style) { case UNDERLINE_DOUBLE: case UNDERLINE_CURLY: y_ofs = min(underline_offset(term, font), term->cell_height - thickness * 3); break; case UNDERLINE_DASHED: case UNDERLINE_DOTTED: y_ofs = min(underline_offset(term, font), term->cell_height - thickness); break; case UNDERLINE_NONE: case UNDERLINE_SINGLE: default: BUG("unexpected underline style: %d", (int)style); return; } const int ceil_w = cols * term->cell_width; switch (style) { case UNDERLINE_DOUBLE: { const pixman_rectangle16_t rects[] = { {x, y + y_ofs, ceil_w, thickness}, {x, y + y_ofs + thickness * 2, ceil_w, thickness}}; pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); break; } case UNDERLINE_DASHED: { const int ceil_w = cols * term->cell_width; const int dash_w = ceil_w / 3 + (ceil_w % 3 > 0); const pixman_rectangle16_t rects[] = { {x, y + y_ofs, dash_w, thickness}, {x + dash_w * 2, y + y_ofs, dash_w, thickness}, }; pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 2, rects); break; } case UNDERLINE_DOTTED: { /* Number of dots per cell */ int per_cell = (term->cell_width / thickness) / 2; if (per_cell == 0) per_cell = 1; xassert(per_cell >= 1); /* Spacing between dots; start with the same width as the dots themselves, then widen them if necessary, to consume unused pixels */ int spacing[per_cell]; for (int i = 0; i < per_cell; i++) spacing[i] = thickness; /* Pixels remaining at the end of the cell */ int remaining = term->cell_width - (per_cell * 2) * thickness; /* Spread out the left-over pixels across the spacing between the dots */ for (int i = 0; remaining > 0; i = (i + 1) % per_cell, remaining--) spacing[i]++; xassert(remaining <= 0); pixman_rectangle16_t rects[per_cell]; int dot_x = x; for (int i = 0; i < per_cell; i++) { rects[i] = (pixman_rectangle16_t){dot_x, y + y_ofs, thickness, thickness}; dot_x += thickness + spacing[i]; } pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, per_cell, rects); break; } case UNDERLINE_CURLY: { const int top = y + y_ofs; const int bot = top + thickness * 3; const int half_x = x + ceil_w / 2.0, full_x = x + ceil_w; const double bt_2 = (bot - top) * (bot - top); const double th_2 = thickness * thickness; const double hx_2 = ceil_w * ceil_w / 4.0; const int th = round(sqrt(th_2 + (th_2 * bt_2 / hx_2)) / 2.); #define I(x) pixman_int_to_fixed(x) const pixman_trapezoid_t traps[] = { #if 0 /* characters sit within the "dips" of the curlies */ { I(top), I(bot), {{I(x), I(top + th)}, {I(half_x), I(bot + th)}}, {{I(x), I(top - th)}, {I(half_x), I(bot - th)}}, }, { I(top), I(bot), {{I(half_x), I(bot - th)}, {I(full_x), I(top - th)}}, {{I(half_x), I(bot + th)}, {I(full_x), I(top + th)}}, } #else /* characters sit on top of the curlies */ { I(top), I(bot), {{I(x), I(bot - th)}, {I(half_x), I(top - th)}}, {{I(x), I(bot + th)}, {I(half_x), I(top + th)}}, }, { I(top), I(bot), {{I(half_x), I(top + th)}, {I(full_x), I(bot + th)}}, {{I(half_x), I(top - th)}, {I(full_x), I(bot - th)}}, } #endif }; pixman_image_t *fill = pixman_image_create_solid_fill(color); pixman_composite_trapezoids(PIXMAN_OP_OVER, fill, pix, PIXMAN_a8, 0, 0, 0, 0, sizeof(traps) / sizeof(traps[0]), traps); pixman_image_unref(fill); break; } case UNDERLINE_NONE: case UNDERLINE_SINGLE: BUG("underline styles not supposed to be handled here"); break; } } 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) { const int thickness = term->conf->strikeout_thickness.px >= 0 ? term_pt_or_px_as_pixels(term, &term->conf->strikeout_thickness) : font->strikeout.thickness; /* Try to center custom strikeout */ const int position = term->conf->strikeout_thickness.px >= 0 ? font->strikeout.position - round(font->strikeout.thickness / 2.) + round(thickness / 2.) : font->strikeout.position; pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x, y + term->font_baseline - position, cols * term->cell_width, 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 gamma_correct) { if (term->colors.cursor_bg >> 31) *cursor_color = color_hex_to_pixman(term->colors.cursor_bg, gamma_correct); else *cursor_color = *fg; if (term->colors.cursor_fg >> 31) *text_color = color_hex_to_pixman(term->colors.cursor_fg, gamma_correct); else { xassert(bg->alpha == 0xffff); *text_color = *bg; } if (text_color->red == cursor_color->red && text_color->green == cursor_color->green && text_color->blue == cursor_color->blue) { *text_color = color_hex_to_pixman(term->colors.bg, gamma_correct); *cursor_color = color_hex_to_pixman(term->colors.fg, gamma_correct); } } 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, wayl_do_linear_blending(term->wl, term->conf)); if (unlikely(!term->kbd_focus)) { switch (term->conf->cursor.unfocused_style) { case CURSOR_UNFOCUSED_UNCHANGED: break; case CURSOR_UNFOCUSED_HOLLOW: draw_hollow_block(term, pix, &cursor_color, x, y, cols); return; case CURSOR_UNFOCUSED_NONE: return; } } switch (term->cursor_style) { case CURSOR_BLOCK: if (likely(term->cursor_blink.state == CURSOR_BLINK_ON) || !term->kbd_focus) { *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; case CURSOR_HOLLOW: if (likely(term->cursor_blink.state == CURSOR_BLINK_ON)) draw_hollow_block(term, pix, &cursor_color, x, y, cols); break; } } static int render_cell(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, struct row *row, int row_no, int col, 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; uint32_t _fg = 0; uint32_t _bg = 0; uint16_t alpha = 0xffff; const bool is_selected = cell->attrs.selected; /* Use cell specific color, if set, otherwise the default colors (possible * reversed) */ switch (cell->attrs.fg_src) { case COLOR_RGB: _fg = cell->attrs.fg; break; case COLOR_BASE16: case COLOR_BASE256: xassert(cell->attrs.fg < ALEN(term->colors.table)); _fg = term->colors.table[cell->attrs.fg]; break; case COLOR_DEFAULT: _fg = term->reverse ? term->colors.bg : term->colors.fg; break; } switch (cell->attrs.bg_src) { case COLOR_RGB: _bg = cell->attrs.bg; break; case COLOR_BASE16: case COLOR_BASE256: xassert(cell->attrs.bg < ALEN(term->colors.table)); _bg = term->colors.table[cell->attrs.bg]; break; case COLOR_DEFAULT: _bg = term->reverse ? term->colors.fg : term->colors.bg; break; } if (unlikely(is_selected)) { const uint32_t cell_fg = _fg; const uint32_t cell_bg = _bg; const bool custom_fg = term->colors.selection_fg >> 24 == 0; const bool custom_bg = term->colors.selection_bg >> 24 == 0; const bool custom_both = custom_fg && custom_bg; if (custom_both) { _fg = term->colors.selection_fg; _bg = term->colors.selection_bg; } else if (custom_bg) { _bg = term->colors.selection_bg; _fg = cell->attrs.reverse ? cell_bg : cell_fg; } else if (custom_fg) { _fg = term->colors.selection_fg; _bg = cell->attrs.reverse ? cell_fg : cell_bg; } else { _bg = cell_fg; _fg = cell_bg; } if (unlikely(_fg == _bg)) { /* Invert bg when selected/highlighted text has same fg/bg */ _bg = ~_bg; alpha = 0xffff; } } else { if (unlikely(cell->attrs.reverse)) { uint32_t swap = _fg; _fg = _bg; _bg = swap; } else if (!term->window->is_fullscreen && term->colors.alpha != 0xffff) { switch (term->conf->colors_dark.alpha_mode) { case ALPHA_MODE_DEFAULT: { if (cell->attrs.bg_src == COLOR_DEFAULT) { alpha = term->colors.alpha; } break; } case ALPHA_MODE_MATCHING: { if (cell->attrs.bg_src == COLOR_DEFAULT || ((cell->attrs.bg_src == COLOR_BASE16 || cell->attrs.bg_src == COLOR_BASE256) && term->colors.table[cell->attrs.bg] == term->colors.bg) || (cell->attrs.bg_src == COLOR_RGB && cell->attrs.bg == term->colors.bg)) { alpha = term->colors.alpha; } break; } case ALPHA_MODE_ALL: { alpha = term->colors.alpha; break; } } } else { /* * Note: disable transparency when fullscreened. * * This is because the wayland protocol mandates no screen * content is shown behind the fullscreened window. * * The _intent_ of the specification is that a black (or * other static color) should be used as background. * * There's a bit of gray area however, and some * compositors have chosen to interpret the specification * in a way that allows wallpapers to be seen through a * fullscreen window. * * Given that a) the intent of the specification, and b) * we don't know what the compositor will do, we simply * disable transparency while in fullscreen. * * To see why, consider what happens if we keep our * transparency. For example, if the background color is * white, and alpha is 0.5, then the window will be drawn * in a shade of gray while fullscreened. * * See * https://gitlab.freedesktop.org/wayland/wayland-protocols/-/issues/116 * for a discussion on whether transparent, fullscreen * windows should be allowed in some way or not. * * NOTE: if changing this, also update render_margin() */ xassert(alpha == 0xffff); } } if (cell->attrs.dim) _fg = color_dim(term, _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_blend_towards(_fg, 0x00000000, term->conf->dim.amount); const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); pixman_color_t fg = color_hex_to_pixman(_fg, gamma_correct); pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); 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; char32_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) || /* * Unicode 16 "Symbols for Legacy Computing Supplement" * * Note, the full range is U+1CC00 - U+1CEAF */ (base >= GLYPH_OCTANTS_FIRST && base <= GLYPH_OCTANTS_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_OCTANTS_FIRST) { arr = &term->custom_glyphs.octants; count = GLYPH_OCTANTS_COUNT; idx = base - GLYPH_OCTANTS_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_rasterize_grapheme_utf32( font, composed->count, composed->chars, term->font_subpixel); } if (grapheme != NULL) { const int forced_width = composed->forced_width; cell_cols = forced_width > 0 ? forced_width : composed->width; composed = NULL; glyphs = grapheme->glyphs; glyph_count = grapheme->count; if (forced_width > 0) glyph_count = min(glyph_count, forced_width); } } if (single == NULL && grapheme == NULL) { if (unlikely(base >= CELL_SPACER)) { glyph_count = 0; cell_cols = 1; } else { xassert(base != 0); single = fcft_rasterize_char_utf32(font, base, term->font_subpixel); if (single == NULL) { glyph_count = 0; cell_cols = 1; } else { glyph_count = 1; glyphs = &single; const size_t forced_width = composed != NULL ? composed->forced_width : 0; cell_cols = forced_width > 0 ? forced_width : 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); if (damage != NULL) { pixman_region32_union_rect(damage, damage, x, y, render_width, term->cell_height); } 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 (unlikely(has_cursor && term->cursor_style == CURSOR_BLOCK && term->kbd_focus)) { const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, x, y, cell_cols); } if (cell->wc == 0 || cell->wc >= CELL_SPACER || cell->wc == U'\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 && cell_cols == 1) g_x -= term->cell_width; if (unlikely(glyph->is_color_glyph)) { /* 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 + term->font_baseline - 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 + term->font_baseline - g_y, glyph->width, glyph->height); /* Combining characters */ if (composed != NULL) { assert(glyph_count == 1); for (size_t j = 1; j < composed->count; j++) { const struct fcft_glyph *g = fcft_rasterize_char_utf32( font, composed->chars[j], 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 = cell_cols == 1 ? g->x < 0 ? cell_cols * term->cell_width : (cell_cols - 1) * term->cell_width : 0; if (cell_cols > 1) pen_x += 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 + letter_x_ofs + x_ofs + g->x, y + term->font_baseline - g->y, g->width, g->height); } } } pen_x += cell_cols > 1 ? term->cell_width : glyph->advance.x; } pixman_image_unref(clr_pix); /* Underline */ if (cell->attrs.underline) { pixman_color_t underline_color = fg; enum underline_style underline_style = UNDERLINE_SINGLE; /* Check if cell has a styled underline. This lookup is fairly expensive... */ if (row->extra != NULL) { for (int i = 0; i < row->extra->underline_ranges.count; i++) { const struct row_range *range = &row->extra->underline_ranges.v[i]; if (range->start > col) break; if (range->start <= col && col <= range->end) { switch (range->underline.color_src) { case COLOR_BASE256: underline_color = color_hex_to_pixman( term->colors.table[range->underline.color], gamma_correct); break; case COLOR_RGB: underline_color = color_hex_to_pixman(range->underline.color, gamma_correct); break; case COLOR_DEFAULT: break; case COLOR_BASE16: BUG("underline color can't be base-16"); break; } underline_style = range->underline.style; break; } } } draw_styled_underline(term, pix, font, &underline_color, underline_style, x, y, cell_cols); } if (cell->attrs.strikethrough) draw_strikeout(term, pix, font, &fg, x, y, cell_cols); if (unlikely(cell->attrs.url && term->conf->url.style != UNDERLINE_NONE)) { pixman_color_t url_color = color_hex_to_pixman( term->conf->colors_dark.use_custom.url ? term->conf->colors_dark.url : term->colors.table[3], gamma_correct); draw_styled_underline(term, pix, font, &url_color, term->conf->url.style, x, y, cell_cols); } draw_cursor: if (has_cursor && (term->cursor_style != CURSOR_BLOCK || !term->kbd_focus)) { const pixman_color_t bg_without_alpha = color_hex_to_pixman(_bg, gamma_correct); draw_cursor(term, cell, font, pix, &fg, &bg_without_alpha, 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, pixman_region32_t *damage, struct row *row, int row_no, int cursor_col) { for (int col = term->cols - 1; col >= 0; col--) render_cell(term, pix, damage, row, row_no, col, cursor_col == col); } static void render_urgency(struct terminal *term, struct buffer *buf) { uint32_t red = term->colors.table[1]; pixman_color_t bg = color_hex_to_pixman(red, wayl_do_linear_blending(term->wl, term->conf)); 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; const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); const uint32_t _bg = !term->reverse ? term->colors.bg : term->colors.fg; uint16_t alpha = term->colors.alpha; if (term->window->is_fullscreen) { /* Disable alpha in fullscreen - see render_cell() for details */ alpha = 0xffff; } pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); 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[0], &buf->dirty[0], 0, 0, term->width, term->margins.top); pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, bmargin, term->width, term->margins.bottom); pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, 0, term->margins.left, term->height); pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], rmargin, 0, term->margins.right, term->height); if (apply_damage) { /* Top */ wl_surface_damage_buffer(term->window->surface.surf, 0, 0, term->width, term->margins.top); /* Bottom */ wl_surface_damage_buffer(term->window->surface.surf, 0, bmargin, term->width, term->margins.bottom); /* Left */ wl_surface_damage_buffer(term->window->surface.surf, 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.surf, 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) { LOG_DBG("damage: SCROLL: %d-%d by %d lines", dmg->region.start, dmg->region.end, dmg->lines); const int region_size = dmg->region.end - dmg->region.start; if (dmg->lines >= region_size) { /* The entire scroll region will be scrolled out (i.e. replaced) */ return; } const int height = (region_size - dmg->lines) * term->cell_height; xassert(height > 0); #if TIME_SCROLL_DAMAGE struct timespec start_time; clock_gettime(CLOCK_MONOTONIC, &start_time); #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 timespec end_time; clock_gettime(CLOCK_MONOTONIC, &end_time); struct timespec memmove_time; timespec_sub(&end_time, &start_time, &memmove_time); LOG_INFO("scrolled %dKB (%d lines) using %s in %lds %ldns", height * buf->stride / 1024, dmg->lines, did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", (long)memmove_time.tv_sec, memmove_time.tv_nsec); #endif wl_surface_damage_buffer( term->window->surface.surf, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); /* * TODO: remove this if re-enabling scroll damage when re-applying * last frame's damage (see reapply_old_damage() */ pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); } static void grid_render_scroll_reverse(struct terminal *term, struct buffer *buf, const struct damage *dmg) { LOG_DBG("damage: SCROLL REVERSE: %d-%d by %d lines", dmg->region.start, dmg->region.end, dmg->lines); const int region_size = dmg->region.end - dmg->region.start; if (dmg->lines >= region_size) { /* The entire scroll region will be scrolled out (i.e. replaced) */ return; } const int height = (region_size - dmg->lines) * term->cell_height; xassert(height > 0); #if TIME_SCROLL_DAMAGE struct timespec start_time; clock_gettime(CLOCK_MONOTONIC, &start_time); #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 timespec end_time; clock_gettime(CLOCK_MONOTONIC, &end_time); struct timespec memmove_time; timespec_sub(&end_time, &start_time, &memmove_time); LOG_INFO("scrolled REVERSE %dKB (%d lines) using %s in %lds %ldns", height * buf->stride / 1024, dmg->lines, did_shm_scroll ? "SHM" : try_shm_scroll ? "memmove (SHM failed)" : "memmove", (long)memmove_time.tv_sec, memmove_time.tv_nsec); #endif wl_surface_damage_buffer( term->window->surface.surf, term->margins.left, dst_y, term->width - term->margins.left - term->margins.right, height); /* * TODO: remove this if re-enabling scroll damage when re-applying * last frame's damage (see reapply_old_damage() */ pixman_region32_union_rect(&buf->dirty[0], &buf->dirty[0], 0, dst_y, buf->width, height); } static void render_sixel_chunk(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, 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); if (damage != NULL) pixman_region32_union_rect(damage, damage, x, y, width, height); } static void render_sixel(struct terminal *term, pixman_image_t *pix, pixman_region32_t *damage, const struct coord *cursor, const struct sixel *sixel) { xassert(sixel->pix != NULL); xassert(sixel->width >= 0); xassert(sixel->height >= 0); 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, damage, 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 */ render_row(term, pix, damage, 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, damage, row, term_row_no, col, 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, pixman_region32_t *damage, 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; } sixel_sync_cache(term, &it->item); render_sixel(term, pix, damage, 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; const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); /* 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 = xmalloc(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, c32width(cell->wc)); if (col_idx + i + width > term->cols) break; row->cells[col_idx + i] = *cell; render_cell(term, buf->pix[0], NULL, row, row_idx, col_idx + i, 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, gamma_correct); pixman_color_t bg = color_hex_to_pixman(term->colors.bg, gamma_correct); pixman_color_t cursor_color, text_color; cursor_colors_for_cell(term, start_cell, &fg, &bg, &cursor_color, &text_color, gamma_correct); 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_hollow_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); const int damage_x = term->margins.left + col_idx * term->cell_width; const int damage_y = term->margins.top + row_idx * term->cell_height; const int damage_w = cells_used * term->cell_width; const int damage_h = term->cell_height; wl_surface_damage_buffer(term->window->surface.surf, damage_x, damage_y, damage_w, damage_h); } #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 } static void render_overlay_single_pixel(struct terminal *term, enum overlay_style style, pixman_color_t color) { struct wayland *wayl = term->wl; struct wayl_sub_surface *overlay = &term->window->overlay; struct wl_buffer *buf = NULL; /* * In an ideal world, we'd only update the surface (i.e. commit * any changes) if anything has actually changed. * * For technical reasons, we can't do that, since we can't * determine whether the last committed buffer is still valid * (i.e. does it correspond to the current overlay style, *and* * does last frame's size match the current size?) * * What we _can_ do is use the fact that single-pixel buffers * don't have a size; you have to use a viewport to "size" them. * * This means we can check if the last frame's overlay style is * the same as the current size. If so, then we *know* that the * currently attached buffer is valid, and we *don't* have to * create a new single-pixel buffer. * * What we do *not* know if the *size* is still valid. This means * we do have to do the viewport calls, and a surface commit. * * This is still better than *always* creating a new buffer. */ assert(style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH); assert(wayl->single_pixel_manager != NULL); assert(overlay->surface.viewport != NULL); quirk_weston_subsurface_desync_on(overlay->sub); if (style != term->render.last_overlay_style) { buf = wp_single_pixel_buffer_manager_v1_create_u32_rgba_buffer( wayl->single_pixel_manager, (double)color.red / 0xffff * 0xffffffff, (double)color.green / 0xffff * 0xffffffff, (double)color.blue / 0xffff * 0xffffffff, (double)color.alpha / 0xffff * 0xffffffff); wl_surface_set_buffer_scale(overlay->surface.surf, 1); wl_surface_attach(overlay->surface.surf, buf, 0, 0); } wp_viewport_set_destination(overlay->surface.viewport, roundf(term->width / term->scale), roundf(term->height / term->scale)); wl_subsurface_set_position(overlay->sub, 0, 0); wl_surface_damage_buffer(overlay->surface.surf, 0, 0, term->width, term->height); wl_surface_commit(overlay->surface.surf); quirk_weston_subsurface_desync_off(overlay->sub); term->render.last_overlay_style = style; if (buf != NULL) { wl_buffer_destroy(buf); } } void render_overlay(struct terminal *term) { struct wayl_sub_surface *overlay = &term->window->overlay; const bool unicode_mode_active = term->unicode_mode.active; const enum overlay_style style = term->flash.active ? OVERLAY_FLASH : unicode_mode_active ? OVERLAY_UNICODE_MODE : OVERLAY_NONE; if (likely(style == OVERLAY_NONE)) { if (term->render.last_overlay_style != OVERLAY_NONE) { /* Unmap overlay sub-surface */ wl_surface_attach(overlay->surface.surf, NULL, 0, 0); wl_surface_commit(overlay->surface.surf); term->render.last_overlay_style = OVERLAY_NONE; term->render.last_overlay_buf = NULL; } return; } pixman_color_t color; switch (style) { case OVERLAY_SEARCH: case OVERLAY_UNICODE_MODE: color = (pixman_color_t){0, 0, 0, 0x7fff}; break; case OVERLAY_FLASH: color = color_hex_to_pixman_with_alpha( term->conf->colors_dark.flash, term->conf->colors_dark.flash_alpha, wayl_do_linear_blending(term->wl, term->conf)); break; case OVERLAY_NONE: xassert(false); break; } const bool single_pixel = (style == OVERLAY_UNICODE_MODE || style == OVERLAY_FLASH) && term->wl->single_pixel_manager != NULL && overlay->surface.viewport != NULL; if (single_pixel) { render_overlay_single_pixel(term, style, color); return; } struct buffer *buf = shm_get_buffer(term->render.chains.overlay, term->width, term->height); pixman_image_set_clip_region32(buf->pix[0], NULL); /* Bounding rectangle of damaged areas - for wl_surface_damage_buffer() */ pixman_box32_t damage_bounds; if (style == OVERLAY_SEARCH) { /* * When possible, we only update the areas that have *changed* * since the last frame. That means: * * - clearing/erasing cells that are now selected, but weren't * in the last frame * - dimming cells that were selected, but aren't anymore * * To do this, we save the last frame's selected cells as a * pixman region. * * Then, we calculate the corresponding region for this * frame's selected cells. * * Last frame's region minus this frame's region gives us the * region that needs to be *dimmed* in this frame * * This frame's region minus last frame's region gives us the * region that needs to be *cleared* in this frame. * * Finally, the union of the two "diff" regions above, gives * us the total region affected by a change, in either way. We * use this as the bounding box for the * wl_surface_damage_buffer() call. */ pixman_region32_t *see_through = &term->render.last_overlay_clip; pixman_region32_t old_see_through; const bool buffer_reuse = buf == term->render.last_overlay_buf && style == term->render.last_overlay_style && buf->age == 0; if (!buffer_reuse) { /* Can't reuse last frame's damage - set to full window, * to ensure *everything* is updated */ pixman_region32_init_rect(&old_see_through, 0, 0, buf->width, buf->height); } else { /* Use last frame's saved region */ pixman_region32_init(&old_see_through); pixman_region32_copy(&old_see_through, see_through); } pixman_region32_clear(see_through); /* Build region consisting of all current search matches */ struct search_match_iterator iter = search_matches_new_iter(term); for (struct range match = search_matches_next(&iter); match.start.row >= 0; match = search_matches_next(&iter)) { int r = match.start.row; int start_col = match.start.col; const int end_row = match.end.row; while (true) { const int end_col = r == end_row ? match.end.col : term->cols - 1; int x = term->margins.left + start_col * term->cell_width; int y = term->margins.top + r * term->cell_height; int width = (end_col + 1 - start_col) * term->cell_width; int height = 1 * term->cell_height; pixman_region32_union_rect(see_through, see_through, x, y, width, height); if (++r > end_row) break; start_col = 0; } } /* Areas that need to be cleared: cells that were dimmed in * the last frame but is now see-through */ pixman_region32_t new_see_through; pixman_region32_init(&new_see_through); if (buffer_reuse) pixman_region32_subtract(&new_see_through, see_through, &old_see_through); else { /* Buffer content is unknown - explicitly clear *all* * current see-through areas */ pixman_region32_copy(&new_see_through, see_through); } pixman_image_set_clip_region32(buf->pix[0], &new_see_through); /* Areas that need to be dimmed: cells that were cleared in * the last frame but is not anymore */ pixman_region32_t new_dimmed; pixman_region32_init(&new_dimmed); pixman_region32_subtract(&new_dimmed, &old_see_through, see_through); pixman_region32_fini(&old_see_through); /* Total affected area */ pixman_region32_t damage; pixman_region32_init(&damage); pixman_region32_union(&damage, &new_see_through, &new_dimmed); damage_bounds = damage.extents; /* Clear cells that became selected in this frame. */ pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &(pixman_color_t){0}, 1, &(pixman_rectangle16_t){0, 0, term->width, term->height}); /* Set clip region for the newly dimmed cells. The actual * paint call is done below */ pixman_image_set_clip_region32(buf->pix[0], &new_dimmed); pixman_region32_fini(&new_see_through); pixman_region32_fini(&new_dimmed); pixman_region32_fini(&damage); } else if (buf == term->render.last_overlay_buf && style == term->render.last_overlay_style) { xassert(style == OVERLAY_FLASH || style == OVERLAY_UNICODE_MODE); shm_did_not_use_buf(buf); return; } else { pixman_image_set_clip_region32(buf->pix[0], NULL); damage_bounds = (pixman_box32_t){0, 0, buf->width, buf->height}; } pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){0, 0, term->width, term->height}); quirk_weston_subsurface_desync_on(overlay->sub); wayl_surface_scale(term->window, &overlay->surface, buf, term->scale); wl_subsurface_set_position(overlay->sub, 0, 0); wl_surface_attach(overlay->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer( overlay->surface.surf, damage_bounds.x1, damage_bounds.y1, damage_bounds.x2 - damage_bounds.x1, damage_bounds.y2 - damage_bounds.y1); wl_surface_commit(overlay->surface.surf); quirk_weston_subsurface_desync_off(overlay->sub); buf->age = 0; term->render.last_overlay_buf = buf; term->render.last_overlay_style = style; } 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: { 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], &buf->dirty[my_id], row, row_no, cursor_col); break; } case -1: frame_done = true; sem_post(done); break; case -2: return 0; case -3: { if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.start); mtx_lock(&term->render.workers.preapplied_damage.lock); buf = term->render.workers.preapplied_damage.buf; xassert(buf != NULL); if (likely(term->render.last_buf != NULL)) { mtx_unlock(&term->render.workers.preapplied_damage.lock); pixman_region32_t dmg; pixman_region32_init(&dmg); if (buf->age == 0) ; /* No need to do anything */ else if (buf->age == 1) pixman_region32_copy(&dmg, &term->render.last_buf->dirty[0]); else pixman_region32_init_rect(&dmg, 0, 0, buf->width, buf->height); pixman_image_set_clip_region32(buf->pix[my_id], &dmg); pixman_image_composite32( PIXMAN_OP_SRC, term->render.last_buf->pix[my_id], NULL, buf->pix[my_id], 0, 0, 0, 0, 0, 0, buf->width, buf->height); pixman_region32_fini(&dmg); buf->age = 0; shm_unref(term->render.last_buf); shm_addref(buf); term->render.last_buf = buf; mtx_lock(&term->render.workers.preapplied_damage.lock); } term->render.workers.preapplied_damage.buf = NULL; cnd_signal(&term->render.workers.preapplied_damage.cond); mtx_unlock(&term->render.workers.preapplied_damage.lock); if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) clock_gettime(CLOCK_MONOTONIC, &term->render.workers.preapplied_damage.stop); frame_done = true; break; } } } }; return -1; } void render_wait_for_preapply_damage(struct terminal *term) { if (!term->render.preapply_last_frame_damage) return; if (term->render.workers.preapplied_damage.buf == NULL) return; mtx_lock(&term->render.workers.preapplied_damage.lock); while (term->render.workers.preapplied_damage.buf != NULL) { cnd_wait(&term->render.workers.preapplied_damage.cond, &term->render.workers.preapplied_damage.lock); } mtx_unlock(&term->render.workers.preapplied_damage.lock); } struct csd_data get_csd_data(const struct terminal *term, enum csd_surface surf_idx) { xassert(term->window->csd_mode == CSD_YES); const bool borders_visible = wayl_win_csd_borders_visible(term->window); const bool title_visible = wayl_win_csd_titlebar_visible(term->window); const float scale = term->scale; const int border_width = borders_visible ? roundf(term->conf->csd.border_width * scale) : 0; const int title_height = title_visible ? roundf(term->conf->csd.title_height * scale) : 0; const int button_width = title_visible ? roundf(term->conf->csd.button_width * scale) : 0; int remaining_width = term->width; const int button_close_width = remaining_width >= button_width ? button_width : 0; remaining_width -= button_close_width; const int button_close_start = remaining_width; const int button_maximize_width = remaining_width >= button_width && term->window->wm_capabilities.maximize ? button_width : 0; remaining_width -= button_maximize_width; const int button_maximize_start = remaining_width; const int button_minimize_width = remaining_width >= button_width && term->window->wm_capabilities.minimize ? button_width : 0; remaining_width -= button_minimize_width; const int button_minimize_start = remaining_width; /* * With fractional scaling, we must ensure the offset, when * divided by the scale (in set_position()), and the scaled back * (by the compositor), matches the actual pixel count made up by * the titlebar and the border. */ const int top_offset = roundf( scale * (roundf(-title_height / scale) - roundf(border_width / scale))); const int top_bottom_width = roundf( scale * (roundf(term->width / scale) + 2 * roundf(border_width / scale))); const int left_right_height = roundf( scale * (roundf(title_height / scale) + roundf(term->height / scale))); 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, left_right_height}; case CSD_SURF_RIGHT: return (struct csd_data){term->width, -title_height, border_width, left_right_height}; case CSD_SURF_TOP: return (struct csd_data){-border_width, top_offset, top_bottom_width, border_width}; case CSD_SURF_BOTTOM: return (struct csd_data){-border_width, term->height, top_bottom_width, border_width}; /* Positioned relative to CSD_SURF_TITLE */ case CSD_SURF_MINIMIZE: return (struct csd_data){button_minimize_start, 0, button_minimize_width, title_height}; case CSD_SURF_MAXIMIZE: return (struct csd_data){button_maximize_start, 0, button_maximize_width, title_height}; case CSD_SURF_CLOSE: return (struct csd_data){button_close_start, 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 wayl_surface *surf, struct buffer *buf) { wayl_surface_scale(term->window, surf, buf, term->scale); wl_surface_attach(surf->surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(surf->surf, 0, 0, buf->width, buf->height); wl_surface_commit(surf->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_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], color, 1, &(pixman_rectangle16_t){0, 0, buf->width, buf->height}); } static void render_osd(struct terminal *term, const struct wayl_sub_surface *sub_surf, struct fcft_font *font, struct buffer *buf, const char32_t *text, uint32_t _fg, uint32_t _bg, unsigned x) { 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); const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); uint16_t alpha = _bg >> 24 | (_bg >> 24 << 8); pixman_color_t bg = color_hex_to_pixman_with_alpha(_bg, alpha, gamma_correct); 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, gamma_correct); const int x_ofs = term->font_x_ofs; const size_t len = c32len(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_rasterize_text_run_utf32(font, len, (const char32_t *)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_rasterize_char_utf32(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); /* Calculate baseline */ unsigned y; { const int line_height = buf->height; const int font_height = max(font->height, font->ascent + font->descent); const int glyph_top_y = round((line_height - font_height) / 2.); y = term->font_y_ofs + glyph_top_y + font->ascent; } for (size_t i = 0; i < glyph_count; i++) { const struct fcft_glyph *glyph = glyphs[i]; if (unlikely(glyph->is_color_glyph)) { pixman_image_composite32(PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y - 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 - 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); quirk_weston_subsurface_desync_on(sub_surf->sub); wayl_surface_scale(term->window, &sub_surf->surface, buf, term->scale); wl_surface_attach(sub_surf->surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(sub_surf->surface.surf, 0, 0, buf->width, buf->height); if (alpha == 0xffff) { 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(sub_surf->surface.surf, region); wl_region_destroy(region); } } else wl_surface_set_opaque_region(sub_surf->surface.surf, NULL); wl_surface_commit(sub_surf->surface.surf); quirk_weston_subsurface_desync_off(sub_surf->sub); } static void render_csd_title(struct terminal *term, const struct csd_data *info, struct buffer *buf) { xassert(term->window->csd_mode == CSD_YES); struct wayl_sub_surface *surf = &term->window->csd.surface[CSD_SURF_TITLE]; if (info->width == 0 || info->height == 0) return; uint32_t bg = term->conf->csd.color.title_set ? term->conf->csd.color.title : 0xffu << 24 | term->conf->colors_dark.fg; uint32_t fg = term->conf->csd.color.buttons_set ? term->conf->csd.color.buttons : term->conf->colors_dark.bg; if (!term->visual_focus) { bg = color_dim(term, bg); fg = color_dim(term, fg); } char32_t *_title_text = ambstoc32(term->window_title); const char32_t *title_text = _title_text != NULL ? _title_text : U""; struct wl_window *win = term->window; const struct fcft_glyph *M = fcft_rasterize_char_utf32(win->csd.font, U'M', term->font_subpixel); const int margin = M != NULL ? M->advance.x : win->csd.font->max_advance.x; render_osd(term, surf, win->csd.font, buf, title_text, fg, bg, margin); csd_commit(term, &surf->surface, 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 wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; if (info->width == 0 || info->height == 0) return; const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); { /* Fully transparent - no need to do a color space transform */ pixman_color_t color = color_hex_to_pixman_with_alpha(0, 0, gamma_correct); render_csd_part(term, surf->surf, buf, info->width, info->height, &color); } /* * The "visible" border. */ float scale = term->scale; int bwidth = (int)roundf(term->conf->csd.border_width * scale); int vwidth = (int)roundf(term->conf->csd.border_width_visible * scale); /* Visible size */ xassert(bwidth >= vwidth); if (vwidth > 0) { const struct config *conf = term->conf; int x = 0, y = 0, w = 0, h = 0; switch (surf_idx) { case CSD_SURF_TOP: case CSD_SURF_BOTTOM: x = bwidth - vwidth; y = surf_idx == CSD_SURF_TOP ? info->height - vwidth : 0; w = info->width - 2 * x; h = vwidth; break; case CSD_SURF_LEFT: case CSD_SURF_RIGHT: x = surf_idx == CSD_SURF_LEFT ? bwidth - vwidth : 0; y = 0; w = vwidth; h = info->height; break; case CSD_SURF_TITLE: case CSD_SURF_MINIMIZE: case CSD_SURF_MAXIMIZE: case CSD_SURF_CLOSE: case CSD_SURF_COUNT: BUG("unexpected CSD surface type"); } xassert(x >= 0); xassert(y >= 0); xassert(w >= 0); xassert(h >= 0); xassert(x + w <= info->width); xassert(y + h <= info->height); uint32_t _color = conf->csd.color.border_set ? conf->csd.color.border : conf->csd.color.title_set ? conf->csd.color.title : 0xffu << 24 | term->conf->colors_dark.fg; if (!term->visual_focus) _color = color_dim(term, _color); uint16_t alpha = _color >> 24 | (_color >> 24 << 8); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){x, y, w, h}); } csd_commit(term, surf, buf); } static pixman_color_t get_csd_button_fg_color(const struct terminal *term) { const struct config *conf = term->conf; uint32_t _color = conf->colors_dark.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, wayl_do_linear_blending(term->wl, term->conf)); } static void render_csd_button_minimize(struct terminal *term, struct buffer *buf) { pixman_color_t color = get_csd_button_fg_color(term); 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 = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; xassert(x_margin + width - thick >= 0); xassert(width - 2 * thick >= 0); xassert(y_margin + width - thick >= 0); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 1, (pixman_rectangle16_t[]){ {x_margin, y_margin + width - thick, width, thick}}); 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); 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 = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; const int shrink = 1; xassert(x_margin + width - thick >= 0); xassert(width - 2 * thick >= 0); xassert(y_margin + width - thick >= 0); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &color, 4, (pixman_rectangle16_t[]){ {x_margin + shrink, y_margin + shrink, width - 2 * shrink, thick}, {x_margin + shrink, y_margin + thick, thick, width - 2 * thick - shrink}, {x_margin + width - thick - shrink, y_margin + thick, thick, width - 2 * thick - shrink}, {x_margin + shrink, y_margin + width - thick - shrink, width - 2 * shrink, 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); 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 = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; xassert(x_margin + width - thick >= 0); xassert(width - 2 * thick >= 0); xassert(y_margin + width - thick >= 0); 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(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); 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 = min(width / 2, 1 * term->scale); const int x_margin = (buf->width - width) / 2; const int y_margin = (buf->height - width) / 2; xassert(x_margin + width - thick >= 0); xassert(width - 2 * thick >= 0); xassert(y_margin + width - thick >= 0); pixman_triangle_t tri[4] = { { .p1 = { .x = pixman_int_to_fixed(x_margin), .y = pixman_int_to_fixed(y_margin + thick), }, .p2 = { .x = pixman_int_to_fixed(x_margin + width - thick), .y = pixman_int_to_fixed(y_margin + width), }, .p3 = { .x = pixman_int_to_fixed(x_margin + thick), .y = pixman_int_to_fixed(y_margin), }, }, { .p1 = { .x = pixman_int_to_fixed(x_margin + width), .y = pixman_int_to_fixed(y_margin + width - thick), }, .p2 = { .x = pixman_int_to_fixed(x_margin + thick), .y = pixman_int_to_fixed(y_margin), }, .p3 = { .x = pixman_int_to_fixed(x_margin + width - thick), .y = pixman_int_to_fixed(y_margin + width), }, }, { .p1 = { .x = pixman_int_to_fixed(x_margin), .y = pixman_int_to_fixed(y_margin + width - thick), }, .p2 = { .x = pixman_int_to_fixed(x_margin + width), .y = pixman_int_to_fixed(y_margin + thick), }, .p3 = { .x = pixman_int_to_fixed(x_margin + thick), .y = pixman_int_to_fixed(y_margin + width), }, }, { .p1 = { .x = pixman_int_to_fixed(x_margin + width), .y = pixman_int_to_fixed(y_margin + thick), }, .p2 = { .x = pixman_int_to_fixed(x_margin), .y = pixman_int_to_fixed(y_margin + width - thick), }, .p3 = { .x = pixman_int_to_fixed(x_margin + width - thick), .y = pixman_int_to_fixed(y_margin), }, }, }; pixman_composite_triangles(PIXMAN_OP_OVER, src, buf->pix[0], PIXMAN_a1, 0, 0, 0, 0, 4, tri); pixman_image_unref(src); } static bool any_pointer_is_on_button(const struct terminal *term, enum csd_surface csd_surface) { if (unlikely(tll_length(term->wl->seats) == 0)) return false; tll_foreach(term->wl->seats, it) { const struct seat *seat = &it->item; if (seat->mouse.x < 0) continue; if (seat->mouse.y < 0) continue; struct csd_data info = get_csd_data(term, csd_surface); if (seat->mouse.x > info.width) continue; if (seat->mouse.y > info.height) continue; return true; } return false; } 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 wayl_surface *surf = &term->window->csd.surface[surf_idx].surface; if (info->width == 0 || info->height == 0) return; 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_dark.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 && any_pointer_is_on_button(term, CSD_SURF_MINIMIZE); break; case CSD_SURF_MAXIMIZE: _color = term->conf->colors_dark.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 && any_pointer_is_on_button(term, CSD_SURF_MAXIMIZE); break; case CSD_SURF_CLOSE: _color = term->conf->colors_dark.table[1]; /* red */ is_set = term->conf->csd.color.close_set; conf_color = &term->conf->csd.color.quit; is_active = term->active_surface == TERM_SURF_BUTTON_CLOSE && any_pointer_is_on_button(term, CSD_SURF_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(term, _color); const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); pixman_color_t color = color_hex_to_pixman_with_alpha(_color, alpha, gamma_correct); render_csd_part(term, surf->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; 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; const float scale = term->scale; 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].surface.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, roundf(x / scale), roundf(y / 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.surface.surf != NULL) wayl_win_subsurface_destroy(&win->scrollback_indicator); return; } if (win->scrollback_indicator.surface.surf == NULL) { if (!wayl_win_subsurface_new(win, &win->scrollback_indicator, false)) { LOG_ERR("failed to create scrollback indicator surface"); return; } } xassert(win->scrollback_indicator.surface.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); char32_t _text[64]; const char32_t *text = _text; int cell_count = 0; /* *What* to render */ switch (term->conf->scrollback.indicator.format) { case SCROLLBACK_INDICATOR_FORMAT_PERCENTAGE: { char percent_str[8]; snprintf(percent_str, sizeof(percent_str), "%u%%", (int)(100 * percent)); mbstoc32(_text, percent_str, ALEN(_text)); cell_count = 3; break; } case SCROLLBACK_INDICATOR_FORMAT_LINENO: { char lineno_str[64]; snprintf(lineno_str, sizeof(lineno_str), "%d", rebased_view + 1); mbstoc32(_text, lineno_str, ALEN(_text)); cell_count = (int)ceilf(log10f(term->grid->num_rows)); break; } case SCROLLBACK_INDICATOR_FORMAT_TEXT: text = term->conf->scrollback.indicator.text; cell_count = c32len(text); break; } const float scale = term->scale; const int margin = (int)roundf(3. * scale); int width = margin + cell_count * term->cell_width + margin; int height = margin + term->cell_height + margin; width = roundf(scale * ceilf(width / scale)); height = roundf(scale * ceilf(height / 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; } } int x = term->width - margin - width; int y = term->margins.top + surf_top; x = roundf(scale * ceilf(x / scale)); y = roundf(scale * ceilf(y / scale)); if (y + height > term->height) { wl_surface_attach(win->scrollback_indicator.surface.surf, NULL, 0, 0); wl_surface_commit(win->scrollback_indicator.surface.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, roundf(x / scale), roundf(y / scale)); uint32_t fg = term->colors.table[0]; uint32_t bg = term->colors.table[8 + 4]; if (term->conf->colors_dark.use_custom.scrollback_indicator) { fg = term->conf->colors_dark.scrollback_indicator.fg; bg = term->conf->colors_dark.scrollback_indicator.bg; } render_osd(term, &win->scrollback_indicator, term->fonts[0], buf, text, fg, 0xffu << 24 | bg, width - margin - c32len(text) * term->cell_width); } static void render_render_timer(struct terminal *term, struct timespec render_time) { struct wl_window *win = term->window; char usecs_str[256]; double usecs = render_time.tv_sec * 1000000 + render_time.tv_nsec / 1000.0; snprintf(usecs_str, sizeof(usecs_str), "%.2f µs", usecs); char32_t text[256]; mbstoc32(text, usecs_str, ALEN(text)); const float scale = term->scale; const int cell_count = c32len(text); const int margin = (int)roundf(3. * scale); int width = margin + cell_count * term->cell_width + margin; int height = margin + term->cell_height + margin; width = roundf(scale * ceilf(width / scale)); height = roundf(scale * ceilf(height / 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, roundf(margin / scale), roundf((term->margins.top + term->cell_height - margin) / scale)); render_osd(term, &win->render_timer, term->fonts[0], buf, text, term->colors.table[0], 0xffu << 24 | term->colors.table[8 + 1], 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) { if (new->age > 1) { memcpy(new->data, old->data, new->height * new->stride); return; } pixman_region32_t dirty; pixman_region32_init(&dirty); /* * Figure out current frame's damage region * * If current frame doesn't have any scroll damage, we can simply * subtract this frame's damage from the last frame's damage. That * way, we don't have to copy areas from the old frame that'll * just get overwritten by current frame. * * Note that this is row based. A "half damaged" row is not * excluded. I.e. the entire row will be copied from the old frame * to the new, and then when actually rendering the new frame, the * updated cells will overwrite parts of the copied row. * * Since we're scanning the entire viewport anyway, we also track * whether *all* cells are to be updated. In this case, just force * a full re-rendering, and don't copy anything from the old * frame. */ bool full_repaint_needed = true; for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); if (!row->dirty) { full_repaint_needed = false; continue; } 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; } /* * TODO: re-apply last frame's scroll damage * * We used to do this, but it turned out to be buggy. If we decide * to re-add it, this is where to do it. Note that we'd also have * to remove the updates to buf->dirty from grid_render_scroll() * and grid_render_scroll_reverse(). */ if (tll_length(term->grid->scroll_damage) == 0) { /* * We can only subtract current frame's damage from the old * frame's if we don't have any scroll damage. * * If we do have scroll damage, the damage region we * calculated above is not (yet) valid - we need to apply the * current frame's scroll damage *first*. This is done later, * when rendering the frame. */ pixman_region32_subtract(&dirty, &old->dirty[0], &dirty); pixman_image_set_clip_region32(new->pix[0], &dirty); } else { /* Copy *all* of last frame's damaged areas */ pixman_image_set_clip_region32(new->pix[0], &old->dirty[0]); } pixman_image_composite32(PIXMAN_OP_SRC, old->pix[0], NULL, new->pix[0], 0, 0, 0, 0, 0, 0, term->width, term->height); pixman_image_set_clip_region32(new->pix[0], NULL); pixman_region32_fini(&dirty); } static void dirty_old_cursor(struct terminal *term) { if (term->render.last_cursor.row != NULL && !term->render.last_cursor.hidden) { struct row *row = term->render.last_cursor.row; struct cell *cell = &row->cells[term->render.last_cursor.col]; cell->attrs.clean = 0; row->dirty = true; } /* Remember current cursor position, for the next frame */ term->render.last_cursor.row = grid_row(term->grid, term->grid->cursor.point.row); term->render.last_cursor.col = term->grid->cursor.point.col; term->render.last_cursor.hidden = term->hide_cursor; } static void dirty_cursor(struct terminal *term) { if (term->hide_cursor) return; const struct coord *cursor = &term->grid->cursor.point; struct row *row = grid_row(term->grid, cursor->row); struct cell *cell = &row->cells[cursor->col]; cell->attrs.clean = 0; row->dirty = true; } static void grid_render(struct terminal *term) { if (term->shutdown.in_progress) return; struct timespec start_time; struct timespec start_wait_preapply = {0}, stop_wait_preapply = {0}; struct timespec start_double_buffering = {0}, stop_double_buffering = {0}; /* Might be a thread doing pre-applied damage */ if (unlikely(term->render.preapply_last_frame_damage && term->render.workers.preapplied_damage.buf != NULL)) { clock_gettime(CLOCK_MONOTONIC, &start_wait_preapply); render_wait_for_preapply_damage(term); clock_gettime(CLOCK_MONOTONIC, &stop_wait_preapply); } if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) clock_gettime(CLOCK_MONOTONIC, &start_time); xassert(term->width > 0); xassert(term->height > 0); struct buffer_chain *chain = term->render.chains.grid; struct buffer *buf = shm_get_buffer(chain, term->width, term->height); /* Dirty old and current cursor cell, to ensure they're repainted */ dirty_old_cursor(term); dirty_cursor(term); LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); if (term->render.last_buf == NULL || term->render.last_buf->width != buf->width || term->render.last_buf->height != buf->height || term->render.margins) { force_full_repaint(term, buf); } else if (buf->age > 0) { LOG_DBG("buffer age: %u (%p)", buf->age, (void *)buf); xassert(term->render.last_buf != NULL); xassert(term->render.last_buf != buf); xassert(term->render.last_buf->width == buf->width); xassert(term->render.last_buf->height == buf->height); if (++term->render.frames_since_last_immediate_release > 10) { static bool have_warned = false; if (!term->render.preapply_last_frame_damage && term->conf->tweak.preapply_damage && term->render.workers.count > 0) { LOG_INFO("enabling pre-applied frame damage"); term->render.preapply_last_frame_damage = true; } else if (!have_warned && !term->render.preapply_last_frame_damage) { LOG_WARN("compositor is not releasing buffers immediately; " "expect lower rendering performance"); have_warned = true; } } clock_gettime(CLOCK_MONOTONIC, &start_double_buffering); reapply_old_damage(term, buf, term->render.last_buf); clock_gettime(CLOCK_MONOTONIC, &stop_double_buffering); } else if (!term->render.preapply_last_frame_damage) { term->render.frames_since_last_immediate_release = 0; } if (term->render.last_buf != NULL) { shm_unref(term->render.last_buf); term->render.last_buf = NULL; } term->render.last_buf = buf; shm_addref(buf); buf->age = 0; 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); } /* * Ensure selected cells have their 'selected' bit set. This is * normally "automatically" true - the bit is set when the * selection is made. * * However, if the cell is updated (printed to) while the * selection is active, the 'selected' bit is cleared. Checking * for this and re-setting the bit in term_print() is too * expensive performance wise. * * Instead, we synchronize the selection bits here and now. This * makes the performance impact linear to the number of selected * cells rather than to the number of updated cells. * * (note that selection_dirty_cells() will not set the dirty flag * on cells where the 'selected' bit is already set) */ selection_dirty_cells(term); /* Translate offset-relative row to view-relative, unless cursor * is hidden, then we just set it to -1 */ 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; } if (term->conf->tweak.overflowing_glyphs) { /* * Pre-pass to dirty cells affected by overflowing glyphs. * * Given any two pair of cells where the first cell is * overflowing into the second, *both* cells must be * re-rendered if any one of them is dirty. * * Thus, given a string of overflowing glyphs, with a single * dirty cell in the middle, we need to re-render the entire * string. */ for (int r = 0; r < term->rows; r++) { struct row *row = grid_row_in_view(term->grid, r); if (!row->dirty) continue; /* Loop row from left to right, looking for dirty cells */ for (struct cell *cell = &row->cells[0]; cell < &row->cells[term->cols]; cell++) { if (cell->attrs.clean) continue; /* * Cell is dirty, go back and dirty previous cells, if * they are overflowing. * * As soon as we see a non-overflowing cell we can * stop, since it isn't affecting the string of * overflowing glyphs that follows it. * * As soon as we see a dirty cell, we can stop, since * that means we've already handled it (remember the * outer loop goes from left to right). */ for (struct cell *c = cell - 1; c >= &row->cells[0]; c--) { if (c->attrs.confined) break; if (!c->attrs.clean) break; c->attrs.clean = false; } /* * Now move forward, dirtying all cells until we hit a * non-overflowing cell. * * Note that the first non-overflowing cell must be * re-rendered as well, but any cell *after* that is * unaffected by the string of overflowing glyphs * we're dealing with right now. * * For performance, this iterates the *outer* loop's * cell pointer - no point in re-checking all these * glyphs again, in the outer loop. */ for (; cell < &row->cells[term->cols]; cell++) { cell->attrs.clean = false; if (cell->attrs.confined) break; } } } } #if defined(_DEBUG) for (int r = 0; r < term->rows; r++) { const struct row *row = grid_row_in_view(term->grid, r); if (row->dirty) { bool all_clean = true; for (int c = 0; c < term->cols; c++) { if (!row->cells[c].attrs.clean) { all_clean = false; break; } } if (all_clean) BUG("row #%d is dirty, but all cells are marked as clean", r); } else { for (int c = 0; c < term->cols; c++) { if (!row->cells[c].attrs.clean) BUG("row #%d is clean, but cell #%d is dirty", r, c); } } } #endif pixman_region32_t damage; pixman_region32_init(&damage); render_sixel_images(term, buf->pix[0], &damage, &cursor); if (term->render.workers.count > 0) { mtx_lock(&term->render.workers.lock); term->render.workers.buf = buf; for (size_t i = 0; i < term->render.workers.count; i++) sem_post(&term->render.workers.start); xassert(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; row->dirty = false; if (term->render.workers.count > 0) tll_push_back(term->render.workers.queue, r); else { /* TODO: damage region */ int cursor_col = cursor.row == r ? cursor.col : -1; render_row(term, buf->pix[0], &damage, row, r, cursor_col); } } /* Signal workers the frame is done */ if (term->render.workers.count > 0) { for (size_t i = 0; i < term->render.workers.count; i++) tll_push_back(term->render.workers.queue, -1); mtx_unlock(&term->render.workers.lock); for (size_t i = 0; i < term->render.workers.count; i++) sem_wait(&term->render.workers.done); term->render.workers.buf = NULL; } for (size_t i = 0; i < term->render.workers.count; i++) pixman_region32_union(&damage, &damage, &buf->dirty[i + 1]); pixman_region32_union(&buf->dirty[0], &buf->dirty[0], &damage); { int box_count = 0; pixman_box32_t *boxes = pixman_region32_rectangles(&damage, &box_count); for (size_t i = 0; i < box_count; i++) { wl_surface_damage_buffer(term->window->surface.surf, boxes[i].x1, boxes[i].y1, boxes[i].x2 - boxes[i].x1, boxes[i].y2 - boxes[i].y1); } } pixman_region32_fini(&damage); render_overlay(term); render_ime_preedit(term, buf); render_scrollback_position(term); if (term->conf->tweak.render_timer != RENDER_TIMER_NONE) { struct timespec end_time; clock_gettime(CLOCK_MONOTONIC, &end_time); struct timespec wait_time; timespec_sub(&stop_wait_preapply, &start_wait_preapply, &wait_time); struct timespec render_time; timespec_sub(&end_time, &start_time, &render_time); struct timespec double_buffering_time; timespec_sub(&stop_double_buffering, &start_double_buffering, &double_buffering_time); struct timespec preapply_damage; timespec_sub(&term->render.workers.preapplied_damage.stop, &term->render.workers.preapplied_damage.start, &preapply_damage); struct timespec total_render_time; timespec_add(&render_time, &double_buffering_time, &total_render_time); timespec_add(&wait_time, &total_render_time, &total_render_time); switch (term->conf->tweak.render_timer) { case RENDER_TIMER_LOG: case RENDER_TIMER_BOTH: LOG_INFO("frame rendered in %lds %9ldns " "(%lds %9ldns wait, %lds %9ldns rendering, %lds %9ldns double " "buffering) not included: %lds %ldns pre-apply damage", (long)total_render_time.tv_sec, total_render_time.tv_nsec, (long)wait_time.tv_sec, wait_time.tv_nsec, (long)render_time.tv_sec, render_time.tv_nsec, (long)double_buffering_time.tv_sec, double_buffering_time.tv_nsec, (long)preapply_damage.tv_sec, preapply_damage.tv_nsec); break; case RENDER_TIMER_OSD: case RENDER_TIMER_NONE: break; } switch (term->conf->tweak.render_timer) { case RENDER_TIMER_OSD: case RENDER_TIMER_BOTH: render_render_timer(term, total_render_time); break; case RENDER_TIMER_LOG: case RENDER_TIMER_NONE: break; } } xassert(term->grid->offset >= 0 && term->grid->offset < term->grid->num_rows); xassert(term->grid->view >= 0 && term->grid->view < term->grid->num_rows); xassert(term->window->frame_callback == NULL); term->window->frame_callback = wl_surface_frame(term->window->surface.surf); wl_callback_add_listener(term->window->frame_callback, &frame_listener, term); wayl_win_scale(term->window, buf); if (term->wl->presentation != NULL && term->conf->presentation_timings) { struct timespec commit_time; clock_gettime(term->wl->presentation_clock_id, &commit_time); struct wp_presentation_feedback *feedback = wp_presentation_feedback( term->wl->presentation, term->window->surface.surf); if (feedback == NULL) { LOG_WARN("failed to create presentation feedback"); } else { struct presentation_context *ctx = xmalloc(sizeof(*ctx)); *ctx = (struct presentation_context){ .term = term, .input.tv_sec = term->render.input_time.tv_sec, .input.tv_usec = term->render.input_time.tv_nsec / 1000, .commit.tv_sec = commit_time.tv_sec, .commit.tv_usec = commit_time.tv_nsec / 1000, }; wp_presentation_feedback_add_listener( feedback, &presentation_feedback_listener, ctx); term->render.input_time.tv_sec = 0; term->render.input_time.tv_nsec = 0; } } if (term->conf->tweak.damage_whole_window) { wl_surface_damage_buffer(term->window->surface.surf, 0, 0, INT32_MAX, INT32_MAX); } wl_surface_attach(term->window->surface.surf, buf->wl_buf, 0, 0); wl_surface_commit(term->window->surface.surf); } static void render_search_box(struct terminal *term) { xassert(term->window->search.sub != NULL); /* * We treat the search box pretty much like a row of cells. That * is, a glyph is either 1 or 2 (or more) "cells" wide. * * The search 'length', and 'cursor' (position) is in * *characters*, not cells. This means we need to translate from * character count to cell count when calculating the length of * the search box, where in the search string we should start * rendering etc. */ #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED /* TODO: do we want to/need to handle multi-seat? */ struct seat *ime_seat = NULL; tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) { ime_seat = &it->item; break; } } size_t text_len = term->search.len; if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) text_len += c32len(ime_seat->ime.preedit.text); char32_t *text = xmalloc((text_len + 1) * sizeof(char32_t)); text[0] = U'\0'; /* Copy everything up to the cursor */ c32ncpy(text, term->search.buf, term->search.cursor); text[term->search.cursor] = U'\0'; /* Insert pre-edit text at cursor */ if (ime_seat != NULL && ime_seat->ime.preedit.text != NULL) c32cat(text, ime_seat->ime.preedit.text); /* And finally everything after the cursor */ c32ncat(text, &term->search.buf[term->search.cursor], term->search.len - term->search.cursor); #else const char32_t *text = term->search.buf; const size_t text_len = term->search.len; #endif /* Password modes: replace every codepoint with a mask glyph for display. */ char32_t *masked_text = NULL; char32_t *ime_alloc = NULL; #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED ime_alloc = text; /* remember the IME-allocated buffer for cleanup */ #endif if (term->search.mode == SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD || term->search.mode == SEARCH_MODE_SESSION_LOAD_PASSWORD) { masked_text = xmalloc((text_len + 1) * sizeof(char32_t)); for (size_t i = 0; i < text_len; i++) masked_text[i] = U'•'; masked_text[text_len] = U'\0'; text = masked_text; } /* Calculate the width of each character */ int widths[text_len + 1]; for (size_t i = 0; i < text_len; i++) widths[i] = max(0, c32width(text[i])); widths[text_len] = 0; const size_t total_cells = c32swidth(text, text_len); size_t wanted_visible_cells = max(20, total_cells); if (term->search.mode == SEARCH_MODE_SESSION_LOAD) { /* Make room for the longest session name */ for (size_t i = 0; i < term->session_picker.all_count; i++) { size_t n = strlen(term->session_picker.all[i]); if (n > wanted_visible_cells) wanted_visible_cells = n; } wanted_visible_cells = max(wanted_visible_cells, 30); } /* * Build status string: " Aa Wb Re 3/17 ↻" * Aa - case mode (blank=smart, A=sensitive, a=insensitive) * Wb - whole-word toggle * Re - regex toggle * N/M - match counter * ↻ - wrap indicator */ char32_t status[64]; size_t st_len = 0; if (term->search.mode != SEARCH_MODE_NORMAL) { char label_buf[128]; const char *label; switch (term->search.mode) { case SEARCH_MODE_SESSION_OVERWRITE_CONFIRM: snprintf(label_buf, sizeof(label_buf), "Overwrite '%s'? (y/n) ", term->session_pending_name != NULL ? term->session_pending_name : ""); label = label_buf; break; case SEARCH_MODE_SESSION_SAVE: label = "Save State As: "; break; case SEARCH_MODE_SESSION_SAVE_SECURE_NAME: label = "Secure Save As: "; break; case SEARCH_MODE_SESSION_SAVE_SECURE_PASSWORD: label = "Password (new): "; break; case SEARCH_MODE_SESSION_LOAD: label = "Load State: "; break; case SEARCH_MODE_SESSION_LOAD_PASSWORD: label = "Decrypt Password: "; break; default: label = ""; break; } for (const char *p = label; *p != '\0' && st_len < ALEN(status) - 1; p++) status[st_len++] = (char32_t)(unsigned char)*p; } else { if (term->search.case_mode == SEARCH_CASE_SENSITIVE) { status[st_len++] = U'A'; status[st_len++] = U'a'; status[st_len++] = U' '; } else if (term->search.case_mode == SEARCH_CASE_INSENSITIVE) { status[st_len++] = U'a'; status[st_len++] = U'a'; status[st_len++] = U' '; } if (term->search.whole_word) { status[st_len++] = U'W'; status[st_len++] = U'b'; status[st_len++] = U' '; } if (term->search.regex) { status[st_len++] = U'.'; status[st_len++] = U'*'; status[st_len++] = U' '; } if (term->search.wrapped) { status[st_len++] = U'~'; status[st_len++] = U' '; } if (term->search.len > 0 && term->search.total_count > 0) { char tmp[32]; snprintf(tmp, sizeof(tmp), "%zu/%zu", term->search.current_idx, term->search.total_count); for (size_t i = 0; tmp[i] != '\0' && st_len < ALEN(status) - 1; i++) status[st_len++] = (char32_t)tmp[i]; status[st_len++] = U' '; } } status[st_len] = U'\0'; const size_t status_cells = c32swidth(status, st_len); const float scale = term->scale; xassert(scale >= 1.); const size_t margin = (size_t)roundf(3 * scale); const size_t outer_margin = (size_t)roundf(6 * scale); const size_t close_gap = (size_t)roundf(4 * scale); const size_t close_w = term->cell_width; const size_t status_w = status_cells * term->cell_width; const size_t chrome_w = margin + status_w + close_gap + close_w + margin; const size_t max_box_w = term->width > (int)(2 * outer_margin) ? (size_t)term->width - 2 * outer_margin : (size_t)term->width; const size_t want_box_w = chrome_w + wanted_visible_cells * term->cell_width; size_t width = min(want_box_w, max_box_w); /* Session-load picker: grow box to fit a few rows of session names */ size_t list_rows = 0; if (term->search.mode == SEARCH_MODE_SESSION_LOAD) { const size_t max_visible = 10; list_rows = min(max_visible, term->session_picker.filtered_count); /* Reserve at least one row so empty state is visible */ if (list_rows == 0) list_rows = 1; } size_t height = min( term->height - 2 * outer_margin, margin + (1 + list_rows) * term->cell_height + margin); width = roundf(scale * ceilf(width / scale)); height = roundf(scale * ceilf(height / scale)); const size_t text_area_w = width > chrome_w ? width - chrome_w : 0; const size_t visible_cells = text_area_w / term->cell_width; size_t glyph_offset = term->render.search_glyph_offset; struct buffer_chain *chain = term->render.chains.search; struct buffer *buf = shm_get_buffer(chain, width, height); pixman_region32_t clip; pixman_region32_init_rect(&clip, 0, 0, width, height); pixman_image_set_clip_region32(buf->pix[0], &clip); pixman_region32_fini(&clip); #define WINDOW_X(x) ((int)term->width - (int)width - (int)outer_margin + (x)) #define WINDOW_Y(y) ((int)outer_margin + (y)) /* Use the terminal's own bg/fg for the popup, tinted to indicate * the search state. */ const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); uint32_t bg_hex = term->colors.bg; uint32_t fg_hex = term->colors.fg; if (term->search.mode == SEARCH_MODE_NORMAL) { if (term->search.len > 0 && term->search.match_len == 0) { bg_hex = term->colors.table[1]; fg_hex = term->colors.table[0]; } else if (term->search.wrapped) { bg_hex = term->colors.table[3]; fg_hex = term->colors.table[0]; } } const pixman_color_t color = color_hex_to_pixman(bg_hex, gamma_correct); pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &color, 1, &(pixman_rectangle16_t){0, 0, width, height}); struct fcft_font *font = term->fonts[0]; const int x_left = margin; const int x_ofs = term->font_x_ofs; int x = x_left; int y = margin; pixman_color_t fg = color_hex_to_pixman(fg_hex, gamma_correct); /* Move offset we start rendering at, to ensure the cursor is visible */ for (size_t i = 0, cell_idx = 0; i <= term->search.cursor; cell_idx += widths[i], i++) { if (i != term->search.cursor) continue; #if (FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) { if (ime_seat->ime.preedit.cursor.start == ime_seat->ime.preedit.cursor.end) { /* All IME's I've seen so far keeps the cursor at * index 0, so ensure the *end* of the pre-edit string * is visible */ cell_idx += ime_seat->ime.preedit.count; } else { /* Try to predict in which direction we'll shift the text */ if (cell_idx + ime_seat->ime.preedit.cursor.start > glyph_offset) cell_idx += ime_seat->ime.preedit.cursor.end; else cell_idx += ime_seat->ime.preedit.cursor.start; } } #endif if (cell_idx < glyph_offset) { /* Shift to the *left*, making *this* character the * *first* visible one */ term->render.search_glyph_offset = glyph_offset = cell_idx; } else if (cell_idx > glyph_offset + visible_cells) { /* Shift to the *right*, making *this* character the * *last* visible one */ term->render.search_glyph_offset = glyph_offset = cell_idx - min(cell_idx, visible_cells); } /* Adjust offset if there is free space available */ if (total_cells - glyph_offset < visible_cells) { term->render.search_glyph_offset = glyph_offset = total_cells - min(total_cells, visible_cells); } break; } /* Ensure offset is at a character boundary */ for (size_t i = 0, cell_idx = 0; i <= text_len; cell_idx += widths[i], i++) { if (cell_idx >= glyph_offset) { term->render.search_glyph_offset = glyph_offset = cell_idx; break; } } /* * Render the search string, starting at 'glyph_offset'. Note that * glyph_offset is in cells, not characters */ for (size_t i = 0, cell_idx = 0, width = widths[i], next_cell_idx = width; i < text_len; i++, cell_idx = next_cell_idx, width = widths[i], next_cell_idx += width) { /* Convert subsurface coordinates to window coordinates*/ /* Render cursor */ if (i == term->search.cursor) { #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED bool have_preedit = ime_seat != NULL && ime_seat->ime.preedit.cells != NULL; bool hidden = ime_seat != NULL && ime_seat->ime.preedit.cursor.hidden; if (have_preedit && !hidden) { /* Cursor may be outside the visible area: * cell_idx-glyph_offset can be negative */ int cells_left = visible_cells - max((ssize_t)(cell_idx - glyph_offset), 0); /* If cursor is outside the visible area, we need to * adjust our rectangle's position */ int start = ime_seat->ime.preedit.cursor.start + min((ssize_t)(cell_idx - glyph_offset), 0); int end = ime_seat->ime.preedit.cursor.end + min((ssize_t)(cell_idx - glyph_offset), 0); if (start == end) { int count = min(ime_seat->ime.preedit.count, cells_left); /* Underline the entire (visible part of) pre-edit text */ draw_underline(term, buf->pix[0], font, &fg, x, y, count); /* Bar-styled cursor, if in the visible area */ if (start >= 0 && start <= visible_cells) { draw_beam_cursor(term, buf->pix[0], font, &fg, x + start * term->cell_width, y); } term_ime_set_cursor_rect(term, WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), 1, term->cell_height); } else { /* Underline everything before and after the cursor */ int count1 = min(start, cells_left); int count2 = max(min(ime_seat->ime.preedit.count - ime_seat->ime.preedit.cursor.end, cells_left - end), 0); draw_underline(term, buf->pix[0], font, &fg, x, y, count1); draw_underline(term, buf->pix[0], font, &fg, x + end * term->cell_width, y, count2); /* TODO: how do we handle a partially hidden rectangle? */ if (start >= 0 && end <= visible_cells) { draw_hollow_block(term, buf->pix[0], &fg, x + start * term->cell_width, y, end - start); } term_ime_set_cursor_rect( term, WINDOW_X(x + start * term->cell_width), WINDOW_Y(y), term->cell_width * (end - start), term->cell_height); } } else if (!have_preedit) #endif { /* Cursor *should* be in the visible area */ xassert(cell_idx >= glyph_offset); xassert(cell_idx <= glyph_offset + visible_cells); draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); term_ime_set_cursor_rect(term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); } } if (next_cell_idx >= glyph_offset && next_cell_idx - glyph_offset > visible_cells) { /* We're now beyond the visible area - nothing more to render */ break; } if (cell_idx < glyph_offset) { /* We haven't yet reached the visible part of the string */ cell_idx = next_cell_idx; continue; } const struct fcft_glyph *glyph = fcft_rasterize_char_utf32(font, text[i], term->font_subpixel); if (glyph == NULL) { cell_idx = next_cell_idx; continue; } if (unlikely(glyph->is_color_glyph)) { pixman_image_composite32(PIXMAN_OP_OVER, glyph->pix, NULL, buf->pix[0], 0, 0, 0, 0, x + x_ofs + glyph->x, y + term->font_baseline - glyph->y, glyph->width, glyph->height); } else { int combining_ofs = width == 0 ? (glyph->x < 0 ? width * term->cell_width : (width - 1) * term->cell_width) : 0; /* Not a zero-width character - no additional offset */ 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, 0, x + x_ofs + combining_ofs + glyph->x, y + term->font_baseline - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } x += width * term->cell_width; cell_idx = next_cell_idx; } #if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED if (ime_seat != NULL && ime_seat->ime.preedit.cells != NULL) /* Already rendered */; else #endif if (term->search.cursor >= term->search.len) { draw_beam_cursor(term, buf->pix[0], font, &fg, x, y); term_ime_set_cursor_rect(term, WINDOW_X(x), WINDOW_Y(y), 1, term->cell_height); } /* Status string (toggles + counter) immediately to the left of * the close glyph */ { int sx = (int)width - (int)margin - (int)close_w - (int)close_gap - (int)status_w; const int sy = (int)margin; for (size_t i = 0; i < st_len; i++) { const struct fcft_glyph *g = fcft_rasterize_char_utf32(font, status[i], term->font_subpixel); int w = max(1, c32width(status[i])); if (g != NULL && !g->is_color_glyph) { pixman_image_t *src = pixman_image_create_solid_fill(&fg); pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0, 0, 0, 0, sx + x_ofs + g->x, sy + term->font_baseline - g->y, g->width, g->height); pixman_image_unref(src); } sx += w * (int)term->cell_width; } } /* Close glyph (×) at the top-right of the popup */ { const struct fcft_glyph *cg = fcft_rasterize_char_utf32(font, U'×', term->font_subpixel); if (cg != NULL) { const int cx = (int)width - (int)margin - (int)close_w; const int cy = (int)margin; pixman_image_t *src = pixman_image_create_solid_fill(&fg); pixman_image_composite32(PIXMAN_OP_OVER, src, cg->pix, buf->pix[0], 0, 0, 0, 0, cx + x_ofs + cg->x, cy + term->font_baseline - cg->y, cg->width, cg->height); pixman_image_unref(src); } } /* Session-load list */ if (term->search.mode == SEARCH_MODE_SESSION_LOAD) { const pixman_color_t sel_bg = color_hex_to_pixman( term->colors.table[4], gamma_correct); const size_t name_x = margin; const size_t name_w = width > 2 * margin ? width - 2 * margin : 0; if (term->session_picker.filtered_count == 0) { const char32_t *msg = U"(no sessions)"; int rx = (int)name_x; const int ry = (int)margin + (int)term->cell_height; for (size_t i = 0; msg[i] != U'\0'; i++) { const struct fcft_glyph *g = fcft_rasterize_char_utf32(font, msg[i], term->font_subpixel); int gw = max(1, c32width(msg[i])); if (g != NULL && !g->is_color_glyph) { pixman_image_t *src = pixman_image_create_solid_fill(&fg); pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0, 0, 0, 0, rx + x_ofs + g->x, ry + term->font_baseline - g->y, g->width, g->height); pixman_image_unref(src); } rx += gw * (int)term->cell_width; } } else { const size_t shown = min(list_rows, term->session_picker.filtered_count); /* Scroll the list so the selection is visible */ size_t first = 0; if (term->session_picker.sel >= shown) first = term->session_picker.sel - shown + 1; for (size_t r = 0; r < shown; r++) { size_t fi = first + r; if (fi >= term->session_picker.filtered_count) break; const char *name = term->session_picker.all[term->session_picker.filtered[fi]]; const int ry = (int)margin + (int)(1 + r) * (int)term->cell_height; const bool selected = fi == term->session_picker.sel; if (selected) { pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &sel_bg, 1, &(pixman_rectangle16_t){(int16_t)name_x, (int16_t)ry, (uint16_t)name_w, (uint16_t)term->cell_height}); } const pixman_color_t row_fg = selected ? color_hex_to_pixman(term->colors.bg, gamma_correct) : fg; int rx = (int)name_x; for (const char *p = name; *p != '\0'; p++) { char32_t ch = (char32_t)(unsigned char)*p; const struct fcft_glyph *g = fcft_rasterize_char_utf32(font, ch, term->font_subpixel); if (g != NULL && !g->is_color_glyph) { pixman_image_t *src = pixman_image_create_solid_fill(&row_fg); pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, buf->pix[0], 0, 0, 0, 0, rx + x_ofs + g->x, ry + term->font_baseline - g->y, g->width, g->height); pixman_image_unref(src); } rx += (int)term->cell_width; if (rx > (int)(name_x + name_w)) break; } } } } quirk_weston_subsurface_desync_on(term->window->search.sub); /* TODO: this is only necessary on a window resize */ wl_subsurface_set_position( term->window->search.sub, (int32_t)roundf(max(0, (int32_t)term->width - (int32_t)width - (int32_t)outer_margin) / scale), (int32_t)roundf(outer_margin / scale)); wayl_surface_scale(term->window, &term->window->search.surface, buf, scale); wl_surface_attach(term->window->search.surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(term->window->search.surface.surf, 0, 0, width, height); struct wl_region *region = wl_compositor_create_region(term->wl->compositor); if (region != NULL) { wl_region_add(region, 0, 0, width, height); wl_surface_set_opaque_region(term->window->search.surface.surf, region); wl_region_destroy(region); } wl_surface_commit(term->window->search.surface.surf); quirk_weston_subsurface_desync_off(term->window->search.sub); free(ime_alloc); free(masked_text); #undef WINDOW_X #undef WINDOW_Y } static void render_urls(struct terminal *term) { struct wl_window *win = term->window; xassert(tll_length(win->urls) > 0); const float scale = term->scale; const int x_margin = (int)roundf(2 * scale); const int y_margin = (int)roundf(1 * scale); /* Calculate view start, counted from the *current* scrollback start */ 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; const bool show_url = term->urls_show_uri_on_jump_label; /* * There can potentially be a lot of URLs. * * Since each URL is a separate sub-surface, and requires its own * SHM buffer, we may be allocating a lot of buffers. * * SHM buffers normally have their own, private SHM buffer * pool. Each pool is mmapped, and thus allocates *at least* * 4K. Since URL labels are typically small, we end up using an * excessive amount of both virtual and physical memory. * * For this reason, we instead use shm_get_many(), which uses a * single, shared pool for all buffers. * * To be able to use it, we need to have all the *all* the buffer * dimensions up front. * * Thus, the first iteration through the URLs do the heavy * lifting: builds the label contents and calculates both its * position and size. But instead of rendering the label * immediately, we store the calculated data, and then do a second * pass, where we first get all our buffers, and then render to * them. */ /* Positioning data + label contents */ struct { const struct wl_url *url; char32_t *text; int x; int y; } info[tll_length(win->urls)]; /* For shm_get_many() */ int widths[tll_length(win->urls)]; int heights[tll_length(win->urls)]; size_t render_count = 0; tll_foreach(win->urls, it) { const struct url *url = it->item.url; const char32_t *key = url->key; const size_t entered_key_len = c32len(term->url_keys); if (key == NULL) { /* TODO: if we decide to use the .text field, we cannot * just skip the entire jump label like this */ continue; } struct wl_surface *surf = it->item.surf.surface.surf; struct wl_subsurface *sub_surf = it->item.surf.sub; if (surf == NULL || sub_surf == NULL) continue; bool hide = false; const struct coord *pos = &url->range.start; const int _row = (pos->row - scrollback_end + term->grid->num_rows) & (term->grid->num_rows - 1); if (_row < view_start || _row > view_end) hide = true; if (c32len(key) <= entered_key_len) hide = true; if (c32ncasecmp(term->url_keys, key, entered_key_len) != 0) hide = true; if (hide) { wl_surface_attach(surf, NULL, 0, 0); wl_surface_commit(surf); continue; } int col = pos->col; int row = pos->row - term->grid->view; while (row < 0) row += term->grid->num_rows; row &= (term->grid->num_rows - 1); /* Position label slightly above and to the left */ int x = col * term->cell_width - 15 * term->cell_width / 10; int y = row * term->cell_height - 5 * term->cell_height / 10; /* Don't position it outside our window */ if (x < -term->margins.left) x = -term->margins.left; if (y < -term->margins.top) y = -term->margins.top; /* Maximum width of label, in pixels */ const int max_width = term->width - term->margins.left - term->margins.right - x; const int max_cols = max_width / term->cell_width; const size_t key_len = c32len(key); size_t url_len = mbstoc32(NULL, url->url, 0); if (url_len == (size_t)-1) url_len = 0; char32_t url_wchars[url_len + 1]; mbstoc32(url_wchars, url->url, url_len + 1); /* Format label, not yet subject to any size limitations */ size_t chars = key_len + (show_url ? (2 + url_len) : 0); char32_t label[chars + 1]; label[chars] = U'\0'; if (show_url) { c32cpy(label, key); c32cat(label, U": "); c32cat(label, url_wchars); } else c32ncpy(label, key, chars); /* Upper case the key characters */ for (size_t i = 0; i < c32len(key); i++) label[i] = toc32upper(label[i]); /* Blank already entered key characters */ for (size_t i = 0; i < entered_key_len; i++) label[i] = U' '; /* * Don't extend outside our window * * Truncate label so that it doesn't extend outside our * window. * * Do it in a way such that we don't cut the label in the * middle of a double-width character. */ int cols = 0; for (size_t i = 0; i <= c32len(label); i++) { int _cols = c32swidth(label, i); if (_cols == (size_t)-1) continue; if (_cols >= max_cols) { if (i > 0) label[i - 1] = U'…'; label[i] = U'\0'; cols = max_cols; break; } cols = _cols; } if (cols == 0) continue; int width = x_margin + cols * term->cell_width + x_margin; int height = y_margin + term->cell_height + y_margin; width = roundf(scale * ceilf(width / scale)); height = roundf(scale * ceilf(height / scale)); info[render_count].url = &it->item; info[render_count].text = xc32dup(label); info[render_count].x = x; info[render_count].y = y; widths[render_count] = width; heights[render_count] = height; render_count++; } struct buffer_chain *chain = term->render.chains.url; struct buffer *bufs[render_count]; shm_get_many(chain, render_count, widths, heights, bufs); uint32_t fg = term->conf->colors_dark.use_custom.jump_label ? term->conf->colors_dark.jump_label.fg : term->colors.table[0]; uint32_t bg = term->conf->colors_dark.use_custom.jump_label ? term->conf->colors_dark.jump_label.bg : term->colors.table[3]; for (size_t i = 0; i < render_count; i++) { const struct wayl_sub_surface *sub_surf = &info[i].url->surf; const char32_t *label = info[i].text; const int x = info[i].x; const int y = info[i].y; xassert(sub_surf->surface.surf != NULL); xassert(sub_surf->sub != NULL); wl_subsurface_set_position(sub_surf->sub, roundf((term->margins.left + x) / scale), roundf((term->margins.top + y) / scale)); render_osd(term, sub_surf, term->fonts[0], bufs[i], label, fg, 0xffu << 24 | bg, x_margin); free(info[i].text); } } static void render_update_title(struct terminal *term) { static const size_t max_len = 2048; const char *title = term->window_title != NULL ? term->window_title : "foot"; char *copy = NULL; if (strlen(title) > max_len) { copy = xstrndup(title, max_len); title = copy; } xdg_toplevel_set_title(term->window->xdg_toplevel, title); free(copy); } static void frame_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) { struct terminal *term = data; xassert(term->window->frame_callback == wl_callback); wl_callback_destroy(wl_callback); term->window->frame_callback = NULL; bool grid = term->render.pending.grid; bool csd = term->render.pending.csd; bool search = term->is_searching && term->render.pending.search; bool urls = urls_mode_is_active(term) > 0 && term->render.pending.urls; term->render.pending.grid = false; term->render.pending.csd = false; term->render.pending.search = false; term->render.pending.urls = false; struct grid *original_grid = term->grid; if (urls_mode_is_active(term)) { xassert(term->url_grid_snapshot != NULL); term->grid = term->url_grid_snapshot; } if (csd && term->window->csd_mode == CSD_YES) { quirk_weston_csd_on(term); render_csd(term); quirk_weston_csd_off(term); } if (search) render_search_box(term); if (urls) render_urls(term); if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) render_tab_overview(term); if ((grid && !term->delayed_render_timer.is_armed) || (csd | search | urls)) grid_render(term); tll_foreach(term->wl->seats, it) { if (it->item.ime_focus == term) ime_update_cursor_rect(&it->item); } term->grid = original_grid; } static void tiocswinsz(struct terminal *term) { if (term->ptmx >= 0) { if (ioctl(term->ptmx, (unsigned int)TIOCSWINSZ, &(struct winsize){.ws_row = term->rows, .ws_col = term->cols, .ws_xpixel = term->cols * term->cell_width, .ws_ypixel = term->rows * term->cell_height}) < 0) { LOG_ERRNO("TIOCSWINSZ"); } term_send_size_notification(term); } } static void delayed_reflow_of_normal_grid(struct terminal *term) { if (term->interactive_resizing.grid == NULL) return; xassert(term->interactive_resizing.new_rows > 0); struct coord *const tracking_points[] = { &term->selection.coords.start, &term->selection.coords.end, }; /* Reflow the original (since before the resize was started) grid, * to the *current* dimensions */ grid_resize_and_reflow( term->interactive_resizing.grid, term, term->interactive_resizing.new_rows, term->normal.num_cols, term->interactive_resizing.old_screen_rows, term->rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); /* Replace the current, truncated, "normal" grid with the * correctly reflowed one */ grid_free(&term->normal); term->normal = *term->interactive_resizing.grid; free(term->interactive_resizing.grid); term->hide_cursor = term->interactive_resizing.old_hide_cursor; /* Reset */ term->interactive_resizing.grid = NULL; term->interactive_resizing.old_screen_rows = 0; term->interactive_resizing.new_rows = 0; term->interactive_resizing.old_hide_cursor = false; /* Invalidate render pointers */ render_wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term->render.last_cursor.row = NULL; tll_free(term->normal.scroll_damage); sixel_reflow_grid(term, &term->normal); if (term->grid == &term->normal) { term_damage_view(term); render_refresh(term); } term_ptmx_resume(term); } static bool fdm_tiocswinsz(struct fdm *fdm, int fd, int events, void *data) { struct terminal *term = data; if (events & EPOLLIN) { tiocswinsz(term); delayed_reflow_of_normal_grid(term); } if (term->window->resize_timeout_fd >= 0) { fdm_del(fdm, term->window->resize_timeout_fd); term->window->resize_timeout_fd = -1; } return true; } static void send_dimensions_to_client(struct terminal *term) { struct wl_window *win = term->window; if (!win->is_resizing || term->conf->resize_delay_ms == 0) { /* Send new dimensions to client immediately */ tiocswinsz(term); delayed_reflow_of_normal_grid(term); /* And make sure to reset and deallocate a lingering timer */ if (win->resize_timeout_fd >= 0) { fdm_del(term->fdm, win->resize_timeout_fd); win->resize_timeout_fd = -1; } } else { /* Send new dimensions to client "in a while" */ assert(win->is_resizing && term->conf->resize_delay_ms > 0); int fd = win->resize_timeout_fd; uint16_t delay_ms = term->conf->resize_delay_ms; bool successfully_scheduled = false; if (fd < 0) { /* Lazy create timer fd */ fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK); if (fd < 0) LOG_ERRNO("failed to create TIOCSWINSZ timer"); else if (!fdm_add(term->fdm, fd, EPOLLIN, &fdm_tiocswinsz, term)) { close(fd); fd = -1; } win->resize_timeout_fd = fd; } if (fd >= 0) { /* Reset timeout */ const struct itimerspec timeout = { .it_value = { .tv_sec = delay_ms / 1000, .tv_nsec = (delay_ms % 1000) * 1000000, }, }; if (timerfd_settime(fd, 0, &timeout, NULL) < 0) { LOG_ERRNO("failed to arm TIOCSWINSZ timer"); fdm_del(term->fdm, fd); win->resize_timeout_fd = -1; } else successfully_scheduled = true; } if (!successfully_scheduled) { tiocswinsz(term); delayed_reflow_of_normal_grid(term); } } } static void set_size_from_grid(struct terminal *term, int *width, int *height, int cols, int rows) { int new_width, new_height; /* Nominal grid dimensions */ new_width = cols * term->cell_width; new_height = rows * term->cell_height; /* Include any configured padding */ new_width += (term->conf->pad_left + term->conf->pad_right) * term->scale; new_height += (term->conf->pad_top + term->conf->pad_bottom) * term->scale; /* Round to multiples of scale */ new_width = round(term->scale * round(new_width / term->scale)); new_height = round(term->scale * round(new_height / term->scale)); if (width != NULL) *width = new_width; if (height != NULL) *height = new_height; } /* Move to terminal.c? */ bool render_resize(struct terminal *term, int width, int height, uint8_t opts) { if (term->shutdown.in_progress) return false; if (!term->window->is_configured) return false; if (term->cell_width == 0 && term->cell_height == 0) return false; const bool is_floating = !term->window->is_maximized && !term->window->is_fullscreen && !term->window->is_tiled; /* Convert logical size to physical size */ const float scale = term->scale; width = round(width * scale); height = round(height * scale); /* If the grid should be kept, the size should be overridden */ if (is_floating && (opts & RESIZE_KEEP_GRID)) { set_size_from_grid(term, &width, &height, term->cols, term->rows); } if (width == 0) { /* The compositor is letting us choose the width */ if (term->stashed_width != 0) { /* If a default size is requested, prefer the "last used" size */ width = term->stashed_width; } else { /* Otherwise, use a user-configured size */ switch (term->conf->size.type) { case CONF_SIZE_PX: width = term->conf->size.width; if (wayl_win_csd_borders_visible(term->window)) width -= 2 * term->conf->csd.border_width_visible; width *= scale; break; case CONF_SIZE_CELLS: set_size_from_grid(term, &width, NULL, term->conf->size.width, term->conf->size.height); break; } } } if (height == 0) { /* The compositor is letting us choose the height */ if (term->stashed_height != 0) { /* If a default size is requested, prefer the "last used" size */ height = term->stashed_height; } else { /* Otherwise, use a user-configured size */ switch (term->conf->size.type) { case CONF_SIZE_PX: height = term->conf->size.height; /* Take CSDs into account */ if (wayl_win_csd_titlebar_visible(term->window)) height -= term->conf->csd.title_height; if (wayl_win_csd_borders_visible(term->window)) height -= 2 * term->conf->csd.border_width_visible; height *= scale; break; case CONF_SIZE_CELLS: set_size_from_grid(term, NULL, &height, term->conf->size.width, term->conf->size.height); break; } } } /* Don't shrink grid too much */ const int min_cols = 1; const int min_rows = 1; /* Minimum window size (must be divisible by the scaling factor)*/ const int min_width = roundf(scale * ceilf((min_cols * term->cell_width) / scale)); const int min_height = roundf(scale * ceilf((min_rows * term->cell_height) / scale)); width = max(width, min_width); height = max(height, min_height); /* Padding */ const int max_pad_x = (width - min_width) / 2; const int max_pad_y = (height - min_height) / 2; /* Total bar height = pill height + margin (margin only used in floating * layout) */ const uint16_t tabs_bar_logical = term->conf->tabs.enabled ? (term->conf->tabs.height + (term->conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING ? term->conf->tabs.margin : 0)) : 0; const int conf_pad_top = term->conf->pad_top + (term->conf->tabs.enabled && term->conf->tabs.position == CONF_TABS_POSITION_TOP ? tabs_bar_logical : 0); const int conf_pad_bottom = term->conf->pad_bottom + (term->conf->tabs.enabled && term->conf->tabs.position == CONF_TABS_POSITION_BOTTOM ? tabs_bar_logical : 0); const int pad_left = min(max_pad_x, scale * term->conf->pad_left); const int pad_right = min(max_pad_x, scale * term->conf->pad_right); const int pad_top = min(max_pad_y, scale * conf_pad_top); const int pad_bottom = min(max_pad_y, scale * conf_pad_bottom); if (is_floating && (opts & RESIZE_BY_CELLS) && term->conf->resize_by_cells) { /* If resizing in cell increments, restrict the width and height */ width = ((width - (pad_left + pad_right)) / term->cell_width) * term->cell_width + (pad_left + pad_right); width = max(min_width, roundf(scale * roundf(width / scale))); height = ((height - (pad_top + pad_bottom)) / term->cell_height) * term->cell_height + (pad_top + pad_bottom); height = max(min_height, roundf(scale * roundf(height / scale))); } if (!(opts & RESIZE_FORCE) && width == term->width && height == term->height && scale == term->scale) { return false; } /* Cancel an application initiated "Synchronized Update" */ term_disable_app_sync_updates(term); /* Drop out of URL mode */ urls_reset(term); LOG_DBG("resized: size=%dx%d (scale=%.2f)", width, height, term->scale); term->width = width; term->height = height; /* Screen rows/cols before resize */ int old_cols = term->cols; int old_rows = term->rows; /* Screen rows/cols after resize */ const int new_cols = (term->width - (pad_left + pad_right)) / term->cell_width; const int new_rows = (term->height - (pad_top + pad_bottom)) / term->cell_height; /* * Requirements for scrollback: * * a) total number of rows (visible + scrollback history) must be * a power of two * b) must be representable in a plain int (signed) * * This means that on a "normal" system, where ints are 32-bit, * the largest possible scrollback size is 1073741824 (0x40000000, * 1 << 30). * * The largest *signed* int is 2147483647 (0x7fffffff), which is * *not* a power of two. * * Note that these are theoretical limits. Most of the time, * you'll get a memory allocation failure when trying to allocate * the grid array. */ const unsigned max_scrollback = (INT_MAX >> 1) + 1; const unsigned scrollback_lines_not_yet_power_of_two = min( (uint64_t)term->render.scrollback_lines + new_rows - 1, max_scrollback); /* Grid rows/cols after resize */ const int new_normal_grid_rows = min(1u << (32 - __builtin_clz(scrollback_lines_not_yet_power_of_two)), max_scrollback); const int new_alt_grid_rows = min(1u << (32 - __builtin_clz(new_rows)), max_scrollback); LOG_DBG("grid rows: %d", new_normal_grid_rows); xassert(new_cols >= 1); xassert(new_rows >= 1); /* Margins */ const int grid_width = new_cols * term->cell_width; const int grid_height = new_rows * term->cell_height; const int total_x_pad = term->width - grid_width; const int total_y_pad = term->height - grid_height; const enum center_when center = term->conf->center_when; const bool centered_padding = center == CENTER_ALWAYS || (center == CENTER_MAXIMIZED_AND_FULLSCREEN && (term->window->is_fullscreen || term->window->is_maximized)) || (center == CENTER_FULLSCREEN && term->window->is_fullscreen); if (centered_padding && !term->window->is_resizing) { term->margins.left = max(pad_left, min(total_x_pad - pad_right, total_x_pad / 2)); term->margins.top = max(pad_top, min(total_y_pad - pad_bottom, total_y_pad / 2)); } else { term->margins.left = pad_left; term->margins.top = pad_top; } term->margins.right = total_x_pad - term->margins.left; term->margins.bottom = total_y_pad - term->margins.top; xassert(term->margins.left >= pad_left); xassert(term->margins.right >= pad_right); xassert(term->margins.top >= pad_top); xassert(term->margins.bottom >= pad_bottom); if (new_cols == old_cols && new_rows == old_rows) { LOG_DBG("grid layout unaffected; skipping reflow"); term->interactive_resizing.new_rows = new_normal_grid_rows; goto damage_view; } /* * Since text reflow is slow, don't do it *while* resizing. Only * do it when done, or after "pausing" the resize for sufficiently * long. We reuse the TIOCSWINSZ timer to handle this. See * send_dimensions_to_client() and fdm_tiocswinsz(). * * To be able to do the final reflow correctly, we need a copy of * the original grid, before the resize started. */ if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { if (term->interactive_resizing.grid == NULL) { term_ptmx_pause(term); /* Stash the current 'normal' grid, as-is, to be used when * doing the final reflow */ term->interactive_resizing.old_screen_rows = term->rows; term->interactive_resizing.old_cols = term->cols; term->interactive_resizing.old_hide_cursor = term->hide_cursor; term->interactive_resizing.grid = xmalloc(sizeof(*term->interactive_resizing.grid)); *term->interactive_resizing.grid = term->normal; if (term->grid == &term->normal) term->interactive_resizing.selection_coords = term->selection.coords; } else { /* We'll replace the current temporary grid, with a new * one (again based on the original grid) */ grid_free(&term->normal); } struct grid *orig = term->interactive_resizing.grid; /* * Copy the current viewport (of the original grid) to a new * grid that will be used during the resize. For now, throw * away sixels and OSC-8 URLs. They'll be "restored" when we * do the final reflow. * * Note that OSC-8 URLs are perfectly ok to throw away; they * cannot be interacted with during the resize. And, even if * url.osc8-underline=always, the "underline" attribute is * part of the cell, not the URI struct (and thus our faked * grid will still render OSC-8 links underlined). * * TODO: * - sixels? */ struct grid g = { .num_rows = 1 << (32 - __builtin_clz( term->interactive_resizing.old_screen_rows)), .num_cols = term->interactive_resizing.old_cols, .offset = 0, .view = 0, .cursor = orig->cursor, .saved_cursor = orig->saved_cursor, .rows = xcalloc(g.num_rows, sizeof(g.rows[0])), .cur_row = NULL, .scroll_damage = tll_init(), .sixel_images = tll_init(), .kitty_kbd = orig->kitty_kbd, }; term->selection.coords.start.row -= orig->view; term->selection.coords.end.row -= orig->view; for (size_t i = 0, j = orig->view; i < term->interactive_resizing.old_screen_rows; i++, j = (j + 1) & (orig->num_rows - 1)) { g.rows[i] = grid_row_alloc(g.num_cols, false); memcpy(g.rows[i]->cells, orig->rows[j]->cells, g.num_cols * sizeof(g.rows[i]->cells[0])); if (orig->rows[j]->extra == NULL || orig->rows[j]->extra->underline_ranges.count == 0) { continue; } /* * Copy underline ranges */ const struct row_ranges *underline_src = &orig->rows[j]->extra->underline_ranges; const int count = underline_src->count; g.rows[i]->extra = xcalloc(1, sizeof(*g.rows[i]->extra)); g.rows[i]->extra->underline_ranges.v = xmalloc(count * sizeof(g.rows[i]->extra->underline_ranges.v[0])); struct row_ranges *underline_dst = &g.rows[i]->extra->underline_ranges; underline_dst->count = underline_dst->size = count; for (int k = 0; k < count; k++) underline_dst->v[k] = underline_src->v[k]; } term->normal = g; term->hide_cursor = true; } if (term->grid == &term->alt) selection_cancel(term); else { /* * Don't cancel, but make sure there aren't any ongoing * selections after the resize. */ tll_foreach(term->wl->seats, it) { if (it->item.kbd_focus == term) selection_finalize(&it->item, term, it->item.pointer.serial); } } /* * TODO: if we remove the selection_finalize() call above (i.e. if * we start allowing selections to be ongoing across resizes), the * selection's pivot point coordinates *must* be added to the * tracking points list. */ /* Resize grids */ if (term->window->is_resizing && term->conf->resize_delay_ms > 0) { /* Simple truncating resize, *while* an interactive resize is * ongoing. */ xassert(term->interactive_resizing.grid != NULL); xassert(new_normal_grid_rows > 0); term->interactive_resizing.new_rows = new_normal_grid_rows; grid_resize_without_reflow(&term->normal, new_alt_grid_rows, new_cols, term->interactive_resizing.old_screen_rows, new_rows); } else { /* Full text reflow */ int old_normal_rows = old_rows; if (term->interactive_resizing.grid != NULL) { /* Throw away the current, truncated, "normal" grid, and * use the original grid instead (from before the resize * started) */ grid_free(&term->normal); term->normal = *term->interactive_resizing.grid; free(term->interactive_resizing.grid); term->hide_cursor = term->interactive_resizing.old_hide_cursor; term->selection.coords = term->interactive_resizing.selection_coords; old_normal_rows = term->interactive_resizing.old_screen_rows; term->interactive_resizing.grid = NULL; term->interactive_resizing.old_screen_rows = 0; term->interactive_resizing.new_rows = 0; term->interactive_resizing.old_hide_cursor = false; term->interactive_resizing.selection_coords = (struct range){{-1, -1}, {-1, -1}}; term_ptmx_resume(term); } struct coord *const tracking_points[] = { &term->selection.coords.start, &term->selection.coords.end, }; grid_resize_and_reflow( &term->normal, term, new_normal_grid_rows, new_cols, old_normal_rows, new_rows, term->selection.coords.end.row >= 0 ? ALEN(tracking_points) : 0, tracking_points); } grid_resize_without_reflow(&term->alt, new_alt_grid_rows, new_cols, old_rows, new_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; sixel_reflow(term); LOG_DBG("resized: grid: cols=%d, rows=%d " "(left-margin=%d, right-margin=%d, top-margin=%d, bottom-margin=%d)", term->cols, term->rows, term->margins.left, term->margins.right, term->margins.top, term->margins.bottom); if (term->scroll_region.start >= term->rows) term->scroll_region.start = 0; if (term->scroll_region.end > term->rows || term->scroll_region.end >= old_rows) { term->scroll_region.end = term->rows; } term->render.last_cursor.row = NULL; damage_view: /* Signal TIOCSWINSZ */ send_dimensions_to_client(term); if (is_floating) { /* Stash current size, to enable us to restore it when we're * being un-maximized/fullscreened/tiled */ term->stashed_width = term->width; term->stashed_height = term->height; } { const bool title_shown = wayl_win_csd_titlebar_visible(term->window); const bool border_shown = wayl_win_csd_borders_visible(term->window); const int title = title_shown ? roundf(term->conf->csd.title_height * scale) : 0; const int border = border_shown ? roundf(term->conf->csd.border_width_visible * scale) : 0; /* Must use surface logical coordinates (same calculations as in get_csd_data(), but with different inputs) */ const int toplevel_min_width = roundf(border / scale) + roundf(min_width / scale) + roundf(border / scale); const int toplevel_min_height = roundf(border / scale) + roundf(title / scale) + roundf(min_height / scale) + roundf(border / scale); const int toplevel_width = roundf(border / scale) + roundf(term->width / scale) + roundf(border / scale); const int toplevel_height = roundf(border / scale) + roundf(title / scale) + roundf(term->height / scale) + roundf(border / scale); const int x = roundf(-border / scale); const int y = roundf(-title / scale) - roundf(border / scale); xdg_toplevel_set_min_size(term->window->xdg_toplevel, toplevel_min_width, toplevel_min_height); xdg_surface_set_window_geometry(term->window->xdg_surface, x, y, toplevel_width, toplevel_height); } tll_free(term->normal.scroll_damage); tll_free(term->alt.scroll_damage); render_wait_for_preapply_damage(term); shm_unref(term->render.last_buf); term->render.last_buf = NULL; term_damage_view(term); render_refresh_csd(term); render_refresh_search(term); render_refresh(term); return true; } static void xcursor_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data); static const struct wl_callback_listener xcursor_listener = { .done = &xcursor_callback, }; bool render_xcursor_is_valid(const struct seat *seat, const char *cursor) { if (cursor == NULL) return false; if (seat->pointer.theme == NULL) return false; return wl_cursor_theme_get_cursor(seat->pointer.theme, cursor) != NULL; } static void render_xcursor_update(struct seat *seat) { /* If called from a frame callback, we may no longer have mouse focus */ if (!seat->mouse_focus) return; xassert(seat->pointer.shape != CURSOR_SHAPE_NONE); if (seat->pointer.shape == CURSOR_SHAPE_HIDDEN) { /* Hide cursor */ LOG_DBG("hiding cursor using client-side NULL-surface"); wl_surface_attach(seat->pointer.surface.surf, NULL, 0, 0); wl_pointer_set_cursor(seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, 0, 0); wl_surface_commit(seat->pointer.surface.surf); return; } const enum cursor_shape shape = seat->pointer.shape; const char *const xcursor = seat->pointer.last_custom_xcursor; if (seat->pointer.shape_device != NULL) { xassert(shape != CURSOR_SHAPE_CUSTOM || xcursor != NULL); const enum wp_cursor_shape_device_v1_shape custom_shape = (shape == CURSOR_SHAPE_CUSTOM && xcursor != NULL ? cursor_string_to_server_shape(xcursor, seat->wayl->shape_manager_version) : 0); if (shape != CURSOR_SHAPE_CUSTOM || custom_shape != 0) { xassert(custom_shape == 0 || shape == CURSOR_SHAPE_CUSTOM); const enum wp_cursor_shape_device_v1_shape wp_shape = custom_shape != 0 ? custom_shape : cursor_shape_to_server_shape(shape); LOG_DBG("setting %scursor shape using cursor-shape-v1", custom_shape != 0 ? "custom " : ""); wp_cursor_shape_device_v1_set_shape(seat->pointer.shape_device, seat->pointer.serial, wp_shape); return; } } LOG_DBG("setting %scursor shape using a client-side cursor surface", seat->pointer.shape == CURSOR_SHAPE_CUSTOM ? "custom " : ""); if (seat->pointer.cursor == NULL) { /* * Normally, we never get here with a NULL-cursor, because we * only schedule a cursor update when we succeed to load the * cursor image. * * However, it is possible that we did succeed to load an * image, and scheduled an update. But, *before* the scheduled * update triggers, the user mvoes the pointer, and we try to * load a new cursor image. This time failing. * * In this case, we have a NULL cursor, but the scheduled * update is still scheduled. */ return; } const float scale = seat->pointer.scale; struct wl_cursor_image *image = seat->pointer.cursor->images[0]; struct wl_buffer *buf = wl_cursor_image_get_buffer(image); wayl_surface_scale_explicit_width_height(seat->mouse_focus->window, &seat->pointer.surface, image->width, image->height, scale); wl_surface_attach(seat->pointer.surface.surf, buf, 0, 0); wl_pointer_set_cursor(seat->wl_pointer, seat->pointer.serial, seat->pointer.surface.surf, image->hotspot_x / scale, image->hotspot_y / scale); wl_surface_damage_buffer(seat->pointer.surface.surf, 0, 0, INT32_MAX, INT32_MAX); xassert(seat->pointer.xcursor_callback == NULL); seat->pointer.xcursor_callback = wl_surface_frame(seat->pointer.surface.surf); wl_callback_add_listener(seat->pointer.xcursor_callback, &xcursor_listener, seat); wl_surface_commit(seat->pointer.surface.surf); } static void xcursor_callback(void *data, struct wl_callback *wl_callback, uint32_t callback_data) { struct seat *seat = data; xassert(seat->pointer.xcursor_callback == wl_callback); wl_callback_destroy(wl_callback); seat->pointer.xcursor_callback = NULL; if (seat->pointer.xcursor_pending) { render_xcursor_update(seat); seat->pointer.xcursor_pending = false; } } static void fdm_hook_refresh_pending_terminals(struct fdm *fdm, void *data) { struct renderer *renderer = data; struct wayland *wayl = renderer->wayl; tll_foreach(renderer->wayl->terms, it) { struct terminal *term = it->item; if (unlikely(term->shutdown.in_progress || !term->window->is_configured)) continue; /* Skip inactive tabs - they process PTY data but don't render */ if (term->window->term != term) continue; bool grid = term->render.refresh.grid; bool csd = term->render.refresh.csd; bool search = term->is_searching && term->render.refresh.search; bool urls = urls_mode_is_active(term) && term->render.refresh.urls; if (!(grid | csd | search | urls)) continue; if (term->render.app_sync_updates.enabled && !(csd | search | urls)) continue; term->render.refresh.grid = false; term->render.refresh.csd = false; term->render.refresh.search = false; term->render.refresh.urls = false; if (term->window->frame_callback == NULL) { struct grid *original_grid = term->grid; if (urls_mode_is_active(term)) { xassert(term->url_grid_snapshot != NULL); term->grid = term->url_grid_snapshot; } if (csd && term->window->csd_mode == CSD_YES) { quirk_weston_csd_on(term); render_csd(term); quirk_weston_csd_off(term); } if (search) render_search_box(term); if (urls) render_urls(term); if (term->conf->tabs.enabled) render_tab_bar(term); if (term->conf->tabs.enabled && tab_overview_is_active(term->window)) render_tab_overview(term); if (grid | csd | search | urls) grid_render(term); tll_foreach(term->wl->seats, it) { if (it->item.ime_focus == term) ime_update_cursor_rect(&it->item); } term->grid = original_grid; } else { /* Tells the frame callback to render again */ term->render.pending.grid |= grid; term->render.pending.csd |= csd; term->render.pending.search |= search; term->render.pending.urls |= urls; if (grid && term->conf->tabs.enabled) render_tab_bar(term); if (grid && term->conf->tabs.enabled && tab_overview_is_active(term->window)) render_tab_overview(term); } } tll_foreach(wayl->seats, it) { if (it->item.pointer.xcursor_pending) { if (it->item.pointer.xcursor_callback == NULL) { render_xcursor_update(&it->item); it->item.pointer.xcursor_pending = false; } else { /* Frame callback will call render_xcursor_update() */ } } } } void render_refresh_title(struct terminal *term) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; struct timespec diff; timespec_sub(&now, &term->render.title.last_update, &diff); if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { const struct itimerspec timeout = { .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, }; timerfd_settime(term->render.title.timer_fd, 0, &timeout, NULL); } else { term->render.title.last_update = now; render_update_title(term); } render_refresh_csd(term); } void render_refresh_app_id(struct terminal *term) { struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; struct timespec diff; timespec_sub(&now, &term->render.app_id.last_update, &diff); if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { const struct itimerspec timeout = { .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, }; timerfd_settime(term->render.app_id.timer_fd, 0, &timeout, NULL); return; } const char *app_id = term->app_id != NULL ? term->app_id : term->conf->app_id; xdg_toplevel_set_app_id(term->window->xdg_toplevel, app_id); term->render.app_id.last_update = now; } void render_refresh_icon(struct terminal *term) { if (term->wl->toplevel_icon_manager == NULL) { LOG_DBG("compositor does not implement xdg-toplevel-icon: " "ignoring request to refresh window icon"); return; } struct timespec now; if (clock_gettime(CLOCK_MONOTONIC, &now) < 0) return; struct timespec diff; timespec_sub(&now, &term->render.icon.last_update, &diff); if (diff.tv_sec == 0 && diff.tv_nsec < 8333 * 1000) { const struct itimerspec timeout = { .it_value = {.tv_nsec = 8333 * 1000 - diff.tv_nsec}, }; timerfd_settime(term->render.icon.timer_fd, 0, &timeout, NULL); return; } const char *icon_name = term_icon(term); LOG_DBG("setting toplevel icon: %s", icon_name); struct xdg_toplevel_icon_v1 *icon = xdg_toplevel_icon_manager_v1_create_icon(term->wl->toplevel_icon_manager); xdg_toplevel_icon_v1_set_name(icon, icon_name); xdg_toplevel_icon_manager_v1_set_icon(term->wl->toplevel_icon_manager, term->window->xdg_toplevel, icon); xdg_toplevel_icon_v1_destroy(icon); term->render.icon.last_update = now; } void render_refresh(struct terminal *term) { term->render.refresh.grid = true; } void render_refresh_csd(struct terminal *term) { if (term->window->csd_mode == CSD_YES) term->render.refresh.csd = true; } void render_refresh_search(struct terminal *term) { if (term->is_searching) term->render.refresh.search = true; } void render_refresh_urls(struct terminal *term) { if (urls_mode_is_active(term)) term->render.refresh.urls = true; } void render_refresh_tab_bar(struct terminal *term) { if (term->conf->tabs.enabled) term->render.refresh.grid = true; /* triggers full re-render which includes tab bar */ } static void draw_rounded_corner_aa(pixman_image_t *pix, const pixman_color_t *color, int dx, int dy, int r, int cx_inside, int cy_inside) { if (r <= 0) return; const int stride = (r + 3) & ~3; /* A8 stride aligned to 4 bytes */ uint8_t *data = xcalloc(stride * r, sizeof(uint8_t)); /* Circle center in local (0..r, 0..r) coords of the corner tile. */ const float cx = cx_inside > 0 ? (float)r : 0.f; const float cy = cy_inside > 0 ? (float)r : 0.f; const float r_f = (float)r; for (int py = 0; py < r; py++) { for (int px = 0; px < r; px++) { const float ex = (float)px + 0.5f - cx; const float ey = (float)py + 0.5f - cy; const float dist = sqrtf(ex * ex + ey * ey); float cov = r_f - dist + 0.5f; if (cov <= 0.f) cov = 0.f; else if (cov >= 1.f) cov = 1.f; data[py * stride + px] = (uint8_t)(cov * 255.f + 0.5f); } } pixman_image_t *mask = pixman_image_create_bits(PIXMAN_a8, r, r, (uint32_t *)data, stride); pixman_image_t *src = pixman_image_create_solid_fill(color); pixman_image_composite32(PIXMAN_OP_OVER, src, mask, pix, 0, 0, 0, 0, dx, dy, r, r); pixman_image_unref(mask); pixman_image_unref(src); free(data); } static void draw_rounded_rect(pixman_image_t *pix, const pixman_color_t *color, int x, int y, int w, int h, int r, unsigned corners) { if (r <= 0 || w <= 0 || h <= 0) { pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x, y, w, h}); return; } r = min(r, min(w / 2, h / 2)); const bool tl = corners & 1; const bool tr = corners & 2; const bool bl = corners & 4; const bool br = corners & 8; /* Fill body regions. Corners handled separately if rounded. */ /* Top band */ pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x + (tl ? r : 0), y, w - (tl ? r : 0) - (tr ? r : 0), r}); /* Middle band (full width) */ pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x, y + r, w, h - 2 * r}); /* Bottom band */ pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x + (bl ? r : 0), y + h - r, w - (bl ? r : 0) - (br ? r : 0), r}); /* Square corners: fill the r×r square. */ if (!tl) pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x, y, r, r}); if (!tr) pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x + w - r, y, r, r}); if (!bl) pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x, y + h - r, r, r}); if (!br) pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, color, 1, &(pixman_rectangle16_t){x + w - r, y + h - r, r, r}); /* Rounded corners: anti-aliased quarter-circles. */ if (tl) draw_rounded_corner_aa(pix, color, x, y, r, 1, 1); if (tr) draw_rounded_corner_aa(pix, color, x + w - r, y, r, -1, 1); if (bl) draw_rounded_corner_aa(pix, color, x, y + h - r, r, 1, -1); if (br) draw_rounded_corner_aa(pix, color, x + w - r, y + h - r, r, -1, -1); } static int tab_label_build(char *buf, size_t bufsz, const struct terminal *tab, size_t idx) { const char *title = NULL; if (tab->window_title_has_been_set && tab->window_title != NULL) title = tab->window_title; else if (tab->cwd != NULL) { const char *slash = strrchr(tab->cwd, '/'); title = (slash != NULL && slash[1] != '\0') ? slash + 1 : tab->cwd; } int len = snprintf(buf, bufsz, "%zu: %s", idx + 1, title != NULL ? title : ""); if (len < 0) return 0; if ((size_t)len >= bufsz) len = (int)bufsz - 1; return len; } static int tab_label_width(struct fcft_font *font, const char *buf, int len) { if (font == NULL || len <= 0) return 0; int total = 0; const unsigned char *p = (const unsigned char *)buf; const unsigned char *end = p + len; while (p < end) { utf8proc_int32_t cp; utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); if (consumed <= 0 || cp < 0) { p++; continue; } p += consumed; const struct fcft_glyph *g = fcft_rasterize_char_utf32(font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); if (g != NULL) total += g->advance.x; } return total; } /* Width (px) of the unread indicator + trailing gap, or 0 if disabled */ static int tab_unread_indicator_width(struct fcft_font *font, const struct config *conf, bool has_unread, float scale) { if (!has_unread || font == NULL || conf->tabs.unread_indicator == NULL || conf->tabs.unread_indicator[0] == '\0') return 0; int w = tab_label_width(font, conf->tabs.unread_indicator, (int)strlen(conf->tabs.unread_indicator)); if (w <= 0) return 0; return w + (int)roundf(4 * scale); } /* Draw the unread indicator at (x, baseline_y); returns the x offset to * advance past the indicator (including trailing gap). */ static int render_tab_unread_indicator(pixman_image_t *pix, struct fcft_font *font, const struct config *conf, float scale, int x, int baseline_y, bool gamma_correct) { if (font == NULL || conf->tabs.unread_indicator == NULL || conf->tabs.unread_indicator[0] == '\0') return 0; const pixman_color_t fg = color_hex_to_pixman(conf->tabs.colors.unread_fg, gamma_correct); int gx = x; const unsigned char *p = (const unsigned char *)conf->tabs.unread_indicator; const unsigned char *end = p + strlen(conf->tabs.unread_indicator); while (p < end) { utf8proc_int32_t cp; utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); if (consumed <= 0 || cp < 0) { p++; continue; } p += consumed; const struct fcft_glyph *g = fcft_rasterize_char_utf32(font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); if (g == NULL) continue; if (pixman_image_get_format(g->pix) == PIXMAN_a8r8g8b8) { pixman_image_composite32(PIXMAN_OP_OVER, g->pix, NULL, pix, 0, 0, 0, 0, gx + g->x, baseline_y - g->y, g->width, g->height); } else { pixman_image_t *src = pixman_image_create_solid_fill(&fg); pixman_image_composite32(PIXMAN_OP_OVER, src, g->pix, pix, 0, 0, 0, 0, gx + g->x, baseline_y - g->y, g->width, g->height); pixman_image_unref(src); } gx += g->advance.x; } return (gx - x) + (int)roundf(4 * scale); } static void render_tab_label(pixman_image_t *pix, struct fcft_font *font, const pixman_color_t *fg, const struct terminal *tab, size_t idx, int x, int y, int max_x, float scale) { char label_buf[256]; int label_len = tab_label_build(label_buf, sizeof(label_buf), tab, idx); if (label_len <= 0 || font == NULL) return; const int pad = (int)roundf(tab->conf->tabs.label_padding * scale); int gx = x + pad; const int clip_x = max_x - pad; const unsigned char *p = (const unsigned char *)label_buf; const unsigned char *end = p + label_len; while (p < end) { utf8proc_int32_t cp; utf8proc_ssize_t consumed = utf8proc_iterate(p, end - p, &cp); if (consumed <= 0 || cp < 0) { p++; continue; } p += consumed; const struct fcft_glyph *glyph = fcft_rasterize_char_utf32(font, (uint32_t)cp, FCFT_SUBPIXEL_NONE); if (glyph == NULL) continue; if (gx + glyph->advance.x > clip_x) break; if (pixman_image_get_format(glyph->pix) == PIXMAN_a8r8g8b8) { pixman_image_composite32(PIXMAN_OP_OVER, glyph->pix, NULL, pix, 0, 0, 0, 0, gx + glyph->x, y - glyph->y, glyph->width, glyph->height); } else { pixman_image_t *src = pixman_image_create_solid_fill(fg); pixman_image_composite32(PIXMAN_OP_OVER, src, glyph->pix, pix, 0, 0, 0, 0, gx + glyph->x, y - glyph->y, glyph->width, glyph->height); pixman_image_unref(src); } gx += glyph->advance.x; } } /* Greyscale ramp indices for the gradient tab style. * The 256-color palette greyscale ramp lives at indices 232..255. */ #define GRADIENT_BAR_BG_IDX 234 #define GRADIENT_PILL_ACTIVE_IDX 250 #define GRADIENT_PILL_INACTIVE_IDX 240 #define GRADIENT_FG_ACTIVE_IDX 232 #define GRADIENT_FG_INACTIVE_IDX 250 /* Hardcoded fade level masks (braille bitmasks); index = fade level (1..6). * Mirrors the visual progression: ⠐ ⠡ ⡐ ⢔ ⣑ ⣪ */ static const uint8_t gradient_fade_masks[7] = { 0x00, 0x10, 0x21, 0x50, 0x94, 0xD1, 0xEA, }; /* Braille bit (0..7) → (col, row) within the 2-col × 4-row dot grid */ static const struct { uint8_t col, row; } gradient_dot_pos[8] = { {0, 0}, {0, 1}, {0, 2}, {1, 0}, {1, 1}, {1, 2}, {0, 3}, {1, 3}, }; static void draw_gradient_pill(pixman_image_t *pix, const struct terminal *term, uint8_t pill_idx, uint8_t bar_bg_idx, bool gamma_correct, int x, int y, int w, int h, float scale) { const int n_levels = (int)ALEN(gradient_fade_masks) - 1; /* 6 */ const int cell_w = max(2, (int)roundf(scale * 4)); const int fade_w = n_levels * cell_w; const pixman_color_t pill = color_hex_to_pixman(term->colors.table[pill_idx], gamma_correct); if (w <= 2 * fade_w) { pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &pill, 1, &(pixman_rectangle16_t){x, y, w, h}); return; } /* Solid pill interior */ pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, &pill, 1, &(pixman_rectangle16_t){x + fade_w, y, w - 2 * fade_w, h}); const int dot_size = max(1, (int)roundf(scale)); const float sub_w = (float)cell_w / 2.0f; const float sub_h = (float)h / 4.0f; const int inner = (int)pill_idx - 1; const int outer = (int)bar_bg_idx; const int range = inner - outer; const int t_den = max(1, n_levels - 1); for (int side = 0; side < 2; side++) { const bool left = (side == 0); for (int i = 1; i <= n_levels; i++) { /* i == 1 is the outermost cell, i == n_levels the innermost */ const int cell_x = left ? x + (i - 1) * cell_w : x + w - i * cell_w; const uint8_t band_idx = (uint8_t)(outer + (range * (i - 1) + t_den / 2) / t_den); const uint8_t dot_idx = band_idx > 233 ? band_idx - 2 : 232; const pixman_color_t band = color_hex_to_pixman(term->colors.table[band_idx], gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, pix, &band, 1, &(pixman_rectangle16_t){cell_x, y, cell_w, h}); const uint8_t mask = gradient_fade_masks[i]; const pixman_color_t dot = color_hex_to_pixman(term->colors.table[dot_idx], gamma_correct); pixman_rectangle16_t dot_rects[8]; int n_dots = 0; for (int b = 0; b < 8; b++) { if (!(mask & (1u << b))) continue; const int dx = (int)roundf((gradient_dot_pos[b].col + 0.5f) * sub_w) - dot_size / 2; const int dy = (int)roundf((gradient_dot_pos[b].row + 0.5f) * sub_h) - dot_size / 2; dot_rects[n_dots++] = (pixman_rectangle16_t){ cell_x + dx, y + dy, dot_size, dot_size, }; } if (n_dots > 0) pixman_image_fill_rectangles(PIXMAN_OP_SRC, pix, &dot, n_dots, dot_rects); } } } static void render_tab_bar(struct terminal *term) { struct wl_window *win = term->window; if (win->tab_bar.sub == NULL) return; const struct config *conf = term->conf; const float scale = term->scale; const bool floating = (conf->tabs.layout == CONF_TABS_LAYOUT_FLOATING); const bool pos_top = (conf->tabs.position == CONF_TABS_POSITION_TOP); /* pill height is always conf->tabs.height; total bar = pill + margin in * floating */ const int margin_px = floating ? (int)roundf(scale * conf->tabs.margin) : 0; const int tab_h_px = (int)roundf(scale * conf->tabs.height); const int total_h = tab_h_px + margin_px; const int width = term->width; if (width <= 0 || total_h <= 0) return; struct buffer_chain *chain = term->render.chains.tab_bar; struct buffer *buf = shm_get_buffer(chain, width, total_h); if (buf == NULL) return; const bool gamma_correct = wayl_do_linear_blending(win->term->wl, conf); const bool rounded = (conf->tabs.style == CONF_TABS_STYLE_ROUNDED); const bool gradient = (conf->tabs.style == CONF_TABS_STYLE_GRADIENT); const int r = rounded ? (int)roundf(scale * conf->tabs.corner_radius) : 0; /* Clear buffer: transparent for floating (shows terminal behind gaps), bg for * span. Gradient style overrides the configured bar bg with a fixed greyscale * ramp index. */ const pixman_color_t transparent = {0, 0, 0, 0}; const pixman_color_t bg_color = gradient ? color_hex_to_pixman(term->colors.table[GRADIENT_BAR_BG_IDX], gamma_correct) : color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct); pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], floating ? &transparent : &bg_color, 1, &(pixman_rectangle16_t){0, 0, width, total_h}); const size_t tab_count = win->tab_count; if (tab_count == 0) goto commit; struct fcft_font *font = term->fonts[0]; /* Per-tab layout arrays */ int tab_xs[TAB_MAX]; int tab_ws[TAB_MAX]; int tab_y, tab_h; const size_t n = min(tab_count, (size_t)TAB_MAX); if (floating) { const int pad_px = (int)roundf(scale * conf->tabs.tab_padding); const int max_tw = (int)roundf(scale * conf->tabs.tab_width); const int label_pad = (int)roundf(scale * conf->tabs.label_padding); const int min_tw = max(2 * label_pad, 1); /* Measure each tab's natural width (label + padding), capped at max_tw */ int natural_w[TAB_MAX]; int total_w = 0; for (size_t i = 0; i < n; i++) { char buf_s[256]; int len = tab_label_build(buf_s, sizeof(buf_s), win->tabs[i], i); int lw = tab_label_width(font, buf_s, len); int iw = tab_unread_indicator_width(font, conf, win->tabs[i]->has_unread, scale); int w = lw + iw + 2 * label_pad; if (w > max_tw) w = max_tw; if (w < min_tw) w = min_tw; natural_w[i] = w; total_w += w; } const int total_pads = pad_px * ((int)n - 1); int bar_w = total_w + total_pads; /* Shrink uniformly if we overflow the bar width */ if (bar_w > width && total_w > 0) { const int avail = max(width - total_pads, (int)n); int used = 0; for (size_t i = 0; i < n; i++) { int w = (int)((int64_t)natural_w[i] * avail / total_w); if (w < 1) w = 1; tab_ws[i] = w; used += w; } /* Distribute rounding residue to the first few tabs */ int residue = avail - used; for (size_t i = 0; i < n && residue > 0; i++, residue--) tab_ws[i]++; bar_w = avail + total_pads; } else { for (size_t i = 0; i < n; i++) tab_ws[i] = natural_w[i]; } int x_cur = (width - bar_w) / 2; for (size_t i = 0; i < n; i++) { tab_xs[i] = x_cur; x_cur += tab_ws[i] + pad_px; } tab_y = pos_top ? margin_px : 0; tab_h = tab_h_px; } else { const int base = width / (int)n; int x_cur = 0; for (size_t i = 0; i < n; i++) { tab_xs[i] = x_cur; tab_ws[i] = (i + 1 == n) ? (width - x_cur) : base; x_cur += base; } tab_y = 0; tab_h = tab_h_px; } /* Text baseline centered within the pill */ const int text_baseline = tab_y + (font ? (tab_h - (int)(font->ascent + font->descent)) / 2 + (int)font->ascent : tab_h / 2); for (size_t i = 0; i < n; i++) { struct terminal *tab = win->tabs[i]; const bool is_active = (i == win->active_tab); const int x = tab_xs[i]; const int w = tab_ws[i]; const pixman_color_t fg_color = gradient ? color_hex_to_pixman( term->colors.table[is_active ? GRADIENT_FG_ACTIVE_IDX : GRADIENT_FG_INACTIVE_IDX], gamma_correct) : color_hex_to_pixman(is_active ? conf->tabs.colors.active_fg : conf->tabs.colors.fg, gamma_correct); if (gradient) { const uint8_t pill_idx = is_active ? GRADIENT_PILL_ACTIVE_IDX : GRADIENT_PILL_INACTIVE_IDX; draw_gradient_pill(buf->pix[0], term, pill_idx, GRADIENT_BAR_BG_IDX, gamma_correct, x, tab_y, w, tab_h, scale); } else if (rounded) { /* Floating: all 4 corners rounded. Span: only the open edge rounded. */ const pixman_color_t tab_bg = color_hex_to_pixman( is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, gamma_correct); unsigned corners; if (floating) { corners = 0xf; /* all corners */ } else if (pos_top) { corners = 0x3; /* top-left + top-right */ } else { corners = 0xc; /* bottom-left + bottom-right */ } draw_rounded_rect(buf->pix[0], &tab_bg, x, tab_y, w, tab_h, r, corners); } else { const pixman_color_t tab_bg = color_hex_to_pixman( is_active ? conf->tabs.colors.active_bg : conf->tabs.colors.bg, gamma_correct); pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &tab_bg, 1, &(pixman_rectangle16_t){x, tab_y, w, tab_h}); } if (!floating && !is_active && i + 1 < n && !gradient) { const pixman_color_t sep = color_hex_to_pixman(conf->tabs.colors.bg, gamma_correct); pixman_image_fill_rectangles( PIXMAN_OP_SRC, buf->pix[0], &sep, 1, &(pixman_rectangle16_t){x + w - 1, tab_y + 2, 1, tab_h - 4}); } if (font != NULL) { int label_x = x; if (tab->has_unread) { const int pad = (int)roundf(conf->tabs.label_padding * scale); int adv = render_tab_unread_indicator(buf->pix[0], font, conf, scale, x + pad, text_baseline, gamma_correct); label_x = x + adv; } render_tab_label(buf->pix[0], font, &fg_color, tab, i, label_x, text_baseline, x + w, scale); } } /* Publish layout for hit-testing */ for (size_t i = 0; i < n; i++) { win->tab_layout.xs[i] = tab_xs[i]; win->tab_layout.ws[i] = tab_ws[i]; } win->tab_layout.y = tab_y; win->tab_layout.h = tab_h; win->tab_layout.count = n; commit:; if (tab_count == 0) win->tab_layout.count = 0; const int y_pos = pos_top ? 0 : term->height - total_h; wl_subsurface_set_position(win->tab_bar.sub, 0, (int)roundf(y_pos / scale)); wayl_surface_scale(win, &win->tab_bar.surface, buf, scale); wl_surface_attach(win->tab_bar.surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(win->tab_bar.surface.surf, 0, 0, width, total_h); if (!floating) { struct wl_region *region = wl_compositor_create_region(term->wl->compositor); if (region != NULL) { wl_region_add(region, 0, 0, width, total_h); wl_surface_set_opaque_region(win->tab_bar.surface.surf, region); wl_region_destroy(region); } } else { wl_surface_set_opaque_region(win->tab_bar.surface.surf, NULL); } wl_surface_commit(win->tab_bar.surface.surf); } /* === Tab overview === * * A full-window sub-surface that grids out all tabs as live downscaled * thumbnails, animating in/out with a 400ms ease-out-cubic on scale + * opacity. The animation duration matches libadwaita's Adw.TabOverview. */ #define TAB_OVERVIEW_DURATION_NS 400000000ull static uint64_t now_monotonic_ns(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000000000ull + (uint64_t)ts.tv_nsec; } static float ease_out_cubic(float t) { if (t <= 0.f) return 0.f; if (t >= 1.f) return 1.f; float u = 1.f - t; return 1.f - u * u * u; } bool tab_overview_is_active(const struct wl_window *win) { if (win == NULL) return false; const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; return s->target_open || s->progress > 0.f || s->visible; } void tab_overview_toggle(struct wl_window *win) { if (win == NULL || win->tab_overview.sub == NULL) return; /* Snapshot current progress so the animation continues smoothly even * if the user toggles mid-fade. */ struct terminal *active = win->tabs[win->active_tab]; if (active == NULL) return; win->tab_overview_state.target_open = !win->tab_overview_state.target_open; win->tab_overview_state.anim_from = win->tab_overview_state.progress; win->tab_overview_state.anim_start_ns = now_monotonic_ns(); win->tab_overview_state.sel_idx = win->active_tab; win->tab_overview_state.hover_idx = -1; render_refresh(active); } void tab_overview_close_instant(struct wl_window *win) { if (win == NULL || win->tab_overview.sub == NULL) return; __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; st->target_open = false; st->progress = 0.f; st->anim_from = 0.f; st->anim_start_ns = 0; st->hover_idx = -1; /* Hide the sub-surface immediately so the new active tab is visible * without going through the fade-out frames. */ if (st->visible) { wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); wl_surface_commit(win->tab_overview.surface.surf); st->visible = false; } st->card_count = 0; struct terminal *active = win->tabs[win->active_tab]; if (active != NULL) render_refresh(active); } void render_refresh_tab_overview(struct terminal *term) { if (term->conf->tabs.enabled) term->render.refresh.grid = true; } /* Compute the {cols, rows, card_w, card_h, gap, top_left} layout for n * tabs in a window of (win_w, win_h) buffer pixels. The card aspect ratio * mirrors the window's. */ static void overview_layout(int win_w, int win_h, size_t n, int *out_cols, int *out_rows, int *out_card_w, int *out_card_h, int *out_gap, int *out_origin_x, int *out_origin_y, int *out_title_h) { if (n == 0 || win_w <= 0 || win_h <= 0) { *out_cols = *out_rows = 0; *out_card_w = *out_card_h = 0; *out_gap = *out_origin_x = *out_origin_y = *out_title_h = 0; return; } const int margin = max(24, win_w / 32); const int gap = max(12, win_w / 64); const int title_h = max(18, win_h / 36); /* Pick column count that yields cards closest to window aspect ratio. * cols = round(sqrt(n * win_w/win_h)) is the standard heuristic. */ const float aspect = (float)win_w / (float)win_h; int cols = (int)roundf(sqrtf((float)n * aspect)); if (cols < 1) cols = 1; if ((size_t)cols > n) cols = (int)n; int rows = (int)((n + cols - 1) / cols); /* Compute card size that fits both axes */ int avail_w = win_w - 2 * margin - (cols - 1) * gap; int avail_h = win_h - 2 * margin - (rows - 1) * gap; if (avail_w < 1) avail_w = 1; if (avail_h < 1) avail_h = 1; int card_w = avail_w / cols; int card_h_from_w = (int)((int64_t)card_w * win_h / win_w); int card_h_avail = (avail_h - rows * title_h) / rows; if (card_h_avail < 1) card_h_avail = 1; int card_h; if (card_h_from_w <= card_h_avail) { card_h = card_h_from_w; } else { card_h = card_h_avail; card_w = (int)((int64_t)card_h * win_w / win_h); } if (card_w < 1) card_w = 1; if (card_h < 1) card_h = 1; const int total_w = cols * card_w + (cols - 1) * gap; const int total_h = rows * (card_h + title_h) + (rows - 1) * gap; const int origin_x = (win_w - total_w) / 2; const int origin_y = (win_h - total_h) / 2; *out_cols = cols; *out_rows = rows; *out_card_w = card_w; *out_card_h = card_h; *out_gap = gap; *out_origin_x = origin_x; *out_origin_y = origin_y; *out_title_h = title_h; } static void draw_card_thumbnail(pixman_image_t *dst, struct buffer *src_buf, int dst_x, int dst_y, int dst_w, int dst_h) { if (src_buf == NULL || src_buf->pix == NULL || src_buf->pix[0] == NULL || src_buf->width <= 0 || src_buf->height <= 0 || dst_w <= 0 || dst_h <= 0) return; pixman_image_t *src = src_buf->pix[0]; pixman_image_set_filter(src, PIXMAN_FILTER_BILINEAR, NULL, 0); pixman_transform_t xform; pixman_transform_init_scale( &xform, pixman_double_to_fixed((double)src_buf->width / dst_w), pixman_double_to_fixed((double)src_buf->height / dst_h)); pixman_image_set_transform(src, &xform); pixman_image_composite32(PIXMAN_OP_SRC, src, NULL, dst, 0, 0, 0, 0, dst_x, dst_y, dst_w, dst_h); /* Reset for subsequent normal use */ pixman_image_set_transform(src, NULL); pixman_image_set_filter(src, PIXMAN_FILTER_NEAREST, NULL, 0); } static void render_tab_overview(struct terminal *term) { struct wl_window *win = term->window; if (win->tab_overview.sub == NULL) return; __typeof__(win->tab_overview_state) *st = &win->tab_overview_state; /* Advance animation */ if (st->progress != (st->target_open ? 1.f : 0.f)) { const uint64_t now = now_monotonic_ns(); const uint64_t elapsed = now - st->anim_start_ns; float t = (float)elapsed / (float)TAB_OVERVIEW_DURATION_NS; if (t < 0.f) t = 0.f; if (t > 1.f) t = 1.f; const float eased = ease_out_cubic(t); const float target = st->target_open ? 1.f : 0.f; st->progress = st->anim_from + (target - st->anim_from) * eased; if (t >= 1.f) st->progress = target; } /* Hide sub-surface entirely when fully closed */ if (st->progress <= 0.f && !st->target_open) { if (st->visible) { wl_surface_attach(win->tab_overview.surface.surf, NULL, 0, 0); wl_surface_commit(win->tab_overview.surface.surf); st->visible = false; } st->card_count = 0; return; } const float scale = term->scale; const int width = term->width; const int height = term->height; if (width <= 0 || height <= 0) return; struct buffer_chain *chain = term->render.chains.tab_overview; struct buffer *buf = shm_get_buffer(chain, width, height); if (buf == NULL) return; const bool gamma_correct = wayl_do_linear_blending(term->wl, term->conf); /* Backdrop dim: black at progress*0.55 alpha */ { const uint16_t dim = (uint16_t)(0xffff * (0.55f * st->progress)); const pixman_color_t dim_color = {0, 0, 0, dim}; pixman_image_fill_rectangles(PIXMAN_OP_SRC, buf->pix[0], &dim_color, 1, &(pixman_rectangle16_t){0, 0, width, height}); } const size_t n = min(win->tab_count, (size_t)TAB_MAX); if (n == 0) goto commit; int cols, rows, card_w, card_h, gap, origin_x, origin_y, title_h; overview_layout(width, height, n, &cols, &rows, &card_w, &card_h, &gap, &origin_x, &origin_y, &title_h); /* Scale factor: 0.92 -> 1.0 over the animation */ const float card_scale = 0.92f + 0.08f * st->progress; const int sc_w = (int)roundf(card_w * card_scale); const int sc_h = (int)roundf(card_h * card_scale); const int sc_title_h = (int)roundf(title_h * card_scale); const int corner_r = max(6, (int)roundf(8.f * scale)); struct fcft_font *font = term->fonts[0]; /* Card opacity: ramps in faster than the backdrop */ const float card_alpha = st->progress; const uint16_t alpha16 = (uint16_t)(0xffff * card_alpha); /* Use the window/terminal palette to make cards feel native */ pixman_color_t card_bg = color_hex_to_pixman(term->colors.bg, gamma_correct); pixman_color_t card_fg = color_hex_to_pixman(term->colors.fg, gamma_correct); /* Distinct ring colors so the active tab is visually distinguishable * from the keyboard/hover-selected tab as the user moves through the grid. */ pixman_color_t ring_active = color_hex_to_pixman( term->conf->tabs.colors.overview_active_border, gamma_correct); pixman_color_t ring_select = color_hex_to_pixman( term->conf->tabs.colors.overview_select_border, gamma_correct); card_bg.alpha = alpha16; ring_active.alpha = alpha16; ring_select.alpha = alpha16; pixman_color_t title_bg = {0, 0, 0, (uint16_t)(0x8000 * card_alpha)}; st->card_count = n; for (size_t i = 0; i < n; i++) { const int col = (int)(i % (size_t)cols); const int row = (int)(i / (size_t)cols); /* Per-card layout (full size) */ const int cell_x = origin_x + col * (card_w + gap); const int cell_y = origin_y + row * (card_h + title_h + gap); /* Cache full-size geometry for hit-testing */ st->cards[i].x = cell_x; st->cards[i].y = cell_y; st->cards[i].w = card_w; st->cards[i].h = card_h + title_h; /* Scaled-around-center geometry for drawing */ const int draw_x = cell_x + (card_w - sc_w) / 2; const int draw_y = cell_y + (card_h + title_h - sc_h - sc_title_h) / 2; const bool is_active = (i == win->active_tab); const bool is_sel = (i == st->sel_idx); const bool is_hover = ((int)i == st->hover_idx); /* Card background */ draw_rounded_rect(buf->pix[0], &card_bg, draw_x, draw_y, sc_w, sc_h + sc_title_h, corner_r, 0xf); /* Thumbnail of the tab's last rendered grid */ struct terminal *tab = win->tabs[i]; if (tab != NULL && sc_w > 4 && sc_h > 4) { draw_card_thumbnail(buf->pix[0], tab->render.last_buf, draw_x + 2, draw_y + 2, sc_w - 4, sc_h - 4); } /* Title bar strip across the bottom of the card */ pixman_image_fill_rectangles( PIXMAN_OP_OVER, buf->pix[0], &title_bg, 1, &(pixman_rectangle16_t){draw_x, draw_y + sc_h, sc_w, sc_title_h}); if (font != NULL && tab != NULL) { const int baseline = draw_y + sc_h + (sc_title_h - (int)(font->ascent + font->descent)) / 2 + (int)font->ascent; const int label_pad = max(4, (int)roundf(4.f * scale)); render_tab_label(buf->pix[0], font, &card_fg, tab, i, draw_x + label_pad, baseline, draw_x + sc_w - label_pad, scale); } /* Highlights (drawn on top): a 2-3 px ring. * * Priority when a card is both active and selected/hovered: * draw the active ring (it's the persistent "you are here" * marker); the selection ring is layered on the inside so the * two colors are both visible. */ if (is_active || is_sel || is_hover) { const int ring = max(2, (int)roundf(2.f * scale)) + (is_active ? 1 : 0); pixman_color_t ring_color; if (is_active) { ring_color = ring_active; } else { ring_color = ring_select; if (is_hover && !is_sel) ring_color.alpha = (uint16_t)(0xc000 * card_alpha); else ring_color.alpha = alpha16; } /* Top */ pixman_image_fill_rectangles( PIXMAN_OP_OVER, buf->pix[0], &ring_color, 1, &(pixman_rectangle16_t){draw_x, draw_y, sc_w, ring}); /* Bottom */ pixman_image_fill_rectangles( PIXMAN_OP_OVER, buf->pix[0], &ring_color, 1, &(pixman_rectangle16_t){draw_x, draw_y + sc_h + sc_title_h - ring, sc_w, ring}); /* Left */ pixman_image_fill_rectangles( PIXMAN_OP_OVER, buf->pix[0], &ring_color, 1, &(pixman_rectangle16_t){draw_x, draw_y, ring, sc_h + sc_title_h}); /* Right */ pixman_image_fill_rectangles(PIXMAN_OP_OVER, buf->pix[0], &ring_color, 1, &(pixman_rectangle16_t){draw_x + sc_w - ring, draw_y, ring, sc_h + sc_title_h}); } } commit: wl_subsurface_set_position(win->tab_overview.sub, 0, 0); wayl_surface_scale(win, &win->tab_overview.surface, buf, scale); wl_surface_attach(win->tab_overview.surface.surf, buf->wl_buf, 0, 0); wl_surface_damage_buffer(win->tab_overview.surface.surf, 0, 0, width, height); wl_surface_set_opaque_region(win->tab_overview.surface.surf, NULL); wl_surface_commit(win->tab_overview.surface.surf); st->visible = true; /* Keep animating until we reach the target */ if (st->progress != (st->target_open ? 1.f : 0.f)) term->render.refresh.grid = true; } int tab_overview_hit_test(struct wl_window *win, int x_buf, int y_buf) { if (win == NULL || !tab_overview_is_active(win)) return -1; const __typeof__(win->tab_overview_state) *s = &win->tab_overview_state; for (size_t i = 0; i < s->card_count; i++) { const int x = s->cards[i].x; const int y = s->cards[i].y; const int w = s->cards[i].w; const int h = s->cards[i].h; if (x_buf >= x && x_buf < x + w && y_buf >= y && y_buf < y + h) return (int)i; } return -1; } bool render_xcursor_set(struct seat *seat, struct terminal *term, enum cursor_shape shape) { if (seat->pointer.theme == NULL && seat->pointer.shape_device == NULL) return false; if (seat->mouse_focus == NULL) { seat->pointer.shape = CURSOR_SHAPE_NONE; return true; } if (seat->mouse_focus != term) { /* This terminal doesn't have mouse focus */ return true; } if (seat->pointer.shape == shape && !(shape == CURSOR_SHAPE_CUSTOM && !streq(seat->pointer.last_custom_xcursor, term->mouse_user_cursor))) { return true; } if (shape == CURSOR_SHAPE_HIDDEN) { seat->pointer.cursor = NULL; free(seat->pointer.last_custom_xcursor); seat->pointer.last_custom_xcursor = NULL; } else if (seat->pointer.shape_device == NULL) { const char *const custom_xcursors[] = {term->mouse_user_cursor, NULL}; const char *const *xcursors = shape == CURSOR_SHAPE_CUSTOM ? custom_xcursors : cursor_shape_to_string(shape); xassert(xcursors[0] != NULL); seat->pointer.cursor = NULL; for (size_t i = 0; xcursors[i] != NULL; i++) { seat->pointer.cursor = wl_cursor_theme_get_cursor(seat->pointer.theme, xcursors[i]); if (seat->pointer.cursor != NULL) { LOG_DBG("loaded xcursor %s", xcursors[i]); break; } } if (seat->pointer.cursor == NULL) { LOG_ERR("failed to load xcursor pointer '%s', and all of its fallbacks", xcursors[0]); return false; } } else { /* Server-side cursors - no need to load anything */ } if (shape == CURSOR_SHAPE_CUSTOM) { free(seat->pointer.last_custom_xcursor); seat->pointer.last_custom_xcursor = xstrdup(term->mouse_user_cursor); } /* FDM hook takes care of actual rendering */ seat->pointer.shape = shape; seat->pointer.xcursor_pending = true; return true; } void render_buffer_release_callback(struct buffer *buf, void *data) { /* * Called from shm.c when a buffer is released * * We use it to pre-apply last-frame's damage to it, when we're * forced to double buffer (compositor doesn't release buffers * immediately). * * The timeline is thus: * 1. We render and push a new frame * 2. Some (hopefully short) time after that, the compositor releases the * previous buffer * 3. We're called, and kick off the thread that copies the changes from (1) * to the just freed buffer * 4. Time passes.... * 5. The compositor calls our frame callback, signalling to us that it's * time to start rendering the next frame * 6. Hopefully, our thread is already done with copying the changes, * otherwise we stall, waiting for it * 7. We render the frame as if the compositor does immediate releases. * * What's the gain? Reduced latency, by applying the previous * frame's damage as soon as possible, we shorten the time it * takes to render the frame after the frame callback. * * This means the compositor can, in theory, push the frame * callback closer to the vblank deadline, and thus reduce input * latency. Not all compositors (most, in fact?) don't adapt like * this, unfortunately. But some allows the user to manually * configure the deadline. */ struct terminal *term = data; if (likely(buf->age != 1)) return; if (likely(!term->render.preapply_last_frame_damage)) return; if (term->render.last_buf == NULL) return; if (term->render.last_buf->age != 0) return; if (buf->width != term->render.last_buf->width) return; if (buf->height != term->render.last_buf->height) return; xassert(term->render.workers.count > 0); xassert(term->render.last_buf != NULL); xassert(term->render.last_buf->age == 0); xassert(term->render.last_buf != buf); mtx_lock(&term->render.workers.preapplied_damage.lock); if (term->render.workers.preapplied_damage.buf != NULL) { mtx_unlock(&term->render.workers.preapplied_damage.lock); return; } xassert(term->render.workers.preapplied_damage.buf == NULL); term->render.workers.preapplied_damage.buf = buf; term->render.workers.preapplied_damage.start = (struct timespec){0}; term->render.workers.preapplied_damage.stop = (struct timespec){0}; mtx_unlock(&term->render.workers.preapplied_damage.lock); mtx_lock(&term->render.workers.lock); sem_post(&term->render.workers.start); xassert(tll_length(term->render.workers.queue) == 0); tll_push_back(term->render.workers.queue, -3); mtx_unlock(&term->render.workers.lock); }