2017-01-17 20:06:45 -06:00
// 17 january 2017
2016-05-05 17:23:54 -05:00
# include "uipriv_unix.h"
# include "draw.h"
2017-01-21 09:21:39 -06:00
// TODO
// - if the RTL override is at the beginning of a line, the preceding space is included?
2017-01-17 20:06:45 -06:00
struct uiDrawTextLayout {
PangoLayout * layout ;
2017-01-20 15:36:44 -06:00
uiDrawTextLayoutLineMetrics * lineMetrics ;
2016-05-05 17:23:54 -05:00
} ;
2017-01-17 20:06:45 -06:00
// See https://developer.gnome.org/pango/1.30/pango-Cairo-Rendering.html#pango-Cairo-Rendering.description
// For the conversion, see https://developer.gnome.org/pango/1.30/pango-Glyph-Storage.html#pango-units-to-double and https://developer.gnome.org/pango/1.30/pango-Glyph-Storage.html#pango-units-from-double
# define pangoToCairo(pango) (pango_units_to_double(pango))
# define cairoToPango(cairo) (pango_units_from_double(cairo))
2016-05-05 17:23:54 -05:00
2017-01-17 20:06:45 -06:00
// we need a context for a few things
// the documentation suggests creating cairo_t-specific, GdkScreen-specific, or even GtkWidget-specific contexts, but we can't really do that because we want our uiDrawTextFonts and uiDrawTextLayouts to be context-independent
// we could use pango_font_map_create_context(pango_cairo_font_map_get_default()) but that will ignore GDK-specific settings
// so let's use gdk_pango_context_get() instead; even though it's for the default screen only, it's good enough for us
# define mkGenericPangoCairoContext() (gdk_pango_context_get())
2016-05-05 17:23:54 -05:00
static const PangoStyle pangoItalics [ ] = {
[ uiDrawTextItalicNormal ] = PANGO_STYLE_NORMAL ,
[ uiDrawTextItalicOblique ] = PANGO_STYLE_OBLIQUE ,
[ uiDrawTextItalicItalic ] = PANGO_STYLE_ITALIC ,
} ;
static const PangoStretch pangoStretches [ ] = {
[ uiDrawTextStretchUltraCondensed ] = PANGO_STRETCH_ULTRA_CONDENSED ,
[ uiDrawTextStretchExtraCondensed ] = PANGO_STRETCH_EXTRA_CONDENSED ,
[ uiDrawTextStretchCondensed ] = PANGO_STRETCH_CONDENSED ,
[ uiDrawTextStretchSemiCondensed ] = PANGO_STRETCH_SEMI_CONDENSED ,
[ uiDrawTextStretchNormal ] = PANGO_STRETCH_NORMAL ,
[ uiDrawTextStretchSemiExpanded ] = PANGO_STRETCH_SEMI_EXPANDED ,
[ uiDrawTextStretchExpanded ] = PANGO_STRETCH_EXPANDED ,
[ uiDrawTextStretchExtraExpanded ] = PANGO_STRETCH_EXTRA_EXPANDED ,
[ uiDrawTextStretchUltraExpanded ] = PANGO_STRETCH_ULTRA_EXPANDED ,
} ;
2017-01-20 15:36:44 -06:00
// TODO neither these nor the overall extents seem to include trailing whitespace... we need to figure that out too
static void computeLineMetrics ( uiDrawTextLayout * tl )
{
PangoLayoutIter * iter ;
PangoLayoutLine * pll ;
PangoRectangle lineStartPos , lineExtents ;
int i , n ;
uiDrawTextLayoutLineMetrics * m ;
n = pango_layout_get_line_count ( tl - > layout ) ;
tl - > lineMetrics = ( uiDrawTextLayoutLineMetrics * ) uiAlloc ( n * sizeof ( uiDrawTextLayoutLineMetrics ) , " uiDrawTextLayoutLineMetrics[] (text layout) " ) ;
iter = pango_layout_get_iter ( tl - > layout ) ;
m = tl - > lineMetrics ;
for ( i = 0 ; i < n ; i + + ) {
int baselineY ;
// TODO we use this instead of _get_yrange() because of the block of text in that function's description about how line spacing is distributed in Pango; we have to worry about this when we start adding line spacing...
baselineY = pango_layout_iter_get_baseline ( iter ) ;
pll = pango_layout_iter_get_line_readonly ( iter ) ;
pango_layout_index_to_pos ( tl - > layout , pll - > start_index , & lineStartPos ) ;
pango_layout_line_get_extents ( pll , NULL , & lineExtents ) ;
// TODO unref pll?
// TODO is this correct for RTL glyphs?
m - > X = pangoToCairo ( lineStartPos . x ) ;
// TODO fix the whole combined not being updated shenanigans in the static build (here because ugh)
m - > Y = pangoToCairo ( baselineY - PANGO_ASCENT ( lineExtents ) ) ;
2017-02-07 18:14:51 -06:00
// TODO this does not include the last space if any
2017-01-20 15:36:44 -06:00
m - > Width = pangoToCairo ( lineExtents . width ) ;
m - > Height = pangoToCairo ( lineExtents . height ) ;
m - > BaselineY = pangoToCairo ( baselineY ) ;
m - > Ascent = pangoToCairo ( PANGO_ASCENT ( lineExtents ) ) ;
m - > Descent = pangoToCairo ( PANGO_DESCENT ( lineExtents ) ) ;
m - > Leading = 0 ; // TODO
m - > ParagraphSpacingBefore = 0 ; // TODO
m - > LineHeightSpace = 0 ; // TODO
m - > LineSpacing = 0 ; // TODO
m - > ParagraphSpacing = 0 ; // TODO
// don't worry about the return value; we're not using this after the last line
pango_layout_iter_next_line ( iter ) ;
m + + ;
}
pango_layout_iter_free ( iter ) ;
}
2017-01-17 20:06:45 -06:00
uiDrawTextLayout * uiDrawNewTextLayout ( uiAttributedString * s , uiDrawFontDescriptor * defaultFont , double width )
2016-05-05 17:23:54 -05:00
{
2017-01-17 20:06:45 -06:00
uiDrawTextLayout * tl ;
2016-05-05 17:23:54 -05:00
PangoContext * context ;
2017-01-17 20:06:45 -06:00
PangoFontDescription * desc ;
int pangoWidth ;
tl = uiNew ( uiDrawTextLayout ) ;
2016-05-05 17:23:54 -05:00
2017-01-17 20:06:45 -06:00
// in this case, the context is necessary to create the layout
// the layout takes a ref on the context so we can unref it afterward
2016-05-05 17:23:54 -05:00
context = mkGenericPangoCairoContext ( ) ;
2017-01-17 20:06:45 -06:00
tl - > layout = pango_layout_new ( context ) ;
2016-05-05 17:23:54 -05:00
g_object_unref ( context ) ;
2017-01-17 20:06:45 -06:00
// this is safe; pango_layout_set_text() copies the string
pango_layout_set_text ( tl - > layout , uiAttributedStringString ( s ) , - 1 ) ;
desc = pango_font_description_new ( ) ;
pango_font_description_set_family ( desc , defaultFont - > Family ) ;
pango_font_description_set_style ( desc , pangoItalics [ defaultFont - > Italic ] ) ;
// for the most part, pango weights correlate to ours
// the differences:
// - Book — libui: 350, Pango: 380
// - Ultra Heavy — libui: 950, Pango: 1000
// TODO figure out what to do about this misalignment
pango_font_description_set_weight ( desc , defaultFont - > Weight ) ;
pango_font_description_set_stretch ( desc , pangoStretches [ defaultFont - > Stretch ] ) ;
// see https://developer.gnome.org/pango/1.30/pango-Fonts.html#pango-font-description-set-size and https://developer.gnome.org/pango/1.30/pango-Glyph-Storage.html#pango-units-from-double
pango_font_description_set_size ( desc , pango_units_from_double ( defaultFont - > Size ) ) ;
pango_layout_set_font_description ( tl - > layout , desc ) ;
// this is safe; the description is copied
pango_font_description_free ( desc ) ;
2016-05-05 17:23:54 -05:00
2017-01-17 20:06:45 -06:00
pangoWidth = cairoToPango ( width ) ;
if ( width < 0 )
pangoWidth = - 1 ;
pango_layout_set_width ( tl - > layout , pangoWidth ) ;
2016-05-05 17:23:54 -05:00
2017-01-17 20:06:45 -06:00
// TODO attributes
2016-05-05 17:23:54 -05:00
2017-01-20 15:36:44 -06:00
computeLineMetrics ( tl ) ;
2017-01-17 20:06:45 -06:00
return tl ;
2016-05-05 17:23:54 -05:00
}
2017-01-17 20:06:45 -06:00
void uiDrawFreeTextLayout ( uiDrawTextLayout * tl )
2016-05-05 17:23:54 -05:00
{
2017-01-20 15:36:44 -06:00
uiFree ( tl - > lineMetrics ) ;
2017-01-17 20:06:45 -06:00
g_object_unref ( tl - > layout ) ;
uiFree ( tl ) ;
2016-05-05 17:23:54 -05:00
}
2017-01-17 20:06:45 -06:00
void uiDrawText ( uiDrawContext * c , uiDrawTextLayout * tl , double x , double y )
2016-05-05 17:23:54 -05:00
{
2017-01-17 20:06:45 -06:00
cairo_move_to ( c - > cr , x , y ) ;
pango_cairo_show_layout ( c - > cr , tl - > layout ) ;
2016-05-05 17:23:54 -05:00
}
2017-01-17 20:06:45 -06:00
void uiDrawTextLayoutExtents ( uiDrawTextLayout * tl , double * width , double * height )
2016-05-05 17:23:54 -05:00
{
2017-01-17 20:06:45 -06:00
PangoRectangle logical ;
2016-05-05 17:23:54 -05:00
2017-01-17 20:06:45 -06:00
pango_layout_get_extents ( tl - > layout , NULL , & logical ) ;
* width = pangoToCairo ( logical . width ) ;
* height = pangoToCairo ( logical . height ) ;
2016-05-05 17:23:54 -05:00
}
2017-01-17 20:06:45 -06:00
int uiDrawTextLayoutNumLines ( uiDrawTextLayout * tl )
2016-05-05 17:23:54 -05:00
{
2017-01-17 20:06:45 -06:00
return pango_layout_get_line_count ( tl - > layout ) ;
2016-05-05 17:23:54 -05:00
}
2017-01-17 20:06:45 -06:00
void uiDrawTextLayoutLineByteRange ( uiDrawTextLayout * tl , int line , size_t * start , size_t * end )
2016-05-05 17:23:54 -05:00
{
2017-01-17 20:06:45 -06:00
PangoLayoutLine * pll ;
pll = pango_layout_get_line_readonly ( tl - > layout , line ) ;
* start = pll - > start_index ;
* end = pll - > start_index + pll - > length ;
2017-01-20 15:36:44 -06:00
// TODO unref pll?
2016-05-05 17:23:54 -05:00
}
2017-01-17 20:06:45 -06:00
void uiDrawTextLayoutLineGetMetrics ( uiDrawTextLayout * tl , int line , uiDrawTextLayoutLineMetrics * m )
2016-05-05 17:23:54 -05:00
{
2017-01-20 15:36:44 -06:00
* m = tl - > lineMetrics [ line ] ;
2016-05-05 17:23:54 -05:00
}
2017-01-17 20:06:45 -06:00
// TODO
#if 0
2016-05-05 17:23:54 -05:00
{
2017-01-17 20:06:45 -06:00
PangoLayoutLine * pll ;
2016-05-05 17:23:54 -05:00
2017-01-17 20:06:45 -06:00
pll = pango_layout_get_line_readonly ( tl - > layout , line ) ;
// TODO unref?
2016-05-05 17:23:54 -05:00
}
2017-01-17 20:06:45 -06:00
# endif
2016-05-05 17:23:54 -05:00
2017-02-08 20:19:49 -06:00
#if 0 /* TODO */
2017-02-07 18:14:51 -06:00
// caret behavior, based on GtkTextView (both 2 and 3) and Qt's text views (both 4 and 5), which are all based on TkTextView:
// - clicking on the right side of a line places the cursor at the beginning of the LAST grapheme on the line
// - pressing Right goes to the first grapheme of the next line, pressing Left goes back to the last grapheme of the original line
// - as such, there is *no* way to get the cursor on the end of the line
// - this includes whitespace, hyphens, and character wraps
// - spaces get special treatment (it seems): you don't see them there and they don't show up if you select one and only one!
// - and the best part is that word processors don't typically do this; they do what other platforms do!
2017-01-17 20:06:45 -06:00
void uiDrawTextLayoutHitTest ( uiDrawTextLayout * tl , double x , double y , uiDrawTextLayoutHitTestResult * result )
2016-05-05 17:23:54 -05:00
{
2017-02-06 09:11:45 -06:00
int index ;
int trailing ;
int line ;
2017-02-07 18:14:51 -06:00
int caretX ;
2017-02-06 09:11:45 -06:00
double width , height ;
// disregard the return value; we do our own bounds checking
// TODO see if there's a way we can use some other function (possibly this one again) to extrapolate more precise bounds information
pango_layout_xy_to_index ( tl - > layout ,
cairoToPango ( x ) , cairoToPango ( y ) ,
& index , & trailing ) ;
2017-02-07 18:14:51 -06:00
// figure out what line that was, and also the caret X position since we can get that here too
// TODO should we use the dedicated function instead?
2017-02-06 09:11:45 -06:00
pango_layout_index_to_line_x ( tl - > layout , index , trailing ,
2017-02-07 18:14:51 -06:00
& line , & caretX ) ;
2017-02-06 09:11:45 -06:00
result - > Pos = index ;
result - > Line = line ;
uiDrawTextLayoutExtents ( tl , & width , & height ) ;
result - > XPosition = uiDrawTextLayoutHitTestPositionInside ;
if ( x < 0 )
result - > XPosition = uiDrawTextLayoutHitTestPositionBefore ;
else if ( x > = width )
result - > XPosition = uiDrawTextLayoutHitTestPositionAfter ;
result - > YPosition = uiDrawTextLayoutHitTestPositionInside ;
if ( y < 0 )
result - > YPosition = uiDrawTextLayoutHitTestPositionBefore ;
else if ( y > = height )
result - > YPosition = uiDrawTextLayoutHitTestPositionAfter ;
2017-02-07 18:14:51 -06:00
result - > CaretX = pangoToCairo ( caretX ) ;
result - > CaretY = tl - > lineMetrics [ line ] . Y ;
result - > CaretHeight = tl - > lineMetrics [ line ] . Height ;
2016-05-05 17:23:54 -05:00
}
2017-02-07 18:14:51 -06:00
// TODO consider providing a uiDrawTextLayoutByteIndexToCursorPos() function for manual positions by byte index only?
2017-02-06 09:11:45 -06:00
// TODO is this correct for RTL?
2017-01-17 20:06:45 -06:00
void uiDrawTextLayoutByteRangeToRectangle ( uiDrawTextLayout * tl , size_t start , size_t end , uiDrawTextLayoutByteRangeRectangle * r )
2016-05-05 17:23:54 -05:00
{
2017-02-06 09:11:45 -06:00
PangoRectangle startRect , endRect ;
2017-02-06 09:30:26 -06:00
int line , index ;
2017-02-06 09:11:45 -06:00
PangoLayoutLine * pll ;
pango_layout_index_to_pos ( tl - > layout , start , & startRect ) ;
// figure out what line that was
// TODO talk about leading edge here
pango_layout_index_to_line_x ( tl - > layout , start , 1 ,
& line , NULL ) ;
pll = pango_layout_get_line_readonly ( tl - > layout , line ) ;
if ( end > ( pll - > start_index + pll - > length ) )
end = pll - > start_index + pll - > length ;
pango_layout_index_to_pos ( tl - > layout , end , & endRect ) ;
r - > X = pangoToCairo ( startRect . x ) ;
r - > Y = tl - > lineMetrics [ line ] . Y ;
r - > Width = pangoToCairo ( endRect . x ) - r - > X ;
r - > Height = tl - > lineMetrics [ line ] . Height ;
// and figure out the correct start pos
pango_layout_line_x_to_index ( pll , startRect . x ,
& index , NULL ) ;
r - > RealStart = index ;
r - > RealEnd = end ;
// TODO unref pll?
2016-05-05 17:23:54 -05:00
}
2017-02-08 20:19:49 -06:00
# endif
// this algorithm comes from Microsoft's PadWrite sample, following TextEditor::SetSelectionFromPoint()
// TODO this or the next one doesn't work right for the end of a line? it still behaves like TkTextView...
void uiDrawTextLayoutHitTest ( uiDrawTextLayout * tl , double x , double y , size_t * pos , int * line )
{
int p , trailing ;
pango_layout_xy_to_index ( tl - > layout ,
cairoToPango ( x ) , cairoToPango ( y ) ,
& p , & trailing ) ;
// on a trailing hit, align to the nearest cluster
// fortunately Pango provides that info directly
if ( trailing ! = 0 )
p + = trailing ;
if ( pos ! = NULL )
* pos = p ;
// TODO do the line detection unconditionally?
// TODO optimize the call to pango_layout_get_line_count()
if ( line ! = NULL ) {
int i ;
for ( i = 0 ; i < pango_layout_get_line_count ( tl - > layout ) ; i + + ) {
double ltop , lbottom ;
ltop = tl - > lineMetrics [ i ] . Y ;
lbottom = ltop + tl - > lineMetrics [ i ] . Height ;
// y will already >= ltop at this point since the past lbottom should == ltop
if ( y < lbottom )
break ;
}
if ( i = = pango_layout_get_line_count ( tl - > layout ) )
i - - ;
* line = i ;
}
}
// TODO find a good API for indicating that this character isn't on the line for when the layout is resized and doing that recalculation...
double uiDrawTextLayoutByteLocationInLine ( uiDrawTextLayout * tl , size_t pos , int line )
{
PangoLayoutLine * pll ;
gboolean trailing ;
int pangox ;
pll = pango_layout_get_line_readonly ( tl - > layout , line ) ;
// this behavior seems correct
// there's also PadWrite's TextEditor::GetCaretRect() but that requires state...
// TODO where does this fail?
// TODO optimize everything to avoid calling strlen()
trailing = 0 ;
if ( pos ! = 0 & & pos ! = strlen ( pango_layout_get_text ( tl - > layout ) ) & & pos = = ( pll - > start_index + pll - > length ) ) {
pos - - ;
trailing = 1 ;
}
pango_layout_line_index_to_x ( pll , pos , trailing , & pangox ) ;
// TODO unref pll?
return pangoToCairo ( pangox ) ;
}