From e0169edec93f3ef9de9755a71a04a7087e952928 Mon Sep 17 00:00:00 2001 From: bakkeby Date: Fri, 5 Jun 2020 13:43:14 +0200 Subject: [PATCH] Adding ligatures patch as requested in #4 --- Makefile | 12 +++-- README.md | 5 ++ config.mk | 8 +-- hb.c | 136 ++++++++++++++++++++++++++++++++++++++++++++++++++ hb.h | 6 +++ patches.def.h | 15 +++++- st.c | 6 +++ st.h | 11 ++++ win.h | 4 ++ x.c | 27 ++++++++++ 10 files changed, 222 insertions(+), 8 deletions(-) create mode 100644 hb.c create mode 100644 hb.h diff --git a/Makefile b/Makefile index ff9d256..42dfc10 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,11 @@ include config.mk -SRC = st.c x.c +# Uncomment the line below and the hb.o line further down for the ligatures patch +#LIGATURES_C = hb.c +#LIGATURES_H = hb.h + +SRC = st.c x.c $(LIGATURES_C) OBJ = $(SRC:.c=.o) all: options st @@ -25,7 +29,9 @@ patches.h: $(CC) $(STCFLAGS) -c $< st.o: config.h st.h win.h -x.o: arg.h config.h st.h win.h +x.o: arg.h config.h st.h win.h $(LIGATURES_H) +# Uncomment the below line for the ligatures patch +#hb.o: st.h $(OBJ): config.h config.mk patches.h @@ -38,7 +44,7 @@ clean: dist: clean mkdir -p st-$(VERSION) cp -R FAQ LEGACY TODO LICENSE Makefile README config.mk\ - config.def.h st.info st.1 arg.h st.h win.h $(SRC)\ + config.def.h st.info st.1 arg.h st.h win.h $(LIGATURES_H) $(SRC)\ st-$(VERSION) tar -cf - st-$(VERSION) | gzip > st-$(VERSION).tar.gz rm -rf st-$(VERSION) diff --git a/README.md b/README.md index 2c18acc..42a025a 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,8 @@ Refer to [https://st.suckless.org/](https://st.suckless.org/) for details on the ### Changelog: +2020-06-05 - Added the ligatures patch + 2020-05-20 - Upgrade to 222876, 2020-05-09, and removed visualbell 1, 2, 3 patches and force redraw after keypress due to incompatibility. Refer to tag [371878](https://github.com/bakkeby/st-flexipatch/tree/371878) if you want to try these out. 2020-04-20 - Upgrade to c279f5, 2020-04-19, and added the force redraw on pselect after key is pressed patch and the externalpipein patch @@ -93,6 +95,9 @@ Refer to [https://st.suckless.org/](https://st.suckless.org/) for details on the - [keyboard-select](https://st.suckless.org/patches/keyboard_select/) - allows you to select text on the terminal using keyboard shortcuts + - [ligatures](https://st.suckless.org/patches/ligatures/) + - adds support for drawing ligatures using the Harfbuzz library to transform original text of a single line to a list of glyphs with ligatures included + - [newterm](https://st.suckless.org/patches/newterm/) - allows you to spawn a new st terminal using Ctrl-Shift-Return - it will have the same CWD (current working directory) as the original st instance diff --git a/config.mk b/config.mk index 07709fc..e0fbcbf 100644 --- a/config.mk +++ b/config.mk @@ -18,13 +18,15 @@ PKG_CONFIG = pkg-config # Uncomment this for the themed cursor patch / THEMED_CURSOR_PATCH #XCURSOR = -lXcursor -# includes and libs +# includes and libs, uncomment harfbuzz for the ligatures patch INCS = -I$(X11INC) \ `$(PKG_CONFIG) --cflags fontconfig` \ - `$(PKG_CONFIG) --cflags freetype2` + `$(PKG_CONFIG) --cflags freetype2` \ +# `$(PKG_CONFIG) --cflags harfbuzz` LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft ${XRENDER} ${XCURSOR}\ `$(PKG_CONFIG) --libs fontconfig` \ - `$(PKG_CONFIG) --libs freetype2` + `$(PKG_CONFIG) --libs freetype2` \ +# `$(PKG_CONFIG) --libs harfbuzz` # flags STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600 diff --git a/hb.c b/hb.c new file mode 100644 index 0000000..61fae1e --- /dev/null +++ b/hb.c @@ -0,0 +1,136 @@ +#include +#include +#include +#include +#include +#include + +#include "st.h" + +void hbtransformsegment(XftFont *xfont, const Glyph *string, hb_codepoint_t *codepoints, int start, int length); +hb_font_t *hbfindfont(XftFont *match); + +typedef struct { + XftFont *match; + hb_font_t *font; +} HbFontMatch; + +static int hbfontslen = 0; +static HbFontMatch *hbfontcache = NULL; + +void +hbunloadfonts() +{ + for (int i = 0; i < hbfontslen; i++) { + hb_font_destroy(hbfontcache[i].font); + XftUnlockFace(hbfontcache[i].match); + } + + if (hbfontcache != NULL) { + free(hbfontcache); + hbfontcache = NULL; + } + hbfontslen = 0; +} + +hb_font_t * +hbfindfont(XftFont *match) +{ + for (int i = 0; i < hbfontslen; i++) { + if (hbfontcache[i].match == match) + return hbfontcache[i].font; + } + + /* Font not found in cache, caching it now. */ + hbfontcache = realloc(hbfontcache, sizeof(HbFontMatch) * (hbfontslen + 1)); + FT_Face face = XftLockFace(match); + hb_font_t *font = hb_ft_font_create(face, NULL); + if (font == NULL) + die("Failed to load Harfbuzz font."); + + hbfontcache[hbfontslen].match = match; + hbfontcache[hbfontslen].font = font; + hbfontslen += 1; + + return font; +} + +void +hbtransform(XftGlyphFontSpec *specs, const Glyph *glyphs, size_t len, int x, int y) +{ + int start = 0, length = 1, gstart = 0; + hb_codepoint_t *codepoints = calloc(len, sizeof(hb_codepoint_t)); + + for (int idx = 1, specidx = 1; idx < len; idx++) { + if (glyphs[idx].mode & ATTR_WDUMMY) { + length += 1; + continue; + } + + if (specs[specidx].font != specs[start].font || ATTRCMP(glyphs[gstart], glyphs[idx]) || selected(x + idx, y) != selected(x + gstart, y)) { + hbtransformsegment(specs[start].font, glyphs, codepoints, gstart, length); + + /* Reset the sequence. */ + length = 1; + start = specidx; + gstart = idx; + } else { + length += 1; + } + + specidx++; + } + + /* EOL. */ + hbtransformsegment(specs[start].font, glyphs, codepoints, gstart, length); + + /* Apply the transformation to glyph specs. */ + for (int i = 0, specidx = 0; i < len; i++) { + if (glyphs[i].mode & ATTR_WDUMMY) + continue; + + if (codepoints[i] != specs[specidx].glyph) + ((Glyph *)glyphs)[i].mode |= ATTR_LIGA; + + specs[specidx++].glyph = codepoints[i]; + } + + free(codepoints); +} + +void +hbtransformsegment(XftFont *xfont, const Glyph *string, hb_codepoint_t *codepoints, int start, int length) +{ + hb_font_t *font = hbfindfont(xfont); + if (font == NULL) + return; + + Rune rune; + ushort mode = USHRT_MAX; + hb_buffer_t *buffer = hb_buffer_create(); + hb_buffer_set_direction(buffer, HB_DIRECTION_LTR); + + /* Fill buffer with codepoints. */ + for (int i = start; i < (start+length); i++) { + rune = string[i].u; + mode = string[i].mode; + if (mode & ATTR_WDUMMY) + rune = 0x0020; + hb_buffer_add_codepoints(buffer, &rune, 1, 0, 1); + } + + /* Shape the segment. */ + hb_shape(font, buffer, NULL, 0); + + /* Get new glyph info. */ + hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buffer, NULL); + + /* Write new codepoints. */ + for (int i = 0; i < length; i++) { + hb_codepoint_t gid = info[i].codepoint; + codepoints[start+i] = gid; + } + + /* Cleanup. */ + hb_buffer_destroy(buffer); +} \ No newline at end of file diff --git a/hb.h b/hb.h new file mode 100644 index 0000000..07888df --- /dev/null +++ b/hb.h @@ -0,0 +1,6 @@ +#include +#include +#include + +void hbunloadfonts(); +void hbtransform(XftGlyphFontSpec *, const Glyph *, size_t, int, int); diff --git a/patches.def.h b/patches.def.h index 42e1b94..fc279bf 100644 --- a/patches.def.h +++ b/patches.def.h @@ -10,13 +10,13 @@ /* Patches */ /* The alpha patch adds transparency for the terminal. + * You need to uncomment the corresponding line in config.mk to use the -lXrender library + * when including this patch. * https://st.suckless.org/patches/alpha/ */ #define ALPHA_PATCH 0 /* This patch allows st to reize to any pixel size rather than snapping to character width/height. - * You need to uncomment the corresponding line in config.mk to use the -lXrender library - * when including this patch. * https://st.suckless.org/patches/anysize/ */ #define ANYSIZE_PATCH 0 @@ -126,6 +126,17 @@ */ #define KEYBOARDSELECT_PATCH 0 +/* This patch adds support for drawing ligatures using the Harfbuzz library to transform + * original text of a single line to a list of glyphs with ligatures included. + * This patch depends on the Harfbuzz library and headers to compile. + * You need to uncomment the corresponding line in config.mk to use the harfbuzz library + * when including this patch. + * You need to uncomment the corresponding lines in Makefile when including this patch. + * https://github.com/cog1to/st-ligatures + * https://st.suckless.org/patches/ligatures/ + */ +#define LIGATURES_PATCH 0 + /* This patch allows you to spawn a new st terminal using Ctrl-Shift-Return. It will have the * same CWD (current working directory) as the original st instance. * https://st.suckless.org/patches/newterm/ diff --git a/st.c b/st.c index b03597d..ac5394d 100644 --- a/st.c +++ b/st.c @@ -2784,8 +2784,14 @@ draw(void) #if SCROLLBACK_PATCH if (term.scr == 0) #endif // SCROLLBACK_PATCH + #if LIGATURES_PATCH + xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], + term.ocx, term.ocy, term.line[term.ocy][term.ocx], + term.line[term.ocy], term.col); + #else xdrawcursor(cx, term.c.y, term.line[term.c.y][cx], term.ocx, term.ocy, term.line[term.ocy][term.ocx]); + #endif // LIGATURES_PATCH term.ocx = cx; term.ocy = term.c.y; xfinishdraw(); diff --git a/st.h b/st.h index 1d48e18..9208090 100644 --- a/st.h +++ b/st.h @@ -12,8 +12,14 @@ #define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d)) #define DEFAULT(a, b) (a) = (a) ? (a) : (b) #define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x) +#if LIGATURES_PATCH +#define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP) & (~ATTR_LIGA)) != ((b).mode & (~ATTR_WRAP) & (~ATTR_LIGA)) || \ + (a).fg != (b).fg || \ + (a).bg != (b).bg) +#else #define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \ (a).bg != (b).bg) +#endif // LIGATURES_PATCH #define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \ (t1.tv_nsec-t2.tv_nsec)/1E6) #define MODBIT(x, set, bit) ((set) ? ((x) |= (bit)) : ((x) &= ~(bit))) @@ -36,6 +42,11 @@ enum glyph_attribute { ATTR_WDUMMY = 1 << 10, #if BOXDRAW_PATCH ATTR_BOXDRAW = 1 << 11, + #if LIGATURES_PATCH + ATTR_LIGA = 1 << 12, + #endif // LIGATURES_PATCH + #elif LIGATURES_PATCH + ATTR_LIGA = 1 << 11, #endif // BOXDRAW_PATCH ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT, }; diff --git a/win.h b/win.h index 0272cbe..b932250 100644 --- a/win.h +++ b/win.h @@ -28,7 +28,11 @@ enum win_mode { void xbell(void); void xclipcopy(void); +#if LIGATURES_PATCH +void xdrawcursor(int, int, Glyph, int, int, Glyph, Line, int); +#else void xdrawcursor(int, int, Glyph, int, int, Glyph); +#endif // LIGATURES_PATCH void xdrawline(Line, int, int, int); void xfinishdraw(void); void xloadcols(void); diff --git a/x.c b/x.c index 0c99255..6a5e5d3 100644 --- a/x.c +++ b/x.c @@ -19,6 +19,9 @@ char *argv0; #include "arg.h" #include "st.h" #include "win.h" +#if LIGATURES_PATCH +#include "hb.h" +#endif // LIGATURES_PATCH #if THEMED_CURSOR_PATCH #include @@ -1216,6 +1219,11 @@ xunloadfont(Font *f) void xunloadfonts(void) { + #if LIGATURES_PATCH + /* Clear Harfbuzz font cache. */ + hbunloadfonts(); + #endif // LIGATURES_PATCH + /* Free the loaded fonts in the font cache. */ while (frclen > 0) XftFontClose(xw.dpy, frc[--frclen].font); @@ -1449,7 +1457,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x mode = glyphs[i].mode; /* Skip dummy wide-character spacing. */ + #if LIGATURES_PATCH + if (mode & ATTR_WDUMMY) + #else if (mode == ATTR_WDUMMY) + #endif // LIGATURES_PATCH continue; /* Determine font for glyph if different from previous glyph. */ @@ -1570,6 +1582,11 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x numspecs++; } + #if LIGATURES_PATCH + /* Harfbuzz transformation for ligatures. */ + hbtransform(specs, glyphs, len, x, y); + #endif // LIGATURES_PATCH + return numspecs; } @@ -1780,14 +1797,24 @@ xdrawglyph(Glyph g, int x, int y) } void +#if LIGATURES_PATCH +xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int len) +#else xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og) +#endif // LIGATURES_PATCH { Color drawcol; /* remove the old cursor */ if (selected(ox, oy)) og.mode ^= ATTR_REVERSE; + #if LIGATURES_PATCH + /* Redraw the line where cursor was previously. + * It will restore the ligatures broken by the cursor. */ + xdrawline(line, 0, oy, len); + #else xdrawglyph(og, ox, oy); + #endif // LIGATURES_PATCH if (IS_SET(MODE_HIDE)) return;