diff options
Diffstat (limited to 'src/libui_sdl/libui/windows/drawtext.cpp')
-rw-r--r-- | src/libui_sdl/libui/windows/drawtext.cpp | 531 |
1 files changed, 531 insertions, 0 deletions
diff --git a/src/libui_sdl/libui/windows/drawtext.cpp b/src/libui_sdl/libui/windows/drawtext.cpp new file mode 100644 index 0000000..05a24f6 --- /dev/null +++ b/src/libui_sdl/libui/windows/drawtext.cpp @@ -0,0 +1,531 @@ +// 22 december 2015 +#include "uipriv_windows.hpp" +#include "draw.hpp" +// TODO really migrate + +// notes: +// only available in windows 8 and newer: +// - character spacing +// - kerning control +// - justficiation (how could I possibly be making this up?!) +// - vertical text (SERIOUSLY?! WHAT THE ACTUAL FUCK, MICROSOFT?!?!?!? DID YOU NOT THINK ABOUT THIS THE FIRST TIME, TRYING TO IMPROVE THE INTERNATIONALIZATION OF WINDOWS 7?!?!?! bonus: some parts of MSDN even say 8.1 and up only!) + +struct uiDrawFontFamilies { + fontCollection *fc; +}; + +uiDrawFontFamilies *uiDrawListFontFamilies(void) +{ + struct uiDrawFontFamilies *ff; + + ff = uiNew(struct uiDrawFontFamilies); + ff->fc = loadFontCollection(); + return ff; +} + +int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff) +{ + return ff->fc->fonts->GetFontFamilyCount(); +} + +char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n) +{ + IDWriteFontFamily *family; + WCHAR *wname; + char *name; + HRESULT hr; + + hr = ff->fc->fonts->GetFontFamily(n, &family); + if (hr != S_OK) + logHRESULT(L"error getting font out of collection", hr); + wname = fontCollectionFamilyName(ff->fc, family); + name = toUTF8(wname); + uiFree(wname); + family->Release(); + return name; +} + +void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff) +{ + fontCollectionFree(ff->fc); + uiFree(ff); +} + +struct uiDrawTextFont { + IDWriteFont *f; + WCHAR *family; // save for convenience in uiDrawNewTextLayout() + double size; +}; + +uiDrawTextFont *mkTextFont(IDWriteFont *df, BOOL addRef, WCHAR *family, BOOL copyFamily, double size) +{ + uiDrawTextFont *font; + WCHAR *copy; + HRESULT hr; + + font = uiNew(uiDrawTextFont); + font->f = df; + if (addRef) + font->f->AddRef(); + if (copyFamily) { + copy = (WCHAR *) uiAlloc((wcslen(family) + 1) * sizeof (WCHAR), "WCHAR[]"); + wcscpy(copy, family); + font->family = copy; + } else + font->family = family; + font->size = size; + return font; +} + +// TODO consider moving these all to dwrite.cpp + +// TODO MinGW-w64 is missing this one +#define DWRITE_FONT_WEIGHT_SEMI_LIGHT (DWRITE_FONT_WEIGHT(350)) +static const struct { + bool lastOne; + uiDrawTextWeight uival; + DWRITE_FONT_WEIGHT dwval; +} dwriteWeights[] = { + { false, uiDrawTextWeightThin, DWRITE_FONT_WEIGHT_THIN }, + { false, uiDrawTextWeightUltraLight, DWRITE_FONT_WEIGHT_ULTRA_LIGHT }, + { false, uiDrawTextWeightLight, DWRITE_FONT_WEIGHT_LIGHT }, + { false, uiDrawTextWeightBook, DWRITE_FONT_WEIGHT_SEMI_LIGHT }, + { false, uiDrawTextWeightNormal, DWRITE_FONT_WEIGHT_NORMAL }, + { false, uiDrawTextWeightMedium, DWRITE_FONT_WEIGHT_MEDIUM }, + { false, uiDrawTextWeightSemiBold, DWRITE_FONT_WEIGHT_SEMI_BOLD }, + { false, uiDrawTextWeightBold, DWRITE_FONT_WEIGHT_BOLD }, + { false, uiDrawTextWeightUltraBold, DWRITE_FONT_WEIGHT_ULTRA_BOLD }, + { false, uiDrawTextWeightHeavy, DWRITE_FONT_WEIGHT_HEAVY }, + { true, uiDrawTextWeightUltraHeavy, DWRITE_FONT_WEIGHT_ULTRA_BLACK, }, +}; + +static const struct { + bool lastOne; + uiDrawTextItalic uival; + DWRITE_FONT_STYLE dwval; +} dwriteItalics[] = { + { false, uiDrawTextItalicNormal, DWRITE_FONT_STYLE_NORMAL }, + { false, uiDrawTextItalicOblique, DWRITE_FONT_STYLE_OBLIQUE }, + { true, uiDrawTextItalicItalic, DWRITE_FONT_STYLE_ITALIC }, +}; + +static const struct { + bool lastOne; + uiDrawTextStretch uival; + DWRITE_FONT_STRETCH dwval; +} dwriteStretches[] = { + { false, uiDrawTextStretchUltraCondensed, DWRITE_FONT_STRETCH_ULTRA_CONDENSED }, + { false, uiDrawTextStretchExtraCondensed, DWRITE_FONT_STRETCH_EXTRA_CONDENSED }, + { false, uiDrawTextStretchCondensed, DWRITE_FONT_STRETCH_CONDENSED }, + { false, uiDrawTextStretchSemiCondensed, DWRITE_FONT_STRETCH_SEMI_CONDENSED }, + { false, uiDrawTextStretchNormal, DWRITE_FONT_STRETCH_NORMAL }, + { false, uiDrawTextStretchSemiExpanded, DWRITE_FONT_STRETCH_SEMI_EXPANDED }, + { false, uiDrawTextStretchExpanded, DWRITE_FONT_STRETCH_EXPANDED }, + { false, uiDrawTextStretchExtraExpanded, DWRITE_FONT_STRETCH_EXTRA_EXPANDED }, + { true, uiDrawTextStretchUltraExpanded, DWRITE_FONT_STRETCH_ULTRA_EXPANDED }, +}; + +void attrToDWriteAttr(struct dwriteAttr *attr) +{ + bool found; + int i; + + found = false; + for (i = 0; ; i++) { + if (dwriteWeights[i].uival == attr->weight) { + attr->dweight = dwriteWeights[i].dwval; + found = true; + break; + } + if (dwriteWeights[i].lastOne) + break; + } + if (!found) + userbug("Invalid text weight %d passed to text function.", attr->weight); + + found = false; + for (i = 0; ; i++) { + if (dwriteItalics[i].uival == attr->italic) { + attr->ditalic = dwriteItalics[i].dwval; + found = true; + break; + } + if (dwriteItalics[i].lastOne) + break; + } + if (!found) + userbug("Invalid text italic %d passed to text function.", attr->italic); + + found = false; + for (i = 0; ; i++) { + if (dwriteStretches[i].uival == attr->stretch) { + attr->dstretch = dwriteStretches[i].dwval; + found = true; + break; + } + if (dwriteStretches[i].lastOne) + break; + } + if (!found) + // TODO on other platforms too + userbug("Invalid text stretch %d passed to text function.", attr->stretch); +} + +void dwriteAttrToAttr(struct dwriteAttr *attr) +{ + int weight, against, n; + int curdiff, curindex; + bool found; + int i; + + // weight is scaled; we need to test to see what's nearest + weight = (int) (attr->dweight); + against = (int) (dwriteWeights[0].dwval); + curdiff = abs(against - weight); + curindex = 0; + for (i = 1; ; i++) { + against = (int) (dwriteWeights[i].dwval); + n = abs(against - weight); + if (n < curdiff) { + curdiff = n; + curindex = i; + } + if (dwriteWeights[i].lastOne) + break; + } + attr->weight = dwriteWeights[i].uival; + + // italic and stretch are simple values; we can just do a matching search + found = false; + for (i = 0; ; i++) { + if (dwriteItalics[i].dwval == attr->ditalic) { + attr->italic = dwriteItalics[i].uival; + found = true; + break; + } + if (dwriteItalics[i].lastOne) + break; + } + if (!found) + // these are implbug()s because users shouldn't be able to get here directly; TODO? + implbug("invalid italic %d passed to dwriteAttrToAttr()", attr->ditalic); + + found = false; + for (i = 0; ; i++) { + if (dwriteStretches[i].dwval == attr->dstretch) { + attr->stretch = dwriteStretches[i].uival; + found = true; + break; + } + if (dwriteStretches[i].lastOne) + break; + } + if (!found) + implbug("invalid stretch %d passed to dwriteAttrToAttr()", attr->dstretch); +} + +uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc) +{ + uiDrawTextFont *font; + IDWriteFontCollection *collection; + UINT32 index; + BOOL exists; + struct dwriteAttr attr; + IDWriteFontFamily *family; + WCHAR *wfamily; + IDWriteFont *match; + HRESULT hr; + + // always get the latest available font information + hr = dwfactory->GetSystemFontCollection(&collection, TRUE); + if (hr != S_OK) + logHRESULT(L"error getting system font collection", hr); + + wfamily = toUTF16(desc->Family); + hr = collection->FindFamilyName(wfamily, &index, &exists); + if (hr != S_OK) + logHRESULT(L"error finding font family", hr); + if (!exists) + implbug("LONGTERM family not found in uiDrawLoadClosestFont()", hr); + hr = collection->GetFontFamily(index, &family); + if (hr != S_OK) + logHRESULT(L"error loading font family", hr); + + attr.weight = desc->Weight; + attr.italic = desc->Italic; + attr.stretch = desc->Stretch; + attrToDWriteAttr(&attr); + hr = family->GetFirstMatchingFont( + attr.dweight, + attr.dstretch, + attr.ditalic, + &match); + if (hr != S_OK) + logHRESULT(L"error loading font", hr); + + font = mkTextFont(match, + FALSE, // we own the initial reference; no need to add another one + wfamily, FALSE, // will be freed with font + desc->Size); + + family->Release(); + collection->Release(); + + return font; +} + +void uiDrawFreeTextFont(uiDrawTextFont *font) +{ + font->f->Release(); + uiFree(font->family); + uiFree(font); +} + +uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font) +{ + return (uintptr_t) (font->f); +} + +void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc) +{ + // TODO + + desc->Size = font->size; + + // TODO +} + +// text sizes are 1/72 of an inch +// points in Direct2D are 1/96 of an inch (https://msdn.microsoft.com/en-us/library/windows/desktop/ff684173%28v=vs.85%29.aspx, https://msdn.microsoft.com/en-us/library/windows/desktop/hh447022%28v=vs.85%29.aspx) +// As for the actual conversion from design units, see: +// - http://cboard.cprogramming.com/windows-programming/136733-directwrite-font-height-issues.html +// - https://sourceforge.net/p/vstgui/mailman/message/32483143/ +// - http://xboxforums.create.msdn.com/forums/t/109445.aspx +// - https://msdn.microsoft.com/en-us/library/dd183564%28v=vs.85%29.aspx +// - http://www.fontbureau.com/blog/the-em/ +// TODO make points here about how DIPs in DirectWrite == DIPs in Direct2D; if not, figure out what they really are? for the width and layout functions later +static double scaleUnits(double what, double designUnitsPerEm, double size) +{ + return (what / designUnitsPerEm) * (size * (96.0 / 72.0)); +} + +void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics) +{ + DWRITE_FONT_METRICS dm; + + font->f->GetMetrics(&dm); + metrics->Ascent = scaleUnits(dm.ascent, dm.designUnitsPerEm, font->size); + metrics->Descent = scaleUnits(dm.descent, dm.designUnitsPerEm, font->size); + // TODO what happens if dm.xxx is negative? + // TODO remember what this was for + metrics->Leading = scaleUnits(dm.lineGap, dm.designUnitsPerEm, font->size); + metrics->UnderlinePos = scaleUnits(dm.underlinePosition, dm.designUnitsPerEm, font->size); + metrics->UnderlineThickness = scaleUnits(dm.underlineThickness, dm.designUnitsPerEm, font->size); +} + +// some attributes, such as foreground color, can't be applied until after we establish a Direct2D context :/ so we have to prepare all attributes in advance +// also since there's no way to clear the attributes from a layout en masse (apart from overwriting them all), we'll play it safe by creating a new layout each time +enum layoutAttrType { + layoutAttrColor, +}; + +struct layoutAttr { + enum layoutAttrType type; + int start; + int end; + double components[4]; +}; + +struct uiDrawTextLayout { + WCHAR *text; + size_t textlen; + size_t *graphemes; + double width; + IDWriteTextFormat *format; + std::vector<struct layoutAttr> *attrs; +}; + +uiDrawTextLayout *uiDrawNewTextLayout(const char *text, uiDrawTextFont *defaultFont, double width) +{ + uiDrawTextLayout *layout; + HRESULT hr; + + layout = uiNew(uiDrawTextLayout); + + hr = dwfactory->CreateTextFormat(defaultFont->family, + NULL, + defaultFont->f->GetWeight(), + defaultFont->f->GetStyle(), + defaultFont->f->GetStretch(), + // typographic points are 1/72 inch; this parameter is 1/96 inch + // fortunately Microsoft does this too, in https://msdn.microsoft.com/en-us/library/windows/desktop/dd371554%28v=vs.85%29.aspx + defaultFont->size * (96.0 / 72.0), + // see http://stackoverflow.com/questions/28397971/idwritefactorycreatetextformat-failing and https://msdn.microsoft.com/en-us/library/windows/desktop/dd368203.aspx + // TODO use the current locale again? + L"", + &(layout->format)); + if (hr != S_OK) + logHRESULT(L"error creating IDWriteTextFormat", hr); + + layout->text = toUTF16(text); + layout->textlen = wcslen(layout->text); + layout->graphemes = graphemes(layout->text); + + uiDrawTextLayoutSetWidth(layout, width); + + layout->attrs = new std::vector<struct layoutAttr>; + + return layout; +} + +void uiDrawFreeTextLayout(uiDrawTextLayout *layout) +{ + delete layout->attrs; + layout->format->Release(); + uiFree(layout->graphemes); + uiFree(layout->text); + uiFree(layout); +} + +static ID2D1SolidColorBrush *mkSolidBrush(ID2D1RenderTarget *rt, double r, double g, double b, double a) +{ + D2D1_BRUSH_PROPERTIES props; + D2D1_COLOR_F color; + ID2D1SolidColorBrush *brush; + HRESULT hr; + + ZeroMemory(&props, sizeof (D2D1_BRUSH_PROPERTIES)); + props.opacity = 1.0; + // identity matrix + props.transform._11 = 1; + props.transform._22 = 1; + color.r = r; + color.g = g; + color.b = b; + color.a = a; + hr = rt->CreateSolidColorBrush( + &color, + &props, + &brush); + if (hr != S_OK) + logHRESULT(L"error creating solid brush", hr); + return brush; +} + +IDWriteTextLayout *prepareLayout(uiDrawTextLayout *layout, ID2D1RenderTarget *rt) +{ + IDWriteTextLayout *dl; + DWRITE_TEXT_RANGE range; + IUnknown *unkBrush; + DWRITE_WORD_WRAPPING wrap; + FLOAT maxWidth; + HRESULT hr; + + hr = dwfactory->CreateTextLayout(layout->text, layout->textlen, + layout->format, + // FLOAT is float, not double, so this should work... TODO + FLT_MAX, FLT_MAX, + &dl); + if (hr != S_OK) + logHRESULT(L"error creating IDWriteTextLayout", hr); + + for (const struct layoutAttr &attr : *(layout->attrs)) { + range.startPosition = layout->graphemes[attr.start]; + range.length = layout->graphemes[attr.end] - layout->graphemes[attr.start]; + switch (attr.type) { + case layoutAttrColor: + if (rt == NULL) // determining extents, not drawing + break; + unkBrush = mkSolidBrush(rt, + attr.components[0], + attr.components[1], + attr.components[2], + attr.components[3]); + hr = dl->SetDrawingEffect(unkBrush, range); + unkBrush->Release(); // associated with dl + break; + default: + hr = E_FAIL; + logHRESULT(L"invalid text attribute type", hr); + } + if (hr != S_OK) + logHRESULT(L"error adding attribute to text layout", hr); + } + + // and set the width + // this is the only wrapping mode (apart from "no wrap") available prior to Windows 8.1 + wrap = DWRITE_WORD_WRAPPING_WRAP; + maxWidth = layout->width; + if (layout->width < 0) { + wrap = DWRITE_WORD_WRAPPING_NO_WRAP; + // setting the max width in this case technically isn't needed since the wrap mode will simply ignore the max width, but let's do it just to be safe + maxWidth = FLT_MAX; // see TODO above + } + hr = dl->SetWordWrapping(wrap); + if (hr != S_OK) + logHRESULT(L"error setting word wrapping mode", hr); + hr = dl->SetMaxWidth(maxWidth); + if (hr != S_OK) + logHRESULT(L"error setting max layout width", hr); + + return dl; +} + + +void uiDrawTextLayoutSetWidth(uiDrawTextLayout *layout, double width) +{ + layout->width = width; +} + +// TODO for a single line the height includes the leading; it should not +void uiDrawTextLayoutExtents(uiDrawTextLayout *layout, double *width, double *height) +{ + IDWriteTextLayout *dl; + DWRITE_TEXT_METRICS metrics; + HRESULT hr; + + dl = prepareLayout(layout, NULL); + hr = dl->GetMetrics(&metrics); + if (hr != S_OK) + logHRESULT(L"error getting layout metrics", hr); + *width = metrics.width; + // TODO make sure the behavior of this on empty strings is the same on all platforms + *height = metrics.height; + dl->Release(); +} + +void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout) +{ + IDWriteTextLayout *dl; + D2D1_POINT_2F pt; + ID2D1Brush *black; + HRESULT hr; + + // TODO document that fully opaque black is the default text color; figure out whether this is upheld in various scenarios on other platforms + black = mkSolidBrush(c->rt, 0.0, 0.0, 0.0, 1.0); + + dl = prepareLayout(layout, c->rt); + pt.x = x; + pt.y = y; + // TODO D2D1_DRAW_TEXT_OPTIONS_NO_SNAP? + // TODO D2D1_DRAW_TEXT_OPTIONS_CLIP? + // TODO when setting 8.1 as minimum, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT? + c->rt->DrawTextLayout(pt, dl, black, D2D1_DRAW_TEXT_OPTIONS_NONE); + dl->Release(); + + black->Release(); +} + +void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a) +{ + struct layoutAttr attr; + + attr.type = layoutAttrColor; + attr.start = startChar; + attr.end = endChar; + attr.components[0] = r; + attr.components[1] = g; + attr.components[2] = b; + attr.components[3] = a; + layout->attrs->push_back(attr); +} |