From: Foxe Chen Date: Wed, 22 Jul 2026 09:31:53 +0000 (+0000) Subject: patch 9.2.0828: GTK4: hardware rendering can be improved X-Git-Tag: v9.2.0828^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=79a2e3403c70bbe5613bfc9b6554152c3c3e9cdb;p=thirdparty%2Fvim.git patch 9.2.0828: GTK4: hardware rendering can be improved Problem: GTK4: hardware rendering can be improved Solution: Refactor code (Foxe Chen) This change does the following: - Make each cell have its own array of glyphs, that represent it. Before glyphs were stored per span of drawn text. - Background color, invert blend is now stored as a single row pixel buffer, that is scaled up to size when rendering. No need to have logic for merging visual attributes together. - Underlines, undercurls, and strikethroughs use a mask outline, and a pixel buffer (similar to the above) that is opaque at parts where there are under decorations. This means having 100 individual undercurls that are all separated by spaces in a row will only result in a single mask node. - Render nodes are cached per row, because creating render nodes are cheap. The actual heavy stuff would be shaping text into glyphs, which are cached per cell. - Use PangoAttrFontFeatures to handle guiligatures option, instead of manually splitting draw calls, which is buggy and complex (some complex unicode characters are not rendered correctly). - Render block cursor separately, this prevents a full redraw of the text for the row the cursor is on (only if glyphs in cell underneath cursor did not change). Complex unicode characters that bleed outside of the cell should look better, since the background is always rendered before all glyphs, meaning they are not clipped off. fixes: #13435 closes: #20674 Signed-off-by: Foxe Chen Signed-off-by: Christian Brabandt --- diff --git a/src/gui.c b/src/gui.c index 7b08f786ff..3457c88f13 100644 --- a/src/gui.c +++ b/src/gui.c @@ -24,7 +24,9 @@ static void set_guifontwide(char_u *font_name); static void gui_check_pos(void); static void gui_reset_scroll_region(void); static void gui_outstr(char_u *, int); +#ifndef USE_GTK4_SNAPSHOT static int gui_screenchar(int off, int flags, guicolor_T fg, guicolor_T bg, int back); +#endif static int gui_outstr_nowrap(char_u *s, int len, int flags, guicolor_T fg, guicolor_T bg, int back); static void gui_delete_lines(int row, int count); static void gui_insert_lines(int row, int count); @@ -1354,13 +1356,28 @@ gui_update_cursor( */ if (!gui.in_focus) { +#ifdef USE_GTK4_SNAPSHOT + gui_gtk4_draw_cursor(cbg, cfg, -1, -1); +#else gui_mch_draw_hollow_cursor(cbg); +#endif return; } +#ifdef USE_GTK4_SNAPSHOT + // Make sure that character underneath is drawn again in case it is part of + // a ligature. + gui_redraw_block(gui.row, gui.col, gui.row, gui.col, GUI_MON_NOCLEAR); + // gui_redraw_block() will invalidate the cursor + gui.cursor_is_valid = true; +#endif + old_hl_mask = gui.highlight_mask; if (shape->shape == SHAPE_BLOCK) { +#ifdef USE_GTK4_SNAPSHOT + gui_gtk4_draw_cursor(cbg, cfg, 0, 0); +#else /* * Draw the text character with the cursor colors. Use the * character attributes plus the cursor attributes. @@ -1368,6 +1385,7 @@ gui_update_cursor( gui.highlight_mask = (cattr | attr); (void)gui_screenchar(LineOffset[gui.row] + gui.col, GUI_MON_IS_CURSOR | GUI_MON_NOCLEAR, cfg, cbg, 0); +#endif } else { @@ -1406,15 +1424,18 @@ gui_update_cursor( } #endif } +#ifdef USE_GTK4_SNAPSHOT + gui_gtk4_draw_cursor(cbg, cfg, cur_width, cur_height); +#else gui_mch_draw_part_cursor(cur_width, cur_height, cbg); +#endif #if defined(FEAT_RIGHTLEFT) if (col_off) --gui.col; #endif - // Doesn't seem to work for MSWindows. Not necessary when using - // GtkSnapshot, because everything is drawn in order in the snapshot - // vfunc. + // Doesn't seem to work for MSWindows. We call gui_redraw_block() above + // for GtkSnapshot. #if !defined(FEAT_GUI_MSWIN) && !defined(USE_GTK4_SNAPSHOT) gui.highlight_mask = ScreenAttrs[LineOffset[gui.row] + gui.col]; (void)gui_screenchar(LineOffset[gui.row] + gui.col, @@ -2212,6 +2233,7 @@ gui_outstr(char_u *s, int len) } } +#ifndef USE_GTK4_SNAPSHOT /* * Output one character (may be one or two display cells). * Caller must check for valid "off". @@ -2248,6 +2270,7 @@ gui_screenchar( enc_dbcs ? (*mb_ptr2len)(ScreenLines + off) : 1, flags, fg, bg, back); } +#endif #ifdef FEAT_GUI_GTK /* diff --git a/src/gui_gtk4.c b/src/gui_gtk4.c index a194edd760..b29b180d63 100644 --- a/src/gui_gtk4.c +++ b/src/gui_gtk4.c @@ -1439,11 +1439,16 @@ gui_gtk4_update_size(void) } # ifdef FEAT_NETBEANS_INTG - cairo_t * -gui_gtk4_get_multisign_context(int x, int y, int w, int h) + void +gui_gtk4_add_multisign( + cairo_surface_t *surf, + int row, + int col, + int width, + int height) { - return vim_draw_area_get_multisign_cairo( - VIM_DRAW_AREA(gui.drawarea), x, y, w, h); + vim_draw_area_add_multisign(VIM_DRAW_AREA(gui.drawarea), surf, row, col, + width, height); } # endif #else // USE_GTK4_SNAPSHOT @@ -1762,13 +1767,24 @@ gui_mch_insert_lines(int row, int num_lines) gtk_widget_queue_draw(gui.drawarea); } +#if defined(USE_GTK4_SNAPSHOT) || defined(PROTO) +/* + * See vim_draw_area_set_cursor() for more information + */ + void +gui_gtk4_draw_cursor(guicolor_T bg, guicolor_T fg, int w, int h) +{ + gui_mch_set_bg_color(bg); + gui_mch_set_fg_color(fg); + vim_draw_area_set_cursor(VIM_DRAW_AREA(gui.drawarea), w, h); + gtk_widget_queue_draw(gui.drawarea); +} +#endif + +#if !defined(USE_GTK4_SNAPSHOT) || defined(PROTO) void gui_mch_draw_hollow_cursor(guicolor_T color) { -#ifdef USE_GTK4_SNAPSHOT - gui_mch_set_fg_color(color); - vim_draw_area_set_hollow_cursor(VIM_DRAW_AREA(gui.drawarea)); -#else cairo_t *cr; int i = 1; @@ -1788,7 +1804,6 @@ gui_mch_draw_hollow_cursor(guicolor_T color) i * gui.char_width - 1, gui.char_height - 1); cairo_stroke(cr); cairo_destroy(cr); -#endif gtk_widget_queue_draw(gui.drawarea); } @@ -1796,10 +1811,6 @@ gui_mch_draw_hollow_cursor(guicolor_T color) void gui_mch_draw_part_cursor(int w, int h, guicolor_T color) { -#ifdef USE_GTK4_SNAPSHOT - gui_mch_set_fg_color(color); - vim_draw_area_set_part_cursor(VIM_DRAW_AREA(gui.drawarea), w, h); -#else cairo_t *cr; if (gui.surface == NULL) @@ -1818,10 +1829,10 @@ gui_mch_draw_part_cursor(int w, int h, guicolor_T color) w, h); cairo_fill(cr); cairo_destroy(cr); -#endif gtk_widget_queue_draw(gui.drawarea); } +#endif void gui_mch_flash(int msec) @@ -3517,12 +3528,13 @@ gui_mch_destroy_sign(void *sign) * ============================================================ */ +#ifndef USE_GTK4_SNAPSHOT /* * Ligature and text drawing support. * Ported from gui_gtk_x11.c (GTK3) to support 'guiligatures' in GTK4. */ -#define INSERT_PANGO_ATTR(Attribute, AttrList, Start, End) \ +# define INSERT_PANGO_ATTR(Attribute, AttrList, Start, End) \ G_STMT_START{ \ PangoAttribute *tmp_attr_; \ tmp_attr_ = (Attribute); \ @@ -3651,7 +3663,6 @@ setup_zero_width_cluster(PangoItem *item, PangoGlyphInfo *glyph, glyph->geometry.x_offset = -width + MAX(0, width - ink_rect.width) / 2; } -#ifndef USE_GTK4_SNAPSHOT /* * Draw a single glyph string segment: background, foreground, and fake bold. */ @@ -3737,7 +3748,6 @@ draw_under(int flags, int row, int col, int cells, cairo_t *cr) cairo_stroke(cr); } } -#endif /* * Draw a string of characters on the screen. @@ -3757,21 +3767,12 @@ gui_gtk_draw_string_ext( PangoGlyphString *glyphs; int column_offset = 0; int i; -#ifdef USE_GTK4_SNAPSHOT - gboolean s_alloced = FALSE; -#else GdkRectangle area; cairo_t *cr; -#endif - if (gui.text_context == NULL -#ifndef USE_GTK4_SNAPSHOT - || gui.surface == NULL -#endif - ) + if (gui.text_context == NULL || gui.surface == NULL) return len; -#ifndef USE_GTK4_SNAPSHOT // Restrict all drawing to the current screen line. area.x = gui.border_offset; area.y = FILL_Y(row); @@ -3781,7 +3782,6 @@ gui_gtk_draw_string_ext( cr = cairo_create(gui.surface); cairo_rectangle(cr, area.x, area.y, area.width, area.height); cairo_clip(cr); -#endif glyphs = pango_glyph_string_new(); @@ -3806,12 +3806,7 @@ gui_gtk_draw_string_ext( glyphs->log_clusters[i] = i; } -#ifdef USE_GTK4_SNAPSHOT - vim_draw_area_add_glyphs(VIM_DRAW_AREA(gui.drawarea), row, col, len, - flags, gui.ascii_font, glyphs); -#else draw_glyph_string(row, col, len, flags, gui.ascii_font, glyphs, cr); -#endif column_offset = len; } @@ -3827,17 +3822,8 @@ not_ascii:; // Safety check: pango crashes with invalid utf-8. if (!utf_valid_string(s, s + len)) { -#ifdef USE_GTK4_SNAPSHOT - // vim_draw_area_add_glyphs() also handles under decorations. Make - // "str" a string of spaces so that under decorations are still - // applied. - s = g_malloc(len); - memset(s, ' ', len); - s_alloced = TRUE; -#else column_offset = len; goto skipitall; -#endif } cluster_width = PANGO_SCALE * gui.char_width; @@ -3929,14 +3915,8 @@ not_ascii:; } } -#ifdef USE_GTK4_SNAPSHOT - vim_draw_area_add_glyphs(VIM_DRAW_AREA(gui.drawarea), - row, col + column_offset, item_cells, - flags, item->analysis.font, glyphs); -#else draw_glyph_string(row, col + column_offset, item_cells, - flags, item->analysis.font, glyphs, cr); -#endif + flags, item->analysis.font, glyphs, cr); pango_item_free(item); @@ -3946,23 +3926,18 @@ not_ascii:; pango_attr_list_unref(attr_list); } -#ifdef USE_GTK4_SNAPSHOT - if (s_alloced) - g_free(s); -#else skipitall: draw_under(flags, row, col, column_offset, cr); cairo_destroy(cr); -#endif pango_glyph_string_free(glyphs); - if (gui.drawarea != NULL) gtk_widget_queue_draw(gui.drawarea); return column_offset; } +#endif // !USE_GTK4_SNAPSHOT /* * Draw a string of characters on the screen using the current font and colors. @@ -3974,6 +3949,17 @@ skipitall: int gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags) { +#ifdef USE_GTK4_SNAPSHOT + int cells; + + cells = vim_draw_area_draw_string(VIM_DRAW_AREA(gui.drawarea), + row, col, s, len, flags); + + if (gui.drawarea != NULL) + gtk_widget_queue_draw(gui.drawarea); + + return cells; +#else char_u *conv_buf = NULL; int convlen; int len_sum; @@ -3986,11 +3972,7 @@ gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags) int is_utf8; char_u backup_ch; - if (gui.text_context == NULL -#ifndef USE_GTK4_SNAPSHOT - || gui.surface == NULL -#endif - ) + if (gui.text_context == NULL || gui.surface == NULL) return len; if (output_conv.vc_type != CONV_NONE) @@ -4090,6 +4072,7 @@ gui_gtk_draw_string(int row, int col, char_u *s, int len, int flags) } vim_free(conv_buf); return len_sum; +#endif } int diff --git a/src/gui_gtk4_da.c b/src/gui_gtk4_da.c index 8db224ea67..4a7211fd70 100644 --- a/src/gui_gtk4_da.c +++ b/src/gui_gtk4_da.c @@ -14,83 +14,148 @@ #include #include "gui_gtk4_da.h" -#define DRAW_NODE_DIRTY 1 // Draw node is dirty -#define DRAW_NODE_NOBG 2 // Don't create background node -#define DRAW_NODE_NOINK 4 // Draw node has no ink -#define DRAW_NODE_UNDER 8 // Has under decorations (for convenience) -#define DRAW_NODE_CLIP 16 // Text node should be clipped to draw node bounds. +#ifdef FEAT_IMAGE_GDK +/* + * Struct containing information about an image. This is designed to map well + * with how Vim handles the kitty graphics protocol. + */ typedef struct { - int refcount; - - PangoGlyphInfo *glyphs; - int n_glyphs; - char_u dnode_flags; // DRAW_NODE_* flags - GskRenderNode *node; // This is either a text node, or a container node - // (if there is more than one node). - - PangoFont *font; - GdkRGBA fg_color; - GdkRGBA bg_color; - GdkRGBA sp_color; - int flags; // DRAW_* flags + int id; + int zindex; + GskRenderNode *node; // Cached clip node, which has the texture node as its + // child. May be NULL +} DrawImage; +#endif - int start_col; - int n_cells; -} DrawNode; +#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS) +/* + * Used for sign icons and netbeans multisign indicator. It is in a DrawGlyphs + * so that it can move/be manipulated with the text. + */ +typedef struct +{ + GdkTexture *texture; + int width; + int height; +} DrawSign; +#endif -#define END_COL(dn) ((dn)->start_col + (dn)->n_cells - 1) -#define HAS_INK(r) ((r)->width != 0 || (r)->height != 0) +/* + * Temporary struct when drawing string representing visual attributes of a cell + */ +typedef struct +{ + uint32_t bg_color; + uint32_t fg_color; // Only used for underlines + uint32_t sp_color; + int draw_flags; // DRAW_* flags, only DRAW_UNDERL, DRAW_UNDERC, and + // DRAW_STRIKE are used. +} DrawAttr; /* - * Each cell holds its own reference to a draw node if any. A draw node may span - * multiple cells, which represents how many cells it takes up on screen. + * Represents the glyphs of a single cell. */ typedef struct { - DrawNode *dnode; // May be NULL - gboolean invert; // If this cell is inverted + int refcount; + + uint32_t fg_color; + PangoFont *font; + // If "font" is NULL, "dsign" is used, otherwise "glyphs" is used. + union + { + PangoGlyphInfo *glyphs; // May be NULL +#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS) + DrawSign *dsign; +#endif + }; + int n_glyphs; +} DrawGlyphs; + +typedef struct +{ + DrawGlyphs *dglyphs; } DrawCell; -#ifdef FEAT_IMAGE_GDK +typedef enum +{ + DRAW_LAYER_BACKGROUND, // Background color, buf stores bg color + DRAW_LAYER_TEXT, // Text glyphs + underline, buf stores fg color (for + // underline) + DRAW_LAYER_SPECIAL, // Undercurls and strikethrough, buf store special + // color + DRAW_LAYER_OVERLAY, // Invert blend, buf stores white color + N_DRAW_LAYERS +} DrawLayerType; + +typedef struct +{ + GskRenderNode *node; + gboolean dirty; + + // RGBA buffer, pixel size is number of cells in the row. Note that for the + // special layer, it is twice the size, first part is for undercurl, second + // part for strikethrough. + uint32_t *buf; +} DrawLayer; + +#define UNDERC_OFFSET(n_cols) (0) +#define STRIKE_OFFSET(n_cols) (n_cols) + +typedef struct +{ + DrawLayer dlayers[N_DRAW_LAYERS]; + DrawCell *cells; + int n_cells; // For convenience + int row; // For convenience + + // Pre generated mask of of under decorations + GskRenderNode *underl_mask; + GskRenderNode *underc_mask; + GskRenderNode *strike_mask; +} DrawRow; + +#define INSERT_PANGO_ATTR(Attribute, AttrList, Start, End) \ + do { \ + PangoAttribute *tmp_attr_; \ + tmp_attr_ = (Attribute); \ + tmp_attr_->start_index = (Start); \ + tmp_attr_->end_index = (End); \ + pango_attr_list_insert((AttrList), tmp_attr_); \ + } while (FALSE) +#define CELLS2PANGO(c) ((c) * gui.char_width * PANGO_SCALE) + /* - * Struct containing information about an image. This is designed to map well - * with how Vim handles the kitty graphics protocol. + * Attributes of cursor */ typedef struct { - int id; - int zindex; - GskRenderNode *node; // Cached clip node, which has the texture node as its - // child. May be NULL -} DrawImage; -#endif + gboolean draw; // If cursor should be drawn + int width; + int height; + GdkRGBA bg_color; + GdkRGBA fg_color; +} DrawCursor; struct _VimDrawArea { GtkWidget parent; - DrawCell *cells; // May be NULL, always check! - int n_rows; - int n_cols; + DrawRow *rows; - int bleed_right; + int n_rows; + int n_cols; - // Used for hollow and part style cursors. For the block cursor, that is - // simply rendered as a cell using vim_draw_area_add_glyphs(). May be NULL. - GskRenderNode *cursor_node; + int bleed_right; -#if defined(FEAT_SIGN_ICONS) - // Queue of sign icon render nodes. Icons at the end of the queue are drawn - // ontop of earlier ones. - GQueue *signs; -#endif + // Used for snapshot vfunc call when rendering text, kept around so there is + // no need to allocate a new one each time. + GArray *glyph_buf; + GPtrArray *node_buf; -#ifdef FEAT_NETBEANS_INTG - // Cairo render node for multi sign indicator for Netbeans. May be NULL - GskRenderNode *multisign_node; -#endif + DrawCursor cursor; #ifdef FEAT_IMAGE_GDK // Queue of DrawImage structs. Sorted in ascending order of zindex, so that @@ -99,30 +164,29 @@ struct _VimDrawArea #endif }; -#define GET_ROW(da, n) ((da)->cells + (da)->n_cols * (n)) - -G_DEFINE_TYPE(VimDrawArea, vim_draw_area, GTK_TYPE_WIDGET) - -#ifdef FEAT_IMAGE_GDK static void draw_image_free(DrawImage *dimg); -#endif +static void draw_row_init(DrawRow *drow, int row, int cols); +static void draw_row_clear(DrawRow *drow); +static void draw_row_dirty_layer(DrawRow *drow, DrawLayerType dlayer_t); static void vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot); +static GdkRGBA white_rgba = {1, 1, 1, 1}; + +G_DEFINE_TYPE(VimDrawArea, vim_draw_area, GTK_TYPE_WIDGET) + static void vim_draw_area_finalize(GObject *obj) { VimDrawArea *self = VIM_DRAW_AREA(obj); - // "multisign_node" and "cursor_node" will be freed in - // vim_draw_area_clear_block(). vim_draw_area_clear(self); + for (int r = 0; r < self->n_rows; r++) + draw_row_clear(self->rows + r); + g_free(self->rows); + + g_array_free(self->glyph_buf, TRUE); + g_ptr_array_free(self->node_buf, TRUE); - g_free(self->cells); -#ifdef FEAT_SIGN_ICONS - // vim_draw_area_clear_block() should have removed all the sign icons - assert(g_queue_is_empty(self->signs)); - g_queue_free(self->signs); -#endif #ifdef FEAT_IMAGE_GDK g_queue_free_full(self->images, (GDestroyNotify)draw_image_free); #endif @@ -142,18 +206,19 @@ vim_draw_area_class_init(VimDrawAreaClass *class) // Add a layout manager so it can handle child popovers gtk_widget_class_set_layout_manager_type(widget_class, GTK_TYPE_BIN_LAYOUT); - } static void vim_draw_area_init(VimDrawArea *self) { -#ifdef FEAT_SIGN_ICONS - self->signs = g_queue_new(); -#endif + self->bleed_right = -1; #ifdef FEAT_IMAGE_GDK self->images = g_queue_new(); #endif + self->glyph_buf = g_array_new(FALSE, FALSE, sizeof(PangoGlyphInfo)); + self->node_buf = g_ptr_array_new_with_free_func( + (GDestroyNotify)gsk_render_node_unref); + } GtkWidget * @@ -168,179 +233,730 @@ vim_draw_area_new(void) void vim_draw_area_set_size(VimDrawArea *self, int rows, int cols) { - if (self->cells != NULL && self->n_rows == rows && self->n_cols == cols) + if (self->rows != NULL && self->n_rows == rows && self->n_cols == cols) return; if (rows == 0 || cols == 0) return; vim_draw_area_clear(self); + for (int r = 0; r < self->n_rows; r++) + draw_row_clear(self->rows + r); + self->rows = g_realloc_n(self->rows, rows, sizeof(DrawRow)); + for (int r = 0; r < rows; r++) + draw_row_init(self->rows + r, r, cols); + self->n_rows = rows; self->n_cols = cols; - self->cells = g_realloc_n(self->cells, rows * cols, sizeof(DrawCell)); - memset(self->cells, 0, rows * (sizeof(DrawCell) * cols)); } - static void -node_unref(GskRenderNode *node) +/* + * Convert the GdkRGBA to a packed uint32_t. + */ + static uint32_t +rgba_to_u32(const GdkRGBA *rgba) { - if (node != NULL) - gsk_render_node_unref(node); + union { uint32_t val; uint8_t bytes[4]; } color; + + color.bytes[0] = (uint8_t)(CLAMP(rgba->red, 0.0, 1.0) * 255.0 + 0.5); + color.bytes[1] = (uint8_t)(CLAMP(rgba->green, 0.0, 1.0) * 255.0 + 0.5); + color.bytes[2] = (uint8_t)(CLAMP(rgba->blue, 0.0, 1.0) * 255.0 + 0.5); + color.bytes[3] = (uint8_t)(CLAMP(rgba->alpha, 0.0, 1.0) * 255.0 + 0.5); + + return color.val; +} + +/* + * Convert the 32 bit RGBA value to a GdkRGBA + */ + static GdkRGBA +u32_to_rgba(uint32_t val) +{ + union { uint32_t val; uint8_t bytes[4]; } packed; + GdkRGBA color; + + packed.val = val; + color.red = packed.bytes[0] / 255.0; + color.green = packed.bytes[1] / 255.0; + color.blue = packed.bytes[2] / 255.0; + color.alpha = packed.bytes[3] / 255.0; + return color; } /* - * Return TRUE if "glyphs" take up space (not entirely whitespace). + * Return TRUE if "rgb" equals "rgba", ignoring the alpha channel. */ static gboolean -glyphs_has_ink(PangoFont *font, const PangoGlyphInfo *glyphs, int n_glyphs) +color_equal_u32(guicolor_T rgb, uint32_t rgba) { - for (int i = 0; i < n_glyphs; i++) + union { uint32_t val; uint8_t bytes[4]; } packed; + guicolor_T res; + + packed.val = rgba; + res = ((guicolor_T)packed.bytes[0] << 16) | + ((guicolor_T)packed.bytes[1] << 8) | + packed.bytes[2]; + return res == rgb; +} + +/* + * Checks if a RGBA pixel buffer is fully transparent. + */ + static inline gboolean +rgba_is_transparent(const uint32_t *buf, int n_pixels) +{ + // Could also compare 2 pixels at once using a 64 bit integer, but not sure + // if that might cause alignment issues... + for (int i = 0; i < n_pixels; i++) + if (buf[i] != 0) + return FALSE; + return TRUE; +} + +/* + * Return TRUE if "glyph" has ink (not whitespace). + */ + static gboolean +glyph_has_ink(PangoFont *font, PangoGlyphInfo glyph) +{ + PangoRectangle ink; + + pango_font_get_glyph_extents (font, glyph.glyph, &ink, NULL); + + return ink.width > 0 && ink.height > 0; +} + +/* + * Apply the 'guifontwide' font to double-width characters in the string. + */ + static void +apply_wide_font_attr(char_u *s, int len, PangoAttrList *attr_list) +{ + char_u *start = NULL; + char_u *p; + int uc; + + for (p = s; p < s + len; p += utf_byte2len(*p)) { - PangoRectangle glyph_ink; + uc = utf_ptr2char(p); - pango_font_get_glyph_extents (font, glyphs[i].glyph, &glyph_ink, NULL); + if (start == NULL) + { + if (uc >= 0x80 && utf_char2cells(uc) == 2) + start = p; + } + else if (uc < 0x80 + || (utf_char2cells(uc) != 2 && !utf_iscomposing(uc))) + { + INSERT_PANGO_ATTR(pango_attr_font_desc_new(gui.wide_font), + attr_list, start - s, p - s); + start = NULL; + } + } - if (HAS_INK(&glyph_ink)) - return TRUE; + if (start != NULL) + INSERT_PANGO_ATTR(pango_attr_font_desc_new(gui.wide_font), + attr_list, start - s, len); +} + +/* + * Count the number of display cells occupied by a glyph cluster. + */ + static int +count_cluster_cells( + char_u *s, + PangoItem *item, + PangoGlyphString *glyphs, + int i, + int *cluster_width, + int *last_glyph_rbearing) +{ + char_u *p; + int next; + int start, end; + int width; + int uc; + int cellcount = 0; + + width = glyphs->glyphs[i].geometry.width; + + for (next = i + 1; next < glyphs->num_glyphs; ++next) + { + if (glyphs->glyphs[next].attr.is_cluster_start) + break; + else if (glyphs->glyphs[next].geometry.width > width) + width = glyphs->glyphs[next].geometry.width; } - return FALSE; + + start = item->offset + glyphs->log_clusters[i]; + end = item->offset + ((next < glyphs->num_glyphs) ? + glyphs->log_clusters[next] : item->length); + + for (p = s + start; p < s + end; p += utf_byte2len(*p)) + { + uc = utf_ptr2char(p); + if (uc < 0x80) + ++cellcount; + else if (!utf_iscomposing(uc)) + cellcount += utf_char2cells(uc); + } + + if (last_glyph_rbearing != NULL + && cellcount > 0 && next == glyphs->num_glyphs) + { + PangoRectangle ink_rect; + + pango_font_get_glyph_extents(item->analysis.font, + glyphs->glyphs[i].glyph, + &ink_rect, NULL); + + if (PANGO_RBEARING(ink_rect) > 0) + *last_glyph_rbearing = PANGO_RBEARING(ink_rect); + } + + if (cellcount > 0) + *cluster_width = width; + + return cellcount; } /* - * Realloc "glyphs" to "n_glyphs" and return the new reallocated pointer. + * Handle combining characters that form a zero-width cluster. */ - static PangoGlyphInfo * -glyphs_resize(PangoGlyphInfo *glyphs, int n_glyphs) + static void +setup_zero_width_cluster( + PangoItem *item, + PangoGlyphInfo *glyph, + int last_cellcount, + int last_cluster_width, + int last_glyph_rbearing) { - return g_realloc_n(glyphs, n_glyphs, sizeof(PangoGlyphInfo)); + PangoRectangle ink_rect; + PangoRectangle logical_rect; + int width; + + width = last_cellcount * gui.char_width * PANGO_SCALE; + glyph->geometry.x_offset = -width + MAX(0, width - last_cluster_width) / 2; + glyph->geometry.width = 0; + + pango_font_get_glyph_extents(item->analysis.font, + glyph->glyph, + &ink_rect, &logical_rect); + if (ink_rect.x < 0) + { + glyph->geometry.x_offset += last_glyph_rbearing; + glyph->geometry.y_offset = logical_rect.height + - (gui.char_height - p_linespace) * PANGO_SCALE; + } + else + glyph->geometry.x_offset = -width + MAX(0, width - ink_rect.width) / 2; +} + +#ifdef FEAT_IMAGE_GDK + static void +draw_image_free(DrawImage *dimg) +{ + gsk_render_node_unref(dimg->node); + g_free(dimg); } +#endif + +#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS) /* - * Return TRUE if "bg" is the same as the default background color. + * Create a new draw sign icon with the given texture. */ - static gboolean -color_is_default_bg(const GdkRGBA *bg) + static DrawSign * +draw_sign_new(GdkTexture *texture, int width, int height) { - guicolor_T bgc = ((guicolor_T)(bg->red * 255) << 16) - | ((guicolor_T)(bg->green * 255) << 8) - | (guicolor_T)(bg->blue * 255); - return bgc == gui.back_pixel; + DrawSign *dsign = g_new0(DrawSign, 1); + + dsign->texture = g_object_ref(texture); + dsign->width = width; + dsign->height = height; + + return dsign; +} + + static void +draw_sign_free(DrawSign *dsign) +{ + g_object_unref(dsign->texture); + g_free(dsign); +} +#endif + + static void +draw_attr_init( + DrawAttr *dattr, + const GdkRGBA *bg_color, + const GdkRGBA *fg_color, + const GdkRGBA *sp_color, + int draw_flags) +{ + dattr->bg_color = rgba_to_u32(bg_color); + dattr->fg_color = rgba_to_u32(fg_color); + dattr->sp_color = rgba_to_u32(sp_color); + dattr->draw_flags = draw_flags & (DRAW_UNDERL | DRAW_UNDERC | DRAW_STRIKE); } /* - * Convert the given cell offset into an index in the "glyphs" array. + * Apply the draw attr to the row for the given col/cell. Only dirty layers that + * have actually changed. */ - static int -cell_offset_to_glyph(const PangoGlyphInfo *glyphs, int n_glyphs, int cell_offset) + static void +draw_attr_apply(DrawAttr *dattr, int col, DrawRow *drow) { - int cells_seen = 0; + uint32_t *bg_cell = drow->dlayers[DRAW_LAYER_BACKGROUND].buf + col; + uint32_t *text_cell = drow->dlayers[DRAW_LAYER_TEXT].buf + col; + uint32_t *underc_cell; + uint32_t *strike_cell; + uint32_t fg_color = 0, bg_color = 0, sp_color = 0; + int draw_flags = 0; + gboolean bg_is_default = TRUE; + + underc_cell = drow->dlayers[DRAW_LAYER_SPECIAL].buf + + col + UNDERC_OFFSET(drow->n_cells); + strike_cell = drow->dlayers[DRAW_LAYER_SPECIAL].buf + + col + STRIKE_OFFSET(drow->n_cells); + + // If "dattr" is NULL, then make the pixels transparent. + if (dattr != NULL) + { + fg_color = dattr->fg_color; + bg_color = dattr->bg_color; + sp_color = dattr->sp_color; + draw_flags = dattr->draw_flags; + bg_is_default = color_equal_u32(gui.back_pixel, bg_color); + } - for (int i = 0; i < n_glyphs; i++) + if (*bg_cell != bg_color && !(*bg_cell == 0 && bg_is_default)) { - const PangoGlyphInfo *glyph = glyphs + i; + draw_row_dirty_layer(drow, DRAW_LAYER_BACKGROUND); - if (cells_seen >= cell_offset) - return i; + // If "bg_color" is same as global background color, then use + // transparent color. + if (bg_is_default) + *bg_cell = 0; + else + *bg_cell = bg_color; + } + if ((draw_flags & DRAW_UNDERL && *text_cell != fg_color) + || (!(draw_flags & DRAW_UNDERL) && *text_cell != 0)) + { + draw_row_dirty_layer(drow, DRAW_LAYER_TEXT); + if (draw_flags & DRAW_UNDERL) + *text_cell = fg_color; + else + *text_cell = 0; + } + if ((draw_flags & DRAW_UNDERC && *underc_cell != sp_color) + || (!(draw_flags & DRAW_UNDERC) && *underc_cell != 0)) + { + draw_row_dirty_layer(drow, DRAW_LAYER_SPECIAL); + if (draw_flags & DRAW_UNDERC) + *underc_cell = sp_color; + else + *underc_cell = 0; + } + if ((draw_flags & DRAW_STRIKE && *strike_cell != sp_color) + || (!(draw_flags & DRAW_STRIKE) && *strike_cell != 0)) + { + draw_row_dirty_layer(drow, DRAW_LAYER_SPECIAL); + if (draw_flags & DRAW_STRIKE) + *strike_cell = sp_color; + else + *strike_cell = 0; + } +} - cells_seen += glyph->geometry.width / (gui.char_width * PANGO_SCALE); + static DrawGlyphs * +draw_glyphs_new( + PangoFont *font, + PangoGlyphInfo *glyphs, + int n_glyphs, + uint32_t fg_color) +{ + DrawGlyphs *dglyphs = g_new0(DrawGlyphs, 1); + + dglyphs->refcount = 1; + dglyphs->font = g_object_ref(font); + dglyphs->glyphs = g_memdup2(glyphs, sizeof(*glyphs) * n_glyphs); + dglyphs->n_glyphs = n_glyphs; + dglyphs->fg_color = fg_color; + + return dglyphs; +} + +#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS) +/* + * Create a new draw glyphs that shows the given sign. Note that ownership of + * "dsign" is taken. + */ + static DrawGlyphs * +draw_glyphs_new_sign(DrawSign *dsign) +{ + DrawGlyphs *dglyphs = g_new0(DrawGlyphs, 1); + + dglyphs->refcount = 1; + dglyphs->dsign = dsign; + + return dglyphs; +} +#endif + + static DrawGlyphs * +draw_glyphs_ref(DrawGlyphs *dglyphs) +{ + dglyphs->refcount++; + return dglyphs; +} + + static void +draw_glyphs_unref(DrawGlyphs *dglyphs) +{ + if (--dglyphs->refcount <= 0) + { +#if defined(FEAT_NETBEANS_INTG) || defined(FEAT_SIGN_ICONS) + if (dglyphs->font == NULL) + draw_sign_free(dglyphs->dsign); + else +#endif + { + g_free(dglyphs->glyphs); + g_object_unref(dglyphs->font); + } + g_free(dglyphs); } - return n_glyphs; } /* - * Create a new under decoration node with the given flags. Returns NULL if no - * under decorations are needed. + * Return TRUE if "a" and "b" are same. If either are NULL, return FALSE, if + * both are NULL, return TRUE. + */ + static gboolean +draw_glyphs_same(DrawGlyphs *a, DrawGlyphs *b) +{ + if (a == NULL && b == NULL) + return TRUE; + if (a == NULL || b == NULL) + return FALSE; + if (a->fg_color != b->fg_color) + return FALSE; + if (a->n_glyphs != b->n_glyphs || a->font != b->font) + return FALSE; + for (int i = 0; i < a->n_glyphs; i++) + if (a->glyphs[i].glyph != b->glyphs[i].glyph + || a->glyphs[i].geometry.width != b->glyphs[i].geometry.width + || a->glyphs[i].geometry.x_offset != b->glyphs[i].geometry.x_offset + || a->glyphs[i].geometry.y_offset != b->glyphs[i].geometry.y_offset + || a->glyphs[i].attr.is_cluster_start != b->glyphs[i].attr.is_cluster_start) + return FALSE; + return TRUE; +} + +/* + * Convert the buffer for the given layer to a texture node, scaled up to the + * correct size. "off" is the pixel offset in the buffer to start from, and + * "bleed" is TRUE, then expand the buffer by 1 cell (clipped by draw area + * width), using the color of the rightmost cell. */ static GskRenderNode * -create_under_decor_node( - int row, - int start_col, - int n_cells, - int flags, - const GdkRGBA *fg_color, - const GdkRGBA *sp_color) +draw_layer_get_texture( + DrawLayer *dlayer, + int row, + VimDrawArea *da, + int off, + gboolean bleed) { - GskRenderNode *nodes[3]; - int n_nodes = 0; - GskRenderNode *container; + GBytes *bytes; + GdkTexture *texture; + GskRenderNode *node; + + if (bleed) + { + GByteArray *arr = g_byte_array_new(); + + g_byte_array_append(arr, (uint8_t *)(dlayer->buf + off), + sizeof(uint32_t) * da->n_cols); + g_byte_array_append(arr, + (uint8_t *)(dlayer->buf + off + (da->n_cols - 1)), + sizeof(uint32_t)); // Add bleed using last pixel of buffer + bytes = g_byte_array_free_to_bytes(arr); + } + else + bytes = g_bytes_new(dlayer->buf + off, sizeof(uint32_t) * da->n_cols); + + texture = gdk_memory_texture_new(da->n_cols + bleed, 1, + GDK_MEMORY_R8G8B8A8, bytes, (da->n_cols + bleed) * 4); + g_bytes_unref(bytes); + + // Scale texture to actual size + node = gsk_texture_scale_node_new(texture, + &GRAPHENE_RECT_INIT(FILL_X(0), FILL_Y(row), + (da->n_cols + bleed) * gui.char_width, gui.char_height), + GSK_SCALING_FILTER_NEAREST); + if (bleed) + { + GskRenderNode *new; + + new = gsk_clip_node_new(node, + &GRAPHENE_RECT_INIT(FILL_X(0), FILL_Y(row), + da->n_cols * gui.char_width + da->bleed_right, + gui.char_height)); + gsk_render_node_unref(node); + node = new; + } + g_object_unref(texture); + return node; +} + + static void +draw_row_init(DrawRow *drow, int row, int cols) +{ + drow->cells = g_malloc0_n(cols, sizeof(DrawCell)); + drow->n_cells = cols; + drow->row = row; + drow->underl_mask = drow->underc_mask = drow->strike_mask = NULL; + + memset(drow->dlayers, 0, sizeof(DrawLayer) * N_DRAW_LAYERS); + for (int l = 0; l < N_DRAW_LAYERS; l++) + { + DrawLayer *dlayer = drow->dlayers + l; + + // Make the buffer transparent initially + if (l == DRAW_LAYER_SPECIAL) + dlayer->buf = g_malloc0_n(cols * 2, 4); + else + dlayer->buf = g_malloc0_n(cols, 4); + } +} + + static void +draw_row_clear(DrawRow *drow) +{ + g_free(drow->cells); + for (int l = 0; l < N_DRAW_LAYERS; l++) + { + DrawLayer *dlayer = drow->dlayers + l; + + if (dlayer->node != NULL) + gsk_render_node_unref(dlayer->node); + g_free(dlayer->buf); + } + g_clear_pointer(&drow->underl_mask, gsk_render_node_unref); + g_clear_pointer(&drow->underc_mask, gsk_render_node_unref); + g_clear_pointer(&drow->strike_mask, gsk_render_node_unref); +} + +/* + * Dirty the given layer for the row so it is rendered again. + */ + static void +draw_row_dirty_layer(DrawRow *drow, DrawLayerType dlayer_t) +{ + DrawLayer *dlayer = drow->dlayers + dlayer_t; + + dlayer->dirty = TRUE; + g_clear_pointer(&dlayer->node, gsk_render_node_unref); +} + +/* + * If the cell at "col" is inverted, then uninvert and set the layer as dirty. + * Otherwise do nothing. + */ + static void +draw_row_uninvert_cell(DrawRow *drow, int col) +{ + uint32_t *buf = drow->dlayers[DRAW_LAYER_OVERLAY].buf; + + // If pixel is white, then it is inverted. + if (buf[col] == 0xffffffff) + { + draw_row_dirty_layer(drow, DRAW_LAYER_OVERLAY); + buf[col] = 0; + } +} + +/* + * Set the cell in the row to the glyphs and attrs (that represents the cell). + * This will dirty the changed layers of the row. + */ + static void +draw_row_set_cell(DrawRow *drow, int col, DrawGlyphs *dglyphs, DrawAttr *dattr) +{ + DrawCell *dcell = drow->cells + col; + + if (!draw_glyphs_same(dglyphs, dcell->dglyphs)) + draw_row_dirty_layer(drow, DRAW_LAYER_TEXT); + + // When drawing double width characters in the snapshot vfunc, pointer + // equality of the DrawGlyphs is used. Must make sure to always keep the + // pointer in sync, even if glyphs are technically the "same". + if (dcell->dglyphs != dglyphs) + { + if (dcell->dglyphs != NULL) + draw_glyphs_unref(dcell->dglyphs); + dcell->dglyphs = dglyphs == NULL ? NULL : draw_glyphs_ref(dglyphs); + } + + draw_attr_apply(dattr, col, drow); + draw_row_uninvert_cell(drow, col); +} + +/* + * Same as draw_row_set_cell(), but fills in the cells between "col1" and "col2" + * (inclusive). + */ + static void +draw_row_fill( + DrawRow *drow, + int col1, + int col2, + DrawGlyphs *dglyphs, + DrawAttr *dattr, + DrawCursor *cursor) +{ + for (int c = col1; c <= col2; c++) + draw_row_set_cell(drow, c, dglyphs, dattr); + + // Clear cursor if its in the region + if (gui.cursor_row == drow->row && gui.cursor_col >= col1 + && gui.cursor_col <= col2) + cursor->draw = FALSE; +} - if (flags & DRAW_UNDERL) - nodes[n_nodes++] = gsk_color_node_new(fg_color, - &GRAPHENE_RECT_INIT(FILL_X(start_col), - FILL_Y(row + 1) - 1, - FILL_X(start_col + n_cells) - FILL_X(start_col), 1)); +/* + * Move the cells between "col1" and "col2" from "src" to "dest", overwriting + * the existing cells. This will clear the source cells. + */ + static void +draw_row_move_to( + DrawRow *dest_row, + DrawRow *src_row, + int col1, + int col2, + VimDrawArea *da) +{ + int move_size = (col2 - col1 + 1) * sizeof(DrawCell); - if (flags & DRAW_STRIKE) - nodes[n_nodes++] = gsk_color_node_new(fg_color, - &GRAPHENE_RECT_INIT(FILL_X(start_col), - FILL_Y(row) + (int)(gui.char_height / 2), - FILL_X(start_col + n_cells) - FILL_X(start_col), 1)); + draw_row_fill(dest_row, col1, col2, NULL, NULL, &da->cursor); + memmove(dest_row->cells + col1, src_row->cells + col1, move_size); + // NULL the draw cells so we don't double unreference. + memset(src_row->cells + col1, 0, move_size); + draw_row_dirty_layer(src_row, DRAW_LAYER_TEXT); - if (flags & DRAW_UNDERC) + // Move the layer buffers + move_size = (col2 - col1 + 1) * 4; + for (int l = 0; l < N_DRAW_LAYERS; l++) + { + uint32_t *src = src_row->dlayers[l].buf; + uint32_t *dest = dest_row->dlayers[l].buf; + + if (l == DRAW_LAYER_SPECIAL) + { + memmove(dest + col1 + UNDERC_OFFSET(dest_row->n_cells), + src + col1 + UNDERC_OFFSET(src_row->n_cells), move_size); + memmove(dest + col1 + STRIKE_OFFSET(dest_row->n_cells), + src + col1 + STRIKE_OFFSET(src_row->n_cells), move_size); + } + else + memmove(dest + col1, src + col1, move_size); + draw_row_dirty_layer(dest_row, l); + } +} + +/* + * If background layer is dirty, then render it. + */ + static void +draw_row_render_background(DrawRow *drow, VimDrawArea *da) +{ + DrawLayer *dlayer = drow->dlayers + DRAW_LAYER_BACKGROUND; + + if (!dlayer->dirty) + return; + + dlayer->dirty = FALSE; + + // If background is transparent, don't add a render node. + if (rgba_is_transparent(dlayer->buf, da->n_cols)) + return; + + // Add a 1 cell bleed, then use a clip node to clip it to the draw area + // width. This just makes things like status bars look a bit nicer when draw + // area size is not an exact multiple of the cell size. + dlayer->node = draw_layer_get_texture(dlayer, drow->row, da, 0, TRUE); +} + +/* + * Ensure the given under decorations are generated. + */ + static void +draw_row_ensure_decor(DrawRow *drow, int flags) +{ + if (flags & DRAW_UNDERL && drow->underl_mask == NULL) + drow->underl_mask = gsk_color_node_new(&white_rgba, + &GRAPHENE_RECT_INIT(FILL_X(0), FILL_Y(drow->row + 1) - 1, + FILL_X(drow->n_cells), 1)); + + if (flags & DRAW_STRIKE && drow->strike_mask == NULL) + drow->strike_mask = gsk_color_node_new(&white_rgba, + &GRAPHENE_RECT_INIT(FILL_X(0), + FILL_Y(drow->row) + (int)(gui.char_height / 2), + FILL_X(drow->n_cells), 1)); + + if (flags & DRAW_UNDERC && drow->underc_mask == NULL) { - int y = FILL_Y(row + 1) - 1; // Top of underneath line, - // upwards by one pixel. - int x_start = FILL_X(start_col); - int x_end = FILL_X(start_col + n_cells); + static const int val[8] = {1, 0, 0, 0, 1, 2, 2, 2}; + + int y = FILL_Y(drow->row + 1) - 1; + int x_start = FILL_X(0); + int x_end = FILL_X(drow->n_cells); // GskPath was added in GSK 4.14, otherwise use cairo #if GTK_CHECK_VERSION(4, 14, 0) GskPathBuilder *builder; GskPath *path; - GskStroke *stroke; + GskStroke *stroke; GskRenderNode *color_node; graphene_rect_t bounds; - const int half_wave = 4; // Half-cycle width (e.g., 4px up, 4px - // down) - const int amplitude = 2; // Peak height from baseline - int toggle = -1; // Start by pulling up (-Y is up in GTK) - builder = gsk_path_builder_new(); - gsk_path_builder_move_to(builder, x_start, y); - // Each cycle contains two quadratic bezier curves, one going up and one - // going down. - for (int x = x_start; x < x_end; x += half_wave) - { - int current_half = half_wave; - if (x + current_half > x_end) - { - current_half = x_end - x; - } - - // The control point sits exactly halfway horizontally through the arc - int cp_x = x + (current_half / 2); - int cp_y = y + (toggle * amplitude); - int end_x = x + current_half; + gsk_path_builder_move_to(builder, + x_start + 1, + y - 2 + 0.5); - gsk_path_builder_quad_to(builder, cp_x, cp_y, end_x, y); + for (int i = x_start + 1; i < x_end; i++) + { + int offset = val[i % 8]; - toggle = -toggle; // Flip direction for the next half-wave + gsk_path_builder_line_to(builder, + i, y - offset + 0.5); } path = gsk_path_builder_free_to_path(builder); - stroke = gsk_stroke_new(1.0f); + + stroke = gsk_stroke_new(1.0); gsk_path_get_stroke_bounds (path, stroke, &bounds); - color_node = gsk_color_node_new(sp_color, &bounds); + color_node = gsk_color_node_new(&white_rgba, &bounds); - nodes[n_nodes++] = gsk_stroke_node_new(color_node, path, stroke); + drow->underc_mask = gsk_stroke_node_new(color_node, path, stroke); gsk_stroke_free(stroke); gsk_path_unref(path); gsk_render_node_unref(color_node); #else - static const int val[8] = {1, 0, 0, 0, 1, 2, 2, 2}; - cairo_t *cr; - GskRenderNode *node; + cairo_t *cr; + GskRenderNode *node; node = gsk_cairo_node_new( &GRAPHENE_RECT_INIT(x_start, y - 3, x_end - x_start, 5)); cr = gsk_cairo_node_get_draw_context(node); cairo_set_line_width(cr, 1.0); - cairo_set_source_rgba(cr, sp_color->red, sp_color->green, - sp_color->blue, sp_color->alpha); + cairo_set_source_rgba(cr, 1.0, 1.0, 1.0, 1.0); cairo_move_to(cr, x_start + 1, y - 2 + 0.5); @@ -352,656 +968,533 @@ create_under_decor_node( cairo_stroke(cr); cairo_destroy(cr); - nodes[n_nodes++] = node; + drow->underc_mask = node; #endif } - - if (n_nodes == 0) - return NULL; - if (n_nodes == 1) - return nodes[0]; - - container = gsk_container_node_new(nodes, n_nodes); - for (int i = 0; i < n_nodes; i++) - // Container node takes its own reference to each. - gsk_render_node_unref(nodes[i]); - return container; } /* - * Create a new draw node with a reference count of 1. Note that this may be - * NULL if creating a new draw node is not necessary. + * If the text layer of the draw row is dirty, then render it and set the render + * node. Note that the render node may still be NULL if there is nothing to + * display. */ - static DrawNode * -draw_node_new( - PangoFont *font, - const PangoGlyphInfo *glyphs, - int n_glyphs, - const GdkRGBA *bg_color, - const GdkRGBA *fg_color, - const GdkRGBA *sp_color, - int flags, - int start_col, - int n_cells) -{ - DrawNode *dnode; - gboolean has_ink = glyphs_has_ink(font, glyphs, n_glyphs); - gboolean is_def_bg = color_is_default_bg(bg_color); - gboolean has_under = flags & (DRAW_UNDERL | DRAW_UNDERC | DRAW_STRIKE); - - // If there is no ink to be displayed, and the background color is the same - // as the default background color (the color that will be displayed behind - // everything), then there is no point in creating a new draw node. - if (!has_ink && !has_under && (flags & DRAW_TRANSP || is_def_bg)) - return NULL; - - dnode = g_new0(DrawNode, 1); - - dnode->refcount = 1; - - dnode->glyphs = g_memdup2(glyphs, sizeof(PangoGlyphInfo) * n_glyphs); - dnode->n_glyphs = n_glyphs; - dnode->dnode_flags |= DRAW_NODE_DIRTY; - if (is_def_bg || flags & DRAW_TRANSP) - dnode->dnode_flags |= DRAW_NODE_NOBG; - if (!has_ink) - dnode->dnode_flags |= DRAW_NODE_NOINK; - if (has_under) - dnode->dnode_flags |= DRAW_NODE_UNDER; - - dnode->font = g_object_ref(font); - dnode->bg_color = *bg_color; - dnode->fg_color = *fg_color; - dnode->sp_color = *sp_color; - dnode->flags = flags; - - dnode->start_col = start_col; - dnode->n_cells = n_cells; - - return dnode; -} - - static DrawNode * -draw_node_ref(DrawNode *dnode) -{ - dnode->refcount++; - return dnode; -} - static void -draw_node_unref(DrawNode *dnode) +draw_row_render_text(DrawRow *drow, VimDrawArea *da) { - if (dnode != NULL && --dnode->refcount <= 0) - { - g_free(dnode->glyphs); - node_unref(dnode->node); - g_object_unref(dnode->font); - g_free(dnode); - } -} - -/* - * Dirty the draw node. This will remove the render node if any, and mark it to - * have a new render node created for it on the next snapshot vfunc call. - * Returns TRUE if draw node is not necessary anymore. - */ - static gboolean -draw_node_make_dirty(DrawNode *dnode) -{ - int flags = dnode->dnode_flags; - - g_clear_pointer(&dnode->node, gsk_render_node_unref); - dnode->dnode_flags |= DRAW_NODE_DIRTY; - - return (flags & DRAW_NODE_NOINK) && !(flags & (DRAW_NODE_UNDER)) - && flags & DRAW_NODE_NOBG; -} - - static DrawNode * -draw_node_copy(DrawNode *dnode) -{ - DrawNode *copy = draw_node_new( - dnode->font, dnode->glyphs, dnode->n_glyphs, - &dnode->bg_color, &dnode->fg_color, &dnode->sp_color, - dnode->flags, dnode->start_col, dnode->n_cells - ); + DrawLayer *dlayer = drow->dlayers + DRAW_LAYER_TEXT; + GArray *glyph_buf; + GPtrArray *nodes; + PangoFont *cur_font = NULL; + int start_col = 0; + int empty_cells = 0; + uint32_t cur_fg = 0; + int c; + + if (!dlayer->dirty) + return; - // "copy" should never be NULL, so we don't need to check for NULL. - if (unlikely(dnode->dnode_flags & DRAW_NODE_CLIP)) - copy->dnode_flags |= DRAW_NODE_CLIP; + glyph_buf = da->glyph_buf; + nodes = da->node_buf; + + g_array_set_size(glyph_buf, 0); + g_ptr_array_set_size(nodes, 0); + + // GskTextNode does not use the "log_clusters" array of PangoGlyphString, so + // we don't need to set it. +#define FLUSH_NODE() \ + do { \ + if (cur_font != NULL) \ + { \ + PangoGlyphString glyph_str; \ + GskRenderNode *node; \ + GdkRGBA fg_color = u32_to_rgba(cur_fg); \ + glyph_str.glyphs = (PangoGlyphInfo *)glyph_buf->data; \ + glyph_str.num_glyphs = glyph_buf->len; \ + node = gsk_text_node_new(cur_font, &glyph_str, &fg_color, \ + &GRAPHENE_POINT_INIT(TEXT_X(start_col), \ + TEXT_Y(drow->row))); \ + if (node != NULL) \ + g_ptr_array_add(nodes, node); \ + g_array_set_size(glyph_buf, 0); \ + cur_font = NULL; \ + } \ + } while (FALSE) + + for (c = 0; c < da->n_cols; c++) + { + DrawCell *dcell = drow->cells + c; + DrawGlyphs *dglyphs = dcell->dglyphs; + int prev_len; - return copy; -} + if (dglyphs == NULL) + { + empty_cells++; + continue; + } + else if (dglyphs->font == NULL) + { + // Add sign icon + GskRenderNode *snode; + int w; + + FLUSH_NODE(); + + snode = gsk_texture_scale_node_new(dglyphs->dsign->texture, + &GRAPHENE_RECT_INIT(FILL_X(c), FILL_Y(drow->row), + dglyphs->dsign->width, dglyphs->dsign->height), + GSK_SCALING_FILTER_TRILINEAR); + w = (dglyphs->dsign->width + gui.char_width - 1) / gui.char_width; + + if (snode != NULL) + g_ptr_array_add(nodes, snode); + // Skip the cells the draw sign covers. + c += w - 1; // Subtract one because we increment by one in the for + // loop + continue; + } + else if (dglyphs->font != cur_font || cur_fg != dglyphs->fg_color) + { + FLUSH_NODE(); + cur_font = dglyphs->font; + start_col = c; + cur_fg = dglyphs->fg_color; + } -/* - * Split the draw node at the given cell offset in place (exclusive). If - * "keep_left" is TRUE, then keep the left halve (discard right halve), and vice - * versa. This will dirty the draw node. - * - * Returns TRUE if the new split draw node is not necessary anymore (see - * draw_node_new()), otherwise FALSE. - */ - static gboolean -draw_node_split(DrawNode *dnode, int cell_offset, gboolean keep_left) -{ - int glyph_offset; - gboolean split = TRUE; - gboolean clip = FALSE; - - glyph_offset = cell_offset_to_glyph(dnode->glyphs, - dnode->n_glyphs, cell_offset); - - // Some fonts emulate ligatures by having spacer glyphs followed by a glyph - // that contains all the ink. If we tried splitting this type of ligature, - // then one side will incorrectly be empty. - // - // To handle this case, always clip the draw node so that the extra ink does - // not bleed out. If we are keeping the left side, then do not split, - // because we want to keep the glyph with all the ink. If we are keeping the - // right side, then we can split because the glyph with the ink will be on - // the right side always anyways. - for (int i = glyph_offset; i < dnode->n_glyphs; i++) - { - PangoRectangle ink; + // Don't want to render double width characters twice. + if (c > 0 && drow->cells[c - 1].dglyphs == dglyphs) + continue; - pango_font_get_glyph_extents(dnode->font, dnode->glyphs[i].glyph, - &ink, NULL); + prev_len = glyph_buf->len; + g_array_append_vals(glyph_buf, dglyphs->glyphs, dglyphs->n_glyphs); - if (HAS_INK(&ink)) + // Inject the accumulated space width into the glyphs that + // have ink. + if (empty_cells > 0) { - if (ink.x < 0) + if (prev_len > 0) { - split = !keep_left; - clip = TRUE; + // Spaces occurred between valid text: extend + // previous character's advance + PangoGlyphInfo *last = &g_array_index( + glyph_buf, PangoGlyphInfo, + prev_len - 1); + last->geometry.width += CELLS2PANGO(empty_cells); } - break; + empty_cells = 0; // Width consumed } } - if (unlikely(clip)) - dnode->dnode_flags |= DRAW_NODE_CLIP; + FLUSH_NODE(); - if (keep_left) - { - if (likely(split)) - dnode->n_glyphs = glyph_offset; - dnode->n_cells = cell_offset; - } - else +#undef FLUSH_NODE + + if (nodes->len > 0) { - if (likely(split)) + // Add underline mask (if needed). + GskRenderNode *source; + GskRenderNode *node; + + draw_row_ensure_decor(drow, DRAW_UNDERL); + if (!rgba_is_transparent(dlayer->buf, drow->n_cells)) { - // If this results in zero, then glyphs_has_ink() will return FALSE - // so it is fine. - dnode->n_glyphs -= glyph_offset; - // Shift glyphs after offset to beginning - memmove(dnode->glyphs, dnode->glyphs + glyph_offset, - sizeof(PangoGlyphInfo) * dnode->n_glyphs); + source = draw_layer_get_texture(dlayer, drow->row, da, 0, FALSE); + node = gsk_mask_node_new(source, drow->underl_mask, + GSK_MASK_MODE_ALPHA); + gsk_render_node_unref(source); + g_ptr_array_add(nodes, node); } - dnode->n_cells -= cell_offset; - dnode->start_col += cell_offset; - } - - if (likely(split)) - { - dnode->glyphs = glyphs_resize(dnode->glyphs, dnode->n_glyphs); - - // Recheck if new split glyphs has ink - if (glyphs_has_ink(dnode->font, dnode->glyphs, dnode->n_glyphs)) - dnode->dnode_flags &= ~DRAW_NODE_NOINK; - else - dnode->dnode_flags |= DRAW_NODE_NOINK; + dlayer->node = gsk_container_node_new( + (GskRenderNode **)nodes->pdata, nodes->len); } - - return draw_node_make_dirty(dnode); + dlayer->dirty = FALSE; } /* - * If "dnode" is dirty, create a new render node for it at the given row and - * store it, then undirty it. + * Render undercurl and strikethough, which use the special colour. */ static void -draw_node_render(DrawNode *dnode, int row, VimDrawArea *da) +draw_row_render_special(DrawRow *drow, VimDrawArea *da) { - GskRenderNode *nodes[3]; - int n_nodes = 0; - GskRenderNode *decor_node; + DrawLayer *dlayer = drow->dlayers + DRAW_LAYER_SPECIAL; + GskRenderNode *source; + GskRenderNode *nodes[2]; + int n_nodes = 0; - if (!(dnode->dnode_flags & DRAW_NODE_DIRTY)) + if (!dlayer->dirty) return; - if (!(dnode->dnode_flags & DRAW_NODE_NOBG)) - { - int width = dnode->n_cells * gui.char_width; + draw_row_ensure_decor(drow, DRAW_UNDERC | DRAW_STRIKE); - // If this draw node touches the end of the draw area. Bleed its - // background to the right if the space the draw area covers is slightly - // bigger than its actual visible area (that all cells cover). This just - // makes things like status bars look a bit nicer - // - // Don't do this for the bottom, because that will make the cursor in - // the cmdline look weird. Instead only bleed downwards when drawing the - // global background color (see vim_draw_area_snapshot()) - if (END_COL(dnode) == da->n_cols - 1) - width += gui.bleed_right; - - nodes[n_nodes++] = gsk_color_node_new(&dnode->bg_color, - &GRAPHENE_RECT_INIT(FILL_X(dnode->start_col), FILL_Y(row), - width, gui.char_height)); + if (!rgba_is_transparent(dlayer->buf + UNDERC_OFFSET(drow->n_cells), + drow->n_cells)) + { + source = draw_layer_get_texture(dlayer, drow->row, da, + UNDERC_OFFSET(drow->n_cells), FALSE); + nodes[n_nodes++] = gsk_mask_node_new(source, drow->underc_mask, + GSK_MASK_MODE_ALPHA); + gsk_render_node_unref(source); } - if (!(dnode->dnode_flags & DRAW_NODE_NOINK)) + if (!rgba_is_transparent(dlayer->buf + STRIKE_OFFSET(drow->n_cells), + drow->n_cells)) { - GskRenderNode *text_node; - PangoGlyphString glyphs_str; - - // gsk_text_node_new() only uses the "glyphs" field, don't need to worry - // about the "log_clusters" array. - glyphs_str.glyphs = dnode->glyphs; - glyphs_str.num_glyphs = dnode->n_glyphs; - text_node = gsk_text_node_new(dnode->font, &glyphs_str, &dnode->fg_color, - &GRAPHENE_POINT_INIT(TEXT_X(dnode->start_col), TEXT_Y(row))); - // Should never be NULL since we check beforehand if there is ink. - assert(text_node != NULL); - - if (dnode->dnode_flags & DRAW_NODE_CLIP) - { - GskRenderNode *old = text_node; - - text_node = gsk_clip_node_new(text_node, - &GRAPHENE_RECT_INIT(FILL_X(dnode->start_col), FILL_Y(row), - dnode->n_cells * gui.char_width, gui.char_height)); - gsk_render_node_unref(old); - assert(text_node != NULL); - } - - nodes[n_nodes++] = text_node; + source = draw_layer_get_texture(dlayer, drow->row, da, + STRIKE_OFFSET(drow->n_cells), FALSE); + nodes[n_nodes++] = gsk_mask_node_new(source, drow->strike_mask, + GSK_MASK_MODE_ALPHA); + gsk_render_node_unref(source); } - decor_node = create_under_decor_node(row, dnode->start_col, dnode->n_cells, - dnode->flags, &dnode->fg_color, &dnode->sp_color); - if (decor_node != NULL) - nodes[n_nodes++] = decor_node; - - // Should never be zero - assert(n_nodes > 0); - - if (likely(n_nodes == 1)) - dnode->node = nodes[0]; - else + if (n_nodes == 1) + { + dlayer->node = nodes[0]; + } + else if (n_nodes > 0) { - dnode->node = gsk_container_node_new(nodes, n_nodes); - // gsk_container_node_new() takes its own reference - for (int i = 0; i < n_nodes; i++) + dlayer->node = gsk_container_node_new(nodes, 2); + // gsk_container_node_new() takes its own ref + for (int i = 0; i < ARRAY_LENGTH(nodes); i++) gsk_render_node_unref(nodes[i]); } - dnode->dnode_flags &= ~DRAW_NODE_DIRTY; + dlayer->dirty = FALSE; } /* - * Returns true if "dnode" matches "font" + "flags" in terms of - * color/visual attributes. + * Render white color blocks used for blend to invert cells. */ - static gboolean -draw_node_match(DrawNode *dnode, PangoFont *font, int flags) + static void +draw_row_render_overlay(DrawRow *drow, VimDrawArea *da) { - if (dnode->flags != flags) - return FALSE; + DrawLayer *dlayer = drow->dlayers + DRAW_LAYER_OVERLAY; - if (!(flags & DRAW_TRANSP) - && !gdk_rgba_equal(&dnode->bg_color, gui.bgcolor)) - return FALSE; + if (!dlayer->dirty) + return; - if (!gdk_rgba_equal(&dnode->fg_color, gui.fgcolor)) - return FALSE; + dlayer->dirty = FALSE; - // Special color is only used for undercurls - if (flags & DRAW_UNDERC && !gdk_rgba_equal(&dnode->sp_color, gui.spcolor)) - return FALSE; + // If no invert regions, don't do anything. + if (rgba_is_transparent(dlayer->buf, da->n_cols)) + return; - // This may not work all the time, but creating two PangoFontDescription - // each time to compare equality seems slow... - return dnode->font == font; + dlayer->node = draw_layer_get_texture(dlayer, drow->row, da, 0, TRUE); } /* - * Append or prepend the given glyphs to the draw node. If "start" is TRUE, then - * prepend, otherwise append. This will invalidate the draw node. Note that - * prepending does not update "start_col" or "n_cells". + * Add the glyph cluster to the raw area with given attributes and font. */ static void -draw_node_extend( - DrawNode *dnode, - const PangoGlyphInfo *glyphs, - int n_glyphs, - bool start) +draw_row_add_cluster( + DrawRow *drow, + DrawAttr *dattr, + int col, + int n_cells, + PangoFont *font, + PangoGlyphInfo *glyphs, + int n_glyphs, + VimDrawArea *da) { - dnode->glyphs = glyphs_resize(dnode->glyphs, dnode->n_glyphs + n_glyphs); - - if (start) - { - // Move the existing glyphs forward first - memmove(dnode->glyphs + n_glyphs, dnode->glyphs, - dnode->n_glyphs * sizeof(PangoGlyphInfo)); - memcpy(dnode->glyphs, glyphs, n_glyphs * sizeof(PangoGlyphInfo)); - } - else - memcpy(dnode->glyphs + dnode->n_glyphs, glyphs, - n_glyphs * sizeof(PangoGlyphInfo)); + DrawGlyphs *dglyphs = NULL; - dnode->n_glyphs += n_glyphs; + if (col + n_cells > da->n_cols) + return; - if (glyphs_has_ink(dnode->font, dnode->glyphs, dnode->n_glyphs)) - dnode->dnode_flags &= ~DRAW_NODE_NOINK; - else - dnode->dnode_flags |= DRAW_NODE_NOINK; - (void)draw_node_make_dirty(dnode); -} + // If there is no ink ("glyphs" is NULL), then just set the "dglyphs" of + // each cell to NULL. + if (glyphs != NULL) + dglyphs = draw_glyphs_new(font, glyphs, n_glyphs, dattr->fg_color); -/* - * Set the given cell to the draw node (which may be NULL), adding a new - * reference to it. - */ - static void -draw_cell_set(DrawCell *dcell, DrawNode *dnode) -{ - draw_node_unref(dcell->dnode); - dcell->dnode = dnode == NULL ? NULL : draw_node_ref(dnode); - dcell->invert = FALSE; -} + draw_row_fill(drow, col, col + n_cells - 1, dglyphs, dattr, &da->cursor); -/* - * Set the cells between "col1" and "col2" (inclusive) to "dnode" (which may be - * NULL). - */ - static void -draw_row_fill(DrawCell *drow, int col1, int col2, DrawNode *dnode) -{ - for (int c = col1; c <= col2; c++) - draw_cell_set(drow + c, dnode); + if (dglyphs != NULL) + draw_glyphs_unref(dglyphs); } /* - * Same as draw_row_fill(), but also handle truncating/splitting any draw nodes - * that overlap onto the set region. If "split" is TRUE, then only - * truncating/splitting is done. - * - * If "copy" is TRUE, then "dnode" is ignored and instead any draw nodes in the - * region that overlap outside of it are copied and clipped in addition to - * truncating draw nodes outside the region. + * Add the string to the draw area. This will handle the 'guiligatures' option. */ - static void -draw_row_set( - DrawCell *drow, - int col1, - int col2, - DrawNode *dnode, - gboolean copy, - gboolean split) + static int +vim_draw_area_add_string( + VimDrawArea *self, + DrawRow *drow, + DrawAttr *dattr, + int col, + char_u *s, + int len, + int draw_flags) { - DrawNode *ldnode = drow[col1].dnode; - DrawNode *rdnode = drow[col2].dnode; - DrawNode *new_dnode = NULL; - - if (ldnode != NULL && ldnode == rdnode - && (ldnode->start_col != col1 || END_COL(ldnode) > col2)) + PangoGlyphString *glyphs; + PangoAttrList *attr_list = NULL; + int start_idx = -1; + GList *item_list; + int column_offset = 0; + int cluster_width; + int last_glyph_rbearing; + int cells = 0; + int i = 0; + gboolean has_unicode = FALSE; + gboolean needs_shaping = FALSE; + + // Scan the string, and find spans where ligatures should not happen. We + // must ensure that attribute boundaries do not split a base character from + // its combining marks, as Pango breaks items at attribute boundaries. + while (i < len) { - // Region in completely inside a single draw node. Truncate the existing - // draw node, and create a new draw node to be used as the right split. - if (END_COL(ldnode) > col2) + int clen = 1; + int is_composing = 0; + bool want_disable; + + if (s[i] >= 0x80) { - rdnode = draw_node_copy(ldnode); - draw_row_fill(drow, col2 + 1, END_COL(rdnode), rdnode); - draw_node_unref(rdnode); + clen = utf_ptr2len(s + i); + is_composing = utf_iscomposing(utf_ptr2char(s + i)); + has_unicode = TRUE; } - else - // "ldnode" does not extend past "col2", no point in creating a new - // draw node on the right. - rdnode = NULL; - - if (copy) - // Make another copy for the new draw node inside the set region. - // Must fill it in the row after, since "ldnode" may be unreferenced - // fully. - new_dnode = draw_node_copy(ldnode); - } - if (ldnode != NULL && ldnode->start_col != col1) - { - if (copy && new_dnode == NULL) + if (is_composing) + // Do not change state mid-cluster. Inherit the previous character's + // state. + want_disable = (start_idx != -1); + else + // Base character: disable ligatures if it's an ASCII non-ligature + // char. + want_disable = (s[i] < 0x80 && !gui.ligatures_map[s[i]]); + + // If any base character actually wants ligature/contextual shaping, + // the string must go through Pango's shaping engine -- the ASCII + // fast path below draws cached glyphs one-by-one and can never + // produce a ligature, regardless of 'guiligatures'. + if (!is_composing && !want_disable) + needs_shaping = TRUE; + + if (want_disable) { - // Make a copy for the right halve. - DrawNode *new_right = draw_node_copy(ldnode); - - if (draw_node_split(new_right, col1 - ldnode->start_col, FALSE)) - g_clear_pointer(&new_right, draw_node_unref); - draw_row_fill(drow, col1, END_COL(ldnode), new_right); - draw_node_unref(new_right); + if (start_idx == -1) + start_idx = i; } - - // Leftmost draw node overlaps onto region, split it and discard right - // halve. - if (draw_node_split(ldnode, col1 - ldnode->start_col, TRUE)) - // Draw node is not necessary anymore, clear it from the row. - draw_row_fill(drow, ldnode->start_col, col1 - 1, NULL); - } - if (rdnode != NULL && END_COL(rdnode) > col2) - { - if (copy && new_dnode == NULL) + else if (start_idx != -1) { - // Make a copy for the left halve. - DrawNode *new_left = draw_node_copy(rdnode); - - if (draw_node_split(new_left, col2 - rdnode->start_col + 1, TRUE)) - g_clear_pointer(&new_left, draw_node_unref); - draw_row_fill(drow, rdnode->start_col, col2, new_left); - draw_node_unref(new_left); + if (attr_list == NULL) + attr_list = pango_attr_list_new(); + INSERT_PANGO_ATTR(pango_attr_font_features_new("liga 0, calt 0"), + attr_list, start_idx, i); + start_idx = -1; } - - // Rightmost draw node overlaps onto region, split it and discard left - // halve. - if (draw_node_split(rdnode, col2 - rdnode->start_col + 1, FALSE)) - draw_row_fill(drow, col2 + 1, END_COL(rdnode), NULL); + i += clen; } - if (copy) + // Fast path for pure ASCII: use cached glyph table. Skip this path when + // there are non-ascii characters in the string, font attributes, or if + // theres a possible ligature. + if (!(draw_flags & DRAW_ITALIC) + && !((draw_flags & DRAW_BOLD) && gui.font_can_bold) + && gui.ascii_glyphs != NULL + && !has_unicode + && !needs_shaping) { - if (new_dnode != NULL) + for (i = 0; i < len; ++i) { - if (draw_node_split(new_dnode, col1 - new_dnode->start_col, FALSE) - || draw_node_split(new_dnode, - col2 - new_dnode->start_col + 1, TRUE)) - g_clear_pointer(&new_dnode, draw_node_unref); + PangoGlyphInfo glyph = gui.ascii_glyphs->glyphs[2 * s[i]]; + gboolean has_ink; + + has_ink = glyph_has_ink(gui.ascii_font, glyph); - draw_row_fill(drow, col1, col2, new_dnode); - draw_node_unref(new_dnode); + draw_row_add_cluster(drow, dattr, col + i, 1, + gui.ascii_font, has_ink ? &glyph : NULL, 1, self); } + + column_offset = len; + goto exit; } - else if (!split) - draw_row_fill(drow, col1, col2, dnode); -} -/* - * Move the cells between "col1" and "col2" from "src" to "dest", overwriting - * the existing cells. This will handle clipping any draw nodes. - */ - static void -draw_row_move_to(DrawCell *dest_row, DrawCell *src_row, int col1, int col2) -{ - int move_size = (col2 - col1 + 1) * sizeof(DrawCell); + glyphs = pango_glyph_string_new(); - // Make sure that we free/truncate any draw nodes before we overwrite - // them. - draw_row_set(dest_row, col1, col2, NULL, FALSE, FALSE); + if (attr_list == NULL) + attr_list = pango_attr_list_new(); - // Make sure that draw nodes at the "col1" and "col2" of "src_row" are - // clipped so that they all fit in the region being moved. - draw_row_set(src_row, col1, col2, NULL, TRUE, FALSE); + if (start_idx != -1) + INSERT_PANGO_ATTR(pango_attr_font_features_new("liga 0, calt 0"), + attr_list, start_idx, len); - memmove(dest_row + col1, src_row + col1, move_size); + cluster_width = PANGO_SCALE * gui.char_width; + last_glyph_rbearing = PANGO_SCALE * gui.char_width; - // Dirty the moved cells - for (int c = col1; c <= col2;) - if (dest_row[c].dnode != NULL) - { - (void)draw_node_make_dirty(dest_row[c].dnode); - c += dest_row[c].dnode->n_cells; - } - else - c++; + // If 'guifontwide' is set then use that for double-width characters. + if (gui.wide_font != NULL) + apply_wide_font_attr(s, len, attr_list); - // NULL the draw nodes so we don't double unreference. - memset(src_row + col1, 0, (col2 - col1 + 1) * sizeof(DrawCell)); -} + if ((draw_flags & DRAW_BOLD) && gui.font_can_bold) + INSERT_PANGO_ATTR(pango_attr_weight_new(PANGO_WEIGHT_BOLD), + attr_list, 0, len); + if (draw_flags & DRAW_ITALIC) + INSERT_PANGO_ATTR(pango_attr_style_new(PANGO_STYLE_ITALIC), + attr_list, 0, len); -/* - * Should be called after modifying draw nodes within the given region. - */ -static void -vim_draw_area_check_bounds( - VimDrawArea *self, - int row1, - int row2, - int col1, - int col2) -{ -#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG) - graphene_rect_t bounds = GRAPHENE_RECT_INIT( - FILL_X(col1), FILL_Y(row1), - gui.char_width * (col2 - col1 + 1), - gui.char_height * (row2 - row1 + 1)); -#endif + item_list = pango_itemize(gui.text_context, + (const char *)s, 0, len, attr_list, NULL); - if (self->cursor_node != NULL) - // Check if cursor node is within the the updated region. If so, then - // remove the render node. This only applies to the part and hollow - // cursor, the block cursor will be cleared in draw_row_make_space(). - if (gui.row >= row1 && gui.row <= row2 - && gui.col >= col1 && gui.col <= col2) - g_clear_pointer(&self->cursor_node, gsk_render_node_unref); - -#ifdef FEAT_SIGN_ICONS - // Clear any sign icons within the modified block if any - for (GList *s = self->signs->head; s != NULL;) + while (item_list != NULL) { - GList *next = s->next; - graphene_rect_t rect; + PangoItem *item = item_list->data; + int item_cells = 0; + int cluster_start_idx = 0; + int current_cluster_col = col + column_offset; + int current_cluster_cells = 0; + gboolean current_cluster_ink = FALSE; + + item_list = g_list_delete_link(item_list, item_list); - gsk_render_node_get_bounds(s->data, &rect); + // Force LTR direction; Vim handles bidi on its own. + item->analysis.level = (item->analysis.level + 1) & (~1U); - if (graphene_rect_contains_rect(&bounds, &rect)) + pango_shape_full((const char *)s + item->offset, item->length, + (const char *)s, len, &item->analysis, glyphs); + + // Fixed-width hack: assign a fixed width to each glyph based on the + // number of cells it occupies, handling composing characters and + // cluster boundaries properly. + // + // At the end of each cluster, add it to the draw area. + for (i = 0; i < glyphs->num_glyphs; ++i) { - // Keep going in case there are multiple sign icons within this - // block. - gsk_render_node_unref(s->data); - g_queue_delete_link(self->signs, s); + PangoGlyphInfo *glyph; + + glyph = &glyphs->glyphs[i]; + + if (glyph->attr.is_cluster_start) + { + int cellcount; + + cellcount = count_cluster_cells( + s, item, glyphs, i, &cluster_width, + (item_list != NULL) ? &last_glyph_rbearing : NULL); + + if (i > 0 && cellcount > 0) + { + draw_row_add_cluster(drow, dattr, + current_cluster_col, current_cluster_cells, + item->analysis.font, + current_cluster_ink ? + glyphs->glyphs + cluster_start_idx : NULL, + i - cluster_start_idx, self); + + current_cluster_col += current_cluster_cells; + cluster_start_idx = i; + current_cluster_ink = FALSE; + current_cluster_cells = 0; + } + + if (cellcount > 0) + { + int width; + + width = cellcount * gui.char_width * PANGO_SCALE; + glyph->geometry.x_offset += + MAX(0, width - cluster_width) / 2; + glyph->geometry.width = width; + } + else + setup_zero_width_cluster(item, glyph, cells, + cluster_width, + last_glyph_rbearing); + + item_cells += cellcount; + cells = cellcount; + current_cluster_cells += cellcount; + } + else if (i > 0) + { + int width; + + if (glyph->geometry.x_offset >= 0) + { + glyphs->glyphs[i].geometry.width = + glyphs->glyphs[i - 1].geometry.width; + glyphs->glyphs[i - 1].geometry.width = 0; + } + width = cells * gui.char_width * PANGO_SCALE; + glyph->geometry.x_offset += + MAX(0, width - cluster_width) / 2; + } + else + glyph->geometry.width = 0; + + + if (!current_cluster_ink) + current_cluster_ink = glyph_has_ink( + item->analysis.font, *glyph); } - s = next; + if (glyphs->num_glyphs > 0) + draw_row_add_cluster(drow, dattr, + current_cluster_col, current_cluster_cells, + item->analysis.font, + current_cluster_ink ? + glyphs->glyphs + cluster_start_idx : NULL, + glyphs->num_glyphs - cluster_start_idx, self); + + pango_item_free(item); + column_offset += item_cells; } -#endif -#ifdef FEAT_NETBEANS_INTG - // Remove multi sign indicator if it is within the modified region. - if (self->multisign_node != NULL) - { - graphene_rect_t rect; - gsk_render_node_get_bounds(self->multisign_node, &rect); - if (graphene_rect_contains_rect(&bounds, &rect)) - g_clear_pointer(&self->multisign_node, gsk_render_node_unref); - } -#endif + pango_glyph_string_free(glyphs); + +exit: + if (attr_list != NULL) + pango_attr_list_unref(attr_list); + + return column_offset; } /* - * Add the glyph string starting at column "col" in row "row". This will handle - * any background colours, fake bold, and under decorations. This does not queue - * a redraw for the widget. + * Draw the given string at position (col, row) on the draw area. */ - void -vim_draw_area_add_glyphs( + int +vim_draw_area_draw_string( VimDrawArea *self, - int row, - int col, - int num_cells, - int flags, - PangoFont *font, - PangoGlyphString *glyphs) + int row, + int col, + char_u *s, + int len, + int flags) { - DrawCell *drow; - DrawNode *dnode = NULL; - int end_col = col + num_cells - 1; + DrawRow *drow; + DrawAttr dattr; + char_u *conv_buf = NULL; + int convlen; + int n_cells ; - if (unlikely(self->cells == NULL + if (unlikely(self->rows == NULL || row >= self->n_rows - || col >= self->n_cols - || col + num_cells > self->n_cols)) - return; - - drow = GET_ROW(self, row); - - draw_row_set(drow, col, end_col, NULL, FALSE, TRUE); + || col >= self->n_cols)) + return len; - // Check if leftmost draw node (if any) has the same visual - // attributes/colours as the glyph string being added. If so, then just - // extend that draw node with the new glyphs. - if (col > 0) + if (output_conv.vc_type != CONV_NONE) { - DrawNode *ldnode = drow[col - 1].dnode; - - // Don't want to try merging draw nodes that are clipped, because the - // glyphs in them may not match one to one with the actual bounds of the - // draw node. - if (ldnode != NULL && !(ldnode->dnode_flags & DRAW_NODE_CLIP) - && draw_node_match(ldnode, font, flags)) + convlen = len; + conv_buf = string_convert(&output_conv, s, &convlen); + if (conv_buf != NULL) { - draw_node_extend(ldnode, glyphs->glyphs, glyphs->num_glyphs, FALSE); - draw_row_fill(drow, col, end_col, ldnode); - ldnode->n_cells += num_cells; - dnode = ldnode; + s = conv_buf; + len = convlen; } } - // Check if we can use the existing draw node on the right. If so, then shift - // "rdnode" to the "col", and extend it. If we merged the left draw node, then - // instead extend it normally and unreference the right draw node. - if (col + num_cells < self->n_cols) + // Safety check: pango crashes with invalid utf-8. + if (!utf_valid_string(s, s + len)) { - DrawNode *rdnode = drow[col + num_cells].dnode; - - if (rdnode != NULL && !(rdnode->dnode_flags & DRAW_NODE_CLIP) - && draw_node_match(rdnode, font, flags)) - { - if (dnode != NULL) - { - assert(rdnode->start_col == col + num_cells); - draw_node_extend(dnode, rdnode->glyphs, rdnode->n_glyphs, FALSE); - dnode->n_cells += rdnode->n_cells; - draw_row_fill(drow, rdnode->start_col, END_COL(rdnode), dnode); - } - else - { - draw_node_extend(rdnode, glyphs->glyphs, glyphs->num_glyphs, TRUE); - draw_row_fill(drow, col, end_col, rdnode); - rdnode->start_col = col; - rdnode->n_cells += num_cells; - dnode = rdnode; - } - } + vim_free(conv_buf); + return len; } - if (dnode != NULL) - return; + drow = self->rows + row; + draw_attr_init(&dattr, gui.bgcolor, gui.fgcolor, gui.spcolor, flags); - dnode = draw_node_new( - font, glyphs->glyphs, glyphs->num_glyphs, gui.bgcolor, - gui.fgcolor, gui.spcolor, flags, col, num_cells - ); - draw_row_fill(drow, col, end_col, dnode); - draw_node_unref(dnode); + n_cells = vim_draw_area_add_string(self, drow, &dattr, col, s, len, flags); - vim_draw_area_check_bounds(self, row, row, col, col + num_cells - 1); + vim_free(conv_buf); + + return n_cells; } /* @@ -1015,7 +1508,7 @@ vim_draw_area_clear_block( int row2, int col2) { - if (unlikely(self->cells == NULL + if (unlikely(self->rows == NULL || row1 >= self->n_rows || col1 >= self->n_cols || row2 >= self->n_rows @@ -1023,9 +1516,7 @@ vim_draw_area_clear_block( return; for (int r = row1; r <= row2; r++) - draw_row_set(GET_ROW(self, r), col1, col2, NULL, FALSE, FALSE); - - vim_draw_area_check_bounds(self, row1, row2, col1, col2); + draw_row_fill(self->rows + r, col1, col2, NULL, NULL, &self->cursor); } /* @@ -1051,17 +1542,9 @@ vim_draw_area_move_block( int col1, int col2) { + int offset = row2 - row1; - int offset = row2 - row1; -#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG) - graphene_rect_t bounds = GRAPHENE_RECT_INIT( - FILL_X(col1), FILL_Y(row1), - gui.char_width * (col2 - col1 + 1), - gui.char_height * (row2 - row1 + 1)); - graphene_rect_t clear_rect; -#endif - - if (unlikely(self->cells == NULL + if (unlikely(self->rows == NULL || row1 >= self->n_rows || row2 >= self->n_rows || to >= self->n_rows @@ -1078,8 +1561,8 @@ vim_draw_area_move_block( // "row1" is below "to", start moving rows starting at "row1". Rows are // being shifted upwards. for (int o = 0; o <= offset; o++) - draw_row_move_to(GET_ROW(self, to + o), GET_ROW(self, row1 + o), - col1, col2); + draw_row_move_to(self->rows + to + o, self->rows + row1 + o, + col1, col2, self); } else { @@ -1089,158 +1572,27 @@ vim_draw_area_move_block( if (to + o >= self->n_rows) // "src_row" is being "moved" off the screen, no need to move // it physically. - gui_clear_block(row1 + o, col1, row1 + o, col2); - else - draw_row_move_to(GET_ROW(self, to + o), GET_ROW(self, row1 + o), - col1, col2); - } - - // Do not call vim_draw_area_check_bounds(), because we moved cells, not - // modified them. - -#if defined(FEAT_SIGN_ICONS) || defined(FEAT_NETBEANS_INTG) - if (row1 > to) - clear_rect = GRAPHENE_RECT_INIT( - FILL_X(col1), FILL_Y(to), - gui.char_width * (col2 - col1 + 1), - gui.char_height * (row1 - to)); - else - clear_rect = GRAPHENE_RECT_INIT( - FILL_X(col1), FILL_Y(row2 + 1), - gui.char_width * (col2 - col1 + 1), - gui.char_height * (to - row1)); -#endif - -#ifdef FEAT_SIGN_ICONS - // Move sign icons if they are in the moved region - for (GList *s = self->signs->head; s != NULL;) - { - GList *next = s->next; - GskRenderNode *node = s->data; - graphene_rect_t rect; - - gsk_render_node_get_bounds(node, &rect); - - // Check if icon moved off screen, if so then remove it. - if (graphene_rect_contains_rect(&clear_rect, &rect)) - { - gsk_render_node_unref(s->data); - g_queue_delete_link(self->signs, s); - s = next; - continue; - } - - if (graphene_rect_contains_rect(&bounds, &rect)) - { - GdkTexture *texture; - GskRenderNode *new; - float new_y; - - texture = gsk_texture_scale_node_get_texture(node); - new_y = graphene_rect_get_y(&rect) - graphene_rect_get_y(&bounds); - new_y += FILL_Y(to); - - if (new_y >= 0 && new_y < gtk_widget_get_height(GTK_WIDGET(self))) - { - rect.origin.y = new_y; - new = gsk_texture_scale_node_new(texture, &rect, - GSK_SCALING_FILTER_TRILINEAR); - gsk_render_node_unref(node); - s->data = new; - } - else - { - gsk_render_node_unref(s->data); - g_queue_delete_link(self->signs, s); - } - } - s = next; - } -#endif -#ifdef FEAT_NETBEANS_INTG - // Move multisign indicator node if needed - if (self->multisign_node != NULL) - { - graphene_rect_t rect; - - gsk_render_node_get_bounds(self->multisign_node, &rect); - - if (graphene_rect_contains_rect(&clear_rect, &rect)) - g_clear_pointer(&self->multisign_node, gsk_render_node_unref); - else if (graphene_rect_contains_rect(&bounds, &rect)) - { - float new_y = - graphene_rect_get_y(&rect) - graphene_rect_get_y(&bounds); - - new_y += FILL_Y(to); - - if (new_y >= 0 && new_y < gtk_widget_get_height(GTK_WIDGET(self))) - { - cairo_surface_t *surface; - GskRenderNode *new; - cairo_t *cr; - - surface = gsk_cairo_node_get_surface(self->multisign_node); - rect.origin.y = new_y; - new = gsk_cairo_node_new(&rect); - cr = gsk_cairo_node_get_draw_context(new); - cairo_set_source_surface(cr, surface, 0, 0); - cairo_paint(cr); - cairo_destroy(cr); - - gsk_render_node_unref(self->multisign_node); - self->multisign_node = new; - } + vim_draw_area_clear_block(self, row1 + o, col1, row1 + o, col2); else - g_clear_pointer(&self->multisign_node, gsk_render_node_unref); - } + draw_row_move_to(self->rows + to + o, self->rows + row1 + o, + col1, col2, self); } -#endif -} - -/* - * Draw a hollow cursor at the cursor position using the current foreground - * color. Note that this does not queue a redraw - */ - void -vim_draw_area_set_hollow_cursor(VimDrawArea *self) -{ - GskRoundedRect outline; - int i = 1; - static const float border[4] = {1.0f, 1.0f, 1.0f, 1.0f}; - const GdkRGBA color[4] = { - *gui.fgcolor, *gui.fgcolor, - *gui.fgcolor, *gui.fgcolor - } ; - - // Double cursor width if double width character - if (mb_lefthalve(gui.row, gui.col)) - i = 2; - - gsk_rounded_rect_init_from_rect(&outline, - &GRAPHENE_RECT_INIT(FILL_X(gui.col), FILL_Y(gui.row), - i * gui.char_width, gui.char_height), - 0.0f); - - node_unref(self->cursor_node); - self->cursor_node = gsk_border_node_new(&outline, border, color); } /* - * Draw a part cursor with width "w" and height "h". Note that this does not - * queue a redraw + * Set the cursor to the given width and height, use "gui.bgcolor" and + * "gui.fgcolor" for the colors as well. If "w" and "h" is zero, then a full + * block cursor is used, if "w" and "h" is -1 then a hollow cursor is used, + * otherwise a part cursor with dimensions "w" and "h" is used. */ void -vim_draw_area_set_part_cursor(VimDrawArea *self, int w, int h) +vim_draw_area_set_cursor(VimDrawArea *self, int w, int h) { - node_unref(self->cursor_node); - self->cursor_node = gsk_color_node_new(gui.fgcolor, - &GRAPHENE_RECT_INIT( -#ifdef FEAT_RIGHTLEFT - CURSOR_BAR_RIGHT ? FILL_X(gui.col + 1) - w : -#endif - FILL_X(gui.col), FILL_Y(gui.row) + gui.char_height - h, - w, h)); + self->cursor.draw = TRUE; + self->cursor.width = w; + self->cursor.height = h; + self->cursor.bg_color = *gui.bgcolor; + self->cursor.fg_color = *gui.fgcolor; } /* @@ -1254,7 +1606,7 @@ vim_draw_area_invert_block( int nrows, int ncols) { - if (unlikely(self->cells == NULL + if (unlikely(self->rows == NULL || row >= self->n_rows || col >= self->n_cols || row + nrows - 1 >= self->n_rows @@ -1263,14 +1615,12 @@ vim_draw_area_invert_block( for (int r = row; r < row + nrows; r++) { - DrawCell *drow = GET_ROW(self, r); - - for (int c = col; c < col + ncols; c++) - { - DrawCell *dcell = drow + c; + DrawRow *drow = self->rows + r; + DrawLayer *dlayer = drow->dlayers + DRAW_LAYER_OVERLAY; - dcell->invert = !dcell->invert; - } + for (int c = 0; c < ncols; c++) + dlayer->buf[c + col] = ~dlayer->buf[c + col]; + draw_row_dirty_layer(drow, DRAW_LAYER_OVERLAY); } } @@ -1282,41 +1632,83 @@ vim_draw_area_invert_block( void vim_draw_area_add_sign( VimDrawArea *self, - GdkTexture *sign, - int row, - int col, - int width, - int height) + GdkTexture *sign, + int row, + int col, + int width, + int height) { - GskRenderNode *node; + DrawRow *drow; + DrawSign *dsign; + DrawGlyphs *dglyphs; + int cells; - if (unlikely(self->cells == NULL + if (unlikely(self->rows == NULL || row >= self->n_rows || col >= self->n_cols)) return; - node = gsk_texture_scale_node_new(sign, - &GRAPHENE_RECT_INIT(FILL_X(col), FILL_Y(row), width, height), - GSK_SCALING_FILTER_TRILINEAR); - if (node == NULL) - return; - g_queue_push_tail(self->signs, node); + drow = self->rows + row; + dsign = draw_sign_new(sign, width, height); + dglyphs = draw_glyphs_new_sign(dsign); + cells = (width + gui.char_width - 1) / gui.char_width; + + draw_row_fill(drow, col, col + cells - 1, dglyphs, NULL, &self->cursor); + draw_glyphs_unref(dglyphs); } #endif #ifdef FEAT_NETBEANS_INTG - cairo_t * -vim_draw_area_get_multisign_cairo(VimDrawArea *self, int x, int y, int w, int h) +/* + * Add a multi sign indicator at the given position. "surf" should be in ARGB + * format. + */ + void +vim_draw_area_add_multisign( + VimDrawArea *self, + cairo_surface_t *surf, + int row, + int col, + int width, + int height) { - node_unref(self->multisign_node); - self->multisign_node = gsk_cairo_node_new( - &GRAPHENE_RECT_INIT( x, y, w, h)); - return gsk_cairo_node_get_draw_context(self->multisign_node); + DrawRow *drow; + DrawSign *dsign; + DrawGlyphs *dglyphs; + int cells; + GdkTexture *texture; + GBytes *bytes; + + if (unlikely(self->rows == NULL + || row >= self->n_rows + || col >= self->n_cols)) + return; + + cairo_surface_flush(surf); + bytes = g_bytes_new_with_free_func(cairo_image_surface_get_data(surf), + cairo_image_surface_get_height(surf) + * cairo_image_surface_get_stride(surf), + (GDestroyNotify)cairo_surface_destroy, + cairo_surface_reference(surf)); + + texture = gdk_memory_texture_new(cairo_image_surface_get_width(surf), + cairo_image_surface_get_height(surf), + GDK_MEMORY_A8R8G8B8_PREMULTIPLIED, + bytes, cairo_image_surface_get_stride(surf)); + g_bytes_unref(bytes); + + drow = self->rows + row; + dsign = draw_sign_new(texture, width, height); + g_object_unref(texture); + dglyphs = draw_glyphs_new_sign(dsign); + cells = (width + gui.char_width - 1) / gui.char_width; + + draw_row_fill(drow, col, col + cells - 1, dglyphs, NULL, &self->cursor); + draw_glyphs_unref(dglyphs); } #endif #ifdef FEAT_IMAGE_GDK - /* * Get the draw image with the given id, return NULL if not exists. */ @@ -1381,7 +1773,7 @@ vim_draw_area_add_image( GList *link; DrawImage *dimg; - if (unlikely(self->cells == NULL + if (unlikely(self->rows == NULL || row >= self->n_rows || col >= self->n_cols)) return; @@ -1433,13 +1825,6 @@ vim_draw_area_add_image( vim_draw_area_queue_image(self, link); } - static void -draw_image_free(DrawImage *dimg) -{ - gsk_render_node_unref(dimg->node); - g_free(dimg); -} - /* * Remove the image with the given id if it exists */ @@ -1456,122 +1841,167 @@ vim_draw_area_remove_image(VimDrawArea *self, int id) } #endif +/* + * Add the cursor to the snapshot. + */ static void -flush_invert_ga(garray_T *invert_ga, int row, int start, int len) +vim_draw_area_snapshot_cursor( + VimDrawArea *self, + GskRenderNode *text_node, // May be NULL + GskRenderNode *special_node, // May be NULL + GtkSnapshot *snapshot) { - if (ga_grow(invert_ga, 1) == OK) + DrawCursor *cursor = &self->cursor; + int w = cursor->width; + int h = cursor->height; + graphene_rect_t rect; + + if (!cursor->draw) + return; + + if (cursor->width <= 0 && cursor->height <= 0) + { + // Double width if double width character + w += gui.char_width * (1 + mb_lefthalve(gui.row, gui.col)); + h = gui.char_height; + } + + graphene_rect_init(&rect, +#ifdef FEAT_RIGHTLEFT + CURSOR_BAR_RIGHT ? FILL_X(gui.cursor_col + 1) - w : +#endif + FILL_X(gui.cursor_col), + FILL_Y(gui.cursor_row) + gui.char_height - h, w, h); + + + if (cursor->width == -1 && cursor->height == -1) + { + // Create hollow cursor + GskRoundedRect outline; + static const float border[4] = {1.0f, 1.0f, 1.0f, 1.0f}; + const GdkRGBA color[4] = { + cursor->bg_color, cursor->bg_color, + cursor->bg_color, cursor->bg_color + } ; + + gsk_rounded_rect_init_from_rect(&outline, &rect, 0.0f); + gtk_snapshot_append_border(snapshot, &outline, border, color); + } + else { - graphene_rect_t *arr = (graphene_rect_t *)invert_ga->ga_data; + // Create block style cursor, by masking the text + under decorations + // with the fg color. + gtk_snapshot_append_color(snapshot, &cursor->bg_color, &rect); - graphene_rect_init(arr + invert_ga->ga_len++, - FILL_X(start), FILL_Y(row), - len * gui.char_width, gui.char_height); + gtk_snapshot_push_mask(snapshot, GSK_MASK_MODE_ALPHA); + + // Used as an "outline" for the fg color. + if (text_node != NULL) + gtk_snapshot_append_node(snapshot, text_node); + if (special_node != NULL) + gtk_snapshot_append_node(snapshot, special_node); + + gtk_snapshot_pop(snapshot); + + gtk_snapshot_append_color(snapshot, &cursor->fg_color, &rect); + gtk_snapshot_pop(snapshot); } } static void vim_draw_area_snapshot(GtkWidget *widget, GtkSnapshot *snapshot) { - VimDrawArea *self = VIM_DRAW_AREA(widget); - int height, width; - static const GdkRGBA white = {1, 1, 1, 1}; - garray_T invert_ga; + VimDrawArea *self = VIM_DRAW_AREA(widget); + int height, width; + GtkSnapshot *body_snapshot; + GtkSnapshot *invert_snapshot = NULL; + GskRenderNode *body_node; + GskRenderNode *invert_node = NULL; gui_mch_set_bg_color(gui.back_pixel); height = gtk_widget_get_height(widget) + gui.bleed_bot; width = gtk_widget_get_width(widget) + gui.bleed_right; - if (self->cells == NULL) + if (self->rows == NULL) { gtk_snapshot_append_color(snapshot, gui.bgcolor, &GRAPHENE_RECT_INIT(0, 0, width, height)); return; } - // If number of pixels to bleed has changed, then dirty the nodes at the - // right edge of the draw area. - if (self->bleed_right != gui.bleed_right) + // If number of pixels to bleed has changed, then dirty the background and + // overlay layers of all rows. + if (self->bleed_right == -1 || self->bleed_right != gui.bleed_right) { self->bleed_right = gui.bleed_right; for (int r = 0; r < self->n_rows; r++) { - DrawCell *dcell = &GET_ROW(self, r)[self->n_cols - 1]; - - if (dcell->dnode != NULL) - { - (void)draw_node_make_dirty(dcell->dnode); - draw_node_render(dcell->dnode, r, self); - } + draw_row_dirty_layer(self->rows + r, DRAW_LAYER_BACKGROUND); + draw_row_dirty_layer(self->rows + r, DRAW_LAYER_OVERLAY); } } - // For inverted cells, we first build an array of bounds that represent - // blocks of inverted cells. Then we apply a white color to each of those - // bounds and then finish the blend. - gtk_snapshot_push_blend(snapshot, GSK_BLEND_MODE_DIFFERENCE); - ga_init2(&invert_ga, sizeof(graphene_rect_t), 8); - - gtk_snapshot_append_color(snapshot, gui.bgcolor, - &GRAPHENE_RECT_INIT(0, 0, width, height)); + // First append everything that should be inverted to another snapshot, then + // free that snapshot into a node so it can be blended (if needed). + body_snapshot = gtk_snapshot_new(); for (int r = 0; r < self->n_rows; r++) { - DrawCell *drow = GET_ROW(self, r); - int inv_len = 0; - int inv_start; - - for (int c = 0; c < self->n_cols; c++) - { - DrawCell *dcell = drow + c; - DrawNode *dnode = dcell->dnode; + DrawRow *drow = self->rows + r; - // Batch inverted cells as single row rectangles. - if (dcell->invert) - { - if (inv_len == 0) - inv_start = c; - inv_len++; - } - else if (!dcell->invert && inv_len > 0) - { - flush_invert_ga(&invert_ga, r, inv_start, inv_len); - inv_len = 0; - } + draw_row_render_background(drow, self); + draw_row_render_text(drow, self); + draw_row_render_special(drow, self); + draw_row_render_overlay(drow, self); - if (dnode == NULL) - continue; + for (int l = 0; l < N_DRAW_LAYERS; l++) + { + DrawLayer *dlayer = drow->dlayers + l; - if (dnode->start_col == c) + if (dlayer->node != NULL) { - draw_node_render(dnode, r, self); - assert(dnode->node != NULL); - gtk_snapshot_append_node(snapshot, dnode->node); + if (l == DRAW_LAYER_OVERLAY) + { + if (invert_snapshot == NULL) + invert_snapshot = gtk_snapshot_new(); + gtk_snapshot_append_node(invert_snapshot, dlayer->node); + } + else + gtk_snapshot_append_node(body_snapshot, dlayer->node); } } - // Flush trailing inverted blocks at end of row loop - if (inv_len > 0) - flush_invert_ga(&invert_ga, r, inv_start, inv_len); + + if (r == gui.cursor_row) + vim_draw_area_snapshot_cursor(self, + drow->dlayers[DRAW_LAYER_TEXT].node, + drow->dlayers[DRAW_LAYER_SPECIAL].node, + body_snapshot); } -#ifdef FEAT_SIGN_ICONS - // Order of where the sign icon should be placed shouldn't matter, - // since caller will add whitespace padding in the region it covers. - // Probably should put it behind cursor though. - for (GList *s = self->signs->head; s != NULL; s = s->next) - gtk_snapshot_append_node(snapshot, s->data); -#endif + if (invert_snapshot != NULL) + gtk_snapshot_push_blend(snapshot, GSK_BLEND_MODE_DIFFERENCE); - if (self->cursor_node != NULL) - gtk_snapshot_append_node(snapshot, self->cursor_node); + gtk_snapshot_append_color(snapshot, gui.bgcolor, + &GRAPHENE_RECT_INIT(0, 0, width, height)); + body_node = gtk_snapshot_free_to_node(body_snapshot); + if (body_node != NULL) + { + gtk_snapshot_append_node(snapshot, body_node); + gsk_render_node_unref(body_node); + } - gtk_snapshot_pop(snapshot); - for (int i = 0; i < invert_ga.ga_len; i++) + if (invert_snapshot != NULL) { - graphene_rect_t *rect = &((graphene_rect_t *)invert_ga.ga_data)[i]; - gtk_snapshot_append_color(snapshot, &white, rect); + gtk_snapshot_pop(snapshot); + invert_node = gtk_snapshot_free_to_node(invert_snapshot); + // Not sure if it can be NULL but still check + if (invert_node != NULL) + { + gtk_snapshot_append_node(snapshot, invert_node); + gsk_render_node_unref(invert_node); + } + gtk_snapshot_pop(snapshot); } - gtk_snapshot_pop(snapshot); - ga_clear(&invert_ga); #ifdef FEAT_IMAGE_GDK // Draw images after any possible inversions diff --git a/src/gui_gtk4_da.h b/src/gui_gtk4_da.h index 100d1dba06..5cd1cdc552 100644 --- a/src/gui_gtk4_da.h +++ b/src/gui_gtk4_da.h @@ -21,17 +21,22 @@ G_DECLARE_FINAL_TYPE(VimDrawArea, vim_draw_area, VIM, DRAW_AREA, GtkWidget) GtkWidget *vim_draw_area_new(void); void vim_draw_area_set_size(VimDrawArea *self, int rows, int cols); -void vim_draw_area_add_glyphs(VimDrawArea *self, int row, int col, int num_cells, int flags, PangoFont *font, PangoGlyphString *glyphs); +int vim_draw_area_draw_string(VimDrawArea *self, int row, int col, char_u *s, int len, int flags); void vim_draw_area_clear_block(VimDrawArea *self, int row1, int col1, int row2, int col2); void vim_draw_area_clear(VimDrawArea *self); void vim_draw_area_move_block(VimDrawArea *self, int to, int row1, int row2, int col1, int col2); -void vim_draw_area_set_hollow_cursor(VimDrawArea *self); -void vim_draw_area_set_part_cursor(VimDrawArea *self, int w, int h); +void vim_draw_area_set_cursor(VimDrawArea *self, int w, int h); void vim_draw_area_invert_block(VimDrawArea *self, int row, int col, int nrows, int ncols); +# if defined(FEAT_SIGN_ICONS) void vim_draw_area_add_sign(VimDrawArea *self, GdkTexture *sign, int row, int col, int width, int height); -cairo_t *vim_draw_area_get_multisign_cairo(VimDrawArea *self, int x, int y, int w, int h); -void vim_draw_area_add_image(VimDrawArea *self, GdkTexture *image, int row, int col, int src_x, int src_y, int draw_w, int draw_h, int zindex, int id); +# endif +# ifdef FEAT_NETBEANS_INTG +void vim_draw_area_add_multisign(VimDrawArea *self, cairo_surface_t *surf, int row, int col, int width, int height); +# endif +# ifdef FEAT_IMAGE_GDK +void vim_draw_area_add_image(VimDrawArea *self, GdkTexture *image, int row, int col, int src_x, int src_y, int draw_w, int draw_h, int zindex, int id); void vim_draw_area_remove_image(VimDrawArea *self, int id); +# endif #endif diff --git a/src/netbeans.c b/src/netbeans.c index 99fa9549c2..abe5d11bc5 100644 --- a/src/netbeans.c +++ b/src/netbeans.c @@ -3100,16 +3100,25 @@ netbeans_draw_multisign_indicator(int row) # else GdkDrawable *drawable = gui.drawarea->window; # endif +# ifdef USE_GTK4_SNAPSHOT + cairo_surface_t *surf; +# endif if (!NETBEANS_OPEN) return; x = 0; +# ifdef USE_GTK4_SNAPSHOT + y = 2; +# else y = row * gui.char_height + 2; +# endif # if GTK_CHECK_VERSION(3,0,0) # ifdef USE_GTK4_SNAPSHOT - cr = gui_gtk4_get_multisign_context(x, y, 5, gui.char_height); + surf = cairo_image_surface_create( + CAIRO_FORMAT_ARGB32, 5, gui.char_height); + cr = cairo_create(surf); # else cr = cairo_create(gui.surface); # endif @@ -3143,9 +3152,17 @@ netbeans_draw_multisign_indicator(int row) gdk_draw_point(drawable, gui.text_gc, x+2, y); # endif +# ifdef USE_GTK4_SNAPSHOT + cairo_fill(cr); + gui_gtk4_add_multisign(surf, row, 0, 5, gui.char_height); +# endif + # if GTK_CHECK_VERSION(3,0,0) cairo_destroy(cr); # endif +# ifdef USE_GTK4_SNAPSHOT + cairo_surface_destroy(surf); +# endif } #endif // FEAT_GUI_GTK diff --git a/src/proto/gui_gtk4.pro b/src/proto/gui_gtk4.pro index fddaec21a4..af55b50fcf 100644 --- a/src/proto/gui_gtk4.pro +++ b/src/proto/gui_gtk4.pro @@ -39,7 +39,7 @@ void gui_mch_set_bg_color(guicolor_T color); void gui_mch_set_sp_color(guicolor_T color); guicolor_T gui_mch_get_rgb(guicolor_T pixel); void gui_gtk4_update_size(void); -cairo_t *gui_gtk4_get_multisign_context(int x, int y, int w, int h); +void gui_gtk4_add_multisign(cairo_surface_t *surf, int row, int col, int width, int height); void gui_mch_clear_block(int row1, int col1, int row2, int col2); void gui_mch_clear_all(void); void gui_gtk4_remove_image(win_T *wp); @@ -48,6 +48,7 @@ bool gui_mch_update_popup_image_pixels(win_T *wp); void gui_mch_draw_popup_image(win_T *wp, int row, int col, int src_x, int src_y, int draw_w, int draw_h); void gui_mch_delete_lines(int row, int num_lines); void gui_mch_insert_lines(int row, int num_lines); +void gui_gtk4_draw_cursor(guicolor_T bg, guicolor_T fg, int w, int h); void gui_mch_draw_hollow_cursor(guicolor_T color); void gui_mch_draw_part_cursor(int w, int h, guicolor_T color); void gui_mch_flash(int msec); diff --git a/src/version.c b/src/version.c index 9c342a6419..182dd1d240 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 828, /**/ 827, /**/