// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "CustomTextRenderer.h" #include #include #include using namespace Microsoft::Console::Render; #pragma region IDWritePixelSnapping methods // Routine Description: // - Implementation of IDWritePixelSnapping::IsPixelSnappingDisabled // - Determines if we're allowed to snap text to pixels for this particular drawing context // Arguments: // - clientDrawingContext - Pointer to structure of information required to draw // - isDisabled - TRUE if we do not snap to nearest pixels. FALSE otherwise. // Return Value: // - S_OK [[nodiscard]] HRESULT CustomTextRenderer::IsPixelSnappingDisabled(void* /*clientDrawingContext*/, _Out_ BOOL* isDisabled) { *isDisabled = false; return S_OK; } // Routine Description: // - Implementation of IDWritePixelSnapping::GetPixelsPerDip // - Retrieves the number of real monitor pixels to use per device-independent-pixel (DIP) // - DIPs are used by DirectX all the way until the final drawing surface so things are only // scaled at the very end and the complexity can be abstracted. // Arguments: // - clientDrawingContext - Pointer to structure of information required to draw // - pixelsPerDip - The number of pixels per DIP. 96 is standard DPI. // Return Value: // - S_OK [[nodiscard]] HRESULT CustomTextRenderer::GetPixelsPerDip(void* clientDrawingContext, _Out_ FLOAT* pixelsPerDip) { DrawingContext* drawingContext = static_cast(clientDrawingContext); float dpiX, dpiY; drawingContext->renderTarget->GetDpi(&dpiX, &dpiY); *pixelsPerDip = dpiX / USER_DEFAULT_SCREEN_DPI; return S_OK; } // Routine Description: // - Implementation of IDWritePixelSnapping::GetCurrentTransform // - Retrieves the the matrix transform to be used while laying pixels onto the // drawing context // Arguments: // - clientDrawingContext - Pointer to structure of information required to draw // - transform - The matrix transform to use to adapt DIP representations into real monitor coordinates. // Return Value: // - S_OK [[nodiscard]] HRESULT CustomTextRenderer::GetCurrentTransform(void* clientDrawingContext, DWRITE_MATRIX* transform) { DrawingContext* drawingContext = static_cast(clientDrawingContext); // Matrix structures are defined identically drawingContext->renderTarget->GetTransform((D2D1_MATRIX_3X2_F*)transform); return S_OK; } #pragma endregion #pragma region IDWriteTextRenderer methods // Routine Description: // - Implementation of IDWriteTextRenderer::DrawUnderline // - Directs us to draw an underline on the given context at the given position. // Arguments: // - clientDrawingContext - Pointer to structure of information required to draw // - baselineOriginX - The text baseline position's X coordinate // - baselineOriginY - The text baseline position's Y coordinate // - The baseline is generally not the top nor the bottom of the "cell" that // text is drawn into. It's usually somewhere "in the middle" and depends on the // font and the glyphs. It can be calculated during layout and analysis in respect // to the given font and glyphs. // - underline - The properties of the underline that we should use for drawing // - clientDrawingEffect - any special effect to pass along for rendering // Return Value: // - S_OK [[nodiscard]] HRESULT CustomTextRenderer::DrawUnderline(void* clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, _In_ const DWRITE_UNDERLINE* underline, IUnknown* clientDrawingEffect) { _FillRectangle(clientDrawingContext, clientDrawingEffect, baselineOriginX, baselineOriginY + underline->offset, underline->width, underline->thickness, underline->readingDirection, underline->flowDirection); return S_OK; } // Routine Description: // - Implementation of IDWriteTextRenderer::DrawStrikethrough // - Directs us to draw a strikethrough on the given context at the given position. // Arguments: // - clientDrawingContext - Pointer to structure of information required to draw // - baselineOriginX - The text baseline position's X coordinate // - baselineOriginY - The text baseline position's Y coordinate // - The baseline is generally not the top nor the bottom of the "cell" that // text is drawn into. It's usually somewhere "in the middle" and depends on the // font and the glyphs. It can be calculated during layout and analysis in respect // to the given font and glyphs. // - strikethrough - The properties of the strikethrough that we should use for drawing // - clientDrawingEffect - any special effect to pass along for rendering // Return Value: // - S_OK [[nodiscard]] HRESULT CustomTextRenderer::DrawStrikethrough(void* clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, _In_ const DWRITE_STRIKETHROUGH* strikethrough, IUnknown* clientDrawingEffect) { _FillRectangle(clientDrawingContext, clientDrawingEffect, baselineOriginX, baselineOriginY + strikethrough->offset, strikethrough->width, strikethrough->thickness, strikethrough->readingDirection, strikethrough->flowDirection); return S_OK; } // Routine Description: // - Helper method to draw a line through our text. // Arguments: // - clientDrawingContext - Pointer to structure of information required to draw // - clientDrawingEffect - any special effect passed along for rendering // - x - The left coordinate of the rectangle // - y - The top coordinate of the rectangle // - width - The width of the rectangle (from X to the right) // - height - The height of the rectangle (from Y down) // - readingDirection - textual reading information that could affect the rectangle // - flowDirection - textual flow information that could affect the rectangle // Return Value: // - S_OK void CustomTextRenderer::_FillRectangle(void* clientDrawingContext, IUnknown* clientDrawingEffect, float x, float y, float width, float thickness, DWRITE_READING_DIRECTION /*readingDirection*/, DWRITE_FLOW_DIRECTION /*flowDirection*/) { DrawingContext* drawingContext = static_cast(clientDrawingContext); // Get brush ID2D1Brush* brush = drawingContext->foregroundBrush; if (clientDrawingEffect != nullptr) { brush = static_cast(clientDrawingEffect); } D2D1_RECT_F rect = D2D1::RectF(x, y, x + width, y + thickness); drawingContext->renderTarget->FillRectangle(&rect, brush); } // Routine Description: // - Implementation of IDWriteTextRenderer::DrawInlineObject // - Passes drawing control from the outer layout down into the context of an embedded object // which can have its own drawing layout and renderer properties at a given position // Arguments: // - clientDrawingContext - Pointer to structure of information required to draw // - originX - The left coordinate of the draw position // - originY - The top coordinate of the draw position // - inlineObject - The object to draw at the position // - isSideways - Should be drawn vertically instead of horizontally // - isRightToLeft - Should be drawn RTL (or bottom to top) instead of the default way // - clientDrawingEffect - any special effect passed along for rendering // Return Value: // - S_OK or appropriate error from the delegated inline object's draw call [[nodiscard]] HRESULT CustomTextRenderer::DrawInlineObject(void* clientDrawingContext, FLOAT originX, FLOAT originY, IDWriteInlineObject* inlineObject, BOOL isSideways, BOOL isRightToLeft, IUnknown* clientDrawingEffect) { return inlineObject->Draw(clientDrawingContext, this, originX, originY, isSideways, isRightToLeft, clientDrawingEffect); } // Routine Description: // - Implementation of IDWriteTextRenderer::DrawInlineObject // - Passes drawing control from the outer layout down into the context of an embedded object // which can have its own drawing layout and renderer properties at a given position // Arguments: // - clientDrawingContext - Pointer to structure of information required to draw // - baselineOriginX - The text baseline position's X coordinate // - baselineOriginY - The text baseline position's Y coordinate // - The baseline is generally not the top nor the bottom of the "cell" that // text is drawn into. It's usually somewhere "in the middle" and depends on the // font and the glyphs. It can be calculated during layout and analysis in respect // to the given font and glyphs. // - measuringMode - The mode to measure glyphs in the DirectWrite context // - glyphRun - Information on the glyphs // - glyphRunDescription - Further metadata about the glyphs used while drawing // - clientDrawingEffect - any special effect passed along for rendering // Return Value: // - S_OK, GSL/WIL/STL error, or appropriate DirectX/Direct2D/DirectWrite based error while drawing. [[nodiscard]] HRESULT CustomTextRenderer::DrawGlyphRun( void* clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, DWRITE_MEASURING_MODE measuringMode, const DWRITE_GLYPH_RUN* glyphRun, const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, IUnknown* /*clientDrawingEffect*/) { // Color glyph rendering sourced from https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/DWriteColorGlyph DrawingContext* drawingContext = static_cast(clientDrawingContext); // Since we've delegated the drawing of the background of the text into this function, the origin passed in isn't actually the baseline. // It's the top left corner. Save that off first. D2D1_POINT_2F origin = D2D1::Point2F(baselineOriginX, baselineOriginY); // Then make a copy for the baseline origin (which is part way down the left side of the text, not the top or bottom). // We'll use this baseline Origin for drawing the actual text. D2D1_POINT_2F baselineOrigin = origin; baselineOrigin.y += drawingContext->spacing.baseline; ::Microsoft::WRL::ComPtr d2dContext; RETURN_IF_FAILED(drawingContext->renderTarget->QueryInterface(d2dContext.GetAddressOf())); // Draw the background D2D1_RECT_F rect; rect.top = origin.y; rect.bottom = rect.top + drawingContext->cellSize.height; rect.left = origin.x; rect.right = rect.left; for (UINT32 i = 0; i < glyphRun->glyphCount; i++) { rect.right += glyphRun->glyphAdvances[i]; } d2dContext->FillRectangle(rect, drawingContext->backgroundBrush); // Now go onto drawing the text. // First check if we want a color font and try to extract color emoji first. // Color emoji are only available on Windows 10+ if (WI_IsFlagSet(drawingContext->options, D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT) && IsWindows10OrGreater()) { ::Microsoft::WRL::ComPtr d2dContext4; RETURN_IF_FAILED(d2dContext.As(&d2dContext4)); ::Microsoft::WRL::ComPtr dwriteFactory4; RETURN_IF_FAILED(drawingContext->dwriteFactory->QueryInterface(dwriteFactory4.GetAddressOf())); // The list of glyph image formats this renderer is prepared to support. DWRITE_GLYPH_IMAGE_FORMATS supportedFormats = DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE | DWRITE_GLYPH_IMAGE_FORMATS_CFF | DWRITE_GLYPH_IMAGE_FORMATS_COLR | DWRITE_GLYPH_IMAGE_FORMATS_SVG | DWRITE_GLYPH_IMAGE_FORMATS_PNG | DWRITE_GLYPH_IMAGE_FORMATS_JPEG | DWRITE_GLYPH_IMAGE_FORMATS_TIFF | DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8; // Determine whether there are any color glyph runs within glyphRun. If // there are, glyphRunEnumerator can be used to iterate through them. ::Microsoft::WRL::ComPtr glyphRunEnumerator; HRESULT hr = dwriteFactory4->TranslateColorGlyphRun(baselineOrigin, glyphRun, glyphRunDescription, supportedFormats, measuringMode, nullptr, 0, &glyphRunEnumerator); // If the analysis found no color glyphs in the run, just draw normally. if (hr == DWRITE_E_NOCOLOR) { RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription, drawingContext->foregroundBrush)); } else { RETURN_IF_FAILED(hr); ::Microsoft::WRL::ComPtr tempBrush; // Complex case: the run has one or more color runs within it. Iterate // over the sub-runs and draw them, depending on their format. for (;;) { BOOL haveRun; RETURN_IF_FAILED(glyphRunEnumerator->MoveNext(&haveRun)); if (!haveRun) break; DWRITE_COLOR_GLYPH_RUN1 const* colorRun; RETURN_IF_FAILED(glyphRunEnumerator->GetCurrentRun(&colorRun)); D2D1_POINT_2F currentBaselineOrigin = D2D1::Point2F(colorRun->baselineOriginX, colorRun->baselineOriginY); switch (colorRun->glyphImageFormat) { case DWRITE_GLYPH_IMAGE_FORMATS_PNG: case DWRITE_GLYPH_IMAGE_FORMATS_JPEG: case DWRITE_GLYPH_IMAGE_FORMATS_TIFF: case DWRITE_GLYPH_IMAGE_FORMATS_PREMULTIPLIED_B8G8R8A8: { // This run is bitmap glyphs. Use Direct2D to draw them. d2dContext4->DrawColorBitmapGlyphRun(colorRun->glyphImageFormat, currentBaselineOrigin, &colorRun->glyphRun, measuringMode); } break; case DWRITE_GLYPH_IMAGE_FORMATS_SVG: { // This run is SVG glyphs. Use Direct2D to draw them. d2dContext4->DrawSvgGlyphRun(currentBaselineOrigin, &colorRun->glyphRun, drawingContext->foregroundBrush, nullptr, // svgGlyphStyle 0, // colorPaletteIndex measuringMode); } break; case DWRITE_GLYPH_IMAGE_FORMATS_TRUETYPE: case DWRITE_GLYPH_IMAGE_FORMATS_CFF: case DWRITE_GLYPH_IMAGE_FORMATS_COLR: default: { // This run is solid-color outlines, either from non-color // glyphs or from COLR glyph layers. Use Direct2D to draw them. ID2D1Brush* layerBrush; // The rule is "if 0xffff, use current brush." See: // https://docs.microsoft.com/en-us/windows/desktop/api/dwrite_2/ns-dwrite_2-dwrite_color_glyph_run if (colorRun->paletteIndex == 0xFFFF) { // This run uses the current text color. layerBrush = drawingContext->foregroundBrush; } else { if (!tempBrush) { RETURN_IF_FAILED(d2dContext4->CreateSolidColorBrush(colorRun->runColor, &tempBrush)); } else { // This run specifies its own color. tempBrush->SetColor(colorRun->runColor); } layerBrush = tempBrush.Get(); } // Draw the run with the selected color. RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext, currentBaselineOrigin, measuringMode, &colorRun->glyphRun, colorRun->glyphRunDescription, layerBrush)); } break; } } } } else { // Simple case: the run has no color glyphs. Draw the main glyph run // using the current text color. RETURN_IF_FAILED(_DrawBasicGlyphRun(drawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription, drawingContext->foregroundBrush)); } return S_OK; } #pragma endregion [[nodiscard]] HRESULT CustomTextRenderer::_DrawBasicGlyphRun(DrawingContext* clientDrawingContext, D2D1_POINT_2F baselineOrigin, DWRITE_MEASURING_MODE measuringMode, _In_ const DWRITE_GLYPH_RUN* glyphRun, _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription, ID2D1Brush* brush) { ::Microsoft::WRL::ComPtr d2dContext; RETURN_IF_FAILED(clientDrawingContext->renderTarget->QueryInterface(d2dContext.GetAddressOf())); // Using the context is the easiest/default way of drawing. d2dContext->DrawGlyphRun(baselineOrigin, glyphRun, glyphRunDescription, brush, measuringMode); // However, we could probably add options here and switch out to one of these other drawing methods (making it // conditional based on the IUnknown* clientDrawingEffect or on some other switches and try these out instead: //_DrawBasicGlyphRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription); //_DrawGlowGlyphRun(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription); return S_OK; } [[nodiscard]] HRESULT CustomTextRenderer::_DrawBasicGlyphRunManually(DrawingContext* clientDrawingContext, D2D1_POINT_2F baselineOrigin, DWRITE_MEASURING_MODE /*measuringMode*/, _In_ const DWRITE_GLYPH_RUN* glyphRun, _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/) { // This is regular text but manually ::Microsoft::WRL::ComPtr d2dFactory; clientDrawingContext->renderTarget->GetFactory(d2dFactory.GetAddressOf()); ::Microsoft::WRL::ComPtr pathGeometry; d2dFactory->CreatePathGeometry(pathGeometry.GetAddressOf()); ::Microsoft::WRL::ComPtr geometrySink; pathGeometry->Open(geometrySink.GetAddressOf()); glyphRun->fontFace->GetGlyphRunOutline( glyphRun->fontEmSize, glyphRun->glyphIndices, glyphRun->glyphAdvances, glyphRun->glyphOffsets, glyphRun->glyphCount, glyphRun->isSideways, glyphRun->bidiLevel % 2, geometrySink.Get()); geometrySink->Close(); D2D1::Matrix3x2F const matrixAlign = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y); ::Microsoft::WRL::ComPtr transformedGeometry; d2dFactory->CreateTransformedGeometry(pathGeometry.Get(), &matrixAlign, transformedGeometry.GetAddressOf()); clientDrawingContext->renderTarget->FillGeometry(transformedGeometry.Get(), clientDrawingContext->foregroundBrush); return S_OK; } [[nodiscard]] HRESULT CustomTextRenderer::_DrawGlowGlyphRun(DrawingContext* clientDrawingContext, D2D1_POINT_2F baselineOrigin, DWRITE_MEASURING_MODE /*measuringMode*/, _In_ const DWRITE_GLYPH_RUN* glyphRun, _In_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/) { // This is glow text manually ::Microsoft::WRL::ComPtr d2dFactory; clientDrawingContext->renderTarget->GetFactory(d2dFactory.GetAddressOf()); ::Microsoft::WRL::ComPtr pathGeometry; d2dFactory->CreatePathGeometry(pathGeometry.GetAddressOf()); ::Microsoft::WRL::ComPtr geometrySink; pathGeometry->Open(geometrySink.GetAddressOf()); glyphRun->fontFace->GetGlyphRunOutline( glyphRun->fontEmSize, glyphRun->glyphIndices, glyphRun->glyphAdvances, glyphRun->glyphOffsets, glyphRun->glyphCount, glyphRun->isSideways, glyphRun->bidiLevel % 2, geometrySink.Get()); geometrySink->Close(); D2D1::Matrix3x2F const matrixAlign = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y); ::Microsoft::WRL::ComPtr transformedGeometry; d2dFactory->CreateTransformedGeometry(pathGeometry.Get(), &matrixAlign, transformedGeometry.GetAddressOf()); ::Microsoft::WRL::ComPtr alignedGeometry; d2dFactory->CreateTransformedGeometry(pathGeometry.Get(), &matrixAlign, alignedGeometry.GetAddressOf()); ::Microsoft::WRL::ComPtr brush; ::Microsoft::WRL::ComPtr outlineBrush; clientDrawingContext->renderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::White, 1.0f), brush.GetAddressOf()); clientDrawingContext->renderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Red, 1.0f), outlineBrush.GetAddressOf()); clientDrawingContext->renderTarget->DrawGeometry(transformedGeometry.Get(), outlineBrush.Get(), 2.0f); clientDrawingContext->renderTarget->FillGeometry(alignedGeometry.Get(), brush.Get()); return S_OK; }