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
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 ;
// for converting DirectWrite indices to byte offsets
size_t * u16tou8 ;
size_t nu16tou8 ; // TODO I don't like the casing of this name; is it even necessary?
} ;
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 {
size_t startPos ;
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 ;
UINT32 i ;
DWRITE_HIT_TEST_METRICS htm ;
UINT32 unused ;
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-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 ,
& htm , 1 , & unused ) ;
2017-01-20 19:57:20 -06:00
// TODO this happens with the hit test string on the line with the RTL override (presumably the overridden part is its own separate result); see how it affects metrics
2017-01-19 20:13:03 -06:00
if ( hr = = E_NOT_SUFFICIENT_BUFFER )
2017-01-20 19:57:20 -06:00
; else // logHRESULT(L"TODO CONTACT ANDLABS — IDWriteTextLayout::HitTestTextRange() can return multiple ranges for a single line", hr);
2017-01-19 20:13:03 -06:00
if ( hr ! = S_OK )
logHRESULT ( L " error getting IDWriteTextLayout line rect " , hr ) ;
// TODO verify htm.textPosition and htm.length?
tl - > lineInfo [ i ] . x = htm . left ;
tl - > lineInfo [ i ] . y = htm . top ;
tl - > lineInfo [ i ] . width = htm . width ;
tl - > lineInfo [ i ] . height = htm . height ;
// TODO verify dlm[i].height == htm.height
// 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 ) ;
// and finally copy the UTF-16 to UTF-8 index conversion table
tl - > u16tou8 = attrstrCopyUTF16ToUTF8 ( s , & ( tl - > nu16tou8 ) ) ;
// TODO can/should this be moved elsewhere?
uiFree ( wDefaultFamily ) ;
return tl ;
}
void uiDrawFreeTextLayout ( uiDrawTextLayout * tl )
{
uiFree ( tl - > u16tou8 ) ;
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
}
void uiDrawTextLayoutHitTest ( uiDrawTextLayout * tl , double x , double y , uiDrawTextLayoutHitTestResult * result )
{
// TODO
}
void uiDrawTextLayoutByteRangeToRectangle ( uiDrawTextLayout * tl , size_t start , size_t end , uiDrawTextLayoutByteRangeRectangle * r )
{
}