77a212a72c
wordbreakMarkup() uses wxStringTokenizer to split text into words for
line-wrapping. When processing content inside markup nodes (overbar,
subscript, superscript), the tokenizer collapsed consecutive spaces
into a single space because wxTOKEN_RET_DELIMS skips empty tokens
between consecutive delimiters.
This caused textboxes to render "~{ }" as "~{ }" since textboxes
use LinebreakText() while regular text items do not.
Add an aInsideMarkup flag to skip tokenization for content nodes that
are children of markup nodes. Their content is concatenated verbatim
into the parent markup word, preserving all spaces.
Also fix width calculation for space-only tokens outside markup that
were getting zero width after Trim().
Fixes https://gitlab.com/kicad/code/kicad/-/issues/22913
704 lines
22 KiB
C++
704 lines
22 KiB
C++
/*
|
|
* This program source code file is part of KICAD, a free EDA CAD application.
|
|
*
|
|
* Copyright (C) 2021 Ola Rinta-Koski
|
|
* Copyright The KiCad Developers, see AUTHORS.txt for contributors.
|
|
*
|
|
* Font abstract base class
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, you may find one here:
|
|
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
|
|
* or you may search the http://www.gnu.org website for the version 2 license,
|
|
* or you may write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
|
*/
|
|
|
|
#include <list>
|
|
#include <mutex>
|
|
#include <unordered_map>
|
|
|
|
#include <macros.h>
|
|
#include <string_utils.h>
|
|
#include <gal/graphics_abstraction_layer.h>
|
|
#include <font/stroke_font.h>
|
|
#include <font/outline_font.h>
|
|
#include <trigo.h>
|
|
#include <markup_parser.h>
|
|
|
|
// The "official" name of the Kicad stroke font (always existing)
|
|
#include <font/kicad_font_name.h>
|
|
#include <wx/tokenzr.h>
|
|
|
|
// markup_parser.h includes pegtl.hpp which includes windows.h... which leaks #define DrawText
|
|
#undef DrawText
|
|
|
|
|
|
using namespace KIFONT;
|
|
|
|
METRICS g_defaultMetrics;
|
|
|
|
|
|
const METRICS& METRICS::Default()
|
|
{
|
|
return g_defaultMetrics;
|
|
}
|
|
|
|
|
|
FONT* FONT::s_defaultFont = nullptr;
|
|
|
|
std::map< std::tuple<wxString, bool, bool, bool>, FONT*> FONT::s_fontMap;
|
|
|
|
class MARKUP_CACHE
|
|
{
|
|
public:
|
|
struct ENTRY
|
|
{
|
|
std::string source;
|
|
std::unique_ptr<MARKUP::NODE> root;
|
|
};
|
|
|
|
MARKUP_CACHE( size_t aMaxSize ) :
|
|
m_maxSize( aMaxSize )
|
|
{
|
|
}
|
|
|
|
ENTRY& Put( const wxString& aQuery, ENTRY&& aResult )
|
|
{
|
|
auto it = m_cache.find( aQuery );
|
|
|
|
m_cacheMru.emplace_front( std::make_pair( aQuery, std::move( aResult ) ) );
|
|
|
|
if( it != m_cache.end() )
|
|
{
|
|
m_cacheMru.erase( it->second );
|
|
m_cache.erase( it );
|
|
}
|
|
|
|
m_cache[aQuery] = m_cacheMru.begin();
|
|
|
|
if( m_cache.size() > m_maxSize )
|
|
{
|
|
auto last = m_cacheMru.end();
|
|
last--;
|
|
m_cache.erase( last->first );
|
|
m_cacheMru.pop_back();
|
|
}
|
|
|
|
return m_cacheMru.begin()->second;
|
|
}
|
|
|
|
ENTRY* Get( const wxString& aQuery )
|
|
{
|
|
auto it = m_cache.find( aQuery );
|
|
|
|
if( it == m_cache.end() )
|
|
return nullptr;
|
|
|
|
m_cacheMru.splice( m_cacheMru.begin(), m_cacheMru, it->second );
|
|
|
|
return &m_cacheMru.begin()->second;
|
|
}
|
|
|
|
void Clear()
|
|
{
|
|
m_cacheMru.clear();
|
|
m_cache.clear();
|
|
}
|
|
|
|
private:
|
|
size_t m_maxSize;
|
|
std::list<std::pair<wxString, ENTRY>> m_cacheMru;
|
|
std::unordered_map<wxString, std::list<std::pair<wxString, ENTRY>>::iterator> m_cache;
|
|
};
|
|
|
|
|
|
static MARKUP_CACHE s_markupCache( 1024 );
|
|
static std::mutex s_markupCacheMutex;
|
|
static std::mutex s_defaultFontMutex;;
|
|
|
|
|
|
FONT::FONT()
|
|
{
|
|
}
|
|
|
|
|
|
FONT* FONT::getDefaultFont()
|
|
{
|
|
std::lock_guard lock( s_defaultFontMutex );
|
|
|
|
if( !s_defaultFont )
|
|
s_defaultFont = STROKE_FONT::LoadFont( wxEmptyString );
|
|
|
|
return s_defaultFont;
|
|
}
|
|
|
|
|
|
FONT* FONT::GetFont( const wxString& aFontName, bool aBold, bool aItalic,
|
|
const std::vector<wxString>* aEmbeddedFiles, bool aForDrawingSheet )
|
|
{
|
|
if( aFontName.empty() || aFontName.StartsWith( KICAD_FONT_NAME ) )
|
|
return getDefaultFont();
|
|
|
|
std::tuple<wxString, bool, bool, bool> key = { aFontName, aBold, aItalic, aForDrawingSheet };
|
|
|
|
FONT* font = nullptr;
|
|
|
|
if( s_fontMap.find( key ) != s_fontMap.end() )
|
|
font = s_fontMap[key];
|
|
|
|
if( !font )
|
|
font = OUTLINE_FONT::LoadFont( aFontName, aBold, aItalic, aEmbeddedFiles,
|
|
aForDrawingSheet );
|
|
|
|
if( !font )
|
|
font = getDefaultFont();
|
|
|
|
s_fontMap[key] = font;
|
|
|
|
return font;
|
|
}
|
|
|
|
|
|
bool FONT::IsStroke( const wxString& aFontName )
|
|
{
|
|
// This would need a more complex implementation if we ever support more stroke fonts
|
|
// than the KiCad Font.
|
|
return aFontName == _( "Default Font" ) || aFontName == KICAD_FONT_NAME;
|
|
}
|
|
|
|
|
|
void FONT::getLinePositions( const wxString& aText, const VECTOR2I& aPosition,
|
|
wxArrayString& aTextLines, std::vector<VECTOR2I>& aPositions,
|
|
std::vector<VECTOR2I>& aExtents, const TEXT_ATTRIBUTES& aAttrs,
|
|
const METRICS& aFontMetrics ) const
|
|
{
|
|
wxStringSplit( aText, aTextLines, '\n' );
|
|
int lineCount = aTextLines.Count();
|
|
aPositions.reserve( lineCount );
|
|
|
|
int interline = GetInterline( aAttrs.m_Size.y, aFontMetrics ) * aAttrs.m_LineSpacing;
|
|
int height = 0;
|
|
|
|
for( int i = 0; i < lineCount; i++ )
|
|
{
|
|
VECTOR2I pos( aPosition.x, aPosition.y + i * interline );
|
|
VECTOR2I end = boundingBoxSingleLine( nullptr, aTextLines[i], pos, aAttrs.m_Size,
|
|
aAttrs.m_Italic, aFontMetrics );
|
|
VECTOR2I bBox( end - pos );
|
|
|
|
aExtents.push_back( bBox );
|
|
|
|
if( i == 0 )
|
|
height += ( aAttrs.m_Size.y * 1.17 ); // 1.17 is a fudge to match 6.0 positioning
|
|
else
|
|
height += interline;
|
|
}
|
|
|
|
VECTOR2I offset( 0, 0 );
|
|
offset.y += aAttrs.m_Size.y;
|
|
|
|
if( IsStroke() )
|
|
{
|
|
// Fudge factors to match 6.0 positioning
|
|
offset.x += aAttrs.m_StrokeWidth / 1.52;
|
|
offset.y -= aAttrs.m_StrokeWidth * 0.052;
|
|
}
|
|
|
|
switch( aAttrs.m_Valign )
|
|
{
|
|
case GR_TEXT_V_ALIGN_TOP: break;
|
|
case GR_TEXT_V_ALIGN_CENTER: offset.y -= height / 2; break;
|
|
case GR_TEXT_V_ALIGN_BOTTOM: offset.y -= height; break;
|
|
case GR_TEXT_V_ALIGN_INDETERMINATE:
|
|
wxFAIL_MSG( wxT( "Indeterminate state legal only in dialogs." ) );
|
|
break;
|
|
}
|
|
|
|
for( int i = 0; i < lineCount; i++ )
|
|
{
|
|
VECTOR2I lineSize = aExtents.at( i );
|
|
VECTOR2I lineOffset( offset );
|
|
|
|
lineOffset.y += i * interline;
|
|
|
|
switch( aAttrs.m_Halign )
|
|
{
|
|
case GR_TEXT_H_ALIGN_LEFT: break;
|
|
case GR_TEXT_H_ALIGN_CENTER: lineOffset.x = -lineSize.x / 2; break;
|
|
case GR_TEXT_H_ALIGN_RIGHT: lineOffset.x = -( lineSize.x + offset.x ); break;
|
|
case GR_TEXT_H_ALIGN_INDETERMINATE:
|
|
wxFAIL_MSG( wxT( "Indeterminate state legal only in dialogs." ) );
|
|
break;
|
|
}
|
|
|
|
aPositions.push_back( aPosition + lineOffset );
|
|
}
|
|
}
|
|
|
|
|
|
void FONT::Draw( KIGFX::GAL* aGal, const wxString& aText, const VECTOR2I& aPosition, const VECTOR2I& aCursor,
|
|
const TEXT_ATTRIBUTES& aAttrs, const METRICS& aFontMetrics, std::optional<VECTOR2I> aMousePos,
|
|
wxString* aActiveUrl ) const
|
|
{
|
|
if( !aGal || aText.empty() )
|
|
return;
|
|
|
|
VECTOR2I position( aPosition - aCursor );
|
|
|
|
// Split multiline strings into separate ones and draw them line by line
|
|
wxArrayString strings_list;
|
|
std::vector<VECTOR2I> positions;
|
|
std::vector<VECTOR2I> extents;
|
|
|
|
getLinePositions( aText, position, strings_list, positions, extents, aAttrs, aFontMetrics );
|
|
|
|
aGal->SetLineWidth( aAttrs.m_StrokeWidth );
|
|
|
|
for( size_t i = 0; i < strings_list.GetCount(); i++ )
|
|
{
|
|
drawSingleLineText( aGal, nullptr, strings_list[i], positions[i], aAttrs.m_Size, aAttrs.m_Angle,
|
|
aAttrs.m_Mirrored, aPosition, aAttrs.m_Italic, aAttrs.m_Underlined, aAttrs.m_Hover,
|
|
aFontMetrics, aMousePos, aActiveUrl );
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @return position of cursor for drawing next substring.
|
|
*/
|
|
VECTOR2I drawMarkup( BOX2I* aBoundingBox, std::vector<std::unique_ptr<GLYPH>>* aGlyphs, const MARKUP::NODE* aNode,
|
|
const VECTOR2I& aPosition, const KIFONT::FONT* aFont, const VECTOR2I& aSize,
|
|
const EDA_ANGLE& aAngle, bool aMirror, const VECTOR2I& aOrigin, TEXT_STYLE_FLAGS aTextStyle,
|
|
const METRICS& aFontMetrics, std::optional<VECTOR2I> aMousePos, wxString* aActiveUrl )
|
|
{
|
|
VECTOR2I nextPosition = aPosition;
|
|
bool drawUnderline = false;
|
|
bool drawOverbar = false;
|
|
bool useHoverColor = false;
|
|
int start = aGlyphs ? (int) aGlyphs->size() : 0;
|
|
|
|
|
|
if( aNode )
|
|
{
|
|
TEXT_STYLE_FLAGS textStyle = aTextStyle;
|
|
|
|
if( !aNode->is_root() )
|
|
{
|
|
if( aNode->isSubscript() )
|
|
textStyle |= TEXT_STYLE::SUBSCRIPT;
|
|
else if( aNode->isSuperscript() )
|
|
textStyle |= TEXT_STYLE::SUPERSCRIPT;
|
|
|
|
if( aNode->isOverbar() )
|
|
drawOverbar = true;
|
|
|
|
if( aTextStyle & TEXT_STYLE::UNDERLINE )
|
|
drawUnderline = true;
|
|
|
|
if( aNode->has_content() )
|
|
{
|
|
BOX2I bbox;
|
|
|
|
nextPosition = aFont->GetTextAsGlyphs( &bbox, aGlyphs, aNode->asWxString(), aSize,
|
|
nextPosition, aAngle, aMirror, aOrigin, textStyle );
|
|
|
|
if( aBoundingBox )
|
|
aBoundingBox->Merge( bbox );
|
|
|
|
if( aNode->isURL() && aMousePos.has_value() && bbox.Contains( aMousePos.value() ) )
|
|
{
|
|
useHoverColor = true;
|
|
drawUnderline = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
for( const std::unique_ptr<MARKUP::NODE>& child : aNode->children )
|
|
{
|
|
nextPosition = drawMarkup( aBoundingBox, aGlyphs, child.get(), nextPosition, aFont, aSize,
|
|
aAngle, aMirror, aOrigin, textStyle, aFontMetrics, aMousePos,
|
|
aActiveUrl );
|
|
}
|
|
}
|
|
|
|
if( drawUnderline )
|
|
{
|
|
// Shorten the bar a little so its rounded ends don't make it over-long
|
|
double barTrim = aSize.x * 0.1;
|
|
double barOffset = aFontMetrics.GetUnderlineVerticalPosition( aSize.y );
|
|
|
|
VECTOR2D barStart( aPosition.x + barTrim, aPosition.y - barOffset );
|
|
VECTOR2D barEnd( nextPosition.x - barTrim, nextPosition.y - barOffset );
|
|
|
|
if( aGlyphs )
|
|
{
|
|
STROKE_GLYPH barGlyph;
|
|
|
|
barGlyph.AddPoint( barStart );
|
|
barGlyph.AddPoint( barEnd );
|
|
barGlyph.Finalize();
|
|
|
|
aGlyphs->push_back( barGlyph.Transform( { 1.0, 1.0 }, { 0, 0 }, false, aAngle, aMirror,
|
|
aOrigin ) );
|
|
}
|
|
}
|
|
|
|
if( drawOverbar )
|
|
{
|
|
// Shorten the bar a little so its rounded ends don't make it over-long
|
|
double barTrim = aSize.x * 0.1;
|
|
double barOffset = aFontMetrics.GetOverbarVerticalPosition( aSize.y );
|
|
|
|
VECTOR2D barStart( aPosition.x + barTrim, aPosition.y - barOffset );
|
|
VECTOR2D barEnd( nextPosition.x - barTrim, nextPosition.y - barOffset );
|
|
|
|
if( aGlyphs )
|
|
{
|
|
STROKE_GLYPH barGlyph;
|
|
|
|
barGlyph.AddPoint( barStart );
|
|
barGlyph.AddPoint( barEnd );
|
|
barGlyph.Finalize();
|
|
|
|
aGlyphs->push_back( barGlyph.Transform( { 1.0, 1.0 }, { 0, 0 }, false, aAngle, aMirror,
|
|
aOrigin ) );
|
|
}
|
|
}
|
|
|
|
if( useHoverColor )
|
|
{
|
|
for( int ii = start; ii < (int) aGlyphs->size(); ++ii )
|
|
(*aGlyphs)[ii]->SetIsHover( true );
|
|
|
|
if( aActiveUrl && aActiveUrl->IsEmpty() )
|
|
*aActiveUrl = aNode->asWxString();
|
|
}
|
|
|
|
return nextPosition;
|
|
}
|
|
|
|
|
|
VECTOR2I FONT::drawMarkup( BOX2I* aBoundingBox, std::vector<std::unique_ptr<GLYPH>>* aGlyphs,
|
|
const wxString& aText, const VECTOR2I& aPosition, const VECTOR2I& aSize,
|
|
const EDA_ANGLE& aAngle, bool aMirror, const VECTOR2I& aOrigin,
|
|
TEXT_STYLE_FLAGS aTextStyle, const METRICS& aFontMetrics,
|
|
std::optional<VECTOR2I> aMousePos, wxString* aActiveUrl ) const
|
|
{
|
|
std::lock_guard<std::mutex> lock( s_markupCacheMutex );
|
|
|
|
MARKUP_CACHE::ENTRY* markup = s_markupCache.Get( aText );
|
|
|
|
if( !markup || !markup->root )
|
|
{
|
|
MARKUP_CACHE::ENTRY& cached = s_markupCache.Put( aText, {} );
|
|
|
|
cached.source = TO_UTF8( aText );
|
|
MARKUP::MARKUP_PARSER markupParser( &cached.source );
|
|
cached.root = markupParser.Parse();
|
|
markup = &cached;
|
|
}
|
|
|
|
wxASSERT( markup && markup->root );
|
|
|
|
return ::drawMarkup( aBoundingBox, aGlyphs, markup->root.get(), aPosition, this, aSize, aAngle,
|
|
aMirror, aOrigin, aTextStyle, aFontMetrics, aMousePos, aActiveUrl );
|
|
}
|
|
|
|
|
|
void FONT::drawSingleLineText( KIGFX::GAL* aGal, BOX2I* aBoundingBox, const wxString& aText,
|
|
const VECTOR2I& aPosition, const VECTOR2I& aSize, const EDA_ANGLE& aAngle,
|
|
bool aMirror, const VECTOR2I& aOrigin, bool aItalic, bool aUnderline,
|
|
bool aHover, const METRICS& aFontMetrics, std::optional<VECTOR2I> aMousePos,
|
|
wxString* aActiveUrl ) const
|
|
{
|
|
if( !aGal )
|
|
return;
|
|
|
|
TEXT_STYLE_FLAGS textStyle = 0;
|
|
|
|
if( aItalic )
|
|
textStyle |= TEXT_STYLE::ITALIC;
|
|
|
|
if( aUnderline )
|
|
textStyle |= TEXT_STYLE::UNDERLINE;
|
|
|
|
std::vector<std::unique_ptr<GLYPH>> glyphs;
|
|
|
|
(void) drawMarkup( aBoundingBox, &glyphs, aText, aPosition, aSize, aAngle, aMirror, aOrigin,
|
|
textStyle, aFontMetrics, aMousePos, aActiveUrl );
|
|
|
|
if( aHover )
|
|
{
|
|
for( std::unique_ptr<GLYPH>& glyph : glyphs )
|
|
glyph->SetIsHover( true );
|
|
}
|
|
|
|
aGal->DrawGlyphs( glyphs );
|
|
}
|
|
|
|
|
|
VECTOR2I FONT::StringBoundaryLimits( const wxString& aText, const VECTOR2I& aSize, int aThickness,
|
|
bool aBold, bool aItalic, const METRICS& aFontMetrics ) const
|
|
{
|
|
// TODO do we need to parse every time - have we already parsed?
|
|
BOX2I boundingBox;
|
|
TEXT_STYLE_FLAGS textStyle = 0;
|
|
|
|
if( aBold )
|
|
textStyle |= TEXT_STYLE::BOLD;
|
|
|
|
if( aItalic )
|
|
textStyle |= TEXT_STYLE::ITALIC;
|
|
|
|
(void) drawMarkup( &boundingBox, nullptr, aText, VECTOR2I(), aSize, ANGLE_0, false, VECTOR2I(),
|
|
textStyle, aFontMetrics );
|
|
|
|
if( IsStroke() )
|
|
{
|
|
// Inflate by a bit more than thickness/2 to catch diacriticals, descenders, etc.
|
|
boundingBox.Inflate( KiROUND( aThickness * 1.5 ) );
|
|
}
|
|
else if( IsOutline() )
|
|
{
|
|
// Outline fonts have thickness built in, and *usually* stay within their ascent/descent
|
|
}
|
|
|
|
return boundingBox.GetSize();
|
|
}
|
|
|
|
|
|
VECTOR2I FONT::boundingBoxSingleLine( BOX2I* aBBox, const wxString& aText,
|
|
const VECTOR2I& aPosition, const VECTOR2I& aSize,
|
|
bool aItalic, const METRICS& aFontMetrics ) const
|
|
{
|
|
TEXT_STYLE_FLAGS textStyle = 0;
|
|
|
|
if( aItalic )
|
|
textStyle |= TEXT_STYLE::ITALIC;
|
|
|
|
VECTOR2I extents = drawMarkup( aBBox, nullptr, aText, aPosition, aSize, ANGLE_0, false,
|
|
VECTOR2I(), textStyle, aFontMetrics );
|
|
|
|
return extents;
|
|
}
|
|
|
|
|
|
/**
|
|
* Break marked-up text into "words".
|
|
*
|
|
* In this context, a "word" is EITHER a run of marked-up text (subscript, superscript or
|
|
* overbar), OR a run of non-marked-up text separated by spaces.
|
|
*/
|
|
void wordbreakMarkup( std::vector<std::pair<wxString, int>>* aWords,
|
|
const std::unique_ptr<MARKUP::NODE>& aNode, const KIFONT::FONT* aFont,
|
|
const VECTOR2I& aSize, TEXT_STYLE_FLAGS aTextStyle,
|
|
bool aInsideMarkup = false )
|
|
{
|
|
TEXT_STYLE_FLAGS textStyle = aTextStyle;
|
|
|
|
if( !aNode->is_root() )
|
|
{
|
|
wxChar escapeChar = 0;
|
|
|
|
if( aNode->isSubscript() )
|
|
{
|
|
escapeChar = '_';
|
|
textStyle = TEXT_STYLE::SUBSCRIPT;
|
|
}
|
|
else if( aNode->isSuperscript() )
|
|
{
|
|
escapeChar = '^';
|
|
textStyle = TEXT_STYLE::SUPERSCRIPT;
|
|
}
|
|
|
|
if( aNode->isOverbar() )
|
|
{
|
|
escapeChar = '~';
|
|
textStyle |= TEXT_STYLE::OVERBAR;
|
|
}
|
|
|
|
if( escapeChar )
|
|
{
|
|
wxString word = wxString::Format( wxT( "%c{" ), escapeChar );
|
|
int width = 0;
|
|
|
|
if( aNode->has_content() )
|
|
{
|
|
VECTOR2I next = aFont->GetTextAsGlyphs( nullptr, nullptr, aNode->asWxString(),
|
|
aSize, { 0, 0 }, ANGLE_0, false, { 0, 0 },
|
|
textStyle );
|
|
word += aNode->asWxString();
|
|
width += next.x;
|
|
}
|
|
|
|
std::vector<std::pair<wxString, int>> childWords;
|
|
|
|
for( const std::unique_ptr<MARKUP::NODE>& child : aNode->children )
|
|
wordbreakMarkup( &childWords, child, aFont, aSize, textStyle, true );
|
|
|
|
for( const std::pair<wxString, int>& childWord : childWords )
|
|
{
|
|
word += childWord.first;
|
|
width += childWord.second;
|
|
}
|
|
|
|
word += wxT( "}" );
|
|
aWords->emplace_back( std::make_pair( word, width ) );
|
|
return;
|
|
}
|
|
else if( aInsideMarkup )
|
|
{
|
|
// Inside a markup node, preserve the content verbatim (no tokenization).
|
|
// wxStringTokenizer collapses consecutive spaces, which corrupts content
|
|
// like ~{ } where spaces are intentional.
|
|
wxString content = aNode->asWxString();
|
|
int w = aFont->GetTextAsGlyphs( nullptr, nullptr, content, aSize, { 0, 0 },
|
|
ANGLE_0, false, { 0, 0 }, textStyle ).x;
|
|
|
|
aWords->emplace_back( std::make_pair( content, w ) );
|
|
}
|
|
else
|
|
{
|
|
wxString textRun = aNode->asWxString();
|
|
wxStringTokenizer tokenizer( textRun, " ", wxTOKEN_RET_DELIMS );
|
|
std::vector<wxString> words;
|
|
|
|
while( tokenizer.HasMoreTokens() )
|
|
words.emplace_back( tokenizer.GetNextToken() );
|
|
|
|
for( const wxString& word : words )
|
|
{
|
|
wxString chars = word;
|
|
chars.Trim();
|
|
|
|
int w = aFont->GetTextAsGlyphs( nullptr, nullptr, chars.IsEmpty() ? word : chars,
|
|
aSize, { 0, 0 }, ANGLE_0, false, { 0, 0 },
|
|
textStyle ).x;
|
|
|
|
aWords->emplace_back( std::make_pair( word, w ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
for( const std::unique_ptr<MARKUP::NODE>& child : aNode->children )
|
|
wordbreakMarkup( aWords, child, aFont, aSize, textStyle, aInsideMarkup );
|
|
}
|
|
|
|
|
|
void FONT::wordbreakMarkup( std::vector<std::pair<wxString, int>>* aWords, const wxString& aText,
|
|
const VECTOR2I& aSize, TEXT_STYLE_FLAGS aTextStyle ) const
|
|
{
|
|
MARKUP::MARKUP_PARSER markupParser( TO_UTF8( aText ) );
|
|
std::unique_ptr<MARKUP::NODE> root = markupParser.Parse();
|
|
|
|
::wordbreakMarkup( aWords, root, this, aSize, aTextStyle );
|
|
}
|
|
|
|
|
|
void FONT::LinebreakText( wxString& aText, int aColumnWidth, const VECTOR2I& aSize, int aThickness,
|
|
bool aBold, bool aItalic ) const
|
|
{
|
|
TEXT_STYLE_FLAGS textStyle = 0;
|
|
|
|
if( aBold )
|
|
textStyle |= TEXT_STYLE::BOLD;
|
|
|
|
if( aItalic )
|
|
textStyle |= TEXT_STYLE::ITALIC;
|
|
|
|
int spaceWidth = GetTextAsGlyphs( nullptr, nullptr, wxS( " " ), aSize, VECTOR2I(), ANGLE_0,
|
|
false, VECTOR2I(), textStyle ).x;
|
|
|
|
wxArrayString textLines;
|
|
wxStringSplit( aText, textLines, '\n' );
|
|
|
|
aText = wxEmptyString;
|
|
|
|
for( size_t ii = 0; ii < textLines.Count(); ++ii )
|
|
{
|
|
std::vector<std::pair<wxString, int>> markup;
|
|
std::vector<std::pair<wxString, int>> words;
|
|
|
|
wordbreakMarkup( &markup, textLines[ii], aSize, textStyle );
|
|
|
|
for( const auto& [ run, runWidth ] : markup )
|
|
{
|
|
if( !words.empty() && !words.back().first.EndsWith( ' ' ) )
|
|
{
|
|
words.back().first += run;
|
|
words.back().second += runWidth;
|
|
}
|
|
else
|
|
{
|
|
words.emplace_back( std::make_pair( run, runWidth ) );
|
|
}
|
|
}
|
|
|
|
bool buryMode = false;
|
|
int lineWidth = 0;
|
|
wxString pendingSpaces;
|
|
|
|
for( const auto& [ word, wordWidth ] : words )
|
|
{
|
|
int pendingSpaceWidth = (int) pendingSpaces.Length() * spaceWidth;
|
|
bool overflow = lineWidth + pendingSpaceWidth + wordWidth > aColumnWidth - aThickness;
|
|
|
|
if( overflow && pendingSpaces.Length() > 0 )
|
|
{
|
|
aText += '\n';
|
|
lineWidth = 0;
|
|
pendingSpaces = wxEmptyString;
|
|
pendingSpaceWidth = 0;
|
|
buryMode = true;
|
|
}
|
|
|
|
if( word == wxS( " " ) )
|
|
{
|
|
pendingSpaces += word;
|
|
}
|
|
else
|
|
{
|
|
if( buryMode )
|
|
{
|
|
buryMode = false;
|
|
}
|
|
else
|
|
{
|
|
aText += pendingSpaces;
|
|
lineWidth += pendingSpaceWidth;
|
|
}
|
|
|
|
if( word.EndsWith( ' ' ) )
|
|
{
|
|
aText += word.Left( word.Length() - 1 );
|
|
pendingSpaces = wxS( " " );
|
|
}
|
|
else
|
|
{
|
|
aText += word;
|
|
pendingSpaces = wxEmptyString;
|
|
}
|
|
|
|
lineWidth += wordWidth;
|
|
}
|
|
}
|
|
|
|
// Add the newlines back onto the string
|
|
if( ii != ( textLines.Count() - 1 ) )
|
|
aText += '\n';
|
|
}
|
|
}
|
|
|
|
|