# HG changeset patch # User Bram Moolenaar # Date 1634414405 -7200 # Node ID aade8ef975d551e4c6056dbfe28a1808aea28596 # Parent 483b3a06ac6805d2a06de5d628ade3a86820418e patch 8.2.3524: GUI: ligatures are not used Commit: https://github.com/vim/vim/commit/4eeedc09fed0cbbb3ba48317e0a01e20cd0b4f80 Author: Dusan Popovic 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) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt --- 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|), diff --git a/src/errors.h b/src/errors.h --- 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")); diff --git a/src/gui.c b/src/gui.c --- 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) { diff --git a/src/gui.h b/src/gui.h --- 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) \ diff --git a/src/gui_gtk_x11.c b/src/gui_gtk_x11.c --- 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); diff --git a/src/option.h b/src/option.h --- 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 diff --git a/src/optiondefs.h b/src/optiondefs.h --- 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, diff --git a/src/optionstr.c b/src/optionstr.c --- 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' diff --git a/src/proto/gui.pro b/src/proto/gui.pro --- 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); diff --git a/src/proto/gui_gtk_x11.pro b/src/proto/gui_gtk_x11.pro --- 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); diff --git a/src/testdir/test_gui.vim b/src/testdir/test_gui.vim --- 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 = '' diff --git a/src/version.c b/src/version.c --- 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,