diff --git a/src/common/common.vcxproj b/src/common/common.vcxproj index c4fb0065..5f2107c2 100644 --- a/src/common/common.vcxproj +++ b/src/common/common.vcxproj @@ -43,6 +43,7 @@ + @@ -79,6 +80,7 @@ + diff --git a/src/common/common.vcxproj.filters b/src/common/common.vcxproj.filters index 5b5f19c1..76723605 100644 --- a/src/common/common.vcxproj.filters +++ b/src/common/common.vcxproj.filters @@ -98,6 +98,9 @@ Header Files + + Header Files + Header Files @@ -202,6 +205,9 @@ Source Files + + Source Files + Source Files diff --git a/src/common/hexcolor.c b/src/common/hexcolor.c new file mode 100644 index 00000000..ec7629a0 --- /dev/null +++ b/src/common/hexcolor.c @@ -0,0 +1,42 @@ +/* ZoiteChat + * Parsing of \004 (hex color) formatting codes: RRGGBB[,RRGGBB]. + * Kept free of other application dependencies so the xtext widget tests can + * compile it on its own. + * Distributed under GPL-2.0-or-later; see COPYING. */ + +#include "hexcolor.h" + +static int +hexcolor_component (const char *s, int avail, guint32 *rgb) +{ + int i; + guint32 value = 0; + + if (avail < 6) + return FALSE; + for (i = 0; i < 6; i++) + { + if (!g_ascii_isxdigit (s[i])) + return FALSE; + value = (value << 4) | g_ascii_xdigit_value (s[i]); + } + if (rgb) + *rgb = value; + return TRUE; +} + +int +hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg) +{ + if (has_bg) + *has_bg = FALSE; + if (!hexcolor_component (s, avail, fg)) + return 0; + if (avail >= 7 && s[6] == ',' && hexcolor_component (s + 7, avail - 7, bg)) + { + if (has_bg) + *has_bg = TRUE; + return 13; + } + return 6; +} diff --git a/src/common/hexcolor.h b/src/common/hexcolor.h new file mode 100644 index 00000000..32256224 --- /dev/null +++ b/src/common/hexcolor.h @@ -0,0 +1,16 @@ +/* ZoiteChat + * Parsing of \004 (hex color) formatting codes: RRGGBB[,RRGGBB]. + * See https://modern.ircdocs.horse/formatting + * Distributed under GPL-2.0-or-later; see COPYING. */ + +#ifndef ZOITECHAT_HEXCOLOR_H +#define ZOITECHAT_HEXCOLOR_H + +#include + +/* Parses the parameters following a \004 byte. Returns the number of bytes + * consumed (6 or 13), or 0 if no valid RRGGBB follows. fg, bg and has_bg + * may be NULL. */ +int hexcolor_parse (const char *s, int avail, guint32 *fg, guint32 *bg, int *has_bg); + +#endif diff --git a/src/common/meson.build b/src/common/meson.build index 53e7643a..e288d165 100644 --- a/src/common/meson.build +++ b/src/common/meson.build @@ -25,6 +25,7 @@ common_sources = [ 'tree.c', 'url.c', 'userlist.c', + 'hexcolor.c', 'util.c' ] @@ -197,3 +198,18 @@ test('Emoji Data Tests', emoji_data_tests, protocol: 'tap', timeout: 120, ) + +hexcolor_tests = executable('hexcolor_tests', + [ + 'tests/test-hexcolor.c', + 'hexcolor.c', + 'util.c', + ], + include_directories: [config_h_include, include_directories('.')], + dependencies: [libgio_dep] + (libssl_dep.found() ? [libssl_dep] : []), +) + +test('Hex Color Tests', hexcolor_tests, + protocol: 'tap', + timeout: 120, +) diff --git a/src/common/tests/test-hexcolor.c b/src/common/tests/test-hexcolor.c new file mode 100644 index 00000000..b635a976 --- /dev/null +++ b/src/common/tests/test-hexcolor.c @@ -0,0 +1,117 @@ +/* ZoiteChat + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +#include + +#include + +#include "../hexcolor.h" +#include "../util.h" + +static void +test_parse_foreground (void) +{ + guint32 fg = 0, bg = 0xDEAD; + int has_bg = TRUE; + + g_assert_cmpint (hexcolor_parse ("FF8800text", 10, &fg, &bg, &has_bg), ==, 6); + g_assert_cmphex (fg, ==, 0xFF8800); + g_assert_false (has_bg); + g_assert_cmphex (bg, ==, 0xDEAD); +} + +static void +test_parse_foreground_and_background (void) +{ + guint32 fg = 0, bg = 0; + int has_bg = FALSE; + + g_assert_cmpint (hexcolor_parse ("112233,a0b0c0x", 14, &fg, &bg, &has_bg), ==, 13); + g_assert_cmphex (fg, ==, 0x112233); + g_assert_cmphex (bg, ==, 0xA0B0C0); + g_assert_true (has_bg); +} + +static void +test_parse_bad_background_keeps_foreground (void) +{ + guint32 fg = 0; + int has_bg = TRUE; + + /* ",12" is plain text after a valid foreground, not a background */ + g_assert_cmpint (hexcolor_parse ("112233,12 hi", 12, &fg, NULL, &has_bg), ==, 6); + g_assert_cmphex (fg, ==, 0x112233); + g_assert_false (has_bg); +} + +static void +test_parse_bare (void) +{ + /* a bare \004 resets colors, like a bare \003 */ + g_assert_cmpint (hexcolor_parse (" text", 5, NULL, NULL, NULL), ==, 0); + g_assert_cmpint (hexcolor_parse ("12345G", 6, NULL, NULL, NULL), ==, 0); + g_assert_cmpint (hexcolor_parse ("", 0, NULL, NULL, NULL), ==, 0); +} + +static void +test_parse_respects_length (void) +{ + g_assert_cmpint (hexcolor_parse ("FF8800", 5, NULL, NULL, NULL), ==, 0); + g_assert_cmpint (hexcolor_parse ("FF8800,112233", 12, NULL, NULL, NULL), ==, 6); +} + +static void +assert_stripped (const char *in, int flags, const char *expected) +{ + char out[128]; + + strip_color2 (in, -1, out, flags); + g_assert_cmpstr (out, ==, expected); +} + +static void +test_strip (void) +{ + assert_stripped ("\004FF8800alice\017: hi", STRIP_ALL, "alice: hi"); + assert_stripped ("\004112233,445566bg", STRIP_ALL, "bg"); + assert_stripped ("a\004 b", STRIP_ALL, "a b"); + assert_stripped ("\004FF8800\00312x", STRIP_ALL, "x"); + /* a trailing code with too few digits leaves the rest as text */ + assert_stripped ("x\004FF88", STRIP_ALL, "xFF88"); +} + +static void +test_strip_keeps_hex_without_strip_color (void) +{ + assert_stripped ("\004FF8800red\002", STRIP_ATTRIB, "\004FF8800red"); +} + +int +main (int argc, char *argv[]) +{ + g_test_init (&argc, &argv, NULL); + + g_test_add_func ("/hexcolor/parse/foreground", test_parse_foreground); + g_test_add_func ("/hexcolor/parse/foreground-and-background", test_parse_foreground_and_background); + g_test_add_func ("/hexcolor/parse/bad-background", test_parse_bad_background_keeps_foreground); + g_test_add_func ("/hexcolor/parse/bare", test_parse_bare); + g_test_add_func ("/hexcolor/parse/length", test_parse_respects_length); + g_test_add_func ("/hexcolor/strip/all", test_strip); + g_test_add_func ("/hexcolor/strip/attrib-only", test_strip_keeps_hex_without_strip_color); + + return g_test_run (); +} diff --git a/src/common/util.c b/src/common/util.c index 77f286a6..001dc083 100644 --- a/src/common/util.c +++ b/src/common/util.c @@ -47,6 +47,7 @@ #include "zoitechatc.h" #include #include "util.h" +#include "hexcolor.h" #if defined (__FreeBSD__) || defined (__APPLE__) #include @@ -330,6 +331,14 @@ strip_color2 (const char *src, int len, char *dst, int flags) if (!(flags & STRIP_COLOR)) goto pass_char; rcol = 2; break; + case '\004': /*ATTR_HEXCOLOR: */ + if (!(flags & STRIP_COLOR)) goto pass_char; + { + int skip = hexcolor_parse (src + 1, len, NULL, NULL, NULL); + src += skip; + len -= skip; + } + break; case HIDDEN_CHAR: /* CL: invisible text (for event formats only) */ /* this takes care of the topic */ if (!(flags & STRIP_HIDDEN)) goto pass_char; break; diff --git a/src/fe-gtk/meson.build b/src/fe-gtk/meson.build index d74ff813..4f3d0aef 100644 --- a/src/fe-gtk/meson.build +++ b/src/fe-gtk/meson.build @@ -280,7 +280,7 @@ test('Emoji Font Tests', emoji_font_tests, ) xtext_tests = executable('xtext_tests', - 'tests/test-xtext.c', + ['tests/test-xtext.c', '../common/hexcolor.c'], include_directories: [config_h_include], dependencies: [gtk_dep], ) diff --git a/src/fe-gtk/xtext.c b/src/fe-gtk/xtext.c index 37ec9b39..c9235567 100644 --- a/src/fe-gtk/xtext.c +++ b/src/fe-gtk/xtext.c @@ -35,6 +35,7 @@ #include "../common/zoitechat.h" #include "../common/fe.h" #include "../common/util.h" +#include "../common/hexcolor.h" #include "../common/zoitechatc.h" #include "../common/url.h" @@ -775,16 +776,35 @@ backend_draw_text_emph (GtkXText *xtext, gboolean dofill, int x, int y, cairo_destroy (cr); } +static XTextColor +xtext_color_for_index (GtkXText *xtext, int index) +{ + if (index == XTEXT_HEX_FG) + return xtext->hex_fg; + if (index == XTEXT_HEX_BG) + return xtext->hex_bg; + return xtext->palette[index]; +} + static void xtext_set_fg (GtkXText *xtext, int index) { - xtext->fgc = xtext->palette[index]; + xtext->fgc = xtext_color_for_index (xtext, index); } static void xtext_set_bg (GtkXText *xtext, int index) { - xtext->bgc = xtext->palette[index]; + xtext->bgc = xtext_color_for_index (xtext, index); +} + +static void +xtext_rgb_to_color (guint32 rgb, XTextColor *color) +{ + color->red = ((rgb >> 16) & 0xFF) / 255.0; + color->green = ((rgb >> 8) & 0xFF) / 255.0; + color->blue = (rgb & 0xFF) / 255.0; + color->alpha = 1.0; } static void @@ -2999,6 +3019,14 @@ gtk_xtext_strip_color (unsigned char *text, int len, unsigned char *outbuf, xtext_do_chunk (&c); rcol = 2; break; + case ATTR_HEXCOLOR: + xtext_do_chunk (&c); + { + int skip = hexcolor_parse ((char *)text + 1, len - 1, NULL, NULL, NULL); + text += skip; + len -= skip; + } + break; case ATTR_BEEP: case ATTR_RESET: case ATTR_REVERSE: @@ -3545,6 +3573,35 @@ gtk_xtext_render_str (GtkXText * xtext, int y, textentry * ent, pstr += j + 1; j = 0; break; + case ATTR_HEXCOLOR: + RENDER_FLUSH; + { + guint32 fg = 0, bg = 0; + int has_bg; + int skip = hexcolor_parse ((char *)str + i + 1, len - i - 1, &fg, &bg, &has_bg); + + if (skip == 0) + gtk_xtext_reset (xtext, mark, FALSE); + else + { + xtext_rgb_to_color (fg, &xtext->hex_fg); + xtext->col_fore = XTEXT_HEX_FG; + if (!mark) + xtext_set_fg (xtext, XTEXT_HEX_FG); + if (has_bg) + { + xtext_rgb_to_color (bg, &xtext->hex_bg); + xtext->col_back = XTEXT_HEX_BG; + xtext->backcolor = TRUE; + if (!mark) + xtext_set_bg (xtext, XTEXT_HEX_BG); + } + } + pstr += j + 1 + skip; + j = 0; + i += skip; + } + break; default: tmp = charlen (str + i); /* invalid utf8 safe guard */ @@ -3762,6 +3819,13 @@ find_next_wrap (GtkXText * xtext, textentry * ent, unsigned char *str, limit_offset++; str++; break; + case ATTR_HEXCOLOR: + { + int skip = hexcolor_parse ((char *)str + 1, (ent->str + ent->str_len) - str - 1, NULL, NULL, NULL); + limit_offset += 1 + skip; + str += 1 + skip; + } + break; case ATTR_HIDDEN: if (xtext->ignore_hidden) goto def; diff --git a/src/fe-gtk/xtext.h b/src/fe-gtk/xtext.h index 9b312d85..fdd442a9 100644 --- a/src/fe-gtk/xtext.h +++ b/src/fe-gtk/xtext.h @@ -38,6 +38,7 @@ GType gtk_xtext_get_type (void); #define ATTR_BOLD '\002' #define ATTR_COLOR '\003' +#define ATTR_HEXCOLOR '\004' #define ATTR_BLINK '\006' #define ATTR_BEEP '\007' #define ATTR_HIDDEN '\010' @@ -57,6 +58,8 @@ GType gtk_xtext_get_type (void); #define XTEXT_BG 102 #define XTEXT_MARKER 103 /* for marker line */ #define XTEXT_MAX_COLOR 98 +#define XTEXT_HEX_FG 200 /* not palette slots: colors from \004RRGGBB */ +#define XTEXT_HEX_BG 201 typedef struct textentry textentry; /* @@ -181,6 +184,8 @@ struct _GtkXText int col_fore; int col_back; + XTextColor hex_fg; + XTextColor hex_bg; int depth; /* gdk window depth */