diff --git a/bin/unit_tests/assets/html/lobsters_item.html b/bin/unit_tests/assets/html/lobsters_item.html
new file mode 100644
index 000000000..d29921a57
--- /dev/null
+++ b/bin/unit_tests/assets/html/lobsters_item.html
@@ -0,0 +1,158 @@
+
+
+
Lobsters
+
+
+
+
+
+
+ -
+
+
+
+
via
+
jbauer
+
+
|
+
+ caches
+
+
+
+
+
+
+
+
+
+
diff --git a/include/eepp/scene/event.hpp b/include/eepp/scene/event.hpp
index ddaa5d86d..cc0bf7caf 100644
--- a/include/eepp/scene/event.hpp
+++ b/include/eepp/scene/event.hpp
@@ -127,6 +127,7 @@ class EE_API Event {
OnNavigationCompleted,
OnNavigationError,
OnTitleChanged,
+ OnToggle,
NoEvent = eeINDEX_NOT_FOUND
};
diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp
index 6a010b4e4..908beb8f6 100644
--- a/include/eepp/ui.hpp
+++ b/include/eepp/ui.hpp
@@ -93,6 +93,7 @@
#include
#include
#include
+#include
#include
#include
#include
diff --git a/include/eepp/ui/css/propertydefinition.hpp b/include/eepp/ui/css/propertydefinition.hpp
index 3516a90f3..3790b1b48 100644
--- a/include/eepp/ui/css/propertydefinition.hpp
+++ b/include/eepp/ui/css/propertydefinition.hpp
@@ -251,6 +251,7 @@ enum class PropertyId : Uint32 {
Cols = String::hash( "cols" ),
InputMode = String::hash( "input-mode" ),
Hidden = String::hash( "hidden" ),
+ Open = String::hash( "open" ),
Display = String::hash( "display" ),
Position = String::hash( "position" ),
Top = String::hash( "top" ),
diff --git a/include/eepp/ui/csslayouttypes.hpp b/include/eepp/ui/csslayouttypes.hpp
index 0933e83c7..f9a90debe 100644
--- a/include/eepp/ui/csslayouttypes.hpp
+++ b/include/eepp/ui/csslayouttypes.hpp
@@ -44,7 +44,9 @@ enum class CSSListStyleType {
LowerAlpha,
UpperAlpha,
LowerRoman,
- UpperRoman
+ UpperRoman,
+ DisclosureClosed,
+ DisclosureOpen
};
struct EE_API CSSListStyleTypeHelper {
diff --git a/include/eepp/ui/uihelper.hpp b/include/eepp/ui/uihelper.hpp
index 15e1d31ed..8472c26c7 100644
--- a/include/eepp/ui/uihelper.hpp
+++ b/include/eepp/ui/uihelper.hpp
@@ -132,6 +132,8 @@ enum UINodeType {
UI_TYPE_HTML_LIST_ITEM,
UI_TYPE_HTML_IMAGE,
UI_TYPE_HTML_FORM,
+ UI_TYPE_HTML_DETAILS,
+ UI_TYPE_HTML_SUMMARY,
UI_TYPE_WEBVIEW,
UI_TYPE_SVG,
UI_TYPE_TEXTNODE,
diff --git a/include/eepp/ui/uihtmldetails.hpp b/include/eepp/ui/uihtmldetails.hpp
new file mode 100644
index 000000000..c5c7ea6de
--- /dev/null
+++ b/include/eepp/ui/uihtmldetails.hpp
@@ -0,0 +1,110 @@
+#ifndef EE_UI_UIHTMLDETAILS_HPP
+#define EE_UI_UIHTMLDETAILS_HPP
+
+#include
+#include
+#include
+
+namespace EE { namespace UI {
+
+class UIHTMLSummary;
+
+class EE_API UIHTMLDetails : public UIRichText {
+ public:
+ static UIHTMLDetails* New();
+
+ virtual Uint32 getType() const override;
+
+ virtual bool isType( const Uint32& type ) const override;
+
+ virtual void loadFromXmlNode( const pugi::xml_node& node ) override;
+
+ virtual bool applyProperty( const StyleSheetProperty& attribute ) override;
+
+ virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
+ const Uint32& propertyIndex = 0 ) const override;
+
+ virtual std::vector getPropertiesImplemented() const override;
+
+ virtual void updateLayout() override;
+
+ bool isOpen() const;
+
+ void setOpen( bool open );
+
+ UIHTMLSummary* findSummaryChild() const;
+
+ bool isActiveSummary( const UIHTMLSummary* summary ) const;
+
+ void markSummaryMarkersDirty();
+
+ protected:
+ bool mOpen{ false };
+ UIHTMLSummary* mAutoSummary{ nullptr };
+ UnorderedMap mForcedHiddenChildren;
+ bool mSyncingDetailsChildren{ false };
+
+ UIHTMLDetails();
+
+ void ensureAutoSummary();
+
+ void syncDetailsChildrenVisibility();
+
+ virtual void onChildCountChange( Node* child, const bool& removed ) override;
+};
+
+class EE_API UIHTMLSummary : public UIRichText {
+ public:
+ static UIHTMLSummary* New();
+
+ virtual Uint32 getType() const override;
+
+ virtual bool isType( const Uint32& type ) const override;
+
+ virtual void draw() override;
+
+ virtual Uint32 onMouseClick( const Vector2i& position, const Uint32& flags ) override;
+
+ virtual Uint32 onKeyDown( const KeyEvent& event ) override;
+
+ void setDisclosureMarkerDirty();
+
+ CSSListStyleType getListStyleType() const { return mListStyleType; }
+
+ void setListStyleType( CSSListStyleType type );
+
+ virtual bool applyProperty( const StyleSheetProperty& attribute ) override;
+
+ virtual std::string getPropertyString( const PropertyDefinition* propertyDef,
+ const Uint32& propertyIndex = 0 ) const override;
+
+ virtual std::vector getPropertiesImplemented() const override;
+
+ UIHTMLDetails* findParentDetails() const;
+
+ bool toggleParentDetails();
+
+ protected:
+ CSSListStyleType mListStyleType{ CSSListStyleType::DisclosureClosed };
+ std::unique_ptr mListMarkerText;
+ bool mUsingDefaultListStylePadding{ true };
+ bool mApplyingDefaultListStylePadding{ false };
+
+ UIHTMLSummary();
+
+ CSSListStyleType getDisclosureMarkerType() const;
+
+ int countPrecedingSummarySiblings() const;
+
+ void invalidateListMarker();
+
+ void syncDefaultListStylePadding();
+
+ virtual void onFontChanged() override;
+
+ virtual void onFontStyleChanged() override;
+};
+
+}} // namespace EE::UI
+
+#endif
diff --git a/include/eepp/ui/uihtmlliststyle.hpp b/include/eepp/ui/uihtmlliststyle.hpp
new file mode 100644
index 000000000..f26cc9773
--- /dev/null
+++ b/include/eepp/ui/uihtmlliststyle.hpp
@@ -0,0 +1,26 @@
+#ifndef EE_UI_UIHTMLLISTSTYLE_HPP
+#define EE_UI_UIHTMLLISTSTYLE_HPP
+
+#include
+#include
+#include
+#include
+
+namespace EE { namespace UI {
+
+class EE_API UIHTMLListStyle {
+ public:
+ static bool isPrimitiveMarker( CSSListStyleType type );
+
+ static bool isTextMarker( CSSListStyleType type );
+
+ static String getTextMarkerString( CSSListStyleType type, int index );
+
+ static void drawPrimitiveMarker( CSSListStyleType type, const Vector2f& screenPos,
+ const Rectf& paddingPx,
+ const Graphics::FontStyleConfig& style );
+};
+
+}} // namespace EE::UI
+
+#endif
diff --git a/src/eepp/graphics/csslayouttypes.cpp b/src/eepp/graphics/csslayouttypes.cpp
index 3a3e92253..804f25063 100644
--- a/src/eepp/graphics/csslayouttypes.cpp
+++ b/src/eepp/graphics/csslayouttypes.cpp
@@ -107,6 +107,10 @@ std::string CSSListStyleTypeHelper::toString( CSSListStyleType type ) {
return "lower-roman";
case CSSListStyleType::UpperRoman:
return "upper-roman";
+ case CSSListStyleType::DisclosureClosed:
+ return "disclosure-closed";
+ case CSSListStyleType::DisclosureOpen:
+ return "disclosure-open";
case CSSListStyleType::None:
default:
return "none";
@@ -130,6 +134,10 @@ CSSListStyleType CSSListStyleTypeHelper::fromString( std::string_view val ) {
return CSSListStyleType::LowerRoman;
if ( val == "upper-roman" )
return CSSListStyleType::UpperRoman;
+ if ( val == "disclosure-closed" )
+ return CSSListStyleType::DisclosureClosed;
+ if ( val == "disclosure-open" )
+ return CSSListStyleType::DisclosureOpen;
return CSSListStyleType::None;
}
diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp
index dcd00ed00..f9b08137d 100644
--- a/src/eepp/ui/css/stylesheetspecification.cpp
+++ b/src/eepp/ui/css/stylesheetspecification.cpp
@@ -445,6 +445,7 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerProperty( "input-mode", "normal" ).setType( PropertyType::String );
registerProperty( "hidden", "" ).setType( PropertyType::Bool );
+ registerProperty( "open", "" ).setType( PropertyType::Bool );
registerProperty( "display", "inline" ).setType( PropertyType::String );
registerProperty( "position", "static" ).setType( PropertyType::String );
registerProperty( "float", "none" ).setType( PropertyType::String );
diff --git a/src/eepp/ui/uihtmldetails.cpp b/src/eepp/ui/uihtmldetails.cpp
new file mode 100644
index 000000000..febe6fd3e
--- /dev/null
+++ b/src/eepp/ui/uihtmldetails.cpp
@@ -0,0 +1,381 @@
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace EE { namespace UI {
+
+static bool htmlBoolAttributeIsTrue( const StyleSheetProperty& property ) {
+ if ( property.value().empty() )
+ return true;
+
+ std::string value( property.value() );
+ String::toLowerInPlace( value );
+ if ( value == property.getName() )
+ return true;
+ if ( value == "false" || value == "0" || value == "no" )
+ return false;
+ return property.asBool();
+}
+
+UIHTMLDetails* UIHTMLDetails::New() {
+ return eeNew( UIHTMLDetails, () );
+}
+
+UIHTMLDetails::UIHTMLDetails() : UIRichText( "details" ) {}
+
+Uint32 UIHTMLDetails::getType() const {
+ return UI_TYPE_HTML_DETAILS;
+}
+
+bool UIHTMLDetails::isType( const Uint32& type ) const {
+ return UIHTMLDetails::getType() == type ? true : UIRichText::isType( type );
+}
+
+void UIHTMLDetails::loadFromXmlNode( const pugi::xml_node& node ) {
+ UIRichText::loadFromXmlNode( node );
+ syncDetailsChildrenVisibility();
+}
+
+bool UIHTMLDetails::applyProperty( const StyleSheetProperty& attribute ) {
+ if ( !checkPropertyDefinition( attribute ) )
+ return false;
+
+ if ( attribute.getPropertyDefinition()->getPropertyId() == PropertyId::Open ) {
+ setOpen( htmlBoolAttributeIsTrue( attribute ) );
+ return true;
+ }
+
+ return UIRichText::applyProperty( attribute );
+}
+
+std::string UIHTMLDetails::getPropertyString( const PropertyDefinition* propertyDef,
+ const Uint32& propertyIndex ) const {
+ if ( NULL == propertyDef )
+ return "";
+
+ switch ( propertyDef->getPropertyId() ) {
+ case PropertyId::Open:
+ return mOpen ? "true" : "false";
+ default:
+ return UIRichText::getPropertyString( propertyDef, propertyIndex );
+ }
+}
+
+std::vector UIHTMLDetails::getPropertiesImplemented() const {
+ auto props = UIRichText::getPropertiesImplemented();
+ props.push_back( PropertyId::Open );
+ return props;
+}
+
+void UIHTMLDetails::updateLayout() {
+ syncDetailsChildrenVisibility();
+ UIRichText::updateLayout();
+}
+
+bool UIHTMLDetails::isOpen() const {
+ return mOpen;
+}
+
+void UIHTMLDetails::setOpen( bool open ) {
+ if ( mOpen == open )
+ return;
+
+ mOpen = open;
+ syncDetailsChildrenVisibility();
+ markSummaryMarkersDirty();
+ sendCommonEvent( Event::OnToggle );
+ setLayoutDirty();
+ invalidateDraw();
+}
+
+UIHTMLSummary* UIHTMLDetails::findSummaryChild() const {
+ Node* child = getFirstChild();
+ while ( child ) {
+ if ( child->isType( UI_TYPE_HTML_SUMMARY ) ) {
+ auto* summary = child->asType();
+ if ( summary != mAutoSummary )
+ return summary;
+ }
+ child = child->getNextNode();
+ }
+
+ return mAutoSummary && mAutoSummary->getParent() == this ? mAutoSummary : nullptr;
+}
+
+bool UIHTMLDetails::isActiveSummary( const UIHTMLSummary* summary ) const {
+ return summary != nullptr && findSummaryChild() == summary;
+}
+
+void UIHTMLDetails::markSummaryMarkersDirty() {
+ Node* child = getFirstChild();
+ while ( child ) {
+ if ( child->isType( UI_TYPE_HTML_SUMMARY ) )
+ child->asType()->setDisclosureMarkerDirty();
+ child = child->getNextNode();
+ }
+}
+
+void UIHTMLDetails::ensureAutoSummary() {
+ Node* child = getFirstChild();
+ while ( child ) {
+ if ( child->isType( UI_TYPE_HTML_SUMMARY ) && child != mAutoSummary )
+ return;
+ child = child->getNextNode();
+ }
+
+ if ( mAutoSummary && mAutoSummary->getParent() == this )
+ return;
+
+ mAutoSummary = UIHTMLSummary::New();
+ mAutoSummary->setParent( this );
+ mAutoSummary->toBack();
+
+ auto* text = UITextNode::New();
+ text->setText( "Details" );
+ text->setParent( mAutoSummary );
+}
+
+void UIHTMLDetails::syncDetailsChildrenVisibility() {
+ if ( mSyncingDetailsChildren )
+ return;
+
+ mSyncingDetailsChildren = true;
+ ensureAutoSummary();
+ UIHTMLSummary* activeSummary = findSummaryChild();
+
+ for ( auto it = mForcedHiddenChildren.begin(); it != mForcedHiddenChildren.end(); ) {
+ if ( it->first->getParent() != this )
+ it = mForcedHiddenChildren.erase( it );
+ else
+ ++it;
+ }
+
+ Node* child = getFirstChild();
+ while ( child ) {
+ Node* next = child->getNextNode();
+ if ( child == mAutoSummary && activeSummary != mAutoSummary ) {
+ mForcedHiddenChildren.erase( child );
+ child->setVisible( false );
+ } else if ( child == activeSummary ) {
+ auto forcedIt = mForcedHiddenChildren.find( child );
+ if ( forcedIt != mForcedHiddenChildren.end() ) {
+ child->setVisible( forcedIt->second );
+ mForcedHiddenChildren.erase( forcedIt );
+ } else if ( !child->isVisible() ) {
+ child->setVisible( true );
+ }
+ } else if ( mOpen ) {
+ auto forcedIt = mForcedHiddenChildren.find( child );
+ if ( forcedIt != mForcedHiddenChildren.end() ) {
+ child->setVisible( forcedIt->second );
+ mForcedHiddenChildren.erase( forcedIt );
+ }
+ } else {
+ if ( mForcedHiddenChildren.find( child ) == mForcedHiddenChildren.end() )
+ mForcedHiddenChildren[child] = child->isVisible();
+ child->setVisible( false );
+ }
+ child = next;
+ }
+ mSyncingDetailsChildren = false;
+}
+
+void UIHTMLDetails::onChildCountChange( Node* child, const bool& removed ) {
+ if ( removed ) {
+ mForcedHiddenChildren.erase( child );
+ if ( child == mAutoSummary && !mSyncingDetailsChildren )
+ mAutoSummary = nullptr;
+ }
+
+ UIRichText::onChildCountChange( child, removed );
+ if ( !mSyncingDetailsChildren )
+ syncDetailsChildrenVisibility();
+}
+
+UIHTMLSummary* UIHTMLSummary::New() {
+ return eeNew( UIHTMLSummary, () );
+}
+
+UIHTMLSummary::UIHTMLSummary() : UIRichText( "summary" ) {
+ mDisplay = CSSDisplay::ListItem;
+ setFlags( UI_CREATING_NODE );
+ applyProperty( StyleSheetProperty( "cursor", "pointer" ) );
+ applyProperty( StyleSheetProperty( "padding-left", "20dp" ) );
+ applyProperty( StyleSheetProperty( "list-style-type", "disclosure-closed" ) );
+ getUIStyle()->setStyleSheetProperty( StyleSheetProperty( "cursor", "pointer" ) );
+ getUIStyle()->setStyleSheetProperty(
+ StyleSheetProperty( "list-style-type", "disclosure-closed" ) );
+ unsetFlags( UI_CREATING_NODE );
+ setFlags( UI_TAB_FOCUSABLE );
+}
+
+Uint32 UIHTMLSummary::getType() const {
+ return UI_TYPE_HTML_SUMMARY;
+}
+
+bool UIHTMLSummary::isType( const Uint32& type ) const {
+ return UIHTMLSummary::getType() == type ? true : UIRichText::isType( type );
+}
+
+void UIHTMLSummary::draw() {
+ UIRichText::draw();
+ if ( mVisible && 0.f != mAlpha ) {
+ const CSSListStyleType markerType = getDisclosureMarkerType();
+ if ( UIHTMLListStyle::isPrimitiveMarker( markerType ) ) {
+ UIHTMLListStyle::drawPrimitiveMarker( markerType, mScreenPos, mPaddingPx,
+ mRichText.getFontStyleConfig() );
+ } else if ( mListMarkerText && !mListMarkerText->getString().empty() ) {
+ const Float fontSize = mRichText.getFontStyleConfig().CharacterSize;
+ const Float offset = 0.25f * fontSize;
+ const Float markerX =
+ mScreenPos.x + mPaddingPx.Left - mListMarkerText->getTextWidth() - offset;
+ mListMarkerText->draw( markerX, mScreenPos.y + mPaddingPx.Top, Vector2f::One, 0.f,
+ getBlendMode() );
+ }
+ }
+}
+
+void UIHTMLSummary::setDisclosureMarkerDirty() {
+ invalidateDraw();
+}
+
+UIHTMLDetails* UIHTMLSummary::findParentDetails() const {
+ Node* parent = getParent();
+ while ( parent ) {
+ if ( parent->isType( UI_TYPE_HTML_DETAILS ) )
+ return parent->asType();
+ parent = parent->getParent();
+ }
+ return nullptr;
+}
+
+bool UIHTMLSummary::toggleParentDetails() {
+ UIHTMLDetails* details = findParentDetails();
+ if ( details && details->isActiveSummary( this ) ) {
+ details->setOpen( !details->isOpen() );
+ return true;
+ }
+ return false;
+}
+
+void UIHTMLSummary::setListStyleType( CSSListStyleType type ) {
+ if ( mListStyleType != type ) {
+ mListStyleType = type;
+ syncDefaultListStylePadding();
+ invalidateListMarker();
+ }
+}
+
+bool UIHTMLSummary::applyProperty( const StyleSheetProperty& attribute ) {
+ if ( !checkPropertyDefinition( attribute ) )
+ return false;
+
+ switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
+ case PropertyId::ListStyleType:
+ setListStyleType( CSSListStyleTypeHelper::fromString( attribute.value() ) );
+ return true;
+ case PropertyId::PaddingLeft:
+ if ( !isCreatingNode() && !mApplyingDefaultListStylePadding )
+ mUsingDefaultListStylePadding = false;
+ break;
+ default:
+ break;
+ }
+
+ return UIRichText::applyProperty( attribute );
+}
+
+std::string UIHTMLSummary::getPropertyString( const PropertyDefinition* propertyDef,
+ const Uint32& propertyIndex ) const {
+ if ( NULL == propertyDef )
+ return "";
+
+ switch ( propertyDef->getPropertyId() ) {
+ case PropertyId::ListStyleType:
+ return CSSListStyleTypeHelper::toString( mListStyleType );
+ default:
+ return UIRichText::getPropertyString( propertyDef, propertyIndex );
+ }
+}
+
+std::vector UIHTMLSummary::getPropertiesImplemented() const {
+ auto props = UIRichText::getPropertiesImplemented();
+ props.push_back( PropertyId::ListStyleType );
+ return props;
+}
+
+CSSListStyleType UIHTMLSummary::getDisclosureMarkerType() const {
+ UIHTMLDetails* details = findParentDetails();
+ if ( mListStyleType == CSSListStyleType::DisclosureClosed && details && details->isOpen() )
+ return CSSListStyleType::DisclosureOpen;
+ return mListStyleType;
+}
+
+int UIHTMLSummary::countPrecedingSummarySiblings() const {
+ int count = 0;
+ Node* prev = getPrevNode();
+ while ( prev ) {
+ if ( prev->isWidget() && prev->asType()->getElementTag() == "summary" )
+ count++;
+ prev = prev->getPrevNode();
+ }
+ return count;
+}
+
+void UIHTMLSummary::invalidateListMarker() {
+ if ( !UIHTMLListStyle::isTextMarker( mListStyleType ) ) {
+ mListMarkerText.reset();
+ } else {
+ String markerStr =
+ UIHTMLListStyle::getTextMarkerString( mListStyleType, countPrecedingSummarySiblings() );
+ if ( !markerStr.empty() ) {
+ if ( !mListMarkerText )
+ mListMarkerText = std::make_unique();
+ mListMarkerText->setString( markerStr );
+ mListMarkerText->setStyleConfig( mRichText.getFontStyleConfig() );
+ } else {
+ mListMarkerText.reset();
+ }
+ }
+ invalidateDraw();
+}
+
+void UIHTMLSummary::syncDefaultListStylePadding() {
+ if ( !mUsingDefaultListStylePadding )
+ return;
+
+ mApplyingDefaultListStylePadding = true;
+ UIRichText::applyProperty( StyleSheetProperty(
+ "padding-left", mListStyleType == CSSListStyleType::None ? "0dp" : "20dp" ) );
+ mApplyingDefaultListStylePadding = false;
+}
+
+Uint32 UIHTMLSummary::onMouseClick( const Vector2i& position, const Uint32& flags ) {
+ if ( toggleParentDetails() )
+ return 1;
+ return UIRichText::onMouseClick( position, flags );
+}
+
+Uint32 UIHTMLSummary::onKeyDown( const KeyEvent& event ) {
+ if ( event.getKeyCode() == KEY_RETURN || event.getKeyCode() == KEY_KP_ENTER ||
+ event.getKeyCode() == KEY_SPACE ) {
+ if ( toggleParentDetails() )
+ return 1;
+ }
+ return UIRichText::onKeyDown( event );
+}
+
+void UIHTMLSummary::onFontChanged() {
+ UIRichText::onFontChanged();
+ invalidateListMarker();
+}
+
+void UIHTMLSummary::onFontStyleChanged() {
+ UIRichText::onFontStyleChanged();
+ invalidateListMarker();
+}
+
+}} // namespace EE::UI
diff --git a/src/eepp/ui/uihtmllistitem.cpp b/src/eepp/ui/uihtmllistitem.cpp
index f43f330a1..453abbd4c 100644
--- a/src/eepp/ui/uihtmllistitem.cpp
+++ b/src/eepp/ui/uihtmllistitem.cpp
@@ -1,6 +1,6 @@
-#include
#include
#include
+#include
#include
namespace EE { namespace UI {
@@ -41,36 +41,10 @@ void UIHTMLListItem::draw() {
const FontStyleConfig& style = mRichText.getFontStyleConfig();
Float fontSize = style.CharacterSize;
Float offset = 0.25f * fontSize;
- Float lineHeight =
- style.Font ? style.Font->getLineSpacing( (unsigned int)fontSize ) : fontSize;
Float lineTop = mScreenPos.y + mPaddingPx.Top;
- if ( mListStyleType == CSSListStyleType::Disc ) {
- Float radius = fontSize * 0.22f;
- Float markerX = std::floor( mScreenPos.x + mPaddingPx.Left - radius * 2.f - offset );
- Float markerY = std::floor( lineTop + ( lineHeight - radius * 2.f ) * 0.5f + radius );
- Primitives p;
- p.setColor( style.FontColor );
- p.setFillMode( PrimitiveFillMode::DRAW_FILL );
- p.drawCircle( { markerX, markerY }, radius );
- } else if ( mListStyleType == CSSListStyleType::Circle ) {
- Float radius = fontSize * 0.2f;
- Float lineWidth = fontSize * 0.04f;
- Float markerX = std::floor( mScreenPos.x + mPaddingPx.Left - radius * 2.f - offset );
- Float markerY = std::floor( lineTop + ( lineHeight - radius * 2.f ) * 0.5f + radius );
- Primitives p;
- p.setColor( style.FontColor );
- p.setFillMode( PrimitiveFillMode::DRAW_LINE );
- p.setLineWidth( lineWidth );
- p.drawCircle( { markerX, markerY }, radius );
- } else if ( mListStyleType == CSSListStyleType::Square ) {
- Float size = fontSize * 0.38f;
- Float markerX = std::floor( mScreenPos.x + mPaddingPx.Left - size - fontSize * 0.5 );
- Float markerY = std::floor( lineTop + ( lineHeight - size ) * 0.5f );
- Primitives p;
- p.setColor( style.FontColor );
- p.setFillMode( PrimitiveFillMode::DRAW_FILL );
- p.drawRectangle( Rectf( markerX, markerY, markerX + size, markerY + size ) );
+ if ( UIHTMLListStyle::isPrimitiveMarker( mListStyleType ) ) {
+ UIHTMLListStyle::drawPrimitiveMarker( mListStyleType, mScreenPos, mPaddingPx, style );
} else if ( mListMarkerText && !mListMarkerText->getString().empty() ) {
Float markerX =
mScreenPos.x + mPaddingPx.Left - mListMarkerText->getTextWidth() - offset;
@@ -118,9 +92,7 @@ std::vector UIHTMLListItem::getPropertiesImplemented() const {
}
void UIHTMLListItem::invalidateList() {
- if ( mListStyleType == CSSListStyleType::None || mListStyleType == CSSListStyleType::Disc ||
- mListStyleType == CSSListStyleType::Circle ||
- mListStyleType == CSSListStyleType::Square ) {
+ if ( !UIHTMLListStyle::isTextMarker( mListStyleType ) ) {
mListMarkerText.reset();
} else {
String::View markerStr = getListMarkerString();
@@ -150,51 +122,11 @@ int UIHTMLListItem::countPrecedingLiSiblings() const {
String::View UIHTMLListItem::getListMarkerString() const {
static String sBuf;
- switch ( mListStyleType ) {
- case CSSListStyleType::None:
- case CSSListStyleType::Disc:
- case CSSListStyleType::Circle:
- case CSSListStyleType::Square:
- return {};
- case CSSListStyleType::Decimal: {
- int idx = countPrecedingLiSiblings() + 1;
- sBuf = String( String::toString( idx ) + ". " );
- return sBuf.view();
- }
- case CSSListStyleType::LowerAlpha: {
- int idx = countPrecedingLiSiblings();
- char c = 'a' + ( idx % 26 );
- sBuf = String( 1, (String::StringBaseType)c ) + ". ";
- return sBuf.view();
- }
- case CSSListStyleType::UpperAlpha: {
- int idx = countPrecedingLiSiblings();
- char c = 'A' + ( idx % 26 );
- sBuf = String( 1, (String::StringBaseType)c ) + ". ";
- return sBuf.view();
- }
- case CSSListStyleType::LowerRoman: {
- static const char* numerals[] = { "i", "ii", "iii", "iv", "v", "vi",
- "vii", "viii", "ix", "x", "xi", "xii" };
- int idx = countPrecedingLiSiblings();
- if ( idx < 12 )
- sBuf = String( numerals[idx] ) + ". ";
- else
- sBuf = String( String::toString( idx + 1 ) + ". " );
- return sBuf.view();
- }
- case CSSListStyleType::UpperRoman: {
- static const char* numerals[] = { "I", "II", "III", "IV", "V", "VI",
- "VII", "VIII", "IX", "X", "XI", "XII" };
- int idx = countPrecedingLiSiblings();
- if ( idx < 12 )
- sBuf = String( numerals[idx] ) + ". ";
- else
- sBuf = String( String::toString( idx + 1 ) + ". " );
- return sBuf.view();
- }
- }
- return {};
+ if ( !UIHTMLListStyle::isTextMarker( mListStyleType ) )
+ return {};
+
+ sBuf = UIHTMLListStyle::getTextMarkerString( mListStyleType, countPrecedingLiSiblings() );
+ return sBuf.view();
}
}} // namespace EE::UI
diff --git a/src/eepp/ui/uihtmlliststyle.cpp b/src/eepp/ui/uihtmlliststyle.cpp
new file mode 100644
index 000000000..071af31ba
--- /dev/null
+++ b/src/eepp/ui/uihtmlliststyle.cpp
@@ -0,0 +1,115 @@
+#include
+#include
+#include
+
+namespace EE { namespace UI {
+
+bool UIHTMLListStyle::isPrimitiveMarker( CSSListStyleType type ) {
+ switch ( type ) {
+ case CSSListStyleType::Disc:
+ case CSSListStyleType::Circle:
+ case CSSListStyleType::Square:
+ case CSSListStyleType::DisclosureClosed:
+ case CSSListStyleType::DisclosureOpen:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool UIHTMLListStyle::isTextMarker( CSSListStyleType type ) {
+ return type != CSSListStyleType::None && !isPrimitiveMarker( type );
+}
+
+String UIHTMLListStyle::getTextMarkerString( CSSListStyleType type, int index ) {
+ switch ( type ) {
+ case CSSListStyleType::Decimal:
+ return String( String::toString( index + 1 ) + ". " );
+ case CSSListStyleType::LowerAlpha: {
+ char c = 'a' + ( index % 26 );
+ return String( 1, (String::StringBaseType)c ) + ". ";
+ }
+ case CSSListStyleType::UpperAlpha: {
+ char c = 'A' + ( index % 26 );
+ return String( 1, (String::StringBaseType)c ) + ". ";
+ }
+ case CSSListStyleType::LowerRoman: {
+ static const char* numerals[] = { "i", "ii", "iii", "iv", "v", "vi",
+ "vii", "viii", "ix", "x", "xi", "xii" };
+ return index < 12 ? String( numerals[index] ) + ". "
+ : String( String::toString( index + 1 ) + ". " );
+ }
+ case CSSListStyleType::UpperRoman: {
+ static const char* numerals[] = { "I", "II", "III", "IV", "V", "VI",
+ "VII", "VIII", "IX", "X", "XI", "XII" };
+ return index < 12 ? String( numerals[index] ) + ". "
+ : String( String::toString( index + 1 ) + ". " );
+ }
+ default:
+ return {};
+ }
+}
+
+void UIHTMLListStyle::drawPrimitiveMarker( CSSListStyleType type, const Vector2f& screenPos,
+ const Rectf& paddingPx,
+ const Graphics::FontStyleConfig& style ) {
+ Float fontSize = style.CharacterSize;
+ Float offset = 0.25f * fontSize;
+ Float lineHeight = style.Font ? style.Font->getLineSpacing( (unsigned int)fontSize ) : fontSize;
+ Float lineTop = screenPos.y + paddingPx.Top;
+ Graphics::Primitives p;
+ p.setColor( style.FontColor );
+
+ switch ( type ) {
+ case CSSListStyleType::Disc: {
+ Float radius = fontSize * 0.22f;
+ Float markerX = std::floor( screenPos.x + paddingPx.Left - radius * 2.f - offset );
+ Float markerY = std::floor( lineTop + ( lineHeight - radius * 2.f ) * 0.5f + radius );
+ p.setFillMode( Graphics::PrimitiveFillMode::DRAW_FILL );
+ p.drawCircle( { markerX, markerY }, radius );
+ break;
+ }
+ case CSSListStyleType::Circle: {
+ Float radius = fontSize * 0.2f;
+ Float lineWidth = fontSize * 0.04f;
+ Float markerX = std::floor( screenPos.x + paddingPx.Left - radius * 2.f - offset );
+ Float markerY = std::floor( lineTop + ( lineHeight - radius * 2.f ) * 0.5f + radius );
+ p.setFillMode( Graphics::PrimitiveFillMode::DRAW_LINE );
+ p.setLineWidth( lineWidth );
+ p.drawCircle( { markerX, markerY }, radius );
+ break;
+ }
+ case CSSListStyleType::Square: {
+ Float size = fontSize * 0.38f;
+ Float markerX = std::floor( screenPos.x + paddingPx.Left - size - fontSize * 0.5 );
+ Float markerY = std::floor( lineTop + ( lineHeight - size ) * 0.5f );
+ p.setFillMode( Graphics::PrimitiveFillMode::DRAW_FILL );
+ p.drawRectangle( Rectf( markerX, markerY, markerX + size, markerY + size ) );
+ break;
+ }
+ case CSSListStyleType::DisclosureClosed: {
+ Float size = fontSize * 0.48f;
+ Float cx = std::floor( screenPos.x + paddingPx.Left - fontSize * 0.8f );
+ Float cy = std::floor( lineTop + lineHeight * 0.5f );
+ p.setFillMode( Graphics::PrimitiveFillMode::DRAW_FILL );
+ p.drawTriangle( Triangle2f( { cx - size * 0.35f, cy - size * 0.5f },
+ { cx - size * 0.35f, cy + size * 0.5f },
+ { cx + size * 0.35f, cy } ) );
+ break;
+ }
+ case CSSListStyleType::DisclosureOpen: {
+ Float size = fontSize * 0.52f;
+ Float cx = std::floor( screenPos.x + paddingPx.Left - fontSize * 0.8f );
+ Float cy = std::floor( lineTop + lineHeight * 0.5f );
+ p.setFillMode( Graphics::PrimitiveFillMode::DRAW_FILL );
+ p.drawTriangle( Triangle2f( { cx - size * 0.5f, cy - size * 0.25f },
+ { cx + size * 0.5f, cy - size * 0.25f },
+ { cx, cy + size * 0.45f } ) );
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+}} // namespace EE::UI
diff --git a/src/eepp/ui/uiwidgetcreator.cpp b/src/eepp/ui/uiwidgetcreator.cpp
index 58397b810..265a8160c 100644
--- a/src/eepp/ui/uiwidgetcreator.cpp
+++ b/src/eepp/ui/uiwidgetcreator.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -200,6 +201,8 @@ void UIWidgetCreator::createBaseWidgetList() {
return w;
};
registeredWidget["li"] = UIHTMLListItem::New;
+ registeredWidget["details"] = UIHTMLDetails::New;
+ registeredWidget["summary"] = UIHTMLSummary::New;
registeredWidget["pre"] = UIRichText::NewPre;
registeredWidget["picture"] = [] { return UITextSpan::NewWithTag( "picture" ); };
registeredWidget["img"] = UIHTMLImage::New;
diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp
index 871636caa..757ea6bc6 100644
--- a/src/tests/unit_tests/uihtml_tests.cpp
+++ b/src/tests/unit_tests/uihtml_tests.cpp
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -12,6 +13,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -580,6 +582,329 @@ UTEST( UIHTML, DataProperties ) {
Engine::destroySingleton();
}
+UTEST( UIHTMLDetails, closedByDefault ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+ Label
Content
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->find( "d" )->asType();
+ auto* summary = sceneNode->getRoot()->find( "s" )->asType();
+ auto* content = sceneNode->getRoot()->find( "p" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( summary != nullptr );
+ ASSERT_TRUE( content != nullptr );
+ EXPECT_FALSE( details->isOpen() );
+ EXPECT_TRUE( summary->isVisible() );
+ EXPECT_FALSE( content->isVisible() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, summaryListStyleType ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+
+ Default
+ Open marker
+ Decimal marker
+
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
+ ASSERT_TRUE( propDef != nullptr );
+
+ auto* s1 = sceneNode->getRoot()->find( "s1" )->asType();
+ auto* s2 = sceneNode->getRoot()->find( "s2" )->asType();
+ auto* s3 = sceneNode->getRoot()->find( "s3" )->asType();
+ ASSERT_TRUE( s1 != nullptr );
+ ASSERT_TRUE( s2 != nullptr );
+ ASSERT_TRUE( s3 != nullptr );
+
+ EXPECT_TRUE( s1->getPropertyString( propDef ) == "disclosure-closed" );
+ EXPECT_TRUE( s2->getPropertyString( propDef ) == "disclosure-open" );
+ EXPECT_TRUE( s3->getPropertyString( propDef ) == "decimal" );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, summaryListStyleNoneClearsDefaultPadding ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+
+ Default marker
+ No marker
+
+ Explicit padding
+
+
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* defaultSummary = sceneNode->getRoot()->find( "default" )->asType();
+ auto* noneSummary = sceneNode->getRoot()->find( "none" )->asType();
+ auto* explicitSummary = sceneNode->getRoot()->find( "explicit" )->asType();
+ ASSERT_TRUE( defaultSummary != nullptr );
+ ASSERT_TRUE( noneSummary != nullptr );
+ ASSERT_TRUE( explicitSummary != nullptr );
+
+ EXPECT_GT( defaultSummary->getPixelsPadding().Left, 0.f );
+ EXPECT_NEAR( noneSummary->getPixelsPadding().Left, 0.f, 0.5f );
+ EXPECT_NEAR( explicitSummary->getPixelsPadding().Left, 7.f, 0.5f );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, inlineBlockSummaryListStyleNoneSize ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ std::string html;
+ FileSystem::fileGet( "assets/html/lobsters_item.html", html );
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->querySelector( ".caches" )->asType();
+ auto* summary =
+ sceneNode->getRoot()->querySelector( ".caches summary" )->asType();
+ auto* author = sceneNode->getRoot()->querySelector( ".u-author" )->asType();
+ auto* time = sceneNode->getRoot()->querySelector( "time" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( summary != nullptr );
+ ASSERT_TRUE( author != nullptr );
+ ASSERT_TRUE( time != nullptr );
+
+ EXPECT_EQ( details->getDisplay(), CSSDisplay::InlineBlock );
+ EXPECT_EQ( summary->getListStyleType(), CSSListStyleType::None );
+ EXPECT_NEAR( summary->getPixelsPadding().Left, 0.f, 0.5f );
+ EXPECT_LE( details->getPixelsSize().getHeight(),
+ eemax( author->getPixelsSize().getHeight(), time->getPixelsSize().getHeight() ) +
+ 1.f );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, openAttribute ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+ Label
Content
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->find( "d" )->asType();
+ auto* content = sceneNode->getRoot()->find( "p" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( content != nullptr );
+ EXPECT_TRUE( details->isOpen() );
+ EXPECT_TRUE( content->isVisible() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, openAttributeExplicitFalse ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+ Label
Content
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->find( "d" )->asType();
+ auto* content = sceneNode->getRoot()->find( "p" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( content != nullptr );
+ EXPECT_FALSE( details->isOpen() );
+ EXPECT_FALSE( content->isVisible() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, toggleViaMouse ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+ Label
Content
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->find( "d" )->asType();
+ auto* summary = sceneNode->getRoot()->find( "s" )->asType();
+ auto* content = sceneNode->getRoot()->find( "p" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( summary != nullptr );
+ ASSERT_TRUE( content != nullptr );
+
+ summary->onMouseClick( summary->getPixelsPosition().asInt(), EE_BUTTON_LMASK );
+ EXPECT_TRUE( details->isOpen() );
+ EXPECT_TRUE( content->isVisible() );
+ summary->onMouseClick( summary->getPixelsPosition().asInt(), EE_BUTTON_LMASK );
+ EXPECT_FALSE( details->isOpen() );
+ EXPECT_FALSE( content->isVisible() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, toggleViaKeyboard ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+ Label
Content
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->find( "d" )->asType();
+ auto* summary = sceneNode->getRoot()->find( "s" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( summary != nullptr );
+
+ KeyEvent enter( summary, Event::KeyDown, KEY_RETURN, SCANCODE_RETURN, 0, 0 );
+ KeyEvent space( summary, Event::KeyDown, KEY_SPACE, SCANCODE_SPACE, 0, 0 );
+ EXPECT_EQ( summary->onKeyDown( enter ), 1u );
+ EXPECT_TRUE( details->isOpen() );
+ EXPECT_EQ( summary->onKeyDown( space ), 1u );
+ EXPECT_FALSE( details->isOpen() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, toggleEvent ) {
+ init_ui_test();
+ auto* details = UIHTMLDetails::New();
+ details->setParent( SceneManager::instance()->getUISceneNode()->getRoot() );
+ int toggleCount = 0;
+ details->on( Event::OnToggle, [&toggleCount]( const Event* ) { toggleCount++; } );
+
+ details->setOpen( true );
+ details->setOpen( true );
+ details->setOpen( false );
+ EXPECT_EQ( toggleCount, 2 );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, autoSummary ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+ Content
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->find( "d" )->asType();
+ auto* content = sceneNode->getRoot()->find( "p" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( content != nullptr );
+ auto* summary = details->findSummaryChild();
+ ASSERT_TRUE( summary != nullptr );
+ EXPECT_TRUE( summary->isVisible() );
+ EXPECT_FALSE( content->isVisible() );
+ EXPECT_TRUE( summary->toggleParentDetails() );
+ EXPECT_TRUE( details->isOpen() );
+ EXPECT_TRUE( content->isVisible() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, multipleSummaries ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+
+ One
+ Two
+ Content
+
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->find( "d" )->asType();
+ auto* s1 = sceneNode->getRoot()->find( "s1" )->asType();
+ auto* s2 = sceneNode->getRoot()->find( "s2" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( s1 != nullptr );
+ ASSERT_TRUE( s2 != nullptr );
+ EXPECT_TRUE( s1->isVisible() );
+ EXPECT_FALSE( s2->isVisible() );
+ EXPECT_TRUE( s1->toggleParentDetails() );
+ EXPECT_TRUE( details->isOpen() );
+ EXPECT_TRUE( s2->isVisible() );
+ EXPECT_FALSE( s2->toggleParentDetails() );
+ EXPECT_TRUE( details->isOpen() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, nested ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+
+ Outer
+
+ Inner
+ Inner content
+
+
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* outer = sceneNode->getRoot()->find( "outer" )->asType();
+ auto* inner = sceneNode->getRoot()->find( "inner" )->asType();
+ auto* innerSummary = sceneNode->getRoot()->find( "inner_s" )->asType();
+ ASSERT_TRUE( outer != nullptr );
+ ASSERT_TRUE( inner != nullptr );
+ ASSERT_TRUE( innerSummary != nullptr );
+ EXPECT_TRUE( outer->isOpen() );
+ EXPECT_FALSE( inner->isOpen() );
+ EXPECT_TRUE( innerSummary->toggleParentDetails() );
+ EXPECT_TRUE( outer->isOpen() );
+ EXPECT_TRUE( inner->isOpen() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, hiddenChildPreserved ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( R"html(
+ Label
Content
+ )html" ) );
+ sceneNode->updateDirtyLayouts();
+
+ auto* details = sceneNode->getRoot()->find( "d" )->asType();
+ auto* content = sceneNode->getRoot()->find( "p" )->asType();
+ ASSERT_TRUE( details != nullptr );
+ ASSERT_TRUE( content != nullptr );
+ EXPECT_FALSE( content->isVisible() );
+ details->setOpen( false );
+ details->setOpen( true );
+ EXPECT_FALSE( content->isVisible() );
+
+ Engine::destroySingleton();
+}
+
+UTEST( UIHTMLDetails, dynamicChildAddedWhileClosed ) {
+ init_ui_test();
+ auto* details = UIHTMLDetails::New();
+ details->setParent( SceneManager::instance()->getUISceneNode()->getRoot() );
+ auto* summary = UIHTMLSummary::New();
+ summary->setParent( details );
+ details->setOpen( false );
+ auto* content = UIRichText::NewParagraph();
+ content->setParent( details );
+ details->updateLayout();
+
+ EXPECT_TRUE( summary->isVisible() );
+ EXPECT_FALSE( content->isVisible() );
+
+ Engine::destroySingleton();
+}
+
UTEST( UIHTMLTextArea, rowsColsAttribute ) {
init_ui_test();
auto* scene = SceneManager::instance()->getUISceneNode();
@@ -883,6 +1208,34 @@ UTEST( UILayout, listStyleTypeDisc ) {
Engine::destroySingleton();
}
+UTEST( UILayout, listStyleTypeDisclosure ) {
+ init_ui_test();
+ auto* sceneNode = SceneManager::instance()->getUISceneNode();
+ sceneNode->loadLayoutFromString( R"html(
+
+
+
+ )html" );
+
+ sceneNode->updateDirtyLayouts();
+
+ const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
+ ASSERT_TRUE( propDef != nullptr );
+
+ auto* li1 = sceneNode->getRoot()->find( "li1" )->asType();
+ auto* li2 = sceneNode->getRoot()->find( "li2" )->asType();
+ ASSERT_TRUE( li1 != nullptr );
+ ASSERT_TRUE( li2 != nullptr );
+
+ EXPECT_TRUE( li1->getPropertyString( propDef ) == "disclosure-closed" );
+ EXPECT_TRUE( li2->getPropertyString( propDef ) == "disclosure-open" );
+
+ Engine::destroySingleton();
+}
+
UTEST( UILayout, listStyleShorthand ) {
init_ui_test();
auto* sceneNode = SceneManager::instance()->getUISceneNode();