diff --git a/.ecode/project_build.json b/.ecode/project_build.json index b32e7a0e1..85e572b3d 100644 --- a/.ecode/project_build.json +++ b/.ecode/project_build.json @@ -375,6 +375,12 @@ "command": "${project_root}/bin/eepp-ui-dropdownmodellist-debug", "name": "eepp-ui-dropdownmodellist-debug", "working_dir": "${project_root}/bin" + }, + { + "args": "", + "command": "${project_root}/bin/eepp-ui-html-debug", + "name": "eepp-ui-html-debug", + "working_dir": "${project_root}/bin" } ], "var": { diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index c24662e92..fcabc9604 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -107,7 +107,7 @@ h6 { margin: 1.67em 0; } -code { +markdownview code { font-family: monospace; background-color: var(--list-back); } @@ -134,18 +134,24 @@ li { } a { - color: var(--primary); - selection-color: var(--font-selected-pressed); - selection-back-color: var(--primary); cursor: arrow; text-decoration: none; - gravity: bottom; } a:hover { - color: var(--font-highlight); - cursor: hand; text-decoration: underline; + cursor: hand; +} + +markdownview a { + color: var(--primary); + selection-color: var(--font-selected-pressed); + selection-back-color: var(--primary); + gravity: bottom; +} + +markdownview a:hover { + color: var(--font-highlight); } br { @@ -392,7 +398,7 @@ RadioButton::active { ListBox, DropDownList::ListBox, ComboBox::DropDownList::ListBox, -Table, +MarkdownView Table, ListView { background-color: var(--list-back); border-color: var(--button-border); diff --git a/bin/unit_tests/assets/html/base.css b/bin/unit_tests/assets/html/base.css new file mode 100644 index 000000000..cb9ccef09 --- /dev/null +++ b/bin/unit_tests/assets/html/base.css @@ -0,0 +1,228 @@ +body { + margin-top: 8px; + margin-right: 8px; + margin-bottom: 8px; + margin-left: 8px; + font-size: 11px; + color: black; + background-color: white; +} + +h1 { + font-size: 2em; + margin-top: 0.67em; + margin-right: 0; + font-weight: bold; +} + +h2 { + font-size: 1.5em; + margin-top: 0.83em; + margin-right: 0; + font-weight: bold; +} + +h3 { + font-size: 1.17em; + margin-top: 1em; + margin-right: 0; + font-weight: bold; +} + +h4 { + font-size: 1em; + margin-top: 1.33em; + margin-right: 0; + font-weight: bold; +} + +h5 { + font-size: 0.83em; + margin-top: 1.67em; + margin-right: 0; + font-weight: bold; +} + +h6 { + font-size: 0.67em; + margin-top: 1.67em; + margin-right: 0; + font-weight: bold; +} + +p { + margin-top: 1em; + margin-right: 0; +} + +blockquote { + margin-top: 1em; + margin-right: 0; +} + +dd { + margin-top: 1em; + margin-right: 0; +} + +dl { + margin-top: 1em; + margin-right: 0; +} + +ol { + margin-top: 1em; + margin-right: 0; +} + +ul { + margin-top: 1em; + margin-right: 0; +} + +figure { + margin-top: 1em; + margin-right: 0; +} + +pre { + margin-top: 1em; + margin-right: 0; +} + +blockquote { + margin-top: 1em; + margin-right: 40px; +} + +pre { + font-family: monospace; + font-size: 1em; +} + +code { + font-family: monospace; + font-size: 1em; +} + +kbd { + font-family: monospace; + font-size: 1em; +} + +samp { + font-family: monospace; + font-size: 1em; +} + +tt { + font-family: monospace; + font-size: 1em; +} + +var { + font-family: monospace; + font-size: 1em; +} + +pre { + margin-top: 1em; + margin-right: 0; +} + +ul { + margin-top: 1em; + margin-right: 0; +} + +ol { + list-style-type: decimal; + margin-top: 1em; + margin-right: 0; +} + +li { + text-align: match-parent; +} + +b { + font-weight: bold; +} + +strong { + font-weight: bold; +} + +i { + font-style: italic; +} + +em { + font-style: italic; +} + +cite { + font-style: italic; +} + +u { + text-decoration: underline; +} + +ins { + text-decoration: underline; +} + +s { + text-decoration: line-through; +} + +strike { + text-decoration: line-through; +} + +del { + text-decoration: line-through; +} + +big { + font-size: 16dp; +} + +small { + font-size: 9dp; +} + +sub { + vertical-align: sub; + font-size: 9dp; +} + +sup { + vertical-align: super; + font-size: 9dp; +} + +a:link { + color: #0000EE; + text-decoration: underline; +} + +a:visited { + color: #551A8B; + text-decoration: underline; +} + +th { + font-weight: bold; + text-align: center; +} + +hr { + margin-top: 0.5em; + border-top-width: 1px; + border-right-width: 1px; + border-bottom-width: 1px; + border-left-width: 1px; + color: gray; +} diff --git a/bin/unit_tests/assets/html/hn_thread_test.html b/bin/unit_tests/assets/html/hn_thread_test.html new file mode 100644 index 000000000..d2544b057 --- /dev/null +++ b/bin/unit_tests/assets/html/hn_thread_test.html @@ -0,0 +1,186 @@ + + + + + + + + + + + $500 GPU outperforms Claude Sonnet on coding benchmarks | Hacker News + + + + +
+ + + + + + + + + + + +
+ + + + + + +
+ + + Hacker Newsnew | + threads | + past | + comments | ask | + show | jobs | + submit + + SpartanJ (209) | + logout +
+
+ + + + + + + + + + + + + + + + + + + +
+ + + $500 GPU outperforms Claude Sonnet + on coding + benchmarks + (github.com/itigges22) +
+ 107 points + by yogthos + 10 hours + ago + | + flag + | + hide + | + past + | + favorite + | 33 comments +
+
+
+
+
+ + + + +
+ + + + + + +
+ + +
+ mmaunder + 2 hours + ago + + | + + | + + [–] +
+
+
+
+ I’d encourage devs to use MiniMax, Kimi, etc for + real world tasks that require intelligence. The down + sides emerge pretty fast: much higher reasoning + token use, slower outputs, and degradation that is + palpable. Sadly, you do get what you pay for right + now. However that doesn’t prevent you from saving + tons through smart model routing, being smart about + reasoning budgets, and using max output tokens + wisely. And optimize your apps and prompts to reduce + output tokens. +
+
+

+ reply +

+
+
+
+
+

+
+ + + + + +
+
+
+ Guidelines | + FAQ | Lists | + API | + Security | + Legal | + Apply to YC | + Contact

+
+
+
+ + + + diff --git a/bin/unit_tests/assets/html/news.css b/bin/unit_tests/assets/html/news.css new file mode 100644 index 000000000..d85db99a2 --- /dev/null +++ b/bin/unit_tests/assets/html/news.css @@ -0,0 +1,176 @@ +body { font-family:Verdana, Geneva, sans-serif; font-size:10pt; color:#828282; } +td { font-family:Verdana, Geneva, sans-serif; font-size:10pt; color:#828282; } + +.admin td { font-family:Verdana, Geneva, sans-serif; font-size:8.5pt; color:#000000; } +.subtext td { font-family:Verdana, Geneva, sans-serif; font-size: 7pt; color:#828282; } + +input { font-family:monospace; font-size:10pt; } +input[type='submit'] { font-family:Verdana, Geneva, sans-serif; } +textarea { font-family:monospace; font-size:10pt; resize:both; } + +a:link { color:#000000; text-decoration:none; } +a:visited { color:#828282; text-decoration:none; } + +.default { font-family:Verdana, Geneva, sans-serif; font-size: 10pt; color:#828282; } +.admin { font-family:Verdana, Geneva, sans-serif; font-size:8.5pt; color:#000000; } +.title { font-family:Verdana, Geneva, sans-serif; font-size: 10pt; color:#828282; overflow:hidden; } +.subtext { font-family:Verdana, Geneva, sans-serif; font-size: 7pt; color:#828282; } +.yclinks { font-family:Verdana, Geneva, sans-serif; font-size: 8pt; color:#828282; } +.pagetop { font-family:Verdana, Geneva, sans-serif; font-size: 10pt; color:#222222; line-height:12px; } +.comhead { font-family:Verdana, Geneva, sans-serif; font-size: 8pt; color:#828282; } +.comment { font-family:Verdana, Geneva, sans-serif; font-size: 9pt; } +.hnname { margin-left:1px; margin-right: 5px; } + +#hnmain { min-width: 796px; } + +.title a { word-break: break-word; } + +.comment a:link, .comment a:visited { text-decoration: underline; } +.noshow { display: none; } +.nosee { visibility: hidden; pointer-events: none; cursor: default } + +.c00, .c00 a:link { color:#000000; } +.c5a, .c5a a:link, .c5a a:visited { color:#5a5a5a; } +.c73, .c73 a:link, .c73 a:visited { color:#737373; } +.c82, .c82 a:link, .c82 a:visited { color:#828282; } +.c88, .c88 a:link, .c88 a:visited { color:#888888; } +.c9c, .c9c a:link, .c9c a:visited { color:#9c9c9c; } +.cae, .cae a:link, .cae a:visited { color:#aeaeae; } +.cbe, .cbe a:link, .cbe a:visited { color:#bebebe; } +.cce, .cce a:link, .cce a:visited { color:#cecece; } +.cdd, .cdd a:link, .cdd a:visited { color:#dddddd; } + +.pagetop a:visited { color:#000000;} +.topsel a:link, .topsel a:visited { color:#ffffff; } + +.subtext a:link, .subtext a:visited { color:#828282; } +.subtext a:hover { text-decoration:underline; } + +.comhead a:link, .subtext a:visited { color:#828282; } +.comhead a:hover { text-decoration:underline; } + +.hnmore a:link, a:visited { color:#828282; } +.hnmore { text-decoration:underline; } + +.default p { margin-top: 8px; margin-bottom: 0px; } + +.pagebreak {page-break-before:always} + +pre { overflow: auto; padding: 2px; white-space: pre-wrap; overflow-wrap:anywhere; } +pre:hover { overflow:auto } + +.votearrow { + width: 10px; + height: 10px; + border: 0px; + margin: 3px 2px 6px; + background: url("triangle.svg"), linear-gradient(transparent, transparent) no-repeat; + background-size: 10px; +} + +.votelinks.nosee div.votearrow.rotate180 { + display: none; +} + +table.padtab td { padding:0px 10px } + +@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min-device-pixel-ratio: 2) { + .votearrow { + background-size: 10px; + background-image: url("triangle.svg"), linear-gradient(transparent, transparent); + } +} + +.rotate180 { + -webkit-transform: rotate(180deg); /* Chrome and other webkit browsers */ + -moz-transform: rotate(180deg); /* FF */ + -o-transform: rotate(180deg); /* Opera */ + -ms-transform: rotate(180deg); /* IE9 */ + transform: rotate(180deg); /* W3C complaint browsers */ + + /* IE8 and below */ + -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=-1, M12=0, M21=0, M22=-1, DX=0, DY=0, SizingMethod='auto expand')"; +} + +/* mobile device */ +@media only screen +and (min-width : 300px) +and (max-width : 750px) { + #hnmain { width: 100%; min-width: 0; } + body { padding: 0; margin: 0; width: 100%; } + td { height: inherit !important; } + .title, .comment { font-size: inherit; } + span.pagetop { display: block; margin: 3px 5px; font-size: 12px; line-height: normal } + span.pagetop b { display: block; font-size: 15px; } + table.comment-tree .comment a { display: inline-block; max-width: 200px; overflow: hidden; white-space: nowrap; + text-overflow: ellipsis; vertical-align:top; } + img[src='s.gif'][width='40'] { width: 12px; } + img[src='s.gif'][width='80'] { width: 24px; } + img[src='s.gif'][width='120'] { width: 36px; } + img[src='s.gif'][width='160'] { width: 48px; } + img[src='s.gif'][width='200'] { width: 60px; } + img[src='s.gif'][width='240'] { width: 72px; } + img[src='s.gif'][width='280'] { width: 84px; } + img[src='s.gif'][width='320'] { width: 96px; } + img[src='s.gif'][width='360'] { width: 108px; } + img[src='s.gif'][width='400'] { width: 120px; } + img[src='s.gif'][width='440'] { width: 132px; } + img[src='s.gif'][width='480'] { width: 144px; } + img[src='s.gif'][width='520'] { width: 156px; } + img[src='s.gif'][width='560'] { width: 168px; } + img[src='s.gif'][width='600'] { width: 180px; } + img[src='s.gif'][width='640'] { width: 192px; } + img[src='s.gif'][width='680'] { width: 204px; } + img[src='s.gif'][width='720'] { width: 216px; } + img[src='s.gif'][width='760'] { width: 228px; } + img[src='s.gif'][width='800'] { width: 240px; } + img[src='s.gif'][width='840'] { width: 252px; } + .title { font-size: 11pt; line-height: 14pt; } + .subtext { font-size: 9pt; } + .votearrow { transform: scale(1.3,1.3); margin-right: 6px; } + .votearrow.rotate180 { + -webkit-transform: rotate(180deg) scale(1.3,1.3); /* Chrome and other webkit browsers */ + -moz-transform: rotate(180deg) scale(1.3,1.3); /* FF */ + -o-transform: rotate(180deg) scale(1.3,1.3); /* Opera */ + -ms-transform: rotate(180deg) scale(1.3,1.3); /* IE9 */ + transform: rotate(180deg) scale(1.3,1.3); /* W3C complaint browsers */ + } + .votelinks { min-width: 18px; } + .votelinks a { display: block; margin-bottom: 9px; } + input[type='text'], input[type='number'], textarea { font-size: 16px; width: 90%; } +} + +.comment { max-width: 1215px; overflow-wrap:anywhere; } + + + +@media only screen and (min-width : 300px) and (max-width : 389px) { + .comment { max-width: 270px; overflow: hidden } +} +@media only screen and (min-width : 390px) and (max-width : 509px) { + .comment { max-width: 350px; overflow: hidden } +} +@media only screen and (min-width : 510px) and (max-width : 599px) { + .comment { max-width: 460px; overflow: hidden } +} +@media only screen and (min-width : 600px) and (max-width : 689px) { + .comment { max-width: 540px; overflow: hidden } +} +@media only screen and (min-width : 690px) and (max-width : 809px) { + .comment { max-width: 620px; overflow: hidden } +} +@media only screen and (min-width : 810px) and (max-width : 899px) { + .comment { max-width: 730px; overflow: hidden } +} +@media only screen and (min-width : 900px) and (max-width : 1079px) { + .comment { max-width: 810px; overflow: hidden } +} +@media only screen and (min-width : 1080px) and (max-width : 1169px) { + .comment { max-width: 970px; overflow: hidden } +} +@media only screen and (min-width : 1170px) and (max-width : 1259px) { + .comment { max-width: 1050px; overflow: hidden } +} +@media only screen and (min-width : 1260px) and (max-width : 1349px) { + .comment { max-width: 1130px; overflow: hidden } +} diff --git a/include/eepp/ui/uihtmltable.hpp b/include/eepp/ui/uihtmltable.hpp index 8df434851..b3413159c 100644 --- a/include/eepp/ui/uihtmltable.hpp +++ b/include/eepp/ui/uihtmltable.hpp @@ -33,6 +33,8 @@ class EE_API UIHTMLTable : public UILayout { class EE_API UIHTMLTableCell : public UIRichText { public: + friend class UIHTMLTable; + static UIHTMLTableCell* New( const std::string& tag ); explicit UIHTMLTableCell( const std::string& tag ); diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 90d317498..1ad1c9abc 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -692,6 +692,19 @@ class EE_API UISceneNode : public SceneNode { /** @return the document / scene URI used to resolve paths of inner elements */ const URI& getURI() const { return mURI; } + /** Handles opening an specific URI */ + void openURL( URI uri ); + + /* Sets a callback to intercept the openURL calls, returns true if intercepted, false to leave + * the default openURL implementation handle it. + */ + void setURLInterceptorCb( std::function cb ) { mURLInterceptorCb = cb; }; + + /** + * Solves a relative path with no scheme or authority into a complete URI. + */ + URI solveRelativePath( URI uri ); + protected: friend class EE::UI::UIWindow; friend class EE::UI::UIWidget; @@ -720,6 +733,7 @@ class EE_API UISceneNode : public SceneNode { Uint32 mCurOnSizeChangeListener{ 0 }; std::shared_ptr mThreadPool; URI mURI; + std::function mURLInterceptorCb; /** * @brief Protected constructor. diff --git a/premake4.lua b/premake4.lua index c4f613a30..3340ac054 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1598,6 +1598,12 @@ solution "eepp" files { "src/examples/ui_markdownview/*.cpp" } build_link_configuration( "eepp-ui-markdownview", true ) + project "eepp-ui-html" + set_kind() + language "C++" + files { "src/examples/ui_html/*.cpp" } + build_link_configuration( "eepp-ui-html", true ) + project "eepp-richtext" set_kind() language "C++" diff --git a/premake5.lua b/premake5.lua index 299450704..c05bbf3d9 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1478,6 +1478,12 @@ workspace "eepp" files { "src/examples/ui_richtext/*.cpp" } build_link_configuration( "eepp-ui-richtext", true ) + project "eepp-ui-html" + set_kind() + language "C++" + files { "src/examples/ui_html/*.cpp" } + build_link_configuration( "eepp-ui-html", true ) + project "eepp-ui-markdownview" set_kind() language "C++" diff --git a/src/eepp/graphics/text.cpp b/src/eepp/graphics/text.cpp index f06be814d..b1544e689 100644 --- a/src/eepp/graphics/text.cpp +++ b/src/eepp/graphics/text.cpp @@ -81,7 +81,7 @@ Uint32 Text::stringToStyleFlag( const std::string& str ) { flags |= Text::Bold; else if ( "italic" == cur ) flags |= Text::Italic; - else if ( "strikethrough" == cur ) + else if ( "strikethrough" == cur || "line-through" == cur ) flags |= Text::StrikeThrough; else if ( "shadowed" == cur || "shadow" == cur ) flags |= Text::Shadow; diff --git a/src/eepp/ui/doc/syntaxcolorscheme.cpp b/src/eepp/ui/doc/syntaxcolorscheme.cpp index 50bbb7448..2fb94daed 100644 --- a/src/eepp/ui/doc/syntaxcolorscheme.cpp +++ b/src/eepp/ui/doc/syntaxcolorscheme.cpp @@ -112,7 +112,7 @@ SyntaxColorScheme::Style parseStyle( style.style |= Text::Italic; else if ( "underline" == val || "underlined" == val ) style.style |= Text::Underlined; - else if ( "strikethrough" == val ) + else if ( "strikethrough" == val || "line-through" == val ) style.style |= Text::StrikeThrough; else if ( "shadow" == val ) style.style |= Text::Shadow; diff --git a/src/eepp/ui/uihtmltable.cpp b/src/eepp/ui/uihtmltable.cpp index deae0818a..4a1da0718 100644 --- a/src/eepp/ui/uihtmltable.cpp +++ b/src/eepp/ui/uihtmltable.cpp @@ -98,6 +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->updateLayout(); Uint32 cellColspan = cell->getColspan(); if ( cellColspan == 1 ) { @@ -216,7 +217,8 @@ void UIHTMLTable::updateLayout() { mRows[rowCount - 1]->setPixelsPosition( mPaddingPx.Left, 0 ); if ( mHeightPolicy == SizePolicy::WrapContent ) { - setInternalPixelsHeight( headHeight + bodyHeight + footerHeight + mPaddingPx.Bottom ); + setInternalPixelsHeight( mPaddingPx.Top + headHeight + bodyHeight + footerHeight + + mPaddingPx.Bottom ); } mPacking = false; diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index c49161ff3..06f2e9324 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -22,7 +22,9 @@ #include #include #include +#include #include + #define PUGIXML_HEADER_ONLY #include @@ -318,7 +320,9 @@ std::vector UISceneNode::loadNode( pugi::xml_node node, Node* parent, } else if ( String::iequals( widget.name(), "link" ) ) { auto type = widget.attribute( "type" ); auto href = widget.attribute( "href" ); - if ( !type.empty() && !href.empty() && String::iequals( type.value(), "text/css" ) ) { + auto rel = widget.attribute( "rel" ); + if ( !href.empty() && ( String::iequals( type.value(), "text/css" ) || + String::iequals( rel.value(), "stylesheet" ) ) ) { loadCSS( href.as_string() ); } } @@ -1087,29 +1091,46 @@ void UISceneNode::loadFontFaces( const StyleSheetStyleVector& styles ) { } } -void UISceneNode::loadCSS( URI uri ) { - std::string scheme = uri.getScheme(); - if ( !mURI.empty() && scheme.empty() ) { - std::string pathStart = mURI.getPath(); - FileSystem::dirAddSlashAtEnd( pathStart ); - std::string pathEnd = pathStart + uri.getPath(); - uri = mURI; - uri.setPath( pathEnd ); - } +URI UISceneNode::solveRelativePath( URI uri ) { + if ( mURI.empty() ) + return uri; - if ( "file" == scheme || ( scheme.empty() && FileSystem::fileExists( uri.getPath() ) ) ) { + if ( mURI.getScheme().empty() ) + uri.setScheme( "file" ); + + 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() ) + uri.setAuthority( mURI.getAuthority() ); + + return uri; +} + +void UISceneNode::loadCSS( URI uri ) { + uri = solveRelativePath( uri ); + std::string url = uri.toString(); + Log::debug( "UISceneNode::loadCSS: %s", url ); + + if ( "file" == uri.getScheme() || + ( uri.getScheme().empty() && FileSystem::fileExists( uri.getPath() ) ) ) { std::string filePath( uri.getPath() ); std::string css; if ( FileSystem::fileExists( filePath ) && FileSystem::fileGet( filePath, css ) ) { - combineStyleSheet( css, true, String::hash( uri.toString() ) ); + combineStyleSheet( css, true, String::hash( url ) ); + Log::debug( "UISceneNode::loadCSS: Loaded - %s", url ); } - } else if ( "http" == scheme || "https" == scheme ) { + } else if ( "http" == uri.getScheme() || "https" == uri.getScheme() ) { Http::getAsync( - [this, uri]( const Http&, Http::Request&, Http::Response& response ) { + [this, url]( const Http&, Http::Request&, Http::Response& response ) { if ( !response.getBody().empty() ) { std::string css( response.getBody() ); - runOnMainThread( [css = std::move( css ), uri = std::move( uri ), this] { - combineStyleSheet( css, true, String::hash( uri.toString() ) ); + runOnMainThread( [css = std::move( css ), url = std::move( url ), this] { + combineStyleSheet( css, true, String::hash( url ) ); + Log::debug( "UISceneNode::loadCSS: Loaded - %s", url ); } ); } }, @@ -1118,8 +1139,9 @@ void UISceneNode::loadCSS( URI uri ) { IOStream* stream = VFS::instance()->getFileFromPath( uri.getPath() ); CSS::StyleSheetParser parser; if ( parser.loadFromStream( *stream ) ) { - parser.getStyleSheet().setMarker( String::hash( uri.toString() ) ); + parser.getStyleSheet().setMarker( String::hash( url ) ); combineStyleSheet( parser.getStyleSheet() ); + Log::debug( "UISceneNode::loadCSS: Loaded - %s", url ); } } } @@ -1242,4 +1264,10 @@ void UISceneNode::setURI( const URI& uri ) { mURI = uri; } +void UISceneNode::openURL( URI uri ) { + if ( mURLInterceptorCb && mURLInterceptorCb( uri ) ) + return; + Engine::instance()->openURI( uri.toString() ); +} + }} // namespace EE::UI diff --git a/src/eepp/ui/uiscrollview.cpp b/src/eepp/ui/uiscrollview.cpp index 0eba1cdf8..bc2256f81 100644 --- a/src/eepp/ui/uiscrollview.cpp +++ b/src/eepp/ui/uiscrollview.cpp @@ -341,7 +341,8 @@ bool UIScrollView::isTouchOverAllowedChildren() { bool ret = mViewType == ScrollViewType::Outside ? !mVScroll->isMouseOverMeOrChildren() && !mHScroll->isMouseOverMeOrChildren() : true; - return isMouseOverMeOrChildren() && mScrollView->isMouseOverMeOrChildren() && ret; + return isMouseOverMeOrChildren() && mScrollView && mScrollView->isMouseOverMeOrChildren() && + ret; } std::string UIScrollView::getPropertyString( const PropertyDefinition* propertyDef, @@ -437,7 +438,7 @@ bool UIScrollView::applyProperty( const StyleSheetProperty& attribute ) { Uint32 UIScrollView::onMessage( const NodeMessage* Msg ) { switch ( Msg->getMsg() ) { case NodeMessage::MouseUp: { - if ( mVScroll->isEnabled() && 0 != mScrollView->getSize().getHeight() && + if ( mScrollView && mVScroll->isEnabled() && 0 != mScrollView->getSize().getHeight() && isTouchOverAllowedChildren() && Msg->getSender()->isUINode() && !Msg->getSender()->asType()->isScrollable() ) { if ( Msg->getFlags() & EE_BUTTON_WUMASK ) { diff --git a/src/eepp/ui/uitextspan.cpp b/src/eepp/ui/uitextspan.cpp index cadd11c1e..e00a84727 100644 --- a/src/eepp/ui/uitextspan.cpp +++ b/src/eepp/ui/uitextspan.cpp @@ -6,7 +6,6 @@ #include #include #include -#include #define PUGIXML_HEADER_ONLY #include @@ -574,7 +573,7 @@ Uint32 UIAnchorSpan::onMessage( const NodeMessage* Msg ) { switch ( Msg->getMsg() ) { case NodeMessage::MouseClick: { if ( !mHref.empty() && ( Msg->getFlags() & EE_BUTTON_LMASK ) ) - Engine::instance()->openURI( mHref ); + getUISceneNode()->openURL( mHref ); return 1; } } @@ -610,7 +609,7 @@ const std::string& UIAnchorSpan::getHref() const { Uint32 UIAnchorSpan::onKeyDown( const KeyEvent& event ) { if ( event.getKeyCode() == KEY_KP_ENTER || event.getKeyCode() == KEY_RETURN ) { if ( !mHref.empty() ) { - Engine::instance()->openURI( mHref ); + getUISceneNode()->openURL( mHref ); return 1; } } diff --git a/src/eepp/ui/uitextview.cpp b/src/eepp/ui/uitextview.cpp index 6e9f281c3..0b8ff7975 100644 --- a/src/eepp/ui/uitextview.cpp +++ b/src/eepp/ui/uitextview.cpp @@ -11,7 +11,7 @@ #include #include #include -#include + #define PUGIXML_HEADER_ONLY #include @@ -985,7 +985,7 @@ UIAnchor::UIAnchor( const std::string& tag ) : UITextView( tag ) { onClick( [this]( const MouseEvent* ) { if ( !mHref.empty() ) - Engine::instance()->openURI( mHref ); + getUISceneNode()->openURL( mHref ); }, EE_BUTTON_LEFT ); } @@ -1019,7 +1019,7 @@ const std::string& UIAnchor::getHref() const { Uint32 UIAnchor::onKeyDown( const KeyEvent& event ) { if ( event.getKeyCode() == KEY_KP_ENTER || event.getKeyCode() == KEY_RETURN ) { if ( !mHref.empty() ) { - Engine::instance()->openURI( mHref ); + getUISceneNode()->openURL( mHref ); return 1; } } diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp index a1550c477..e2bc3561f 100644 --- a/src/eepp/ui/uiwidgetcreator.cpp +++ b/src/eepp/ui/uiwidgetcreator.cpp @@ -166,16 +166,12 @@ void UIWidgetCreator::createBaseWidgetList() { }; registeredWidget["center"] = [] { auto center = UIRichText::NewWithTag( "center" ); - center->setLayoutWidthPolicy( SizePolicy::WrapContent ); + // center->setLayoutWidthPolicy( SizePolicy::WrapContent ); return center; }; - registeredWidget["html"] = [] { - return UILinearLayout::NewVerticalWidthMatchParent( "html" ); - }; + registeredWidget["html"] = [] { return UIRichText::NewWithTag( "html" ); }; registeredWidget["head"] = [] { return UIWidget::NewWithTag( "head" ); }; - registeredWidget["body"] = [] { - return UILinearLayout::NewVerticalWidthMatchParent( "body" ); - }; + registeredWidget["body"] = [] { return UIRichText::NewWithTag( "body" ); }; registeredWidget["table"] = UIHTMLTable::New; registeredWidget["tr"] = UIHTMLTableRow::New; registeredWidget["thead"] = UIHTMLTableHead::New; diff --git a/src/examples/ui_html/ui_html.cpp b/src/examples/ui_html/ui_html.cpp new file mode 100644 index 000000000..72a354d10 --- /dev/null +++ b/src/examples/ui_html/ui_html.cpp @@ -0,0 +1,93 @@ +#include + +EE_MAIN_FUNC int main( int, char** ) { + UIApplication app( { 1280, 720, "eepp - UI HTML Example" } ); + + Log::instance()->setLogLevelThreshold( LogLevel::Debug ); + Log::instance()->setLogToStdOut( true ); + Log::instance()->setLiveWrite( true ); + + auto win = app.getWindow(); + auto ui = app.getUI(); + + ui->setColorSchemePreference( ColorSchemeExtPreference::Light ); + + ui->loadLayoutFromString( R"xml( + + + + + + + + + )xml" ); + + auto urlBar = ui->find( "url_bar" )->asType(); + auto mainContainer = ui->find( "html_doc" ); + + const auto loadDocument = [&]( URI url ) { + 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() + "/" ); + } + mainContainer->closeAllChildren(); + if ( prevURL ) + ui->getStyleSheet().removeAllWithMarker( prevURL ); + ui->setURI( url ); + auto hash = String::hash( url.toString() ); + ui->loadLayoutFromString( data, mainContainer, hash ); + prevURL = hash; + } + }; + + urlBar->on( Event::OnPressEnter, + [&]( auto event ) { loadDocument( urlBar->getText().toUtf8() ); } ); + + ui->setURLInterceptorCb( [&]( URI uri ) { + loadDocument( ui->solveRelativePath( uri ) ); + return true; + } ); + + win->getInput()->pushCallback( [&loadDocument]( InputEvent* event ) { + switch ( event->Type ) { + case InputEvent::FileDropped: { + std::string file( event->file.file ); + loadDocument( "file://" + file ); + break; + } + case InputEvent::TextDropped: { + loadDocument( event->textdrop.text ); + break; + } + default: + break; + } + } ); + + app.getUI()->on( Event::KeyUp, [&app]( const Event* event ) { + if ( event->asKeyEvent()->getKeyCode() == KEY_F11 ) { + UIWidgetInspector::create( app.getUI() ); + } + } ); + + return app.run(); +} diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp new file mode 100644 index 000000000..dfba8026e --- /dev/null +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -0,0 +1,90 @@ +// #include "compareimages.hpp" +#include "utest.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE; +using namespace EE::Graphics; +using namespace EE::Window; +using namespace EE::Scene; +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 + Log::instance()->setLiveWrite( true ); + Log::instance()->setLogToStdOut( true ); +#endif + + FontTrueType* font = FontTrueType::New( "NotoSans-Regular" ); + font->loadFromFile( "../assets/fonts/NotoSans-Regular.ttf" ); + ASSERT_TRUE( font != nullptr && font->loaded() ); + FontFamily::loadFromRegular( font ); + + UI::UISceneNode* sceneNode = UI::UISceneNode::New(); + SceneManager::instance()->add( sceneNode ); + UI::UIThemeManager* themeManager = sceneNode->getUIThemeManager(); + themeManager->setDefaultFont( font ); + sceneNode->setURI( Sys::getProcessPath() + "assets/html/" ); + sceneNode->loadLayoutFromFile( "assets/html/hn_thread_test.html" ); + win->setClearColor( Color::White ); + + while ( win->isRunning() ) { + win->getInput()->update(); + SceneManager::instance()->update(); + + win->clear(); + SceneManager::instance()->draw(); + win->display(); + } + + auto hnMain = sceneNode->getRoot()->find( "hnmain" ); + auto bigbox = sceneNode->getRoot()->find( "bigbox" ); + auto commentTree = sceneNode->getRoot()->findByClass( "comment-tree" ); + auto votelinks = sceneNode->getRoot()->findByClass( "votelinks" ); + auto commentTd = sceneNode->getRoot()->findByClass( "default" ); + auto comment = sceneNode->getRoot()->findByClass( "comment" ); + auto commtext = sceneNode->getRoot()->findByClass( "commtext" ); + + EXPECT_GT( votelinks->getPixelsSize().getWidth(), 0 ); + EXPECT_GT( votelinks->getPixelsSize().getHeight(), 0 ); + + EXPECT_GT( commentTree->getPixelsSize().getWidth(), 0 ); + EXPECT_GT( commentTree->getPixelsSize().getHeight(), 0 ); + + EXPECT_GT( comment->getPixelsSize().getWidth(), 0 ); + EXPECT_GT( commtext->getPixelsSize().getWidth(), 0 ); + + EXPECT_GT( commentTd->getPixelsSize().getWidth(), 0 ); + EXPECT_GT( commentTd->getPixelsSize().getHeight(), 0 ); + + EXPECT_GE( hnMain->getPixelsSize().getHeight(), bigbox->getPixelsSize().getHeight() ); + Float totalTds = commentTd->getPixelsSize().getWidth() + votelinks->getPixelsSize().getWidth(); + Float mainTotal = hnMain->getPixelsSize().getWidth(); + + EXPECT_GT( totalTds, 0 ); + EXPECT_GT( mainTotal, 0 ); + + // EXPECT_LT( totalTds, mainTotal ); + + // compareImages( utest_state, utest_result, win, "eepp-uihtmltable-complex-layout", "html" ); + + Engine::destroySingleton(); +}