From 8b111779a0e0bca563707dfbc5566505a9e40252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mart=C3=ADn=20Lucas=20Golini?= Date: Sat, 28 Mar 2026 23:14:28 -0300 Subject: [PATCH] Some extra WIP for the HTML support, fixed layout calculation on HTML tables and rich text elements. Fixes in the demo, probably test won't pass yet (i cannot repro the failure!). --- .github/workflows/eepp-linux-build-check.yml | 10 +-- bin/assets/ui/breeze.css | 8 ++ bin/unit_tests/assets/html/triangle.svg | 1 + include/eepp/core/string.hpp | 2 +- include/eepp/network/uri.hpp | 2 +- .../eepp/ui/css/stylesheetselectorrule.hpp | 4 +- include/eepp/ui/tools/htmlformatter.hpp | 2 + include/eepp/ui/uilayout.hpp | 2 + include/eepp/ui/uiscenenode.hpp | 4 + include/eepp/ui/uistate.hpp | 4 + include/eepp/ui/uitextspan.hpp | 2 + src/eepp/core/string.cpp | 2 +- src/eepp/network/uri.cpp | 2 +- src/eepp/ui/css/drawableimageparser.cpp | 8 +- src/eepp/ui/css/stylesheetlength.cpp | 2 +- src/eepp/ui/css/stylesheetselectorrule.cpp | 9 ++- src/eepp/ui/tools/htmlformatter.cpp | 68 +++++++++++++++- src/eepp/ui/uihtmltable.cpp | 2 +- src/eepp/ui/uilayout.cpp | 20 ++++- src/eepp/ui/uirichtext.cpp | 11 ++- src/eepp/ui/uiscenenode.cpp | 80 ++++++++++++++++--- src/eepp/ui/uistate.cpp | 9 ++- src/eepp/ui/uitextspan.cpp | 5 +- src/eepp/ui/uiwidget.cpp | 6 ++ src/eepp/ui/uiwidgetcreator.cpp | 11 +-- src/examples/ui_html/ui_html.cpp | 56 +++++++------ src/tests/unit_tests/uihtml_tests.cpp | 16 ++-- 27 files changed, 264 insertions(+), 84 deletions(-) create mode 100644 bin/unit_tests/assets/html/triangle.svg diff --git a/.github/workflows/eepp-linux-build-check.yml b/.github/workflows/eepp-linux-build-check.yml index f0d2a9a08..008b376df 100644 --- a/.github/workflows/eepp-linux-build-check.yml +++ b/.github/workflows/eepp-linux-build-check.yml @@ -30,15 +30,7 @@ jobs: - name: Unit Tests run: | cd bin/unit_tests - xvfb-run -s "-screen 0 1280x1024x24" \ - gdb --batch --quiet \ - -ex "set confirm off" \ - -ex "run" \ - -ex "bt full" \ - -ex "thread apply all bt full" \ - -ex "info registers" \ - -ex "quit" \ - --args ./eepp-unit_tests + xvfb-run -s "-screen 0 1280x1024x24" gdb --batch --quiet --return-child-result -ex "run" -ex "bt full" --args ./eepp-unit_tests - name: Upload artifacts if: always() uses: actions/upload-artifact@v4 diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index fcabc9604..afef3ec76 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -107,6 +107,14 @@ h6 { margin: 1.67em 0; } +table, td { + text-align: left; +} + +center { + text-align: center; +} + markdownview code { font-family: monospace; background-color: var(--list-back); diff --git a/bin/unit_tests/assets/html/triangle.svg b/bin/unit_tests/assets/html/triangle.svg new file mode 100644 index 000000000..6da385e37 --- /dev/null +++ b/bin/unit_tests/assets/html/triangle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/include/eepp/core/string.hpp b/include/eepp/core/string.hpp index ab677f21a..96633e6a4 100644 --- a/include/eepp/core/string.hpp +++ b/include/eepp/core/string.hpp @@ -382,7 +382,7 @@ class EE_API String { static int fuzzyMatch( const std::string& pattern, const std::string& string ); /** Replace all occurrences of the search string with the replacement string. */ - static void replaceAll( std::string& target, const std::string& that, const std::string& with ); + static void replaceAll( std::string& target, std::string_view that, std::string_view with ); /** Replace all occurrences of the search string with the replacement string. */ static void replaceAll( String& target, const String& that, const String& with ); diff --git a/include/eepp/network/uri.hpp b/include/eepp/network/uri.hpp index 36348e506..ef8bce336 100644 --- a/include/eepp/network/uri.hpp +++ b/include/eepp/network/uri.hpp @@ -179,7 +179,7 @@ class EE_API URI { const std::string& getFragment() const; /** Sets the fragment part of the URI. */ - void getFragment( const std::string& fragment ); + void setFragment( const std::string& fragment ); /** Sets the path, query and fragment parts of the URI. */ void setPathEtc( const std::string& pathEtc ); diff --git a/include/eepp/ui/css/stylesheetselectorrule.hpp b/include/eepp/ui/css/stylesheetselectorrule.hpp index e188e2db9..684fcb882 100644 --- a/include/eepp/ui/css/stylesheetselectorrule.hpp +++ b/include/eepp/ui/css/stylesheetselectorrule.hpp @@ -21,9 +21,11 @@ class EE_API StyleSheetSelectorRule { Pressed = ( 1 << 3 ), Disabled = ( 1 << 4 ), FocusWithin = ( 1 << 5 ), + Link = ( 1 << 6 ), + Visited = ( 1 << 7 ), }; - static constexpr auto PseudoClassesTotal = 6; + static constexpr auto PseudoClassesTotal = 8; enum TypeIdentifier { TAG = 0, diff --git a/include/eepp/ui/tools/htmlformatter.hpp b/include/eepp/ui/tools/htmlformatter.hpp index 38e00ae93..528aa6e08 100644 --- a/include/eepp/ui/tools/htmlformatter.hpp +++ b/include/eepp/ui/tools/htmlformatter.hpp @@ -21,6 +21,8 @@ class EE_API HTMLFormatter { static pugi::xml_node getLogicalNext( const pugi::xml_node& node ); static String collapseXmlWhitespace( const String& text, const pugi::xml_node& node ); + + static std::string HTMLtoXML( const std::string& layoutString ); }; }}} // namespace EE::UI::Tools diff --git a/include/eepp/ui/uilayout.hpp b/include/eepp/ui/uilayout.hpp index 9eed3cfed..910aa23af 100644 --- a/include/eepp/ui/uilayout.hpp +++ b/include/eepp/ui/uilayout.hpp @@ -45,6 +45,8 @@ class EE_API UILayout : public UIWidget { virtual void updateLayoutTree(); + virtual void updateLayoutWrappingContents(); + void setLayoutDirty(); bool setMatchParentIfNeededVerticalGrowth(); diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 1ad1c9abc..740269baa 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -689,6 +689,10 @@ class EE_API UISceneNode : public SceneNode { /** Sets the document / scene URI used to resolve paths of inner elements */ void setURI( const URI& uri ); + /** Sets the document / scene URI used to resolve paths from a complete URI (with + * path+query+fragment+etc) */ + void setURIFromURL( const URI& url ); + /** @return the document / scene URI used to resolve paths of inner elements */ const URI& getURI() const { return mURI; } diff --git a/include/eepp/ui/uistate.hpp b/include/eepp/ui/uistate.hpp index 9ab668b2e..4cf6c7028 100644 --- a/include/eepp/ui/uistate.hpp +++ b/include/eepp/ui/uistate.hpp @@ -20,6 +20,8 @@ class EE_API UIState { StateDisabled, StateChecked, StateFocusWithin, + StateLink, + StateVisited, StateCount }; @@ -34,6 +36,8 @@ class EE_API UIState { StateFlagDisabled = 1 << StateDisabled, StateFlagChecked = 1 << StateChecked, StateFlagFocusWithin = 1 << StateFocusWithin, + StateFlagLink = 1 << StateLink, + StateFlagVisited = 1 << StateVisited, StateFlagCount = StateCount }; diff --git a/include/eepp/ui/uitextspan.hpp b/include/eepp/ui/uitextspan.hpp index 7ae634d11..2827ae3aa 100644 --- a/include/eepp/ui/uitextspan.hpp +++ b/include/eepp/ui/uitextspan.hpp @@ -26,6 +26,8 @@ class EE_API UITextSpan : public UIWidget { static UITextSpan* NewStrikethrough() { return NewWithTag( "s" ); } + static UITextSpan* NewFont() { return NewWithTag( "font" ); } + static UITextSpan* NewMark() { return NewWithTag( "mark" ); } static UITextSpan* NewCode() { return NewWithTag( "code" ); } diff --git a/src/eepp/core/string.cpp b/src/eepp/core/string.cpp index 144de5f92..f991eaff0 100644 --- a/src/eepp/core/string.cpp +++ b/src/eepp/core/string.cpp @@ -1345,7 +1345,7 @@ bool String::icontains( std::string_view haystack, std::string_view needle ) { } ) != haystack.end(); } -void String::replaceAll( std::string& target, const std::string& that, const std::string& with ) { +void String::replaceAll( std::string& target, std::string_view that, std::string_view with ) { std::string::size_type pos = 0; while ( ( pos = target.find( that, pos ) ) != std::string::npos ) { diff --git a/src/eepp/network/uri.cpp b/src/eepp/network/uri.cpp index 910090abf..754631b76 100644 --- a/src/eepp/network/uri.cpp +++ b/src/eepp/network/uri.cpp @@ -277,7 +277,7 @@ std::string URI::getQuery() const { return query; } -void URI::getFragment( const std::string& fragment ) { +void URI::setFragment( const std::string& fragment ) { mFragment.clear(); decode( fragment, mFragment ); } diff --git a/src/eepp/ui/css/drawableimageparser.cpp b/src/eepp/ui/css/drawableimageparser.cpp index af0326aa4..e3aeae294 100644 --- a/src/eepp/ui/css/drawableimageparser.cpp +++ b/src/eepp/ui/css/drawableimageparser.cpp @@ -327,12 +327,14 @@ void DrawableImageParser::registerBaseParsers() { }; mFuncs["url"] = []( const FunctionString& functionType, const Sizef& /*size*/, bool& /*ownIt*/, - UINode* - /*node*/ ) -> Drawable* { + UINode* node ) -> Drawable* { if ( functionType.getParameters().size() < 1 ) return NULL; - return DrawableSearcher::searchByName( functionType.getParameters().at( 0 ) ); + return DrawableSearcher::searchByName( + node->getUISceneNode() + ->solveRelativePath( functionType.getParameters().at( 0 ) ) + .toString() ); }; mFuncs["icon"] = []( const FunctionString& functionType, const Sizef& size, bool&, diff --git a/src/eepp/ui/css/stylesheetlength.cpp b/src/eepp/ui/css/stylesheetlength.cpp index 96368328c..6d4d0390d 100644 --- a/src/eepp/ui/css/stylesheetlength.cpp +++ b/src/eepp/ui/css/stylesheetlength.cpp @@ -117,7 +117,7 @@ StyleSheetLength::Unit StyleSheetLength::unitFromString( std::string unitStr ) { case UnitHashes::Dpr: return Unit::Dpr; } - return Unit::Dp; + return Unit::Px; } std::string StyleSheetLength::unitToString( const StyleSheetLength::Unit& unit ) { diff --git a/src/eepp/ui/css/stylesheetselectorrule.cpp b/src/eepp/ui/css/stylesheetselectorrule.cpp index 15e9c6f54..f1bc399bf 100644 --- a/src/eepp/ui/css/stylesheetselectorrule.cpp +++ b/src/eepp/ui/css/stylesheetselectorrule.cpp @@ -14,8 +14,9 @@ static int numberOfSetBits( Uint32 i ) { // than uint32_t) } -static const char* StatePseudoClasses[] = { "focus", "selected", "hover", "pressed", - "disabled", "focus-within", "active" }; +static const char* StatePseudoClasses[] = { "focus", "selected", "hover", + "pressed", "disabled", "focus-within", + "active", "link", "visited" }; static bool isPseudoClassState( const std::string& pseudoClass ) { for ( Uint32 i = 0; i < eeARRAY_SIZE( StatePseudoClasses ); i++ ) { @@ -54,6 +55,10 @@ StyleSheetSelectorRule::toPseudoClass( std::string_view cls ) { return StyleSheetSelectorRule::PseudoClasses::Disabled; if ( "focus-within" == cls ) return StyleSheetSelectorRule::PseudoClasses::FocusWithin; + if ( "link" == cls ) + return StyleSheetSelectorRule::PseudoClasses::Link; + if ( "visited" == cls ) + return StyleSheetSelectorRule::PseudoClasses::Visited; eeASSERT( false ); return StyleSheetSelectorRule::PseudoClasses::None; } diff --git a/src/eepp/ui/tools/htmlformatter.cpp b/src/eepp/ui/tools/htmlformatter.cpp index 338b688c2..441167c71 100644 --- a/src/eepp/ui/tools/htmlformatter.cpp +++ b/src/eepp/ui/tools/htmlformatter.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -5,6 +6,8 @@ #define PUGIXML_HEADER_ONLY #include +using namespace EE::System; + namespace EE { namespace UI { namespace Tools { // In HTML, whitespace processing depends heavily on whether elements are block-level @@ -42,7 +45,7 @@ bool HTMLFormatter::isInlineNode( const pugi::xml_node& node ) { String::iequals( name, "em" ) || String::iequals( name, "s" ) || String::iequals( name, "u" ) || String::iequals( name, "br" ) || String::iequals( name, "code" ) || String::iequals( name, "img" ) || - String::iequals( name, "mark" ); + String::iequals( name, "mark" ) || String::iequals( name, "font" ); } // "Significant text" in the context of HTML whitespace collapsing means any text @@ -204,4 +207,67 @@ String HTMLFormatter::collapseXmlWhitespace( const String& text, const pugi::xml return res; } +std::string HTMLFormatter::HTMLtoXML( const std::string& layoutString ) { + std::string fixedLayout = layoutString; + + static constexpr std::string_view DOCTYPE_REGEX = "(?i)]*>"; + RegEx doctypeRegex( DOCTYPE_REGEX.data() ); + if ( doctypeRegex.matches( fixedLayout ) ) { + fixedLayout = doctypeRegex.gsub( fixedLayout, "" ); + } + + // JavaScript operators (<, &&) break XML parsers. We must remove "; + RegEx scriptRegex( SCRIPT_REGEX.data() ); + if ( scriptRegex.matches( fixedLayout ) ) { + fixedLayout = scriptRegex.gsub( fixedLayout, "" ); + } + + static constexpr std::string_view VOIDTAG_REGEX = + "(<(?:img|br|hr|input|meta|link|area|base|col|embed|param|source|track|wbr)\\b[^>]*?)(?"; + RegEx voidTagsRegex( VOIDTAG_REGEX.data() ); + if ( voidTagsRegex.matches( fixedLayout ) ) { + fixedLayout = voidTagsRegex.gsub( fixedLayout, "%1 />" ); + } + + static constexpr std::string_view BOOL_ATTR_REGEX = + "(?<=\\s)(checked|disabled|readonly|required|autofocus|multiple|selected|async|defer)\\b(?!" + "\\s*=)(?=[^<]*>)"; + RegEx boolAttrRegex( BOOL_ATTR_REGEX.data() ); + if ( boolAttrRegex.matches( fixedLayout ) ) { + fixedLayout = boolAttrRegex.gsub( fixedLayout, "%1=\"%1\"" ); + } + + static constexpr std::array, 21> entities = { { + { " ", " " }, // Non-breaking space + { "©", "©" }, // Copyright + { "®", "®" }, // Registered trademark + { "™", "™" }, // Trademark + { "€", "€" }, // Euro + { "£", "£" }, // Pound + { "¥", "¥" }, // Yen + { "¢", "¢" }, // Cent + { "—", "—" }, // Em dash + { "–", "–" }, // En dash + { "“", "“" }, // Left double quote + { "”", "”" }, // Right double quote + { "‘", "‘" }, // Left single quote + { "’", "’" }, // Right single quote + { "•", "•" }, // Bullet + { "·", "·" }, // Middle dot + { "…", "…" }, // Horizontal ellipsis + { "°", "°" }, // Degree sign + { "±", "±" }, // Plus-minus sign + { "×", "×" }, // Multiplication sign + { "÷", "÷" } // Division sign + } }; + + for ( const auto& [html_ent, xml_ent] : entities ) { + String::replaceAll( fixedLayout, html_ent, xml_ent ); + } + + return fixedLayout; +} + }}} // namespace EE::UI::Tools diff --git a/src/eepp/ui/uihtmltable.cpp b/src/eepp/ui/uihtmltable.cpp index 4a1da0718..1d7784609 100644 --- a/src/eepp/ui/uihtmltable.cpp +++ b/src/eepp/ui/uihtmltable.cpp @@ -98,7 +98,7 @@ void UIHTMLTable::updateLayout() { for ( Uint32 i = 0; i < end - start; ++i ) { UIHTMLTableCell* cell = mCells[start + i]; cell->setLayoutWidthPolicy( SizePolicy::WrapContent ); - cell->mSize.x = mSize.x; + cell->mSize.x = 0; cell->updateLayout(); Uint32 cellColspan = cell->getColspan(); if ( cellColspan == 1 ) { diff --git a/src/eepp/ui/uilayout.cpp b/src/eepp/ui/uilayout.cpp index b55b95e24..36fb1e2f2 100644 --- a/src/eepp/ui/uilayout.cpp +++ b/src/eepp/ui/uilayout.cpp @@ -122,8 +122,26 @@ bool UILayout::setMatchParentIfNeededVerticalGrowth() { return sizeChanged; } +void UILayout::updateLayoutWrappingContents() { + auto oldWidthPolicy = mWidthPolicy; + auto oldHeightPolicy = mHeightPolicy; + + if ( mWidthPolicy == SizePolicy::MatchParent ) + mWidthPolicy = SizePolicy::WrapContent; + if ( mHeightPolicy == SizePolicy::MatchParent ) + mHeightPolicy = SizePolicy::WrapContent; + + updateLayout(); + + mWidthPolicy = oldWidthPolicy; + mHeightPolicy = oldHeightPolicy; +} + void UILayout::onAutoSizeChild( UIWidget* child ) { - child->onAutoSize(); + if ( child->isLayout() ) { + child->asType()->updateLayoutWrappingContents(); + } else + child->onAutoSize(); } }} // namespace EE::UI diff --git a/src/eepp/ui/uirichtext.cpp b/src/eepp/ui/uirichtext.cpp index e809a2c4b..b5a4916bb 100644 --- a/src/eepp/ui/uirichtext.cpp +++ b/src/eepp/ui/uirichtext.cpp @@ -488,10 +488,13 @@ void UIRichText::rebuildRichText() { } else { Rectf margin = widget->getLayoutPixelsMargin(); - if ( mSize.getWidth() != 0 && - widget->getLayoutWidthPolicy() == SizePolicy::MatchParent ) { - widget->setPixelsSize( mSize.getWidth() - margin.Left - margin.Right, - widget->getPixelsSize().getHeight() ); + if ( widget->getLayoutWidthPolicy() == SizePolicy::MatchParent ) { + if ( mSize.getWidth() != 0 ) { + widget->setPixelsSize( mSize.getWidth() - margin.Left - margin.Right, + widget->getPixelsSize().getHeight() ); + } else { + onAutoSizeChild( widget ); + } } else if ( widget->getLayoutWidthPolicy() == SizePolicy::WrapContent || widget->getLayoutHeightPolicy() == SizePolicy::WrapContent ) { onAutoSizeChild( widget ); diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 06f2e9324..3c04a8ab7 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -448,7 +448,7 @@ void UISceneNode::setThreadPool( const std::shared_ptr& threadPool ) } static std::string getErrorContext( size_t offset, std::string_view content ) { - static constexpr std::size_t CONTEXT_LENGTH = 40; + static constexpr std::size_t CONTEXT_LENGTH = 50; std::size_t minVal = offset >= CONTEXT_LENGTH ? offset - CONTEXT_LENGTH : 0; std::size_t maxVal = offset + CONTEXT_LENGTH; std::size_t left = std::max( static_cast( 0ul ), minVal ); @@ -1027,6 +1027,8 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles ) { if ( !func.getParameters().empty() && func.getName() == "url" ) path = func.getParameters().at( 0 ); + path = solveRelativePath( path ).toString(); + if ( String::startsWith( path, "file://" ) ) { std::string filePath( path.substr( 7 ) ); @@ -1092,20 +1094,44 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles ) { } URI UISceneNode::solveRelativePath( URI uri ) { - if ( mURI.empty() ) + // 1. If base is empty OR the target already has a scheme (it's absolute), return it. + if ( mURI.empty() || !uri.getScheme().empty() ) return uri; - if ( mURI.getScheme().empty() ) - uri.setScheme( "file" ); + // 2. Inherit Scheme and Authority + if ( uri.getScheme().empty() ) { + // Default to "file" if the base also lacks a scheme + uri.setScheme( mURI.getScheme().empty() ? "file" : mURI.getScheme() ); + } - if ( uri.getPath().empty() || uri.getPath().back() != '/' ) - uri.setPath( mURI.getPath() + uri.getPath() ); - - if ( uri.getScheme().empty() ) - uri.setScheme( mURI.getScheme() ); - - if ( uri.getAuthority().empty() ) + if ( uri.getAuthority().empty() ) { uri.setAuthority( mURI.getAuthority() ); + } + + // 3. Safely Resolve the Path + std::string targetPath = uri.getPath(); + std::string basePath = mURI.getPath(); + + if ( !targetPath.empty() && targetPath.front() == '/' ) { + // CASE A: Root-relative path (e.g., "/news.css") + // It ignores the base directory entirely and attaches to the root. + uri.setPath( targetPath ); + } else { + // CASE B: Directory-relative path (e.g., "news.css" or "assets/style.css") + // We must strip the filename from the base path (everything after the last '/') + size_t lastSlashPos = basePath.find_last_of( '/' ); + + if ( lastSlashPos != std::string::npos ) { + // Keep everything up to and including the last '/' + basePath = basePath.substr( 0, lastSlashPos + 1 ); + } else { + // If there's no slash at all in the base path, act as root if there's an authority + basePath = mURI.getAuthority().empty() ? "" : "/"; + } + + // Now it's safe to concatenate + uri.setPath( basePath + targetPath ); + } return uri; } @@ -1126,12 +1152,16 @@ void UISceneNode::loadCSS( URI uri ) { } else if ( "http" == uri.getScheme() || "https" == uri.getScheme() ) { Http::getAsync( [this, url]( const Http&, Http::Request&, Http::Response& response ) { - if ( !response.getBody().empty() ) { + if ( !response.getBody().empty() && + response.getStatus() == Http::Response::Status::Ok ) { std::string css( response.getBody() ); runOnMainThread( [css = std::move( css ), url = std::move( url ), this] { combineStyleSheet( css, true, String::hash( url ) ); Log::debug( "UISceneNode::loadCSS: Loaded - %s", url ); } ); + } else { + Log::debug( "UISceneNode::loadCSS: Failed to load %s - %s", url, + response.getStatusDescription() ); } }, uri, Seconds( 5 ) ); @@ -1264,6 +1294,32 @@ void UISceneNode::setURI( const URI& uri ) { mURI = uri; } +void UISceneNode::setURIFromURL( const URI& url ) { + URI baseURI( url ); + std::string path = baseURI.getPath(); + + // If the path isn't empty and doesn't end with a directory slash... + if ( !path.empty() && path.back() != '/' ) { + size_t lastSlash = path.find_last_of( '/' ); + + if ( lastSlash != std::string::npos ) { + // Keep everything up to and including the last '/' + // Example: "/assets/css/styles.html" -> "/assets/css/" + baseURI.setPath( path.substr( 0, lastSlash + 1 ) ); + } else { + // Fallback if there are no slashes in the path at all + baseURI.setPath( "/" ); + } + } + + // Clear any query strings (?foo=bar) or fragments (#section) from the base URI, + // as they shouldn't be inherited by relative paths. + baseURI.setQuery( "" ); + baseURI.setFragment( "" ); + + setURI( baseURI ); +} + void UISceneNode::openURL( URI uri ) { if ( mURLInterceptorCb && mURLInterceptorCb( uri ) ) return; diff --git a/src/eepp/ui/uistate.cpp b/src/eepp/ui/uistate.cpp index 771fcc13a..65b66a45b 100644 --- a/src/eepp/ui/uistate.cpp +++ b/src/eepp/ui/uistate.cpp @@ -3,16 +3,17 @@ namespace EE { namespace UI { -static const char* UIStatesNames[] = { "normal", "focus", "selected", "hover", - "pressed", "selectedhover", "selectedpressed", "disabled", - "checked", "focus-within" }; +static const char* UIStatesNames[] = { + "normal", "focus", "selected", "hover", "pressed", "selectedhover", + "selectedpressed", "disabled", "checked", "focus-within", "link", "visited" }; static const Uint32 UIStateFlags[] = { UIState::StateFlagNormal, UIState::StateFlagFocus, UIState::StateFlagSelected, UIState::StateFlagHover, UIState::StateFlagPressed, UIState::StateFlagSelectedHover, UIState::StateFlagFocusWithin, UIState::StateFlagSelectedPressed, - UIState::StateFlagDisabled, UIState::StateFlagChecked }; + UIState::StateFlagDisabled, UIState::StateFlagChecked, + UIState::StateFlagLink, UIState::StateFlagVisited }; const char* UIState::getStateName( const Uint32& State ) { return UIStatesNames[State]; diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index e00a84727..0a3a3455e 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -567,7 +567,10 @@ UIAnchorSpan* UIAnchorSpan::New() { return eeNew( UIAnchorSpan, () ); } -UIAnchorSpan::UIAnchorSpan( const std::string& tag ) : UITextSpan( tag ) {} +UIAnchorSpan::UIAnchorSpan( const std::string& tag ) : UITextSpan( tag ) { + mPseudoClasses |= StyleSheetSelectorRule::PseudoClasses::Link; + mState |= UIState::StateFlagLink; +} Uint32 UIAnchorSpan::onMessage( const NodeMessage* Msg ) { switch ( Msg->getMsg() ) { diff --git a/src/eepp/ui/uiwidget.cpp b/src/eepp/ui/uiwidget.cpp index f53db96b6..18424e233 100644 --- a/src/eepp/ui/uiwidget.cpp +++ b/src/eepp/ui/uiwidget.cpp @@ -851,6 +851,12 @@ void UIWidget::updatePseudoClasses() { if ( mState & UIState::StateFlagDisabled ) mPseudoClasses |= StyleSheetSelectorRule::PseudoClasses::Disabled; + if ( mState & UIState::StateFlagLink ) + mPseudoClasses |= StyleSheetSelectorRule::PseudoClasses::Link; + + if ( mState & UIState::StateFlagVisited ) + mPseudoClasses |= StyleSheetSelectorRule::PseudoClasses::Visited; + invalidateDraw(); } diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index e2bc3561f..2888d76a8 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -143,6 +143,7 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["ins"] = UITextSpan::NewUnderline; registeredWidget["s"] = UITextSpan::NewStrikethrough; registeredWidget["del"] = UITextSpan::NewStrikethrough; + registeredWidget["font"] = UITextSpan::NewFont; registeredWidget["code"] = UITextSpan::NewCode; registeredWidget["mark"] = UITextSpan::NewMark; registeredWidget["div"] = UIRichText::NewDiv; @@ -161,14 +162,8 @@ void UIWidgetCreator::createBaseWidgetList() { registeredWidget["pre"] = UIRichText::NewPre; registeredWidget["img"] = [] { return UIImage::NewWithTag( "img" ); }; registeredWidget["input"] = UITextInput::New; - registeredWidget["article"] = [] { - return UILinearLayout::NewVerticalWidthMatchParent( "article" ); - }; - registeredWidget["center"] = [] { - auto center = UIRichText::NewWithTag( "center" ); - // center->setLayoutWidthPolicy( SizePolicy::WrapContent ); - return center; - }; + registeredWidget["article"] = [] { return UIRichText::NewWithTag( "article" ); }; + registeredWidget["center"] = [] { return UIRichText::NewWithTag( "center" ); }; registeredWidget["html"] = [] { return UIRichText::NewWithTag( "html" ); }; registeredWidget["head"] = [] { return UIWidget::NewWithTag( "head" ); }; registeredWidget["body"] = [] { return UIRichText::NewWithTag( "body" ); }; diff --git a/src/examples/ui_html/ui_html.cpp b/src/examples/ui_html/ui_html.cpp index 72a354d10..3066cecec 100644 --- a/src/examples/ui_html/ui_html.cpp +++ b/src/examples/ui_html/ui_html.cpp @@ -27,38 +27,46 @@ EE_MAIN_FUNC int main( int, char** ) { auto urlBar = ui->find( "url_bar" )->asType(); auto mainContainer = ui->find( "html_doc" ); - const auto loadDocument = [&]( URI url ) { + const auto loadDocumentData = [ui, mainContainer, urlBar]( URI url, std::string& data ) { + if ( data.empty() ) + return; static String::HashType prevURL = 0; - std::string data; - if ( !url.getScheme().empty() ) { - if ( url.getScheme() == "https" || url.getScheme() == "http" ) { - auto response = Http::get( url, Seconds( 5 ) ); - data = response.getBody(); - } else if ( url.getScheme() == "file" ) { - FileSystem::fileGet( url.getPath(), data ); - } - } else if ( !url.getPath().empty() && url.getPath().front() == '/' ) { - FileSystem::fileGet( url.getPath(), data ); - } - - if ( !data.empty() ) { - if ( url.getPath().empty() || url.getPath().back() != '/' ) { - if ( url.getScheme() == "file" && - !FileSystem::fileExtension( url.getPath() ).empty() ) { - url.setPath( FileSystem::fileRemoveFileName( url.getPath() ) ); - } - url.setPath( url.getPath() + "/" ); - } + ui->ensureMainThread( [=] { mainContainer->closeAllChildren(); if ( prevURL ) ui->getStyleSheet().removeAllWithMarker( prevURL ); - ui->setURI( url ); - auto hash = String::hash( url.toString() ); - ui->loadLayoutFromString( data, mainContainer, hash ); + ui->setURIFromURL( url ); + auto urlStr = url.toString(); + auto hash = String::hash( urlStr ); + ui->loadLayoutFromString( HTMLFormatter::HTMLtoXML( data ), mainContainer, hash ); prevURL = hash; + urlBar->setText( urlStr ); + } ); + }; + + const auto loadDocument = [&]( URI url ) { + if ( !url.getScheme().empty() ) { + if ( url.getScheme() == "https" || url.getScheme() == "http" ) { + Http::getAsync( + [=]( const Http&, Http::Request&, Http::Response& response ) { + std::string data = response.getBody(); + loadDocumentData( url, data ); + }, + url, Seconds( 5 ) ); + } else if ( url.getScheme() == "file" ) { + std::string data; + FileSystem::fileGet( url.getPath(), data ); + loadDocumentData( url, data ); + } + } else if ( !url.getPath().empty() && url.getPath().front() == '/' ) { + std::string data; + FileSystem::fileGet( url.getPath(), data ); + loadDocumentData( url, data ); } }; + loadDocument( "https://news.ycombinator.com" ); + urlBar->on( Event::OnPressEnter, [&]( auto event ) { loadDocument( urlBar->getText().toUtf8() ); } ); diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index 7d8006c5c..7e6a5ba84 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -22,16 +23,15 @@ using namespace EE::UI; using namespace EE::UI::Tools; UTEST( UIHTMLTable, complexLayout ) { - auto win = Engine::instance()->createWindow( - WindowSettings( 1024, 650, "HTML Tables Test", WindowStyle::Default, WindowBackend::Default, - 32, {}, 1, false, true ), - ContextSettings( false, ContextSettings::FrameRateLimitScreenRefreshRate, 4 ) ); - FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); - -#ifdef EE_DEBUG + // #ifdef EE_DEBUG Log::instance()->setLiveWrite( true ); Log::instance()->setLogToStdOut( true ); -#endif + // #endif + + auto win = Engine::instance()->createWindow( + WindowSettings( 1024, 650, "HTML Tables 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" );