/* * Copyright (c) 2001-2023 Julien Nadeau Carriere * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE * USE OF THIS SOFTWARE EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * The Agar text rendering interface. It provides text rendering, sizing * and access to font metrics. It handles UTF-8 input (internally UCS-4). * The input may also contain ANSI SGR sequences with alternate color and * typography attributes. * * Text is rendered using an underlying font engine. The following base font * engine classes are provided: * * - AG_Font - Base class for all fonts. * - AG_FontFt - FreeType 2.x (.ttf, .otf, .ttc, .woff2, .woff, .dfont, .fnt). * - AG_FontBf - Agar Bitmap Font format (.agbf, .bmp, .png). */ #include #include #include #include #include #include #include #include #ifdef HAVE_FONTCONFIG # include #endif #include #include #ifdef AG_WIDGETS # include # include # include # include #endif #include #include /* #define DEBUG_FONTS */ /* #define DEBUG_ANSI */ #ifndef AG_DEFAULT_FT_FONT_FACE #define AG_DEFAULT_FT_FONT_FACE "algue" #endif #ifndef AG_DEFAULT_FT_FONT_SIZE #define AG_DEFAULT_FT_FONT_SIZE 14.0f #endif #ifndef AG_DEFAULT_BF_FONT_FACE #define AG_DEFAULT_BF_FONT_FACE "agar-minimal.agbf" #endif #ifndef AG_DEFAULT_BF_FONT_SIZE #define AG_DEFAULT_BF_FONT_SIZE 12.0f #endif int agTextFontHeight = 0; /* Default font height (px) */ int agTextFontAscent = 0; /* Default font ascent (px) */ int agTextFontLineSkip = 0; /* Default font line skip (px) */ int agFontconfigInited = 0; /* Initialized Fontconfig library */ _Nonnull_Mutex AG_Mutex agTextLock; /* Lock on FontCache and TextState */ AG_FontQ agFontCache; /* Open fonts */ AG_TextState agTextStateStack[AG_TEXT_STATES_MAX]; /* Text state stack */ int agTextStateCur = 0; /* Height of stack */ /* ANSI color scheme (may be overridden by AG_TextState) */ AG_Color agTextColorANSI[] = { #if AG_MODEL == AG_LARGE { 0x0000, 0x0000, 0x0000, 0xffff }, /* Black (AGSI_BLK) */ { 0xffff, 0x6b6b, 0x6b6b, 0xffff }, /* Red (AGSI_RED) */ { 0x0000, 0xcdcd, 0x0000, 0xffff }, /* Green (AGSI_GRN) */ { 0xcdcd, 0xcdcd, 0x0000, 0xffff }, /* Yellow (AGSI_YEL) */ { 0x8a8a, 0x9696, 0xffff, 0xffff }, /* Blue (AGSI_BLU) */ { 0xcdcd, 0x0000, 0xcdcd, 0xffff }, /* Magenta (AGSI_MAG) */ { 0x0000, 0xcdcd, 0xcdcd, 0xffff }, /* Cyan (AGSI_CYAN) */ { 0xfafa, 0xebeb, 0xebeb, 0xffff }, /* White (AGSI_WHT) */ { 0x7f7f, 0x7f7f, 0x7f7f, 0xffff }, /* Gray (AGSI_BR_BLK | AGSI_GRAY) */ { 0xffff, 0x1e1e, 0x1e1e, 0xffff }, /* Br.Red (AGSI_BR_RED) */ { 0x0000, 0xffff, 0x0000, 0xffff }, /* Br.Green (AGSI_BR_GRN) */ { 0xffff, 0xffff, 0x0000, 0xffff }, /* Br.Yellow (AGSI_BR_YEL) */ { 0x5c5c, 0x5c5c, 0xffff, 0xffff }, /* Br.Blue (AGSI_BR_BLU) */ { 0xffff, 0x0000, 0xffff, 0xffff }, /* Br.Magenta (AGSI_BR_MAG) */ { 0x0000, 0xffff, 0xffff, 0xffff }, /* Br.Cyan (AGSI_BR_CYAN) */ { 0xffff, 0xffff, 0xffff, 0xffff }, /* Br.White (AGSI_BR_WHT) */ #else { 0, 0, 0, 255 }, /* Black (AGSI_BLK) */ { 255, 107, 107, 255 }, /* Red (AGSI_RED) */ { 0, 205, 0, 255 }, /* Green (AGSI_GRN) */ { 205, 205, 0, 255 }, /* Yellow (AGSI_YEL) */ { 138, 150, 255, 255 }, /* Blue (AGSI_BLU) */ { 205, 0, 205, 255 }, /* Magenta (AGSI_MAG) */ { 0, 205, 205, 255 }, /* Cyan (AGSI_CYAN) */ { 250, 235, 235, 255 }, /* White (AGSI_WHT) */ { 127, 127, 127, 255 }, /* Gray (AGSI_BR_BLK | AGSI_GRAY) */ { 255, 30, 30, 255 }, /* Br.Red (AGSI_BR_RED) */ { 0, 255, 0, 255 }, /* Br.Green (AGSI_BR_GRN) */ { 255, 255, 0, 255 }, /* Br.Yellow (AGSI_BR_YEL) */ { 92, 92, 255, 255 }, /* Br.Blue (AGSI_BR_BLU) */ { 255, 0, 255, 255 }, /* Br.Magenta (AGSI_BR_MAG) */ { 0, 255, 255, 255 }, /* Br.Cyan (AGSI_BR_CYAN) */ { 255, 255, 255, 255 }, /* Br.White (AGSI_BR_WHT) */ #endif }; /* Core fonts provided with Agar (and ANSI SGR addressable) */ const char *agCoreFonts[] = { "algue", /* #1 Algue (default font; built-in) */ "unialgue", /* #2 Unialgue (default font w/ extended Unicode) */ "agar-minimal", /* #3 Agar Minimal (our bitmap font) */ "agar-ideograms", /* #4 Agar Ideograms (our graphical icons) */ "monoalgue", /* #5 Monoalgue (a monospace sans-serif) */ "charter", /* #6 Bitstream Charter (a transitional serif font) */ "Noto Serif CJK SC", /* #7 Noto Serif CJK (locale-dependent variant) */ "Noto Sans CJK SC", /* #8 Noto Sans CJK (locale-dependent variant) */ "league-spartan", /* #9 League Spartan (a geometric sans-serif) */ "league-gothic", /* #10 League Gothic (a condensable Gothic font) */ "fraktur", /* #11 Unifraktur Maguntia (a Fraktur font) */ "fira-sans", /* #12 Fira Sans */ "fira-sans-condensed", /* #13 Fira Sans Condensed */ "algue", /* #14 unused */ "algue", /* #15 unused */ "algue", /* #16 unused */ "algue", /* #17 unused */ NULL }; /* Canned dialog titles (window captions). */ const char *agTextMsgTitles[] = { /* For enum ag_text_msg_title */ N_("Error"), N_("Warning"), N_("Information"), NULL }; /* Names of justification modes. */ const char *agTextJustifyNames[] = { /* For enum ag_text_justify */ N_("Left"), N_("Center"), N_("Right"), NULL }; /* Names of vertical alignment modes. */ const char *agTextValignNames[] = { /* For enum ag_text_valign */ N_("Top"), N_("Middle"), N_("Bottom"), NULL }; AG_Font *_Nullable agDefaultFont = NULL; /* Default font */ static int agTextInitedSubsystem = 0; /* AG_Text is initialized */ /* * Save the current text rendering state to the AG_TextState stack and * increment its height by one unit. The text rendering state includes: * * - Current Font (reference to a loaded font). * - Current Background and Foreground colors. * - Current ANSI color scheme (16-color palette; copy-on-write). * - Horizontal justification and vertical alignment modes. * - Width of tabs (\t) in pixels. * * This routine may be called from Widget operations draw(), size_alloc(), * size_request() or from event-handling context. Widgets which do not use * the USE_TEXT flag must push the rendering state prior to modifying it. */ void AG_PushTextState(void) { const AG_TextState *tsPrev = AG_TEXT_STATE_CUR(); AG_TextState *ts; AG_Font *fontPrev; if ((agTextStateCur + 1) >= AG_TEXT_STATES_MAX) AG_FatalError("PushTextState Overflow"); ts = &agTextStateStack[++agTextStateCur]; memcpy(ts, tsPrev, sizeof(AG_TextState)); if (tsPrev->colorANSI != NULL) { const AG_Size size = AG_ANSI_COLOR_LAST * sizeof(AG_Color); ts->colorANSI = Malloc(size); memcpy(ts->colorANSI, tsPrev->colorANSI, size); } fontPrev = tsPrev->font; ts->font = AG_FetchFont(fontPrev->name, fontPrev->spec.size, fontPrev->flags); } /* Create a copy of the current text state in dst. */ void AG_CopyTextState(AG_TextState *dst) { const AG_TextState *ts = AG_TEXT_STATE_CUR(); memcpy(dst, ts, sizeof(AG_TextState)); } /* * Set a color in the ANSI color scheme (16-color palette). * Applies to the current text rendering state. * the default, the modified palette is included in the text state. */ void AG_TextColorANSI(enum ag_ansi_color colorANSI, const AG_Color *c) { AG_TextState *ts = AG_TEXT_STATE_CUR(); AG_Color *tgt; if (colorANSI >= AG_ANSI_COLOR_LAST) { AG_FatalError("colorANSI"); } if (ts->colorANSI == NULL) { const AG_Size size = AG_ANSI_COLOR_LAST * sizeof(AG_Color); ts->colorANSI = Malloc(size); memcpy(ts->colorANSI, agTextColorANSI, size); } tgt = &ts->colorANSI[colorANSI]; memcpy(tgt, c, sizeof(AG_Color)); } /* * Select font family, size, style, weight and width variant. * Applies to the current text rendering state. */ AG_Font * AG_TextFontLookup(const char *face, float size, Uint flags) { AG_Font *newFont; AG_TextState *ts; if ((newFont = AG_FetchFont(face, size, flags)) == NULL) { return (NULL); } ts = AG_TEXT_STATE_CUR(); ts->font = newFont; return (newFont); } /* * Change the font size (in points) of the current font. * Applies to the current text rendering state. */ AG_Font * AG_TextFontPts(float size) { AG_TextState *ts = AG_TEXT_STATE_CUR(); const AG_Font *fontCur = ts->font; AG_Font *font; if (!(font = AG_FetchFont(fontCur->name, size, fontCur->flags))) { return (NULL); } ts->font = font; return (font); } /* * Change the font size (in %) of the current font. * Applies to the current text rendering state. */ AG_Font * AG_TextFontPct(int pct) { AG_TextState *ts = AG_TEXT_STATE_CUR(); const AG_Font *font = ts->font; return AG_FetchFont(font->name, (font->spec.size * pct / 100.0f), font->flags); } /* * Change the font size (in %) and flags (style / weight / width variant) * of the current font. * Applies to the current text rendering state. */ AG_Font * AG_TextFontPctFlags(int pct, Uint flags) { AG_TextState *ts = AG_TEXT_STATE_CUR(); const AG_Font *font = ts->font; return AG_FetchFont(font->name, (font->spec.size * pct / 100.0f), flags); } /* * Restore the text rendering state to the previously saved state and * decrease the height of the stack by one element. */ void AG_PopTextState(void) { if (agTextStateCur == 0) { #ifdef AG_DEBUG AG_Verbose("AG_PopTextState() without Push\n"); #endif return; } --agTextStateCur; } /* Clear the glyph cache. */ void AG_TextClearGlyphCache(AG_Driver *drv) { int i; AG_Glyph *G, *Gnext; for (i = 0; i < AG_GLYPH_NBUCKETS; i++) { for (G = SLIST_FIRST(&drv->glyphCache[i].glyphs); G != SLIST_END(&drv->glyphCache[i].glyphs); G = Gnext) { Gnext = SLIST_NEXT(G, glyphs); AG_SurfaceFree(G->su); free(G); } SLIST_INIT(&drv->glyphCache[i].glyphs); } } static __inline__ void InitMetrics(AG_TextMetrics *_Nonnull Tm) { Tm->w = 0; Tm->h = 0; Tm->wLines = NULL; Tm->nLines = 0; } static __inline__ void FreeMetrics(AG_TextMetrics *_Nonnull Tm) { Free(Tm->wLines); } /* * Return the expected size in pixels of a rendered C string. * If Unicode is supported, the string may contain UTF-8. */ void AG_TextSize(const char *text, int *w, int *h) { #ifdef AG_UNICODE AG_Char *s; #endif if (text == NULL || text[0] == '\0') { if (w != NULL) { *w = 0; } if (h != NULL) { *h = 0; } return; } #ifdef AG_UNICODE if ((s = AG_ImportUnicode("UTF-8", text, NULL, NULL)) != NULL) { AG_TextSizeInternal(s, w,h); free(s); } #else AG_TextSizeInternal((const Uint8 *)text, w,h); #endif } /* * Return the rendered size in pixels of a string of internal AG_Char (which * may be internal UCS-4 or ASCII). */ void AG_TextSizeInternal(const AG_Char *s, int *w, int *h) { AG_TextMetrics Tm; InitMetrics(&Tm); if (s != NULL && (char)(s[0]) != '\0') { const AG_TextState *ts = AG_TEXT_STATE_CUR(); const AG_Font *font = ts->font; #ifdef AG_DEBUG if (!AG_OBJECT_VALID(font) || !AG_FONT_ISA(font)) AG_FatalError("Bad font"); #endif AGFONT_OPS(font)->size(font, s, &Tm, 0); } if (w != NULL) { *w = Tm.w; } if (h != NULL) { *h = Tm.h; } FreeMetrics(&Tm); } /* * Return the rendered size in pixels of a C string (which may contain UTF-8). * Return a line count and an array of line widths. */ void AG_TextSizeMulti(const char *text, int *w, int *h, Uint **wLines, Uint *nLines) { #ifdef AG_UNICODE AG_Char *s; if ((s = AG_ImportUnicode("UTF-8", text, NULL, NULL)) != NULL) { AG_TextSizeMultiInternal(s, w, h, wLines, nLines); free(s); return; } #else AG_TextSizeMultiInternal((const Uint8 *)text, w, h, wLines, nLines); #endif } /* * Compute the rendered size of a string of internal AG_Char (which may * be UCS-4 or ASCII). Return a line count and an array of line widths. */ void AG_TextSizeMultiInternal(const AG_Char *s, int *w, int *h, Uint **wLines, Uint *nLines) { AG_TextMetrics Tm; InitMetrics(&Tm); if (s != NULL && (char)(s[0]) != '\0') { const AG_TextState *ts = AG_TEXT_STATE_CUR(); AG_Font *font = ts->font; #ifdef AG_DEBUG if (!AG_OBJECT_VALID(font) || !AG_FONT_ISA(font)) AG_FatalError("Bad font"); #endif AGFONT_OPS(font)->size(font, s, &Tm, 1); } if (w != NULL) { *w = Tm.w; } if (h != NULL) { *h = Tm.h; } if (Tm.nLines == 1) { Tm.wLines = Realloc(Tm.wLines, sizeof(Uint)); Tm.wLines[0] = Tm.w; } if (wLines != NULL) { *wLines = Tm.wLines; } if (nLines != NULL) { *nLines = Tm.nLines; } } /* * Canned notification and dialog windows. * TODO move this elsewhere */ #ifdef AG_WIDGETS /* Display an informational message window. */ void AG_TextMsg(enum ag_text_msg_title title, const char *fmt, ...) { va_list ap; char *s; va_start(ap, fmt); Vasprintf(&s, fmt, ap); va_end(ap); AG_TextMsgS(title, s); free(s); } void AG_TextMsgS(enum ag_text_msg_title title, const char *s) { AG_Window *win; AG_Box *vb; win = AG_WindowNew(AG_WINDOW_NORESIZE | AG_WINDOW_NOCLOSE | AG_WINDOW_NOMINIMIZE | AG_WINDOW_NOMAXIMIZE); if (win == NULL) return; win->wmType = AG_WINDOW_WM_NOTIFICATION; AG_WindowSetCaptionS(win, _(agTextMsgTitles[title])); AG_WindowSetPosition(win, AG_WINDOW_CENTER, 1); vb = AG_BoxNewVert(win, 0); AG_SetFontSize(vb, "120%"); AG_LabelNewS(vb, 0, s); vb = AG_BoxNewVert(win, AG_BOX_HOMOGENOUS | AG_BOX_EXPAND); AG_WidgetFocus( AG_ButtonNewFn(vb, 0, _("Ok"), AGWINDETACH(win)) ); AG_WindowShow(win); } /* Display the last error message from AG_GetError(). */ void AG_TextMsgFromError(void) { AG_TextMsgS(AG_MSG_ERROR, AG_GetError()); } static Uint32 TextTmsgExpire(AG_Timer *_Nonnull to, AG_Event *_Nonnull event) { AG_Window *win = AGWINDOW(event->argv[1].data.p); if (!AG_OBJECT_VALID(win) || !AG_WINDOW_ISA(win)) { return (0); } AG_ObjectDetach(win); return (0); } /* Display a message for a given period of time (format string). */ void AG_TextTmsg(enum ag_text_msg_title title, Uint32 expire, const char *fmt, ...) { va_list ap; char *s; va_start(ap, fmt); Vasprintf(&s, fmt, ap); va_end(ap); AG_TextTmsgS(title, expire, s); free(s); } void AG_TextTmsgS(enum ag_text_msg_title title, Uint32 ticks, const char *s) { AG_Window *win; AG_Box *vb; AG_Timer *to; win = AG_WindowNew(AG_WINDOW_NORESIZE | AG_WINDOW_NOCLOSE | AG_WINDOW_NOMINIMIZE | AG_WINDOW_NOMAXIMIZE); if (win == NULL) return; win->wmType = AG_WINDOW_WM_NOTIFICATION; AG_WindowSetCaptionS(win, _(agTextMsgTitles[title])); AG_WindowSetPosition(win, AG_WINDOW_CENTER, 1); vb = AG_BoxNewVert(win, 0); AG_SetFontSize(vb, "120%"); AG_LabelNewS(vb, 0, s); AG_WindowShow(win); to = AG_AddTimerAuto(NULL, ticks, TextTmsgExpire, "%p", win); if (to != NULL) Strlcpy(to->name, "textTmsg", sizeof(to->name)); } /* * Display an informational message with a "Don't tell me again" option. * The user preference is preserved in a persistent table. Unlike warnings, * the dialog window is not modal (format string). */ void AG_TextInfo(const char *key, const char *fmt, ...) { va_list ap; char *s; va_start(ap, fmt); Vasprintf(&s, fmt, ap); va_end(ap); AG_TextInfoS(key, s); free(s); } void AG_TextInfoS(const char *key, const char *s) { char disableSw[64]; AG_Variable *Vdisable; AG_Window *win; AG_Box *vb; AG_Checkbox *cb; if (key != NULL) { Strlcpy(disableSw, "info.", sizeof(disableSw)); Strlcat(disableSw, key, sizeof(disableSw)); AG_ObjectLock(agConfig); if (AG_Defined(agConfig, disableSw) && AG_GetInt(agConfig, disableSw) == 1) goto out; } win = AG_WindowNew(AG_WINDOW_NORESIZE | AG_WINDOW_NOCLOSE | AG_WINDOW_NOMINIMIZE | AG_WINDOW_NOMAXIMIZE); if (win == NULL) return; win->wmType = AG_WINDOW_WM_DIALOG; AG_WindowSetCaptionS(win, _(agTextMsgTitles[AG_MSG_INFO])); AG_WindowSetPosition(win, AG_WINDOW_CENTER, 1); vb = AG_BoxNewVert(win, 0); AG_SetFontSize(vb, "120%"); AG_LabelNewS(vb, 0, s); vb = AG_BoxNewVert(win, AG_BOX_HOMOGENOUS | AG_BOX_EXPAND); AG_WidgetFocus( AG_ButtonNewFn(vb, 0, _("Ok"), AGWINDETACH(win)) ); if (key != NULL) { cb = AG_CheckboxNewS(win, AG_CHECKBOX_HFILL, _("Don't tell me again")); Vdisable = AG_SetInt(agConfig, disableSw, 0); AG_BindInt(cb, "state", &Vdisable->data.i); } AG_WindowShow(win); out: if (key != NULL) AG_ObjectUnlock(agConfig); } /* * Display a warning message with a "Don't tell me again" option. * The user preference is preserved in a persistent table. */ void AG_TextWarning(const char *key, const char *fmt, ...) { va_list ap; char *s; va_start(ap, fmt); Vasprintf(&s, fmt, ap); va_end(ap); AG_TextWarningS(key, s); free(s); } void AG_TextWarningS(const char *key, const char *s) { char disableSw[64]; AG_Variable *Vdisable; AG_Window *win; AG_Box *vb; AG_Checkbox *cb; if (key != NULL) { Strlcpy(disableSw, "warn.", sizeof(disableSw)); Strlcat(disableSw, key, sizeof(disableSw)); AG_ObjectLock(agConfig); if (AG_Defined(agConfig, disableSw) && AG_GetInt(agConfig, disableSw) == 1) goto out; } win = AG_WindowNew(AG_WINDOW_NORESIZE | AG_WINDOW_NOCLOSE | AG_WINDOW_NOMINIMIZE | AG_WINDOW_NOMAXIMIZE); if (win == NULL) return; win->wmType = AG_WINDOW_WM_DIALOG; AG_WindowSetCaptionS(win, _(agTextMsgTitles[AG_MSG_WARNING])); AG_WindowSetPosition(win, AG_WINDOW_CENTER, 1); vb = AG_BoxNewVert(win, 0); AG_SetFontSize(vb, "120%"); AG_LabelNewS(vb, 0, s); vb = AG_BoxNewVert(win, AG_BOX_HOMOGENOUS | AG_BOX_EXPAND); AG_WidgetFocus( AG_ButtonNewFn(vb, 0, _("Ok"), AGWINDETACH(win)) ); if (key != NULL) { cb = AG_CheckboxNewS(win, AG_CHECKBOX_HFILL, _("Don't tell me again")); Vdisable = AG_SetInt(agConfig, disableSw, 0); AG_BindInt(cb, "state", &Vdisable->data.i); } AG_WindowShow(win); out: if (key != NULL) AG_ObjectUnlock(agConfig); } /* Display an error message. */ void AG_TextError(const char *fmt, ...) { va_list ap; char *s; va_start(ap, fmt); Vasprintf(&s, fmt, ap); va_end(ap); AG_TextErrorS(s); free(s); } void AG_TextErrorS(const char *s) { AG_Window *win; AG_Box *vb; win = AG_WindowNew(AG_WINDOW_NORESIZE | AG_WINDOW_NOCLOSE | AG_WINDOW_NOMINIMIZE | AG_WINDOW_NOMAXIMIZE); if (win == NULL) return; win->wmType = AG_WINDOW_WM_DIALOG; AG_WindowSetCaptionS(win, _(agTextMsgTitles[AG_MSG_ERROR])); AG_WindowSetPosition(win, AG_WINDOW_CENTER, 1); vb = AG_BoxNewVert(win, 0); AG_SetFontSize(vb, "120%"); AG_LabelNewS(vb, 0, s); vb = AG_BoxNewVert(win, AG_BOX_HOMOGENOUS | AG_BOX_EXPAND); AG_WidgetFocus( AG_ButtonNewFn(vb, 0, _("Ok"), AGWINDETACH(win)) ); AG_WindowShow(win); } #endif /* AG_WIDGETS */ /* Align a text surface inside a given space. */ void AG_TextAlign(int *x, int *y, int wArea, int hArea, int wText, int hText, int lPad, int rPad, int tPad, int bPad, enum ag_text_justify justify, enum ag_text_valign valign) { switch (justify) { case AG_TEXT_LEFT: *x = lPad; break; case AG_TEXT_CENTER: *x = ((wArea + lPad + rPad) >> 1) - (wText >> 1); break; case AG_TEXT_RIGHT: *x = wArea - rPad - wText; break; } switch (valign) { case AG_TEXT_TOP: *y = tPad; break; case AG_TEXT_MIDDLE: *y = ((hArea + tPad + bPad) >> 1) - (hText >> 1); break; case AG_TEXT_BOTTOM: *y = hArea - bPad - wText; break; } } /* * Render text onto a newly-allocated surface with typographical spacings. * Text may contain UTF-8 and ANSI SGR sequences. */ AG_Surface * AG_TextRenderF(const char *fmt, ...) { AG_Surface *S; char *text; va_list args; va_start(args, fmt); Vasprintf(&text, fmt, args); va_end(args); S = AG_TextRender(text); free(text); return (S); } /* * Render text onto a newly-allocated surface with typographical spacings. * Text may contain UTF-8 and ANSI SGR sequences. */ AG_Surface * AG_TextRender(const char *text) { const AG_TextState *ts = AG_TEXT_STATE_CUR(); AG_Surface *S; #ifdef AG_UNICODE AG_Char *us; if ((us = AG_ImportUnicode("UTF-8", text, NULL, NULL)) == NULL) { AG_FatalError(NULL); } S = AG_TextRenderInternal(us, ts->font, &ts->colorBG, &ts->color); free(us); #else S = AG_TextRenderInternal((const Uint8 *)text, ts->font, &ts->colorBG, &ts->color); #endif return (S); } /* * Render text onto a newly-allocated surface with typographical spacings. * Text may contain UTF-8 and ANSI SGR sequences. * Text is rendered Right-to-Left. */ AG_Surface * AG_TextRenderRTL(const char *text) { const AG_TextState *ts = AG_TEXT_STATE_CUR(); AG_Surface *S; #ifdef AG_UNICODE AG_Char *us, *usReversed, *c, *cReversed; AG_Size len; if ((us = AG_ImportUnicode("UTF-8", text, NULL, NULL)) == NULL) { AG_FatalError(NULL); } len = AG_LengthUCS4(us); usReversed = Malloc((len + 1) * sizeof(Uint32)); for (c = &us[0], cReversed = &usReversed[len-1]; *c != '\0'; c++, --cReversed) { *cReversed = *c; } usReversed[len] = '\0'; S = AG_TextRenderInternal(usReversed, ts->font, &ts->colorBG, &ts->color); free(usReversed); free(us); #else S = AG_TextRenderInternal((const Uint8 *)text, ts->font, &ts->colorBG, &ts->color); #endif /* !AG_UNICODE */ return (S); } /* * Render text onto a newly-allocated surface and fast-crop the surface * to its rendered contents (removing all typographical spacings). * * As opposed to the standard AG_TextRender() which returns a padded surface * suitable for typographical alignment, AG_TextRenderCropped() returns the * smallest possible surface that will contain the rendered glyph(s). * * Text may contain UTF-8 and ANSI SGR sequences. */ AG_Surface * AG_TextRenderCropped(const char *text) { const AG_TextState *ts = AG_TEXT_STATE_CUR(); AG_Surface *S; #ifdef AG_UNICODE AG_Char *us; #endif const Uint8 *p; const int threshold = 50; int x, y, xStart, yStart, xEnd, yEnd, xRight, yBottom; #ifdef AG_UNICODE if ((us = AG_ImportUnicode("UTF-8", text, NULL, NULL)) == NULL) { AG_FatalError(NULL); } S = AG_TextRenderInternal(us, ts->font, &ts->colorBG, &ts->color); free(us); #else S = AG_TextRenderInternal((const Uint8 *)text, ts->font, &ts->colorBG, &ts->color); #endif /* * The rendered surface is guaranteed to be Packed 32-bit RGBA * with standard (agSurfaceFmt) masks. */ /* Scan top to bottom */ p = S->pixels; for (y = yStart = 0; y < S->h; y++) { for (x = 0; x < S->w; x++) { const AG_Pixel px = AG_SurfaceGet_At(S,p); #if AG_BYTEORDER == AG_BIG_ENDIAN const Uint8 a = (px & 0x000000ff); #else const Uint8 a = (px & 0xff000000) >> 24; #endif if (a > threshold) { break; } p += 4; } if (x == S->w) { yStart++; } else { break; } p += S->padding; } /* Scan bottom to top */ yBottom = (S->h - 1); for (y = yEnd = yBottom; y >= 0; y--) { p = S->pixels + (y * S->pitch); for (x = 0; x < S->w; x++) { const AG_Pixel px = AG_SurfaceGet_At(S,p); #if AG_BYTEORDER == AG_BIG_ENDIAN const Uint8 a = (px & 0x000000ff); #else const Uint8 a = (px & 0xff000000) >> 24; #endif if (a > threshold) { break; } p += 4; } if (x == S->w) { yEnd--; } else { break; } } if (yStart > 0) { /* Crop top and possibly bottom */ S->h -= yStart + (yBottom - yEnd); S->clipRect.h = S->h; S->pixels += (yStart * S->pitch); } else if (yEnd < yBottom) { /* Crop bottom only */ S->h = yEnd + 1; S->clipRect.h = S->h; } /* Scan left to right */ for (x = xStart = 0; x < S->w; x++) { p = S->pixels + (x << 2); for (y = 0; y < S->h; y++) { const AG_Pixel px = AG_SurfaceGet_At(S,p); #if AG_BYTEORDER == AG_BIG_ENDIAN const Uint8 a = (px & 0x000000ff); #else const Uint8 a = (px & 0xff000000) >> 24; #endif if (a > threshold) { break; } p += S->pitch; } if (y == S->h) { xStart++; } else { break; } } /* Scan right to left */ xRight = (S->w - 1); for (x = xRight, xEnd = x; x >= 0; x--) { p = S->pixels + (x << 2); for (y = 0; y < S->h; y++) { const AG_Pixel px = AG_SurfaceGet_At(S,p); #if AG_BYTEORDER == AG_BIG_ENDIAN const Uint8 a = (px & 0x000000ff); #else const Uint8 a = (px & 0xff000000) >> 24; #endif if (a > threshold) { break; } p += S->pitch; } if (y == S->h) { xEnd--; } else { break; } } if (xStart > 0) { /* Crop left and possibly right */ const int Rcrop = (xRight - xEnd); S->w -= (xStart + Rcrop); S->clipRect.w = S->w; S->Lpadding = (xStart << 2); S->padding = (Rcrop << 2); } else { if (xEnd < xRight) { /* Crop right only */ S->w = xEnd + 1; S->clipRect.w = S->w; S->padding += (xRight - xEnd) << 2; } } return (S); } /* * Render text in internal (UCS-4) format onto a newly-allocated surface * with typographical spacings. */ AG_Surface * AG_TextRenderInternal(const AG_Char *text, AG_Font *font, const AG_Color *cBg, const AG_Color *cFg) { AG_TextMetrics Tm; AG_Surface *S; InitMetrics(&Tm); AGFONT_OPS(font)->size(font, text, &Tm, 1); S = AG_SurfaceNew(agSurfaceFmt, Tm.w, Tm.h, AG_SURFACE_GL_TEXTURE); AG_FillRect(S, NULL, cBg); if (cBg->a == AG_OPAQUE) S->format.Amask = 0; /* No blending needed */ AGFONT_OPS(font)->render(text, S, &Tm, font, cBg,cFg); FreeMetrics(&Tm); return (S); } /* * Interpret a possible ANSI escape sequence within a native string and write * a normalized description into *ansi. Return 0 on success, -1 on failure. */ int AG_TextParseANSI(const AG_TextState *ts, AG_TextANSI *_Nonnull ansi, const AG_Char *_Nonnull s) { const AG_Char *c = &s[0]; int len; switch (*c) { case '[': /* CSI */ break; case 'N': /* SS2 */ case 'O': /* SS3 */ case 'P': /* DCS */ ansi->ctrl = AG_ANSI_SS2 + (*c - (AG_Char)'N'); ansi->len = 1; return (0); case '\\': /* ST */ case ']': /* OSC */ case '^': /* PM */ case '_': /* APC */ ansi->ctrl = AG_ANSI_ST + (*c - (AG_Char)'\\'); ansi->len = 1; return (0); case 'X': /* SOS */ ansi->ctrl = AG_ANSI_SOS; ansi->len = 1; return (0); case 'c': /* RIS */ ansi->ctrl = AG_ANSI_RIS; ansi->len = 1; return (0); default: goto fail; } for (c = &s[1]; (len = (int)(c-s)) <= AG_TEXT_ANSI_SEQ_MAX; c++) { char buf[AG_TEXT_ANSI_PARAM_MAX], *pBuf; const AG_Char *pSrc; char *tok; int sgr, i; switch (*c) { case 'm': /* SGR */ ansi->ctrl = AG_ANSI_CSI_SGR; if (len >= sizeof(buf)) { goto fail; } else if (len == 0) { break; } for (i=0, pSrc=&s[1], pBuf=buf; i < len; i++) { *pBuf = (char)(*pSrc); /* AG_Char -> ASCII */ pBuf++; pSrc++; } pBuf[-1] = '\0'; #ifdef DEBUG_ANSI Debug(NULL, "SGR (%d bytes): \"%s\"\n", len, buf); #endif pBuf = buf; if ((tok = Strsep(&pBuf, ":;")) == NULL) { break; } if ((sgr = atoi(tok)) >= AG_SGR_LAST || sgr < 0) { goto fail; } ansi->sgr = (enum ag_text_sgr_parameter) sgr; #ifdef DEBUG_ANSI Debug(NULL, "SGR parameter %d\n", ansi->sgr); #endif if (sgr == 0) { break; } /* * 3/4-bit color. */ if (sgr >= 30 && sgr <= 37) { /* Normal FG */ ansi->sgr = AG_SGR_FG; memcpy(&ansi->color, (ts->colorANSI) ? &ts->colorANSI[sgr-30] : &agTextColorANSI[sgr-30], sizeof(AG_Color)); break; } else if (sgr >= 40 && sgr <= 47) { /* Normal BG */ ansi->sgr = AG_SGR_BG; memcpy(&ansi->color, (ts->colorANSI) ? &ts->colorANSI[sgr-40] : &agTextColorANSI[sgr-40], sizeof(AG_Color)); break; } else if (sgr >= 90 && sgr <= 97) { /* Bright FG */ ansi->sgr = AG_SGR_FG; memcpy(&ansi->color, (ts->colorANSI) ? &ts->colorANSI[8+sgr-90] : &agTextColorANSI[8+sgr-90], sizeof(AG_Color)); break; } else if (sgr >= 100 && sgr <= 107) { /* Bright BG */ ansi->sgr = AG_SGR_BG; memcpy(&ansi->color, (ts->colorANSI) ? &ts->colorANSI[8+sgr-100] : &agTextColorANSI[8+sgr-100], sizeof(AG_Color)); break; } switch (ansi->sgr) { case AG_SGR_FG: case AG_SGR_BG: if (!(tok = Strsep(&pBuf, ":;")) || tok[0] == '\0' || tok[1] != '\0') { goto fail_param; } switch (tok[0]) { case '2': /* 24-bit RGB */ ansi->color.r = (tok = Strsep(&pBuf, ":;")) ? AG_8toH(atoi(tok)) : 0; ansi->color.g = (tok = Strsep(&pBuf, ":;")) ? AG_8toH(atoi(tok)) : 0; ansi->color.b = (tok = Strsep(&pBuf, ":;")) ? AG_8toH(atoi(tok)) : 0; ansi->color.a = AG_OPAQUE; break; case '5': /* 8-bit indexed */ if (!(tok = Strsep(&pBuf, ":;")) || tok[0] == '\0') { goto fail_param; } if ((i = atoi(tok)) >= 0 && i <= 15) { memcpy(&ansi->color, (ts->colorANSI) ? &ts->colorANSI[i] : &agTextColorANSI[i], sizeof(AG_Color)); } break; } break; case AG_SGR_NO_FG: if ((tok = Strsep(&pBuf, ":;")) && tok[0] == '4' && tok[1] == '9' && tok[2] == '\0') { ansi->sgr = AG_SGR_NO_FG_NO_BG; } break; case AG_SGR_NO_BG: if ((tok = Strsep(&pBuf, ":;")) && tok[0] == '3' && tok[1] == '9' && tok[2] == '\0') { ansi->sgr = AG_SGR_NO_FG_NO_BG; } break; default: break; } break; case 'A': /* CUU */ case 'B': /* CUD */ case 'C': /* CUF */ case 'D': /* CUB */ case 'E': /* CNL */ case 'F': /* CPL */ case 'G': /* CHA */ ansi->ctrl = AG_ANSI_CSI_CUU + (*c - (AG_Char)'A'); break; case 'H': /* CUP */ case 'f': ansi->ctrl = AG_ANSI_CSI_CUP; break; case 'J': /* ED */ case 'K': /* EL */ ansi->ctrl = AG_ANSI_CSI_ED + (*c - (AG_Char)'J'); break; case 'S': /* SU */ case 'T': /* SD */ ansi->ctrl = AG_ANSI_CSI_SU + (*c - (AG_Char)'S'); break; case 'h': /* Private */ case 'i': case 'l': ansi->ctrl = AG_ANSI_PRIVATE; break; case 'n': ansi->ctrl = AG_ANSI_CSI_DSR; break; case 's': ansi->ctrl = AG_ANSI_CSI_SCP; break; case 'u': ansi->ctrl = AG_ANSI_CSI_RCP; break; case '\0': goto fail_param; default: if ((*c >= 0x70 && *c < 0x7e) || /* Private final byte */ strchr("<=>?", *c)) { /* Private parameter */ ansi->ctrl = AG_ANSI_PRIVATE; break; } if ((*c >= 0x30 && *c <= 0x3f) || /* Parameter byte */ (*c >= 0x20 && *c <= 0x2f)) { /* Intermediate byte */ continue; } goto fail_param; } break; } ansi->len = len+1; return (0); fail: AG_SetError("Bad ANSI code (before `%c')", *c); return (-1); fail_param: AG_SetError("Bad ANSI parameter (before `%c')", *c); return (-1); } /* * Convert a string in internal (UCS-4) format to the specified encoding and * write the result to a fixed-size buffer, removing any ANSI SGR sequences. * * At most dstSize-1 bytes will be copied. * The string is always NUL-terminated. */ int AG_TextExportUnicode_StripANSI(const char *encoding, char *dst, const AG_Char *ucs, AG_Size dstSize) { AG_TextANSI ansi; AG_Size len; if (strcmp(encoding, "UTF-8") == 0) { for (len = 0; *ucs != '\0' && len < dstSize; ucs++) { AG_Char uch = *ucs; int chlen, ch1, i; if (ucs[0] == 0x1b && ucs[1] >= 0x40 && ucs[1] <= 0x5f && ucs[2] != '\0') { if (AG_TextParseANSI(AG_TEXT_STATE_CUR(), &ansi, &ucs[1]) == 0) { ucs += ansi.len; continue; } } if (uch < 0x80) { chlen = 1; ch1 = 0; } else if (uch < 0x800) { chlen = 2; ch1 = 0xc0; } else if (uch < 0x10000) { chlen = 3; ch1 = 0xe0; } else if (uch < 0x200000) { chlen = 4; ch1 = 0xf0; } else if (uch < 0x4000000) { chlen = 5; ch1 = 0xf8; } else if (uch <= 0x7fffffff) { chlen = 6; ch1 = 0xfc; } else { AG_SetErrorS("Bad UTF-8 sequence"); return (-1); } if (len+chlen+1 > dstSize) { AG_SetErrorS("Out of space"); return (-1); } for (i = chlen - 1; i > 0; i--) { dst[i] = (uch & 0x3f) | 0x80; uch >>= 6; } dst[0] = uch | ch1; dst += chlen; len += chlen; } *dst = '\0'; return (0); } else if (strcmp(encoding, "US-ASCII") == 0) { for (len = 0; *ucs != '\0' && len < dstSize; ucs++) { if (ucs[0] == 0x1b && ucs[1] >= 0x40 && ucs[1] <= 0x5f && ucs[2] != '\0') { if (AG_TextParseANSI(AG_TEXT_STATE_CUR(), &ansi, &ucs[1]) == 0) { ucs += ansi.len; continue; } } if ((*ucs) & ~0x7f) { AG_SetErrorS("Non-ASCII character"); return (-1); } *dst = (char)*ucs; dst++; len++; } *dst = '\0'; return (0); } else { # ifdef HAVE_ICONV return ExportUnicodeICONV(encoding, dst, ucs, dstSize); # else AG_SetErrorS("No such encoding"); return (-1); # endif } } static AG_Glyph *_Nonnull TextRenderGlyph_Miss(AG_Driver *_Nonnull drv, AG_Font *_Nonnull font, const AG_Color *_Nonnull cBg, const AG_Color *_Nonnull cFg, AG_Char ch) { AG_Glyph *G; AG_Char s[2]; G = Malloc(sizeof(AG_Glyph)); G->font = (AG_Font *)font; G->colorBG = *cBg; G->color = *cFg; G->ch = ch; s[0] = ch; s[1] = '\0'; G->su = AG_TextRenderInternal(s, font, cBg,cFg); /* Render glyph */ AGFONT_OPS(font)->get_glyph_metrics(font, G); /* Get the advance */ AGDRIVER_CLASS(drv)->updateGlyph(drv, G); /* Prepare GPU transfer */ return (G); } /* * Render character ch using the given font and BG/FG colors. Cache the * result in the per-driver glyph cache. The glyph cache is stored in * AG_Driver(3) instances because glyph renderings may be associated with * driver-specific hardware textures. * * Must be called from GUI rendering context. */ AG_Glyph * AG_TextRenderGlyph(AG_Driver *drv, AG_Font *font, const AG_Color *cBg, const AG_Color *cFg, AG_Char ch) { AG_Glyph *G; const Uint h = (Uint)(ch % AG_GLYPH_NBUCKETS); SLIST_FOREACH(G, &drv->glyphCache[h].glyphs, glyphs) { if (ch == G->ch && font == G->font && AG_ColorCompare(cBg, &G->colorBG) == 0 && AG_ColorCompare(cFg, &G->color) == 0) break; } if (G == NULL) { G = TextRenderGlyph_Miss(drv, font, cBg, cFg, ch); SLIST_INSERT_HEAD(&drv->glyphCache[h].glyphs, G, glyphs); } return (G); } /* * Set foreground text color from the given AG_Color(3). * Applies to the current text rendering state. */ void AG_TextColor(const AG_Color *c) { memcpy(&AG_TEXT_STATE_CUR()->color, c, sizeof(AG_Color)); } /* * Set foreground text color from the given 8-bit RGB triplet. * Applies to the current text rendering state. */ void AG_TextColorRGB(Uint8 r, Uint8 g, Uint8 b) { AG_ColorRGB_8(&AG_TEXT_STATE_CUR()->color, r,g,b); } /* * Set foreground text color from the given 8-bit RGBA quadruplet. * Applies to the current text rendering state. */ void AG_TextColorRGBA(Uint8 r, Uint8 g, Uint8 b, Uint8 a) { AG_ColorRGBA_8(&AG_TEXT_STATE_CUR()->color, r,g,b,a); } /* * Set foreground text color from a 32-bit hex value (0xRRGGBBAA). * Applies to the current text rendering state. */ void AG_TextColorHex(Uint32 c) { AG_ColorHex32(&AG_TEXT_STATE_CUR()->color, c); } /* * Set background text color from the given AG_Color(3). * Applies to the current text rendering state. */ void AG_TextBGColor(const AG_Color *c) { memcpy(&AG_TEXT_STATE_CUR()->colorBG, c, sizeof(AG_Color)); } /* * Set background text color from the given 8-bit RGB triplet. * Applies to the current text rendering state. */ void AG_TextBGColorRGB(Uint8 r, Uint8 g, Uint8 b) { AG_ColorRGB_8(&AG_TEXT_STATE_CUR()->colorBG, r,g,b); } /* * Set background text color from the given 8-bit RGBA quadruplet. * Applies to the current text rendering state. */ void AG_TextBGColorRGBA(Uint8 r, Uint8 g, Uint8 b, Uint8 a) { AG_ColorRGBA_8(&AG_TEXT_STATE_CUR()->colorBG, r,g,b,a); } /* * Set background text color from a 32-bit hex value (0xRRGGBBAA). * Applies to the current text rendering state. */ void AG_TextBGColorHex(Uint32 c) { AG_ColorHex(&AG_TEXT_STATE_CUR()->colorBG, c); } /* * Select an existing (loaded) font for rendering text. * Applies to the current text rendering state. */ void AG_TextFont(AG_Font *font) { AG_TEXT_STATE_CUR()->font = font; } /* * Select the horizontal justification mode to use when rendering * multi-line text (or rendering text to an existing surface). * Applies to the current text rendering state. */ void AG_TextJustify(enum ag_text_justify mode) { AG_TEXT_STATE_CUR()->justify = mode; } /* * Select the vertical alignment mode to use when rendering text to * an existing surface. Applies to the current text rendering state. */ void AG_TextValign(enum ag_text_valign mode) { AG_TEXT_STATE_CUR()->valign = mode; } /* * Set the width of rendered tabs ("\t") in pixels. * Applies to the current text rendering state. */ void AG_TextTabWidth(int px) { AG_TEXT_STATE_CUR()->tabWd = px; } static void SetDefaultFontAll(AG_Widget *wid, AG_Font *defaultFontPrev, AG_Font *defaultFontNew) { AG_Widget *chld; if (wid->font == defaultFontPrev) { wid->font = defaultFontNew; } AG_PostEvent(wid, "font-changed", NULL); AG_Redraw(wid); AGOBJECT_FOREACH_CHILD(chld, wid, ag_widget) SetDefaultFontAll(chld, defaultFontPrev, defaultFontNew); } /* * Set the application-global default font to the specified font. * Return a pointer to the previous default font. * Updates the default font settings in AG_Config(3). * Updates agDefaultFont and agDefaultFont{Height,Ascent,LineSkip}. */ AG_Font * AG_SetDefaultFont(AG_Font *font) { AG_Font *prevDefaultFont; AG_Driver *drv; AG_Window *win; int i; AG_MutexLock(&agTextLock); prevDefaultFont = agDefaultFont; if (font != NULL) { agDefaultFont = font; } else { agDefaultFont = font = AG_FetchFont(NULL, 0.0f, 0); } agTextFontHeight = font->height; agTextFontAscent = font->ascent; agTextFontLineSkip = font->lineskip; /* Update the rendering state. */ for (i = 0; i <= agTextStateCur; i++) { AG_TextState *ts = &agTextStateStack[i]; if (ts->font == prevDefaultFont) ts->font = font; } /* Update the AG_Config(3) font settings. */ Strlcpy(agConfig->fontFace, OBJECT(font)->name, sizeof(agConfig->fontFace)); agConfig->fontSize = font->spec.size; agConfig->fontFlags = font->flags; AG_MutexUnlock(&agTextLock); /* Update all font references in the style engine. */ AGOBJECT_FOREACH_CHILD(drv, &agDrivers, ag_driver) { AG_FOREACH_WINDOW(win, drv) { SetDefaultFontAll(WIDGET(win), prevDefaultFont, font); AG_WindowUpdate(win); } } return (prevDefaultFont); } /* * Set the default font from a string ",,