mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
Added a basic RichText class (and added tests and an example).
Fixed LineWrap when using initial X offset and the word does not fit in the current line but it fits in the next. Added basic agent rules.
This commit is contained in:
11
.agent/rules/build-project.md
Normal file
11
.agent/rules/build-project.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
To build the project in debug mode you must run from the root project directory:
|
||||
|
||||
`make -C make/linux -j$(nproc)`
|
||||
|
||||
If any file has been added you should also run (previous to the make command):
|
||||
|
||||
`premake4 --disable-static-build --with-mold-linker --with-debug-symbols --address-sanitizer gmake`
|
||||
13
.agent/rules/project-introduction.md
Normal file
13
.agent/rules/project-introduction.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
[eepp](https://github.com/SpartanJ/eepp/) is an open source cross-platform game and application development framework heavily focused on the development of rich graphical user interfaces.
|
||||
|
||||
Inside this repository also lives [ecode](https://github.com/SpartanJ/ecode/). ecode is a lightweight multi-platform code editor designed for modern hardware with a focus on responsiveness and performance. It has been developed with the hardware-accelerated eepp GUI, which provides the core technology for the editor. The project comes as the first serious project using the eepp GUI, and it's currently being developed to improve the eepp GUI library as part of one of its main objectives.
|
||||
|
||||
Very basic eepp documentation can be found at `docs/articles`. Many class headers have Doxygen documentation, rely on that. eepp headers are at `include/eepp/`.
|
||||
|
||||
A good amount of examples on how to use the library can be found in `src/examples`.
|
||||
|
||||
The `README.md` at the root directory explains in more detail about the project.
|
||||
18
.agent/rules/unit-tests.md
Normal file
18
.agent/rules/unit-tests.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
trigger: always_on
|
||||
---
|
||||
|
||||
Project provide a good range of unit-tests that they must pass to guarantee that changes made do not break functionality.
|
||||
To run the tests you must execute the binary:
|
||||
`bin/unit_tests/eepp-unit_tests-debug`
|
||||
|
||||
This path is from the root directory, you can run it from anywhere, current working directory is managed by the binary.
|
||||
|
||||
If you need to run an specific test you can use the filter parameter, it supports glob patterns, for example:
|
||||
|
||||
`bin/unit_tests/eepp-unit_tests-debug --filter="FontRendering.*Offset*"`
|
||||
|
||||
Will run all tests with "Offset" in its name.
|
||||
It's expected that for *any* requested new functionality you must add new tests and also tests with previously existing ones. Initially always test with the most relevant to the change that's has been made.
|
||||
|
||||
Tests can be found at: `src/tests/unit_tests`. Being `src/tests/unit_tests/fontrendering.cpp` the most complete set of tests related to text rendering.
|
||||
@@ -333,6 +333,12 @@
|
||||
"command": "${project_root}/bin/eepp-MapEditor-debug",
|
||||
"name": "eepp-MapEditor-debug",
|
||||
"working_dir": "${project_root}/bin"
|
||||
},
|
||||
{
|
||||
"args": "",
|
||||
"command": "${project_root}/bin/eepp-richtext-debug",
|
||||
"name": "eepp-richtext-debug",
|
||||
"working_dir": "${project_root}/bin"
|
||||
}
|
||||
],
|
||||
"var": {
|
||||
|
||||
BIN
bin/unit_tests/assets/fontrendering/eepp-rich-text.webp
Normal file
BIN
bin/unit_tests/assets/fontrendering/eepp-rich-text.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
@@ -21,6 +21,7 @@
|
||||
#include <eepp/graphics/globaltextureatlas.hpp>
|
||||
#include <eepp/graphics/glyphdrawable.hpp>
|
||||
#include <eepp/graphics/image.hpp>
|
||||
#include <eepp/graphics/linewrap.hpp>
|
||||
#include <eepp/graphics/ninepatch.hpp>
|
||||
#include <eepp/graphics/ninepatchmanager.hpp>
|
||||
#include <eepp/graphics/particle.hpp>
|
||||
@@ -33,13 +34,16 @@
|
||||
#include <eepp/graphics/renderer/renderergl.hpp>
|
||||
#include <eepp/graphics/renderer/renderergl3.hpp>
|
||||
#include <eepp/graphics/rendermode.hpp>
|
||||
#include <eepp/graphics/richtext.hpp>
|
||||
#include <eepp/graphics/scopedtexture.hpp>
|
||||
#include <eepp/graphics/scrollparallax.hpp>
|
||||
#include <eepp/graphics/shader.hpp>
|
||||
#include <eepp/graphics/shaderprogram.hpp>
|
||||
#include <eepp/graphics/shaderprogrammanager.hpp>
|
||||
#include <eepp/graphics/shapedglyph.hpp>
|
||||
#include <eepp/graphics/sprite.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <eepp/graphics/textlayout.hpp>
|
||||
#include <eepp/graphics/texture.hpp>
|
||||
#include <eepp/graphics/textureatlas.hpp>
|
||||
#include <eepp/graphics/textureatlasloader.hpp>
|
||||
|
||||
@@ -31,6 +31,7 @@ class EE_API Drawable {
|
||||
UINODEDRAWABLE_LAYERDRAWABLE,
|
||||
UIBORDERDRAWABLE,
|
||||
UIBACKGROUNDDRAWABLE,
|
||||
RICHTEXT,
|
||||
CUSTOM
|
||||
};
|
||||
|
||||
|
||||
119
include/eepp/graphics/richtext.hpp
Normal file
119
include/eepp/graphics/richtext.hpp
Normal file
@@ -0,0 +1,119 @@
|
||||
#ifndef EE_GRAPHICS_RICHTEXT_HPP
|
||||
#define EE_GRAPHICS_RICHTEXT_HPP
|
||||
|
||||
#include <eepp/graphics/drawable.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace EE { namespace Graphics {
|
||||
|
||||
/**
|
||||
* @brief A drawable class that renders rich text with multiple styles and spans.
|
||||
*
|
||||
* RichText allows rendering text with different fonts, sizes, colors, and styles mixed together.
|
||||
* It supports word wrapping and alignment.
|
||||
*/
|
||||
class EE_API RichText : public Drawable {
|
||||
public:
|
||||
/** @return A new instance of RichText. */
|
||||
static RichText* New();
|
||||
|
||||
/** @brief Default constructor. */
|
||||
RichText();
|
||||
|
||||
/** @brief Destructor. */
|
||||
~RichText();
|
||||
|
||||
/**
|
||||
* @brief Adds a text span with a specific style configuration.
|
||||
* @param text The text content.
|
||||
* @param style The font style configuration to apply.
|
||||
*/
|
||||
void addSpan( const String& text, const FontStyleConfig& style );
|
||||
|
||||
/**
|
||||
* @brief Adds a text span with individual style parameters.
|
||||
* @param text The text content.
|
||||
* @param font The font to use (optional, uses default if null).
|
||||
* @param characterSize The character size (optional, uses default if 0).
|
||||
* @param color The text color (optional, uses default if White).
|
||||
* @param style The text style (optional, uses default if Regular).
|
||||
*/
|
||||
void addSpan( const String& text, Font* font = nullptr, Uint32 characterSize = 0,
|
||||
Color color = Color::White, Uint32 style = Text::Regular );
|
||||
|
||||
/** @brief Clears all text spans. */
|
||||
void clear();
|
||||
|
||||
/** @brief Sets the default font style configuration used for new spans if not specified. */
|
||||
void setFontStyleConfig( const FontStyleConfig& styleConfig );
|
||||
|
||||
/** @return The default font style configuration. */
|
||||
FontStyleConfig& getFontStyleConfig() { return mDefaultStyle; }
|
||||
|
||||
/** @brief Sets the text alignment (Left, Center, Right). */
|
||||
void setAlign( Uint32 align );
|
||||
|
||||
/** @brief Sets the maximum width for wrapping. If 0, wrapping is disabled. */
|
||||
void setMaxWidth( Float width );
|
||||
|
||||
/** @return The maximum width for wrapping. */
|
||||
Float getMaxWidth() const { return mMaxWidth; }
|
||||
|
||||
/** @return The list of text spans. */
|
||||
std::vector<std::shared_ptr<Text>>& getSpans() { return mSpans; }
|
||||
|
||||
virtual void draw( const Float& X, const Float& Y, const Vector2f& scale = Vector2f::One,
|
||||
const Float& rotation = 0, BlendMode effect = BlendMode::Alpha(),
|
||||
const OriginPoint& rotationCenter = OriginPoint::OriginCenter,
|
||||
const OriginPoint& scaleCenter = OriginPoint::OriginCenter );
|
||||
|
||||
virtual void draw();
|
||||
|
||||
virtual void draw( const Vector2f& position );
|
||||
|
||||
virtual void draw( const Vector2f& position, const Sizef& size );
|
||||
|
||||
virtual bool isStateful() { return false; }
|
||||
|
||||
virtual Sizef getSize();
|
||||
|
||||
virtual Sizef getPixelsSize();
|
||||
|
||||
/** @brief Invalidates the layout, forcing a recalculation on the next update. */
|
||||
void invalidate();
|
||||
|
||||
/** @brief Structure representing a rendered span within a line. */
|
||||
struct RenderSpan {
|
||||
std::shared_ptr<Text> text;
|
||||
Vector2f position; // Local position relative to RichText origin
|
||||
};
|
||||
|
||||
/** @brief Structure representing a rendered paragraph (line). */
|
||||
struct RenderParagraph {
|
||||
std::vector<RenderSpan> spans;
|
||||
Float y{ 0 };
|
||||
Float height{ 0 };
|
||||
Float maxAscent{ 0 };
|
||||
Float width{ 0 };
|
||||
};
|
||||
|
||||
/** @return The list of rendered lines. */
|
||||
const std::vector<RenderParagraph>& getLines() const { return mLines; }
|
||||
|
||||
protected:
|
||||
std::vector<std::shared_ptr<Text>> mSpans;
|
||||
std::vector<RenderParagraph> mLines;
|
||||
FontStyleConfig mDefaultStyle;
|
||||
Uint32 mAlign{ TEXT_ALIGN_LEFT };
|
||||
Float mMaxWidth{ 0.f };
|
||||
Sizef mSize;
|
||||
bool mNeedsLayoutUpdate{ true };
|
||||
|
||||
void updateLayout();
|
||||
};
|
||||
|
||||
}} // namespace EE::Graphics
|
||||
|
||||
#endif
|
||||
@@ -1559,6 +1559,12 @@ solution "eepp"
|
||||
files { "src/examples/ui_hello_world/*.cpp" }
|
||||
build_link_configuration( "eepp-ui-hello-world", true )
|
||||
|
||||
project "eepp-richtext"
|
||||
set_kind()
|
||||
language "C++"
|
||||
files { "src/examples/richtext/*.cpp" }
|
||||
build_link_configuration( "eepp-richtext", true )
|
||||
|
||||
project "eepp-7guis-counter"
|
||||
set_kind()
|
||||
language "C++"
|
||||
|
||||
@@ -1435,6 +1435,12 @@ workspace "eepp"
|
||||
files { "src/examples/ui_hello_world/*.cpp" }
|
||||
build_link_configuration( "eepp-ui-hello-world", true )
|
||||
|
||||
project "eepp-richtext"
|
||||
set_kind()
|
||||
language "C++"
|
||||
files { "src/examples/richtext/*.cpp" }
|
||||
build_link_configuration( "eepp-richtext", true )
|
||||
|
||||
project "eepp-7guis-counter"
|
||||
set_kind()
|
||||
language "C++"
|
||||
|
||||
@@ -129,6 +129,9 @@ LineWrap::computeLineBreaksInternal( const String::View& string, Font* font, Uin
|
||||
: whiteSpaceWidth;
|
||||
|
||||
if ( keepIndentation ) {
|
||||
// paddingStart is the offset added to the wrapped lines based on the initial intendation of
|
||||
// the line. This is to keep the indented wrapped lines aligned with the line indentation.
|
||||
// Useful for code editors.
|
||||
info.paddingStart =
|
||||
LineWrap::computeOffsets( string, font, characterSize, fontStyle, outlineThickness,
|
||||
tabWidth, eemax( maxWidth - hspace, hspace ), tabStops );
|
||||
@@ -142,7 +145,7 @@ LineWrap::computeLineBreaksInternal( const String::View& string, Font* font, Uin
|
||||
static_cast<FontTrueType*>( font )->isIdentifiedAsMonospace() &&
|
||||
Text::canSkipShaping( textDrawHints ) ) );
|
||||
|
||||
size_t lastSpace = 0;
|
||||
size_t lastSpace = std::string::npos;
|
||||
Uint32 prevChar = 0;
|
||||
size_t idx = 0;
|
||||
bool hasWrap = maxWidth > 0 && mode != LineWrapMode::NoWrap;
|
||||
@@ -179,7 +182,7 @@ LineWrap::computeLineBreaksInternal( const String::View& string, Font* font, Uin
|
||||
xoffset += w;
|
||||
|
||||
if ( hasWrap && xoffset > maxWidth ) {
|
||||
if ( mode == LineWrapMode::Word && lastSpace ) {
|
||||
if ( mode == LineWrapMode::Word && lastSpace != std::string::npos ) {
|
||||
if constexpr ( std::is_same_v<LineWrapType, LineWrapInfoEx> ) {
|
||||
info.wrapsWidth.push_back( std::ceil( lastWordWrapWidth ) );
|
||||
}
|
||||
@@ -187,14 +190,32 @@ LineWrap::computeLineBreaksInternal( const String::View& string, Font* font, Uin
|
||||
info.wraps.push_back( lastSpace + 1 );
|
||||
xoffset = info.paddingStart + ( xoffset - lastWidth );
|
||||
} else {
|
||||
if constexpr ( std::is_same_v<LineWrapType, LineWrapInfoEx> ) {
|
||||
info.wrapsWidth.push_back( std::ceil( xoffset - w ) );
|
||||
}
|
||||
// If we are about to split a word, check if we can move it to the next line
|
||||
if ( mode == LineWrapMode::Word && info.wraps.size() == 1 &&
|
||||
xoffset - initialXOffset <= maxWidth ) {
|
||||
// We can move it to the next line
|
||||
if constexpr ( std::is_same_v<LineWrapType, LineWrapInfoEx> ) {
|
||||
info.wrapsWidth.push_back( 0 ); // Empty line width
|
||||
}
|
||||
|
||||
info.wraps.push_back( idx );
|
||||
xoffset = info.paddingStart;
|
||||
// Wrap at the beginning of the line
|
||||
size_t splitIdx = info.wraps.back();
|
||||
info.wraps.push_back( splitIdx );
|
||||
|
||||
// xoffset on next line = paddingStart + (width of content from splitIdx to
|
||||
// idx). width of content from splitIdx to idx = (xoffset - initialXOffset)
|
||||
Float pendingWidth = xoffset - initialXOffset;
|
||||
xoffset = info.paddingStart + pendingWidth;
|
||||
} else {
|
||||
if constexpr ( std::is_same_v<LineWrapType, LineWrapInfoEx> ) {
|
||||
info.wrapsWidth.push_back( std::ceil( xoffset - w ) );
|
||||
}
|
||||
|
||||
info.wraps.push_back( idx );
|
||||
xoffset = info.paddingStart;
|
||||
}
|
||||
}
|
||||
lastSpace = 0;
|
||||
lastSpace = std::string::npos;
|
||||
lastWordWrapWidth = 0.f;
|
||||
} else if ( isWrapChar( curChar ) ) {
|
||||
lastSpace = idx;
|
||||
|
||||
227
src/eepp/graphics/richtext.cpp
Normal file
227
src/eepp/graphics/richtext.cpp
Normal file
@@ -0,0 +1,227 @@
|
||||
#include <eepp/graphics/font.hpp>
|
||||
#include <eepp/graphics/fontmanager.hpp>
|
||||
#include <eepp/graphics/linewrap.hpp>
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/graphics/richtext.hpp>
|
||||
|
||||
namespace EE { namespace Graphics {
|
||||
|
||||
RichText* RichText::New() {
|
||||
return eeNew( RichText, () );
|
||||
}
|
||||
|
||||
RichText::RichText() : Drawable( Drawable::RICHTEXT ) {}
|
||||
|
||||
RichText::~RichText() {}
|
||||
|
||||
void RichText::draw() {
|
||||
draw( mPosition.x, mPosition.y );
|
||||
}
|
||||
|
||||
void RichText::draw( const Vector2f& position ) {
|
||||
draw( position.x, position.y );
|
||||
}
|
||||
|
||||
void RichText::draw( const Vector2f& position, const Sizef& size ) {
|
||||
Sizef s = getSize();
|
||||
if ( s != Sizef::Zero ) {
|
||||
draw( position.x, position.y,
|
||||
{ size.getWidth() / s.getWidth(), size.getHeight() / s.getHeight() } );
|
||||
}
|
||||
}
|
||||
|
||||
void RichText::draw( const Float& X, const Float& Y, const Vector2f& scale, const Float& rotation,
|
||||
BlendMode effect, const OriginPoint& rotationCenter,
|
||||
const OriginPoint& scaleCenter ) {
|
||||
updateLayout();
|
||||
|
||||
for ( auto& line : mLines ) {
|
||||
for ( auto& span : line.spans ) {
|
||||
// span.position is local to the line (x) + baseline offset (y) line.Y is the line
|
||||
// vertical offset
|
||||
|
||||
Vector2f pos = span.position;
|
||||
|
||||
if ( rotation == 0 && scale == Vector2f::One ) {
|
||||
span.text->draw( std::trunc( X + pos.x ), std::trunc( Y + line.y + pos.y ),
|
||||
Vector2f::One, 0, effect );
|
||||
} else {
|
||||
span.text->draw( std::trunc( X + pos.x * scale.x ),
|
||||
std::trunc( Y + ( line.y + pos.y ) * scale.y ), scale, rotation,
|
||||
effect, rotationCenter, scaleCenter );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Sizef RichText::getPixelsSize() {
|
||||
return getSize();
|
||||
}
|
||||
|
||||
void RichText::addSpan( const String& text, const FontStyleConfig& style ) {
|
||||
if ( text.empty() )
|
||||
return;
|
||||
|
||||
auto span = std::make_shared<Text>();
|
||||
span->setString( text );
|
||||
span->setStyleConfig( style );
|
||||
mSpans.push_back( span );
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
|
||||
void RichText::addSpan( const String& text, Font* font, Uint32 characterSize, Color color,
|
||||
Uint32 style ) {
|
||||
FontStyleConfig config;
|
||||
config.Font = font ? font : mDefaultStyle.Font;
|
||||
config.CharacterSize = characterSize != 0 ? characterSize : mDefaultStyle.CharacterSize;
|
||||
config.FontColor = color;
|
||||
config.Style = style;
|
||||
config.ShadowColor = mDefaultStyle.ShadowColor;
|
||||
config.ShadowOffset = mDefaultStyle.ShadowOffset;
|
||||
config.OutlineThickness = mDefaultStyle.OutlineThickness;
|
||||
config.OutlineColor = mDefaultStyle.OutlineColor;
|
||||
|
||||
addSpan( text, config );
|
||||
}
|
||||
|
||||
void RichText::clear() {
|
||||
mSpans.clear();
|
||||
mLines.clear();
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
|
||||
void RichText::setFontStyleConfig( const FontStyleConfig& styleConfig ) {
|
||||
mDefaultStyle = styleConfig;
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
|
||||
void RichText::setAlign( Uint32 align ) {
|
||||
if ( mAlign != align ) {
|
||||
mAlign = align;
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
void RichText::setMaxWidth( Float width ) {
|
||||
if ( mMaxWidth != width ) {
|
||||
mMaxWidth = width;
|
||||
mNeedsLayoutUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
void RichText::invalidate() {
|
||||
mNeedsLayoutUpdate = true;
|
||||
for ( auto& span : mSpans ) {
|
||||
span->invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
void RichText::updateLayout() {
|
||||
if ( !mNeedsLayoutUpdate )
|
||||
return;
|
||||
|
||||
mLines.clear();
|
||||
mLines.push_back( RenderParagraph() );
|
||||
|
||||
Float curX = 0;
|
||||
Float maxWidth = 0;
|
||||
|
||||
for ( auto& span : mSpans ) {
|
||||
if ( span->getString().empty() )
|
||||
continue;
|
||||
|
||||
auto& fontStyle = span->getFontStyleConfig();
|
||||
if ( !fontStyle.Font )
|
||||
continue;
|
||||
|
||||
Uint32 textHints = span->getTextHints();
|
||||
|
||||
LineWrapInfoEx wrapInfo = LineWrap::computeLineBreaksEx(
|
||||
span->getString(), fontStyle, mMaxWidth > 0 ? mMaxWidth : 1e9f,
|
||||
mMaxWidth > 0 ? LineWrapMode::Word : LineWrapMode::NoWrap, false, 4, 0.f, textHints,
|
||||
false, curX );
|
||||
|
||||
// Make sure we have the end of the string as a "wrap" point for the loop
|
||||
if ( wrapInfo.wraps.empty() || wrapInfo.wraps.back() != (Float)span->getString().size() )
|
||||
wrapInfo.wraps.push_back( span->getString().size() );
|
||||
|
||||
for ( size_t i = 0; i < wrapInfo.wraps.size() - 1; ++i ) {
|
||||
size_t startIdx = wrapInfo.wraps[i];
|
||||
size_t endIdx = wrapInfo.wraps[i + 1];
|
||||
bool isNewline = ( endIdx - startIdx == 1 && span->getString()[startIdx] == '\n' );
|
||||
|
||||
if ( !isNewline ) {
|
||||
std::shared_ptr<Text> renderSpanText = std::make_shared<Text>();
|
||||
renderSpanText->setString(
|
||||
span->getString().substr( startIdx, endIdx - startIdx ) );
|
||||
renderSpanText->setStyleConfig( fontStyle );
|
||||
|
||||
RenderSpan renderSpan;
|
||||
renderSpan.text = renderSpanText;
|
||||
renderSpan.position = { curX, 0 }; // Y adjusted later
|
||||
|
||||
RenderParagraph& currentLine = mLines.back();
|
||||
currentLine.spans.push_back( renderSpan );
|
||||
|
||||
Float ascent = fontStyle.Font->getAscent( fontStyle.CharacterSize );
|
||||
Float height = fontStyle.Font->getLineSpacing( fontStyle.CharacterSize );
|
||||
|
||||
currentLine.maxAscent = std::max( currentLine.maxAscent, ascent );
|
||||
currentLine.height = std::max( currentLine.height, height );
|
||||
|
||||
Float spanWidth = renderSpan.text->getTextWidth();
|
||||
curX += spanWidth;
|
||||
currentLine.width += spanWidth;
|
||||
}
|
||||
|
||||
// If it's a newline, or if it's not the very last segment (which means it wrapped),
|
||||
// start a new line. Exception: If the last segment was just a newline, we already
|
||||
// handled it.
|
||||
if ( i < wrapInfo.wraps.size() - 2 || isNewline ) {
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
mLines.push_back( RenderParagraph() );
|
||||
curX = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
maxWidth = std::max( maxWidth, curX );
|
||||
|
||||
if ( !mLines.empty() && mLines.back().spans.empty() && mLines.size() > 1 ) {
|
||||
mLines.pop_back();
|
||||
}
|
||||
|
||||
Float curY = 0;
|
||||
for ( auto& line : mLines ) {
|
||||
line.y = curY;
|
||||
|
||||
Float xOffset = 0;
|
||||
if ( mMaxWidth > 0 && mAlign != 0 ) {
|
||||
Uint32 hAlign = Font::getHorizontalAlign( mAlign );
|
||||
if ( hAlign == TEXT_ALIGN_CENTER ) {
|
||||
xOffset = ( mMaxWidth - line.width ) * 0.5f;
|
||||
} else if ( hAlign == TEXT_ALIGN_RIGHT ) {
|
||||
xOffset = mMaxWidth - line.width;
|
||||
}
|
||||
}
|
||||
|
||||
for ( auto& span : line.spans ) {
|
||||
Float ascent = span.text->getFont()->getAscent( span.text->getCharacterSize() );
|
||||
Float offsetY = line.maxAscent - ascent;
|
||||
span.position.x += xOffset;
|
||||
span.position.y = offsetY;
|
||||
}
|
||||
|
||||
curY += line.height;
|
||||
}
|
||||
|
||||
mSize = Sizef( maxWidth, curY );
|
||||
mNeedsLayoutUpdate = false;
|
||||
}
|
||||
|
||||
Sizef RichText::getSize() {
|
||||
updateLayout();
|
||||
return mSize;
|
||||
}
|
||||
|
||||
}} // namespace EE::Graphics
|
||||
@@ -821,8 +821,8 @@ Vector2f Text::findCharacterPos( std::size_t index ) const {
|
||||
|
||||
Vector2f pos = Text::findCharacterPos(
|
||||
index - startIdx, mFontStyleConfig.Font, mFontStyleConfig.CharacterSize, strWrapper,
|
||||
mFontStyleConfig.Style, mTabWidth, mFontStyleConfig.OutlineThickness, {}, true,
|
||||
mTextHints );
|
||||
mFontStyleConfig.Style, mTabWidth, mFontStyleConfig.OutlineThickness, {}, true, mTextHints,
|
||||
TextDirection::Unspecified, lineIndex == 0 ? mInitialOffset : Vector2f::Zero );
|
||||
|
||||
return Vector2f( pos.x + centerDiffX, y );
|
||||
}
|
||||
@@ -1167,6 +1167,47 @@ Vector2f Text::findCharacterPos( std::size_t index, Font* font, const Uint32& fo
|
||||
}
|
||||
#endif
|
||||
|
||||
// If soft-wrap is enabled and we are not using the shaper (or it's skipped), we need to compute
|
||||
// line breaks to correctly calculate the position.
|
||||
if ( lineWrapMode != LineWrapMode::NoWrap ) {
|
||||
if ( index == 0 )
|
||||
return position;
|
||||
|
||||
LineWrapInfo info = LineWrap::computeLineBreaks(
|
||||
string, font, fontSize, maxWrapWidth, lineWrapMode, style, outlineThickness, false,
|
||||
tabWidth, 0.f, textDrawHints, false, initialOffset.x );
|
||||
|
||||
size_t lineIndex = 0;
|
||||
size_t lineStartIdx = 0;
|
||||
|
||||
for ( size_t i = 1; i < info.wraps.size(); ++i ) {
|
||||
if ( index < static_cast<size_t>( info.wraps[i] ) ) {
|
||||
break;
|
||||
}
|
||||
lineIndex = i;
|
||||
lineStartIdx = info.wraps[i];
|
||||
}
|
||||
|
||||
if ( lineIndex > 0 ) {
|
||||
position.x = info.paddingStart;
|
||||
position.x += info.paddingStart;
|
||||
position.y += vspace * lineIndex;
|
||||
}
|
||||
|
||||
Float segmentWidth = 0;
|
||||
if ( index > lineStartIdx ) {
|
||||
std::optional<Float> currentTabOffset =
|
||||
lineIndex == 0 ? initialOffset.x : info.paddingStart;
|
||||
String::View segment = string.view().substr( lineStartIdx, index - lineStartIdx );
|
||||
segmentWidth =
|
||||
Text::getTextWidth( font, fontSize, segment, style, tabWidth, outlineThickness,
|
||||
textDrawHints, direction, currentTabOffset );
|
||||
}
|
||||
|
||||
position.x += segmentWidth;
|
||||
return position;
|
||||
}
|
||||
|
||||
Uint32 prevChar = 0;
|
||||
bool isMonospace = font->isMonospace();
|
||||
for ( std::size_t i = 0; i < index; ++i ) {
|
||||
|
||||
@@ -578,6 +578,17 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result,
|
||||
|
||||
bool performWordWrap =
|
||||
( lineWrapMode == LineWrapMode::Word && lastSpace != std::string::npos );
|
||||
bool forceNextLine = false;
|
||||
|
||||
if ( !performWordWrap && lineWrapMode == LineWrapMode::Word &&
|
||||
sp.wrapInfo.wraps.size() == 1 ) {
|
||||
// Check if fits in next line
|
||||
Float currentWordWidth =
|
||||
sg.position.x + sg.advance.x - sp.shapedGlyphs[0].position.x;
|
||||
if ( currentWordWidth <= wrapWidth - sp.wrapInfo.paddingStart ) {
|
||||
forceNextLine = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( performWordWrap ) {
|
||||
ShapedGlyph& prevBreakGlyph = sp.shapedGlyphs[lastSpace];
|
||||
@@ -589,6 +600,9 @@ void TextLayout::wrapLayout( const String::View& string, TextLayout& result,
|
||||
if ( performWordWrap ) {
|
||||
breakIndex = lastSpace + 1;
|
||||
breakStringIdx = lastSpaceStringIdx + 1;
|
||||
} else if ( forceNextLine ) {
|
||||
breakIndex = 0;
|
||||
breakStringIdx = sp.shapedGlyphs[0].stringIndex;
|
||||
}
|
||||
|
||||
if ( breakIndex > idx )
|
||||
|
||||
115
src/examples/richtext/richtext.cpp
Normal file
115
src/examples/richtext/richtext.cpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#include <eepp/ee.hpp>
|
||||
#include <eepp/graphics/fontfamily.hpp>
|
||||
#include <eepp/graphics/fonttruetype.hpp>
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/graphics/richtext.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::Graphics;
|
||||
using namespace EE::Window;
|
||||
|
||||
void runRichTextTest() {
|
||||
auto win = Engine::instance()->createWindow( WindowSettings( 1024, 768, "RichText Example" ) );
|
||||
|
||||
if ( !win->isOpen() )
|
||||
return;
|
||||
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
FontTrueType* font =
|
||||
FontTrueType::New( "NotoSans-Regular", "assets/fonts/NotoSans-Regular.ttf" );
|
||||
|
||||
if ( !font || !font->loaded() )
|
||||
return;
|
||||
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
RichText richText;
|
||||
richText.getFontStyleConfig().Font = font;
|
||||
richText.getFontStyleConfig().CharacterSize = 24;
|
||||
richText.setAlign( TEXT_ALIGN_LEFT );
|
||||
|
||||
// Add spans using the helper method
|
||||
richText.addSpan( "Hello " );
|
||||
richText.addSpan( "bold world", nullptr, 24, Color::Red,
|
||||
Text::Bold ); // Use nullptr to use the font associated with the style
|
||||
// (if loaded via FontFamily)
|
||||
richText.addSpan( "! This is a " );
|
||||
richText.addSpan( "colored", nullptr, 0,
|
||||
Color::Green ); // Inherit font and size, change color
|
||||
richText.addSpan( " processing example. " );
|
||||
richText.addSpan( "It should support " );
|
||||
richText.addSpan( "soft wrapping", nullptr, 0, Color::Blue, Text::Italic );
|
||||
richText.addSpan( " across multiple lines if the text is long enough. " );
|
||||
richText.addSpan( "And also " );
|
||||
richText.addSpan( "different font sizes", nullptr, 32,
|
||||
Color( 255, 0, 255, 255 ) ); // Magenta manually
|
||||
richText.addSpan( " in the same block." );
|
||||
|
||||
richText.setMaxWidth( std::ceil( win->getWidth() * 0.4 ) );
|
||||
richText.setPosition( { 25.f, 50.f } );
|
||||
|
||||
RichText richText2 = richText;
|
||||
richText2.setPosition(
|
||||
richText2.getPosition() + Vector2f{ 25.f, 0.f } +
|
||||
Vector2f{ static_cast<Float>( std::ceil( win->getWidth() * 0.4 ) ), 0 } );
|
||||
richText2.setMaxWidth( std::ceil( win->getWidth() * 0.1 ) );
|
||||
|
||||
RichText richText3 = richText2;
|
||||
richText3.setPosition(
|
||||
Vector2f{ 25.f, 50.f } +
|
||||
Vector2f{ static_cast<Float>( std::ceil( win->getWidth() * 0.6 ) ), 0 } );
|
||||
richText3.setMaxWidth( win->getWidth() - richText3.getPosition().x );
|
||||
|
||||
win->getInput()->pushCallback( [&]( InputEvent* event ) {
|
||||
if ( event->Type == InputEvent::VideoResize ) {
|
||||
richText.setMaxWidth( std::ceil( win->getWidth() * 0.4 ) );
|
||||
|
||||
richText2.setPosition(
|
||||
richText.getPosition() + Vector2f{ 25.f, 0.f } +
|
||||
Vector2f{ static_cast<Float>( std::ceil( win->getWidth() * 0.4 ) ), 0 } );
|
||||
richText2.setMaxWidth( std::ceil( win->getWidth() * 0.1 ) );
|
||||
|
||||
richText3.setPosition(
|
||||
Vector2f{ 25.f, 50.f } +
|
||||
Vector2f{ static_cast<Float>( std::ceil( win->getWidth() * 0.6 ) ), 0 } );
|
||||
richText3.setMaxWidth( win->getWidth() - richText3.getPosition().x );
|
||||
}
|
||||
} );
|
||||
|
||||
while ( win->isRunning() ) {
|
||||
win->getInput()->update();
|
||||
|
||||
if ( win->getInput()->isKeyUp( KEY_ESCAPE ) ) {
|
||||
win->close();
|
||||
}
|
||||
|
||||
win->setClearColor( Color( 200, 200, 200 ) );
|
||||
win->clear();
|
||||
|
||||
// Draw a line to show the wrap width
|
||||
Float boxWidth = std::ceil( win->getWidth() * 0.4 );
|
||||
Primitives p;
|
||||
p.setColor( Color::Black );
|
||||
|
||||
Float line1X = richText.getPosition().x + boxWidth;
|
||||
p.drawLine( { { line1X, 0 }, { line1X, (Float)win->getHeight() } } );
|
||||
|
||||
Float line2X =
|
||||
richText2.getPosition().x + static_cast<Float>( std::ceil( win->getWidth() * 0.1 ) );
|
||||
p.drawLine( { { line2X, 0 }, { line2X, (Float)win->getHeight() } } );
|
||||
|
||||
richText.draw();
|
||||
richText2.draw();
|
||||
richText3.draw();
|
||||
|
||||
win->display();
|
||||
}
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
EE_MAIN_FUNC int main( int argc, char* argv[] ) {
|
||||
runRichTextTest();
|
||||
return 0;
|
||||
}
|
||||
60
src/tests/unit_tests/compareimages.hpp
Normal file
60
src/tests/unit_tests/compareimages.hpp
Normal file
@@ -0,0 +1,60 @@
|
||||
#include "utest.h"
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <eepp/graphics/image.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/window/window.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::System;
|
||||
using namespace EE::Graphics;
|
||||
using namespace EE::Window;
|
||||
|
||||
static void compareImages( utest_state_s& utest_state, int* utest_result, EE::Window::Window* win,
|
||||
const std::string& imageName,
|
||||
const std::string& imagesFolder = "fontrendering" ) {
|
||||
auto saveType = Image::SaveType::WEBP;
|
||||
auto saveExt( Image::saveTypeToExtension( saveType ) );
|
||||
std::string expectedImagePath( "assets/" + imagesFolder + "/" + imageName + "." + saveExt );
|
||||
|
||||
Image::FormatConfiguration fconf;
|
||||
fconf.webpSaveLossless( true );
|
||||
|
||||
Image actualImage = win->getFrontBufferImage();
|
||||
actualImage.setImageFormatConfiguration( fconf );
|
||||
|
||||
if ( !FileSystem::fileExists( expectedImagePath ) )
|
||||
actualImage.saveToFile( expectedImagePath, saveType );
|
||||
|
||||
Image expectedImage( expectedImagePath );
|
||||
ASSERT_TRUE( expectedImage.getPixelsPtr() != nullptr );
|
||||
EXPECT_EQ_MSG( expectedImage.getWidth(), actualImage.getWidth(), "Images width not equal" );
|
||||
EXPECT_EQ_MSG( expectedImage.getHeight(), actualImage.getHeight(), "Images height not equal" );
|
||||
|
||||
Image::DiffResult result = actualImage.diff( expectedImage );
|
||||
EXPECT_TRUE( result.areSame() );
|
||||
if ( !result.areSame() ) {
|
||||
auto saveExt( Image::saveTypeToExtension( saveType ) );
|
||||
std::string withTextShaper =
|
||||
Text::TextShaperEnabled
|
||||
? ( Text::TextShaperOptimizations ? "_text_shape_no_opt" : "_text_shape" )
|
||||
: "";
|
||||
std::cerr << "Test FAILED: " << result.numDifferentPixels << " pixels differ." << std::endl;
|
||||
std::cerr << "Maximum perceptual difference (Delta E): " << result.maxDeltaE << std::endl;
|
||||
if ( !FileSystem::fileExists( "output" ) )
|
||||
FileSystem::makeDir( "output" );
|
||||
std::string actualImagePath =
|
||||
"output/" + imageName + "_actual_output" + withTextShaper + "." + saveExt;
|
||||
actualImage.saveToFile( actualImagePath, saveType );
|
||||
std::cerr << "Actual image saved to: " << actualImagePath << std::endl;
|
||||
if ( result.diffImage ) {
|
||||
std::string diffImagePath =
|
||||
"output/" + imageName + "_diff_output" + withTextShaper + "." + saveExt;
|
||||
result.diffImage->setImageFormatConfiguration( fconf );
|
||||
result.diffImage->saveToFile( diffImagePath, saveType );
|
||||
std::cerr << "Visual diff saved to: " << diffImagePath << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
#include "compareimages.hpp"
|
||||
#include "utest.hpp"
|
||||
|
||||
#include <eepp/graphics/batchrenderer.hpp>
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <eepp/graphics/image.hpp>
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/graphics/renderer/renderergl.hpp>
|
||||
#include <eepp/graphics/richtext.hpp>
|
||||
#include <eepp/graphics/text.hpp>
|
||||
#include <eepp/scene/scenemanager.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
@@ -24,8 +26,6 @@
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
#include <eepp/window/engine.hpp>
|
||||
|
||||
#include <iostream>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::Scene;
|
||||
using namespace EE::System;
|
||||
@@ -33,52 +33,6 @@ using namespace EE::Graphics;
|
||||
using namespace EE::Window;
|
||||
using namespace EE::UI;
|
||||
|
||||
static void compareImages( utest_state_s& utest_state, int* utest_result, EE::Window::Window* win,
|
||||
const std::string& imageName ) {
|
||||
auto saveType = Image::SaveType::WEBP;
|
||||
auto saveExt( Image::saveTypeToExtension( saveType ) );
|
||||
std::string expectedImagePath( "assets/fontrendering/" + imageName + "." + saveExt );
|
||||
|
||||
Image::FormatConfiguration fconf;
|
||||
fconf.webpSaveLossless( true );
|
||||
|
||||
Image actualImage = win->getFrontBufferImage();
|
||||
actualImage.setImageFormatConfiguration( fconf );
|
||||
|
||||
if ( !FileSystem::fileExists( expectedImagePath ) )
|
||||
actualImage.saveToFile( expectedImagePath, saveType );
|
||||
|
||||
Image expectedImage( expectedImagePath );
|
||||
ASSERT_TRUE( expectedImage.getPixelsPtr() != nullptr );
|
||||
EXPECT_EQ_MSG( expectedImage.getWidth(), actualImage.getWidth(), "Images width not equal" );
|
||||
EXPECT_EQ_MSG( expectedImage.getHeight(), actualImage.getHeight(), "Images height not equal" );
|
||||
|
||||
Image::DiffResult result = actualImage.diff( expectedImage );
|
||||
EXPECT_TRUE( result.areSame() );
|
||||
if ( !result.areSame() ) {
|
||||
auto saveExt( Image::saveTypeToExtension( saveType ) );
|
||||
std::string withTextShaper =
|
||||
Text::TextShaperEnabled
|
||||
? ( Text::TextShaperOptimizations ? "_text_shape_no_opt" : "_text_shape" )
|
||||
: "";
|
||||
std::cerr << "Test FAILED: " << result.numDifferentPixels << " pixels differ." << std::endl;
|
||||
std::cerr << "Maximum perceptual difference (Delta E): " << result.maxDeltaE << std::endl;
|
||||
if ( !FileSystem::fileExists( "output" ) )
|
||||
FileSystem::makeDir( "output" );
|
||||
std::string actualImagePath =
|
||||
"output/" + imageName + "_actual_output" + withTextShaper + "." + saveExt;
|
||||
actualImage.saveToFile( actualImagePath, saveType );
|
||||
std::cerr << "Actual image saved to: " << actualImagePath << std::endl;
|
||||
if ( result.diffImage ) {
|
||||
std::string diffImagePath =
|
||||
"output/" + imageName + "_diff_output" + withTextShaper + "." + saveExt;
|
||||
result.diffImage->setImageFormatConfiguration( fconf );
|
||||
result.diffImage->saveToFile( diffImagePath, saveType );
|
||||
std::cerr << "Visual diff saved to: " << diffImagePath << std::endl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UTEST( FontRendering, fontsTest ) {
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
|
||||
235
src/tests/unit_tests/richtext.cpp
Normal file
235
src/tests/unit_tests/richtext.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
#include "compareimages.hpp"
|
||||
#include "utest.hpp"
|
||||
|
||||
#include <eepp/graphics/fontfamily.hpp>
|
||||
#include <eepp/graphics/fonttruetype.hpp>
|
||||
#include <eepp/graphics/primitives.hpp>
|
||||
#include <eepp/graphics/richtext.hpp>
|
||||
#include <eepp/system/filesystem.hpp>
|
||||
#include <eepp/system/scopedop.hpp>
|
||||
#include <eepp/system/sys.hpp>
|
||||
#include <eepp/window/engine.hpp>
|
||||
|
||||
using namespace EE;
|
||||
using namespace EE::Graphics;
|
||||
using namespace EE::Window;
|
||||
|
||||
UTEST( RichText, basicFunctionality ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Test",
|
||||
WindowStyle::Default, WindowBackend::Default,
|
||||
32, {}, 1, false, true ) );
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
|
||||
ASSERT_TRUE( font->loaded() );
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
RichText richText;
|
||||
richText.getFontStyleConfig().Font = font;
|
||||
richText.getFontStyleConfig().CharacterSize = 12;
|
||||
|
||||
richText.addSpan( "Hello " );
|
||||
richText.addSpan( "world" );
|
||||
richText.addSpan( "bold", nullptr, 0, Color::White, Text::Bold );
|
||||
|
||||
// Force layout update
|
||||
Sizef size = richText.getSize();
|
||||
EXPECT_TRUE( size.getWidth() > 0 );
|
||||
EXPECT_TRUE( size.getHeight() > 0 );
|
||||
|
||||
// Check that we have lines and spans
|
||||
const auto& lines = richText.getLines();
|
||||
EXPECT_FALSE( lines.empty() );
|
||||
if ( !lines.empty() ) {
|
||||
EXPECT_FALSE( lines[0].spans.empty() );
|
||||
// Check that spans have increasing X positions
|
||||
if ( lines[0].spans.size() >= 2 ) {
|
||||
EXPECT_GT( lines[0].spans[1].position.x, lines[0].spans[0].position.x );
|
||||
}
|
||||
}
|
||||
|
||||
// Check wrapping
|
||||
Float fullWidth = size.getWidth();
|
||||
richText.setMaxWidth( fullWidth / 2 );
|
||||
|
||||
Sizef wrappedSize = richText.getSize();
|
||||
EXPECT_LT( wrappedSize.getWidth(), fullWidth );
|
||||
EXPECT_GT( wrappedSize.getHeight(), size.getHeight() );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( RichText, BaselineAlignment ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "RichText Baseline",
|
||||
WindowStyle::Default, WindowBackend::Default,
|
||||
32, {}, 1, false, true ) );
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
ASSERT_TRUE( font->loaded() );
|
||||
|
||||
RichText richText;
|
||||
richText.getFontStyleConfig().Font = font;
|
||||
richText.addSpan( "Large", nullptr, 30 );
|
||||
richText.addSpan( "Small", nullptr, 12 );
|
||||
|
||||
richText.getSize(); // Update layout
|
||||
|
||||
const auto& lines = richText.getLines();
|
||||
ASSERT_EQ( lines.size(), (size_t)1 );
|
||||
ASSERT_EQ( lines[0].spans.size(), (size_t)2 );
|
||||
|
||||
const auto& largeSpan = lines[0].spans[0];
|
||||
const auto& smallSpan = lines[0].spans[1];
|
||||
|
||||
// Large span should be at the top of the line (offset 0 relative to ascent difference)
|
||||
// Small span should be pushed down
|
||||
Float largeAscent = font->getAscent( 30 );
|
||||
Float smallAscent = font->getAscent( 12 );
|
||||
|
||||
// Expected offsets
|
||||
Float expectedLargeY = 0; // maxAscent - largeAscent = 0
|
||||
Float expectedSmallY = largeAscent - smallAscent;
|
||||
|
||||
EXPECT_NEAR( largeSpan.position.y, expectedLargeY, 0.001f );
|
||||
EXPECT_NEAR( smallSpan.position.y, expectedSmallY, 0.001f );
|
||||
|
||||
EXPECT_GT( smallSpan.position.y, largeSpan.position.y );
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( LineWrap, SoftWrapPreventsWordSplitWithOffset ) {
|
||||
Engine::instance()->createWindow( WindowSettings( 800, 600, "LineWrap Test",
|
||||
WindowStyle::Default, WindowBackend::Default,
|
||||
32, {}, 1, false, true ) );
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
FontTrueType* font = FontTrueType::New( "NotoSans-Regular" );
|
||||
font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
ASSERT_TRUE( font->loaded() );
|
||||
|
||||
String text = " World";
|
||||
Float width = Text::getTextWidth( font, 20, text, 0, 4, 0.f );
|
||||
Float maxWidth = width * 1.5f;
|
||||
Float offset = maxWidth - ( width * 0.5f );
|
||||
|
||||
LineWrapInfo info =
|
||||
LineWrap::computeLineBreaks( text, font, 20, maxWidth, LineWrapMode::Word, 0, 0.f, false, 4,
|
||||
0.f, TextHints::None, false, offset );
|
||||
|
||||
// With " World" (space at 0, W at 1).
|
||||
// LineWrap returns the index of the wrap.
|
||||
// Index 0 is always pushed at start.
|
||||
// If we wrap at 1 (skipping space), we should have at least 2 wraps: 0 and 1.
|
||||
ASSERT_GE( info.wraps.size(), (size_t)2 );
|
||||
EXPECT_EQ( info.wraps[1], 1 );
|
||||
|
||||
delete font;
|
||||
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
UTEST( RichText, RichTextTest ) {
|
||||
const auto& createRichText = []( Font* font ) -> RichText {
|
||||
RichText richText;
|
||||
richText.getFontStyleConfig().Font = font;
|
||||
richText.getFontStyleConfig().CharacterSize = 24;
|
||||
richText.setAlign( TEXT_ALIGN_LEFT );
|
||||
|
||||
// Add spans using the helper method
|
||||
richText.addSpan( "Hello " );
|
||||
richText.addSpan( "bold world", nullptr, 24, Color::Red,
|
||||
Text::Bold ); // Use nullptr to use the font associated with the style
|
||||
// (if loaded via FontFamily)
|
||||
richText.addSpan( "! This is a " );
|
||||
richText.addSpan( "colored", nullptr, 0,
|
||||
Color::Green ); // Inherit font and size, change color
|
||||
richText.addSpan( " processing example. " );
|
||||
richText.addSpan( "It should support " );
|
||||
richText.addSpan( "soft wrapping", nullptr, 0, Color::Blue, Text::Italic );
|
||||
richText.addSpan( " across multiple lines if the text is long enough. " );
|
||||
richText.addSpan( "And also " );
|
||||
richText.addSpan( "different font sizes", nullptr, 32,
|
||||
Color( 255, 0, 255, 255 ) ); // Magenta manually
|
||||
richText.addSpan( " in the same block." );
|
||||
return richText;
|
||||
};
|
||||
|
||||
const auto& runTest = [&createRichText, &utest_result]() {
|
||||
auto win =
|
||||
Engine::instance()->createWindow( WindowSettings( 1024, 650, "RichText Example" ) );
|
||||
|
||||
if ( !win->isOpen() )
|
||||
return;
|
||||
|
||||
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
|
||||
|
||||
FontTrueType* font =
|
||||
FontTrueType::New( "NotoSans-Regular", "../assets/fonts/NotoSans-Regular.ttf" );
|
||||
|
||||
ASSERT_TRUE( font && font->loaded() );
|
||||
|
||||
FontFamily::loadFromRegular( font );
|
||||
|
||||
RichText richText = createRichText( font );
|
||||
richText.setMaxWidth( std::ceil( win->getWidth() * 0.4 ) );
|
||||
richText.setPosition( { 50.f, 50.f } );
|
||||
|
||||
richText.setMaxWidth( std::ceil( win->getWidth() * 0.4 ) );
|
||||
richText.setPosition( { 25.f, 50.f } );
|
||||
|
||||
RichText richText2 = richText;
|
||||
richText2.setPosition(
|
||||
richText2.getPosition() + Vector2f{ 25.f, 0.f } +
|
||||
Vector2f{ static_cast<Float>( std::ceil( win->getWidth() * 0.4 ) ), 0 } );
|
||||
richText2.setMaxWidth( std::ceil( win->getWidth() * 0.15 ) );
|
||||
|
||||
RichText richText3 = richText2;
|
||||
richText3.setPosition(
|
||||
Vector2f{ 25.f, 50.f } +
|
||||
Vector2f{ static_cast<Float>( std::ceil( win->getWidth() * 0.6 ) ), 0 } );
|
||||
richText3.setMaxWidth( win->getWidth() - richText3.getPosition().x );
|
||||
|
||||
win->setClearColor( Color( 200, 200, 200 ) );
|
||||
win->clear();
|
||||
|
||||
// Draw a line to show the wrap width
|
||||
Float boxWidth = std::ceil( win->getWidth() * 0.4 );
|
||||
Primitives p;
|
||||
p.setColor( Color::Black );
|
||||
|
||||
Float line1X = richText.getPosition().x + boxWidth;
|
||||
p.drawLine( { { line1X, 0 }, { line1X, (Float)win->getHeight() } } );
|
||||
|
||||
Float line2X =
|
||||
richText2.getPosition().x + static_cast<Float>( std::ceil( win->getWidth() * 0.15 ) );
|
||||
p.drawLine( { { line2X, 0 }, { line2X, (Float)win->getHeight() } } );
|
||||
|
||||
richText.draw();
|
||||
richText2.draw();
|
||||
richText3.draw();
|
||||
|
||||
compareImages( utest_state, utest_result, win, "eepp-rich-text" );
|
||||
|
||||
Engine::destroySingleton();
|
||||
};
|
||||
|
||||
UTEST_PRINT_STEP( "Text Shaper disabled" );
|
||||
{
|
||||
BoolScopedOp op( Text::TextShaperEnabled, false );
|
||||
runTest();
|
||||
}
|
||||
|
||||
UTEST_PRINT_STEP( "Text Shaper enabled" );
|
||||
{
|
||||
BoolScopedOp op( Text::TextShaperEnabled, true );
|
||||
runTest();
|
||||
|
||||
UTEST_PRINT_STEP( "Text Shaper enabled w/o optimizations" );
|
||||
BoolScopedOp op2( Text::TextShaperOptimizations, false );
|
||||
runTest();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user