Grid layout WIP.

This commit is contained in:
Martín Lucas Golini
2026-06-01 02:14:00 -03:00
parent f0c631fa8a
commit 46b3fa44f0
16 changed files with 4364 additions and 36 deletions

View File

@@ -860,3 +860,82 @@ Do not checkpoint a failing phase as complete.
- Existing flex, table, block, inline, and out-of-flow layout tests still pass.
- `.agent/rules/html-layout-architecture.md` is updated with `GridLayouter` support and any
intentional limitations.
## Bug Tracking
### FIXED (34)
| # | Bug | Test |
|---|-----|------|
| 1 | Stack overflow: `setPixelsSize` on container → re-layout cycle | `UIHTML.GridDownloadLayout` |
| 2 | Missing `mPacking` guard + transaction | `UIHTML.GridDownloadLayout` |
| 3 | `parseTrackList` didn't propagate `hasAutoRepeat`/`autoRepeatTemplate` | `GridParser.autoRepeatExpand` |
| 4 | `expandAutoRepeat` count from max only (ignored `minmax` min) | `GridContainer.autoFitColumnCount` |
| 5 | `expandAutoRepeat` never called from `readContainerStyle` | `GridContainer.autoFitColumnCount` |
| 6 | Items filled parent width: `MatchParent` → fill containing block | `GridContainer.itemWidthStretch` |
| 7 | Items uneven height: `WrapContent` height → each card had its own height | `GridContainer.itemHeightStretch` |
| 8 | `sizeTracksForAxis` uses `mColumnGap` for both axes | `GridContainer.rowGapWorks` |
| 9 | Unsigned underflow `tracks.size()-1` when empty | `GridContainer.emptyGridNoCrash` |
| 10 | Multi-span `cellW`/`cellH` missing internal gaps | `GridContainer.spanningItemWidthIncludesGaps` |
| 11 | `justify-content` space-between/around/evenly as uniform offset | `GridContainer.justifyContentSpaceBetween` |
| 12 | `expandAutoRepeat` appends to end (wrong insertion pos) | `GridParser.autoRepeatPositionInsert` |
| 13 | Row axis auto-fit collapse never implemented | `GridContainer.autoFitRowCount` |
| 14 | Negative line overflow not clamped to 1 | `GridContainer.negativeLineClamped` |
| 15 | P1: Negative `fr` values accepted (`-1fr`) | `GridParser.rejectNegativeFr` |
| 16 | P2: `isNumber()` accepts malformed decimals `12.34.56` | (unit testable) |
| 17 | P3: Nested auto-repeat in fixed-repeat silently drops tracks | (unit testable) |
| 18 | P4: Auto-repeat zero-min tracks collapses to count=1 | `GridContainer.autoRepeatZeroMinFillsContainer` |
| 19 | P7: Custom-idents starting with "span" misinterpreted | (unit testable) |
| 20 | PL1+PL4: Span on definite axis ignored for semi-auto items | `GridContainer.definiteColumnSpanAutoRow` |
| 21 | PL2: Dense mode missing in both-auto placement | `GridContainer.denseAutoPlacement` |
| 22 | PL3: `colLimit` static throughout loop | (implicitly tested) |
| 23 | PL5: Both span drops start span (now uses end span) | (unit testable) |
| 24 | PL6: Named grid lines never resolved | (unit testable) |
| 25 | TS1: Percentage `max` treated as infinite growth limit | (unit testable) |
| 26 | TS2: `FitContent` max treated as infinite growth limit | (unit testable) |
| 27 | TS4: Flex distribution never clamps to growth limits | `GridContainer.flexGrowthLimit` |
| 28 | TS6: Spanning extra clamped to growth limit w/o redistribute | (unit testable) |
| 29 | TS7: `grid-auto-rows`/`grid-auto-columns` properties ignored | `GridContainer.implicitTrackSizing` |
| 30 | I1: `computeIntrinsicWidths` ignores auto-repeat tracks | `GridContainer.intrinsicWidthAutoRepeat` |
| 31 | I2: `computeIntrinsicWidths` uses uninitialized `mColumnGap` | `GridContainer.intrinsicWidthAutoRepeat` |
| 32 | L2: `collapseEmptyAutoFitTracks` may remove implicit tracks | (unit testable) |
| 33 | L3: Track sizing adds gaps for content but cells omit them | `GridContainer.spanningItemWidthIncludesGaps` |
| 34 | TS3: Content-based `max` ignored for `minmax(fixed, auto)` | (implicitly fixed) |
### NOT FIXED (9)
#### Parser (2)
| # | Sev | Bug | File:Line |
|---|-----|-----|-----------|
| P5 | L | Invalid tokens silently become `Length(0)` | `gridlayouter.cpp:112-122` |
| P6 | L | Malformed bracket tokens flow to function parsing | `gridlayouter.cpp:265-271` |
#### Layout (3)
| # | Sev | Bug | File:Line |
|---|-----|-----|-----------|
| L1 | H | `preSizeItemsForRowSizing` doesn't reflow content after width change | `gridlayouter.cpp:661-691` |
| L4 | M | Baseline computed as bottom edge instead of text baseline | `gridlayouter.cpp:871-878` |
| L5 | L | `mPacking` may silently skip needed re-layout | `gridlayouter.cpp:856` |
#### Track Sizing (1)
| # | Sev | Bug | File:Line |
|---|-----|-----|-----------|
| TS5 | L | Floating-point error from iterative subtraction in flex distribution | `gridlayouter.cpp:657-669` |
#### Intrinsic / Edge (3)
| # | Sev | Bug | File:Line |
|---|-----|-----|-----------|
| I3 | L | Auto-repeat expanded once with potentially zero container size | `gridlayouter.cpp:460-469` |
| I4 | L | Span stored in `resolvedEnd` (overloaded, works but fragile) | `gridlayouter.cpp:1009-1024` |
| I5 | L | Negative `contentBoxSize` not handled | `gridlayouter.cpp:486-492` |
### Stats
- **Total bugs found**: 43
- **Fixed**: 34
- **Unfixed**: 9
- **By severity**: 1 High, 1 Medium, 7 Low (remaining)

View File

@@ -0,0 +1,217 @@
<!doctype html>
<!-- saved from url=(0018)https://zsnes.com/ -->
<html lang="en">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
background-color: #1e2a38;
font:
13px Verdana,
Sans-Serif;
color: #d0d8e0;
line-height: 1.5;
}
a {
color: #60c0e0;
text-decoration: underline;
}
a:hover {
background-color: #2a3a4a;
color: #80d8f0;
}
header {
text-align: center;
padding: 30px 20px 10px;
}
header img.logo {
max-width: 400px;
height: auto;
}
nav {
background-color: #162028;
text-align: center;
padding: 8px;
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid #2a3a4a;
}
nav a {
margin: 0 10px;
font:
bold 11px Verdana,
Sans-Serif;
text-decoration: none;
color: #80b8d0;
}
nav a:hover {
background-color: #2a3a4a;
color: #a0d8f0;
}
main {
max-width: 960px;
margin: 20px auto;
padding: 0 20px;
}
section {
margin-bottom: 30px;
}
h2 {
background-color: #162028;
color: #60c0e0;
font:
bold 14px Arial,
Sans-Serif;
padding: 5px 8px;
border-left: 1px solid #2a3a4a;
border-top: 1px solid #2a3a4a;
}
section p,
section ul {
background-color: #1a2838;
padding: 10px;
border-left: 1px solid #2a3a4a;
border-right: 1px solid #2a3a4a;
}
section p {
text-indent: 12px;
}
section ul {
padding-left: 20px;
}
section ul li {
margin-bottom: 6px;
}
strong {
color: #e0b800;
}
.download-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
}
.download-card {
background-color: #1a2838;
border: 1px solid #2a3a4a;
padding: 12px;
text-align: center;
}
.download-card h3 {
font:
bold 12px Verdana,
Sans-Serif;
margin-bottom: 8px;
}
.download-card a {
display: inline-block;
padding: 6px 14px;
background-color: #162028;
color: #60c0e0;
text-decoration: none;
font:
11px Verdana,
Sans-Serif;
border: 1px solid #2a3a4a;
}
.download-card a:hover {
background-color: #2a3a4a;
color: #80d8f0;
}
.download-card .pending {
color: #8090a0;
font-style: italic;
}
footer {
text-align: center;
padding: 20px;
font:
11px Verdana,
Sans-Serif;
color: #607080;
}
@media (max-width: 600px) {
header img.logo {
max-width: 280px;
}
nav {
display: flex;
flex-wrap: wrap;
justify-content: center;
}
nav a {
display: block;
margin: 4px 6px;
}
}
</style>
</head>
<body>
<main>
<section id="downloads">
<h2>Downloads</h2>
<div class="download-grid">
<div class="download-card">
<h3>Windows</h3>
<a href="https://zsnes.com/files/SuperZSNES_v0.201.zip">Download</a>
</div>
<div class="download-card">
<h3>Mac</h3>
<a href="https://zsnes.com/files/SuperZSNES_v0.201.dmg">Download</a>
</div>
<div class="download-card">
<h3>Linux</h3>
<a href="https://zsnes.com/files/SuperZSNES_v0.201.tar.gz"
>Download</a
>
</div>
<div class="download-card">
<h3>Android</h3>
<a
href="https://play.google.com/store/apps/details?id=com.zsnes.superzsnes"
target="_blank"
rel="noopener"
>Google Play</a
>
</div>
<div class="download-card">
<h3>iOS</h3>
<span class="pending">Coming Soon</span>
</div>
</div>
</section>
</main>
</body>
</html>

View File

@@ -288,6 +288,23 @@ enum class PropertyId : Uint32 {
RowGap = String::hash( "row-gap" ),
ColumnGap = String::hash( "column-gap" ),
Gap = String::hash( "gap" ),
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" ),
};
enum class PropertyType : Uint32 {

View File

@@ -14,6 +14,8 @@ enum class CSSDisplay {
ListItem,
Flex,
InlineFlex,
Grid,
InlineGrid,
None,
Table,
TableRow,
@@ -162,6 +164,27 @@ struct EE_API CSSBaselineAlignmentHelper {
static CSSBaselineAlignValue fromKeyword( std::string_view val );
};
enum class CSSGridAutoFlow { Row, Column };
struct EE_API CSSGridAutoFlowHelper {
static std::string toString( CSSGridAutoFlow val );
static CSSGridAutoFlow fromString( std::string_view val );
};
enum class CSSJustifyItems { Normal, Stretch, Start, End, Center, FlexStart, FlexEnd };
struct EE_API CSSJustifyItemsHelper {
static std::string toString( CSSJustifyItems val );
static CSSJustifyItems fromString( std::string_view val );
};
enum class CSSJustifySelf { Auto, Normal, Stretch, Start, End, Center, FlexStart, FlexEnd };
struct EE_API CSSJustifySelfHelper {
static std::string toString( CSSJustifySelf val );
static CSSJustifySelf fromString( std::string_view val );
};
enum class CSSVisibility { Visible, Hidden, Collapse };
struct EE_API CSSVisibilityHelper {

View File

@@ -0,0 +1,170 @@
#ifndef EE_UI_GRIDLAYOUTER_HPP
#define EE_UI_GRIDLAYOUTER_HPP
#include <eepp/core/containers.hpp>
#include <eepp/core/small_vector.hpp>
#include <eepp/math/rect.hpp>
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/csslayouttypes.hpp>
#include <eepp/ui/uihelper.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <limits>
namespace EE { namespace UI {
class UIWidget;
class UIHTMLWidget;
// ── Structured grid track data model (used by GridLayouter and tests) ──
enum class GridTrackBreadthType {
Length,
Percentage,
Flex,
Auto,
MinContent,
MaxContent,
FitContent
};
struct EE_API GridTrackBreadth {
GridTrackBreadthType type{ GridTrackBreadthType::Auto };
Float value{ 0.f };
std::string raw{ "auto" };
bool valid{ true };
};
struct EE_API GridTrackSize {
GridTrackBreadth min;
GridTrackBreadth max;
};
struct EE_API GridExplicitTrack {
SmallVector<std::string, 2> beforeLineNames;
GridTrackSize size;
};
struct EE_API GridTrackList {
std::vector<GridExplicitTrack> tracks;
bool none{ false };
bool valid{ true };
bool hasAutoRepeat{ false };
bool autoRepeatIsFit{ false };
size_t autoRepeatPosition{ 0 };
std::vector<GridExplicitTrack> autoRepeatTemplate;
};
struct EE_API GridTrackParser {
static GridTrackList parseTrackList( const std::string& value );
static bool isValidTrackList( const std::string& value );
static void expandAutoRepeat( GridTrackList& list, Float availableSize, Float gap );
};
struct EE_API GridTrack {
GridTrackSize definition;
Float baseSize{ 0.f };
Float growthLimit{ std::numeric_limits<Float>::infinity() };
bool isFlex{ false };
};
struct EE_API GridItem {
UIWidget* widget;
int order{ 0 };
std::string gridRowStart{ "auto" };
std::string gridRowEnd{ "auto" };
std::string gridColumnStart{ "auto" };
std::string gridColumnEnd{ "auto" };
int resolvedRowStart{ 0 }; // 0 = auto
int resolvedRowEnd{ 0 };
int resolvedColumnStart{ 0 };
int resolvedColumnEnd{ 0 };
CSSJustifySelf justifySelf{ CSSJustifySelf::Stretch };
CSSAlignSelf alignSelf{ CSSAlignSelf::Stretch };
bool isTextNode{ false };
bool isOutOfFlow{ false };
};
// ── Parser helper exposed for tests ──
struct EE_API GridAreaTemplate {
struct Area {
int startRow{ 1 };
int startColumn{ 1 };
int endRow{ 2 };
int endColumn{ 2 };
};
UnorderedMap<std::string, Area> areas;
bool valid{ true };
bool none{ false };
};
struct EE_API GridAreasParser {
static GridAreaTemplate parseAreas( const std::string& value );
static bool isValidAreas( const std::string& value );
static std::vector<std::string> getLineNamesForRowLine( const GridAreaTemplate& tmpl, int line );
static std::vector<std::string> getLineNamesForColumnLine( const GridAreaTemplate& tmpl,
int line );
};
struct EE_API GridLineResult {
bool isAuto{ true };
bool isSpan{ false };
int lineNumber{ 0 };
int spanCount{ 1 };
std::string nameId;
};
struct EE_API GridLineParser {
static GridLineResult parse( const std::string& value );
};
// ── Layouter ──
class EE_API GridLayouter : public UILayouter {
public:
GridLayouter( UIWidget* container ) : UILayouter( container ) {}
void updateLayout() override;
void computeIntrinsicWidths() override;
Float getMinIntrinsicWidth() override;
Float getMaxIntrinsicWidth() override;
Float getBaseline() const { return mContainerBaseline; }
const SmallVector<GridItem, 16>& getItems() const { return mItems; }
const std::vector<GridTrack>& getColumns() const { return mColumns; }
protected:
void readContainerStyle();
void collectGridItems();
void resolveDefinitePlacements();
void autoPlaceItems();
void buildImplicitTracks();
void collapseEmptyAutoFitTracks();
void preSizeItemsForRowSizing();
void sizeTracksForAxis( bool isColumns );
void applyLayout();
SmallVector<GridItem, 16> mItems;
std::vector<GridTrack> mColumns;
std::vector<GridTrack> mRows;
GridAreaTemplate mTemplateAreas;
Float mColumnGap{ 0.f };
Float mRowGap{ 0.f };
Float mContainerBaseline{ 0.f };
int mMaxColumn{ 0 };
int mMaxRow{ 0 };
bool mAutoFlowDense{ false };
bool mColAutoRepeatIsFit{ false };
bool mRowAutoRepeatIsFit{ false };
GridTrackSize mAutoColumnSize;
GridTrackSize mAutoRowSize;
size_t mExplicitColCount{ 0 };
size_t mExplicitRowCount{ 0 };
UnorderedMap<std::string, std::vector<int>> mColLineNames;
UnorderedMap<std::string, std::vector<int>> mRowLineNames;
};
}} // namespace EE::UI
#endif

View File

@@ -27,6 +27,23 @@ struct UIHTMLWidgetFlexState {
std::string columnGap{ "0" };
};
struct UIHTMLWidgetGridState {
std::string templateRows{ "none" };
std::string templateColumns{ "none" };
std::string templateAreas{ "none" };
std::string autoRows{ "auto" };
std::string autoColumns{ "auto" };
CSSGridAutoFlow autoFlow{ CSSGridAutoFlow::Row };
bool autoFlowDense{ false };
std::string rowStart{ "auto" };
std::string rowEnd{ "auto" };
std::string columnStart{ "auto" };
std::string columnEnd{ "auto" };
std::string area{ "auto" };
CSSJustifyItems justifyItems{ CSSJustifyItems::Normal };
CSSJustifySelf justifySelf{ CSSJustifySelf::Auto };
};
class EE_API UIHTMLWidget : public UILayout {
public:
static UIHTMLWidget* New();
@@ -51,6 +68,8 @@ class EE_API UIHTMLWidget : public UILayout {
bool isFlex() const;
bool isGrid() const;
CSSPosition getCSSPosition() const { return mPosition; }
void setCSSPosition( CSSPosition position );
@@ -89,6 +108,14 @@ class EE_API UIHTMLWidget : public UILayout {
return mFlexState;
}
UIHTMLWidgetGridState* getGridState() const { return mGridState; }
UIHTMLWidgetGridState* ensureGridState() {
if ( !mGridState )
mGridState = eeNew( UIHTMLWidgetGridState, () );
return mGridState;
}
CSSFlexDirection getFlexDirection() const {
return mFlexState ? mFlexState->direction : CSSFlexDirection::Row;
}
@@ -135,6 +162,84 @@ class EE_API UIHTMLWidget : public UILayout {
int getOrder() const { return mFlexState ? mFlexState->order : 0; }
void setOrder( int val );
const std::string& getGridTemplateRows() const {
static const std::string sDefault( "none" );
return mGridState ? mGridState->templateRows : sDefault;
}
void setGridTemplateRows( const std::string& val );
const std::string& getGridTemplateColumns() const {
static const std::string sDefault( "none" );
return mGridState ? mGridState->templateColumns : sDefault;
}
void setGridTemplateColumns( const std::string& val );
const std::string& getGridTemplateAreas() const {
static const std::string sDefault( "none" );
return mGridState ? mGridState->templateAreas : sDefault;
}
void setGridTemplateAreas( const std::string& val );
const std::string& getGridAutoRows() const {
static const std::string sDefault( "auto" );
return mGridState ? mGridState->autoRows : sDefault;
}
void setGridAutoRows( const std::string& val );
const std::string& getGridAutoColumns() const {
static const std::string sDefault( "auto" );
return mGridState ? mGridState->autoColumns : sDefault;
}
void setGridAutoColumns( const std::string& val );
CSSGridAutoFlow getGridAutoFlow() const {
return mGridState ? mGridState->autoFlow : CSSGridAutoFlow::Row;
}
void setGridAutoFlow( CSSGridAutoFlow val );
bool getGridAutoFlowDense() const { return mGridState ? mGridState->autoFlowDense : false; }
void setGridAutoFlowDense( bool val );
const std::string& getGridRowStart() const {
static const std::string sDefault( "auto" );
return mGridState ? mGridState->rowStart : sDefault;
}
void setGridRowStart( const std::string& val );
const std::string& getGridRowEnd() const {
static const std::string sDefault( "auto" );
return mGridState ? mGridState->rowEnd : sDefault;
}
void setGridRowEnd( const std::string& val );
const std::string& getGridColumnStart() const {
static const std::string sDefault( "auto" );
return mGridState ? mGridState->columnStart : sDefault;
}
void setGridColumnStart( const std::string& val );
const std::string& getGridColumnEnd() const {
static const std::string sDefault( "auto" );
return mGridState ? mGridState->columnEnd : sDefault;
}
void setGridColumnEnd( const std::string& val );
const std::string& getGridArea() const {
static const std::string sDefault( "auto" );
return mGridState ? mGridState->area : sDefault;
}
void setGridArea( const std::string& val );
CSSJustifyItems getJustifyItems() const {
return mGridState ? mGridState->justifyItems : CSSJustifyItems::Normal;
}
void setJustifyItems( CSSJustifyItems val );
CSSJustifySelf getJustifySelf() const {
return mGridState ? mGridState->justifySelf : CSSJustifySelf::Auto;
}
void setJustifySelf( CSSJustifySelf val );
const std::string& getRowGap() const {
static const std::string sDefault( "0" );
return mFlexState ? mFlexState->rowGap : sDefault;
@@ -220,6 +325,7 @@ class EE_API UIHTMLWidget : public UILayout {
bool mNeedsOrderSort{ false };
UILayouter* mLayouter{ nullptr };
UIHTMLWidgetFlexState* mFlexState{ nullptr };
UIHTMLWidgetGridState* mGridState{ nullptr };
UnorderedMap<std::string, StyleSheetProperty> mDataProperties;
Uint32 mScrollCb{ 0 };

View File

@@ -5,6 +5,8 @@
namespace EE { namespace UI {
class GridLayouter;
class EE_API UILayout : public UIWidget {
public:
virtual Uint32 getType() const;
@@ -28,6 +30,7 @@ class EE_API UILayout : public UIWidget {
void setLayoutDirty();
protected:
friend class GridLayouter;
friend class UISceneNode;
friend class UILayouter;

View File

@@ -34,6 +34,7 @@ class EE_API UINode : public Node {
public:
friend class BlockLayouter;
friend class FlexLayouter;
friend class GridLayouter;
friend class InlineLayouter;
friend class TableLayouter;
/**

View File

@@ -15,6 +15,10 @@ std::string CSSDisplayHelper::toString( CSSDisplay display ) {
return "flex";
case CSSDisplay::InlineFlex:
return "inline-flex";
case CSSDisplay::Grid:
return "grid";
case CSSDisplay::InlineGrid:
return "inline-grid";
case CSSDisplay::None:
return "none";
case CSSDisplay::Table:
@@ -61,6 +65,10 @@ CSSDisplay CSSDisplayHelper::fromString( std::string_view val ) {
display = CSSDisplay::Flex;
else if ( val == "inline-flex" )
display = CSSDisplay::InlineFlex;
else if ( val == "grid" )
display = CSSDisplay::Grid;
else if ( val == "inline-grid" )
display = CSSDisplay::InlineGrid;
return display;
}
@@ -444,6 +452,98 @@ CSSAlignSelf CSSAlignSelfHelper::fromString( std::string_view val ) {
return CSSAlignSelf::Auto;
}
std::string CSSGridAutoFlowHelper::toString( CSSGridAutoFlow val ) {
switch ( val ) {
case CSSGridAutoFlow::Column:
return "column";
case CSSGridAutoFlow::Row:
default:
return "row";
}
}
CSSGridAutoFlow CSSGridAutoFlowHelper::fromString( std::string_view val ) {
if ( val == "column" )
return CSSGridAutoFlow::Column;
return CSSGridAutoFlow::Row;
}
std::string CSSJustifyItemsHelper::toString( CSSJustifyItems val ) {
switch ( val ) {
case CSSJustifyItems::Stretch:
return "stretch";
case CSSJustifyItems::Start:
return "start";
case CSSJustifyItems::End:
return "end";
case CSSJustifyItems::Center:
return "center";
case CSSJustifyItems::FlexStart:
return "flex-start";
case CSSJustifyItems::FlexEnd:
return "flex-end";
case CSSJustifyItems::Normal:
default:
return "normal";
}
}
CSSJustifyItems CSSJustifyItemsHelper::fromString( std::string_view val ) {
if ( val == "stretch" )
return CSSJustifyItems::Stretch;
if ( val == "start" )
return CSSJustifyItems::Start;
if ( val == "end" )
return CSSJustifyItems::End;
if ( val == "center" )
return CSSJustifyItems::Center;
if ( val == "flex-start" )
return CSSJustifyItems::FlexStart;
if ( val == "flex-end" )
return CSSJustifyItems::FlexEnd;
return CSSJustifyItems::Normal;
}
std::string CSSJustifySelfHelper::toString( CSSJustifySelf val ) {
switch ( val ) {
case CSSJustifySelf::Stretch:
return "stretch";
case CSSJustifySelf::Start:
return "start";
case CSSJustifySelf::End:
return "end";
case CSSJustifySelf::Center:
return "center";
case CSSJustifySelf::FlexStart:
return "flex-start";
case CSSJustifySelf::FlexEnd:
return "flex-end";
case CSSJustifySelf::Normal:
return "normal";
case CSSJustifySelf::Auto:
default:
return "auto";
}
}
CSSJustifySelf CSSJustifySelfHelper::fromString( std::string_view val ) {
if ( val == "stretch" )
return CSSJustifySelf::Stretch;
if ( val == "start" )
return CSSJustifySelf::Start;
if ( val == "end" )
return CSSJustifySelf::End;
if ( val == "center" )
return CSSJustifySelf::Center;
if ( val == "flex-start" )
return CSSJustifySelf::FlexStart;
if ( val == "flex-end" )
return CSSJustifySelf::FlexEnd;
if ( val == "normal" )
return CSSJustifySelf::Normal;
return CSSJustifySelf::Auto;
}
std::string CSSVisibilityHelper::toString( CSSVisibility val ) {
switch ( val ) {
case CSSVisibility::Visible:

View File

@@ -511,6 +511,22 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerProperty( "row-gap", "0px" ).setType( PropertyType::NumberLength );
registerProperty( "column-gap", "0px" ).setType( PropertyType::NumberLength );
registerProperty( "grid-template-rows", "none" ).setType( PropertyType::String );
registerProperty( "grid-template-columns", "none" ).setType( PropertyType::String );
registerProperty( "grid-template-areas", "none" ).setType( PropertyType::String );
registerProperty( "grid-auto-rows", "auto" ).setType( PropertyType::String );
registerProperty( "grid-auto-columns", "auto" ).setType( PropertyType::String );
registerProperty( "grid-auto-flow", "row" ).setType( PropertyType::String );
registerProperty( "grid-row-start", "auto" ).setType( PropertyType::String );
registerProperty( "grid-row-end", "auto" ).setType( PropertyType::String );
registerProperty( "grid-column-start", "auto" ).setType( PropertyType::String );
registerProperty( "grid-column-end", "auto" ).setType( PropertyType::String );
registerProperty( "grid-row", "auto" ).setType( PropertyType::String );
registerProperty( "grid-column", "auto" ).setType( PropertyType::String );
registerProperty( "grid-area", "auto" ).setType( PropertyType::String );
registerProperty( "justify-items", "normal" ).setType( PropertyType::String );
registerProperty( "justify-self", "auto" ).setType( PropertyType::String );
// Shorthands
registerShorthand( "margin", { "margin-top", "margin-right", "margin-bottom", "margin-left" },
"box" );
@@ -584,6 +600,16 @@ void StyleSheetSpecification::registerDefaultProperties() {
registerShorthand( "flex-flow", { "flex-direction", "flex-wrap" }, "single-value-vector" );
registerShorthand( "flex", { "flex-grow", "flex-shrink", "flex-basis" }, "flex" );
registerShorthand( "gap", { "row-gap", "column-gap" }, "vector2" );
registerShorthand( "grid-template",
{ "grid-template-rows", "grid-template-columns", "grid-template-areas" },
"grid-template" );
registerShorthand( "grid",
{ "grid-template-rows", "grid-template-columns", "grid-template-areas",
"grid-auto-rows", "grid-auto-columns", "grid-auto-flow" },
"grid" );
registerShorthand( "place-items", { "align-items", "justify-items" }, "vector2" );
registerShorthand( "place-self", { "align-self", "justify-self" }, "vector2" );
registerShorthand( "place-content", { "align-content", "justify-content" }, "vector2" );
}
void StyleSheetSpecification::registerNodeSelector( const std::string& name,
@@ -1654,6 +1680,33 @@ void StyleSheetSpecification::registerDefaultShorthandParsers() {
return properties;
};
mShorthandParsers["grid-template"] =
[]( const ShorthandDefinition* shorthand,
std::string value ) -> std::vector<StyleSheetProperty> {
value = String::trim( value );
const auto& props = shorthand->getProperties();
if ( value == "none" )
return { StyleSheetProperty( props[0], "none" ), StyleSheetProperty( props[1], "none" ),
StyleSheetProperty( props[2], "none" ) };
// Split on '/' for rows vs columns
size_t slashPos = value.find( '/' );
std::string rowsAndAreas = String::trim( value.substr( 0, slashPos ) );
std::string cols = ( slashPos != std::string::npos )
? String::trim( value.substr( slashPos + 1 ) )
: "none";
return { StyleSheetProperty( props[0], rowsAndAreas ), StyleSheetProperty( props[1], cols ),
StyleSheetProperty( props[2], value ) };
};
mShorthandParsers["grid"] = []( const ShorthandDefinition* shorthand,
std::string value ) -> std::vector<StyleSheetProperty> {
value = String::trim( value );
if ( value == "none" )
return { StyleSheetProperty( "grid-template-rows", "none" ) };
return { StyleSheetProperty( "grid-template-rows", value ) };
};
}
}}} // namespace EE::UI::CSS

1605
src/eepp/ui/gridlayouter.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -78,6 +78,7 @@ UIHTMLWidget::~UIHTMLWidget() {
mScrollTarget->removeEventListener( mScrollCb );
eeSAFE_DELETE( mLayouter );
eeSAFE_DELETE( mFlexState );
eeSAFE_DELETE( mGridState );
}
UILayouter* UIHTMLWidget::getLayouter() {
@@ -114,11 +115,11 @@ void UIHTMLWidget::setDisplay( CSSDisplay display ) {
mNodeFlags |= NODE_FLAG_OVER_FIND_ALLOWED;
if ( mDisplay == CSSDisplay::InlineBlock || mDisplay == CSSDisplay::Inline ||
mDisplay == CSSDisplay::InlineFlex ) {
mDisplay == CSSDisplay::InlineFlex || mDisplay == CSSDisplay::InlineGrid ) {
if ( getLayoutWidthPolicy() == SizePolicy::MatchParent )
setLayoutWidthPolicy( SizePolicy::WrapContent );
} else if ( mDisplay == CSSDisplay::Block || mDisplay == CSSDisplay::ListItem ||
mDisplay == CSSDisplay::Flex ) {
mDisplay == CSSDisplay::Flex || mDisplay == CSSDisplay::Grid ) {
if ( getLayoutWidthPolicy() == SizePolicy::WrapContent &&
mPosition != CSSPosition::Absolute && mPosition != CSSPosition::Fixed )
setLayoutWidthPolicy( SizePolicy::MatchParent );
@@ -138,6 +139,10 @@ bool UIHTMLWidget::isFlex() const {
return mDisplay == CSSDisplay::Flex || mDisplay == CSSDisplay::InlineFlex;
}
bool UIHTMLWidget::isGrid() const {
return mDisplay == CSSDisplay::Grid || mDisplay == CSSDisplay::InlineGrid;
}
Float UIHTMLWidget::getBaseline() const {
if ( isFlex() && mLayouter ) {
auto* flex = reinterpret_cast<FlexLayouter*>( mLayouter );
@@ -223,37 +228,38 @@ void UIHTMLWidget::setNeedsOrderSort( bool val ) {
}
void UIHTMLWidget::drawChildren() {
bool needsFlexDirectionReverse = false;
if ( isFlex() ) {
CSSFlexDirection dir = getFlexDirection();
bool needsDirectionReverse =
needsFlexDirectionReverse =
dir == CSSFlexDirection::RowReverse || dir == CSSFlexDirection::ColumnReverse;
if ( mNeedsOrderSort || needsDirectionReverse ) {
std::vector<Node*> sortedChildren;
for ( Node* child = getFirstChild(); child; child = child->getNextNode() )
sortedChildren.push_back( child );
}
if ( mNeedsOrderSort ) {
std::stable_sort(
sortedChildren.begin(), sortedChildren.end(), []( Node* a, Node* b ) {
int aOrder = ( a->isWidget() && a->isType( UI_TYPE_HTML_WIDGET ) )
? static_cast<UIHTMLWidget*>( a )->getOrder()
: 0;
int bOrder = ( b->isWidget() && b->isType( UI_TYPE_HTML_WIDGET ) )
? static_cast<UIHTMLWidget*>( b )->getOrder()
: 0;
return aOrder < bOrder;
} );
}
if ( mNeedsOrderSort || needsFlexDirectionReverse ) {
std::vector<Node*> sortedChildren;
for ( Node* child = getFirstChild(); child; child = child->getNextNode() )
sortedChildren.push_back( child );
if ( needsDirectionReverse )
std::reverse( sortedChildren.begin(), sortedChildren.end() );
for ( auto* child : sortedChildren ) {
if ( child->isVisible() )
child->nodeDraw();
}
return;
if ( mNeedsOrderSort ) {
std::stable_sort( sortedChildren.begin(), sortedChildren.end(), []( Node* a, Node* b ) {
int aOrder = ( a->isWidget() && a->isType( UI_TYPE_HTML_WIDGET ) )
? static_cast<UIHTMLWidget*>( a )->getOrder()
: 0;
int bOrder = ( b->isWidget() && b->isType( UI_TYPE_HTML_WIDGET ) )
? static_cast<UIHTMLWidget*>( b )->getOrder()
: 0;
return aOrder < bOrder;
} );
}
if ( needsFlexDirectionReverse )
std::reverse( sortedChildren.begin(), sortedChildren.end() );
for ( auto* child : sortedChildren ) {
if ( child->isVisible() )
child->nodeDraw();
}
return;
}
Node::drawChildren();
}
@@ -354,6 +360,118 @@ void UIHTMLWidget::setColumnGap( const std::string& val ) {
}
}
void UIHTMLWidget::setGridTemplateRows( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->templateRows != val ) {
gs->templateRows = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridTemplateColumns( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->templateColumns != val ) {
gs->templateColumns = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridTemplateAreas( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->templateAreas != val ) {
gs->templateAreas = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridAutoRows( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->autoRows != val ) {
gs->autoRows = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridAutoColumns( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->autoColumns != val ) {
gs->autoColumns = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridAutoFlow( CSSGridAutoFlow val ) {
auto* gs = ensureGridState();
if ( gs->autoFlow != val ) {
gs->autoFlow = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridAutoFlowDense( bool val ) {
auto* gs = ensureGridState();
if ( gs->autoFlowDense != val ) {
gs->autoFlowDense = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridRowStart( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->rowStart != val ) {
gs->rowStart = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridRowEnd( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->rowEnd != val ) {
gs->rowEnd = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridColumnStart( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->columnStart != val ) {
gs->columnStart = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridColumnEnd( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->columnEnd != val ) {
gs->columnEnd = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setGridArea( const std::string& val ) {
auto* gs = ensureGridState();
if ( gs->area != val ) {
gs->area = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setJustifyItems( CSSJustifyItems val ) {
auto* gs = ensureGridState();
if ( gs->justifyItems != val ) {
gs->justifyItems = val;
notifyLayoutAttrChange();
}
}
void UIHTMLWidget::setJustifySelf( CSSJustifySelf val ) {
auto* gs = ensureGridState();
if ( gs->justifySelf != val ) {
gs->justifySelf = val;
notifyLayoutAttrChange();
}
}
std::vector<PropertyId> UIHTMLWidget::getPropertiesImplemented() const {
auto props = UILayout::getPropertiesImplemented();
auto local = { PropertyId::Display,
@@ -380,7 +498,24 @@ std::vector<PropertyId> UIHTMLWidget::getPropertiesImplemented() const {
PropertyId::Order,
PropertyId::ColumnGap,
PropertyId::RowGap,
PropertyId::Gap };
PropertyId::Gap,
PropertyId::GridTemplateRows,
PropertyId::GridTemplateColumns,
PropertyId::GridTemplateAreas,
PropertyId::GridTemplate,
PropertyId::GridAutoRows,
PropertyId::GridAutoColumns,
PropertyId::GridAutoFlow,
PropertyId::Grid,
PropertyId::GridRowStart,
PropertyId::GridRowEnd,
PropertyId::GridColumnStart,
PropertyId::GridColumnEnd,
PropertyId::GridRow,
PropertyId::GridColumn,
PropertyId::GridArea,
PropertyId::JustifyItems,
PropertyId::JustifySelf };
props.insert( props.end(), local.begin(), local.end() );
return props;
}
@@ -435,6 +570,32 @@ std::string UIHTMLWidget::getPropertyString( const PropertyDefinition* propertyD
return getRowGap();
case PropertyId::ColumnGap:
return getColumnGap();
case PropertyId::GridTemplateRows:
return getGridTemplateRows();
case PropertyId::GridTemplateColumns:
return getGridTemplateColumns();
case PropertyId::GridTemplateAreas:
return getGridTemplateAreas();
case PropertyId::GridAutoRows:
return getGridAutoRows();
case PropertyId::GridAutoColumns:
return getGridAutoColumns();
case PropertyId::GridAutoFlow:
return CSSGridAutoFlowHelper::toString( getGridAutoFlow() );
case PropertyId::GridRowStart:
return getGridRowStart();
case PropertyId::GridRowEnd:
return getGridRowEnd();
case PropertyId::GridColumnStart:
return getGridColumnStart();
case PropertyId::GridColumnEnd:
return getGridColumnEnd();
case PropertyId::GridArea:
return getGridArea();
case PropertyId::JustifyItems:
return CSSJustifyItemsHelper::toString( getJustifyItems() );
case PropertyId::JustifySelf:
return CSSJustifySelfHelper::toString( getJustifySelf() );
default:
return UILayout::getPropertyString( propertyDef );
}
@@ -547,6 +708,64 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
setColumnGap( attribute.asString() );
return true;
}
case PropertyId::GridTemplateRows: {
setGridTemplateRows( attribute.asString() );
return true;
}
case PropertyId::GridTemplateColumns: {
setGridTemplateColumns( attribute.asString() );
return true;
}
case PropertyId::GridTemplateAreas: {
setGridTemplateAreas( attribute.asString() );
return true;
}
case PropertyId::GridAutoRows: {
setGridAutoRows( attribute.asString() );
return true;
}
case PropertyId::GridAutoColumns: {
setGridAutoColumns( attribute.asString() );
return true;
}
case PropertyId::GridAutoFlow: {
std::string val = attribute.asString();
String::toLowerInPlace( val );
setGridAutoFlowDense( val.find( "dense" ) != std::string::npos );
if ( val.find( "column" ) != std::string::npos )
setGridAutoFlow( CSSGridAutoFlow::Column );
else
setGridAutoFlow( CSSGridAutoFlow::Row );
return true;
}
case PropertyId::GridRowStart: {
setGridRowStart( attribute.asString() );
return true;
}
case PropertyId::GridRowEnd: {
setGridRowEnd( attribute.asString() );
return true;
}
case PropertyId::GridColumnStart: {
setGridColumnStart( attribute.asString() );
return true;
}
case PropertyId::GridColumnEnd: {
setGridColumnEnd( attribute.asString() );
return true;
}
case PropertyId::GridArea: {
setGridArea( attribute.asString() );
return true;
}
case PropertyId::JustifyItems: {
setJustifyItems( CSSJustifyItemsHelper::fromString( attribute.asString() ) );
return true;
}
case PropertyId::JustifySelf: {
setJustifySelf( CSSJustifySelfHelper::fromString( attribute.asString() ) );
return true;
}
default:
break;
}

View File

@@ -1,6 +1,7 @@
#include <eepp/core/memorymanager.hpp>
#include <eepp/ui/blocklayouter.hpp>
#include <eepp/ui/flexlayouter.hpp>
#include <eepp/ui/gridlayouter.hpp>
#include <eepp/ui/inlinelayouter.hpp>
#include <eepp/ui/nonelayouter.hpp>
#include <eepp/ui/tablelayouter.hpp>
@@ -16,12 +17,22 @@ static bool parentIsFlexContainer( UIWidget* widget ) {
parent->asType<UIHTMLWidget>()->isFlex();
}
static bool parentIsGridContainer( UIWidget* widget ) {
Node* parent = widget->getParent();
return parent && parent->isType( UI_TYPE_HTML_WIDGET ) &&
parent->asType<UIHTMLWidget>()->isGrid();
}
UILayouter* UILayouterManager::create( CSSDisplay display, UIWidget* container ) {
// Blockification per CSS Flexbox §4: children of flex containers are block-level
if ( parentIsFlexContainer( container ) ) {
// But a child that is itself a flex container still needs FlexLayouter
// Blockification per CSS Flexbox §4 and CSS Grid §6: children of flex / grid containers
// are block-level
if ( parentIsFlexContainer( container ) || parentIsGridContainer( container ) ) {
// But a child that is itself a flex container still needs FlexLayouter,
// and a child that is itself a grid container still needs GridLayouter.
if ( display == CSSDisplay::Flex || display == CSSDisplay::InlineFlex )
return eeNew( FlexLayouter, ( container ) );
if ( display == CSSDisplay::Grid || display == CSSDisplay::InlineGrid )
return eeNew( GridLayouter, ( container ) );
return eeNew( BlockLayouter, ( container ) );
}
@@ -34,6 +45,9 @@ UILayouter* UILayouterManager::create( CSSDisplay display, UIWidget* container )
case CSSDisplay::Flex:
case CSSDisplay::InlineFlex:
return eeNew( FlexLayouter, ( container ) );
case CSSDisplay::Grid:
case CSSDisplay::InlineGrid:
return eeNew( GridLayouter, ( container ) );
case CSSDisplay::Inline:
if ( container->isType( UI_TYPE_TEXTSPAN ) )
return eeNew( InlineLayouter, ( container ) );

View File

@@ -1197,7 +1197,10 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
Node* parentNode = container->getParent();
bool parentIsFlex = parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
parentNode->asType<UIHTMLWidget>()->isFlex();
if ( !selfSpan->getText().empty() && ( !selfSpan->isInline() || parentIsFlex ) &&
bool parentIsGrid = parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
parentNode->asType<UIHTMLWidget>()->isGrid();
if ( !selfSpan->getText().empty() &&
( !selfSpan->isInline() || parentIsFlex || parentIsGrid ) &&
NULL != selfSpan->getFontStyleConfig().Font ) {
String::View selfText = selfSpan->getText().view();
String transformed;

File diff suppressed because it is too large Load Diff

View File

@@ -178,6 +178,7 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
FileSystem::changeWorkingDirectory( Sys::getProcessPath() );
if ( !FileSystem::fileExists( "assets/html/reddit_old_thread_files/reddit.ETA_etA2z5U.css" ) ) {
Engine::destroySingleton();
UTEST_SKIP( "old Reddit fixture CSS asset is not readable" );
}
@@ -348,8 +349,8 @@ UTEST( UIHTML, redditOldThreadWebViewSmoke ) {
<< ") "
<< "header=(" << headerPos.x << "," << headerPos.y << " "
<< header->asType<UIWidget>()->getPixelsSize().getWidth() << "x"
<< header->asType<UIWidget>()->getPixelsSize().getHeight() << " offset="
<< header->asType<UIWidget>()->getPixelsContentOffset().Left << ","
<< header->asType<UIWidget>()->getPixelsSize().getHeight()
<< " offset=" << header->asType<UIWidget>()->getPixelsContentOffset().Left << ","
<< header->asType<UIWidget>()->getPixelsContentOffset().Top << ","
<< header->asType<UIWidget>()->getPixelsContentOffset().Right << ","
<< header->asType<UIWidget>()->getPixelsContentOffset().Bottom << ") "
@@ -2589,8 +2590,7 @@ UTEST( UIHTML, BodyHeightMiscalculationFixture ) {
// With justify-content: space-between in a row-direction flex:
// .brand should be at the left side of .wrap (near padding edge).
EXPECT_LT( brandWidget->getPixelsPosition().x,
wrapWidget->getPixelsSize().getWidth() * 0.25f );
EXPECT_LT( brandWidget->getPixelsPosition().x, wrapWidget->getPixelsSize().getWidth() * 0.25f );
// .links should be at the right side of .wrap, NOT next to .brand.
Float wrapW = wrapWidget->getPixelsSize().getWidth();