mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-05-28 17:16:29 +03:00
Fix anchors in UIRichText by using an special Anchor implementation for UITextSpan (extends from it).
Implemented proper hit-boxes for UITextSpan.
This commit is contained in:
@@ -115,10 +115,19 @@ class EE_API UITextSpan : public UIWidget {
|
||||
bool hasFontShadowOffset() const;
|
||||
bool hasFontBackgroundColor() const;
|
||||
|
||||
std::vector<Rectf>& getHitBoxes();
|
||||
|
||||
const std::vector<Rectf>& getHitBoxes() const;
|
||||
|
||||
void setHitBoxes( std::vector<Rectf>&& hitBoxes );
|
||||
|
||||
virtual Node* overFind( const Vector2f& point );
|
||||
|
||||
protected:
|
||||
Uint32 mStyleState{ StyleStateNone };
|
||||
String mText;
|
||||
UIFontStyleConfig mFontStyleConfig;
|
||||
std::vector<Rectf> mHitBoxes;
|
||||
|
||||
explicit UITextSpan( const std::string& tag = "span" );
|
||||
|
||||
@@ -135,6 +144,29 @@ class EE_API UITextSpan : public UIWidget {
|
||||
virtual Uint32 onMessage( const NodeMessage* Msg );
|
||||
};
|
||||
|
||||
class EE_API UIAnchorSpan : public UITextSpan {
|
||||
public:
|
||||
static UIAnchorSpan* New();
|
||||
|
||||
virtual bool applyProperty( const StyleSheetProperty& attribute );
|
||||
|
||||
virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
|
||||
const Uint32& propertyIndex = 0 ) const;
|
||||
|
||||
virtual std::vector<PropertyId> getPropertiesImplemented() const;
|
||||
|
||||
void setHref( const std::string& href );
|
||||
|
||||
const std::string& getHref() const;
|
||||
|
||||
protected:
|
||||
UIAnchorSpan( const std::string& tag = "a" );
|
||||
|
||||
std::string mHref;
|
||||
|
||||
virtual Uint32 onKeyDown( const KeyEvent& event );
|
||||
};
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
#endif
|
||||
|
||||
@@ -492,7 +492,6 @@ void UIRichText::rebuildRichText() {
|
||||
|
||||
void UIRichText::positionChildren() {
|
||||
const auto& lines = mRichText.getLines();
|
||||
|
||||
Node* child = mChild;
|
||||
|
||||
size_t currentLine = 0;
|
||||
@@ -514,42 +513,80 @@ void UIRichText::positionChildren() {
|
||||
return nullptr;
|
||||
};
|
||||
|
||||
Int64 curCharIdx = 0;
|
||||
|
||||
auto processWidget = [&]( UIWidget* widget, auto& processWidgetRef ) -> Rectf {
|
||||
Rectf bounds( std::numeric_limits<Float>::max(), std::numeric_limits<Float>::max(),
|
||||
std::numeric_limits<Float>::min(), std::numeric_limits<Float>::min() );
|
||||
constexpr Float maxF = std::numeric_limits<Float>::max();
|
||||
constexpr Float lowF = std::numeric_limits<Float>::lowest();
|
||||
Rectf bounds( maxF, maxF, lowF, lowF );
|
||||
|
||||
Vector2f offset( 0, 0 );
|
||||
Node* p = widget->getParent();
|
||||
while ( p && p != this ) {
|
||||
offset += p->isWidget() ? p->asType<UIWidget>()->getPixelsPosition() : p->getPosition();
|
||||
p = p->getParent();
|
||||
}
|
||||
|
||||
if ( widget->isType( UI_TYPE_TEXTSPAN ) ) {
|
||||
UITextSpan* textSpan = static_cast<UITextSpan*>( widget );
|
||||
Int64 startChar = curCharIdx;
|
||||
Int64 endChar = curCharIdx;
|
||||
if ( !textSpan->getText().empty() ) {
|
||||
endChar += textSpan->getText().length();
|
||||
curCharIdx = endChar;
|
||||
}
|
||||
|
||||
std::vector<Rectf>& hitBoxes = textSpan->getHitBoxes();
|
||||
hitBoxes.clear();
|
||||
|
||||
if ( startChar < endChar ) {
|
||||
for ( const auto& line : lines ) {
|
||||
bool passedText = false;
|
||||
for ( const auto& rspan : line.spans ) {
|
||||
if ( rspan.startCharIndex >= startChar && rspan.endCharIndex <= endChar ) {
|
||||
Rectf hb( mPaddingPx.Left + rspan.position.x,
|
||||
mPaddingPx.Top + line.y + rspan.position.y,
|
||||
mPaddingPx.Left + rspan.position.x + rspan.size.getWidth(),
|
||||
mPaddingPx.Top + line.y + rspan.position.y +
|
||||
rspan.size.getHeight() );
|
||||
|
||||
hitBoxes.push_back( hb );
|
||||
bounds.expand( hb );
|
||||
} else if ( rspan.startCharIndex > endChar ) {
|
||||
passedText = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( passedText )
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Node* spanChild = widget->getFirstChild();
|
||||
while ( spanChild != NULL ) {
|
||||
if ( spanChild->isWidget() ) {
|
||||
Rectf childBounds =
|
||||
processWidgetRef( static_cast<UIWidget*>( spanChild ), processWidgetRef );
|
||||
if ( childBounds.Left < bounds.Left )
|
||||
bounds.Left = childBounds.Left;
|
||||
if ( childBounds.Top < bounds.Top )
|
||||
bounds.Top = childBounds.Top;
|
||||
if ( childBounds.Right > bounds.Right )
|
||||
bounds.Right = childBounds.Right;
|
||||
if ( childBounds.Bottom > bounds.Bottom )
|
||||
bounds.Bottom = childBounds.Bottom;
|
||||
bounds.expand(
|
||||
processWidgetRef( static_cast<UIWidget*>( spanChild ), processWidgetRef ) );
|
||||
}
|
||||
spanChild = spanChild->getNextNode();
|
||||
}
|
||||
|
||||
// Ensure the parent span at least has enough size to cover its children
|
||||
if ( bounds.Left <= bounds.Right && bounds.Top <= bounds.Bottom ) {
|
||||
Vector2f offset( 0, 0 );
|
||||
Node* p = widget->getParent();
|
||||
while ( p && p != this ) {
|
||||
offset += p->isWidget() ? p->asType<UIWidget>()->getPixelsPosition()
|
||||
: p->getPosition();
|
||||
p = p->getParent();
|
||||
}
|
||||
widget->setPixelsPosition( Vector2f( bounds.Left, bounds.Top ) - offset );
|
||||
widget->setPixelsSize(
|
||||
Sizef( bounds.Right - bounds.Left, bounds.Bottom - bounds.Top ) );
|
||||
Vector2f boundsPos = bounds.getPosition();
|
||||
|
||||
widget->setPixelsPosition( boundsPos - offset );
|
||||
widget->setPixelsSize( bounds.getSize() );
|
||||
|
||||
for ( auto& hb : hitBoxes )
|
||||
hb.move( -boundsPos );
|
||||
|
||||
} else {
|
||||
hitBoxes.clear();
|
||||
}
|
||||
|
||||
} else {
|
||||
curCharIdx += 1;
|
||||
const auto* span = getNextCustomSpan();
|
||||
if ( span ) {
|
||||
size_t lineIdx = currentSpan > 0 ? currentLine : currentLine - 1;
|
||||
@@ -558,18 +595,9 @@ void UIRichText::positionChildren() {
|
||||
Vector2f targetPos( mPaddingPx.Left + span->position.x,
|
||||
mPaddingPx.Top + lineY + span->position.y );
|
||||
|
||||
Vector2f offset( 0, 0 );
|
||||
Node* p = widget->getParent();
|
||||
while ( p && p != this ) {
|
||||
offset += p->isWidget() ? p->asType<UIWidget>()->getPixelsPosition()
|
||||
: p->getPosition();
|
||||
p = p->getParent();
|
||||
}
|
||||
|
||||
widget->setPixelsPosition( targetPos - offset );
|
||||
bounds = Rectf( targetPos.x, targetPos.y,
|
||||
targetPos.x + widget->getPixelsSize().getWidth(),
|
||||
targetPos.y + widget->getPixelsSize().getHeight() );
|
||||
|
||||
bounds = Rectf( targetPos, widget->getPixelsSize() );
|
||||
}
|
||||
}
|
||||
return bounds;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <eepp/ui/uitextspan.hpp>
|
||||
#include <eepp/ui/uithememanager.hpp>
|
||||
#include <eepp/ui/uiwidgetcreator.hpp>
|
||||
#include <eepp/window/engine.hpp>
|
||||
|
||||
#define PUGIXML_HEADER_ONLY
|
||||
#include <pugixml/pugixml.hpp>
|
||||
@@ -483,4 +484,130 @@ bool UITextSpan::hasFontBackgroundColor() const {
|
||||
return 0 != ( mStyleState & StyleStateFontBackgroundColor );
|
||||
}
|
||||
|
||||
std::vector<Rectf>& UITextSpan::getHitBoxes() {
|
||||
return mHitBoxes;
|
||||
}
|
||||
|
||||
const std::vector<Rectf>& UITextSpan::getHitBoxes() const {
|
||||
return mHitBoxes;
|
||||
}
|
||||
|
||||
void UITextSpan::setHitBoxes( std::vector<Rectf>&& hitBoxes ) {
|
||||
mHitBoxes = std::move( hitBoxes );
|
||||
}
|
||||
|
||||
Node* UITextSpan::overFind( const Vector2f& point ) {
|
||||
Node* pOver = NULL;
|
||||
if ( ( mNodeFlags & NODE_FLAG_OVER_FIND_ALLOWED ) && mEnabled && mVisible ) {
|
||||
updateWorldPolygon();
|
||||
if ( mWorldBounds.contains( point ) && mPoly.pointInside( point ) ) {
|
||||
bool hit = false;
|
||||
if ( !mHitBoxes.empty() ) {
|
||||
Vector2f localPoint = convertToNodeSpace( point );
|
||||
for ( const auto& rect : mHitBoxes ) {
|
||||
if ( rect.contains( localPoint ) ) {
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hit = true;
|
||||
}
|
||||
|
||||
if ( hit ) {
|
||||
writeNodeFlag( NODE_FLAG_MOUSEOVER_ME_OR_CHILD, 1 );
|
||||
mSceneNode->addMouseOverNode( this );
|
||||
|
||||
Node* child = mChildLast;
|
||||
|
||||
while ( NULL != child ) {
|
||||
Node* childOver = child->overFind( point );
|
||||
|
||||
if ( NULL != childOver ) {
|
||||
pOver = childOver;
|
||||
break;
|
||||
}
|
||||
|
||||
child = child->getPrevNode();
|
||||
}
|
||||
|
||||
if ( NULL == pOver )
|
||||
pOver = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pOver;
|
||||
}
|
||||
|
||||
UIAnchorSpan* UIAnchorSpan::New() {
|
||||
return eeNew( UIAnchorSpan, () );
|
||||
}
|
||||
|
||||
UIAnchorSpan::UIAnchorSpan( const std::string& tag ) : UITextSpan( tag ) {
|
||||
onClick(
|
||||
[this]( const MouseEvent* ) {
|
||||
if ( !mHref.empty() )
|
||||
Engine::instance()->openURI( mHref );
|
||||
},
|
||||
EE_BUTTON_LEFT );
|
||||
}
|
||||
|
||||
bool UIAnchorSpan::applyProperty( const StyleSheetProperty& attribute ) {
|
||||
if ( !checkPropertyDefinition( attribute ) )
|
||||
return false;
|
||||
|
||||
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
|
||||
case PropertyId::Href:
|
||||
setHref( attribute.asString() );
|
||||
break;
|
||||
default:
|
||||
UITextSpan::applyProperty( attribute );
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void UIAnchorSpan::setHref( const std::string& href ) {
|
||||
if ( href != mHref ) {
|
||||
mHref = href;
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& UIAnchorSpan::getHref() const {
|
||||
return mHref;
|
||||
}
|
||||
|
||||
Uint32 UIAnchorSpan::onKeyDown( const KeyEvent& event ) {
|
||||
if ( event.getKeyCode() == KEY_KP_ENTER || event.getKeyCode() == KEY_RETURN ) {
|
||||
if ( !mHref.empty() ) {
|
||||
Engine::instance()->openURI( mHref );
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return UIWidget::onKeyDown( event );
|
||||
}
|
||||
|
||||
std::string UIAnchorSpan::getPropertyString( const PropertyDefinition* propertyDef,
|
||||
const Uint32& propertyIndex ) const {
|
||||
if ( NULL == propertyDef )
|
||||
return "";
|
||||
|
||||
switch ( propertyDef->getPropertyId() ) {
|
||||
case PropertyId::Href:
|
||||
return mHref;
|
||||
default:
|
||||
return UITextSpan::getPropertyString( propertyDef, propertyIndex );
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<PropertyId> UIAnchorSpan::getPropertiesImplemented() const {
|
||||
auto props = UITextSpan::getPropertiesImplemented();
|
||||
auto local = { PropertyId::Href };
|
||||
props.insert( props.end(), local.begin(), local.end() );
|
||||
return props;
|
||||
}
|
||||
|
||||
}} // namespace EE::UI
|
||||
|
||||
@@ -129,7 +129,7 @@ void UIWidgetCreator::createBaseWidgetList() {
|
||||
registeredWidget["tv"] = UITextView::New;
|
||||
|
||||
// HTML elements
|
||||
registeredWidget["a"] = UIAnchor::NewA;
|
||||
registeredWidget["a"] = UIAnchorSpan::New;
|
||||
registeredWidget["span"] = UITextSpan::New;
|
||||
registeredWidget["em"] = UITextSpan::NewEmphasis;
|
||||
registeredWidget["b"] = UITextSpan::NewBold;
|
||||
|
||||
@@ -29,11 +29,14 @@ This is a **bold** text and this is an *italic* text.
|
||||
|
||||
`inline code`
|
||||
|
||||
[this is a link](https://eepp.ensoft.dev)
|
||||
|
||||
```cpp
|
||||
void main() {
|
||||
printf("Hello World");
|
||||
}
|
||||
```
|
||||
|
||||
</MarkdownView>
|
||||
</ScrollView>
|
||||
</vbox>
|
||||
|
||||
@@ -584,3 +584,53 @@ UTEST( UIRichText, RichTextTest ) {
|
||||
runTest();
|
||||
}
|
||||
}
|
||||
|
||||
UTEST( UIRichText, UIAnchorTest ) {
|
||||
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 );
|
||||
|
||||
UI::UISceneNode* sceneNode = UI::UISceneNode::New();
|
||||
UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager();
|
||||
themeManager->setDefaultFont( font );
|
||||
|
||||
String xml = R"xml(
|
||||
<RichText id="rt" font-size="24dp" color="#FF0000" layout_width="300dp" layout_height="wrap_content">Default size <a id="anchor1" href="https://example.com" color="#00FF00">Link text</a> and <a id="anchor2" href="https://example.org">Another link</a></RichText>
|
||||
)xml";
|
||||
|
||||
sceneNode->loadLayoutFromString( xml );
|
||||
|
||||
UI::UIRichText* rt = sceneNode->find<UI::UIRichText>( "rt" );
|
||||
ASSERT_TRUE( rt != nullptr );
|
||||
|
||||
// force layout
|
||||
sceneNode->update( Time::Zero );
|
||||
|
||||
UI::UIAnchorSpan* anchor1 = sceneNode->find<UI::UIAnchorSpan>( "anchor1" );
|
||||
ASSERT_TRUE( anchor1 != nullptr );
|
||||
EXPECT_STRINGEQ( anchor1->getHref(), "https://example.com" );
|
||||
EXPECT_TRUE( anchor1->getHitBoxes().size() >= 1 );
|
||||
|
||||
UI::UIAnchorSpan* anchor2 = sceneNode->find<UI::UIAnchorSpan>( "anchor2" );
|
||||
ASSERT_TRUE( anchor2 != nullptr );
|
||||
EXPECT_STRINGEQ( anchor2->getHref(), "https://example.org" );
|
||||
EXPECT_TRUE( anchor2->getHitBoxes().size() >= 1 );
|
||||
|
||||
// Test that overFind correctly returns the anchor
|
||||
if ( !anchor1->getHitBoxes().empty() ) {
|
||||
Vector2f hitPos = anchor1->convertToWorldSpace(
|
||||
{ anchor1->getHitBoxes()[0].Left + 1, anchor1->getHitBoxes()[0].Top + 1 } );
|
||||
Node* hitNode = rt->overFind( hitPos );
|
||||
EXPECT_EQ( hitNode, anchor1 );
|
||||
}
|
||||
|
||||
eeDelete( sceneNode );
|
||||
Engine::destroySingleton();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user