2018-03-13 21:56:30 -05:00
// 17 january 2017
# include "uipriv_windows.hpp"
# include "draw.hpp"
# include "attrstr.hpp"
// TODO verify our renderer is correct, especially with regards to snapping
struct uiDrawTextLayout {
IDWriteTextFormat * format ;
IDWriteTextLayout * layout ;
2018-03-14 20:08:19 -05:00
std : : vector < struct drawTextBackgroundParams * > * backgroundParams ;
2018-03-13 21:56:30 -05:00
// for converting DirectWrite indices from/to byte offsets
size_t * u8tou16 ;
size_t nUTF8 ;
size_t * u16tou8 ;
size_t nUTF16 ;
} ;
// TODO copy notes about DirectWrite DIPs being equal to Direct2D DIPs here
// 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
# define pointSizeToDWriteSize(size) (size * (96.0 / 72.0))
// TODO move this and the layout creation stuff to attrstr.cpp like the other ports, or move the other ports into their drawtext.* files
// TODO should be const but then I can't operator[] on it; the real solution is to find a way to do designated array initializers in C++11 but I do not know enough C++ voodoo to make it work (it is possible but no one else has actually done it before)
static std : : map < uiDrawTextAlign , DWRITE_TEXT_ALIGNMENT > dwriteAligns = {
{ uiDrawTextAlignLeft , DWRITE_TEXT_ALIGNMENT_LEADING } ,
{ uiDrawTextAlignCenter , DWRITE_TEXT_ALIGNMENT_CENTER } ,
{ uiDrawTextAlignRight , DWRITE_TEXT_ALIGNMENT_TRAILING } ,
} ;
uiDrawTextLayout * uiDrawNewTextLayout ( uiDrawTextLayoutParams * p )
{
uiDrawTextLayout * tl ;
WCHAR * wDefaultFamily ;
DWRITE_WORD_WRAPPING wrap ;
FLOAT maxWidth ;
HRESULT hr ;
2018-04-15 17:12:58 -05:00
tl = uiprivNew ( uiDrawTextLayout ) ;
2018-03-13 21:56:30 -05:00
wDefaultFamily = toUTF16 ( p - > DefaultFont - > Family ) ;
hr = dwfactory - > CreateTextFormat (
wDefaultFamily , NULL ,
uiprivWeightToDWriteWeight ( p - > DefaultFont - > Weight ) ,
uiprivItalicToDWriteStyle ( p - > DefaultFont - > Italic ) ,
uiprivStretchToDWriteStretch ( p - > DefaultFont - > Stretch ) ,
pointSizeToDWriteSize ( p - > DefaultFont - > Size ) ,
// 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?
L " " ,
& ( tl - > format ) ) ;
2018-04-15 17:12:58 -05:00
uiprivFree ( wDefaultFamily ) ;
2018-03-13 21:56:30 -05:00
if ( hr ! = S_OK )
logHRESULT ( L " error creating IDWriteTextFormat " , hr ) ;
hr = tl - > format - > SetTextAlignment ( dwriteAligns [ p - > Align ] ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error applying text layout alignment " , hr ) ;
hr = dwfactory - > CreateTextLayout (
2018-03-17 14:49:00 -05:00
( const WCHAR * ) uiprivAttributedStringUTF16String ( p - > String ) , uiprivAttributedStringUTF16Len ( p - > String ) ,
2018-03-13 21:56:30 -05:00
tl - > format ,
// FLOAT is float, not double, so this should work... TODO
FLT_MAX , FLT_MAX ,
& ( tl - > layout ) ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error creating IDWriteTextLayout " , hr ) ;
// and set the width
// this is the only wrapping mode (apart from "no wrap") available prior to Windows 8.1 (TODO verify this fact) (TODO this should be the default anyway)
wrap = DWRITE_WORD_WRAPPING_WRAP ;
maxWidth = ( FLOAT ) ( p - > Width ) ;
if ( p - > Width < 0 ) {
// TODO is this wrapping juggling even necessary?
wrap = DWRITE_WORD_WRAPPING_NO_WRAP ;
// setting the max width in this case technically isn't needed since the wrap mode will simply ignore the max width, but let's do it just to be safe
maxWidth = FLT_MAX ; // see TODO above
}
hr = tl - > layout - > SetWordWrapping ( wrap ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error setting IDWriteTextLayout word wrapping mode " , hr ) ;
hr = tl - > layout - > SetMaxWidth ( maxWidth ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error setting IDWriteTextLayout max layout width " , hr ) ;
2018-03-17 14:49:00 -05:00
uiprivAttributedStringApplyAttributesToDWriteTextLayout ( p , tl - > layout , & ( tl - > backgroundParams ) ) ;
2018-03-13 21:56:30 -05:00
// and finally copy the UTF-8/UTF-16 index conversion tables
2018-03-17 14:49:00 -05:00
tl - > u8tou16 = uiprivAttributedStringCopyUTF8ToUTF16Table ( p - > String , & ( tl - > nUTF8 ) ) ;
tl - > u16tou8 = uiprivAttributedStringCopyUTF16ToUTF8Table ( p - > String , & ( tl - > nUTF16 ) ) ;
2018-03-13 21:56:30 -05:00
return tl ;
}
void uiDrawFreeTextLayout ( uiDrawTextLayout * tl )
{
2018-04-15 17:12:58 -05:00
uiprivFree ( tl - > u16tou8 ) ;
uiprivFree ( tl - > u8tou16 ) ;
2018-03-17 14:49:00 -05:00
for ( auto p : * ( tl - > backgroundParams ) )
2018-03-14 20:08:19 -05:00
uiprivFree ( p ) ;
delete tl - > backgroundParams ;
2018-03-13 21:56:30 -05:00
tl - > layout - > Release ( ) ;
tl - > format - > Release ( ) ;
2018-04-15 17:12:58 -05:00
uiprivFree ( tl ) ;
2018-03-13 21:56:30 -05:00
}
2018-03-14 20:08:19 -05:00
// TODO make this shared code somehow
2018-03-13 21:56:30 -05:00
static HRESULT mkSolidBrush ( ID2D1RenderTarget * rt , double r , double g , double b , double a , ID2D1SolidColorBrush * * brush )
{
D2D1_BRUSH_PROPERTIES props ;
D2D1_COLOR_F color ;
ZeroMemory ( & props , sizeof ( D2D1_BRUSH_PROPERTIES ) ) ;
props . opacity = 1.0 ;
// identity matrix
props . transform . _11 = 1 ;
props . transform . _22 = 1 ;
color . r = r ;
color . g = g ;
color . b = b ;
color . a = a ;
return rt - > CreateSolidColorBrush (
& color ,
& props ,
brush ) ;
}
static ID2D1SolidColorBrush * mustMakeSolidBrush ( ID2D1RenderTarget * rt , double r , double g , double b , double a )
{
ID2D1SolidColorBrush * brush ;
HRESULT hr ;
hr = mkSolidBrush ( rt , r , g , b , a , & brush ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error creating solid brush " , hr ) ;
return brush ;
}
// some of the stuff we want to do isn't possible with what DirectWrite provides itself; we need to do it ourselves
drawingEffectsAttr : : drawingEffectsAttr ( void )
{
this - > refcount = 1 ;
this - > hasColor = false ;
this - > hasUnderline = false ;
this - > hasUnderlineColor = false ;
}
2018-03-17 14:49:00 -05:00
HRESULT STDMETHODCALLTYPE drawingEffectsAttr : : QueryInterface ( REFIID riid , void * * ppvObject )
2018-03-13 21:56:30 -05:00
{
if ( ppvObject = = NULL )
return E_POINTER ;
if ( riid = = IID_IUnknown ) {
this - > AddRef ( ) ;
* ppvObject = this ;
return S_OK ;
}
* ppvObject = NULL ;
return E_NOINTERFACE ;
}
2018-03-17 14:49:00 -05:00
ULONG STDMETHODCALLTYPE drawingEffectsAttr : : AddRef ( void )
2018-03-13 21:56:30 -05:00
{
this - > refcount + + ;
return this - > refcount ;
}
2018-03-17 14:49:00 -05:00
ULONG STDMETHODCALLTYPE drawingEffectsAttr : : Release ( void )
2018-03-13 21:56:30 -05:00
{
this - > refcount - - ;
if ( this - > refcount = = 0 ) {
delete this ;
return 0 ;
}
return this - > refcount ;
}
void drawingEffectsAttr : : setColor ( double r , double g , double b , double a )
{
this - > hasColor = true ;
this - > r = r ;
this - > g = g ;
this - > b = b ;
this - > a = a ;
}
void drawingEffectsAttr : : setUnderline ( uiUnderline u )
{
this - > hasUnderline = true ;
this - > u = u ;
}
void drawingEffectsAttr : : setUnderlineColor ( double r , double g , double b , double a )
{
this - > hasUnderlineColor = true ;
this - > ur = r ;
this - > ug = g ;
this - > ub = b ;
this - > ua = a ;
}
HRESULT drawingEffectsAttr : : mkColorBrush ( ID2D1RenderTarget * rt , ID2D1SolidColorBrush * * b )
{
if ( ! this - > hasColor ) {
* b = NULL ;
return S_OK ;
}
return mkSolidBrush ( rt , this - > r , this - > g , this - > b , this - > a , b ) ;
}
HRESULT drawingEffectsAttr : : underline ( uiUnderline * u )
{
if ( u = = NULL )
return E_POINTER ;
if ( ! this - > hasUnderline )
return E_UNEXPECTED ;
* u = this - > u ;
return S_OK ;
}
HRESULT drawingEffectsAttr : : mkUnderlineBrush ( ID2D1RenderTarget * rt , ID2D1SolidColorBrush * * b )
{
if ( ! this - > hasUnderlineColor ) {
* b = NULL ;
return S_OK ;
}
return mkSolidBrush ( rt , this - > ur , this - > ug , this - > ub , this - > ua , b ) ;
}
// this is based on http://www.charlespetzold.com/blog/2014/01/Character-Formatting-Extensions-with-DirectWrite.html
class textRenderer : public IDWriteTextRenderer {
ULONG refcount ;
ID2D1RenderTarget * rt ;
BOOL snap ;
ID2D1SolidColorBrush * black ;
public :
textRenderer ( ID2D1RenderTarget * rt , BOOL snap , ID2D1SolidColorBrush * black )
{
this - > refcount = 1 ;
this - > rt = rt ;
this - > snap = snap ;
this - > black = black ;
}
// IUnknown
virtual HRESULT STDMETHODCALLTYPE QueryInterface ( REFIID riid , void * * ppvObject )
{
if ( ppvObject = = NULL )
return E_POINTER ;
if ( riid = = IID_IUnknown | |
riid = = __uuidof ( IDWritePixelSnapping ) | |
riid = = __uuidof ( IDWriteTextRenderer ) ) {
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 ;
}
// IDWritePixelSnapping
virtual HRESULT STDMETHODCALLTYPE GetCurrentTransform ( void * clientDrawingContext , DWRITE_MATRIX * transform )
{
D2D1_MATRIX_3X2_F d2dtf ;
if ( transform = = NULL )
return E_POINTER ;
this - > rt - > GetTransform ( & d2dtf ) ;
transform - > m11 = d2dtf . _11 ;
transform - > m12 = d2dtf . _12 ;
transform - > m21 = d2dtf . _21 ;
transform - > m22 = d2dtf . _22 ;
transform - > dx = d2dtf . _31 ;
transform - > dy = d2dtf . _32 ;
return S_OK ;
}
virtual HRESULT STDMETHODCALLTYPE GetPixelsPerDip ( void * clientDrawingContext , FLOAT * pixelsPerDip )
{
FLOAT dpix , dpiy ;
if ( pixelsPerDip = = NULL )
return E_POINTER ;
this - > rt - > GetDpi ( & dpix , & dpiy ) ;
* pixelsPerDip = dpix / 96 ;
return S_OK ;
}
virtual HRESULT STDMETHODCALLTYPE IsPixelSnappingDisabled ( void * clientDrawingContext , BOOL * isDisabled )
{
if ( isDisabled = = NULL )
return E_POINTER ;
* isDisabled = ! this - > snap ;
return S_OK ;
}
// IDWriteTextRenderer
virtual HRESULT STDMETHODCALLTYPE DrawGlyphRun ( void * clientDrawingContext , FLOAT baselineOriginX , FLOAT baselineOriginY , DWRITE_MEASURING_MODE measuringMode , const DWRITE_GLYPH_RUN * glyphRun , const DWRITE_GLYPH_RUN_DESCRIPTION * glyphRunDescription , IUnknown * clientDrawingEffect )
{
D2D1_POINT_2F baseline ;
2018-03-17 14:49:00 -05:00
drawingEffectsAttr * dea = ( drawingEffectsAttr * ) clientDrawingEffect ;
2018-03-13 21:56:30 -05:00
ID2D1SolidColorBrush * brush ;
baseline . x = baselineOriginX ;
baseline . y = baselineOriginY ;
brush = NULL ;
if ( dea ! = NULL ) {
HRESULT hr ;
hr = dea - > mkColorBrush ( this - > rt , & brush ) ;
if ( hr ! = S_OK )
return hr ;
}
if ( brush = = NULL ) {
brush = this - > black ;
2018-03-17 14:49:00 -05:00
brush - > AddRef ( ) ;
2018-03-13 21:56:30 -05:00
}
this - > rt - > DrawGlyphRun (
baseline ,
glyphRun ,
brush ,
measuringMode ) ;
brush - > Release ( ) ;
return S_OK ;
}
virtual HRESULT STDMETHODCALLTYPE DrawInlineObject ( void * clientDrawingContext , FLOAT originX , FLOAT originY , IDWriteInlineObject * inlineObject , BOOL isSideways , BOOL isRightToLeft , IUnknown * clientDrawingEffect )
{
if ( inlineObject = = NULL )
return E_POINTER ;
return inlineObject - > Draw ( clientDrawingContext , this ,
originX , originY ,
isSideways , isRightToLeft ,
clientDrawingEffect ) ;
}
virtual HRESULT STDMETHODCALLTYPE DrawStrikethrough ( void * clientDrawingContext , FLOAT baselineOriginX , FLOAT baselineOriginY , const DWRITE_STRIKETHROUGH * strikethrough , IUnknown * clientDrawingEffect )
{
// we don't support strikethrough
return E_UNEXPECTED ;
}
2018-03-14 20:08:19 -05:00
// TODO clean this function up
2018-03-13 21:56:30 -05:00
virtual HRESULT STDMETHODCALLTYPE DrawUnderline ( void * clientDrawingContext , FLOAT baselineOriginX , FLOAT baselineOriginY , const DWRITE_UNDERLINE * underline , IUnknown * clientDrawingEffect )
{
drawingEffectsAttr * dea = ( drawingEffectsAttr * ) clientDrawingEffect ;
uiUnderline utype ;
ID2D1SolidColorBrush * brush ;
D2D1_RECT_F rect ;
D2D1 : : Matrix3x2F pixeltf ;
FLOAT dpix , dpiy ;
D2D1_POINT_2F pt ;
HRESULT hr ;
if ( underline = = NULL )
return E_POINTER ;
if ( dea = = NULL ) // we can only get here through an underline
return E_UNEXPECTED ;
hr = dea - > underline ( & utype ) ;
if ( hr ! = S_OK ) // we *should* only get here through an underline that's actually set...
return hr ;
hr = dea - > mkUnderlineBrush ( this - > rt , & brush ) ;
if ( hr ! = S_OK )
return hr ;
if ( brush = = NULL ) {
// TODO document this rule if not already done
hr = dea - > mkColorBrush ( this - > rt , & brush ) ;
if ( hr ! = S_OK )
return hr ;
}
if ( brush = = NULL ) {
brush = this - > black ;
2018-03-17 14:49:00 -05:00
brush - > AddRef ( ) ;
2018-03-13 21:56:30 -05:00
}
rect . left = baselineOriginX ;
rect . top = baselineOriginY + underline - > offset ;
rect . right = rect . left + underline - > width ;
rect . bottom = rect . top + underline - > thickness ;
switch ( utype ) {
2018-03-17 14:49:00 -05:00
case uiUnderlineSingle :
2018-03-13 21:56:30 -05:00
this - > rt - > FillRectangle ( & rect , brush ) ;
break ;
2018-03-17 14:49:00 -05:00
case uiUnderlineDouble :
2018-03-13 21:56:30 -05:00
// TODO do any of the matrix methods return errors?
// TODO standardize double-underline shape across platforms? wavy underline shape?
this - > rt - > GetTransform ( & pixeltf ) ;
this - > rt - > GetDpi ( & dpix , & dpiy ) ;
pixeltf = pixeltf * D2D1 : : Matrix3x2F : : Scale ( dpix / 96 , dpiy / 96 ) ;
pt . x = 0 ;
pt . y = rect . top ;
pt = pixeltf . TransformPoint ( pt ) ;
rect . top = ( FLOAT ) ( ( int ) ( pt . y + 0.5 ) ) ;
pixeltf . Invert ( ) ;
pt = pixeltf . TransformPoint ( pt ) ;
rect . top = pt . y ;
// first line
rect . top - = underline - > thickness ;
// and it seems we need to recompute this
rect . bottom = rect . top + underline - > thickness ;
this - > rt - > FillRectangle ( & rect , brush ) ;
// second line
rect . top + = 2 * underline - > thickness ;
rect . bottom = rect . top + underline - > thickness ;
this - > rt - > FillRectangle ( & rect , brush ) ;
break ;
2018-03-17 14:49:00 -05:00
case uiUnderlineSuggestion :
2018-03-13 21:56:30 -05:00
{ // TODO get rid of the extra block
// TODO properly clean resources on failure
// TODO use fully qualified C overloads for all methods
// TODO ensure all methods properly have errors handled
ID2D1PathGeometry * path ;
ID2D1GeometrySink * sink ;
double amplitude , period , xOffset , yOffset ;
double t ;
bool first = true ;
HRESULT hr ;
hr = d2dfactory - > CreatePathGeometry ( & path ) ;
if ( hr ! = S_OK )
return hr ;
hr = path - > Open ( & sink ) ;
if ( hr ! = S_OK )
return hr ;
amplitude = underline - > thickness ;
period = 5 * underline - > thickness ;
xOffset = baselineOriginX ;
yOffset = baselineOriginY + underline - > offset ;
for ( t = 0 ; t < underline - > width ; t + + ) {
double x , angle , y ;
D2D1_POINT_2F pt ;
x = t + xOffset ;
angle = 2 * uiPi * fmod ( x , period ) / period ;
y = amplitude * sin ( angle ) + yOffset ;
pt . x = x ;
pt . y = y ;
if ( first ) {
sink - > BeginFigure ( pt , D2D1_FIGURE_BEGIN_HOLLOW ) ;
first = false ;
} else
sink - > AddLine ( pt ) ;
}
sink - > EndFigure ( D2D1_FIGURE_END_OPEN ) ;
hr = sink - > Close ( ) ;
if ( hr ! = S_OK )
return hr ;
sink - > Release ( ) ;
this - > rt - > DrawGeometry ( path , brush , underline - > thickness ) ;
path - > Release ( ) ;
}
break ;
}
brush - > Release ( ) ;
return S_OK ;
}
} ;
// TODO this ignores clipping?
void uiDrawText ( uiDrawContext * c , uiDrawTextLayout * tl , double x , double y )
{
D2D1_POINT_2F pt ;
ID2D1SolidColorBrush * black ;
textRenderer * renderer ;
HRESULT hr ;
2018-03-14 20:08:19 -05:00
for ( auto p : * ( tl - > backgroundParams ) ) {
// TODO
}
2018-03-13 21:56:30 -05:00
// TODO document that fully opaque black is the default text color; figure out whether this is upheld in various scenarios on other platforms
// TODO figure out if this needs to be cleaned out
black = mustMakeSolidBrush ( c - > rt , 0.0 , 0.0 , 0.0 , 1.0 ) ;
# define renderD2D 0
# define renderOur 1
# if renderD2D
pt . x = x ;
pt . y = y ;
// TODO D2D1_DRAW_TEXT_OPTIONS_NO_SNAP?
// TODO D2D1_DRAW_TEXT_OPTIONS_CLIP?
// TODO LONGTERM when setting 8.1 as minimum (TODO verify), D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT?
// TODO what is our pixel snapping setting related to the OPTIONS enum values?
c - > rt - > DrawTextLayout ( pt , tl - > layout , black , D2D1_DRAW_TEXT_OPTIONS_NONE ) ;
# endif
# if renderD2D && renderOur
// draw ours semitransparent so we can check
// TODO get the actual color Charles Petzold uses and use that
black - > Release ( ) ;
black = mustMakeSolidBrush ( c - > rt , 1.0 , 0.0 , 0.0 , 0.75 ) ;
# endif
# if renderOur
renderer = new textRenderer ( c - > rt ,
TRUE , // TODO FALSE for no-snap?
black ) ;
hr = tl - > layout - > Draw ( NULL ,
renderer ,
x , y ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error drawing IDWriteTextLayout " , hr ) ;
renderer - > Release ( ) ;
# endif
black - > Release ( ) ;
}
// TODO for a single line the height includes the leading; should it? TextEdit on OS X always includes the leading and/or paragraph spacing, otherwise Klee won't work...
// TODO width does not include trailing whitespace
void uiDrawTextLayoutExtents ( uiDrawTextLayout * tl , double * width , double * height )
{
DWRITE_TEXT_METRICS metrics ;
HRESULT hr ;
hr = tl - > layout - > GetMetrics ( & metrics ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error getting IDWriteTextLayout layout metrics " , hr ) ;
* width = metrics . width ;
// TODO make sure the behavior of this on empty strings is the same on all platforms (ideally should be 0-width, line height-height; TODO note this in the docs too)
* height = metrics . height ;
}