Refactor CSS properties to use dense IDs and bitsets:

Introduce contiguous PropertyId and ShorthandId enums backed by name-to-ID maps, replacing hash-valued property identifiers throughout the style system.
Replace PropertyIdSet's unordered set with a fixed-size bitset, eliminating heap allocations during style state changes while providing deterministic ascending iteration.
Keep name hashes for stylesheet storage and unknown or custom properties, while using dense typed IDs for registered-property lookup and dispatch.
Preserve property aliases and the public Checked compatibility identifier.
Add transaction-end reconciliation for interdependent image sizing properties, deferring percentage-based calculations until their containing block is resolved.
Keep formatting-context-owned sizes intact for table cells and inline content.
Add validation and tests for dense built-in registration, custom IDs, aliases, bitset operations, allocation-free storage, unknown properties, property application order, and image sizing regressions.
This commit is contained in:
Martín Lucas Golini
2026-08-05 20:53:13 -03:00
parent ca97999942
commit 74b0454721
35 changed files with 2140 additions and 1007 deletions
@@ -4,6 +4,7 @@
<style>
:root {
--bg: #1c1917;
--text: #fff;
--surface: #292524;
--surface-hover: #302c2a;
--accent: #d97706;
@@ -170,6 +170,7 @@
--wp-admin-theme-color-darker-20: #005a87;
--wp-admin-theme-color-darker-20--rgb: 0, 90, 135;
--wp-admin-border-width-focus: 2px;
--text-color: #fff;
}
:root {
--wp--preset--font-size--normal: 16px;
+3 -1
View File
@@ -12,7 +12,9 @@ class EE_API ElementDefinition : NonCopyable {
public:
ElementDefinition( const StyleSheetStyleVector& styleSheetStyles );
StyleSheetProperty* getProperty( const Uint32& id );
StyleSheetProperty* getProperty( const PropertyId& id );
StyleSheetProperty* getPropertyByNameHash( const String::HashType& id );
const PropertyIdSet& getPropertyIds() const;
+217
View File
@@ -0,0 +1,217 @@
#ifndef EE_UI_CSS_IDNAMEMAP_HPP
#define EE_UI_CSS_IDNAMEMAP_HPP
#include <eepp/core/containers.hpp>
#include <eepp/system/log.hpp>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
namespace EE { namespace UI { namespace CSS {
/**
* @brief Maps dense process-local IDs to canonical names and back.
*
* The map is the full-string reverse lookup for a dense ID space. It owns no
* definitions: it only tracks which ID belongs to which canonical (or alias)
* name. Built-in IDs must be registered with addBuiltin() before finalizeBuiltins()
* seals the built-in range; runtime names then receive IDs via getOrCreateId().
*
* Numeric IDs are unstable internal identifiers. Names are the stable external
* representation. All names passed into the map are expected to already be
* lowercase and trimmed; the map performs no hidden normalization and no
* hash-only equality.
*/
template <typename Id, std::size_t MaxIds> class IdNameMap {
public:
IdNameMap() {
mNames.emplace_back(); // index zero is Invalid with an empty name
mNextCustomId = static_cast<Id>( 0 );
}
/**
* @brief Registers a built-in ID with its canonical name.
*
* Valid only before custom allocation begins. Rejects Invalid, IDs at or
* beyond MaxIds, duplicate names, and assigning a different name to an
* occupied ID.
*
* @return true on success.
*/
bool addBuiltin( Id id, const std::string& canonicalName ) {
if ( mBuiltinsFinalized ) {
EE::System::Log::error(
"IdNameMap: cannot register built-in \"%s\" after finalization.",
canonicalName.c_str() );
return false;
}
const auto underlying = static_cast<std::underlying_type_t<Id>>( id );
const auto maxUnderlying = static_cast<std::underlying_type_t<Id>>( MaxIds );
if ( underlying == 0 ) {
EE::System::Log::error( "IdNameMap: cannot register Invalid built-in \"%s\".",
canonicalName.c_str() );
return false;
}
if ( underlying >= maxUnderlying ) {
EE::System::Log::error( "IdNameMap: built-in ID %d for \"%s\" is out of range.",
underlying, canonicalName.c_str() );
return false;
}
if ( mIdsByName.find( canonicalName ) != mIdsByName.end() ) {
EE::System::Log::error( "IdNameMap: duplicate built-in name \"%s\".",
canonicalName.c_str() );
return false;
}
if ( underlying < mNames.size() && !mNames[underlying].empty() ) {
EE::System::Log::error(
"IdNameMap: built-in ID %d already bound to \"%s\", not \"%s\".", underlying,
mNames[underlying].c_str(), canonicalName.c_str() );
return false;
}
if ( mNames.size() <= underlying )
mNames.resize( underlying + 1 );
mNames[underlying] = canonicalName;
mIdsByName.emplace( canonicalName, id );
return true;
}
/**
* @brief Adds an alias that resolves to an existing ID.
*
* Only the reverse entry is added; no ID is occupied and the canonical name
* is never replaced.
*/
bool addAlias( const std::string& alias, Id target ) {
if ( !contains( target ) ) {
EE::System::Log::error( "IdNameMap: alias \"%s\" targets an unknown ID.",
alias.c_str() );
return false;
}
const auto existing = mIdsByName.find( alias );
if ( existing != mIdsByName.end() ) {
if ( existing->second == target )
return true;
EE::System::Log::error( "IdNameMap: alias \"%s\" is already bound to another ID.",
alias.c_str() );
return false;
}
mIdsByName.emplace( alias, target );
return true;
}
/**
* @brief Seals the built-in range and enables runtime ID allocation.
*
* Verifies every slot in [1, FirstCustomId) is populated. After this call
* addBuiltin() is permanently rejected.
*
* @return true when all built-in slots are populated.
*/
bool finalizeBuiltins() {
const auto firstCustom = static_cast<std::underlying_type_t<Id>>( Id::FirstCustomId );
bool complete = true;
for ( std::size_t i = 1; i < firstCustom; ++i ) {
if ( i >= mNames.size() || mNames[i].empty() ) {
EE::System::Log::error( "IdNameMap: built-in ID %zu was never registered.", i );
complete = false;
}
}
if ( mNames.size() > firstCustom ) {
EE::System::Log::error(
"IdNameMap: built-in slots beyond FirstCustomId are populated." );
complete = false;
}
if ( complete ) {
mBuiltinsFinalized = true;
mNextCustomId = Id::FirstCustomId;
}
return complete;
}
/**
* @brief Returns the ID for a canonical or alias name.
*
* @return Invalid when the name is unknown.
*/
Id getId( std::string_view name ) const {
const auto it = mIdsByName.find( std::string( name ) );
return it != mIdsByName.end() ? it->second : static_cast<Id>( 0 );
}
Id getId( const std::string& name ) const {
const auto it = mIdsByName.find( name );
return it != mIdsByName.end() ? it->second : static_cast<Id>( 0 );
}
Id getId( const char* name ) const { return getId( std::string_view( name ) ); }
/**
* @brief Returns the canonical name for an ID.
*
* @return The empty invalid name for invalid, out-of-range, or unassigned IDs.
*/
const std::string& getName( Id id ) const {
const auto underlying = static_cast<std::underlying_type_t<Id>>( id );
if ( underlying == 0 || underlying >= mNames.size() )
return mNames[0];
return mNames[underlying];
}
/**
* @brief Returns the existing ID for a canonical name, or allocates a new custom ID.
*
* Only valid after finalizeBuiltins(). Reaching the capacity returns Invalid
* and logs a clear error containing the rejected name.
*/
Id getOrCreateId( const std::string& canonicalName ) {
if ( !mBuiltinsFinalized ) {
EE::System::Log::error(
"IdNameMap: getOrCreateId(\"%s\") called before finalizeBuiltins().",
canonicalName.c_str() );
return static_cast<Id>( 0 );
}
const auto existing = getId( canonicalName );
if ( existing != static_cast<Id>( 0 ) )
return existing;
const auto next = static_cast<std::underlying_type_t<Id>>( mNextCustomId );
if ( next >= static_cast<std::underlying_type_t<Id>>( MaxIds ) ) {
EE::System::Log::error( "IdNameMap: ID capacity exhausted (max %zu), rejecting \"%s\".",
MaxIds, canonicalName.c_str() );
return static_cast<Id>( 0 );
}
if ( mNames.size() <= next )
mNames.resize( next + 1 );
mNames[next] = canonicalName;
mIdsByName.emplace( canonicalName, static_cast<Id>( next ) );
mNextCustomId = static_cast<Id>( next + 1 );
return static_cast<Id>( next );
}
bool contains( Id id ) const {
const auto underlying = static_cast<std::underlying_type_t<Id>>( id );
return underlying > 0 && underlying < mNames.size() && !mNames[underlying].empty();
}
bool contains( std::string_view name ) const {
return mIdsByName.find( std::string( name ) ) != mIdsByName.end();
}
bool contains( const std::string& name ) const {
return mIdsByName.find( name ) != mIdsByName.end();
}
bool contains( const char* name ) const { return contains( std::string_view( name ) ); }
std::size_t size() const { return mIdsByName.size(); }
private:
std::vector<std::string> mNames;
UnorderedMap<std::string, Id> mIdsByName;
Id mNextCustomId;
bool mBuiltinsFinalized{ false };
};
}}} // namespace EE::UI::CSS
#endif // EE_UI_CSS_IDNAMEMAP_HPP
+6 -321
View File
@@ -3,328 +3,10 @@
#include <eepp/config.hpp>
#include <eepp/core/string.hpp>
#include <eepp/ui/css/propertyids.hpp>
namespace EE { namespace UI { namespace CSS {
enum class PropertyId : Uint32 {
Id = String::hash( "id" ),
Class = String::hash( "class" ),
X = String::hash( "x" ),
Y = String::hash( "y" ),
Width = String::hash( "width" ),
Height = String::hash( "height" ),
BackgroundColor = String::hash( "background-color" ),
BackgroundImage = String::hash( "background-image" ),
BackgroundTint = String::hash( "background-tint" ),
BackgroundPositionX = String::hash( "background-position-x" ),
BackgroundPositionY = String::hash( "background-position-y" ),
BackgroundRepeat = String::hash( "background-repeat" ),
BackgroundSize = String::hash( "background-size" ),
BackgroundOrigin = String::hash( "background-origin" ),
BackgroundClip = String::hash( "background-clip" ),
BackgroundAttachment = String::hash( "background-attachment" ),
ForegroundColor = String::hash( "foreground-color" ),
ForegroundTint = String::hash( "foreground-tint" ),
ForegroundImage = String::hash( "foreground-image" ),
ForegroundPositionX = String::hash( "foreground-position-x" ),
ForegroundPositionY = String::hash( "foreground-position-y" ),
ForegroundRepeat = String::hash( "foreground-repeat" ),
ForegroundSize = String::hash( "foreground-size" ),
ForegroundTopLeftRadius = String::hash( "foreground-top-left-radius" ),
ForegroundTopRightRadius = String::hash( "foreground-top-right-radius" ),
ForegroundBottomLeftRadius = String::hash( "foreground-bottom-left-radius" ),
ForegroundBottomRightRadius = String::hash( "foreground-bottom-right-radius" ),
Visible = String::hash( "visible" ),
Visibility = String::hash( "visibility" ),
Enabled = String::hash( "enabled" ),
Theme = String::hash( "theme" ),
Skin = String::hash( "skin" ),
SkinColor = String::hash( "skin-color" ),
Gravity = String::hash( "gravity" ),
Flags = String::hash( "flags" ),
MarginLeft = String::hash( "margin-left" ),
MarginRight = String::hash( "margin-right" ),
MarginTop = String::hash( "margin-top" ),
MarginBottom = String::hash( "margin-bottom" ),
Tooltip = String::hash( "tooltip" ),
LayoutWeight = String::hash( "layout-weight" ),
LayoutGravity = String::hash( "layout-gravity" ),
LayoutWidth = String::hash( "layout-width" ),
LayoutHeight = String::hash( "layout-height" ),
LayoutToLeftOf = String::hash( "layout-to-left-of" ),
LayoutToRightOf = String::hash( "layout-to-right-of" ),
LayoutToTopOf = String::hash( "layout-to-top-of" ),
LayoutToBottomOf = String::hash( "layout-to-bottom-of" ),
Clip = String::hash( "clip" ),
Rotation = String::hash( "rotation" ),
Scale = String::hash( "scale" ),
RotationOriginPointX = String::hash( "rotation-origin-point-x" ),
RotationOriginPointY = String::hash( "rotation-origin-point-y" ),
ScaleOriginPointX = String::hash( "scale-origin-point-x" ),
ScaleOriginPointY = String::hash( "scale-origin-point-y" ),
BlendMode = String::hash( "blend-mode" ),
PaddingLeft = String::hash( "padding-left" ),
PaddingRight = String::hash( "padding-right" ),
PaddingTop = String::hash( "padding-top" ),
PaddingBottom = String::hash( "padding-bottom" ),
Opacity = String::hash( "opacity" ),
Cursor = String::hash( "cursor" ),
Text = String::hash( "text" ),
TextTransform = String::hash( "text-transform" ),
Color = String::hash( "color" ),
TextShadowColor = String::hash( "text-shadow-color" ),
TextShadowOffset = String::hash( "text-shadow-offset" ),
SelectionColor = String::hash( "selection-color" ),
SelectionBackColor = String::hash( "selection-back-color" ),
FontFamily = String::hash( "font-family" ),
FontSize = String::hash( "font-size" ),
FontStyle = String::hash( "font-style" ),
FontWeight = String::hash( "font-weight" ),
TextDecoration = String::hash( "text-decoration" ),
Wordwrap = String::hash( "word-wrap" ),
WhiteSpace = String::hash( "white-space" ),
WhiteSpaceCollapse = String::hash( "white-space-collapse" ),
TextStrokeWidth = String::hash( "text-stroke-width" ),
TextStrokeColor = String::hash( "text-stroke-color" ),
TextSelection = String::hash( "text-selection" ),
TextAlign = String::hash( "text-align" ),
Icon = String::hash( "icon" ),
MinIconSize = String::hash( "min-icon-size" ),
MinSize = String::hash( "min-size" ),
MaxSize = String::hash( "max-size" ),
Src = String::hash( "src" ),
ScaleType = String::hash( "scale-type" ),
Tint = String::hash( "tint" ),
MaxTextLength = String::hash( "max-text-length" ),
MinTabWidth = String::hash( "min-tab-width" ),
MaxTabWidth = String::hash( "max-tab-width" ),
TabClosable = String::hash( "tab-closable" ),
TabsEdgesDiffSkin = String::hash( "tabs-edges-diff-skin" ),
TabSeparation = String::hash( "tab-separation" ),
TabHeight = String::hash( "tab-height" ),
TabCloseButtonVisible = String::hash( "tab-close-button-visible" ),
Selected = String::hash( "selected" ),
Checked = String::hash( "checked" ),
PopUpToRoot = String::hash( "popup-to-root" ),
MaxVisibleItems = String::hash( "max-visible-items" ),
SelectedIndex = String::hash( "selected-index" ),
SelectedText = String::hash( "selected-text" ),
ScrollBarStyle = String::hash( "scrollbar-style" ),
RowHeight = String::hash( "row-height" ),
VScrollMode = String::hash( "vscroll-mode" ),
HScrollMode = String::hash( "hscroll-mode" ),
ColumnMargin = String::hash( "column-margin" ),
RowMargin = String::hash( "row-margin" ),
ColumnMode = String::hash( "column-mode" ),
RowMode = String::hash( "row-mode" ),
ColumnWeight = String::hash( "column-weight" ),
ColumnHeight = String::hash( "column-height" ),
ColumnWidth = String::hash( "column-width" ),
RowWeight = String::hash( "row-weight" ),
ReverseDraw = String::hash( "reverse-draw" ),
Orientation = String::hash( "orientation" ),
Indeterminate = String::hash( "indeterminate" ),
MaxProgress = String::hash( "max-progress" ),
Progress = String::hash( "progress" ),
FillColor = String::hash( "fill-color" ),
Radius = String::hash( "radius" ),
OutlineThickness = String::hash( "outline-thickness" ),
AnimationSpeed = String::hash( "animation-speed" ),
ArcStartAngle = String::hash( "arc-start-angle" ),
MinWidth = String::hash( "min-width" ),
MinHeight = String::hash( "min-height" ),
MaxWidth = String::hash( "max-width" ),
MaxHeight = String::hash( "max-height" ),
TotalSteps = String::hash( "total-steps" ),
VerticalExpand = String::hash( "vertical-expand" ),
DisplayPercent = String::hash( "display-percent" ),
MovementSpeed = String::hash( "movement-speed" ),
MinValue = String::hash( "min-value" ),
MaxValue = String::hash( "max-value" ),
Value = String::hash( "value" ),
ClickStep = String::hash( "click-step" ),
PageStep = String::hash( "page-step" ),
BackgroundExpand = String::hash( "background-expand" ),
ScrollBarMode = String::hash( "scrollbar-mode" ),
HalfSlider = String::hash( "half-slider" ),
Owns = String::hash( "owns" ),
AllowEditing = String::hash( "allow-editing" ),
Locked = String::hash( "locked" ),
MaxLength = String::hash( "max-length" ),
Numeric = String::hash( "numeric" ),
AllowFloat = String::hash( "allow-float" ),
TouchDrag = String::hash( "touch-drag" ),
TouchDragDeceleration = String::hash( "touch-drag-deceleration" ),
WindowTitle = String::hash( "window-title" ),
WindowOpacity = String::hash( "window-opacity" ),
WindowButtonsOffset = String::hash( "window-buttons-offset" ),
WindowFlags = String::hash( "window-flags" ),
WindowTitlebarSize = String::hash( "window-titlebar-size" ),
WindowBorderSize = String::hash( "window-border-size" ),
WindowMinSize = String::hash( "window-min-size" ),
WindowBorderAutoSize = String::hash( "window-border-auto-size" ),
WindowButtonsSeparation = String::hash( "window-buttons-separation" ),
WindowCornerDistance = String::hash( "window-corner-distance" ),
WindowTitlebarAutoSize = String::hash( "window-decoration-auto-size" ),
WindowShadowColor = String::hash( "window-shadow-color" ),
WindowShadowOffset = String::hash( "window-shadow-offset" ),
WindowShadowSize = String::hash( "window-shadow-size" ),
Hint = String::hash( "hint" ),
HintColor = String::hash( "hint-color" ),
HintShadowColor = String::hash( "hint-shadow-color" ),
HintShadowOffset = String::hash( "hint-shadow-offset" ),
HintFontFamily = String::hash( "hint-font-family" ),
HintFontSize = String::hash( "hint-font-size" ),
HintFontStyle = String::hash( "hint-font-style" ),
HintStrokeWidth = String::hash( "hint-stroke-width" ),
HintStrokeColor = String::hash( "hint-stroke-color" ),
HintDisplay = String::hash( "hint-display" ),
Transition = String::hash( "transition" ),
TransitionDelay = String::hash( "transition-delay" ),
TransitionDuration = String::hash( "transition-duration" ),
TransitionProperty = String::hash( "transition-property" ),
TransitionTimingFunction = String::hash( "transition-timing-function" ),
Animation = String::hash( "animation" ),
AnimationName = String::hash( "animation-name" ),
AnimationDelay = String::hash( "animation-delay" ),
AnimationDuration = String::hash( "animation-duration" ),
AnimationFillMode = String::hash( "animation-fill-mode" ),
AnimationIterationCount = String::hash( "animation-iteration-count" ),
AnimationPlayState = String::hash( "animation-play-state" ),
AnimationTimingFunction = String::hash( "animation-timing-function" ),
DragResistance = String::hash( "drag-resistance" ),
ChangePagePercent = String::hash( "change-page-percent" ),
MaxEdgeResistance = String::hash( "max-edge-resistance" ),
PageTransitionDuration = String::hash( "page-transition-duration" ),
TimingFunction = String::hash( "timing-function" ),
PageLocked = String::hash( "page-locked" ),
BorderType = String::hash( "border-type" ),
BorderLeftColor = String::hash( "border-left-color" ),
BorderRightColor = String::hash( "border-right-color" ),
BorderTopColor = String::hash( "border-top-color" ),
BorderBottomColor = String::hash( "border-bottom-color" ),
BorderLeftWidth = String::hash( "border-left-width" ),
BorderRightWidth = String::hash( "border-right-width" ),
BorderTopWidth = String::hash( "border-top-width" ),
BorderBottomWidth = String::hash( "border-bottom-width" ),
BorderLeftStyle = String::hash( "border-left-style" ),
BorderRightStyle = String::hash( "border-right-style" ),
BorderTopStyle = String::hash( "border-top-style" ),
BorderBottomStyle = String::hash( "border-bottom-style" ),
BorderTopLeftRadius = String::hash( "border-top-left-radius" ),
BorderTopRightRadius = String::hash( "border-top-right-radius" ),
BorderBottomLeftRadius = String::hash( "border-bottom-left-radius" ),
BorderBottomRightRadius = String::hash( "border-bottom-right-radius" ),
BorderSmooth = String::hash( "border-smooth" ),
BoxSizing = String::hash( "box-sizing" ),
BackgroundSmooth = String::hash( "background-smooth" ),
ForegroundSmooth = String::hash( "foreground-smooth" ),
TabBarHideOnSingleTab = String::hash( "tabbar-hide-on-single-tab" ),
TabBarAllowRearrange = String::hash( "tabbar-allow-rearrange" ),
TabBarAllowDragAndDrop = String::hash( "tabbar-allow-drag-and-drop-tabs" ),
TabAllowSwitchTabsInEmptySpaces = String::hash( "tabbar-allow-switch-tabs-in-empty-spaces" ),
SplitterPartition = String::hash( "splitter-partition" ),
SplitterAlwaysShow = String::hash( "splitter-always-show" ),
DroppableHoveringColor = String::hash( "droppable-hovering-color" ),
TextAsFallback = String::hash( "text-as-fallback" ),
SelectOnClick = String::hash( "select-on-click" ),
LineSpacing = String::hash( "line-spacing" ),
LineHeight = String::hash( "line-height" ),
TextIndent = String::hash( "text-indent" ),
TabSize = String::hash( "tab-size" ),
GravityOwner = String::hash( "gravity-owner" ),
Href = String::hash( "href" ),
Focusable = String::hash( "focusable" ),
InnerWidgetOrientation = String::hash( "inner-widget-orientation" ),
Glyph = String::hash( "glyph" ),
Name = String::hash( "name" ),
RowValign = String::hash( "row-valign" ),
TextOverflow = String::hash( "text-overflow" ),
CheckMode = String::hash( "check-mode" ),
EnableCodeEditorFlags = String::hash( "enable-editor-flags" ),
DisableCodeEditorFlags = String::hash( "disable-editor-flags" ),
LineWrapMode = String::hash( "line-wrap-mode" ),
LineWrapType = String::hash( "line-wrap-type" ),
DisplayOptions = String::hash( "display-options" ),
MenuWidthMode = String::hash( "menu-width-mode" ),
ExpandText = String::hash( "expand-text" ),
ColSpan = String::hash( "colspan" ),
TableLayout = String::hash( "table-layout" ),
CellPadding = String::hash( "cellpadding" ),
CellSpacing = String::hash( "cellspacing" ),
Size = String::hash( "size" ),
Type = String::hash( "type" ),
Rows = String::hash( "rows" ),
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" ),
Right = String::hash( "right" ),
Bottom = String::hash( "bottom" ),
Left = String::hash( "left" ),
ZIndex = String::hash( "z-index" ),
ListStyleType = String::hash( "list-style-type" ),
ListStylePosition = String::hash( "list-style-position" ),
ListStyleImage = String::hash( "list-style-image" ),
Float = String::hash( "float" ),
Clear = String::hash( "clear" ),
Action = String::hash( "action" ),
Method = String::hash( "method" ),
Enctype = String::hash( "enctype" ),
Overflow = String::hash( "overflow" ),
Target = String::hash( "target" ),
For = String::hash( "for" ),
UnicodeRange = String::hash( "unicode-range" ),
AlignmentBaseline = String::hash( "alignment-baseline" ),
FlexDirection = String::hash( "flex-direction" ),
FlexWrap = String::hash( "flex-wrap" ),
JustifyContent = String::hash( "justify-content" ),
AlignItems = String::hash( "align-items" ),
AlignContent = String::hash( "align-content" ),
AlignSelf = String::hash( "align-self" ),
FlexGrow = String::hash( "flex-grow" ),
FlexShrink = String::hash( "flex-shrink" ),
FlexBasis = String::hash( "flex-basis" ),
Flex = String::hash( "flex" ),
FlexFlow = String::hash( "flex-flow" ),
Order = String::hash( "order" ),
RowGap = String::hash( "row-gap" ),
ColumnGap = String::hash( "column-gap" ),
Gap = String::hash( "gap" ),
Editable = String::hash( "editable" ),
SelectionType = String::hash( "selection-type" ),
SelectionKind = String::hash( "selection-kind" ),
IconSize = String::hash( "icon-size" ),
SortIconSize = String::hash( "sort-icon-size" ),
MainColumn = String::hash( "main-column" ),
RowHeaderWidth = String::hash( "row-header-width" ),
TableFlags = String::hash( "table-flags" ),
IndentWidth = String::hash( "indent-width" ),
ExpanderIconSize = String::hash( "expander-icon-size" ),
TableModel = String::hash( "table-model" ),
GridTemplateRows = String::hash( "grid-template-rows" ),
GridTemplateColumns = String::hash( "grid-template-columns" ),
GridTemplateAreas = String::hash( "grid-template-areas" ),
GridTemplate = String::hash( "grid-template" ),
GridAutoRows = String::hash( "grid-auto-rows" ),
GridAutoColumns = String::hash( "grid-auto-columns" ),
GridAutoFlow = String::hash( "grid-auto-flow" ),
Grid = String::hash( "grid" ),
GridRowStart = String::hash( "grid-row-start" ),
GridRowEnd = String::hash( "grid-row-end" ),
GridColumnStart = String::hash( "grid-column-start" ),
GridColumnEnd = String::hash( "grid-column-end" ),
GridRow = String::hash( "grid-row" ),
GridColumn = String::hash( "grid-column" ),
GridArea = String::hash( "grid-area" ),
JustifyItems = String::hash( "justify-items" ),
JustifySelf = String::hash( "justify-self" ),
Defer = String::hash( "defer" ),
};
enum class PropertyType : Uint32 {
Undefined,
String,
@@ -362,13 +44,15 @@ enum class PropertyRelativeTarget : Uint32 {
class EE_API PropertyDefinition {
public:
PropertyDefinition( const std::string& name, const std::string& defaultValue,
const bool& inherited = false );
PropertyDefinition( PropertyId propertyId, const std::string& name,
const std::string& defaultValue, const bool& inherited = false );
const std::string& getName() const;
/** @brief Returns the canonical property-name hash, not the dense PropertyId. */
const String::HashType& getId() const;
/** @brief Returns the dense identity used for dispatch and PropertyIdSet membership. */
PropertyId getPropertyId() const;
const std::string& getDefaultValue() const;
@@ -400,6 +84,7 @@ class EE_API PropertyDefinition {
protected:
std::string mName;
String::HashType mId;
PropertyId mPropertyId;
std::vector<std::string> mAliases;
std::vector<Uint32> mAliasesHash;
std::string mDefaultValue;
+392
View File
@@ -0,0 +1,392 @@
#ifndef EE_UI_CSS_PROPERTYIDS_HPP
#define EE_UI_CSS_PROPERTYIDS_HPP
#include <eepp/config.hpp>
namespace EE { namespace UI { namespace CSS {
/**
* Dense process-local CSS property identifiers.
*
* Numeric values are unstable internal implementation identifiers assigned at
* compile time. They are never persisted, serialized, sent over IPC, or exposed as
* stable values: names are the stable external representation. Every built-in is a
* contiguous enum below NumDefinedIds; runtime-registered properties continue from
* FirstCustomId. MaxNumIds is an exclusive capacity bound.
*/
enum class PropertyId : Uint16 {
Invalid = 0,
Id,
Class,
X,
Y,
Width,
Height,
BackgroundColor,
BackgroundImage,
BackgroundTint,
BackgroundPositionX,
BackgroundPositionY,
BackgroundRepeat,
BackgroundSize,
BackgroundOrigin,
BackgroundClip,
BackgroundAttachment,
ForegroundColor,
ForegroundTint,
ForegroundImage,
ForegroundPositionX,
ForegroundPositionY,
ForegroundRepeat,
ForegroundSize,
ForegroundTopLeftRadius,
ForegroundTopRightRadius,
ForegroundBottomLeftRadius,
ForegroundBottomRightRadius,
Visible,
Visibility,
Enabled,
Theme,
Skin,
SkinColor,
Gravity,
Flags,
MarginLeft,
MarginRight,
MarginTop,
MarginBottom,
Tooltip,
LayoutWeight,
LayoutGravity,
LayoutWidth,
LayoutHeight,
LayoutToLeftOf,
LayoutToRightOf,
LayoutToTopOf,
LayoutToBottomOf,
Clip,
Rotation,
Scale,
RotationOriginPointX,
RotationOriginPointY,
ScaleOriginPointX,
ScaleOriginPointY,
BlendMode,
PaddingLeft,
PaddingRight,
PaddingTop,
PaddingBottom,
Opacity,
Cursor,
Text,
TextTransform,
Color,
TextShadowColor,
TextShadowOffset,
SelectionColor,
SelectionBackColor,
FontFamily,
FontSize,
FontStyle,
FontWeight,
TextDecoration,
Wordwrap,
WhiteSpace,
WhiteSpaceCollapse,
TextStrokeWidth,
TextStrokeColor,
TextSelection,
TextAlign,
Icon,
MinIconSize,
Src,
ScaleType,
Tint,
MaxTextLength,
MinTabWidth,
MaxTabWidth,
TabClosable,
TabsEdgesDiffSkin,
TabSeparation,
TabHeight,
TabCloseButtonVisible,
Selected,
/** Compatibility spelling: "checked" is an alias of the canonical "selected" property. */
Checked = Selected,
PopUpToRoot,
MaxVisibleItems,
SelectedIndex,
SelectedText,
ScrollBarStyle,
RowHeight,
VScrollMode,
HScrollMode,
ColumnMargin,
RowMargin,
ColumnMode,
RowMode,
ColumnWeight,
ColumnWidth,
RowWeight,
ReverseDraw,
Orientation,
Indeterminate,
MaxProgress,
Progress,
FillColor,
Radius,
OutlineThickness,
AnimationSpeed,
ArcStartAngle,
MinWidth,
MinHeight,
MaxWidth,
MaxHeight,
TotalSteps,
VerticalExpand,
DisplayPercent,
MovementSpeed,
MinValue,
MaxValue,
Value,
ClickStep,
PageStep,
BackgroundExpand,
ScrollBarMode,
HalfSlider,
Owns,
AllowEditing,
Locked,
MaxLength,
Numeric,
AllowFloat,
TouchDrag,
TouchDragDeceleration,
WindowTitle,
WindowOpacity,
WindowButtonsOffset,
WindowFlags,
WindowTitlebarSize,
WindowBorderSize,
WindowMinSize,
WindowBorderAutoSize,
WindowButtonsSeparation,
WindowCornerDistance,
WindowTitlebarAutoSize,
WindowShadowColor,
WindowShadowOffset,
WindowShadowSize,
Hint,
HintColor,
HintShadowColor,
HintShadowOffset,
HintFontFamily,
HintFontSize,
HintFontStyle,
HintStrokeWidth,
HintStrokeColor,
HintDisplay,
Transition,
TransitionDelay,
TransitionDuration,
TransitionProperty,
TransitionTimingFunction,
Animation,
AnimationName,
AnimationDelay,
AnimationDirection,
AnimationDuration,
AnimationFillMode,
AnimationIterationCount,
AnimationPlayState,
AnimationTimingFunction,
DragResistance,
ChangePagePercent,
MaxEdgeResistance,
TimingFunction,
PageLocked,
BorderType,
BorderLeftColor,
BorderRightColor,
BorderTopColor,
BorderBottomColor,
BorderLeftWidth,
BorderRightWidth,
BorderTopWidth,
BorderBottomWidth,
BorderLeftStyle,
BorderRightStyle,
BorderTopStyle,
BorderBottomStyle,
BorderTopLeftRadius,
BorderTopRightRadius,
BorderBottomLeftRadius,
BorderBottomRightRadius,
BorderSmooth,
BoxSizing,
BackgroundSmooth,
ForegroundSmooth,
TabBarHideOnSingleTab,
TabBarAllowRearrange,
TabBarAllowDragAndDrop,
TabAllowSwitchTabsInEmptySpaces,
SplitterPartition,
SplitterAlwaysShow,
DroppableHoveringColor,
TextAsFallback,
SelectOnClick,
LineSpacing,
LineHeight,
TextIndent,
TabSize,
GravityOwner,
Href,
Focusable,
InnerWidgetOrientation,
Glyph,
Name,
RowValign,
TextOverflow,
CheckMode,
EnableCodeEditorFlags,
DisableCodeEditorFlags,
LineWrapMode,
LineWrapType,
DisplayOptions,
MenuWidthMode,
ExpandText,
ColSpan,
TableLayout,
CellPadding,
CellSpacing,
Size,
Type,
Rows,
Cols,
InputMode,
Hidden,
Open,
Display,
Position,
Top,
Right,
Bottom,
Left,
ZIndex,
ListStyleType,
ListStylePosition,
ListStyleImage,
Float,
Clear,
Action,
Method,
Enctype,
Overflow,
Target,
For,
UnicodeRange,
AlignmentBaseline,
FlexDirection,
FlexWrap,
JustifyContent,
AlignItems,
AlignContent,
AlignSelf,
FlexGrow,
FlexShrink,
FlexBasis,
Order,
RowGap,
ColumnGap,
Editable,
SelectionType,
SelectionKind,
IconSize,
SortIconSize,
MainColumn,
RowHeaderWidth,
TableFlags,
IndentWidth,
ExpanderIconSize,
TableModel,
GridTemplateRows,
GridTemplateColumns,
GridTemplateAreas,
GridAutoRows,
GridAutoColumns,
GridAutoFlow,
GridRowStart,
GridRowEnd,
GridColumnStart,
GridColumnEnd,
GridRow,
GridColumn,
GridArea,
JustifyItems,
JustifySelf,
Defer,
NumDefinedIds,
FirstCustomId = NumDefinedIds,
MaxNumIds = 512,
};
/**
* Dense process-local CSS shorthand identifiers.
*
* Same identity rules as PropertyId, in a separate ID space: a shorthand is never
* cast to a PropertyId even when its spelling matches a longhand.
*/
enum class ShorthandId : Uint8 {
Invalid = 0,
Margin,
LayoutMargin,
LayoutMarginUnderscore,
Padding,
Background,
Foreground,
BoxMargin,
BackgroundPosition,
ForegroundPosition,
BorderColor,
BorderWidth,
BorderStyle,
BorderRadius,
ForegroundRadius,
RotationOriginPoint,
RotateOriginPoint,
ScaleOriginPoint,
MinSize,
MaxSize,
Border,
TextShadow,
HintShadow,
BorderLeft,
BorderRight,
BorderTop,
BorderBottom,
ListStyle,
Font,
VerticalAlign,
FlexFlow,
Flex,
Gap,
GridTemplate,
Grid,
PlaceItems,
PlaceSelf,
PlaceContent,
NumDefinedIds,
FirstCustomId = NumDefinedIds,
MaxNumIds = 255,
};
static_assert( static_cast<Uint16>( PropertyId::NumDefinedIds ) <
static_cast<Uint16>( PropertyId::MaxNumIds ),
"PropertyId built-in count must stay below MaxNumIds" );
static_assert( static_cast<Uint8>( ShorthandId::NumDefinedIds ) <
static_cast<Uint8>( ShorthandId::MaxNumIds ),
"ShorthandId built-in count must stay below MaxNumIds" );
}}} // namespace EE::UI::CSS
#endif // EE_UI_CSS_PROPERTYIDS_HPP
+85 -41
View File
@@ -1,37 +1,79 @@
#ifndef EE_UI_CSS_PROPERTYIDSET_HPP
#define EE_UI_CSS_PROPERTYIDSET_HPP
#include <eepp/core/containers.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <bitset>
#include <eepp/core/debug.hpp>
#include <eepp/ui/css/propertyids.hpp>
namespace EE { namespace UI { namespace CSS {
class PropertyIdSetIterator;
namespace detail {
inline std::size_t bitsetFindFirst( const std::bitset<512>& bits ) {
for ( std::size_t i = 0; i < bits.size(); ++i )
if ( bits.test( i ) )
return i;
return bits.size();
}
inline std::size_t bitsetFindNext( const std::bitset<512>& bits, std::size_t from ) {
for ( std::size_t i = from + 1; i < bits.size(); ++i )
if ( bits.test( i ) )
return i;
return bits.size();
}
} // namespace detail
/**
* @brief Fixed-capacity set of dense PropertyId values.
*
* Backed by a 512-bit std::bitset, so construction, clearing, copying, union,
* intersection, and iteration perform zero heap allocations. Only successfully
* registered property definitions occupy bits; unknown names never enter this
* set. Iteration yields present IDs in ascending dense ID order.
*/
class EE_API PropertyIdSet {
private:
UnorderedSet<Uint32> mIds;
std::bitset<512> mBits;
public:
void insert( Uint32 id ) { mIds.insert( id ); }
void insert( PropertyId id ) {
const auto underlying = static_cast<std::size_t>( id );
if ( underlying == 0 )
return; // Invalid is a no-op
if ( underlying >= mBits.size() ) {
eeASSERTM( false, "PropertyId out of PropertyIdSet capacity" );
return;
}
mBits.set( underlying );
}
void clear() { mIds.clear(); }
void clear() { mBits.reset(); }
void erase( Uint32 id ) { mIds.erase( id ); }
void erase( PropertyId id ) {
const auto underlying = static_cast<std::size_t>( id );
if ( underlying == 0 )
return; // Invalid is a no-op
if ( underlying >= mBits.size() ) {
eeASSERTM( false, "PropertyId out of PropertyIdSet capacity" );
return;
}
mBits.reset( underlying );
}
bool empty() const { return mIds.empty(); }
bool empty() const { return mBits.none(); }
bool contains( Uint32 id ) const { return mIds.count( id ) == 1; }
bool contains( PropertyId id ) const {
const auto underlying = static_cast<std::size_t>( id );
return underlying != 0 && underlying < mBits.size() && mBits.test( underlying );
}
size_t size() const { return mIds.size(); }
bool operator==( const PropertyIdSet& other ) const { return mIds == other.mIds; }
bool operator!=( const PropertyIdSet& other ) const { return mIds != other.mIds; }
size_t size() const { return mBits.count(); }
// Union with another set
PropertyIdSet& operator|=( const PropertyIdSet& other ) {
mIds.insert( other.mIds.begin(), other.mIds.end() );
mBits |= other.mBits;
return *this;
}
@@ -43,79 +85,81 @@ class EE_API PropertyIdSet {
// Intersection with another set
PropertyIdSet& operator&=( const PropertyIdSet& other ) {
if ( !mIds.empty() && !other.mIds.empty() ) {
for ( auto it = mIds.begin(); it != mIds.end(); )
if ( other.mIds.count( *it ) == 0 )
it = mIds.erase( it );
else
++it;
} else {
mIds.clear();
}
mBits &= other.mBits;
return *this;
}
PropertyIdSet operator&( const PropertyIdSet& other ) const {
PropertyIdSet result;
if ( !mIds.empty() && !other.mIds.empty() ) {
for ( Uint32 id : mIds )
if ( other.mIds.count( id ) == 1 )
result.mIds.insert( id );
}
PropertyIdSet result = *this;
result &= other;
return result;
}
bool operator==( const PropertyIdSet& other ) const { return mBits == other.mBits; }
bool operator!=( const PropertyIdSet& other ) const { return mBits != other.mBits; }
// Iterator support. Iterates through all the PropertyIds that are set (contained).
// @note: Modifying the container invalidates the iterators. Only const_iterators are provided.
inline PropertyIdSetIterator begin() const;
inline PropertyIdSetIterator end() const;
// Erases the property id represented by a valid iterator. Invalidates any previous iterators.
// @return A new valid iterator pointing to the next element or end().
inline PropertyIdSetIterator erase( const PropertyIdSetIterator& it );
friend class PropertyIdSetIterator;
};
class EE_API PropertyIdSetIterator {
public:
using CustomIdsIt = UnorderedSet<Uint32>::const_iterator;
PropertyIdSetIterator() : container( nullptr ), bitIndex( 0 ) {}
PropertyIdSetIterator() : container( nullptr ), custom_ids_iterator() {}
PropertyIdSetIterator( const PropertyIdSet* container, CustomIdsIt custom_ids_iterator ) :
container( container ), custom_ids_iterator( custom_ids_iterator ) {}
explicit PropertyIdSetIterator( const PropertyIdSet* container, std::size_t bitIndex ) :
container( container ), bitIndex( bitIndex ) {}
PropertyIdSetIterator& operator++() {
++custom_ids_iterator;
if ( container ) {
std::size_t next = detail::bitsetFindNext( container->mBits, bitIndex );
bitIndex = ( next == container->mBits.size() ) ? container->mBits.size() : next;
}
return *this;
}
bool operator==( const PropertyIdSetIterator& other ) const {
return container == other.container && custom_ids_iterator == other.custom_ids_iterator;
return container == other.container && bitIndex == other.bitIndex;
}
bool operator!=( const PropertyIdSetIterator& other ) const { return !( *this == other ); }
Uint32 operator*() const { return *custom_ids_iterator; }
PropertyId operator*() const { return static_cast<PropertyId>( bitIndex ); }
private:
const PropertyIdSet* container;
CustomIdsIt custom_ids_iterator;
std::size_t bitIndex;
friend PropertyIdSetIterator PropertyIdSet::erase( const PropertyIdSetIterator& );
};
PropertyIdSetIterator PropertyIdSet::begin() const {
return PropertyIdSetIterator( this, mIds.begin() );
const std::size_t first = detail::bitsetFindFirst( mBits );
const std::size_t index = ( first == mBits.size() ) ? mBits.size() : first;
return PropertyIdSetIterator( this, index );
}
PropertyIdSetIterator PropertyIdSet::end() const {
return PropertyIdSetIterator( this, mIds.end() );
return PropertyIdSetIterator( this, mBits.size() );
}
PropertyIdSetIterator PropertyIdSet::erase( const PropertyIdSetIterator& it_in ) {
PropertyIdSetIterator it = it_in;
it.custom_ids_iterator = mIds.erase( it.custom_ids_iterator );
if ( it.container == this && it.bitIndex < mBits.size() ) {
mBits.reset( it.bitIndex );
++it;
}
return it;
}
static_assert( sizeof( PropertyIdSet ) == 64, "PropertyIdSet must be a fixed 64-byte bitset" );
}}} // namespace EE::UI::CSS
#endif // EE_UI_CSS_PROPERTYIDSET_HPP
+30 -10
View File
@@ -2,8 +2,12 @@
#define EE_UI_CSS_PROPERTYSPECIFICATION_HPP
#include <eepp/system/singleton.hpp>
#include <eepp/ui/css/idnamemap.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/css/propertyidset.hpp>
#include <eepp/ui/css/shorthanddefinition.hpp>
#include <memory>
#include <vector>
namespace EE { namespace UI { namespace CSS {
@@ -12,37 +16,53 @@ class EE_API PropertySpecification {
public:
~PropertySpecification();
PropertyDefinition& registerProperty( const std::string& propertyVame,
PropertyDefinition& registerProperty( PropertyId id, const std::string& propertyName,
const std::string& defaultValue, bool inherited );
PropertyDefinition* registerProperty( const std::string& propertyName,
const std::string& defaultValue, bool inherited );
const PropertyDefinition* getProperty( const PropertyId& id ) const;
const PropertyDefinition* getProperty( const Uint32& id ) const;
const PropertyDefinition* getProperty( const char* name ) const;
const PropertyDefinition* getProperty( std::string_view name ) const;
const PropertyDefinition* getProperty( const std::string& name ) const;
ShorthandDefinition& registerShorthand( const std::string& name,
ShorthandDefinition& registerShorthand( ShorthandId id, const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandParserName );
const ShorthandDefinition* getShorthand( const String::HashType& id ) const;
ShorthandDefinition* registerShorthand( const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandParserName );
const ShorthandDefinition* getShorthand( const ShorthandId& id ) const;
const ShorthandDefinition* getShorthand( const std::string& name ) const;
bool isShorthand( const std::string& name ) const;
bool isShorthand( const Uint32& id ) const;
const PropertyIdSet& getInheritableProperties() const;
const SmallVector<PropertyId>& getInheritableProperties() const;
void finalizeBuiltins();
protected:
friend class PropertyDefinition;
friend class ShorthandDefinition;
std::unordered_map<Uint32, std::shared_ptr<PropertyDefinition>> mProperties;
std::unordered_map<Uint32, std::shared_ptr<ShorthandDefinition>> mShorthands;
SmallVector<PropertyId> mInheritableProperties;
IdNameMap<PropertyId, 512> mPropertyIds;
IdNameMap<ShorthandId, 255> mShorthandIds;
std::vector<std::shared_ptr<PropertyDefinition>> mPropertiesById;
std::vector<std::shared_ptr<ShorthandDefinition>> mShorthandsById;
PropertyIdSet mInheritableProperties;
const PropertyDefinition* addPropertyAlias( Uint32 aliasId, const PropertyDefinition* propDef );
const PropertyDefinition* addPropertyAlias( const std::string& alias,
const PropertyDefinition* propDef );
const ShorthandDefinition* addShorthandAlias( const std::string& alias,
const ShorthandDefinition* shorthandDef );
};
}}} // namespace EE::UI::CSS
+7 -34
View File
@@ -3,60 +3,31 @@
#include <eepp/config.hpp>
#include <eepp/core/string.hpp>
#include <eepp/ui/css/propertyids.hpp>
#include <eepp/ui/css/stylesheetproperty.hpp>
#include <functional>
namespace EE { namespace UI { namespace CSS {
enum class ShorthandId : Uint32 {
Margin = String::hash( "margin" ),
Padding = String::hash( "padding" ),
Transition = String::hash( "transition" ),
Background = String::hash( "background" ),
Foreground = String::hash( "foreground" ),
BackgroundPosition = String::hash( "background-position" ),
ForegroundPosition = String::hash( "foreground-position" ),
LayoutMargin = String::hash( "layout-margin" ),
LayoutMarginUnderscore = String::hash( "layout_margin" ),
RotationOriginPoint = String::hash( "rotation-origin-point" ),
ScaleOriginPoint = String::hash( "scale-origin-point" ),
BorderColor = String::hash( "border-color" ),
BorderWidth = String::hash( "border-width" ),
BorderRadius = String::hash( "border-radius" ),
MinSize = String::hash( "min-size" ),
MaxSize = String::hash( "max-size" ),
ListStye = String::hash( "list-style" ),
Font = String::hash( "font" ),
VerticalAlign = String::hash( "vertical-align" ),
BoxMargin = String::hash( "box-margin" ),
ForegroundRadius = String::hash( "foreground-radius" ),
RotateOriginPoint = String::hash( "rotate-origin-point" ),
Border = String::hash( "border" ),
TextShadow = String::hash( "text-shadow" ),
HintShadow = String::hash( "hint-shadow" ),
BorderLeft = String::hash( "border-left" ),
BorderRight = String::hash( "border-right" ),
BorderTop = String::hash( "border-top" ),
BorderBottom = String::hash( "border-bottom" ),
};
typedef std::function<std::vector<StyleSheetProperty>( const ShorthandDefinition* shorthand,
std::string value )>
ShorthandParserFunc;
class EE_API ShorthandDefinition {
public:
static ShorthandDefinition* New( const std::string& name,
static ShorthandDefinition* New( ShorthandId shorthandId, const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandParserName );
ShorthandDefinition( const std::string& name, const std::vector<std::string>& properties,
ShorthandDefinition( ShorthandId shorthandId, const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandFuncName );
std::vector<StyleSheetProperty> parse( std::string value ) const;
const std::string& getName() const;
/** @brief Returns the canonical shorthand-name hash, not the dense ShorthandId. */
const String::HashType& getId() const;
ShorthandDefinition& addAlias( const std::string& alias );
@@ -69,6 +40,7 @@ class EE_API ShorthandDefinition {
bool isDefinition( const String::HashType& id ) const;
/** @brief Returns the dense shorthand identity used for lookup and dispatch. */
ShorthandId getShorthandId() const;
const std::vector<std::string>& getProperties() const;
@@ -77,6 +49,7 @@ class EE_API ShorthandDefinition {
std::string mName;
std::string mFuncName;
String::HashType mId;
ShorthandId mShorthandId;
std::vector<std::string> mAliases;
std::vector<String::HashType> mAliasesHash;
std::vector<std::string> mProperties;
+20 -6
View File
@@ -9,6 +9,7 @@
#include <eepp/math/rect.hpp>
#include <eepp/system/color.hpp>
#include <eepp/system/time.hpp>
#include <eepp/ui/css/propertyids.hpp>
#include <eepp/ui/css/stylesheetlength.hpp>
#include <string>
@@ -46,10 +47,23 @@ class EE_API StyleSheetProperty {
const Int64& specificity, bool isVolatile = false,
const Uint32& index = 0 );
/**
* @brief Returns the stable name hash used as the key in StyleSheetProperties maps.
*
* This is not a dense PropertyId or ShorthandId. Unknown and data-* properties
* also return their own name hash here.
*/
Uint32 getId() const;
/** @brief Returns the dense longhand identity, or PropertyId::Invalid. */
PropertyId getPropertyId() const;
/** @brief Returns the dense shorthand identity, or ShorthandId::Invalid. */
ShorthandId getShorthandId() const;
const std::string& getName() const;
/** @brief Returns the stable hash of getName(); this is not a dense ID. */
const String::HashType& getNameHash() const;
const std::string& getValue() const;
@@ -192,18 +206,18 @@ class EE_API StyleSheetProperty {
protected:
std::string mName;
String::HashType mNameHash;
String::HashType mNameHash{ 0 };
std::string mValue;
String::HashType mValueHash;
Int64 mSpecificity;
Uint32 mIndex;
String::HashType mValueHash{ 0 };
Int64 mSpecificity{ 0 };
Uint32 mIndex{ 0 };
bool mVolatile : 1 { false };
bool mImportant : 1 { false };
bool mIsVarValue : 1 { false };
bool mIsLightDarkValue : 1 { false };
bool mCachedProperty : 1 { false };
const PropertyDefinition* mPropertyDefinition;
const ShorthandDefinition* mShorthandDefinition;
const PropertyDefinition* mPropertyDefinition{ nullptr };
const ShorthandDefinition* mShorthandDefinition{ nullptr };
std::vector<StyleSheetProperty> mIndexedProperty;
std::vector<VariableFunctionCache> mVarCache;
@@ -6,6 +6,7 @@
#include <eepp/system/singleton.hpp>
#include <eepp/ui/css/drawableimageparser.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/css/propertyidset.hpp>
#include <eepp/ui/css/shorthanddefinition.hpp>
#include <functional>
@@ -37,27 +38,34 @@ class EE_API StyleSheetSpecification {
~StyleSheetSpecification();
PropertyDefinition& registerProperty( const std::string& propertyVame,
PropertyDefinition& registerProperty( PropertyId id, const std::string& propertyName,
const std::string& defaultValue, bool inherited = false );
PropertyDefinition* registerProperty( const std::string& propertyName,
const std::string& defaultValue, bool inherited = false );
const PropertyDefinition* getProperty( const PropertyId& id ) const;
const PropertyDefinition* getProperty( const Uint32& id ) const;
const PropertyDefinition* getProperty( const char* name ) const;
const PropertyDefinition* getProperty( std::string_view name ) const;
const PropertyDefinition* getProperty( const std::string& name ) const;
const SmallVector<CSS::PropertyId>& getInheritableProperties() const;
const PropertyIdSet& getInheritableProperties() const;
ShorthandDefinition& registerShorthand( const std::string& name,
ShorthandDefinition& registerShorthand( ShorthandId id, const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandFuncName );
const ShorthandDefinition* getShorthand( const Uint32& id ) const;
ShorthandDefinition* registerShorthand( const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandFuncName );
const ShorthandDefinition* getShorthand( const ShorthandId& id ) const;
const ShorthandDefinition* getShorthand( const std::string& name ) const;
bool isShorthand( const Uint32& id ) const;
bool isShorthand( const std::string& name ) const;
void registerNodeSelector( const std::string& name, StyleSheetNodeSelector nodeSelector );
@@ -82,6 +90,8 @@ class EE_API StyleSheetSpecification {
void registerDefaultProperties();
void registerDefaultNodeSelectors();
void validateBuiltinRegistrations();
};
}}} // namespace EE::UI::CSS
+3 -1
View File
@@ -29,11 +29,13 @@ class EE_API StyleSheetStyle {
const StyleSheetVariables& getVariables() const;
StyleSheetProperty* getPropertyById( const PropertyId& id );
const StyleSheetProperty* getPropertyById( const PropertyId& id ) const;
const StyleSheetProperty* getPropertyByDefinition( const PropertyDefinition* def ) const;
StyleSheetProperty* getPropertyById( const Uint32& id );
StyleSheetProperty* getPropertyByNameHash( const String::HashType& id );
void setProperty( const StyleSheetProperty& property );
@@ -64,7 +64,7 @@ class CSSPropertiesModel final : public Model {
mWidget = widget;
mData = widget ? widget->getPropertiesImplemented() : std::vector<PropertyId>();
for ( const auto& prop : mData ) {
const auto* def = StyleSheetSpecification::instance()->getProperty( (Uint32)prop );
const auto* def = StyleSheetSpecification::instance()->getProperty( prop );
if ( !def )
continue;
mProps[prop] = def;
+7
View File
@@ -75,8 +75,14 @@ class EE_API UIHTMLImage : public UIHTMLWidget {
virtual void onDisplayChange();
virtual void onAttributesTransactionEnd();
void autoSizeImage();
void reconcileStyleSize();
bool hasUnresolvedPercentageSize() const;
void calcDestSize();
void clearDrawable();
@@ -95,6 +101,7 @@ class EE_API UIHTMLImage : public UIHTMLWidget {
DrawableResourceConnection mResourceChangeConnection;
Uint32 mSpriteChangeCb{ 0 };
bool mDeferLoad{ false };
bool mNeedsStyleSizeReconciliation{ false };
std::shared_ptr<std::atomic<bool>> mAsyncImageAlive;
Uint64 mRemoteImageLoadId{ 0 };
std::string mAlt;
+5 -5
View File
@@ -37,7 +37,8 @@ class EE_API UIStyle : public UIState {
void onStateChange();
const CSS::StyleSheetProperty* getStatelessStyleSheetProperty( const Uint32& propertyId ) const;
const CSS::StyleSheetProperty*
getStatelessStyleSheetProperty( const CSS::PropertyId& propertyId ) const;
void setStyleSheetProperties( const CSS::StyleSheetProperties& properties );
@@ -75,7 +76,7 @@ class EE_API UIStyle : public UIState {
UnorderedSet<UIWidget*>& getStructurallyVolatileChildren();
const CSS::StyleSheetProperty* getProperty( const CSS::PropertyId& id );
const CSS::StyleSheetProperty* getProperty( const CSS::PropertyId& id ) const;
bool hasProperty( const CSS::PropertyId& propertyId ) const;
@@ -104,7 +105,6 @@ class EE_API UIStyle : public UIState {
UnorderedSet<UIWidget*> mRelatedWidgets;
UnorderedSet<UIWidget*> mSubscribedWidgets;
UnorderedSet<UIWidget*> mStructurallyVolatileChildren;
std::optional<CSS::PropertyIdSet> mChangedProperties;
Uint32 mStateDepthCounter{ 0 };
Uint64 mLoadedVersion{ 0 };
const CSS::StyleSheet* mLoadedStyleSheet{ nullptr };
@@ -149,10 +149,10 @@ class EE_API UIStyle : public UIState {
void removeAnimation( const CSS::PropertyDefinition* propertyDefinition,
const Uint32& propertyIndex );
CSS::StyleSheetProperty* getLocalProperty( Uint32 propId );
CSS::StyleSheetProperty* getLocalProperty( CSS::PropertyId propId );
CSS::StyleSheetProperty*
getResolvedLocalProperty( Uint32 propId,
getResolvedLocalProperty( CSS::PropertyId propId,
std::optional<CSS::StyleSheetProperty>& resolvedProperty );
void addStructurallyVolatileWidgetFromParent();
+15
View File
@@ -1654,6 +1654,21 @@ class EE_API UIWidget : public UINode {
*/
virtual void onSizePolicyChange();
/**
* @brief Called when the outermost attributes transaction ends.
*
* Invoked from endAttributesTransaction() after the transaction count reaches
* zero but before the accumulated layout notifications are emitted. Property
* setters run immediately even inside a transaction, so interdependent
* properties can be observed in a partially applied state mid-loop. This hook
* lets derived widgets perform a final-state reconciliation once the complete
* style is applied, making the resulting geometry independent of the setter
* application order.
*/
virtual void onAttributesTransactionEnd();
bool isInAttributesTransaction() const { return mAttributesTransactionCount > 0; }
/**
* @brief Handles auto-size events.
*
+17 -3
View File
@@ -1,5 +1,6 @@
#include <eepp/system/functionstring.hpp>
#include <eepp/ui/css/elementdefinition.hpp>
#include <eepp/ui/css/stylesheetspecification.hpp>
namespace EE { namespace UI { namespace CSS {
@@ -8,7 +9,15 @@ ElementDefinition::ElementDefinition( const StyleSheetStyleVector& styleSheetSty
refresh();
}
StyleSheetProperty* ElementDefinition::getProperty( const Uint32& id ) {
StyleSheetProperty* ElementDefinition::getProperty( const PropertyId& id ) {
const auto* def = StyleSheetSpecification::instance()->getProperty( id );
if ( nullptr == def )
return nullptr;
auto it = mProperties.find( def->getId() );
return it != mProperties.end() ? &it->second : nullptr;
}
StyleSheetProperty* ElementDefinition::getPropertyByNameHash( const String::HashType& id ) {
auto it = mProperties.find( id );
return it != mProperties.end() ? &it->second : nullptr;
}
@@ -73,8 +82,13 @@ void ElementDefinition::refresh() {
resolveVariables();
for ( auto& property : mProperties )
mPropertyIds.insert( property.first );
for ( auto& property : mProperties ) {
// Only registered properties participate in the dense ID set. Unknown
// and data-* properties stay keyed by their own name hash in
// mProperties but never occupy a PropertyIdSet bit.
if ( property.second.getPropertyDefinition() )
mPropertyIds.insert( property.second.getPropertyDefinition()->getPropertyId() );
}
}
void ElementDefinition::findVariables( const StyleSheetStyle* style ) {
+5 -4
View File
@@ -4,10 +4,11 @@
namespace EE { namespace UI { namespace CSS {
PropertyDefinition::PropertyDefinition( const std::string& name, const std::string& defaultValue,
const bool& inherited ) :
PropertyDefinition::PropertyDefinition( PropertyId propertyId, const std::string& name,
const std::string& defaultValue, const bool& inherited ) :
mName( name ),
mId( String::hash( name ) ),
mPropertyId( propertyId ),
mDefaultValue( defaultValue ),
mInherited( inherited ),
mIndexed( false ),
@@ -23,7 +24,7 @@ const String::HashType& PropertyDefinition::getId() const {
}
PropertyId PropertyDefinition::getPropertyId() const {
return static_cast<PropertyId>( mId );
return mPropertyId;
}
const std::string& PropertyDefinition::getDefaultValue() const {
@@ -57,7 +58,7 @@ PropertyDefinition& PropertyDefinition::addAlias( const std::string& alias ) {
String::HashType aliasId = String::hash( alias );
mAliases.push_back( alias );
mAliasesHash.push_back( aliasId );
PropertySpecification::instance()->addPropertyAlias( aliasId, this );
PropertySpecification::instance()->addPropertyAlias( alias, this );
return *this;
}
+136 -43
View File
@@ -8,22 +8,27 @@ SINGLETON_DECLARE_IMPLEMENTATION( PropertySpecification )
PropertySpecification::~PropertySpecification() {}
PropertyDefinition& PropertySpecification::registerProperty( const std::string& propertyName,
PropertyDefinition& PropertySpecification::registerProperty( PropertyId id,
const std::string& propertyName,
const std::string& defaultValue,
bool inherited ) {
PropertyDefinition* property = const_cast<PropertyDefinition*>( getProperty( propertyName ) );
if ( nullptr != property && !String::startsWith( propertyName, "-" ) ) {
Log::warning( "Property \"%s\" already registered.", propertyName.c_str() );
return *property;
if ( !mPropertyIds.addBuiltin( id, propertyName ) ) {
// A built-in ID already bound to another name is a programming error.
Log::error( "PropertySpecification: failed to register built-in property \"%s\".",
propertyName.c_str() );
eeASSERT( false );
}
PropertyDefinition* propDef = new PropertyDefinition( propertyName, defaultValue, inherited );
PropertyDefinition* propDef =
new PropertyDefinition( id, propertyName, defaultValue, inherited );
mProperties[propDef->getId()] = std::shared_ptr<PropertyDefinition>( propDef );
const size_t idx = static_cast<size_t>( id );
if ( mPropertiesById.size() <= idx )
mPropertiesById.resize( idx + 1 );
mPropertiesById[idx] = std::shared_ptr<PropertyDefinition>( propDef );
if ( inherited )
mInheritableProperties.push_back( propDef->getPropertyId() );
mInheritableProperties.insert( id );
for ( auto& sep : { "-", "_" } ) {
if ( propDef->getName().find( sep ) != std::string::npos ) {
@@ -36,76 +41,164 @@ PropertyDefinition& PropertySpecification::registerProperty( const std::string&
return *propDef;
}
const SmallVector<PropertyId>& PropertySpecification::getInheritableProperties() const {
PropertyDefinition* PropertySpecification::registerProperty( const std::string& propertyName,
const std::string& defaultValue,
bool inherited ) {
PropertyId id = mPropertyIds.getId( propertyName );
PropertyDefinition* existing = ( id != PropertyId::Invalid )
? const_cast<PropertyDefinition*>( getProperty( id ) )
: nullptr;
if ( nullptr != existing && !String::startsWith( propertyName, "-" ) ) {
Log::warning( "Property \"%s\" already registered.", propertyName.c_str() );
return existing;
}
if ( id == PropertyId::Invalid ) {
id = mPropertyIds.getOrCreateId( propertyName );
if ( id == PropertyId::Invalid ) {
Log::error( "PropertySpecification: could not allocate an ID for property \"%s\".",
propertyName.c_str() );
return nullptr;
}
}
PropertyDefinition* propDef =
new PropertyDefinition( id, propertyName, defaultValue, inherited );
const size_t idx = static_cast<size_t>( id );
if ( mPropertiesById.size() <= idx )
mPropertiesById.resize( idx + 1 );
mPropertiesById[idx] = std::shared_ptr<PropertyDefinition>( propDef );
if ( inherited )
mInheritableProperties.insert( id );
else
mInheritableProperties.erase( id );
for ( auto& sep : { "-", "_" } ) {
if ( propDef->getName().find( sep ) != std::string::npos ) {
std::string alias( propDef->getName() );
String::replaceAll( alias, sep, "" );
propDef->addAlias( alias );
}
}
return propDef;
}
const PropertyIdSet& PropertySpecification::getInheritableProperties() const {
return mInheritableProperties;
}
const PropertyDefinition* PropertySpecification::getProperty( const PropertyId& id ) const {
return getProperty( static_cast<std::underlying_type_t<PropertyId>>( id ) );
const size_t idx = static_cast<size_t>( id );
if ( 0 == idx || idx >= mPropertiesById.size() )
return nullptr;
return mPropertiesById[idx].get();
}
const PropertyDefinition* PropertySpecification::getProperty( const Uint32& id ) const {
auto it = mProperties.find( id );
const PropertyDefinition* PropertySpecification::getProperty( const char* name ) const {
return getProperty( mPropertyIds.getId( name ) );
}
if ( it != mProperties.end() )
return it->second.get();
return nullptr;
const PropertyDefinition* PropertySpecification::getProperty( std::string_view name ) const {
return getProperty( mPropertyIds.getId( name ) );
}
const PropertyDefinition* PropertySpecification::getProperty( const std::string& name ) const {
return getProperty( String::hash( name ) );
return getProperty( mPropertyIds.getId( name ) );
}
ShorthandDefinition&
PropertySpecification::registerShorthand( const std::string& name,
PropertySpecification::registerShorthand( ShorthandId id, const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandParserName ) {
ShorthandDefinition* shorthand = const_cast<ShorthandDefinition*>( getShorthand( name ) );
if ( nullptr != shorthand ) {
Log::warning( "Shorthand %s already registered.", name.c_str() );
return *shorthand;
if ( !mShorthandIds.addBuiltin( id, name ) ) {
// A built-in ID already bound to another name is a programming error.
Log::error( "PropertySpecification: failed to register built-in shorthand \"%s\".",
name.c_str() );
eeASSERT( false );
}
ShorthandDefinition* shorthandDef =
new ShorthandDefinition( name, properties, shorthandParserName );
new ShorthandDefinition( id, name, properties, shorthandParserName );
mShorthands[shorthandDef->getId()] = std::shared_ptr<ShorthandDefinition>( shorthandDef );
const size_t idx = static_cast<size_t>( id );
if ( mShorthandsById.size() <= idx )
mShorthandsById.resize( idx + 1 );
mShorthandsById[idx] = std::shared_ptr<ShorthandDefinition>( shorthandDef );
return *shorthandDef;
}
const ShorthandDefinition* PropertySpecification::getShorthand( const String::HashType& id ) const {
auto it = mShorthands.find( id );
ShorthandDefinition*
PropertySpecification::registerShorthand( const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandParserName ) {
ShorthandId id = mShorthandIds.getId( name );
ShorthandDefinition* existing = ( id != ShorthandId::Invalid )
? const_cast<ShorthandDefinition*>( getShorthand( id ) )
: nullptr;
if ( nullptr != existing ) {
Log::warning( "Shorthand %s already registered.", name.c_str() );
return existing;
}
if ( it != mShorthands.end() )
return it->second.get();
id = mShorthandIds.getOrCreateId( name );
if ( id == ShorthandId::Invalid ) {
Log::error( "PropertySpecification: could not allocate an ID for shorthand \"%s\".",
name.c_str() );
return nullptr;
}
return nullptr;
ShorthandDefinition* shorthandDef =
new ShorthandDefinition( id, name, properties, shorthandParserName );
const size_t idx = static_cast<size_t>( id );
if ( mShorthandsById.size() <= idx )
mShorthandsById.resize( idx + 1 );
mShorthandsById[idx] = std::shared_ptr<ShorthandDefinition>( shorthandDef );
return shorthandDef;
}
const ShorthandDefinition* PropertySpecification::getShorthand( const ShorthandId& id ) const {
const size_t idx = static_cast<size_t>( id );
if ( 0 == idx || idx >= mShorthandsById.size() )
return nullptr;
return mShorthandsById[idx].get();
}
const ShorthandDefinition* PropertySpecification::getShorthand( const std::string& name ) const {
return getShorthand( String::hash( name ) );
return getShorthand( mShorthandIds.getId( name ) );
}
bool PropertySpecification::isShorthand( const std::string& name ) const {
return getShorthand( name ) != nullptr;
}
bool PropertySpecification::isShorthand( const Uint32& id ) const {
return getShorthand( id ) != nullptr;
}
const PropertyDefinition*
PropertySpecification::addPropertyAlias( Uint32 aliasId, const PropertyDefinition* propDef ) {
if ( getProperty( aliasId ) == nullptr ) {
auto it = mProperties.find( propDef->getId() );
if ( it != mProperties.end() ) {
mProperties[aliasId] = it->second;
}
}
PropertySpecification::addPropertyAlias( const std::string& alias,
const PropertyDefinition* propDef ) {
mPropertyIds.addAlias( alias, propDef->getPropertyId() );
return propDef;
}
const ShorthandDefinition*
PropertySpecification::addShorthandAlias( const std::string& alias,
const ShorthandDefinition* shorthandDef ) {
mShorthandIds.addAlias( alias, shorthandDef->getShorthandId() );
return shorthandDef;
}
void PropertySpecification::finalizeBuiltins() {
const bool propsOk = mPropertyIds.finalizeBuiltins();
const bool shorthandsOk = mShorthandIds.finalizeBuiltins();
if ( !propsOk || !shorthandsOk ) {
Log::error( "PropertySpecification: built-in registration validation failed." );
eeASSERT( false );
}
}
}}} // namespace EE::UI::CSS
+7 -4
View File
@@ -1,6 +1,7 @@
#include <algorithm>
#include <eepp/core.hpp>
#include <eepp/system/color.hpp>
#include <eepp/ui/css/propertyspecification.hpp>
#include <eepp/ui/css/shorthanddefinition.hpp>
#include <eepp/ui/css/stylesheetspecification.hpp>
@@ -8,18 +9,19 @@ using namespace EE::System;
namespace EE { namespace UI { namespace CSS {
ShorthandDefinition* ShorthandDefinition::New( const std::string& name,
ShorthandDefinition* ShorthandDefinition::New( ShorthandId shorthandId, const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandParserName ) {
return eeNew( ShorthandDefinition, ( name, properties, shorthandParserName ) );
return eeNew( ShorthandDefinition, ( shorthandId, name, properties, shorthandParserName ) );
}
ShorthandDefinition::ShorthandDefinition( const std::string& name,
ShorthandDefinition::ShorthandDefinition( ShorthandId shorthandId, const std::string& name,
const std::vector<std::string>& properties,
const std::string& shorthandParserName ) :
mName( name ),
mFuncName( shorthandParserName ),
mId( String::hash( name ) ),
mShorthandId( shorthandId ),
mProperties( properties ) {
for ( auto& sep : { "-", "_" } ) {
if ( mName.find( sep ) != std::string::npos ) {
@@ -43,7 +45,7 @@ const String::HashType& ShorthandDefinition::getId() const {
}
ShorthandId ShorthandDefinition::getShorthandId() const {
return static_cast<ShorthandId>( mId );
return mShorthandId;
}
const std::vector<std::string>& ShorthandDefinition::getProperties() const {
@@ -53,6 +55,7 @@ const std::vector<std::string>& ShorthandDefinition::getProperties() const {
ShorthandDefinition& ShorthandDefinition::addAlias( const std::string& alias ) {
mAliases.push_back( alias );
mAliasesHash.push_back( String::hash( alias ) );
PropertySpecification::instance()->addShorthandAlias( alias, this );
return *this;
}
+13 -5
View File
@@ -83,9 +83,9 @@ StyleSheetProperty::StyleSheetProperty( const std::string& name, const std::stri
mVolatile( false ),
mImportant( false ),
mIsVarValue( false ),
mPropertyDefinition( StyleSheetSpecification::instance()->getProperty( mNameHash ) ),
mPropertyDefinition( StyleSheetSpecification::instance()->getProperty( mName ) ),
mShorthandDefinition( NULL == mPropertyDefinition
? StyleSheetSpecification::instance()->getShorthand( mNameHash )
? StyleSheetSpecification::instance()->getShorthand( mName )
: NULL ) {
cleanValue();
checkImportant();
@@ -110,9 +110,9 @@ StyleSheetProperty::StyleSheetProperty( const std::string& name, const std::stri
mVolatile( isVolatile ),
mImportant( false ),
mIsVarValue( false ),
mPropertyDefinition( StyleSheetSpecification::instance()->getProperty( mNameHash ) ),
mPropertyDefinition( StyleSheetSpecification::instance()->getProperty( mName ) ),
mShorthandDefinition( NULL == mPropertyDefinition
? StyleSheetSpecification::instance()->getShorthand( mNameHash )
? StyleSheetSpecification::instance()->getShorthand( mName )
: NULL ) {
cleanValue();
checkImportant();
@@ -128,7 +128,15 @@ StyleSheetProperty::StyleSheetProperty( const std::string& name, const std::stri
Uint32 StyleSheetProperty::getId() const {
return NULL != mPropertyDefinition
? mPropertyDefinition->getId()
: ( NULL != mShorthandDefinition ? mShorthandDefinition->getId() : 0 );
: ( NULL != mShorthandDefinition ? mShorthandDefinition->getId() : mNameHash );
}
PropertyId StyleSheetProperty::getPropertyId() const {
return mPropertyDefinition ? mPropertyDefinition->getPropertyId() : PropertyId::Invalid;
}
ShorthandId StyleSheetProperty::getShorthandId() const {
return mShorthandDefinition ? mShorthandDefinition->getShorthandId() : ShorthandId::Invalid;
}
const std::string& StyleSheetProperty::getName() const {
+1 -1
View File
@@ -171,7 +171,7 @@ void StyleSheetSelectorRule::parseFragment( const std::string& selectorFragment
attr.isDataAttribute = isDataAttributeName( attr.name );
if ( !attr.isDataAttribute )
attr.propertyDefinition =
StyleSheetSpecification::instance()->getProperty( String::hash( attr.name ) );
StyleSheetSpecification::instance()->getProperty( attr.name );
mAttributeSelectors.emplace_back( std::move( attr ) );
mSpecificity += SpecificityClass;
File diff suppressed because it is too large Load Diff
+18 -14
View File
@@ -1,4 +1,5 @@
#include <algorithm>
#include <eepp/ui/css/stylesheetspecification.hpp>
#include <eepp/ui/css/stylesheetstyle.hpp>
namespace EE { namespace UI { namespace CSS {
@@ -83,15 +84,20 @@ const StyleSheetVariables& StyleSheetStyle::getVariables() const {
return mVariables;
}
const StyleSheetProperty* StyleSheetStyle::getPropertyById( const PropertyId& id ) const {
for ( auto& prop : mProperties ) {
if ( NULL != prop.second.getPropertyDefinition() &&
prop.second.getPropertyDefinition()->getPropertyId() == id ) {
return &prop.second;
}
}
StyleSheetProperty* StyleSheetStyle::getPropertyById( const PropertyId& id ) {
const auto* def = StyleSheetSpecification::instance()->getProperty( id );
if ( nullptr == def )
return nullptr;
auto it = mProperties.find( def->getId() );
return it != mProperties.end() ? &it->second : nullptr;
}
return nullptr;
const StyleSheetProperty* StyleSheetStyle::getPropertyById( const PropertyId& id ) const {
const auto* def = StyleSheetSpecification::instance()->getProperty( id );
if ( nullptr == def )
return nullptr;
auto it = mProperties.find( def->getId() );
return it != mProperties.end() ? &it->second : nullptr;
}
const StyleSheetProperty*
@@ -106,7 +112,7 @@ StyleSheetStyle::getPropertyByDefinition( const PropertyDefinition* def ) const
return nullptr;
}
StyleSheetProperty* StyleSheetStyle::getPropertyById( const Uint32& id ) {
StyleSheetProperty* StyleSheetStyle::getPropertyByNameHash( const String::HashType& id ) {
auto it = mProperties.find( id );
if ( it != mProperties.end() )
@@ -120,7 +126,7 @@ void StyleSheetStyle::setProperty( const StyleSheetProperty& property ) {
property.getPropertyDefinition()->isIndexed() ) {
// If the property being set is indexed we need to merge any other index set to the new
// property set.
const StyleSheetProperty* currentProperty = getPropertyById( property.getId() );
const StyleSheetProperty* currentProperty = getPropertyById( property.getPropertyId() );
std::vector<std::string> values;
if ( nullptr == currentProperty ) {
if ( property.getIndex() > 0 ) {
@@ -163,10 +169,8 @@ bool StyleSheetStyle::hasProperties() const {
}
bool StyleSheetStyle::hasProperty( PropertyId id ) const {
return std::find_if( mProperties.begin(), mProperties.end(), [&id]( const auto& prop ) {
return prop.second.getPropertyDefinition() &&
prop.second.getPropertyDefinition()->getPropertyId() == id;
} ) != mProperties.end();
const auto* def = StyleSheetSpecification::instance()->getProperty( id );
return def && mProperties.find( def->getId() ) != mProperties.end();
}
bool StyleSheetStyle::hasProperty( const std::string& name ) const {
-2
View File
@@ -239,7 +239,6 @@ std::string UICheckBox::getPropertyString( const PropertyDefinition* propertyDef
switch ( propertyDef->getPropertyId() ) {
case PropertyId::Selected:
case PropertyId::Checked:
case PropertyId::Value:
return isChecked() ? "true" : "false";
case PropertyId::CheckMode:
@@ -270,7 +269,6 @@ bool UICheckBox::applyProperty( const StyleSheetProperty& attribute ) {
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
case PropertyId::Selected:
case PropertyId::Checked:
case PropertyId::Value:
setChecked( attribute.asBool() );
break;
+61 -2
View File
@@ -186,7 +186,8 @@ const Vector2f& UIHTMLImage::getAlignOffset() const {
bool UIHTMLImage::applyProperty( const StyleSheetProperty& property ) {
if ( !checkPropertyDefinition( property ) )
return false;
switch ( property.getPropertyDefinition()->getPropertyId() ) {
const PropertyId propertyId = property.getPropertyDefinition()->getPropertyId();
switch ( propertyId ) {
case PropertyId::Src: {
if ( property.getValue().empty() )
return true;
@@ -234,8 +235,32 @@ bool UIHTMLImage::applyProperty( const StyleSheetProperty& property ) {
case PropertyId::Defer:
mDeferLoad = property.getValue().empty() || property.asBool();
break;
default:
default: {
if ( isInAttributesTransaction() ) {
switch ( propertyId ) {
case PropertyId::Width:
case PropertyId::Height:
case PropertyId::MinWidth:
case PropertyId::MinHeight:
case PropertyId::MaxWidth:
case PropertyId::MaxHeight:
case PropertyId::PaddingLeft:
case PropertyId::PaddingRight:
case PropertyId::PaddingTop:
case PropertyId::PaddingBottom:
case PropertyId::BorderLeftWidth:
case PropertyId::BorderRightWidth:
case PropertyId::BorderTopWidth:
case PropertyId::BorderBottomWidth:
case PropertyId::BoxSizing:
mNeedsStyleSizeReconciliation = true;
break;
default:
break;
}
}
return UIHTMLWidget::applyProperty( property );
}
}
return true;
}
@@ -275,6 +300,7 @@ void UIHTMLImage::scheduledUpdate( const Time& time ) {
}
void UIHTMLImage::updateLayout() {
reconcileStyleSize();
autoSizeImage();
UIHTMLWidget::updateLayout();
}
@@ -357,6 +383,30 @@ void UIHTMLImage::autoSizeImage() {
setInternalPixelsSize( size.floor() );
}
bool UIHTMLImage::hasUnresolvedPercentageSize() const {
const auto* style = getUIStyle();
const auto* width = style ? style->getProperty( PropertyId::Width ) : nullptr;
const auto* height = style ? style->getProperty( PropertyId::Height ) : nullptr;
const bool needsContainingWidth =
( width && StyleSheetLength::isPercentage( width->value() ) ) ||
StyleSheetLength::isPercentage( mMinWidthEq ) ||
StyleSheetLength::isPercentage( mMaxWidthEq );
const bool needsContainingHeight =
( height && StyleSheetLength::isPercentage( height->value() ) ) ||
StyleSheetLength::isPercentage( mMinHeightEq ) ||
StyleSheetLength::isPercentage( mMaxHeightEq );
return ( needsContainingWidth && getContainingBlockContentWidth() <= 0.f ) ||
( needsContainingHeight && getContainingBlockContentHeight() <= 0.f );
}
void UIHTMLImage::reconcileStyleSize() {
if ( !mNeedsStyleSizeReconciliation || hasUnresolvedPercentageSize() )
return;
mNeedsStyleSizeReconciliation = false;
updateCSSContentBoxFixedSize();
autoSizeImage();
}
void UIHTMLImage::onSizeChange() {
autoSizeImage();
calcDestSize();
@@ -375,6 +425,7 @@ void UIHTMLImage::onAlignChange() {
void UIHTMLImage::onParentSizeChange( const Vector2f& change ) {
UIHTMLWidget::onParentSizeChange( change );
reconcileStyleSize();
autoSizeImage();
}
@@ -389,6 +440,14 @@ void UIHTMLImage::onDisplayChange() {
setLayoutWidthPolicy( SizePolicy::WrapContent );
}
void UIHTMLImage::onAttributesTransactionEnd() {
UIHTMLWidget::onAttributesTransactionEnd();
// Replaced sizing depends on the final width/height policies, min/max
// constraints and intrinsic ratio, so reconcile it after the complete style
// transaction instead of depending on property traversal order.
reconcileStyleSize();
}
void UIHTMLImage::calcDestSize() {
if ( mScaleType == UIScaleType::Expand ) {
mDestSize = { mSize.x - mPaddingPx.Left - mPaddingPx.Right,
-2
View File
@@ -117,7 +117,6 @@ bool UIHTMLInput::applyProperty( const StyleSheetProperty& attribute ) {
mHasSpecifiedHeight = !attribute.value().empty() &&
!String::iequals( String::trim( attribute.value() ), "auto" );
break;
case PropertyId::Checked:
case PropertyId::Selected:
mChecked = htmlBoolAttributeIsTrue( attribute );
syncCheckedState();
@@ -152,7 +151,6 @@ std::string UIHTMLInput::getPropertyString( const PropertyDefinition* propertyDe
switch ( propertyDef->getPropertyId() ) {
case PropertyId::Value:
return mValue;
case PropertyId::Checked:
case PropertyId::Selected:
return mChecked ? "true" : "false";
case PropertyId::Type:
-5
View File
@@ -852,7 +852,6 @@ std::vector<PropertyId> UIHTMLWidget::getPropertiesImplemented() const {
PropertyId::AlignmentBaseline,
PropertyId::FlexDirection,
PropertyId::FlexWrap,
PropertyId::FlexFlow,
PropertyId::JustifyContent,
PropertyId::AlignItems,
PropertyId::AlignContent,
@@ -860,19 +859,15 @@ std::vector<PropertyId> UIHTMLWidget::getPropertiesImplemented() const {
PropertyId::FlexGrow,
PropertyId::FlexShrink,
PropertyId::FlexBasis,
PropertyId::Flex,
PropertyId::Order,
PropertyId::ColumnGap,
PropertyId::RowGap,
PropertyId::Gap,
PropertyId::GridTemplateRows,
PropertyId::GridTemplateColumns,
PropertyId::GridTemplateAreas,
PropertyId::GridTemplate,
PropertyId::GridAutoRows,
PropertyId::GridAutoColumns,
PropertyId::GridAutoFlow,
PropertyId::Grid,
PropertyId::GridRowStart,
PropertyId::GridRowEnd,
PropertyId::GridColumnStart,
-2
View File
@@ -132,7 +132,6 @@ bool UIMenuCheckBox::applyProperty( const StyleSheetProperty& attribute ) {
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
case PropertyId::Selected:
case PropertyId::Checked:
case PropertyId::Value:
setActive( attribute.asBool() );
break;
@@ -151,7 +150,6 @@ std::string UIMenuCheckBox::getPropertyString( const PropertyDefinition* propert
switch ( propertyDef->getPropertyId() ) {
case PropertyId::Selected:
case PropertyId::Checked:
case PropertyId::Value:
return isActive() ? "true" : "false";
default:
+32 -30
View File
@@ -229,11 +229,10 @@ bool UIStyle::isStructurallyVolatile() const {
}
void UIStyle::reloadFontFamily() {
if ( mDefinition && mDefinition->getPropertyIds().contains( (Uint32)PropertyId::FontFamily ) ) {
auto propIt = mDefinition->getProperties().find( (Uint32)PropertyId::FontFamily );
if ( propIt != mDefinition->getProperties().end() ) {
applyStyleSheetProperty( propIt->second, nullptr );
}
if ( mDefinition && mDefinition->getPropertyIds().contains( PropertyId::FontFamily ) ) {
StyleSheetProperty* fontProp = mDefinition->getProperty( PropertyId::FontFamily );
if ( fontProp )
applyStyleSheetProperty( *fontProp, nullptr );
}
}
@@ -251,8 +250,8 @@ UnorderedSet<UIWidget*>& UIStyle::getStructurallyVolatileChildren() {
return mStructurallyVolatileChildren;
}
const CSS::StyleSheetProperty* UIStyle::getProperty( const CSS::PropertyId& id ) {
const auto* gProp = mGlobalDefinition ? mGlobalDefinition->getProperty( (Uint32)id ) : nullptr;
const CSS::StyleSheetProperty* UIStyle::getProperty( const CSS::PropertyId& id ) const {
const auto* gProp = mGlobalDefinition ? mGlobalDefinition->getProperty( id ) : nullptr;
const auto* elProp = mElementStyle ? mElementStyle->getPropertyById( id ) : nullptr;
if ( elProp && gProp )
return elProp->getSpecificity() > gProp->getSpecificity() ? elProp : gProp;
@@ -260,13 +259,13 @@ const CSS::StyleSheetProperty* UIStyle::getProperty( const CSS::PropertyId& id )
}
bool UIStyle::hasProperty( const CSS::PropertyId& propertyId ) const {
return ( mGlobalDefinition && mGlobalDefinition->getProperty( (Uint32)propertyId ) ) ||
return ( mGlobalDefinition && mGlobalDefinition->getProperty( propertyId ) ) ||
( mElementStyle && mElementStyle->getPropertyById( propertyId ) );
}
bool UIStyle::hasLocalProperty( PropertyId propId ) const {
return ( mElementStyle && mElementStyle->getPropertyById( propId ) ) ||
( mDefinition && mDefinition->getProperty( (Uint32)propId ) );
( mDefinition && mDefinition->getProperty( propId ) );
}
CSS::StyleSheetProperty* UIStyle::getInheritedProperty( CSS::PropertyId propId,
@@ -279,7 +278,7 @@ CSS::StyleSheetProperty* UIStyle::getInheritedProperty( CSS::PropertyId propId,
UIWidget* parent = parentNode->asType<UIWidget>();
UIStyle* parentStyle = parent->getUIStyle();
if ( parentStyle ) {
auto prop = parentStyle->getLocalProperty( (Uint32)propId );
auto prop = parentStyle->getLocalProperty( propId );
if ( prop ) {
if ( ownerStyle )
*ownerStyle = parentStyle;
@@ -392,10 +391,7 @@ void UIStyle::onStateChange() {
mWidget->getUISceneNode()->getStyleSheet().getElementStyles( mWidget, true );
if ( newDefinition != mDefinition || mForceReapplyProperties ) {
if ( !mChangedProperties )
mChangedProperties.emplace();
PropertyIdSet& changedProperties = *mChangedProperties;
changedProperties.clear();
PropertyIdSet changedProperties;
if ( mDefinition )
changedProperties = mDefinition->getPropertyIds();
@@ -409,7 +405,7 @@ void UIStyle::onStateChange() {
const PropertyIdSet propertiesInBothDefinitions =
( mDefinition->getPropertyIds() & newDefinition->getPropertyIds() );
for ( Uint32 id : propertiesInBothDefinitions ) {
for ( PropertyId id : propertiesInBothDefinitions ) {
const StyleSheetProperty* p0 = mDefinition->getProperty( id );
const StyleSheetProperty* p1 = newDefinition->getProperty( id );
if ( nullptr != p0 && nullptr != p1 && *p0 == *p1 )
@@ -423,8 +419,10 @@ void UIStyle::onStateChange() {
// Local raw values can stay unchanged while their resolved var()/light-dark() value
// changes after a stylesheet update, so include them after definition pruning.
for ( const auto& localProperty : mElementStyle->getProperties() ) {
if ( localProperty.second.needsValueSubstitution() )
changedProperties.insert( localProperty.first );
if ( localProperty.second.needsValueSubstitution() &&
localProperty.second.getPropertyDefinition() )
changedProperties.insert(
localProperty.second.getPropertyDefinition()->getPropertyId() );
}
}
@@ -439,7 +437,7 @@ void UIStyle::onStateChange() {
mDefinition->getTransitionProperties() );
}
for ( auto prop : changedProperties ) {
for ( PropertyId prop : changedProperties ) {
std::optional<StyleSheetProperty> resolvedProperty;
StyleSheetProperty* property = getResolvedLocalProperty( prop, resolvedProperty );
@@ -448,7 +446,7 @@ void UIStyle::onStateChange() {
if ( def && def->isInherited() ) {
UIStyle* inheritedStyle = nullptr;
StyleSheetProperty* inheritedProp =
getInheritedProperty( static_cast<PropertyId>( prop ), &inheritedStyle );
getInheritedProperty( prop, &inheritedStyle );
if ( inheritedProp ) {
std::optional<StyleSheetProperty> resolvedInheritedProperty;
const auto& inheritedPropertyToApply = resolveInheritedProperty(
@@ -464,8 +462,7 @@ void UIStyle::onStateChange() {
if ( property->getValue() == "inherit" &&
!property->getPropertyDefinition()->isIndexed() ) {
UIStyle* inheritedStyle = nullptr;
StyleSheetProperty* inheritedProp =
getInheritedProperty( static_cast<PropertyId>( prop ), &inheritedStyle );
StyleSheetProperty* inheritedProp = getInheritedProperty( prop, &inheritedStyle );
if ( inheritedProp ) {
if ( property->getPropertyDefinition()->getPropertyId() ==
PropertyId::FontSize ) {
@@ -524,8 +521,8 @@ void UIStyle::onStateChange() {
}
const StyleSheetProperty*
UIStyle::getStatelessStyleSheetProperty( const Uint32& propertyId ) const {
if ( 0 == propertyId )
UIStyle::getStatelessStyleSheetProperty( const PropertyId& propertyId ) const {
if ( propertyId == PropertyId::Invalid )
return nullptr;
if ( !mElementStyle->getSelector().hasPseudoClasses() ) {
@@ -629,7 +626,7 @@ void UIStyle::applyStyleSheetProperty( const StyleSheetProperty& originalPropert
if ( mCurrentState != UIState::StateFlagNormal ||
( mCurrentState == UIState::StateFlagNormal && property->isVolatile() ) ) {
const StyleSheetProperty* oldAttribute =
getStatelessStyleSheetProperty( property->getId() );
getStatelessStyleSheetProperty( property->getPropertyId() );
if ( nullptr == oldAttribute && getPreviousState() == UIState::StateFlagNormal ) {
std::string value(
mWidget->getPropertyString( propertyDefinition, property->getIndex() ) );
@@ -651,7 +648,7 @@ void UIStyle::applyStyleSheetProperty( const StyleSheetProperty& originalPropert
if ( !startValue.empty() ) {
// Get the real start value
if ( nullptr != prevDefinition ) {
auto prevProp = prevDefinition->getProperty( property->getId() );
auto prevProp = prevDefinition->getProperty( property->getPropertyId() );
if ( nullptr != prevProp ) {
StyleSheetProperty* curProperty = prevProp;
std::optional<StyleSheetProperty> resolvedPrevProperty;
@@ -843,7 +840,8 @@ void UIStyle::startAnimations( const CSS::AnimationsMap& animations ) {
if ( StyleSheetPropertyAnimation::animationSupported( propDef->getType() ) ) {
if ( propDef->isIndexed() ) {
StyleSheetProperty* prop = mDefinition->getProperty( propDef->getId() );
StyleSheetProperty* prop =
mDefinition->getProperty( propDef->getPropertyId() );
if ( nullptr != prop ) {
for ( size_t i = 0; i < prop->getPropertyIndexCount(); i++ ) {
removeAnimation( propDef, i );
@@ -927,10 +925,14 @@ void UIStyle::removeAnimation( const PropertyDefinition* propertyDefinition,
}
}
StyleSheetProperty* UIStyle::getLocalProperty( Uint32 propId ) {
StyleSheetProperty* UIStyle::getLocalProperty( PropertyId propId ) {
StyleSheetProperty* defProperty = mDefinition ? mDefinition->getProperty( propId ) : nullptr;
StyleSheetProperty* elemProperty =
mElementStyle ? mElementStyle->getPropertyById( propId ) : nullptr;
StyleSheetProperty* elemProperty = nullptr;
if ( mElementStyle ) {
const auto* def = StyleSheetSpecification::instance()->getProperty( propId );
if ( def )
elemProperty = mElementStyle->getPropertyById( def->getPropertyId() );
}
if ( defProperty && elemProperty )
return defProperty->getSpecificity() > elemProperty->getSpecificity() ? defProperty
: elemProperty;
@@ -938,7 +940,7 @@ StyleSheetProperty* UIStyle::getLocalProperty( Uint32 propId ) {
}
StyleSheetProperty*
UIStyle::getResolvedLocalProperty( Uint32 propId,
UIStyle::getResolvedLocalProperty( PropertyId propId,
std::optional<StyleSheetProperty>& resolvedProperty ) {
StyleSheetProperty* property = getLocalProperty( propId );
if ( property && property->needsValueSubstitution() ) {
+3 -9
View File
@@ -298,8 +298,6 @@ std::string UIViewPager::getPropertyString( const PropertyDefinition* propertyDe
return String::fromFloat( mChangePagePercent );
case PropertyId::MaxEdgeResistance:
return String::fromFloat( mMaxEdgeResistance );
case PropertyId::PageTransitionDuration:
return mPageTransitionDuration.toString();
case PropertyId::TimingFunction:
return Ease::toString( mTimingFunction );
case PropertyId::PageLocked:
@@ -311,13 +309,9 @@ std::string UIViewPager::getPropertyString( const PropertyDefinition* propertyDe
std::vector<PropertyId> UIViewPager::getPropertiesImplemented() const {
auto props = UIWidget::getPropertiesImplemented();
auto local = { PropertyId::Orientation,
PropertyId::DragResistance,
PropertyId::ChangePagePercent,
PropertyId::MaxEdgeResistance,
PropertyId::PageTransitionDuration,
PropertyId::TimingFunction,
PropertyId::PageLocked };
auto local = { PropertyId::Orientation, PropertyId::DragResistance,
PropertyId::ChangePagePercent, PropertyId::MaxEdgeResistance,
PropertyId::TimingFunction, PropertyId::PageLocked };
props.insert( props.end(), local.begin(), local.end() );
return props;
}
+9 -2
View File
@@ -1505,10 +1505,18 @@ void UIWidget::beginAttributesTransaction() {
mAttributesTransactionCount++;
}
void UIWidget::onAttributesTransactionEnd() {}
void UIWidget::endAttributesTransaction() {
eeASSERT( mAttributesTransactionCount > 0 );
mAttributesTransactionCount--;
if ( 0 == mAttributesTransactionCount ) {
// Reconcile interdependent properties against the complete, final style
// before emitting the accumulated layout notifications so that any change
// triggered by the reconciliation is folded into the same pending flags.
onAttributesTransactionEnd();
if ( mFlags & UI_ATTRIBUTE_CHANGED ) {
LayoutInvalidationFlags reasons = mPendingLayoutReasons;
mPendingLayoutReasons = 0;
@@ -2372,8 +2380,7 @@ bool UIWidget::applyProperty( const StyleSheetProperty& attribute ) {
case PropertyId::LayoutToRightOf:
case PropertyId::LayoutToTopOf: {
PositionPolicy rule = PositionPolicy::None;
PropertyId layoutId =
static_cast<PropertyId>( attribute.getPropertyDefinition()->getId() );
PropertyId layoutId = attribute.getPropertyDefinition()->getPropertyId();
if ( layoutId == PropertyId::LayoutToLeftOf )
rule = PositionPolicy::LeftOf;
else if ( layoutId == PropertyId::LayoutToRightOf )
+181 -24
View File
@@ -1,72 +1,229 @@
#include "utest.h"
#include <eepp/core/memorymanager.hpp>
#include <eepp/ui/css/idnamemap.hpp>
#include <eepp/ui/css/propertyidset.hpp>
#include <vector>
using namespace EE;
using namespace EE::UI::CSS;
namespace {
enum class TestDenseId : Uint8 {
Invalid = 0,
One,
Two,
FirstCustomId,
MaxNumIds = 5,
};
} // namespace
static PropertyId id( std::size_t value ) {
return static_cast<PropertyId>( value );
}
UTEST( PropertyIdSet, setOperations ) {
PropertyIdSet first;
first.insert( 30 );
first.insert( 10 );
first.insert( 20 );
first.insert( 20 );
first.insert( id( 30 ) );
first.insert( id( 10 ) );
first.insert( id( 20 ) );
first.insert( id( 20 ) );
PropertyIdSet second;
second.insert( 20 );
second.insert( 40 );
second.insert( id( 20 ) );
second.insert( id( 40 ) );
const PropertyIdSet setUnion = first | second;
const PropertyIdSet intersection = first & second;
EXPECT_EQ( first.size(), 3u );
EXPECT_TRUE( setUnion.contains( 10 ) );
EXPECT_TRUE( setUnion.contains( 20 ) );
EXPECT_TRUE( setUnion.contains( 30 ) );
EXPECT_TRUE( setUnion.contains( 40 ) );
EXPECT_TRUE( setUnion.contains( id( 10 ) ) );
EXPECT_TRUE( setUnion.contains( id( 20 ) ) );
EXPECT_TRUE( setUnion.contains( id( 30 ) ) );
EXPECT_TRUE( setUnion.contains( id( 40 ) ) );
EXPECT_EQ( intersection.size(), 1u );
EXPECT_TRUE( intersection.contains( 20 ) );
EXPECT_TRUE( intersection.contains( id( 20 ) ) );
PropertyIdSet sameAsFirst;
sameAsFirst.insert( 20 );
sameAsFirst.insert( 30 );
sameAsFirst.insert( 10 );
sameAsFirst.insert( id( 20 ) );
sameAsFirst.insert( id( 30 ) );
sameAsFirst.insert( id( 10 ) );
EXPECT_TRUE( first == sameAsFirst );
first &= second;
EXPECT_TRUE( first == intersection );
first.erase( 20 );
first.erase( id( 20 ) );
EXPECT_TRUE( first.empty() );
}
UTEST( PropertyIdSet, selfUnionAndIntersection ) {
PropertyIdSet a;
a.insert( id( 7 ) );
a.insert( id( 9 ) );
const PropertyIdSet original = a;
a |= a;
EXPECT_TRUE( a == original );
a &= a;
EXPECT_TRUE( a == original );
EXPECT_EQ( a.size(), 2u );
}
UTEST( PropertyIdSet, iteratorErase ) {
PropertyIdSet ids;
ids.insert( 10 );
ids.insert( 20 );
ids.insert( 30 );
ids.insert( id( 10 ) );
ids.insert( id( 20 ) );
ids.insert( id( 30 ) );
for ( auto it = ids.begin(); it != ids.end(); ) {
if ( *it == 20 )
if ( *it == id( 20 ) )
it = ids.erase( it );
else
++it;
}
EXPECT_EQ( ids.size(), 2u );
EXPECT_FALSE( ids.contains( 20 ) );
EXPECT_FALSE( ids.contains( id( 20 ) ) );
EXPECT_TRUE( ids.contains( id( 10 ) ) );
EXPECT_TRUE( ids.contains( id( 30 ) ) );
}
UTEST( PropertyIdSet, iteratorEraseEveryElement ) {
PropertyIdSet ids;
for ( std::size_t i = 1; i <= 10; ++i )
ids.insert( id( i ) );
for ( auto it = ids.begin(); it != ids.end(); ) {
it = ids.erase( it );
}
EXPECT_TRUE( ids.empty() );
}
UTEST( PropertyIdSet, ascendingIteration ) {
PropertyIdSet ids;
ids.insert( id( 40 ) );
ids.insert( id( 10 ) );
ids.insert( id( 30 ) );
ids.insert( id( 20 ) );
std::vector<PropertyId> seen;
for ( auto prop : ids )
seen.push_back( prop );
ASSERT_EQ( seen.size(), 4u );
EXPECT_TRUE( seen[0] == id( 10 ) );
EXPECT_TRUE( seen[1] == id( 20 ) );
EXPECT_TRUE( seen[2] == id( 30 ) );
EXPECT_TRUE( seen[3] == id( 40 ) );
}
UTEST( PropertyIdSet, invalidAndBoundaryBehavior ) {
PropertyIdSet ids;
ids.insert( PropertyId::Invalid );
ids.erase( PropertyId::Invalid );
EXPECT_FALSE( ids.contains( PropertyId::Invalid ) );
EXPECT_TRUE( ids.empty() );
ids.insert( id( 511 ) );
EXPECT_TRUE( ids.contains( id( 511 ) ) );
ids.erase( id( 511 ) );
EXPECT_TRUE( ids.empty() );
}
UTEST( PropertyIdSet, clearedSetReusesStorage ) {
PropertyIdSet ids;
for ( Uint32 id = 0; id < 16; ++id )
ids.insert( id );
for ( std::size_t i = 1; i <= 16; ++i )
ids.insert( id( i ) );
ids.clear();
const size_t memoryBefore = MemoryManager::getTotalMemoryUsage();
for ( Uint32 id = 0; id < 16; ++id )
ids.insert( id );
for ( std::size_t i = 1; i <= 16; ++i )
ids.insert( id( i ) );
const size_t memoryAfter = MemoryManager::getTotalMemoryUsage();
EXPECT_EQ( ids.size(), 16u );
EXPECT_EQ( memoryAfter, memoryBefore );
}
UTEST( PropertyIdSet, noAllocationOnConstruction ) {
const size_t memoryBefore = MemoryManager::getTotalMemoryUsage();
PropertyIdSet ids;
const size_t memoryAfter = MemoryManager::getTotalMemoryUsage();
EXPECT_EQ( memoryAfter, memoryBefore );
EXPECT_TRUE( ids.empty() );
}
UTEST( PropertyIdSet, copyAndMovePreserveBits ) {
PropertyIdSet source;
source.insert( id( 3 ) );
source.insert( id( 8 ) );
const size_t memoryBefore = MemoryManager::getTotalMemoryUsage();
PropertyIdSet copy( source );
PropertyIdSet moved( std::move( source ) );
const size_t memoryAfter = MemoryManager::getTotalMemoryUsage();
EXPECT_EQ( memoryAfter, memoryBefore );
EXPECT_EQ( copy.size(), 2u );
EXPECT_EQ( moved.size(), 2u );
EXPECT_TRUE( copy == moved );
EXPECT_TRUE( copy.contains( id( 3 ) ) );
EXPECT_TRUE( copy.contains( id( 8 ) ) );
}
UTEST( PropertyIdSet, customIdsBehaveLikeBuiltins ) {
PropertyIdSet ids;
ids.insert( id( 400 ) );
ids.insert( id( 401 ) );
EXPECT_TRUE( ids.contains( id( 400 ) ) );
EXPECT_TRUE( ids.contains( id( 401 ) ) );
EXPECT_EQ( ids.size(), 2u );
PropertyIdSet other;
other.insert( id( 401 ) );
other.insert( id( 500 ) );
const PropertyIdSet result = ids | other;
EXPECT_TRUE( result.contains( id( 400 ) ) );
EXPECT_TRUE( result.contains( id( 401 ) ) );
EXPECT_TRUE( result.contains( id( 500 ) ) );
}
UTEST( PropertyIdSet, objectSizeIsFixed ) {
EXPECT_EQ( sizeof( PropertyIdSet ), 64u );
}
UTEST( PropertyIdSet, checkedCompatibilityAliasUsesSelectedIdentity ) {
EXPECT_TRUE( PropertyId::Checked == PropertyId::Selected );
}
UTEST( IdNameMap, builtinsAliasesAndCustomIds ) {
IdNameMap<TestDenseId, 5> ids;
EXPECT_TRUE( ids.addBuiltin( TestDenseId::One, "one" ) );
EXPECT_TRUE( ids.addBuiltin( TestDenseId::Two, "two" ) );
EXPECT_TRUE( ids.addAlias( "first", TestDenseId::One ) );
EXPECT_TRUE( ids.finalizeBuiltins() );
EXPECT_TRUE( ids.getId( "one" ) == TestDenseId::One );
EXPECT_TRUE( ids.getId( "first" ) == TestDenseId::One );
EXPECT_TRUE( ids.getName( TestDenseId::One ) == "one" );
EXPECT_TRUE( ids.getId( "unknown" ) == TestDenseId::Invalid );
const TestDenseId customOne = ids.getOrCreateId( "custom-one" );
const TestDenseId customTwo = ids.getOrCreateId( "custom-two" );
EXPECT_EQ( static_cast<Uint8>( customOne ), 3u );
EXPECT_EQ( static_cast<Uint8>( customTwo ), 4u );
EXPECT_TRUE( ids.getOrCreateId( "custom-one" ) == customOne );
EXPECT_TRUE( ids.getOrCreateId( "over-capacity" ) == TestDenseId::Invalid );
EXPECT_FALSE( ids.addBuiltin( static_cast<TestDenseId>( 4 ), "late-builtin" ) );
}
UTEST( IdNameMap, failedFinalizationDoesNotEnableCustomIds ) {
IdNameMap<TestDenseId, 5> ids;
EXPECT_TRUE( ids.addBuiltin( TestDenseId::One, "one" ) );
EXPECT_FALSE( ids.finalizeBuiltins() );
EXPECT_TRUE( ids.getOrCreateId( "too-early" ) == TestDenseId::Invalid );
EXPECT_TRUE( ids.addBuiltin( TestDenseId::Two, "two" ) );
EXPECT_TRUE( ids.finalizeBuiltins() );
EXPECT_EQ( static_cast<Uint8>( ids.getOrCreateId( "custom" ) ), 3u );
}
@@ -31,6 +31,19 @@ using namespace EE::UI::CSS;
using namespace EE::Scene;
using namespace EE::Graphics;
UTEST( CSSParser, UnknownPropertiesKeepDistinctNameHashes ) {
StyleSheetProperty first( "data-first", "one" );
StyleSheetProperty second( "data-second", "two" );
EXPECT_TRUE( first.getPropertyDefinition() == nullptr );
EXPECT_TRUE( second.getPropertyDefinition() == nullptr );
EXPECT_EQ( first.getId(), String::hash( "data-first" ) );
EXPECT_EQ( second.getId(), String::hash( "data-second" ) );
EXPECT_NE( first.getId(), second.getId() );
EXPECT_TRUE( first.getPropertyId() == PropertyId::Invalid );
EXPECT_TRUE( first.getShorthandId() == ShorthandId::Invalid );
}
UTEST( CSSParser, CommentsPreserveDeclarationContext ) {
StyleSheetPropertiesParser parser( R"css(
color /* before colon */ : red;
@@ -41,12 +54,18 @@ UTEST( CSSParser, CommentsPreserveDeclarationContext ) {
)css" );
const auto& properties = parser.getProperties();
auto lineHeight = properties.find( static_cast<Uint32>( PropertyId::LineHeight ) );
auto color = properties.find( static_cast<Uint32>( PropertyId::Color ) );
auto marginTop = properties.find( static_cast<Uint32>( PropertyId::MarginTop ) );
auto marginRight = properties.find( static_cast<Uint32>( PropertyId::MarginRight ) );
auto fontFamily = properties.find( static_cast<Uint32>( PropertyId::FontFamily ) );
auto width = properties.find( static_cast<Uint32>( PropertyId::Width ) );
auto lineHeight = properties.find(
StyleSheetSpecification::instance()->getProperty( PropertyId::LineHeight )->getId() );
auto color = properties.find(
StyleSheetSpecification::instance()->getProperty( PropertyId::Color )->getId() );
auto marginTop = properties.find(
StyleSheetSpecification::instance()->getProperty( PropertyId::MarginTop )->getId() );
auto marginRight = properties.find(
StyleSheetSpecification::instance()->getProperty( PropertyId::MarginRight )->getId() );
auto fontFamily = properties.find(
StyleSheetSpecification::instance()->getProperty( PropertyId::FontFamily )->getId() );
auto width = properties.find(
StyleSheetSpecification::instance()->getProperty( PropertyId::Width )->getId() );
ASSERT_TRUE( lineHeight != properties.end() );
ASSERT_TRUE( color != properties.end() );
+145 -14
View File
@@ -393,23 +393,29 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
UIWidget* content =
siteTable->getParent()->isWidget() ? siteTable->getParent()->asType<UIWidget>() : nullptr;
ASSERT_TRUE( content != nullptr );
Vector2f sidePos = side->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f contentPos = content->convertToWorldSpace( { 0, 0 } );
Vector2f midcolPos = midcol->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f entryPos = entry->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f arrowPos = arrow->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srHeaderPos = srHeader->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f redesignButtonPos =
redesignButton->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srDropPos = srDrop->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srListPos = srList->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srHeaderPos = srHeader->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srDropPos = srDrop->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f srFlatListPos = srFlatList->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f headerBottomLeftPos =
headerBottomLeft->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f headerBottomRightPos =
headerBottomRight->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f headerPos = header->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentHelpTogglePos =
commentHelpToggle->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentContentPolicyPos =
commentContentPolicy->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentEditPos = commentEdit->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
/* Vector2f sidePos = side->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f entryPos = entry->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f contentPos = content->convertToWorldSpace( { 0, 0 } );
Vector2f redesignButtonPos =
redesignButton->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f dropChoicesPos = dropChoices->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f selftextMdPos = selftextMd->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f selftextFirstPPos =
@@ -420,13 +426,8 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
Vector2f postUsertextPos = postUsertext->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentAreaPos = commentArea->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentFormPos = commentForm->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentEditPos = commentEdit->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentTextareaPos =
commentTextarea->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentHelpTogglePos =
commentHelpToggle->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
Vector2f commentContentPolicyPos =
commentContentPolicy->asType<UIWidget>()->convertToWorldSpace( { 0, 0 } );
auto fontSizeOf = []( Node* node ) -> Uint32 {
return node->isType( UI_TYPE_RICHTEXT ) ? node->asType<UIRichText>()->getFontSize() : 0;
@@ -527,7 +528,7 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
<< " " << commentHelpToggle->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< commentHelpToggle->asType<UIWidget>()->getPixelsSize().getHeight() << ")"
<< std::endl;
*/
const Float midcolCenter =
midcolPos.x + midcol->asType<UIWidget>()->getPixelsSize().getWidth() / 2.f;
const Float arrowCenter =
@@ -6449,3 +6450,133 @@ UTEST( UIHTML, ImageMaxWidthConstrainsWebpWithHeightAuto ) {
Engine::destroySingleton();
}
// Applies width/height/max-width to an image inside a single attributes
// transaction in the given order, then returns the final pixels size. The reset
// pass restores the image to a known base state (no fixed dimensions and no
// max-width constraint) before the target order is applied. Returns Sizef::Zero
// when the fixture image cannot be found.
static Sizef applyImageSizingOrder( UISceneNode* sceneNode,
const std::vector<std::string>& order ) {
auto images = sceneNode->getRoot()->findAllByTag( "img" );
if ( images.size() != 1 )
return Sizef::Zero;
auto* img = images[0]->asType<UIHTMLImage>();
if ( !img )
return Sizef::Zero;
img->beginAttributesTransaction();
img->applyProperty( StyleSheetProperty( "width", "auto" ) );
img->applyProperty( StyleSheetProperty( "height", "auto" ) );
img->applyProperty( StyleSheetProperty( "max-width", "none" ) );
img->endAttributesTransaction();
img->updateLayout();
img->beginAttributesTransaction();
for ( const auto& name : order ) {
if ( name == "width" )
img->applyProperty( StyleSheetProperty( "width", "2560px" ) );
else if ( name == "height" )
img->applyProperty( StyleSheetProperty( "height", "auto" ) );
else
img->applyProperty( StyleSheetProperty( "max-width", "100%" ) );
}
img->endAttributesTransaction();
img->updateLayout();
return img->getPixelsSize();
}
UTEST( UIHTML, ImagePropertyApplicationOrderIndependent ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 768, "img sizing property order", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
UISceneNode* sceneNode = init_test_inline_block();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
const std::string html = R"html(
<!doctype html>
<html>
<head><style>body { margin: 0; } div { width: 800px; }</style></head>
<body><div><img src="image_width_3.webp" width="2560" height="1436"></div></body>
</html>
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
win->getInput()->update();
SceneManager::instance()->update();
sceneNode->updateDirtyLayouts();
const Sizef base = applyImageSizingOrder( sceneNode, { "width", "height", "max-width" } );
EXPECT_GT( base.getWidth(), 0.f );
EXPECT_GT( base.getHeight(), 0.f );
// max-width:100% constrains the image to the 800px containing block, and
// height:auto keeps the 1436/2560 aspect ratio.
EXPECT_LE( base.getWidth(), 800.f );
EXPECT_NEAR( base.getHeight() / base.getWidth(), 1436.f / 2560.f, 0.01f );
const std::vector<std::vector<std::string>> orders = {
{ "max-width", "height", "width" },
{ "height", "width", "max-width" },
{ "max-width", "width", "height" },
};
for ( const auto& order : orders ) {
const Sizef other = applyImageSizingOrder( sceneNode, order );
EXPECT_NEAR( other.getWidth(), base.getWidth(), 1.f );
EXPECT_NEAR( other.getHeight(), base.getHeight(), 1.f );
}
Engine::destroySingleton();
}
UTEST( UIHTML, ImageHeightAutoMaxWidthStyleStateTransition ) {
auto win = Engine::instance()->createWindow(
WindowSettings( 1024, 768, "img style state transition", WindowStyle::Default,
WindowBackend::Default, 32, {}, 1, false, true ),
ContextSettings( false, 0, 0, GLv_default, true, false ) );
UISceneNode* sceneNode = init_test_inline_block();
sceneNode->setURI( "file://" + Sys::getProcessPath() + "assets/html/" );
const std::string html = R"html(
<!doctype html>
<html>
<head><style>body { margin: 0; } div { width: 800px; }</style></head>
<body><div><img src="image_width_3.webp" width="2560" height="1436"></div></body>
</html>
)html";
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
win->getInput()->update();
SceneManager::instance()->update();
sceneNode->updateDirtyLayouts();
auto images = sceneNode->getRoot()->findAllByTag( "img" );
ASSERT_EQ( images.size(), (size_t)1 );
auto* img = images[0]->asType<UIHTMLImage>();
ASSERT_TRUE( img != nullptr );
// HTML attributes alone keep the fixed 2560x1436 box.
EXPECT_NEAR( img->getPixelsSize().getWidth(), 2560.f, 1.f );
EXPECT_NEAR( img->getPixelsSize().getHeight(), 1436.f, 1.f );
const std::string rule = "img { height: auto; max-width: 100%; }";
// Apply height:auto + max-width:100% through a style state change.
sceneNode->setStyleSheet( rule );
sceneNode->updateDirtyLayouts();
const Float constrainedWidth = img->getPixelsSize().getWidth();
const Float constrainedHeight = img->getPixelsSize().getHeight();
EXPECT_LE( constrainedWidth, 800.f );
EXPECT_NEAR( constrainedHeight / constrainedWidth, 1436.f / 2560.f, 0.01f );
// Remove the rule, then reapply it: the final geometry must be identical.
sceneNode->setStyleSheet( CSS::StyleSheet() );
sceneNode->updateDirtyLayouts();
sceneNode->setStyleSheet( rule );
sceneNode->updateDirtyLayouts();
EXPECT_NEAR( img->getPixelsSize().getWidth(), constrainedWidth, 1.f );
EXPECT_NEAR( img->getPixelsSize().getHeight(), constrainedHeight, 1.f );
Engine::destroySingleton();
}