Scale box drawing glyphs to fit cells for visual bliss (#5743)

## Summary of the Pull Request
Identifies and scales glyphs in the box and line drawing ranges U+2500-U+259F to fit their cells.

## PR Checklist
* [x] Closes #455
* [x] I work here.
* [x] Manual tests. This is all graphical.
* [x] Metric ton of comments
* [x] Math spreadsheet included in PR.
* [x] Double check RTL glyphs.
* [x] Why is there the extra pixel?
* [x] Scrolling the mouse wheel check is done.
* [x] Not drawing outline?
* [x] Am core contributor. Roar.
* [x] Try suppressing negative scale factors and see if that gets rid of weird shading.

## Detailed Description of the Pull Request / Additional comments

### Background
- We want the Terminal to be fast at drawing. To be fast at drawing, we perform differential drawing, or only drawing what is different from the previous frame. We use DXGI's `Present1` method to help us with this as it helps us compose only the deltas onto the previous frame at drawing time and assists us in scrolling regions from the previous frame without intervention. However, it only works on strictly integer pixel row heights.
- Most of the hit testing and size-calculation logic in both the `conhost` and the Terminal products are based on the size of an individual cell. Historically, a cell was always dictated in a `COORD` structure, or two `SHORT` values... which are integers. As such, when we specify the space for any individual glyph to be displayed inside our terminal drawing region, we want it to fall perfectly inside of an integer box to ensure all these other algorithms work correctly and continue to do so.
- Finally, we want the Terminal to have font fallback and locate glyphs that aren't in the primary selected font from any other font it can find on the system that contains the glyph, per DirectWrite's font fallback mechanisms. These glyphs won't necessarily have the same font or glyph metrics as the base font, but we need them to fit inside the same cell dimensions as if they did because the hit testing and other algorithms aren't aware of which particular font is sourcing each glyph, just the dimensions of the bounding box per cell.

### How does Terminal deal with this?
- When we select a font, we perform some calculations using the design metrics of the font and glyphs to determine how we could fit them inside a cell with integer dimensions. Our process here is that we take the requested font size (which is generally a proxy for height), find the matching glyph width for that height then round it to an integer. We back convert from that now integer width to a height value which is almost certainly now a floating point number. But because we need an integer box value, we add line padding above and below the glyphs to ensure that the height is an integer as well as the width. Finally, we don't add the padding strictly equally. We attempt to align the English baseline of the glyph box directly onto an integer pixel multiple so most characters sit crisply on a line when displayed. 
- Note that fonts and their glyphs have a prescribed baseline, line gap, and advance values. We use those as guidelines to get us started, but then to meet our requirements, we pad out from those. This results in fonts that should be properly authored showing gaps. It also results in fonts that are improperly authored looking even worse than they normally would.

### Now how does block and line drawing come in?
- Block and Line drawing glyphs are generally authored so they will look fine when the font and glyph metrics are followed exactly as prescribed by the font. (For some fonts, this still isn't true and we want them to look fine anyway.)
- When we add additional padding or rounding to make glyphs fit inside of a cell, we can be adding more space than was prescribed around these glyphs. This can cause a gap to be visible.
- Additionally, when we move things like baselines to land on a perfect integer pixel, we may be drawing a glyph lower in the bounding box than was prescribed originally.

### And how do we solve it?
- We identify all glyphs in the line and block drawing ranges.
- We find the bounding boxes of both the cell and the glyph.
- We compare the height of the glyph to the height of the cell to see if we need to scale. We prescribe a scale transform if the glyph wouldn't be tall enough to fit the box. (We leave it alone otherwise as some glyphs intentionally overscan the box and scaling them can cause banding effects.)
- We inspect the overhang/underhang above and below the boxes and translate transform them (slide them) so they cover the entire cell area.
- We repeat the previous two steps but in the horizontal direction.

## Validation Steps Performed
- See these commments:
   - https://github.com/microsoft/terminal/issues/455#issuecomment-620248375
   - https://github.com/microsoft/terminal/issues/455#issuecomment-621533916
   - https://github.com/microsoft/terminal/issues/455#issuecomment-622585453

Also see the below one with more screenshots:
   - https://github.com/microsoft/terminal/pull/5743#issuecomment-624940567
This commit is contained in:
Michael Niksa 2020-05-08 14:09:32 -07:00 committed by GitHub
parent d77745d035
commit 70867df077
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 672 additions and 42 deletions

View File

@ -1,3 +1,4 @@
IBox
ICustom
IMap
IObject
@ -6,6 +7,8 @@ NCHITTEST
NCLBUTTONDBLCLK
NCRBUTTONDBLCLK
NOREDIRECTIONBITMAP
oaidl
ocidl
rfind
roundf
SIZENS

1
.gitignore vendored
View File

@ -48,6 +48,7 @@ dlldata.c
project.lock.json
artifacts/
*_h.h
*_i.c
*_p.c
*_i.h

Binary file not shown.

View File

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "BoxDrawingEffect.h"
using namespace Microsoft::Console::Render;
BoxDrawingEffect::BoxDrawingEffect() noexcept :
_scale{ 1.0f, 0.0f, 1.0f, 0.0f }
{
}
#pragma warning(suppress : 26434) // WRL RuntimeClassInitialize base is a no-op and we need this for MakeAndInitialize
HRESULT BoxDrawingEffect::RuntimeClassInitialize(float verticalScale, float verticalTranslate, float horizontalScale, float horizontalTranslate) noexcept
{
_scale.VerticalScale = verticalScale;
_scale.VerticalTranslation = verticalTranslate;
_scale.HorizontalScale = horizontalScale;
_scale.HorizontalTranslation = horizontalTranslate;
return S_OK;
}
[[nodiscard]] HRESULT STDMETHODCALLTYPE BoxDrawingEffect::GetScale(BoxScale* scale) noexcept
{
RETURN_HR_IF_NULL(E_INVALIDARG, scale);
*scale = _scale;
return S_OK;
}

View File

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <wrl.h>
#include <wrl/client.h>
#include <wrl/implements.h>
#include "IBoxDrawingEffect_h.h"
namespace Microsoft::Console::Render
{
class BoxDrawingEffect : public ::Microsoft::WRL::RuntimeClass<::Microsoft::WRL::RuntimeClassFlags<::Microsoft::WRL::ClassicCom | ::Microsoft::WRL::InhibitFtmBase>, IBoxDrawingEffect>
{
public:
BoxDrawingEffect() noexcept;
HRESULT RuntimeClassInitialize(float verticalScale, float verticalTranslate, float horizontalScale, float horizontalTranslate) noexcept;
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetScale(BoxScale* scale) noexcept override;
protected:
private:
BoxScale _scale;
#ifdef UNIT_TESTING
public:
friend class BoxDrawingEffectTests;
#endif
};
}

View File

@ -9,6 +9,8 @@
#include <wrl/client.h>
#include <VersionHelpers.h>
#include "BoxDrawingEffect.h"
using namespace Microsoft::Console::Render;
// Routine Description:
@ -20,16 +22,19 @@ using namespace Microsoft::Console::Render;
// - font - The DirectWrite font face to use while calculating layout (by default, will fallback if necessary)
// - clusters - From the backing buffer, the text to be displayed clustered by the columns it should consume.
// - width - The count of pixels available per column (the expected pixel width of every column)
// - boxEffect - Box drawing scaling effects that are cached for the base font across layouts.
CustomTextLayout::CustomTextLayout(gsl::not_null<IDWriteFactory1*> const factory,
gsl::not_null<IDWriteTextAnalyzer1*> const analyzer,
gsl::not_null<IDWriteTextFormat*> const format,
gsl::not_null<IDWriteFontFace1*> const font,
std::basic_string_view<Cluster> const clusters,
size_t const width) :
size_t const width,
IBoxDrawingEffect* const boxEffect) :
_factory{ factory.get() },
_analyzer{ analyzer.get() },
_format{ format.get() },
_font{ font.get() },
_boxDrawingEffect{ boxEffect },
_localeName{},
_numberSubstitution{},
_readingDirection{ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT },
@ -104,6 +109,11 @@ CustomTextLayout::CustomTextLayout(gsl::not_null<IDWriteFactory1*> const factory
RETURN_IF_FAILED(_AnalyzeRuns());
RETURN_IF_FAILED(_ShapeGlyphRuns());
RETURN_IF_FAILED(_CorrectGlyphRuns());
// Correcting box drawing has to come after both font fallback and
// the glyph run advance correction (which will apply a font size scaling factor).
// We need to know all the proposed X and Y dimension metrics to get this right.
RETURN_IF_FAILED(_CorrectBoxDrawing());
RETURN_IF_FAILED(_DrawGlyphRuns(clientDrawingContext, renderer, { originX, originY }));
return S_OK;
@ -768,7 +778,7 @@ CATCH_RETURN();
DWRITE_MEASURING_MODE_NATURAL,
&glyphRun,
&glyphRunDescription,
nullptr));
run.drawingEffect.Get()));
// Either way, we should be at this point by the end of writing this sequence,
// whether it was LTR or RTL.
@ -1112,6 +1122,7 @@ CATCH_RETURN();
// - textLength - the length of the substring operation
// - font - the font that applies to the substring range
// - scale - the scale of the font to apply
// Return Value:
// - S_OK or appropriate STL/GSL failure code.
[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetMappedFont(UINT32 textPosition,
UINT32 textLength,
@ -1148,6 +1159,386 @@ CATCH_RETURN();
return S_OK;
}
#pragma endregion
#pragma region internal methods for mimicking text analyzer to identify and split box drawing regions
// Routine Description:
// - Helper method to detect if something is a box drawing character.
// Arguments:
// - wch - Specific character.
// Return Value:
// - True if box drawing. False otherwise.
static constexpr bool _IsBoxDrawingCharacter(const wchar_t wch)
{
if (wch >= 0x2500 && wch <= 0x259F)
{
return true;
}
return false;
}
// Routine Description:
// - Corrects all runs for box drawing characteristics. Splits as it walks, if it must.
// If there are fallback fonts, this must happen after that's analyzed and after the
// advances are corrected so we can use the font size scaling factors to determine
// the appropriate layout heights for the correction scale/translate matrix.
// Arguments:
// - <none> - Operates on all runs then orders them back up.
// Return Value:
// - S_OK, STL/GSL errors, or an E_ABORT from mathematical failures.
[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_CorrectBoxDrawing() noexcept
try
{
RETURN_IF_FAILED(_AnalyzeBoxDrawing(this, 0, gsl::narrow<UINT32>(_text.size())));
_OrderRuns();
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - An analyzer to walk through the source text and search for runs of box drawing characters.
// It will segment the text into runs of those characters and mark them for special drawing, if necessary.
// Arguments:
// - source - a text analysis source to retrieve substrings of the text to be analyzed
// - textPosition - the index to start the substring operation
// - textLength - the length of the substring operation
// Result:
// - S_OK, STL/GSL errors, or an E_ABORT from mathematical failures.
[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_AnalyzeBoxDrawing(gsl::not_null<IDWriteTextAnalysisSource*> const source,
UINT32 textPosition,
UINT32 textLength)
try
{
// Walk through and analyze the entire string
while (textLength > 0)
{
// Get the substring of text remaining to analyze.
const WCHAR* text;
UINT32 length;
RETURN_IF_FAILED(source->GetTextAtPosition(textPosition, &text, &length));
// Put it into a view for iterator convenience.
const std::wstring_view str(text, length);
// Find the first box drawing character in the string from the front.
const auto firstBox = std::find_if(str.cbegin(), str.cend(), _IsBoxDrawingCharacter);
// If we found no box drawing characters, move on with life.
if (firstBox == str.cend())
{
return S_OK;
}
// If we found one, keep looking forward until we find NOT a box drawing character.
else
{
// Find the last box drawing character.
const auto lastBox = std::find_if(firstBox, str.cend(), [](wchar_t wch) { return !_IsBoxDrawingCharacter(wch); });
// Skip distance is how far we had to move forward to find a box.
const auto firstBoxDistance = std::distance(str.cbegin(), firstBox);
UINT32 skipDistance;
RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(firstBoxDistance).AssignIfValid(&skipDistance));
// Move the position/length of the outside counters up to the part where boxes start.
textPosition += skipDistance;
textLength -= skipDistance;
// Run distance is how many box characters in a row there are.
const auto runDistance = std::distance(firstBox, lastBox);
UINT32 mappedLength;
RETURN_HR_IF(E_ABORT, !base::MakeCheckedNum(runDistance).AssignIfValid(&mappedLength));
// Split the run and set the box effect on this segment of the run
RETURN_IF_FAILED(_SetBoxEffect(textPosition, mappedLength));
// Move us forward for the outer loop to continue scanning after this point.
textPosition += mappedLength;
textLength -= mappedLength;
}
}
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - A callback to split a run and apply box drawing characteristics to just that sub-run.
// Arguments:
// - textPosition - the index to start the substring operation
// - textLength - the length of the substring operation
// Return Value:
// - S_OK or appropriate STL/GSL failure code.
[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::_SetBoxEffect(UINT32 textPosition,
UINT32 textLength)
try
{
_SetCurrentRun(textPosition);
_SplitCurrentRun(textPosition);
while (textLength > 0)
{
auto& run = _FetchNextRun(textLength);
if (run.fontFace == _font)
{
run.drawingEffect = _boxDrawingEffect;
}
else
{
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> eff;
RETURN_IF_FAILED(s_CalculateBoxEffect(_format.Get(), _width, run.fontFace.Get(), run.fontScale, &eff));
// store data in the run
run.drawingEffect = std::move(eff);
}
}
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - Calculates the box drawing scale/translate matrix values to fit a box glyph into the cell as perfectly as possible.
// Arguments:
// - format - Text format used to determine line spacing (height including ascent & descent) as calculated from the base font.
// - widthPixels - The pixel width of the available cell.
// - face - The font face that is currently being used, may differ from the base font from the layout.
// - fontScale - if the given font face is going to be scaled versus the format, we need to know so we can compensate for that. pass 1.0f for no scaling.
// - effect - Receives the effect to apply to box drawing characters. If no effect is received, special treatment isn't required.
// Return Value:
// - S_OK, GSL/WIL errors, DirectWrite errors, or math errors.
[[nodiscard]] HRESULT STDMETHODCALLTYPE CustomTextLayout::s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept
try
{
// Check for bad in parameters.
RETURN_HR_IF(E_INVALIDARG, !format);
RETURN_HR_IF(E_INVALIDARG, !face);
// Check the out parameter and fill it up with null.
RETURN_HR_IF(E_INVALIDARG, !effect);
*effect = nullptr;
// The format is based around the main font that was specified by the user.
// We need to know its size as well as the final spacing that was calculated around
// it when it was first selected to get an idea of how large the bounding box is.
const auto fontSize = format->GetFontSize();
DWRITE_LINE_SPACING_METHOD spacingMethod;
float lineSpacing; // total height of the cells
float baseline; // vertical position counted down from the top where the characters "sit"
RETURN_IF_FAILED(format->GetLineSpacing(&spacingMethod, &lineSpacing, &baseline));
const float ascentPixels = baseline;
const float descentPixels = lineSpacing - baseline;
// We need this for the designUnitsPerEm which will be required to move back and forth between
// Design Units and Pixels. I'll elaborate below.
DWRITE_FONT_METRICS1 fontMetrics;
face->GetMetrics(&fontMetrics);
// If we had font fallback occur, the size of the font given to us (IDWriteFontFace1) can be different
// than the font size used for the original format (IDWriteTextFormat).
const auto scaledFontSize = fontScale * fontSize;
// This is Unicode FULL BLOCK U+2588.
// We presume that FULL BLOCK should be filling its entire cell in all directions so it should provide a good basis
// in knowing exactly where to touch every single edge.
// We're also presuming that the other box/line drawing glyphs were authored in this font to perfectly inscribe
// inside of FULL BLOCK, with the same left/top/right/bottom bearings so they would look great when drawn adjacent.
const UINT32 blockCodepoint = L'\x2588';
// Get the index of the block out of the font.
UINT16 glyphIndex;
RETURN_IF_FAILED(face->GetGlyphIndicesW(&blockCodepoint, 1, &glyphIndex));
// If it was 0, it wasn't found in the font. We're going to try again with
// Unicode BOX DRAWINGS LIGHT VERTICAL AND HORIZONTAL U+253C which should be touching
// all the edges of the possible rectangle, much like a full block should.
if (glyphIndex == 0)
{
const UINT32 alternateCp = L'\x253C';
RETURN_IF_FAILED(face->GetGlyphIndicesW(&alternateCp, 1, &glyphIndex));
}
// If we still didn't find the glyph index, we haven't implemented any further logic to figure out the box dimensions.
// So we're just going to leave successfully as is and apply no scaling factor. It might look not-right, but it won't
// stop the rendering pipeline.
RETURN_HR_IF(S_FALSE, glyphIndex == 0);
// Get the metrics of the given glyph, which we're going to treat as the outline box in which all line/block drawing
// glyphs will be inscribed within, perfectly touching each edge as to align when two cells meet.
DWRITE_GLYPH_METRICS boxMetrics = { 0 };
RETURN_IF_FAILED(face->GetDesignGlyphMetrics(&glyphIndex, 1, &boxMetrics));
// NOTE: All metrics we receive from DWRITE are going to be in "design units" which are a somewhat agnostic
// way of describing proportions.
// Converting back and forth between real pixels and design units is possible using
// any font's specific fontSize and the designUnitsPerEm FONT_METRIC value.
//
// Here's what to know about the boxMetrics:
//
//
//
// topLeft --> +--------------------------------+ ---
// | ^ | |
// | | topSide | |
// | | Bearing | |
// | v | |
// | +-----------------+ | |
// | | | | |
// | | | | | a
// | | | | | d
// | | | | | v
// +<---->+ | | | a
// | | | | | n
// | left | | | | c
// | Side | | | | e
// | Bea- | | | | H
// | ring | | right | | e
// vertical | | | Side | | i
// OriginY --> x | | Bea- | | g
// | | | ring | | h
// | | | | | t
// | | +<----->+ |
// | +-----------------+ | |
// | ^ | |
// | bottomSide | | |
// | Bearing | | |
// | v | |
// +--------------------------------+ ---
//
//
// | |
// +--------------------------------+
// | advanceWidth |
//
//
// NOTE: The bearings can be negative, in which case it is specifying that the glyphs overhang the box
// as defined by the advanceHeight/width.
// See also: https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics
// The scale is a multiplier and the translation is addition. So *1 and +0 will mean nothing happens.
const float defaultBoxVerticalScaleFactor = 1.0f;
float boxVerticalScaleFactor = defaultBoxVerticalScaleFactor;
const float defaultBoxVerticalTranslation = 0.0f;
float boxVerticalTranslation = defaultBoxVerticalTranslation;
{
// First, find the dimensions of the glyph representing our fully filled box.
// Ascent is how far up from the baseline we'll draw.
// verticalOriginY is the measure from the topLeft corner of the bounding box down to where
// the glyph's version of the baseline is.
// topSideBearing is how much "gap space" is left between that topLeft and where the glyph
// starts drawing. Subtract the gap space to find how far is drawn upward from baseline.
const auto boxAscentDesignUnits = boxMetrics.verticalOriginY - boxMetrics.topSideBearing;
// Descent is how far down from the baseline we'll draw.
// advanceHeight is the total height of the drawn bounding box.
// verticalOriginY is how much was given to the ascent, so subtract that out.
// What remains is then the descent value. Remove the
// bottomSideBearing as the "gap space" on the bottom to find how far is drawn downward from baseline.
const auto boxDescentDesignUnits = boxMetrics.advanceHeight - boxMetrics.verticalOriginY - boxMetrics.bottomSideBearing;
// The height, then, of the entire box is just the sum of the ascent above the baseline and the descent below.
const auto boxHeightDesignUnits = boxAscentDesignUnits + boxDescentDesignUnits;
// Second, find the dimensions of the cell we're going to attempt to fit within.
// We know about the exact ascent/descent units in pixels as calculated when we chose a font and
// adjusted the ascent/descent for a nice perfect baseline and integer total height.
// All we need to do is adapt it into Design Units so it meshes nicely with the Design Units above.
// Use the formula: Pixels * Design Units Per Em / Font Size = Design Units
const auto cellAscentDesignUnits = ascentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellDescentDesignUnits = descentPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellHeightDesignUnits = cellAscentDesignUnits + cellDescentDesignUnits;
// OK, now do a few checks. If the drawn box touches the top and bottom of the cell
// and the box is overall tall enough, then we'll not bother adjusting.
// We will presume the font author has set things as they wish them to be.
const auto boxTouchesCellTop = boxAscentDesignUnits >= cellAscentDesignUnits;
const auto boxTouchesCellBottom = boxDescentDesignUnits >= cellDescentDesignUnits;
const auto boxIsTallEnoughForCell = boxHeightDesignUnits >= cellHeightDesignUnits;
// If not...
if (!(boxTouchesCellTop && boxTouchesCellBottom && boxIsTallEnoughForCell))
{
// Find a scaling factor that will make the total height drawn of this box
// perfectly fit the same number of design units as the cell.
// Since scale factor is a multiplier, it doesn't matter that this is design units.
// The fraction between the two heights in pixels should be exactly the same
// (which is what will matter when we go to actually render it... the pixels that is.)
// Don't scale below 1.0. If it'd shrink, just center it at the prescribed scale.
boxVerticalScaleFactor = std::max(cellHeightDesignUnits / boxHeightDesignUnits, 1.0f);
// The box as scaled might be hanging over the top or bottom of the cell (or both).
// We find out the amount of overhang/underhang on both the top and the bottom.
const auto extraAscent = boxAscentDesignUnits * boxVerticalScaleFactor - cellAscentDesignUnits;
const auto extraDescent = boxDescentDesignUnits * boxVerticalScaleFactor - cellDescentDesignUnits;
// This took a bit of time and effort and it's difficult to put into words, but here goes.
// We want the average of the two magnitudes to find out how much to "take" from one and "give"
// to the other such that both are equal. We presume the glyphs are designed to be drawn
// centered in their box vertically to look good.
// The ordering around subtraction is required to ensure that the direction is correct with a negative
// translation moving up (taking excess descent and adding to ascent) and positive is the opposite.
const auto boxVerticalTranslationDesignUnits = (extraAscent - extraDescent) / 2;
// The translation is just a raw movement of pixels up or down. Since we were working in Design Units,
// we need to run the opposite algorithm shown above to go from Design Units to Pixels.
boxVerticalTranslation = boxVerticalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
}
}
// The horizontal adjustments follow the exact same logic as the vertical ones.
const float defaultBoxHorizontalScaleFactor = 1.0f;
float boxHorizontalScaleFactor = defaultBoxHorizontalScaleFactor;
const float defaultBoxHorizontalTranslation = 0.0f;
float boxHorizontalTranslation = defaultBoxHorizontalTranslation;
{
// This is the only difference. We don't have a horizontalOriginX from the metrics.
// However, https://docs.microsoft.com/en-us/windows/win32/api/dwrite/ns-dwrite-dwrite_glyph_metrics says
// the X coordinate is specified by half the advanceWidth to the right of the horizontalOrigin.
// So we'll use that as the "center" and apply it the role that verticalOriginY had above.
const auto boxCenterDesignUnits = boxMetrics.advanceWidth / 2;
const auto boxLeftDesignUnits = boxCenterDesignUnits - boxMetrics.leftSideBearing;
const auto boxRightDesignUnits = boxMetrics.advanceWidth - boxMetrics.rightSideBearing - boxCenterDesignUnits;
const auto boxWidthDesignUnits = boxLeftDesignUnits + boxRightDesignUnits;
const auto cellWidthDesignUnits = widthPixels * fontMetrics.designUnitsPerEm / scaledFontSize;
const auto cellLeftDesignUnits = cellWidthDesignUnits / 2;
const auto cellRightDesignUnits = cellLeftDesignUnits;
const auto boxTouchesCellLeft = boxLeftDesignUnits >= cellLeftDesignUnits;
const auto boxTouchesCellRight = boxRightDesignUnits >= cellRightDesignUnits;
const auto boxIsWideEnoughForCell = boxWidthDesignUnits >= cellWidthDesignUnits;
if (!(boxTouchesCellLeft && boxTouchesCellRight && boxIsWideEnoughForCell))
{
boxHorizontalScaleFactor = std::max(cellWidthDesignUnits / boxWidthDesignUnits, 1.0f);
const auto extraLeft = boxLeftDesignUnits * boxHorizontalScaleFactor - cellLeftDesignUnits;
const auto extraRight = boxRightDesignUnits * boxHorizontalScaleFactor - cellRightDesignUnits;
const auto boxHorizontalTranslationDesignUnits = (extraLeft - extraRight) / 2;
boxHorizontalTranslation = boxHorizontalTranslationDesignUnits * scaledFontSize / fontMetrics.designUnitsPerEm;
}
}
// If we set anything, make a drawing effect. Otherwise, there isn't one.
if (defaultBoxVerticalScaleFactor != boxVerticalScaleFactor ||
defaultBoxVerticalTranslation != boxVerticalTranslation ||
defaultBoxHorizontalScaleFactor != boxHorizontalScaleFactor ||
defaultBoxHorizontalTranslation != boxHorizontalTranslation)
{
// OK, make the object that will represent our effect, stuff the metrics into it, and return it.
RETURN_IF_FAILED(WRL::MakeAndInitialize<BoxDrawingEffect>(effect, boxVerticalScaleFactor, boxVerticalTranslation, boxHorizontalScaleFactor, boxHorizontalTranslation));
}
return S_OK;
}
CATCH_RETURN()
#pragma endregion

View File

@ -10,6 +10,7 @@
#include <wrl/client.h>
#include <wrl/implements.h>
#include "BoxDrawingEffect.h"
#include "../inc/Cluster.hpp"
namespace Microsoft::Console::Render
@ -24,7 +25,8 @@ namespace Microsoft::Console::Render
gsl::not_null<IDWriteTextFormat*> const format,
gsl::not_null<IDWriteFontFace1*> const font,
const std::basic_string_view<::Microsoft::Console::Render::Cluster> clusters,
size_t const width);
size_t const width,
IBoxDrawingEffect* const boxEffect);
[[nodiscard]] HRESULT STDMETHODCALLTYPE GetColumns(_Out_ UINT32* columns);
@ -64,6 +66,8 @@ namespace Microsoft::Console::Render
UINT32 textLength,
_In_ IDWriteNumberSubstitution* numberSubstitution) override;
[[nodiscard]] static HRESULT STDMETHODCALLTYPE s_CalculateBoxEffect(IDWriteTextFormat* format, size_t widthPixels, IDWriteFontFace1* face, float fontScale, IBoxDrawingEffect** effect) noexcept;
protected:
// A single contiguous run of characters containing the same analysis results.
struct Run
@ -78,7 +82,8 @@ namespace Microsoft::Console::Render
isNumberSubstituted(),
isSideways(),
fontFace{ nullptr },
fontScale{ 1.0 }
fontScale{ 1.0 },
drawingEffect{ nullptr }
{
}
@ -92,6 +97,7 @@ namespace Microsoft::Console::Render
bool isSideways;
::Microsoft::WRL::ComPtr<IDWriteFontFace1> fontFace;
FLOAT fontScale;
::Microsoft::WRL::ComPtr<IUnknown> drawingEffect;
inline bool ContainsTextPosition(UINT32 desiredTextPosition) const noexcept
{
@ -125,11 +131,15 @@ namespace Microsoft::Console::Render
[[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeFontFallback(IDWriteTextAnalysisSource* const source, UINT32 textPosition, UINT32 textLength);
[[nodiscard]] HRESULT STDMETHODCALLTYPE _SetMappedFont(UINT32 textPosition, UINT32 textLength, IDWriteFont* const font, FLOAT const scale);
[[nodiscard]] HRESULT STDMETHODCALLTYPE _AnalyzeBoxDrawing(gsl::not_null<IDWriteTextAnalysisSource*> const source, UINT32 textPosition, UINT32 textLength);
[[nodiscard]] HRESULT STDMETHODCALLTYPE _SetBoxEffect(UINT32 textPosition, UINT32 textLength);
[[nodiscard]] HRESULT _AnalyzeRuns() noexcept;
[[nodiscard]] HRESULT _ShapeGlyphRuns() noexcept;
[[nodiscard]] HRESULT _ShapeGlyphRun(const UINT32 runIndex, UINT32& glyphStart) noexcept;
[[nodiscard]] HRESULT _CorrectGlyphRuns() noexcept;
[[nodiscard]] HRESULT _CorrectGlyphRun(const UINT32 runIndex) noexcept;
[[nodiscard]] HRESULT _CorrectBoxDrawing() noexcept;
[[nodiscard]] HRESULT _DrawGlyphRuns(_In_opt_ void* clientDrawingContext,
IDWriteTextRenderer* renderer,
const D2D_POINT_2F origin) noexcept;
@ -148,6 +158,9 @@ namespace Microsoft::Console::Render
// DirectWrite font face
const ::Microsoft::WRL::ComPtr<IDWriteFontFace1> _font;
// Box drawing effect
const ::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
// The text we're analyzing and processing into a layout
std::wstring _text;
std::vector<UINT16> _textClusterColumns;

View File

@ -245,7 +245,7 @@ using namespace Microsoft::Console::Render;
DWRITE_MEASURING_MODE measuringMode,
const DWRITE_GLYPH_RUN* glyphRun,
const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
IUnknown* /*clientDrawingEffect*/)
IUnknown* clientDrawingEffect)
{
// Color glyph rendering sourced from https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/DWriteColorGlyph
@ -373,7 +373,8 @@ using namespace Microsoft::Console::Render;
measuringMode,
glyphRun,
glyphRunDescription,
drawingContext->foregroundBrush));
drawingContext->foregroundBrush,
clientDrawingEffect));
}
else
{
@ -458,7 +459,8 @@ using namespace Microsoft::Console::Render;
measuringMode,
&colorRun->glyphRun,
colorRun->glyphRunDescription,
layerBrush));
layerBrush,
clientDrawingEffect));
}
break;
}
@ -474,7 +476,8 @@ using namespace Microsoft::Console::Render;
measuringMode,
glyphRun,
glyphRunDescription,
drawingContext->foregroundBrush));
drawingContext->foregroundBrush,
clientDrawingEffect));
}
return S_OK;
}
@ -485,7 +488,8 @@ using namespace Microsoft::Console::Render;
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
ID2D1Brush* brush)
ID2D1Brush* brush,
_In_opt_ IUnknown* clientDrawingEffect)
{
RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingContext);
RETURN_HR_IF_NULL(E_INVALIDARG, glyphRun);
@ -494,28 +498,39 @@ using namespace Microsoft::Console::Render;
::Microsoft::WRL::ComPtr<ID2D1DeviceContext> d2dContext;
RETURN_IF_FAILED(clientDrawingContext->renderTarget->QueryInterface(d2dContext.GetAddressOf()));
// If a special drawing effect was specified, see if we know how to deal with it.
if (clientDrawingEffect)
{
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> boxEffect;
if (SUCCEEDED(clientDrawingEffect->QueryInterface<IBoxDrawingEffect>(&boxEffect)))
{
return _DrawBoxRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription, boxEffect.Get());
}
//_DrawBasicGlyphRunManually(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription);
//_DrawGlowGlyphRun(clientDrawingContext, baselineOrigin, measuringMode, glyphRun, glyphRunDescription);
}
// If we get down here, there either was no special effect or we don't know what to do with it. Use the standard GlyphRun drawing.
// 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_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/) noexcept
[[nodiscard]] HRESULT CustomTextRenderer::_DrawBoxRunManually(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE /*measuringMode*/,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* /*glyphRunDescription*/,
_In_ IBoxDrawingEffect* clientDrawingEffect) noexcept
try
{
RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingContext);
RETURN_HR_IF_NULL(E_INVALIDARG, glyphRun);
RETURN_HR_IF_NULL(E_INVALIDARG, clientDrawingEffect);
// This is regular text but manually
::Microsoft::WRL::ComPtr<ID2D1Factory> d2dFactory;
clientDrawingContext->renderTarget->GetFactory(d2dFactory.GetAddressOf());
@ -537,17 +552,90 @@ using namespace Microsoft::Console::Render;
geometrySink->Close();
D2D1::Matrix3x2F const matrixAlign = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y);
// Can be used to see the dimensions of what is written.
/*D2D1_RECT_F bounds;
pathGeometry->GetBounds(D2D1::IdentityMatrix(), &bounds);*/
// The bounds here are going to be centered around the baseline of the font.
// That is, the DWRITE_GLYPH_METRICS property for this glyph's baseline is going
// to be at the 0 point in the Y direction when we receive the geometry.
// The ascent will go up negative from Y=0 and the descent will go down positive from Y=0.
// As for the horizontal direction, I didn't study this in depth, but it appears to always be
// positive X with both the left and right edges being positive and away from X=0.
// For one particular instance, we might ask for the geometry for a U+2588 box and see the bounds as:
//
// Top=
// -20.315
// -----------
// | |
// | |
// Left= | | Right=
// 13.859 | | 26.135
// | |
// Origin --> X | |
// (0,0) | |
// -----------
// Bottom=
// 5.955
// Dig out the box drawing effect parameters.
BoxScale scale;
RETURN_IF_FAILED(clientDrawingEffect->GetScale(&scale));
// The scale transform will inflate the entire geometry first.
// We want to do this before it moves out of its original location as generally our
// algorithms for fitting cells will blow up the glyph to the size it needs to be first and then
// nudge it into place with the translations.
const auto scaleTransform = D2D1::Matrix3x2F::Scale(scale.HorizontalScale, scale.VerticalScale);
// Now shift it all the way to where the baseline says it should be.
const auto baselineTransform = D2D1::Matrix3x2F::Translation(baselineOrigin.x, baselineOrigin.y);
// Finally apply the little "nudge" that we may have been directed to align it better with the cell.
const auto offsetTransform = D2D1::Matrix3x2F::Translation(scale.HorizontalTranslation, scale.VerticalTranslation);
// The order is important here. Scale it first, then slide it into place.
const auto matrixTransformation = scaleTransform * baselineTransform * offsetTransform;
::Microsoft::WRL::ComPtr<ID2D1TransformedGeometry> transformedGeometry;
d2dFactory->CreateTransformedGeometry(pathGeometry.Get(),
&matrixAlign,
&matrixTransformation,
transformedGeometry.GetAddressOf());
// Can be used to see the dimensions after translation.
/*D2D1_RECT_F boundsAfter;
transformedGeometry->GetBounds(D2D1::IdentityMatrix(), &boundsAfter);*/
// Compare this to the original bounds above to see what the matrix did.
// To make it useful, first visualize for yourself the pixel dimensions of the cell
// based on the baselineOrigin and the exact integer cell width and heights that we're storing.
// You'll also probably need the full-pixel ascent and descent because the point we're given
// is the baseline, not the top left corner of the cell as we're used to.
// Most of these metrics can be found in the initial font creation routines or in
// the line spacing applied to the text format (member variables on the renderer).
// baselineOrigin = (0, 567)
// fullPixelAscent = 39
// fullPixelDescent = 9
// cell dimensions = 26 x 48 (notice 48 height is 39 + 9 or ascent + descent)
// This means that our cell should be the rectangle
//
// T=528
// |-------|
// L=0 | |
// | |
// Baseline->x |
// Origin | | R=26
// |-------|
// B=576
//
// And we'll want to check that the bounds after transform will fit the glyph nicely inside
// this box.
// If not? We didn't do the scaling or translation correctly. Oops.
// Fill in the geometry. Don't outline, it can leave stuff outside the area we expect.
clientDrawingContext->renderTarget->FillGeometry(transformedGeometry.Get(), clientDrawingContext->foregroundBrush);
return S_OK;
}
CATCH_RETURN();
[[nodiscard]] HRESULT CustomTextRenderer::_DrawGlowGlyphRun(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,

View File

@ -4,6 +4,7 @@
#pragma once
#include <wrl/implements.h>
#include "BoxDrawingEffect.h"
namespace Microsoft::Console::Render
{
@ -98,13 +99,15 @@ namespace Microsoft::Console::Render
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
ID2D1Brush* brush);
ID2D1Brush* brush,
_In_opt_ IUnknown* clientDrawingEffect);
[[nodiscard]] HRESULT _DrawBasicGlyphRunManually(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription) noexcept;
[[nodiscard]] HRESULT _DrawBoxRunManually(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,
DWRITE_MEASURING_MODE measuringMode,
_In_ const DWRITE_GLYPH_RUN* glyphRun,
_In_opt_ const DWRITE_GLYPH_RUN_DESCRIPTION* glyphRunDescription,
_In_ IBoxDrawingEffect* clientDrawingEffect) noexcept;
[[nodiscard]] HRESULT _DrawGlowGlyphRun(DrawingContext* clientDrawingContext,
D2D1_POINT_2F baselineOrigin,

View File

@ -81,6 +81,7 @@ DxEngine::DxEngine() :
_backgroundColor{ 0 },
_selectionBackground{},
_glyphCell{},
_boxDrawingEffect{},
_haveDeviceResources{ false },
_retroTerminalEffects{ false },
_antialiasingMode{ D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE },
@ -1230,7 +1231,8 @@ try
_dwriteTextFormat.Get(),
_dwriteFontFace.Get(),
clusters,
_glyphCell.width());
_glyphCell.width(),
_boxDrawingEffect.Get());
// Get the baseline for this font as that's where we draw from
DWRITE_LINE_SPACING spacing;
@ -1606,6 +1608,9 @@ try
_glyphCell = fiFontInfo.GetSize();
// Calculate and cache the box effect for the base font. Scale is 1.0f because the base font is exactly the scale we want already.
RETURN_IF_FAILED(CustomTextLayout::s_CalculateBoxEffect(_dwriteTextFormat.Get(), _glyphCell.width(), _dwriteFontFace.Get(), 1.0f, &_boxDrawingEffect));
return S_OK;
}
CATCH_RETURN();
@ -1739,7 +1744,8 @@ try
_dwriteTextFormat.Get(),
_dwriteFontFace.Get(),
{ &cluster, 1 },
_glyphCell.width());
_glyphCell.width(),
_boxDrawingEffect.Get());
UINT32 columns = 0;
RETURN_IF_FAILED(layout.GetColumns(&columns));
@ -1990,6 +1996,9 @@ CATCH_RETURN();
INT32 advanceInDesignUnits;
THROW_IF_FAILED(face->GetDesignGlyphAdvances(1, &spaceGlyphIndex, &advanceInDesignUnits));
DWRITE_GLYPH_METRICS spaceMetrics = { 0 };
THROW_IF_FAILED(face->GetDesignGlyphMetrics(&spaceGlyphIndex, 1, &spaceMetrics));
// The math here is actually:
// Requested Size in Points * DPI scaling factor * Points to Pixels scaling factor.
// - DPI = dots per inch
@ -2029,6 +2038,10 @@ CATCH_RETURN();
const float ascent = (fontSize * fontMetrics.ascent) / fontMetrics.designUnitsPerEm;
const float descent = (fontSize * fontMetrics.descent) / fontMetrics.designUnitsPerEm;
// Get the gap.
const float gap = (fontSize * fontMetrics.lineGap) / fontMetrics.designUnitsPerEm;
const float halfGap = gap / 2;
// We're going to build a line spacing object here to track all of this data in our format.
DWRITE_LINE_SPACING lineSpacing = {};
lineSpacing.method = DWRITE_LINE_SPACING_METHOD_UNIFORM;
@ -2041,19 +2054,41 @@ CATCH_RETURN();
// and set the baseline to the full round pixel ascent value.
//
// For reference, for the letters "ag":
// aaaaaa ggggggg <===================================
// a g g | |
// aaaaa ggggg |<-ascent |
// a a g | |---- height
// aaaaa a gggggg <-------------------baseline |
// g g |<-descent |
// gggggg <===================================
// ...
// gggggg bottom of previous line
//
const auto fullPixelAscent = ceil(ascent);
const auto fullPixelDescent = ceil(descent);
// ----------------- <===========================================|
// | topSideBearing | 1/2 lineGap |
// aaaaaa ggggggg <-------------------------|-------------| |
// a g g | | |
// aaaaa ggggg |<-ascent | |
// a a g | | |---- lineHeight
// aaaaa a gggggg <----baseline, verticalOriginY----------|---|
// g g |<-descent | |
// gggggg <-------------------------|-------------| |
// | bottomSideBearing | 1/2 lineGap |
// ----------------- <===========================================|
//
// aaaaaa ggggggg top of next line
// ...
//
// Also note...
// We're going to add half the line gap to the ascent and half the line gap to the descent
// to ensure that the spacing is balanced vertically.
// Generally speaking, the line gap is added to the ascent by DirectWrite itself for
// horizontally drawn text which can place the baseline and glyphs "lower" in the drawing
// box than would be desired for proper alignment of things like line and box characters
// which will try to sit centered in the area and touch perfectly with their neighbors.
const auto fullPixelAscent = ceil(ascent + halfGap);
const auto fullPixelDescent = ceil(descent + halfGap);
lineSpacing.height = fullPixelAscent + fullPixelDescent;
lineSpacing.baseline = fullPixelAscent;
// According to MSDN (https://docs.microsoft.com/en-us/windows/win32/api/dwrite_3/ne-dwrite_3-dwrite_font_line_gap_usage)
// Setting "ENABLED" means we've included the line gapping in the spacing numbers given.
lineSpacing.fontLineGapUsage = DWRITE_FONT_LINE_GAP_USAGE_ENABLED;
// Create the font with the fractional pixel height size.
// It should have an integer pixel width by our math above.
// Then below, apply the line spacing to the format to position the floating point pixel height characters
@ -2084,7 +2119,7 @@ CATCH_RETURN();
// of hit testing math and other such multiplication/division.
COORD coordSize = { 0 };
coordSize.X = gsl::narrow<SHORT>(widthExact);
coordSize.Y = gsl::narrow<SHORT>(lineSpacing.height);
coordSize.Y = gsl::narrow_cast<SHORT>(lineSpacing.height);
// Unscaled is for the purposes of re-communicating this font back to the renderer again later.
// As such, we need to give the same original size parameter back here without padding

View File

@ -134,6 +134,7 @@ namespace Microsoft::Console::Render
til::size _displaySizePixels;
til::size _glyphCell;
::Microsoft::WRL::ComPtr<IBoxDrawingEffect> _boxDrawingEffect;
D2D1_COLOR_F _defaultForegroundColor;
D2D1_COLOR_F _defaultBackgroundColor;

View File

@ -0,0 +1,20 @@
import "oaidl.idl";
import "ocidl.idl";
typedef struct BoxScale
{
float VerticalScale;
float VerticalTranslation;
float HorizontalScale;
float HorizontalTranslation;
} BoxScale;
[
uuid("C164926F-1A4D-470D-BB8A-3D2CC4B035E4"),
object,
local
]
interface IBoxDrawingEffect : IUnknown
{
HRESULT GetScale([out] BoxScale* scale);
};

View File

@ -6,10 +6,16 @@
<RootNamespace>dx</RootNamespace>
<ProjectName>RendererDx</ProjectName>
<TargetName>ConRenderDx</TargetName>
<ConfigurationType>StaticLibrary</ConfigurationType>
<ConfigurationType>StaticLibrary</ConfigurationType>
</PropertyGroup>
<ItemDefinitionGroup>
<Midl>
<OutputDirectory>$(SolutionDir)src\renderer\dx\</OutputDirectory>
</Midl>
</ItemDefinitionGroup>
<Import Project="$(SolutionDir)src\common.build.pre.props" />
<ItemGroup>
<ClCompile Include="..\BoxDrawingEffect.cpp" />
<ClCompile Include="..\CustomTextLayout.cpp" />
<ClCompile Include="..\CustomTextRenderer.cpp" />
<ClCompile Include="..\precomp.cpp">
@ -18,6 +24,7 @@
<ClCompile Include="..\DxRenderer.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\BoxDrawingEffect.h" />
<ClInclude Include="..\CustomTextLayout.h" />
<ClInclude Include="..\CustomTextRenderer.h" />
<ClInclude Include="..\precomp.h" />
@ -25,6 +32,9 @@
<ClInclude Include="..\ScreenPixelShader.h" />
<ClInclude Include="..\ScreenVertexShader.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="..\IBoxDrawingEffect.idl" />
</ItemGroup>
<!-- Careful reordering these. Some default props (contained in these files) are order sensitive. -->
<Import Project="$(SolutionDir)src\common.build.post.props" />
</Project>

View File

@ -8,6 +8,7 @@
<ClCompile Include="..\CustomTextRenderer.cpp" />
<ClCompile Include="..\precomp.cpp" />
<ClCompile Include="..\DxRenderer.cpp" />
<ClCompile Include="..\BoxDrawingEffect.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="..\CustomTextLayout.h" />
@ -16,5 +17,9 @@
<ClInclude Include="..\DxRenderer.hpp" />
<ClInclude Include="..\ScreenPixelShader.h" />
<ClInclude Include="..\ScreenVertexShader.h" />
<ClInclude Include="..\BoxDrawingEffect.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="..\IBoxDrawingEffect.idl" />
</ItemGroup>
</Project>