#include "render.h"

#include <limits.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

#include <pthread.h>
#include <sys/epoll.h>
#include <sys/ioctl.h>
#include <sys/time.h>
#include <sys/timerfd.h>

#include "macros.h"
#if HAS_INCLUDE(<pthread_np.h>)
#include <pthread_np.h>
#define pthread_setname_np(thread, name) (pthread_set_name_np(thread, name), 0)
#elif defined(__NetBSD__)
#define pthread_setname_np(thread, name)                                       \
  pthread_setname_np(thread, "%s", (void *)name)
#endif

#include <presentation-time.h>
#include <wayland-cursor.h>
#include <xdg-shell.h>
#include <xdg-toplevel-icon-v1.h>

#include <fcft/fcft.h>

#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

  /* 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);
  const size_t wanted_visible_cells = max(20, total_cells);

  /*
   * 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.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);
  size_t height =
      min(term->height - 2 * outer_margin, margin + 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.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);
    }
  }

  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);

#if defined(FOOT_IME_ENABLED) && FOOT_IME_ENABLED
  free(text);
#endif
#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);
}
