Initial support for HTML element tags: details and summary.

This commit is contained in:
Martín Lucas Golini
2026-05-16 22:23:31 -03:00
parent 702f995608
commit e223ed6d3c
15 changed files with 1172 additions and 78 deletions

View File

@@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang=en>
<title>Lobsters</title>
<style>
:root {
--font-size-default: 1rem;
--font-size-tags: 0.66rem;
--base-light: 255 255 255;
--base-shadow: 0 0 0;
--comment-controls-height: 1.2lh;
--input-outline-width: 2px
--color-fg: black;
--color-fg-link: black;
}
html {
font-size: var(--font-size-default);
}
body > * {
font-family: arial, sans-serif;
color: var(--color-fg);
line-height: 1.45em
}
body {
margin: 0 auto;
padding-bottom: 2em
}
a {
color: var(--color-fg-link);
cursor: pointer;
}
summary {
cursor: pointer
}
ol.stories {
padding: 0;
list-style: none;
margin: 0
}
li.story {
clear: both
}
li div.details {
padding-top: 0.1em
}
li .link {
font-weight: bold;
vertical-align: middle
}
li .link a {
text-decoration: none
}
li.story .description_present {
padding-left: 0.25em;
text-decoration: none;
vertical-align: middle
}
li.story a.tag {
vertical-align: middle
}
li .tags {
margin-right: 0.25em
}
li .domain {
font-style: italic;
text-decoration: none;
vertical-align: middle
}
.caches {
display: inline-block;
position: relative
}
.caches summary {
list-style: none
}
.caches ul {
position: absolute;
border: 1px solid var(--color-box-border);
white-space: nowrap;
list-style: none;
padding: 0;
z-index: 1
}
.caches li {
border-bottom: 1px solid var(--color-box-border)
}
.caches li:last-child {
border-bottom: 0
}
.caches a {
text-decoration: none;
display: block;
padding: 3px 7px
}
.caches a:hover {
text-decoration: underline
}
</style>
</head>
<body data-username data-now-unix=1778970488>
<div id=inside>
<ol class="stories list">
<li id=story_4g74mw data-shortid=4g74mw class=story>
<div class="story_liner h-entry">
<div class=details>
<div class=byline>
<span> via </span>
<a class="u-author h-card" href=https://lobste.rs/~jbauer>jbauer</a>
<time title="2026-05-16 04:45:51" datetime="2026-05-16 04:45:51" data-at-unix=1778924751>12
hours ago</time>
<span aria-hidden=true> | </span>
<details class=caches name=caches>
<summary>caches</summary>
<ul>
<li><a
href=https://web.archive.org/web/3/https%3A%2F%2Fratfactor.com%2Fascetic-computing>Archive.org</a>
</li>
<li><a
href="https://ghostarchive.org/search?term=https%3A%2F%2Fratfactor.com%2Fascetic-computing">Ghostarchive</a>
</li>
</ul>
</details>
<span class=comments_label>
<span aria-hidden=true> | </span>
<a role=heading aria-level=2 href=https://lobste.rs/s/4g74mw/ascetic_computing>
10 comments
</a>
</span>
</div>
</div>
</div>
<a href=https://lobste.rs/s/4g74mw/ascetic_computing class=mobile_comments style=display:none>
</a>
</li>
</ol>
</div>

View File

@@ -127,6 +127,7 @@ class EE_API Event {
OnNavigationCompleted,
OnNavigationError,
OnTitleChanged,
OnToggle,
NoEvent = eeINDEX_NOT_FOUND
};

View File

@@ -93,6 +93,7 @@
#include <eepp/ui/uifontstyleconfig.hpp>
#include <eepp/ui/uigridlayout.hpp>
#include <eepp/ui/uihelper.hpp>
#include <eepp/ui/uihtmldetails.hpp>
#include <eepp/ui/uihtmlform.hpp>
#include <eepp/ui/uihtmlimage.hpp>
#include <eepp/ui/uihtmlinput.hpp>

View File

@@ -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" ),

View File

@@ -44,7 +44,9 @@ enum class CSSListStyleType {
LowerAlpha,
UpperAlpha,
LowerRoman,
UpperRoman
UpperRoman,
DisclosureClosed,
DisclosureOpen
};
struct EE_API CSSListStyleTypeHelper {

View File

@@ -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,

View File

@@ -0,0 +1,110 @@
#ifndef EE_UI_UIHTMLDETAILS_HPP
#define EE_UI_UIHTMLDETAILS_HPP
#include <eepp/graphics/text.hpp>
#include <eepp/ui/uirichtext.hpp>
#include <memory>
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<PropertyId> 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<Node*, bool> 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<PropertyId> getPropertiesImplemented() const override;
UIHTMLDetails* findParentDetails() const;
bool toggleParentDetails();
protected:
CSSListStyleType mListStyleType{ CSSListStyleType::DisclosureClosed };
std::unique_ptr<Graphics::Text> 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

View File

@@ -0,0 +1,26 @@
#ifndef EE_UI_UIHTMLLISTSTYLE_HPP
#define EE_UI_UIHTMLLISTSTYLE_HPP
#include <eepp/core/string.hpp>
#include <eepp/graphics/fontstyleconfig.hpp>
#include <eepp/math/rect.hpp>
#include <eepp/ui/csslayouttypes.hpp>
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

View File

@@ -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;
}

View File

@@ -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 );

View File

@@ -0,0 +1,381 @@
#include <eepp/scene/keyevent.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/uihtmldetails.hpp>
#include <eepp/ui/uihtmlliststyle.hpp>
#include <eepp/ui/uistyle.hpp>
#include <eepp/ui/uitextnode.hpp>
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<PropertyId> 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<UIHTMLSummary>();
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<UIHTMLSummary>()->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<UIHTMLDetails>();
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<PropertyId> 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<UIWidget>()->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<Text>();
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

View File

@@ -1,6 +1,6 @@
#include <eepp/graphics/primitives.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/uihtmllistitem.hpp>
#include <eepp/ui/uihtmlliststyle.hpp>
#include <eepp/ui/uilayouter.hpp>
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<PropertyId> 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

View File

@@ -0,0 +1,115 @@
#include <eepp/graphics/font.hpp>
#include <eepp/graphics/primitives.hpp>
#include <eepp/ui/uihtmlliststyle.hpp>
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

View File

@@ -8,6 +8,7 @@
#include <eepp/ui/uidropdownlist.hpp>
#include <eepp/ui/uidropdownmodellist.hpp>
#include <eepp/ui/uigridlayout.hpp>
#include <eepp/ui/uihtmldetails.hpp>
#include <eepp/ui/uihtmlform.hpp>
#include <eepp/ui/uihtmlimage.hpp>
#include <eepp/ui/uihtmlinput.hpp>
@@ -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;

View File

@@ -4,6 +4,7 @@
#include <eepp/graphics/fontfamily.hpp>
#include <eepp/graphics/fonttruetype.hpp>
#include <eepp/graphics/renderer/renderer.hpp>
#include <eepp/scene/keyevent.hpp>
#include <eepp/scene/scenemanager.hpp>
#include <eepp/system/filesystem.hpp>
#include <eepp/system/sys.hpp>
@@ -12,6 +13,7 @@
#include <eepp/ui/tools/htmlformatter.hpp>
#include <eepp/ui/tools/uiwidgetinspector.hpp>
#include <eepp/ui/uicheckbox.hpp>
#include <eepp/ui/uihtmldetails.hpp>
#include <eepp/ui/uihtmlinput.hpp>
#include <eepp/ui/uihtmltable.hpp>
#include <eepp/ui/uihtmltextarea.hpp>
@@ -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(
<details id="d"><summary id="s">Label</summary><p id="p">Content</p></details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
auto* summary = sceneNode->getRoot()->find( "s" )->asType<UIHTMLSummary>();
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
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(
<details id="d">
<summary id="s1">Default</summary>
<summary id="s2" style="list-style-type: disclosure-open;">Open marker</summary>
<summary id="s3" style="list-style-type: decimal;">Decimal marker</summary>
</details>
)html" ) );
sceneNode->updateDirtyLayouts();
const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
ASSERT_TRUE( propDef != nullptr );
auto* s1 = sceneNode->getRoot()->find( "s1" )->asType<UIHTMLSummary>();
auto* s2 = sceneNode->getRoot()->find( "s2" )->asType<UIHTMLSummary>();
auto* s3 = sceneNode->getRoot()->find( "s3" )->asType<UIHTMLSummary>();
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(
<details>
<summary id="default">Default marker</summary>
<summary id="none" style="list-style-type: none;">No marker</summary>
<summary id="explicit" style="list-style-type: none; padding-left: 7px;">
Explicit padding
</summary>
</details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* defaultSummary = sceneNode->getRoot()->find( "default" )->asType<UIHTMLSummary>();
auto* noneSummary = sceneNode->getRoot()->find( "none" )->asType<UIHTMLSummary>();
auto* explicitSummary = sceneNode->getRoot()->find( "explicit" )->asType<UIHTMLSummary>();
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<UIHTMLDetails>();
auto* summary =
sceneNode->getRoot()->querySelector( ".caches summary" )->asType<UIHTMLSummary>();
auto* author = sceneNode->getRoot()->querySelector( ".u-author" )->asType<UIWidget>();
auto* time = sceneNode->getRoot()->querySelector( "time" )->asType<UIWidget>();
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(
<details id="d" open><summary>Label</summary><p id="p">Content</p></details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
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(
<details id="d" open="false"><summary>Label</summary><p id="p">Content</p></details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
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(
<details id="d"><summary id="s">Label</summary><p id="p">Content</p></details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
auto* summary = sceneNode->getRoot()->find( "s" )->asType<UIHTMLSummary>();
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
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(
<details id="d"><summary id="s">Label</summary><p id="p">Content</p></details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
auto* summary = sceneNode->getRoot()->find( "s" )->asType<UIHTMLSummary>();
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(
<details id="d"><p id="p">Content</p></details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
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(
<details id="d">
<summary id="s1">One</summary>
<summary id="s2">Two</summary>
<p id="p">Content</p>
</details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
auto* s1 = sceneNode->getRoot()->find( "s1" )->asType<UIHTMLSummary>();
auto* s2 = sceneNode->getRoot()->find( "s2" )->asType<UIHTMLSummary>();
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(
<details id="outer" open>
<summary id="outer_s">Outer</summary>
<details id="inner">
<summary id="inner_s">Inner</summary>
<p id="inner_p">Inner content</p>
</details>
</details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* outer = sceneNode->getRoot()->find( "outer" )->asType<UIHTMLDetails>();
auto* inner = sceneNode->getRoot()->find( "inner" )->asType<UIHTMLDetails>();
auto* innerSummary = sceneNode->getRoot()->find( "inner_s" )->asType<UIHTMLSummary>();
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(
<details id="d" open><summary>Label</summary><p id="p" hidden>Content</p></details>
)html" ) );
sceneNode->updateDirtyLayouts();
auto* details = sceneNode->getRoot()->find( "d" )->asType<UIHTMLDetails>();
auto* content = sceneNode->getRoot()->find( "p" )->asType<UIWidget>();
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>
<ul>
<li id="li1" style="list-style-type: disclosure-closed;">Closed</li>
<li id="li2" style="list-style-type: disclosure-open;">Open</li>
</ul>
</html>
)html" );
sceneNode->updateDirtyLayouts();
const auto* propDef = StyleSheetSpecification::instance()->getProperty( "list-style-type" );
ASSERT_TRUE( propDef != nullptr );
auto* li1 = sceneNode->getRoot()->find( "li1" )->asType<UIRichText>();
auto* li2 = sceneNode->getRoot()->find( "li2" )->asType<UIRichText>();
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();