// 12 february 2017 #include "uipriv_windows.hpp" #include "attrstr.hpp" // TODO this whole file needs cleanup // we need to collect all the background blocks and add them all at once // 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 *backgroundFuncs; }; static std::hash 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 (self->colorAttr != NULL) { uiAttributeColor(self->colorAttr, &r, &g, &b, &a); ret ^= doubleHash(r); ret ^= doubleHash(g); ret ^= doubleHash(b); ret ^= doubleHash(a); } if (self->underlineAttr != NULL) ret ^= (size_t) uiAttributeUnderline(self->underlineAttr); if (self->underlineColorAttr != NULL) { uiAttributeUnderlineColor(self->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 (self->colorAttr != NULL) { uiAttributeColor(self->colorAttr, &r, &g, &b, &a); dea->addColor(r, g, b, a); } if (self->underlineAttr != NULL) dea->addUnderline(uiAttributeUnderline(self->underlineAttr)); if (self->underlineColorAttr != NULL) { uiAttributeUnderlineColor(self->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 backgroundFunc mkBackgroundFunc(size_t start, size_t end, double r, double g, double b, double a) { return [=](uiDrawContext *c, uiDrawTextLayout *layout, double x, double y) { uiDrawBrush brush; brush.Type = uiDrawBrushTypeSolid; brush.R = r; brush.G = g; brush.B = b; brush.A = a; drawTextBackground(c, x, y, layout, start, end, &brush, 0); }; } 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; size_t ostart, oend; BOOL hasUnderline; IDWriteTypography *dt; HRESULT hr; ostart = start; oend = end; // TODO fix const correctness start = attrstrUTF8ToUTF16((uiAttributedString *) s, start); end = attrstrUTF8ToUTF16((uiAttributedString *) 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: p->backgroundFuncs->push_back( mkBackgroundFunc(ostart, oend, spec->R, spec->G, spec->B, spec->A)); 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 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 **backgroundFuncs) { struct foreachParams fep; HRESULT hr; fep.s = attrstrUTF16(p->String); fep.len = attrstrUTF16Len(p->String); fep.layout = layout; fep.backgroundFuncs = new std::vector; uiAttributedStringForEachAttribute(p->String, processAttribute, &fep); hr = applyEffectsAttributes(&fep); if (hr != S_OK) logHRESULT(L"error applying effects attributes", hr); *backgroundFuncs = fep.backgroundFuncs; }