2017-01-19 20:13:03 -06:00
// 17 january 2017
# include "uipriv_windows.hpp"
2017-01-20 02:24:06 -06:00
# include "draw.hpp"
2017-01-19 20:13:03 -06:00
2017-01-21 09:21:39 -06:00
// TODO
// - consider the warnings about antialiasing in the PadWrite sample
// - if that's not a problem, do we have overlapping rects in the hittest sample? I can't tell...
2017-02-06 17:38:44 -06:00
// - what happens if any nLines == 0?
2017-01-21 09:21:39 -06:00
2017-01-19 20:13:03 -06:00
struct uiDrawTextLayout {
IDWriteTextFormat * format ;
IDWriteTextLayout * layout ;
2017-01-20 02:24:06 -06:00
UINT32 nLines ;
2017-01-19 20:13:03 -06:00
struct lineInfo * lineInfo ;
2017-02-06 17:38:44 -06:00
// for converting DirectWrite indices from/to byte offsets
size_t * u8tou16 ;
size_t nUTF8 ;
2017-01-19 20:13:03 -06:00
size_t * u16tou8 ;
2017-02-06 17:38:44 -06:00
size_t nUTF16 ;
2017-01-19 20:13:03 -06:00
} ;
2017-01-20 02:24:06 -06:00
// TODO copy notes about DirectWrite DIPs being equal to Direct2D DIPs here
2017-01-19 20:13:03 -06:00
// 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))
2017-01-20 02:24:06 -06:00
// 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 < uiDrawTextItalic , DWRITE_FONT_STYLE > dwriteItalics = {
{ uiDrawTextItalicNormal , DWRITE_FONT_STYLE_NORMAL } ,
{ uiDrawTextItalicOblique , DWRITE_FONT_STYLE_OBLIQUE } ,
{ uiDrawTextItalicItalic , DWRITE_FONT_STYLE_ITALIC } ,
2017-01-19 20:13:03 -06:00
} ;
2017-01-20 02:24:06 -06:00
// 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 < uiDrawTextStretch , DWRITE_FONT_STRETCH > dwriteStretches = {
{ uiDrawTextStretchUltraCondensed , DWRITE_FONT_STRETCH_ULTRA_CONDENSED } ,
{ uiDrawTextStretchExtraCondensed , DWRITE_FONT_STRETCH_EXTRA_CONDENSED } ,
{ uiDrawTextStretchCondensed , DWRITE_FONT_STRETCH_CONDENSED } ,
{ uiDrawTextStretchSemiCondensed , DWRITE_FONT_STRETCH_SEMI_CONDENSED } ,
{ uiDrawTextStretchNormal , DWRITE_FONT_STRETCH_NORMAL } ,
{ uiDrawTextStretchSemiExpanded , DWRITE_FONT_STRETCH_SEMI_EXPANDED } ,
{ uiDrawTextStretchExpanded , DWRITE_FONT_STRETCH_EXPANDED } ,
{ uiDrawTextStretchExtraExpanded , DWRITE_FONT_STRETCH_EXTRA_EXPANDED } ,
{ uiDrawTextStretchUltraExpanded , DWRITE_FONT_STRETCH_ULTRA_EXPANDED } ,
2017-01-19 20:13:03 -06:00
} ;
struct lineInfo {
2017-02-06 17:38:44 -06:00
size_t startPos ; // in UTF-16 points
2017-01-19 20:13:03 -06:00
size_t endPos ;
size_t newlineCount ;
double x ;
double y ;
double width ;
double height ;
double baseline ;
} ;
// this function is deeply indebted to the PadWrite sample: https://github.com/Microsoft/Windows-classic-samples/blob/master/Samples/Win7Samples/multimedia/DirectWrite/PadWrite/TextEditor.cpp
static void computeLineInfo ( uiDrawTextLayout * tl )
{
DWRITE_LINE_METRICS * dlm ;
size_t nextStart ;
2017-01-21 08:39:53 -06:00
UINT32 i , j ;
DWRITE_HIT_TEST_METRICS * htm ;
UINT32 nFragments , unused ;
2017-01-19 20:13:03 -06:00
HRESULT hr ;
// TODO make sure this is legal; if not, switch to GetMetrics() and use its line count field instead
hr = tl - > layout - > GetLineMetrics ( NULL , 0 , & ( tl - > nLines ) ) ;
2017-01-20 02:24:06 -06:00
// ugh, HRESULT_TO_WIN32() is an inline function and is not constexpr so we can't use switch here
if ( hr = = S_OK ) {
2017-01-19 20:13:03 -06:00
// TODO what do we do here
2017-01-20 02:24:06 -06:00
} else if ( hr ! = E_NOT_SUFFICIENT_BUFFER )
2017-01-19 20:13:03 -06:00
logHRESULT ( L " error getting number of lines in IDWriteTextLayout " , hr ) ;
tl - > lineInfo = ( struct lineInfo * ) uiAlloc ( tl - > nLines * sizeof ( struct lineInfo ) , " struct lineInfo[] (text layout) " ) ;
dlm = new DWRITE_LINE_METRICS [ tl - > nLines ] ;
2017-01-20 03:34:15 -06:00
// we can't pass NULL here; it outright crashes if we do
// TODO verify the numbers haven't changed
hr = tl - > layout - > GetLineMetrics ( dlm , tl - > nLines , & unused ) ;
2017-01-19 20:13:03 -06:00
if ( hr ! = S_OK )
logHRESULT ( L " error getting IDWriteTextLayout line metrics " , hr ) ;
// assume the first line starts at position 0 and the string flow is incremental
nextStart = 0 ;
for ( i = 0 ; i < tl - > nLines ; i + + ) {
2017-01-20 02:24:06 -06:00
tl - > lineInfo [ i ] . startPos = nextStart ;
tl - > lineInfo [ i ] . endPos = nextStart + dlm [ i ] . length ;
tl - > lineInfo [ i ] . newlineCount = dlm [ i ] . newlineLength ;
nextStart = tl - > lineInfo [ i ] . endPos ;
2017-01-19 20:13:03 -06:00
2017-01-21 08:39:53 -06:00
// a line can have multiple fragments; for example, if there's a bidirectional override in the middle of a line
2017-01-20 02:24:06 -06:00
hr = tl - > layout - > HitTestTextRange ( tl - > lineInfo [ i ] . startPos , ( tl - > lineInfo [ i ] . endPos - tl - > lineInfo [ i ] . newlineCount ) - tl - > lineInfo [ i ] . startPos ,
2017-01-19 20:13:03 -06:00
0 , 0 ,
2017-01-21 08:39:53 -06:00
NULL , 0 , & nFragments ) ;
if ( hr ! = S_OK & & hr ! = E_NOT_SUFFICIENT_BUFFER )
logHRESULT ( L " error getting IDWriteTextLayout line fragment count " , hr ) ;
htm = new DWRITE_HIT_TEST_METRICS [ nFragments ] ;
// TODO verify unused == nFragments?
hr = tl - > layout - > HitTestTextRange ( tl - > lineInfo [ i ] . startPos , ( tl - > lineInfo [ i ] . endPos - tl - > lineInfo [ i ] . newlineCount ) - tl - > lineInfo [ i ] . startPos ,
0 , 0 ,
htm , nFragments , & unused ) ;
// TODO can this return E_NOT_SUFFICIENT_BUFFER again?
2017-01-19 20:13:03 -06:00
if ( hr ! = S_OK )
2017-01-21 08:39:53 -06:00
logHRESULT ( L " error getting IDWriteTextLayout line fragment metrics " , hr ) ;
// TODO verify htm.textPosition and htm.length against dtm[i]/tl->lineInfo[i]?
tl - > lineInfo [ i ] . x = htm [ 0 ] . left ;
tl - > lineInfo [ i ] . y = htm [ 0 ] . top ;
2017-02-07 18:42:00 -06:00
// TODO does this not include trailing whitespace? I forget
2017-01-21 08:39:53 -06:00
tl - > lineInfo [ i ] . width = htm [ 0 ] . width ;
tl - > lineInfo [ i ] . height = htm [ 0 ] . height ;
for ( j = 1 ; j < nFragments ; j + + ) {
// this is correct even if the leftmost fragment on the line is RTL
if ( tl - > lineInfo [ i ] . x > htm [ j ] . left )
tl - > lineInfo [ i ] . x = htm [ j ] . left ;
tl - > lineInfo [ i ] . width + = htm [ j ] . width ;
// TODO verify y and height haven't changed?
}
// TODO verify dlm[i].height == htm.height?
delete [ ] htm ;
2017-01-19 20:13:03 -06:00
// TODO on Windows 8.1 and/or 10 we can use DWRITE_LINE_METRICS1 to get specific info about the ascent and descent; do we have an alternative?
// TODO and even on those platforms can we somehow split tyographic leading from spacing?
// TODO and on that note, can we have both line spacing proportionally above and uniformly below?
2017-01-20 02:24:06 -06:00
tl - > lineInfo [ i ] . baseline = dlm [ i ] . baseline ;
2017-01-19 20:13:03 -06:00
}
delete [ ] dlm ;
}
uiDrawTextLayout * uiDrawNewTextLayout ( uiAttributedString * s , uiDrawFontDescriptor * defaultFont , double width )
{
uiDrawTextLayout * tl ;
WCHAR * wDefaultFamily ;
DWRITE_WORD_WRAPPING wrap ;
FLOAT maxWidth ;
HRESULT hr ;
tl = uiNew ( uiDrawTextLayout ) ;
wDefaultFamily = toUTF16 ( defaultFont - > Family ) ;
hr = dwfactory - > CreateTextFormat (
wDefaultFamily , NULL ,
// for the most part, DirectWrite weights correlate to ours
// the differences:
// - Minimum — libui: 0, DirectWrite: 1
// - Maximum — libui: 1000, DirectWrite: 999
// TODO figure out what to do about this shorter range (the actual major values are the same (but with different names), so it's just a range issue)
( DWRITE_FONT_WEIGHT ) ( defaultFont - > Weight ) ,
dwriteItalics [ defaultFont - > Italic ] ,
2017-01-20 02:24:06 -06:00
dwriteStretches [ defaultFont - > Stretch ] ,
2017-01-19 20:13:03 -06:00
pointSizeToDWriteSize ( 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 ) ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error creating IDWriteTextFormat " , hr ) ;
hr = dwfactory - > CreateTextLayout (
2017-01-20 02:24:06 -06:00
( const WCHAR * ) attrstrUTF16 ( s ) , attrstrUTF16Len ( s ) ,
2017-01-19 20:13:03 -06: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 ) width ;
if ( 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 ) ;
computeLineInfo ( tl ) ;
2017-02-06 17:38:44 -06:00
// and finally copy the UTF-8/UTF-16 index conversion tables
tl - > u8tou16 = attrstrCopyUTF8ToUTF16 ( s , & ( tl - > nUTF8 ) ) ;
tl - > u16tou8 = attrstrCopyUTF16ToUTF8 ( s , & ( tl - > nUTF16 ) ) ;
2017-01-19 20:13:03 -06:00
// TODO can/should this be moved elsewhere?
uiFree ( wDefaultFamily ) ;
return tl ;
}
void uiDrawFreeTextLayout ( uiDrawTextLayout * tl )
{
uiFree ( tl - > u16tou8 ) ;
2017-02-06 17:38:44 -06:00
uiFree ( tl - > u8tou16 ) ;
2017-01-19 20:13:03 -06:00
uiFree ( tl - > lineInfo ) ;
tl - > layout - > Release ( ) ;
tl - > format - > Release ( ) ;
uiFree ( tl ) ;
}
2017-01-20 02:24:06 -06:00
static ID2D1SolidColorBrush * mkSolidBrush ( ID2D1RenderTarget * rt , double r , double g , double b , double a )
{
D2D1_BRUSH_PROPERTIES props ;
D2D1_COLOR_F color ;
ID2D1SolidColorBrush * brush ;
HRESULT hr ;
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 ;
hr = rt - > CreateSolidColorBrush (
& color ,
& props ,
& brush ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error creating solid brush " , hr ) ;
return brush ;
}
2017-01-20 03:34:15 -06:00
// TODO this ignores clipping?
2017-01-19 20:13:03 -06:00
void uiDrawText ( uiDrawContext * c , uiDrawTextLayout * tl , double x , double y )
{
D2D1_POINT_2F pt ;
ID2D1Brush * black ;
// 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 = mkSolidBrush ( c - > rt , 0.0 , 0.0 , 0.0 , 1.0 ) ;
pt . x = x ;
pt . y = y ;
// TODO D2D1_DRAW_TEXT_OPTIONS_NO_SNAP?
// TODO D2D1_DRAW_TEXT_OPTIONS_CLIP?
// TODO 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 ) ;
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...
2017-01-20 17:09:06 -06:00
// TODO width does not include trailing whitespace
2017-01-19 20:13:03 -06:00
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 ;
}
int uiDrawTextLayoutNumLines ( uiDrawTextLayout * tl )
{
return tl - > nLines ;
}
// DirectWrite doesn't provide a direct way to do this, so we have to do this manually
2017-01-20 17:09:06 -06:00
// TODO does that comment still apply here or to the code at the top of this file?
2017-01-19 20:13:03 -06:00
void uiDrawTextLayoutLineByteRange ( uiDrawTextLayout * tl , int line , size_t * start , size_t * end )
{
* start = tl - > lineInfo [ line ] . startPos ;
* start = tl - > u16tou8 [ * start ] ;
* end = tl - > lineInfo [ line ] . endPos - tl - > lineInfo [ line ] . newlineCount ;
* end = tl - > u16tou8 [ * end ] ;
}
void uiDrawTextLayoutLineGetMetrics ( uiDrawTextLayout * tl , int line , uiDrawTextLayoutLineMetrics * m )
{
2017-01-20 17:09:06 -06:00
m - > X = tl - > lineInfo [ line ] . x ;
m - > Y = tl - > lineInfo [ line ] . y ;
m - > Width = tl - > lineInfo [ line ] . width ;
m - > Height = tl - > lineInfo [ line ] . height ;
// TODO rename tl->lineInfo[line].baseline to .baselineOffset or something of the sort to make its meaning more clear
m - > BaselineY = tl - > lineInfo [ line ] . y + tl - > lineInfo [ line ] . baseline ;
m - > Ascent = tl - > lineInfo [ line ] . baseline ;
m - > Descent = tl - > lineInfo [ line ] . height - tl - > lineInfo [ line ] . baseline ;
m - > Leading = 0 ; // TODO
m - > ParagraphSpacingBefore = 0 ; // TODO
m - > LineHeightSpace = 0 ; // TODO
m - > LineSpacing = 0 ; // TODO
m - > ParagraphSpacing = 0 ; // TODO
2017-01-19 20:13:03 -06:00
}
2017-02-08 19:10:34 -06:00
// this algorithm comes from Microsoft's PadWrite sample, following TextEditor::SetSelectionFromPoint()
2017-02-10 10:52:26 -06:00
// TODO go back through all of these and make sure we convert coordinates properly
// TODO same for OS X
2017-02-08 19:10:34 -06:00
void uiDrawTextLayoutHitTest ( uiDrawTextLayout * tl , double x , double y , size_t * pos , int * line )
{
DWRITE_HIT_TEST_METRICS m ;
BOOL trailing , inside ;
size_t p ;
2017-02-10 10:52:26 -06:00
UINT32 i ;
2017-02-08 19:10:34 -06:00
HRESULT hr ;
hr = tl - > layout - > HitTestPoint ( x , y ,
& trailing , & inside ,
& m ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error hit-testing IDWriteTextLayout " , hr ) ;
p = m . textPosition ;
// on a trailing hit, align to the nearest cluster
if ( trailing ) {
DWRITE_HIT_TEST_METRICS m2 ;
FLOAT x , y ; // crashes if I skip these :/
hr = tl - > layout - > HitTestTextPosition ( m . textPosition , trailing ,
& x , & y , & m2 ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error aligning trailing hit to nearest cluster " , hr ) ;
p = m2 . textPosition + m2 . length ;
}
2017-02-10 10:52:26 -06:00
* pos = tl - > u16tou8 [ p ] ;
for ( i = 0 ; i < tl - > nLines ; i + + ) {
double ltop , lbottom ;
ltop = tl - > lineInfo [ i ] . y ;
lbottom = ltop + tl - > lineInfo [ i ] . height ;
// y will already >= ltop at this point since the past lbottom should == ltop
if ( y < lbottom )
break ;
2017-02-08 19:10:34 -06:00
}
2017-02-10 10:52:26 -06:00
if ( i = = tl - > nLines )
i - - ;
* line = i ;
2017-02-08 19:10:34 -06:00
}
double uiDrawTextLayoutByteLocationInLine ( uiDrawTextLayout * tl , size_t pos , int line )
{
BOOL trailing ;
DWRITE_HIT_TEST_METRICS m ;
FLOAT x , y ;
HRESULT hr ;
2017-02-10 10:52:26 -06:00
if ( line < 0 | | line > = tl - > nLines )
return - 1 ;
2017-02-08 19:10:34 -06:00
pos = tl - > u8tou16 [ pos ] ;
2017-02-10 10:52:26 -06:00
// note: >, not >=, because the position at endPos is valid!
if ( pos < tl - > lineInfo [ line ] . startPos | | pos > tl - > lineInfo [ line ] . endPos )
return - 1 ;
2017-02-08 19:10:34 -06:00
// this behavior seems correct
// there's also PadWrite's TextEditor::GetCaretRect() but that requires state...
// TODO where does this fail?
trailing = FALSE ;
if ( pos ! = 0 & & pos ! = tl - > nUTF16 & & pos = = tl - > lineInfo [ line ] . endPos ) {
pos - - ;
trailing = TRUE ;
}
hr = tl - > layout - > HitTestTextPosition ( pos , trailing ,
& x , & y , & m ) ;
if ( hr ! = S_OK )
logHRESULT ( L " error calling IDWriteTextLayout::HitTestTextPosition() " , hr ) ;
return x ;
}
2017-02-10 16:38:17 -06:00
void caretDrawParams ( uiDrawContext * c , double height , struct caretDrawParams * p )
{
DWORD caretWidth ;
// there seems to be no defined caret color
// the best I can come up with is "inverts colors underneath" (according to https://msdn.microsoft.com/en-us/library/windows/desktop/ms648397(v=vs.85).aspx) which I have no idea how to do (TODO)
// just return black for now
p - > r = 0.0 ;
p - > g = 0.0 ;
p - > b = 0.0 ;
p - > a = 1.0 ;
if ( SystemParametersInfoW ( SPI_GETCARETWIDTH , 0 , & caretWidth , 0 ) = = 0 )
// don't log the failure, fall back gracefully
// the instruction to use this comes from https://msdn.microsoft.com/en-us/library/windows/desktop/ms648399(v=vs.85).aspx
// and we have to assume GetSystemMetrics() always succeeds, so
caretWidth = GetSystemMetrics ( SM_CXBORDER ) ;
// TODO make this a function and split it out of areautil.cpp
{
FLOAT dpix , dpiy ;
// TODO can we pass NULL for dpiy?
c - > rt - > GetDpi ( & dpix , & dpiy ) ;
// see https://msdn.microsoft.com/en-us/library/windows/desktop/dd756649%28v=vs.85%29.aspx (and others; search "direct2d mouse")
p - > width = ( ( double ) ( caretWidth * 96 ) ) / dpix ;
}
// and there doesn't seem to be this either... (TODO check what PadWrite does?)
p - > xoff = 0 ;
}
2017-02-11 15:55:30 -06:00
// TODO split this and the above related font matching code into a separate file?
void fontdescFromIDWriteFont ( IDWriteFont * font , uiDrawFontDescriptor * uidesc )
{
DWRITE_FONT_STYLE dwitalic ;
DWRITE_FONT_STRETCH dwstretch ;
dwitalic = font - > GetStyle ( ) ;
// TODO reverse the above misalignment if it is corrected
uidesc - > Weight = ( uiDrawTextWeight ) ( font - > GetWeight ( ) ) ;
dwstretch = font - > GetStretch ( ) ;
for ( uidesc - > Italic = uiDrawTextItalicNormal ; uidesc - > Italic < uiDrawTextItalicItalic ; uidesc - > Italic + + )
if ( dwriteItalics [ uidesc - > Italic ] = = dwitalic )
break ;
for ( uidesc - > Stretch = uiDrawTextStretchUltraCondensed ; uidesc - > Stretch < uiDrawTextStretchUltraExpanded ; uidesc - > Stretch + + )
if ( dwriteStretches [ uidesc - > Stretch ] = = dwstretch )
break ;
}