libui/windows/attrstr.cpp

417 lines
12 KiB
C++

// 12 february 2017
#include "uipriv_windows.hpp"
#include "attrstr.hpp"
// TODO this whole file needs cleanup
// we need to collect all the background parameters and add them all at once
// TODO consider having background parameters in the drawing effects
// TODO contextual alternates override ligatures?
// TODO rename this struct to something that isn't exclusively foreach-ing?
struct foreachParams {
const uint16_t *s;
size_t len;
IDWriteTextLayout *layout;
std::vector<struct drawTextBackgroundParams *> *backgroundParams;
};
static std::hash<double> doubleHash;
// we need to combine color and underline style into one unit for IDWriteLayout::SetDrawingEffect()
// we also want to combine identical effects, which DirectWrite doesn't seem to provide a way to do
// we can at least try to goad it into doing so if we can deduplicate effects once they're all computed
// so what we do is use this class to store in-progress effects, much like uiprivCombinedFontAttr on the OS X code
// we then deduplicate them later while converting them into a form suitable for drawing with; see applyEffectsAttributes() below
class combinedEffectsAttr : public IUnknown {
ULONG refcount;
uiAttribute *colorAttr;
uiAttribute *underlineAttr;
uiAttribute *underlineColorAttr;
void setAttribute(uiAttribute *a)
{
if (a == NULL)
return;
switch (uiAttributeGetType(a)) {
case uiAttributeTypeColor:
if (this->colorAttr != NULL)
uiprivAttributeRelease(this->colorAttr);
this->colorAttr = uiprivAttributeRetain(a);
break;
case uiAttributeTypeUnderline:
if (this->underlineAttr != NULL)
uiprivAttributeRelease(this->underlineAttr);
this->underlineAttr = uiprivAttributeRetain(a);
break;
case uiAttributeTypeUnderlineColor:
if (this->underlineAttr != NULL)
uiprivAttributeRelease(this->underlineAttr);
this->underlineColorAttr = uiprivAttributeRetain(a);
break;
}
}
// this is needed by applyEffectsAttributes() below
// TODO doesn't uiprivAttributeEqual() already do this; if it doesn't, make it so; if (or when) it does, fix all platforms to avoid this extra check
static bool attrEqual(uiAttribute *a, uiAttribute *b) const
{
if (a == NULL && b == NULL)
return true;
if (a == NULL || b == NULL)
return false;
return uiprivAttributeEqual(a, b);
}
public:
combinedEffectsAttr(uiAttribute *a)
{
this->refcount = 1;
this->colorAttr = NULL;
this->underlineAttr = NULL;
this->underlineColorAttr = NULL;
this->setAttribute(a);
}
~combinedEffectsAttr()
{
if (this->colorAttr != NULL)
uiprivAttributeRelease(this->colorAttr);
if (this->underlineAttr != NULL)
uiprivAttributeRelease(this->underlineAttr);
if (this->underlineColorAttr != NULL)
uiprivAttributeRelease(this->underlineColorAttr);
}
// IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject)
{
if (ppvObject == NULL)
return E_POINTER;
if (riid == IID_IUnknown) {
this->AddRef();
*ppvObject = this;
return S_OK;
}
*ppvObject = NULL;
return E_NOINTERFACE;
}
virtual ULONG STDMETHODCALLTYPE AddRef(void)
{
this->refcount++;
return this->refcount;
}
virtual ULONG STDMETHODCALLTYPE Release(void)
{
this->refcount--;
if (this->refcount == 0) {
delete this;
return 0;
}
return this->refcount;
}
combinedEffectsAttr *cloneWith(uiAttribute *a)
{
combinedEffectsAttr *b;
b = new combinedEffectsAttr(this->colorAttr);
b->setAttribute(this->underlineAttr);
b->setAttribute(this->underlineColorAttr);
b->setAttribute(a);
return b;
}
// and these are also needed by applyEffectsAttributes() below
size_t hash(void) const noexcept
{
size_t ret = 0;
double r, g, b, a;
if (this->colorAttr != NULL) {
uiAttributeColor(this->colorAttr, &r, &g, &b, &a);
ret ^= doubleHash(r);
ret ^= doubleHash(g);
ret ^= doubleHash(b);
ret ^= doubleHash(a);
}
if (this->underlineAttr != NULL)
ret ^= (size_t) uiAttributeUnderline(this->underlineAttr);
if (this->underlineColorAttr != NULL) {
uiAttributeUnderlineColor(this->underlineColorAttr, &colorType, &r, &g, &b, &a);
ret ^= (size_t) colorType;
ret ^= doubleHash(r);
ret ^= doubleHash(g);
ret ^= doubleHash(b);
ret ^= doubleHash(a);
}
return ret;
}
bool equals(const combinedEffectsAttr *b) const
{
if (b == NULL)
return false;
return combinedEffectsAttr::attrEqual(a->colorAttr, b->colorAttr) &&
combinedEffectsAttr::attrEqual(a->underilneAttr, b->underlineAttr) &&
combinedEffectsAttr::attrEqual(a->underlineColorAttr, b->underlineColorAttr);
}
drawingEffectsAttr *toDrawingEffectsAttr(void)
{
drawingEffectsAttr *dea;
double r, g, b, a;
uiUnderlineColor colorType;
dea = new drawingEffectsAttr;
if (this->colorAttr != NULL) {
uiAttributeColor(this->colorAttr, &r, &g, &b, &a);
dea->addColor(r, g, b, a);
}
if (this->underlineAttr != NULL)
dea->addUnderline(uiAttributeUnderline(this->underlineAttr));
if (this->underlineColorAttr != NULL) {
uiAttributeUnderlineColor(this->underlineColor, &colorType, &r, &g, &b, &a);
// TODO see if Microsoft has any standard colors for these
switch (colorType) {
case uiUnderlineColorSpelling:
// TODO consider using the GtkTextView style property error-underline-color here if Microsoft has no preference
r = 1.0;
g = 0.0;
b = 0.0;
a = 1.0;
break;
case uiUnderlineColorGrammar:
r = 0.0;
g = 1.0;
b = 0.0;
a = 1.0;
break;
case uiUnderlineColorAlternate:
r = 0.0;
g = 0.0;
b = 1.0;
a = 1.0;
break;
}
dea->addUnderlineColor(r, g, b, a);
}
return dea;
}
};
// also needed by applyEffectsAttributes() below
class applyEffectsHash {
public:
typedef combinedEffectsAttr *ceaptr;
size_t operator()(applyEffectsHash::ceaptr const &cea) const noexcept
{
return cea->hash();
}
};
class applyEffectsEqualTo {
public:
typedef combinedEffectsAttr *ceaptr;
bool operator()(const applyEffectsEqualTo::ceaptr &a, const applyEffectsEqualTo::ceaptr &b) const
{
return a->equals(b);
}
};
static HRESULT addEffectAttributeToRange(struct foreachParams *p, size_t start, size_t end, uiAttribute *attr)
{
IUnknown *u;
combinedEffectsAttr *cea;
DWRITE_TEXT_RANGE range;
size_t diff;
HRESULT hr;
while (start < end) {
hr = p->layout->GetDrawingEffect(start, &u, &range);
if (hr != S_OK)
{logHRESULT(L"HELP", hr);
// TODO proper cleanup somehow
return hr;
} cea = (combinedEffectsAttr *) u;
if (cea == NULL)
cea = new combinedEffectsAttr(attr);
else
cea = cea->cloneWith(attr);
// clamp range within [start, end)
if (range.startPosition < start) {
diff = start - range.startPosition;
range.startPosition = start;
range.length -= diff;
}
if ((range.startPosition + range.length) > end)
range.length = end - range.startPosition;
hr = p->layout->SetDrawingEffect(cea, range);
if (hr != S_OK)
// TODO proper cleanup somehow
return hr;
// TODO figure out what and when needs to be released
start += range.length;
}
return S_OK;
}
static void addBackgroundParams(struct foreachParams *p, size_t start, size_t end, const uiAttribute *attr)
{
struct drawTextBackgroundParams *params;
params = uiprivNew(struct drawTextBackgroundParams);
params->start = start;
params->end = end;
uiAttributeColor(attr, &(params->r), &(params->g), &(params->b), &(params->a));
p->backgroundParams->push_back(params);
}
static uiForEach processAttribute(const uiAttributedString *s, const uiAttribute *attr, size_t start, size_t end, void *data)
{
struct foreachParams *p = (struct foreachParams *) data;
DWRITE_TEXT_RANGE range;
WCHAR *wfamily;
BOOL hasUnderline;
IDWriteTypography *dt;
HRESULT hr;
start = uiprivAttributedStringUTF8ToUTF16(s, start);
end = uiprivAttributedStringUTF8ToUTF16(s, end);
range.startPosition = start;
range.length = end - start;
switch (uiAttributeGetType(attr)) {
case uiAttributeTypeFamily:
wfamily = toUTF16(uiAttributeFamily(attr));
hr = p->layout->SetFontFamilyName(wfamily, range);
if (hr != S_OK)
logHRESULT(L"error applying family name attribute", hr);
uiFree(wfamily);
break;
case uiAttributeTypeSize:
hr = p->layout->SetFontSize(
// TODO unify with fontmatch.cpp and/or attrstr.hpp
#define pointSizeToDWriteSize(size) (size * (96.0 / 72.0))
pointSizeToDWriteSize(uiAttributeSize(attr)),
range);
if (hr != S_OK)
logHRESULT(L"error applying size attribute", hr);
break;
case uiAttributeTypeWeight:
hr = p->layout->SetFontWeight(
uiprivWeightToDWriteWeight(uiAttributeWeight(attr)),
range);
if (hr != S_OK)
logHRESULT(L"error applying weight attribute", hr);
break;
case uiAttributeTypeItalic:
hr = p->layout->SetFontStyle(
uiprivItalicToDWriteStyle(uiAttributeItalic(attr)),
range);
if (hr != S_OK)
logHRESULT(L"error applying italic attribute", hr);
break;
case uiAttributeTypeStretch:
hr = p->layout->SetFontStretch(
uiprivStretchToDWriteStretch(uiAttributeStretch(attr)),
range);
if (hr != S_OK)
logHRESULT(L"error applying stretch attribute", hr);
break;
case uiAttributeTypeUnderline:
// mark that we have an underline; otherwise, DirectWrite will never call our custom renderer's DrawUnderline() method
hasUnderline = FALSE;
if (uiAttributeUnderline(attr) != uiUnderlineNone)
hasUnderline = TRUE;
hr = p->layout->SetUnderline(hasUnderline, range);
if (hr != S_OK)
logHRESULT(L"error applying underline attribute", hr);
// and fall through to set the underline style through the drawing effect
case uiAttributeTypeColor:
case uiAttributeTypeUnderlineColor:
hr = addEffectAttributeToRange(p, start, end, attr);
if (hr != S_OK)
logHRESULT(L"error applying effect (color, underline, or underline color) attribute", hr);
break;
case uiAttributeBackground:
addBackgroundParams(p, start, end, attr);
break;
case uiAttributeTypeFeatures:
// only generate an attribute if not NULL
// TODO do we still need to do this or not...
if (uiAttributeFeatures(attr) == NULL)
break;
dt = uiprivOpenTypeFeaturesToIDWriteTypography(uiAttributeFeatures(attr));
hr = p->layout->SetTypography(dt, range);
if (hr != S_OK)
logHRESULT(L"error applying features attribute", hr);
dt->Release();
break;
}
return uiForEachContinue;
}
static HRESULT applyEffectsAttributes(struct foreachParams *p)
{
IUnknown *u;
combinedEffectsAttr *cea;
drawingEffectsAttr *dea;
DWRITE_TEXT_RANGE range;
// here's the magic: this std::unordered_map will deduplicate all of our combinedEffectsAttrs, mapping all identical ones to a single drawingEffectsAttr
// because drawingEffectsAttr is the *actual* drawing effect we want for rendering, we also replace the combinedEffectsAttrs with them in the IDWriteTextLayout at the same time
// note the use of our custom hash and equal_to implementations
std::unordered_map<combinedEffectsAttrs *, drawingEffectsAttr *,
applyEffectsHash, applyEffectsEqualTo> effects;
HRESULT hr;
// go through, replacing every combinedEffectsAttr with the proper drawingEffectsAttr
range.startPosition = 0;
while (range.startPosition < p->len) {
hr = p->layout->GetDrawingEffect(range.startPosition, &u, &range);
if (hr != S_OK)
// TODO proper cleanup somehow
return hr;
cea = (combinedEffectsAttr *) cea;
if (cea != NULL) {
auto diter = effects.find(cea);
if (diter != effects.end())
dea = diter->second;
else {
dea = cea->toDrawingEffectsAttr();
effects.insert(cea, dea);
}
hr = p->layout->SetDrawingEffect(dea, range);
if (hr != S_OK)
// TODO proper cleanup somehow
return hr;
}
range.startPosition += range.length;
}
// and clean up, finally destroying the combinedEffectAttrs too
#if 0
TODO
for (auto iter = effects.begin(); iter != effects.end(); iter++) {
iter->first->Release();
iter->second->Release();
}
#endif
return S_OK;
}
void uiprivAttributedStringApplyAttributesToDWriteTextLayout(uiDrawTextLayoutParams *p, IDWriteTextLayout *layout, std::vector<struct drawTextBackgroundParams *> **backgroundParams)
{
struct foreachParams fep;
HRESULT hr;
fep.s = attrstrUTF16(p->String);
fep.len = attrstrUTF16Len(p->String);
fep.layout = layout;
fep.backgroundParams = new std::vector<struct drawTextBackgroundParams *>;
uiAttributedStringForEachAttribute(p->String, processAttribute, &fep);
hr = applyEffectsAttributes(&fep);
if (hr != S_OK)
logHRESULT(L"error applying effects attributes", hr);
*backgroundParams = fep.backgroundParams;
}