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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 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();
+}
+ reply +
+