403 lines
12 KiB
C++
403 lines
12 KiB
C++
// 22 december 2015
|
|
// Before we begin, you may be wondering why this file is C++.
|
|
// Simple: <dwrite.h> is C++ only! Thanks Microsoft!
|
|
// And unlike UI Automation which accidentally just forgets the 'struct' and 'enum' tags in places, <dwrite.h> is a full C++ header file, with class definitions and the use of __uuidof. Oh well :/
|
|
#include "uipriv_windows.h"
|
|
|
|
// 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!)
|
|
|
|
static IDWriteFactory *dwfactory = NULL;
|
|
|
|
HRESULT initDrawText(void)
|
|
{
|
|
// TOOD use DWRITE_FACTORY_TYPE_ISOLATED instead?
|
|
return DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED,
|
|
__uuidof (IDWriteFactory),
|
|
(IUnknown **) (&dwfactory));
|
|
}
|
|
|
|
void uninitDrawText(void)
|
|
{
|
|
dwfactory->Release();
|
|
}
|
|
|
|
struct uiDrawFontFamilies {
|
|
IDWriteFontCollection *fonts;
|
|
WCHAR userLocale[LOCALE_NAME_MAX_LENGTH];
|
|
int userLocaleSuccess;
|
|
};
|
|
|
|
uiDrawFontFamilies *uiDrawListFontFamilies(void)
|
|
{
|
|
uiDrawFontFamilies *ff;
|
|
HRESULT hr;
|
|
|
|
ff = uiNew(uiDrawFontFamilies);
|
|
// always get the latest available font information
|
|
hr = dwfactory->GetSystemFontCollection(&(ff->fonts), TRUE);
|
|
if (hr != S_OK)
|
|
logHRESULT("error getting system font collection in uiDrawListFontFamilies()", hr);
|
|
ff->userLocaleSuccess = GetUserDefaultLocaleName(ff->userLocale, LOCALE_NAME_MAX_LENGTH);
|
|
return ff;
|
|
}
|
|
|
|
uintmax_t uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff)
|
|
{
|
|
return ff->fonts->GetFontFamilyCount();
|
|
}
|
|
|
|
char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, uintmax_t n)
|
|
{
|
|
IDWriteFontFamily *family;
|
|
IDWriteLocalizedStrings *names;
|
|
UINT32 index;
|
|
BOOL exists;
|
|
UINT32 length;
|
|
WCHAR *wname;
|
|
char *name;
|
|
HRESULT hr;
|
|
|
|
hr = ff->fonts->GetFontFamily(n, &family);
|
|
if (hr != S_OK)
|
|
logHRESULT("error getting font out of collection in uiDrawFontFamiliesFamily()", hr);
|
|
hr = family->GetFamilyNames(&names);
|
|
if (hr != S_OK)
|
|
logHRESULT("error getting names of font out in uiDrawFontFamiliesFamily()", hr);
|
|
|
|
// this is complex, but we ignore failure conditions to allow fallbacks
|
|
// 1) If the user locale name was successfully retrieved, try it
|
|
// 2) If the user locale name was not successfully retrieved, or that locale's string does not exist, or an error occurred, try L"en-us", the US English locale
|
|
// 3) And if that fails, assume the first one
|
|
// This algorithm is straight from MSDN: https://msdn.microsoft.com/en-us/library/windows/desktop/dd368214%28v=vs.85%29.aspx
|
|
// For step 2 to work, start by setting hr to S_OK and exists to FALSE.
|
|
// TODO does it skip step 2 entirely if step 1 fails? rewrite it to be a more pure conversion of the MSDN code?
|
|
hr = S_OK;
|
|
exists = FALSE;
|
|
if (ff->userLocaleSuccess != 0)
|
|
hr = names->FindLocaleName(ff->userLocale, &index, &exists);
|
|
if (hr != S_OK || (hr == S_OK && !exists))
|
|
hr = names->FindLocaleName(L"en-us", &index, &exists);
|
|
if (!exists)
|
|
index = 0;
|
|
|
|
hr = names->GetStringLength(index, &length);
|
|
if (hr != S_OK)
|
|
logHRESULT("error getting length of font name in uiDrawFontFamiliesFamily()", hr);
|
|
// GetStringLength() does not include the null terminator, but GetString() does
|
|
wname = (WCHAR *) uiAlloc((length + 1) * sizeof (WCHAR), "WCHAR[]");
|
|
hr = names->GetString(index, wname, length + 1);
|
|
if (hr != S_OK)
|
|
logHRESULT("error getting font name in uiDrawFontFamiliesFamily()", hr);
|
|
|
|
name = toUTF8(wname);
|
|
|
|
uiFree(wname);
|
|
names->Release();
|
|
family->Release();
|
|
return name;
|
|
}
|
|
|
|
void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff)
|
|
{
|
|
ff->fonts->Release();
|
|
uiFree(ff);
|
|
}
|
|
|
|
// 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)
|
|
// the first link above has an example conversion; that seems to confirm that these two functions are right (TODO)
|
|
double uiDrawTextSizeToPoints(double textSize)
|
|
{
|
|
return textSize * (96.0 / 72.0);
|
|
}
|
|
|
|
double uiDrawPointsToTextSize(double points)
|
|
{
|
|
return points * (72.0 / 96.0);
|
|
}
|
|
|
|
struct uiDrawTextFont {
|
|
IDWriteFont *f;
|
|
WCHAR *family; // save for convenience in uiDrawNewTextLayout()
|
|
double size;
|
|
};
|
|
|
|
// Not only does C++11 NOT include C99 designated initializers, but the C++ standards committee has REPEATEDLY REJECTING THEM, covering their ears and yelling "CONSTRUCTORS!!!111 PRIVATE DATA!1111 ENCAPSULATION!11one" at the top of their lungs.
|
|
// So what could have been a simple array lookup is now a loop. Thanks guys.
|
|
|
|
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, uiDrawTextWeightUtraBold, 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 },
|
|
};
|
|
|
|
uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc)
|
|
{
|
|
uiDrawTextFont *font;
|
|
IDWriteFontCollection *collection;
|
|
UINT32 index;
|
|
BOOL exists;
|
|
DWRITE_FONT_WEIGHT weight;
|
|
DWRITE_FONT_STYLE italic;
|
|
DWRITE_FONT_STRETCH stretch;
|
|
bool found;
|
|
int i;
|
|
IDWriteFontFamily *family;
|
|
HRESULT hr;
|
|
|
|
font = uiNew(uiDrawTextFont);
|
|
|
|
// always get the latest available font information
|
|
hr = dwfactory->GetSystemFontCollection(&collection, TRUE);
|
|
if (hr != S_OK)
|
|
logHRESULT("error getting system font collection in uiDrawLoadClosestFont()", hr);
|
|
|
|
font->family = toUTF16(desc->Family);
|
|
hr = collection->FindFamilyName(font->family, &index, &exists);
|
|
if (hr != S_OK)
|
|
logHRESULT("error finding font family in uiDrawLoadClosestFont()", hr);
|
|
if (!exists)
|
|
complain("TODO family not found in uiDrawLoadClosestFont()", hr);
|
|
hr = collection->GetFontFamily(index, &family);
|
|
if (hr != S_OK)
|
|
logHRESULT("error loading font family in uiDrawLoadClosestFont()", hr);
|
|
|
|
found = false;
|
|
for (i = 0; ; i++) {
|
|
if (dwriteWeights[i].uival == desc->Weight) {
|
|
weight = dwriteWeights[i].dwval;
|
|
found = true;
|
|
break;
|
|
}
|
|
if (dwriteWeights[i].lastOne)
|
|
break;
|
|
}
|
|
if (!found)
|
|
complain("invalid initial weight %d passed to uiDrawLoadClosestFont()", desc->Weight);
|
|
|
|
found = false;
|
|
for (i = 0; ; i++) {
|
|
if (dwriteItalics[i].uival == desc->Italic) {
|
|
italic = dwriteItalics[i].dwval;
|
|
found = true;
|
|
break;
|
|
}
|
|
if (dwriteItalics[i].lastOne)
|
|
break;
|
|
}
|
|
if (!found)
|
|
complain("invalid initial italic %d passed to uiDrawLoadClosestFont()", desc->Italic);
|
|
|
|
found = false;
|
|
for (i = 0; ; i++) {
|
|
if (dwriteStretches[i].uival == desc->Stretch) {
|
|
stretch = dwriteStretches[i].dwval;
|
|
found = true;
|
|
break;
|
|
}
|
|
if (dwriteStretches[i].lastOne)
|
|
break;
|
|
}
|
|
if (!found)
|
|
complain("invalid initial stretch %d passed to uiDrawLoadClosestFont()", desc->Stretch);
|
|
|
|
// TODO small caps and gravity
|
|
|
|
hr = family->GetFirstMatchingFont(weight,
|
|
stretch,
|
|
italic,
|
|
&(font->f));
|
|
if (hr != S_OK)
|
|
logHRESULT("error loading font in uiDrawLoadClosestFont()", hr);
|
|
|
|
font->size = 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
|
|
}
|
|
|
|
struct uiDrawTextLayout {
|
|
IDWriteTextFormat *format;
|
|
IDWriteTextLayout *layout;
|
|
intmax_t *bytesToCharacters;
|
|
};
|
|
|
|
#define MBTWC(str, n, wstr, bufsiz) MultiByteToWideChar(CP_UTF8, 0, str, n, wstr, bufsiz)
|
|
|
|
// TODO figure out how ranges are specified in DirectWrite
|
|
// TODO clean up the local variable names and improve documentation
|
|
static intmax_t *toUTF16Offsets(const char *str, WCHAR **wstr, intmax_t *wlenout)
|
|
{
|
|
intmax_t *bytesToCharacters;
|
|
intmax_t i, len;
|
|
int wlen;
|
|
intmax_t outpos;
|
|
|
|
len = strlen(str);
|
|
bytesToCharacters = (intmax_t *) uiAlloc(len * sizeof (intmax_t), "intmax_t[]");
|
|
|
|
wlen = MBTWC(str, -1, NULL, 0);
|
|
if (wlen == 0)
|
|
logLastError("error figuring out number of characters to convert to in toUTF16Offsets()");
|
|
*wstr = (WCHAR *) uiAlloc(wlen * sizeof (WCHAR), "WCHAR[]");
|
|
*wlenout = wlen;
|
|
|
|
i = 0;
|
|
outpos = 0;
|
|
while (i < len) {
|
|
intmax_t n;
|
|
intmax_t j;
|
|
BOOL found;
|
|
int m;
|
|
|
|
// figure out how many characters to convert and convert them
|
|
found = FALSE;
|
|
for (n = 1; (i + n - 1) < len; n++) {
|
|
// TODO do we need MB_ERR_INVALID_CHARS here for this to work properly?
|
|
m = MBTWC(str + i, n, *wstr + outpos, wlen - outpos);
|
|
if (m != 0) { // found a full character
|
|
found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
// if this test passes we reached the end of the string without a successful conversion (invalid string)
|
|
if (!found)
|
|
logLastError("something bad happened when trying to prepare string in uiDrawNewTextLayout()");
|
|
|
|
// now save the character offsets for those bytes
|
|
for (j = 0; j < m; j++)
|
|
bytesToCharacters[j] = outpos;
|
|
|
|
// and go to the next
|
|
i += n;
|
|
outpos += m;
|
|
}
|
|
|
|
return bytesToCharacters;
|
|
}
|
|
|
|
uiDrawTextLayout *uiDrawNewTextLayout(const char *text, uiDrawTextFont *defaultFont)
|
|
{
|
|
uiDrawTextLayout *layout;
|
|
WCHAR *wtext;
|
|
intmax_t wlen;
|
|
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("error creating IDWriteTextFormat in uiDrawNewTextLayout()", hr);
|
|
// TODO small caps
|
|
// TODO gravity
|
|
|
|
layout->bytesToCharacters = toUTF16Offsets(text, &wtext, &wlen);
|
|
hr = dwfactory->CreateTextLayout(wtext, wlen,
|
|
layout->format,
|
|
// FLOAT is float, not double, so this should work... TODO
|
|
FLT_MAX, FLT_MAX,
|
|
&(layout->layout));
|
|
if (hr != S_OK)
|
|
logHRESULT("error creating IDWriteTextLayout in uiDrawNewTextLayout()", hr);
|
|
uiFree(wtext);
|
|
|
|
return layout;
|
|
}
|
|
|
|
void uiDrawFreeTextLayout(uiDrawTextLayout *layout)
|
|
{
|
|
layout->layout->Release();
|
|
layout->format->Release();
|
|
uiFree(layout);
|
|
}
|
|
|
|
void doDrawText(ID2D1RenderTarget *rt, ID2D1Brush *black, double x, double y, uiDrawTextLayout *layout)
|
|
{
|
|
D2D1_POINT_2F pt;
|
|
HRESULT hr;
|
|
|
|
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?
|
|
rt->DrawTextLayout(pt, layout->layout, black, D2D1_DRAW_TEXT_OPTIONS_NONE);
|
|
}
|