changeset 25982:aade8ef975d5 v8.2.3524

patch 8.2.3524: GUI: ligatures are not used Commit: https://github.com/vim/vim/commit/4eeedc09fed0cbbb3ba48317e0a01e20cd0b4f80 Author: Dusan Popovic <dpx@binaryapparatus.com> Date: Sat Oct 16 20:52:05 2021 +0100 patch 8.2.3524: GUI: ligatures are not used Problem: GUI: ligatures are not used. Solution: Add the 'guiligatures' option. (Dusan Popovic, closes https://github.com/vim/vim/issues/8933)
author Bram Moolenaar <Bram@vim.org>
date Sat, 16 Oct 2021 22:00:05 +0200
parents 483b3a06ac68
children e9547957500d
files runtime/doc/options.txt src/errors.h src/gui.c src/gui.h src/gui_gtk_x11.c src/option.h src/optiondefs.h src/optionstr.c src/proto/gui.pro src/proto/gui_gtk_x11.pro src/testdir/test_gui.vim src/version.c
diffstat 12 files changed, 239 insertions(+), 14 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -3790,6 +3790,18 @@ A jump table for the options with a shor
 	screen.  Set it to a negative value to allow windows taller than the
 	screen.
 
+						*'guiligatures'* *'gli'* *E1243*
+'guiligatures' 'gli'	string	(default "")
+			global
+			{only for GTK GUI}
+	List of ASCII characters that, when combined together, can create more
+	complex shapes. Each character must be a printable ASCII character
+	with a value in the 32-127 range.
+	Example: >
+		:set guiligatures=!\"#$%&()*+-./:<=>?@[]^_{\|~
+<	Changing this option updates screen output immediately. Set it to an
+	empty string to disable ligatures.
+
 						*'guioptions'* *'go'*
 'guioptions' 'go'	string	(default "egmrLtT"   (MS-Windows,
 					   "t" is removed in |defaults.vim|),
--- a/src/errors.h
+++ b/src/errors.h
@@ -670,3 +670,5 @@ EXTERN char e_separator_not_supported_st
 	INIT(= N_("E1241: Separator not supported: %s"));
 EXTERN char e_no_white_space_allowed_before_separator_str[]
 	INIT(= N_("E1242: No white space allowed before separator: %s"));
+EXTERN char e_ascii_code_not_in_range[]
+	INIT(= N_("E1243: ASCII code not in 32-127 range"));
--- a/src/gui.c
+++ b/src/gui.c
@@ -460,6 +460,10 @@ gui_init_check(void)
     gui.scrollbar_width = gui.scrollbar_height = SB_DEFAULT_WIDTH;
     gui.prev_wrap = -1;
 
+# ifdef FEAT_GUI_GTK
+    CLEAR_FIELD(gui.ligatures_map);
+#endif
+
 #if defined(ALWAYS_USE_GUI) || defined(VIMDLL)
     result = OK;
 #else
@@ -1065,6 +1069,36 @@ gui_get_wide_font(void)
     return OK;
 }
 
+#if defined(FEAT_GUI_GTK) || defined(PROTO)
+/*
+ * Set list of ascii characters that combined can create ligature.
+ * Store them in char map for quick access from gui_gtk2_draw_string.
+ */
+    void
+gui_set_ligatures(void)
+{
+    char_u	*p;
+
+    if (*p_guiligatures != NUL)
+    {
+	// check for invalid characters
+	for (p = p_guiligatures; *p != NUL; ++p)
+	    if (*p < 32 || *p > 127)
+	    {
+		emsg(_(e_ascii_code_not_in_range));
+		return;
+	    }
+
+	// store valid setting into ligatures_map
+	CLEAR_FIELD(gui.ligatures_map);
+	for (p = p_guiligatures; *p != NUL; ++p)
+	    gui.ligatures_map[*p] = 1;
+    }
+    else
+	CLEAR_FIELD(gui.ligatures_map);
+}
+#endif
+
     static void
 gui_set_cursor(int row, int col)
 {
--- a/src/gui.h
+++ b/src/gui.h
@@ -409,6 +409,9 @@ typedef struct Gui
     char_u	*browse_fname;	    // file name from filedlg
 
     guint32	event_time;
+
+    char_u ligatures_map[256];	    // ascii map for characters 0-255, value is
+				    // 1 if in 'guiligatures'
 #endif	// FEAT_GUI_GTK
 
 #if defined(FEAT_GUI_TABLINE) \
--- a/src/gui_gtk_x11.c
+++ b/src/gui_gtk_x11.c
@@ -5595,18 +5595,22 @@ draw_under(int flags, int row, int col, 
     int
 gui_gtk2_draw_string(int row, int col, char_u *s, int len, int flags)
 {
-    GdkRectangle	area;		    // area for clip mask
-    PangoGlyphString	*glyphs;	    // glyphs of current item
-    int			column_offset = 0;  // column offset in cells
-    int			i;
-    char_u		*conv_buf = NULL;   // result of UTF-8 conversion
-    char_u		*new_conv_buf;
-    int			convlen;
-    char_u		*sp, *bp;
-    int			plen;
-#if GTK_CHECK_VERSION(3,0,0)
-    cairo_t		*cr;
-#endif
+    char_u	*conv_buf = NULL;   // result of UTF-8 conversion
+    char_u	*new_conv_buf;
+    int		convlen;
+    char_u	*sp, *bp;
+    int		plen;
+    int		len_sum;	// return value needs to add up since we are
+				// printing substrings
+    int		byte_sum;	// byte position in string
+    char_u	*cs;		// current *s pointer
+    int		needs_pango;	// look ahead, 0=ascii 1=unicode/ligatures
+    int		should_need_pango;
+    int		slen;
+    int		is_ligature;
+    int		next_is_ligature;
+    int		is_utf8;
+    char_u	backup_ch;
 
     if (gui.text_context == NULL || gtk_widget_get_window(gui.drawarea) == NULL)
 	return len;
@@ -5653,6 +5657,124 @@ gui_gtk2_draw_string(int row, int col, c
     }
 
     /*
+     * Ligature support and complex utf-8 char optimization:
+     * String received to output to screen can print using pre-cached glyphs
+     * (fast) or Pango (slow). Ligatures and multibype utf-8 must use Pango.
+     * Since we receive mixed content string, split it into logical segments
+     * that are guaranteed to go trough glyphs as much as possible. Since
+     * single ligature char prints as ascii, print it that way.
+     */
+    len_sum = 0;    // return value needs to add up since we are printing
+		    // substrings
+    byte_sum = 0;
+    cs = s;
+    // look ahead, 0=ascii 1=unicode/ligatures
+    needs_pango = ((*cs & 0x80) || gui.ligatures_map[*cs]);
+
+    // split string into ascii and non-ascii (ligatures + utf-8) substrings,
+    // print glyphs or use Pango
+    while (cs < s + len)
+    {
+	slen = 0;
+	while (slen < (len - byte_sum))
+	{
+	    is_ligature = gui.ligatures_map[*(cs + slen)];
+	    // look ahead, single ligature char between ascii is ascii
+	    if (is_ligature && !needs_pango)
+	    {
+		if ((slen + 1) < (len - byte_sum))
+		{
+		    next_is_ligature = gui.ligatures_map[*(cs + slen + 1)];
+		    if (!next_is_ligature)
+			is_ligature = 0;
+		}
+		else
+		{
+		    is_ligature = 0;
+		}
+	    }
+	    is_utf8 = *(cs + slen) & 0x80;
+	    should_need_pango = (is_ligature || is_utf8);
+	    if (needs_pango != should_need_pango) // mode switch
+		break;
+	    if (needs_pango)
+	    {
+		if (is_ligature)
+		{
+		    slen++; // ligature char by char
+		}
+		else
+		{
+		    if ((*(cs + slen) & 0xC0) == 0x80)
+		    {
+			// a continuation, find next 0xC0 != 0x80 but don't
+			// include it
+			while ((slen < (len - byte_sum))
+					    && ((*(cs + slen) & 0xC0) == 0x80))
+			{
+			    slen++;
+			}
+		    }
+		    else if ((*(cs + slen) & 0xE0) == 0xC0)
+		    {
+			// + one byte utf8
+			slen++;
+		    }
+		    else if ((*(cs + slen) & 0xF0) == 0xE0)
+		    {
+			// + two bytes utf8
+			slen += 2;
+		    }
+		    else if ((*(cs + slen) & 0xF8) == 0xF0)
+		    {
+			// + three bytes utf8
+			slen += 3;
+		    }
+		    else
+		    {
+			// this should not happen, try moving forward, Pango
+			// will catch it
+			slen++;
+		    }
+		}
+	    }
+	    else
+	    {
+		slen++; // ascii
+	    }
+	}
+	// temporarily zero terminate substring, print, restore char, wrap
+	backup_ch = *(cs + slen);
+	*(cs + slen) = 0;
+	len_sum += gui_gtk2_draw_string_ext(row, col + len_sum,
+						 cs, slen, flags, needs_pango);
+	*(cs + slen) = backup_ch;
+	cs += slen;
+	byte_sum += slen;
+	needs_pango = should_need_pango;
+    }
+    vim_free(conv_buf);
+    return len_sum;
+}
+
+    int
+gui_gtk2_draw_string_ext(
+	int	row,
+	int	col,
+	char_u	*s,
+	int	len,
+	int	flags,
+	int	force_pango)
+{
+    GdkRectangle	area;		    // area for clip mask
+    PangoGlyphString	*glyphs;	    // glyphs of current item
+    int			column_offset = 0;  // column offset in cells
+    int			i;
+#if GTK_CHECK_VERSION(3,0,0)
+    cairo_t		*cr;
+#endif
+
+    /*
      * Restrict all drawing to the current screen line in order to prevent
      * fuzzy font lookups from messing up the screen.
      */
@@ -5679,7 +5801,8 @@ gui_gtk2_draw_string(int row, int col, c
      */
     if (!(flags & DRAW_ITALIC)
 	    && !((flags & DRAW_BOLD) && gui.font_can_bold)
-	    && gui.ascii_glyphs != NULL)
+	    && gui.ascii_glyphs != NULL
+	    && !force_pango)
     {
 	char_u *p;
 
@@ -5883,7 +6006,6 @@ skipitall:
 #endif
 
     pango_glyph_string_free(glyphs);
-    vim_free(conv_buf);
 
 #if GTK_CHECK_VERSION(3,0,0)
     cairo_destroy(cr);
--- a/src/option.h
+++ b/src/option.h
@@ -622,6 +622,9 @@ EXTERN char_u	*p_guifontset;	// 'guifont
 EXTERN char_u	*p_guifontwide;	// 'guifontwide'
 EXTERN int	p_guipty;	// 'guipty'
 #endif
+#ifdef FEAT_GUI_GTK
+EXTERN char_u	*p_guiligatures;  // 'guiligatures'
+# endif
 #if defined(FEAT_GUI_GTK) || defined(FEAT_GUI_X11)
 EXTERN long	p_ghr;		// 'guiheadroom'
 #endif
--- a/src/optiondefs.h
+++ b/src/optiondefs.h
@@ -1208,6 +1208,19 @@ static struct vimoption options[] =
 			    {(char_u *)NULL, (char_u *)0L}
 #endif
 			    SCTX_INIT},
+
+
+    {"guiligatures", "gli", P_STRING|P_VI_DEF|P_RCLR|P_ONECOMMA|P_NODUP,
+#if defined(FEAT_GUI_GTK)
+			    (char_u *)&p_guiligatures, PV_NONE,
+			    {(char_u *)"", (char_u *)0L}
+#else
+			    (char_u *)NULL, PV_NONE,
+			    {(char_u *)NULL, (char_u *)0L}
+#endif
+			    SCTX_INIT},
+
+
     {"guiheadroom", "ghr",  P_NUM|P_VI_DEF,
 #if defined(FEAT_GUI_GTK) || defined(FEAT_GUI_X11)
 			    (char_u *)&p_ghr, PV_NONE,
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -1560,6 +1560,13 @@ ambw_end:
 	redraw_gui_only = TRUE;
     }
 #endif
+# if defined(FEAT_GUI_GTK)
+    else if (varp == &p_guiligatures)
+    {
+	gui_set_ligatures();
+	redraw_gui_only = TRUE;
+    }
+# endif
 
 #ifdef CURSOR_SHAPE
     // 'guicursor'
--- a/src/proto/gui.pro
+++ b/src/proto/gui.pro
@@ -7,6 +7,7 @@ void gui_exit(int rc);
 void gui_shell_closed(void);
 int gui_init_font(char_u *font_list, int fontset);
 int gui_get_wide_font(void);
+void gui_set_ligatures(void);
 void gui_update_cursor(int force, int clear_selection);
 void gui_position_menu(void);
 int gui_get_base_width(void);
--- a/src/proto/gui_gtk_x11.pro
+++ b/src/proto/gui_gtk_x11.pro
@@ -43,6 +43,7 @@ void gui_mch_set_fg_color(guicolor_T col
 void gui_mch_set_bg_color(guicolor_T color);
 void gui_mch_set_sp_color(guicolor_T color);
 int gui_gtk2_draw_string(int row, int col, char_u *s, int len, int flags);
+int gui_gtk2_draw_string_ext(int row, int col, char_u *s, int len, int flags, int force_pango);
 int gui_mch_haskey(char_u *name);
 int gui_get_x11_windis(Window *win, Display **dis);
 Display *gui_mch_get_display(void);
--- a/src/testdir/test_gui.vim
+++ b/src/testdir/test_gui.vim
@@ -567,6 +567,31 @@ func Test_set_guifontwide()
   endif
 endfunc
 
+func Test_set_guiligatures()
+  let skipped = ''
+
+  if !g:x11_based_gui
+    let skipped = g:not_supported . 'guiligatures'
+  else
+    if has('gui_gtk') || has('gui_gtk2') || has('gui_gnome') || has('gui_gtk3')
+      " Try correct value
+      set guiligatures=<>=ab
+      call assert_equal("<>=ab", &guiligatures)
+      " Try to throw error
+      try
+        set guiligatures=<>=šab
+        call assert_report("'set guiligatures=<>=šab should have failed")
+      catch
+        call assert_exception('E1243:')
+      endtry
+    endif
+  endif
+
+  if !empty(skipped)
+    throw skipped
+  endif
+endfunc
+
 func Test_set_guiheadroom()
   let skipped = ''
 
--- a/src/version.c
+++ b/src/version.c
@@ -758,6 +758,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    3524,
+/**/
     3523,
 /**/
     3522,