Merge branch 'feature/grid-layout' into develop

This commit is contained in:
Martín Lucas Golini
2026-06-03 02:07:47 -03:00
25 changed files with 14814 additions and 39 deletions
+63
View File
@@ -860,3 +860,66 @@ 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 (43)
| # | 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) |
| 35 | L1: `preSizeItemsForRowSizing` doesn't reflow content after width change | (implicitly tested) |
| 36 | L4: Baseline computed as bottom edge — grid not handled in `getBaseline()` | (implicitly tested) |
| 37 | P5: Invalid tokens silently become `Length(0)` | (unit testable) |
| 38 | P6: Malformed bracket tokens flow to function parsing | (unit testable) |
| 39 | TS5: Floating-point error from iterative subtraction | (unit testable) |
| 40 | I3: Auto-repeat expanded once with potentially zero container size | (implicitly tested) |
| 41 | I5: Negative `contentBoxSize` now clamped to 0 | (implicitly tested) |
| 42 | L5: `mPacking` converted to RAII guard (exception-safe) | (implicitly tested) |
| 43 | I4: Span decoupled from `resolvedEnd` — explicit `columnSpan`/`rowSpan` fields | (implicitly tested) |
| 44 | PL7: Auto-placement cursor not advancing past placed items (`cursorCol = c+sw`) | `GridContainer.percentHeightWithAutoParentFallsBackToContent` |
| 45 | PL8: Auto-placement column/row limit growing during iteration (captured `colLimit`/`rowLimit`) | `GridContainer.percentHeightWithAutoParentFallsBackToContent` |
| 46 | L6: `height:100%` / `width:100%` on grid with auto parent → fall back to content-based size | `GridContainer.percentHeightWithAutoParentFallsBackToContent` |
| 47 | TS8: `1fr` tracks get `baseSize=0` when container has indefinite width → size from item intrinsic widths | `GridContainer.frTracksWithPercentWidthAndHeight` |
| 48 | L7: `width:100%`/`height:100%` resolved against small intermediate ancestor → scan all ancestors + scene node for max size | `GridContainer.frTracksWithPercentWidthAndHeight` |
| 49 | L8: Percentage fallback ran AFTER track sizing → rows sized with 0 height. Re-run sizing loop after fallback adjusts container size. | `GridContainer.frTracksWithPercentWidthAndHeight` |
| 50 | L9: `height:100%` resolved against viewport instead of auto → use content-based height (CSS spec: % height on auto parent = auto) | `GridContainer.newsblurReducedGrid` |
### Stats
- **Total bugs found**: 50
- **Fixed**: 50
- **By severity**: 0 remaining
@@ -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>
@@ -0,0 +1,451 @@
<!doctype html>
<html>
<head>
<style>
body {
height: 100%;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-overflow-scrolling: touch;
scrollbar-color: #c2c2c2 #fafafa;
background-color: #fff;
border: 0;
outline: 0;
margin: 0;
padding: 0;
line-height: 1.3;
text-decoration: none;
list-style: none;
overflow: hidden;
}
a,
a:active,
a:hover,
a:visited,
button {
outline: none;
}
a img {
border: none;
}
.NB-splash {
display: block;
}
body,
.NB-theme-feed-font-whitney {
font-family:
Whitney SSm A,
Whitney SSm B,
Lucida Grande,
Verdana,
Helvetica Neue,
Helvetica,
sans-serif;
}
.NB-welcome .NB-body-inner {
width: 100%;
height: 100%;
position: absolute;
overflow: auto;
}
.NB-welcome .NB-inner {
box-sizing: border-box;
width: 100%;
max-width: 960px;
height: 100%;
margin: 0 auto;
position: relative;
}
.NB-welcome-features-wrap {
background: linear-gradient(#f5eedd 0%, #ebe0c2 100%);
border-top: 1px solid #0000000f;
border-bottom: 1px solid #0000000f;
padding-bottom: 0;
}
.NB-welcome-feature-groups {
margin: 48px 0 24px;
}
.NB-welcome-feature-groups > .NB-inner {
grid-template-columns: 1fr 1fr;
gap: 36px 48px;
display: grid;
}
.NB-feature-group {
border-left: 3px solid #0000;
padding-left: 20px;
}
.NB-feature-group-read {
border-left-color: #c0a06a;
}
.NB-feature-group-smart {
border-left-color: #7a9a5a;
}
.NB-feature-group-share {
border-left-color: #8a6a9a;
}
.NB-feature-group-everywhere {
border-left-color: #5a8aaa;
}
.NB-feature-group-label {
text-transform: uppercase;
letter-spacing: 2px;
margin: 0 0 14px;
padding: 0;
font-family:
Gotham Narrow A,
Gotham Narrow B,
sans-serif;
font-size: 18px;
font-weight: 700;
}
.NB-feature-group-read .NB-feature-group-label {
color: #b0903a;
}
.NB-feature-group-smart .NB-feature-group-label {
color: #5a7a3a;
}
.NB-feature-group-share .NB-feature-group-label {
color: #7a5a8a;
}
.NB-feature-group-everywhere .NB-feature-group-label {
color: #4a7a9a;
}
.NB-feature-group-items {
flex-direction: column;
gap: 0;
display: flex;
}
a.NB-fgi {
color: inherit;
border-radius: 8px;
align-items: center;
gap: 14px;
padding: 10px 12px 10px 0;
text-decoration: none;
transition: background-color 0.15s;
display: flex;
}
a.NB-fgi img {
object-fit: cover;
object-position: left top;
border-radius: 6px;
flex-shrink: 0;
width: 128px;
height: 128px;
box-shadow: 0 1px 4px #00000026;
}
.NB-fgi-text {
min-width: 0;
}
.NB-fgi-caption {
color: #3a2e1e;
font-family:
Gotham Narrow A,
Gotham Narrow B,
sans-serif;
font-size: 22px;
font-weight: 700;
line-height: 1.2;
}
.NB-fgi-desc {
color: #8a7a6a;
margin-top: 4px;
font-size: 16px;
line-height: 1.45;
}
</style>
</head>
<body
class="NB-welcome NB-premium NB-is-premium NB-is-anonymous NB-pref-story-position-center NB-dashboard-columns-double NB-pref-sticky-story-sideoptions NB-content-preview-1 NB-intelligence-neutral NB-feedlist-hide-read-feeds NB-theme-whitney NB-theme-size-m NB-theme-feed-size-m NB-theme-feed-font-whitney NB-line-spacing-m NB-density-comfortable NB-image-preview-small-right"
style="background-color: rgb(26, 37, 37)"
>
<div class="NB-body-inner">
<div class="NB-welcome-container NB-splash">
<div class="NB-welcome-features-wrap">
<div class="NB-welcome-feature-groups">
<div class="NB-inner">
<div class="NB-feature-group NB-feature-group-read">
<h3 class="NB-feature-group-label">Read Your Way</h3>
<div class="NB-feature-group-items">
<a href="https://newsblur.com/features#speed" class="NB-fgi">
<img src="./grid_newsblur_files/feature_1.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Real-time RSS</div>
<div class="NB-fgi-desc">
Stories are pushed directly to you, so you can read news
as it comes in
</div>
</div>
</a>
<a
href="https://newsblur.com/features#original"
class="NB-fgi"
>
<img src="./grid_newsblur_files/feature_2.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Original Site</div>
<div class="NB-fgi-desc">
Read the content in context, the way it was meant to be
seen
</div>
</div>
</a>
<a
href="https://newsblur.com/features#text-view"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_3.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Read the Full Story</div>
<div class="NB-fgi-desc">
The original story from truncated RSS feeds is
seamlessly expanded
</div>
</div>
</a>
<a
href="https://newsblur.com/features#layouts"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_8.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">
Grid, List, Split, Magazine
</div>
<div class="NB-fgi-desc">
Every site has its own story layout, so you can mix and
match
</div>
</div>
</a>
<a href="https://newsblur.com/features#themes" class="NB-fgi">
<img src="./grid_newsblur_files/subfeature_9.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Dark Mode</div>
<div class="NB-fgi-desc">
Easy on the eyes and built into the web, iOS, and
Android
</div>
</div>
</a>
<a href="https://newsblur.com/features#river" class="NB-fgi">
<img src="./grid_newsblur_files/subfeature_16.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">River of News</div>
<div class="NB-fgi-desc">
Read all your feeds by folder in one scrollable stream
</div>
</div>
</a>
<a
href="https://newsblur.com/features#shortcuts"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_15.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Keyboard Shortcuts</div>
<div class="NB-fgi-desc">
Navigate everything without touching the mouse
</div>
</div>
</a>
</div>
</div>
<div class="NB-feature-group NB-feature-group-smart">
<h3 class="NB-feature-group-label">Stay Smart</h3>
<div class="NB-feature-group-items">
<a
href="https://newsblur.com/features#training"
class="NB-fgi"
>
<img src="./grid_newsblur_files/feature_4.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Intelligence Training</div>
<div class="NB-fgi-desc">
Hide the stories you don't like and highlight the
stories you do
</div>
</div>
</a>
<a href="https://newsblur.com/features#search" class="NB-fgi">
<img src="./grid_newsblur_files/feature_5.jpg" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Full Text Search</div>
<div class="NB-fgi-desc">
Quickly find stories across all of your subscriptions
</div>
</div>
</a>
<a href="https://newsblur.com/features#search" class="NB-fgi">
<img src="./grid_newsblur_files/subfeature_1.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Saved Searches</div>
<div class="NB-fgi-desc">
Regularly used searches are conveniently given their own
feeds
</div>
</div>
</a>
<a href="https://newsblur.com/features#ask-ai" class="NB-fgi">
<img src="./grid_newsblur_files/subfeature_12.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Ask AI</div>
<div class="NB-fgi-desc">
Ask questions about any story and get answers powered by
AI
</div>
</div>
</a>
<a
href="https://newsblur.com/features#briefing"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_13.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Daily Briefing</div>
<div class="NB-fgi-desc">
A summary of your top stories, customizable and
delivered every day
</div>
</div>
</a>
<a
href="https://newsblur.com/features#changes"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_5.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Track Changes</div>
<div class="NB-fgi-desc">
See how a story evolved since it was first published
</div>
</div>
</a>
<a
href="https://newsblur.com/features#notifications"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_14.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Notifications</div>
<div class="NB-fgi-desc">
Get alerted the moment a feed publishes, on web, iOS, or
Android
</div>
</div>
</a>
</div>
</div>
<div class="NB-feature-group NB-feature-group-share">
<h3 class="NB-feature-group-label">Share &amp; Organize</h3>
<div class="NB-feature-group-items">
<a
href="https://newsblur.com/features#blurblog"
class="NB-fgi"
>
<img src="./grid_newsblur_files/feature_3.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Shared Stories</div>
<div class="NB-fgi-desc">
Reading news is better with friends. Share stories on
your public blurblog
</div>
</div>
</a>
<a
href="https://newsblur.com/features#blurblog"
class="NB-fgi"
>
<img src="./grid_newsblur_files/feature_7.jpg" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Blurblog Privacy</div>
<div class="NB-fgi-desc">
Share stories with the world or only with your friends
</div>
</div>
</a>
<a href="https://newsblur.com/features#saved" class="NB-fgi">
<img src="./grid_newsblur_files/feature_6.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Story Tagging</div>
<div class="NB-fgi-desc">
Save stories with custom tags for fast references
</div>
</div>
</a>
<a
href="https://newsblur.com/features#newsletters"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_7.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Email Newsletters</div>
<div class="NB-fgi-desc">
Read your email newsletters where they belong, in a news
reader
</div>
</div>
</a>
</div>
</div>
<div class="NB-feature-group NB-feature-group-everywhere">
<h3 class="NB-feature-group-label">Works Everywhere</h3>
<div class="NB-feature-group-items">
<a href="https://newsblur.com/features#apps" class="NB-fgi">
<img src="./grid_newsblur_files/feature_8.jpg" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Third-party Apps</div>
<div class="NB-fgi-desc">
Supports Reeder, ReadKit, Unread, and loads more
</div>
</div>
</a>
<a
href="https://newsblur.com/features#integrations"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_6.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">IFTTT &amp; Zapier</div>
<div class="NB-fgi-desc">
Hook NewsBlur up to nearly every service on the web
</div>
</div>
</a>
<a
href="https://newsblur.com/features#web-feeds"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_10.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">YouTube</div>
<div class="NB-fgi-desc">
Even sites that don't publish RSS feeds can be followed
</div>
</div>
</a>
<a
href="https://newsblur.com/features#web-feeds"
class="NB-fgi"
>
<img src="./grid_newsblur_files/subfeature_11.png" />
<div class="NB-fgi-text">
<div class="NB-fgi-caption">Web Feeds</div>
<div class="NB-fgi-desc">
A creative feed for any website, even without RSS
</div>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -0,0 +1,138 @@
<!doctype html>
<html lang="en">
<head>
<style>
:root {
--bg: #0c1520;
--bg-surface: #141f2e;
--bg-surface-2: #1a2637;
--text: #bfc8d2;
--text-muted: #566a7c;
--text-bright: #e4e9ee;
--accent: #d4a853;
--accent-hover: #e4bc6a;
--tag-color: #7eb8a8;
--eng-color: #3d7b8a;
--danger: #e07a5f;
--border: rgba(255, 255, 255, 0.07);
--border-strong: rgba(255, 255, 255, 0.12);
--max-width: 54rem;
--read-width: 54rem;
--font-text:
text, "Roboto", -apple-system, BlinkMacSystemFont, sans-serif;
--font-mono:
code, "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
--font-serif: serif, "Fraunces", "Iowan Old Style", Georgia, serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 17.5px;
}
body {
font-family: var(--font-text);
font-weight: 300;
line-height: 1.65;
color: var(--text);
background: var(--bg);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: clip;
}
a {
color: var(--accent);
text-decoration: none;
transition: color 0.15s;
}
main.shell {
flex: 1;
max-width: var(--max-width);
margin: 0 auto;
width: 100%;
padding: 3rem 1.5rem 5rem;
animation: fadeIn 0.3s ease;
}
main.post-shell {
max-width: var(--max-width);
margin: 0 auto;
width: 100%;
padding: 2.5rem 1.5rem 4rem;
}
.header-box {
border: 1px solid var(--border);
border-radius: 0.25rem;
padding: 0.9rem 1.1rem;
background: rgba(255, 255, 255, 0.015);
margin-bottom: 2.5rem;
font-family: var(--font-mono);
font-size: 0.72rem;
}
.header-box .hb-row {
display: grid;
grid-template-columns: 6.5rem minmax(0, 1fr);
gap: 1rem;
padding: 0.2rem 0;
}
.header-box .hb-k {
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 0.64rem;
padding-top: 0.15rem;
}
.header-box .hb-v {
color: var(--text-bright);
text-wrap: pretty;
overflow-wrap: anywhere;
min-width: 0;
}
.header-box .hb-v .tag {
color: var(--tag-color);
margin-right: 0.6rem;
}
.header-box .hb-v .tag::before {
content: "#";
opacity: 0.5;
}
.header-box .hb-v a {
color: var(--text-bright);
border-bottom: 1px dashed var(--border-strong);
}
</style>
</head>
<body>
<main id="main" class="shell post-shell" role="main">
<article id="top">
<div class="header-box" aria-label="Post metadata">
<div class="hb-row">
<span class="hb-k">published</span>
<span class="hb-v">
<time datetime="2026-04-05">2026-04-05</time>
</span>
</div>
<div class="hb-row">
<span class="hb-k">reading</span>
<span class="hb-v">5 min · 1,132 words</span>
</div>
<div class="hb-row">
<span class="hb-k">tags</span>
<span class="hb-v">
<a href="https://jonno.nz/tags/ai/" class="tag">ai</a
><a href="https://jonno.nz/tags/architecture/" class="tag"
>architecture</a
><a href="https://jonno.nz/tags/engineering/" class="tag"
>engineering</a
>
</span>
</div>
</div>
</article>
</main>
</body>
</html>
+63
View File
@@ -0,0 +1,63 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style id="css-output">
* {
box-sizing: border-box;
}
.wrapper {
max-width: 940px;
margin: 0 auto;
}
.wrapper > div {
border: 2px solid rgb(233 171 88);
border-radius: 5px;
background-color: rgb(233 171 88 / 50%);
padding: 1em;
color: #d9480f;
}
.wrapper {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
grid-auto-rows: minmax(100px, auto);
}
.one {
grid-column: 1 / 3;
grid-row: 1;
}
.two {
grid-column: 2 / 4;
grid-row: 1 / 3;
}
.three {
grid-column: 1;
grid-row: 2 / 5;
}
.four {
grid-column: 3;
grid-row: 3;
}
.five {
grid-column: 2;
grid-row: 4;
}
.six {
grid-column: 3;
grid-row: 4;
}
</style>
</head>
<body>
<div class="wrapper">
<div class="one">One</div>
<div class="two">Two</div>
<div class="three">Three</div>
<div class="four">Four</div>
<div class="five">Five</div>
<div class="six">Six</div>
</div>
</body>
</html>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,175 @@
<!doctype html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style>
body {
height: 100%;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-overflow-scrolling: touch;
scrollbar-color: #c2c2c2 #fafafa;
background-color: #fff;
border: 0;
outline: 0;
margin: 0;
padding: 0;
line-height: 1.3;
text-decoration: none;
list-style: none;
overflow: hidden;
}
.NB-splash {
display: block;
}
body,
.NB-theme-feed-font-whitney {
font-family:
Whitney SSm A,
Whitney SSm B,
Lucida Grande,
Verdana,
Helvetica Neue,
Helvetica,
sans-serif;
}
.NB-welcome .NB-body-inner {
width: 100%;
height: 100%;
position: absolute;
overflow: auto;
}
.NB-welcome .NB-inner {
box-sizing: border-box;
width: 100%;
max-width: 960px;
height: 100%;
margin: 0 auto;
position: relative;
}
.NB-welcome-pricing {
clear: both;
background: linear-gradient(#f5eedd 0%, #ebe0c2 100%);
border-bottom: 1px solid #0000000f;
margin: 0;
padding: 48px 0;
overflow: hidden;
}
.NB-pricing-tiers {
flex-direction: row;
align-items: stretch;
gap: 16px;
display: flex;
}
.NB-pricing-tier {
background: #fff;
border: 1px solid #00000014;
border-radius: 12px;
flex-direction: column;
flex: 1;
transition:
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
position: relative;
overflow: hidden;
box-shadow:
0 2px 4px #0000000a,
0 8px 24px #0000000f;
}
.NB-pricing-tier-premium {
flex: 34;
}
.NB-pricing-tier-archive {
flex: 38;
}
.NB-pricing-tier-pro {
flex: 28;
}
.NB-pricing-tier-header {
text-align: center;
padding: 20px 16px;
}
.NB-pricing-tier-premium .NB-pricing-tier-header {
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
}
.NB-pricing-tier-archive .NB-pricing-tier-header {
background: linear-gradient(135deg, #14b8a6 0%, #0d9488 100%);
}
.NB-pricing-tier-pro .NB-pricing-tier-header {
background: linear-gradient(135deg, #f97316 0%, #ea580c 100%);
}
.NB-pricing-tier-name {
color: #fff;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
font-size: 20px;
font-weight: 700;
}
.NB-pricing-tier-price {
justify-content: center;
align-items: baseline;
gap: 2px;
display: flex;
}
.NB-pricing-tier-price-amount {
color: #fff;
font-size: 42px;
font-weight: 800;
line-height: 1;
}
.NB-pricing-tier-price-period {
color: #ffffffd9;
font-size: 16px;
font-weight: 500;
}
</style>
</head>
<body
class="NB-welcome NB-premium NB-is-premium NB-is-anonymous NB-pref-story-position-center NB-dashboard-columns-double NB-pref-sticky-story-sideoptions NB-content-preview-1 NB-intelligence-neutral NB-feedlist-hide-read-feeds NB-theme-whitney NB-theme-size-m NB-theme-feed-size-m NB-theme-feed-font-whitney NB-line-spacing-m NB-density-comfortable NB-image-preview-small-right"
style="background-color: rgb(26, 37, 37)"
>
<div class="NB-body-inner">
<div class="NB-welcome-container NB-splash">
<div class="NB-welcome-pricing">
<div class="NB-inner">
<div class="NB-pricing-tiers">
<div class="NB-pricing-tier NB-pricing-tier-premium">
<div class="NB-pricing-tier-header">
<div class="NB-pricing-tier-name">Premium</div>
<div class="NB-pricing-tier-price">
<span class="NB-pricing-tier-price-amount">$36</span>
<span class="NB-pricing-tier-price-period">/year</span>
</div>
</div>
</div>
<div class="NB-pricing-tier NB-pricing-tier-archive">
<div class="NB-pricing-tier-header">
<div class="NB-pricing-tier-name">Premium Archive</div>
<div class="NB-pricing-tier-price">
<span class="NB-pricing-tier-price-amount">$99</span>
<span class="NB-pricing-tier-price-period">/year</span>
</div>
</div>
</div>
<div class="NB-pricing-tier NB-pricing-tier-pro">
<div class="NB-pricing-tier-header">
<div class="NB-pricing-tier-name">Premium Pro</div>
<div class="NB-pricing-tier-price">
<span class="NB-pricing-tier-price-amount">$29</span>
<span class="NB-pricing-tier-price-period">/month</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
@@ -289,6 +289,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 {
+23
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 {
+174
View File
@@ -0,0 +1,174 @@
#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 };
int columnSpan{ 1 };
int rowSpan{ 1 };
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 };
CSSGridAutoFlow mAutoFlow{ CSSGridAutoFlow::Row };
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
+106
View File
@@ -28,6 +28,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();
@@ -52,6 +69,8 @@ class EE_API UIHTMLWidget : public UILayout {
bool isFlex() const;
bool isGrid() const;
CSSPosition getCSSPosition() const { return mPosition; }
void setCSSPosition( CSSPosition position );
@@ -90,6 +109,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;
}
@@ -136,6 +163,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;
@@ -230,6 +335,7 @@ class EE_API UIHTMLWidget : public UILayout {
bool mNeedsZIndexSort{ false };
UILayouter* mLayouter{ nullptr };
UIHTMLWidgetFlexState* mFlexState{ nullptr };
UIHTMLWidgetGridState* mGridState{ nullptr };
UnorderedMap<std::string, StyleSheetProperty> mDataProperties;
Uint32 mScrollCb{ 0 };
+3
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;
+1
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;
/**
+1
View File
@@ -245,6 +245,7 @@ class EE_API UIHTMLBody : public UIRichText {
protected:
bool mPropagatedBackground{ false };
StyleSheetLength mMinHeightLocal;
bool mSettingBodyHeight{ false };
UIHTMLBody( const std::string& tag = "body" );
};
+100
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:
+9 -1
View File
@@ -77,8 +77,16 @@ void BlockLayouter::updateLayout() {
Node* parentNode = widget->getParent();
bool parentIsFlex = parentNode && parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
parentNode->asType<UIHTMLWidget>()->isFlex();
bool parentIsGrid = parentNode && parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
parentNode->asType<UIHTMLWidget>()->isGrid();
if ( widget->isInline() && !parentIsFlex )
// Inline elements normally do not run their own block layout; the nearest parent RichText
// stream formats and paints them. Flex/grid containers are the exception: CSS blockification
// turns each child into an independent item, so an inline UITextSpan inside any flex/grid
// parent, including inline-flex/inline-grid, still needs this layouter to build its own
// RichText, measure its height, and position text. This is broader than the render ownership
// guard in UIRichText/UITextSpan, which only applies to block-level flex/grid containers.
if ( widget->isInline() && !parentIsFlex && !parentIsGrid )
return;
mResizedCount = 0;
@@ -512,6 +512,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" );
@@ -585,6 +601,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,
@@ -1655,6 +1681,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
+28 -19
View File
@@ -133,6 +133,16 @@ bool getFontStyleFromAncestor( UIWidget* widget, FontStyleConfig& outConfig ) {
return false;
}
static void updateChildLayoutIfDirty( UIWidget* widget, const UILayouter* owner ) {
if ( !widget->isType( UI_TYPE_HTML_WIDGET ) )
return;
auto* childHtml = widget->asType<UIHTMLWidget>();
auto* layouter = childHtml->getLayouter();
if ( layouter && layouter != owner && !layouter->isPacking() && childHtml->isLayoutDirty() )
childHtml->updateLayout();
}
} // namespace
void FlexLayouter::readContainerStyle( CSSFlexDirection& direction, CSSFlexWrap& wrap,
@@ -268,16 +278,26 @@ void FlexLayouter::measureFlexItems( const Axis& mainAxis, const Axis& crossAxis
item.widget->setLayoutWidthPolicy( SizePolicy::WrapContent );
else
item.widget->setLayoutHeightPolicy( SizePolicy::WrapContent );
}
if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) ) {
auto* childHtml = item.widget->asType<UIHTMLWidget>();
auto* layouter = childHtml->getLayouter();
if ( layouter && !layouter->isPacking() && layouter != this ) {
childHtml->updateLayout();
// Set the item's internal size to the container's main size,
// so the block layouter's `else` branch (non-WrapContent)
// wraps text at the full container width. The flex-shrink
// step will then proportionally reduce the item to its final
// size, giving the expected "fill remaining space" behaviour.
Float containerMainDim =
mainAxis.horizontal
? containerWidth - containerPadding.Left - containerPadding.Right
: containerHeight - containerPadding.Top - containerPadding.Bottom;
if ( containerMainDim > 0.f ) {
if ( mainAxis.horizontal )
item.widget->setInternalPixelsWidth( containerMainDim );
else
item.widget->setInternalPixelsHeight( containerMainDim );
}
}
updateChildLayoutIfDirty( item.widget, this );
if ( oldMainPolicy == SizePolicy::MatchParent ) {
if ( mainAxis.horizontal )
item.widget->setLayoutWidthPolicy( oldMainPolicy );
@@ -875,11 +895,7 @@ void FlexLayouter::resolveCrossSizes( FlexLine& line, const Axis& crossAxis,
item.widget->setInternalPixelsWidth( item.targetMainSize );
else
item.widget->setInternalPixelsHeight( item.targetMainSize );
if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) ) {
auto* htmlWidget = item.widget->asType<UIHTMLWidget>();
if ( htmlWidget->getLayouter() && htmlWidget->getLayouter() != this )
htmlWidget->updateLayout();
}
updateChildLayoutIfDirty( item.widget, this );
}
// For text nodes (anonymous flex items), configure a cached Text object for
@@ -1164,14 +1180,7 @@ void FlexLayouter::applyLayout( const SmallVector<FlexLine, 8>& lines, const Axi
item.widget->setPixelsPosition( widgetX, widgetY );
item.widget->setPixelsSize( widgetW, widgetH );
if ( item.widget->isType( UI_TYPE_HTML_WIDGET ) ) {
auto* childHtml = item.widget->asType<UIHTMLWidget>();
if ( childHtml->getLayouter() && childHtml->getLayouter() != this ) {
auto* childLayouter = childHtml->getLayouter();
if ( !childLayouter->isPacking() )
childHtml->updateLayout();
}
}
updateChildLayoutIfDirty( item.widget, this );
}
}
File diff suppressed because it is too large Load Diff
+279 -6
View File
@@ -2,6 +2,7 @@
#include <eepp/ui/css/propertydefinition.hpp>
#include <eepp/ui/css/stylesheetlength.hpp>
#include <eepp/ui/flexlayouter.hpp>
#include <eepp/ui/gridlayouter.hpp>
#include <eepp/ui/uihtmlwidget.hpp>
#include <eepp/ui/uilayouter.hpp>
#include <eepp/ui/uilayoutermanager.hpp>
@@ -78,6 +79,7 @@ UIHTMLWidget::~UIHTMLWidget() {
mScrollTarget->removeEventListener( mScrollCb );
eeSAFE_DELETE( mLayouter );
eeSAFE_DELETE( mFlexState );
eeSAFE_DELETE( mGridState );
}
UILayouter* UIHTMLWidget::getLayouter() {
@@ -114,11 +116,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,10 +140,20 @@ 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 );
return flex->getBaseline();
if ( mLayouter ) {
if ( isFlex() ) {
auto* flex = reinterpret_cast<FlexLayouter*>( mLayouter );
return flex->getBaseline();
}
if ( isGrid() ) {
auto* grid = reinterpret_cast<GridLayouter*>( mLayouter );
return grid->getBaseline();
}
}
if ( mBaselineAlign.type == CSSBaselineAlignment::Length )
return mBaselineAlign.value;
@@ -439,6 +451,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,
@@ -465,7 +589,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;
}
@@ -520,6 +661,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 );
}
@@ -529,6 +696,17 @@ bool UIHTMLWidget::applyProperty( const StyleSheetProperty& attribute ) {
if ( !checkPropertyDefinition( attribute ) )
return false;
auto applyGridLineShorthand = []( const std::string& value, auto setStart, auto setEnd ) {
size_t slash = value.find( '/' );
if ( slash == std::string::npos ) {
setStart( String::trim( value ) );
setEnd( "auto" );
} else {
setStart( String::trim( value.substr( 0, slash ) ) );
setEnd( String::trim( value.substr( slash + 1 ) ) );
}
};
switch ( attribute.getPropertyDefinition()->getPropertyId() ) {
case PropertyId::Display: {
setDisplay( CSSDisplayHelper::fromString( attribute.asString() ) );
@@ -632,6 +810,78 @@ 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::GridRow: {
applyGridLineShorthand(
attribute.asString(),
[this]( const std::string& value ) { setGridRowStart( value ); },
[this]( const std::string& value ) { setGridRowEnd( value ); } );
return true;
}
case PropertyId::GridColumn: {
applyGridLineShorthand(
attribute.asString(),
[this]( const std::string& value ) { setGridColumnStart( value ); },
[this]( const std::string& value ) { setGridColumnEnd( value ); } );
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;
}
@@ -732,6 +982,29 @@ void UIHTMLWidget::updateOutOfFlowPosition() {
bool useLeft = mLeftEq != "auto";
bool useRight = mRightEq != "auto";
// Per CSS §10.1: for absolutely positioned elements, percentage top/bottom
// resolves against the containing block's height. If the containing block
// does not have a definite height, the percentage computes to auto to
// prevent circular dependencies.
auto cbHasDefiniteHeight = [&]() {
if ( !cb->isLayout() )
return true;
auto* cbLayout = cb->asType<UILayout>();
if ( cbLayout->getLayoutHeightPolicy() != SizePolicy::Fixed )
return false;
if ( cb->getUIStyle() ) {
const auto* hprop = cb->getUIStyle()->getProperty( PropertyId::Height );
if ( hprop && StyleSheetLength::isPercentage( hprop->value() ) )
return false;
}
return true;
};
if ( useTop && StyleSheetLength::isPercentage( mTopEq ) && !cbHasDefiniteHeight() )
useTop = false;
if ( useBottom && StyleSheetLength::isPercentage( mBottomEq ) && !cbHasDefiniteHeight() )
useBottom = false;
if ( useLeft )
left = lengthFromValue( mLeftEq, CSS::PropertyRelativeTarget::ContainingBlockWidth, 0 );
if ( useRight )
+17 -3
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 ) );
+46 -4
View File
@@ -248,7 +248,9 @@ bool UIHTMLBody::applyProperty( const StyleSheetProperty& attribute ) {
void UIHTMLBody::updateLayout() {
UIRichText::updateLayout();
if ( mChild && mChild->isWidget() ) {
if ( mChild && mChild->isWidget() && !mSettingBodyHeight ) {
mSettingBodyHeight = true;
Float maxH = 0;
Node* child = mChild;
Float minHeight = std::max(
@@ -279,6 +281,8 @@ void UIHTMLBody::updateLayout() {
if ( dpH != minHeight )
setMinHeight( dpH );
}
mSettingBodyHeight = false;
}
}
@@ -371,6 +375,18 @@ void UIRichText::draw() {
if ( mVisible && 0.f != mAlpha ) {
UIWidget::draw();
// Block-level flex and grid containers do not own text painting for their item children.
// Their children are blockified and laid out/drawn as independent nodes. A container
// can still have a stale mRichText from a previous display state or an intermediate
// layout pass, so bail out here too; otherwise grid/flex items are painted twice: once by
// this parent stream and once by the child UITextSpan::draw().
//
// Do not use isFlex()/isGrid() here: those also include inline-flex/inline-grid. Inline
// flex/grid containers are atomic inline-level boxes in their parent formatting context
// and must not be treated like block-level containers for this paint-ownership shortcut.
if ( getDisplay() == CSSDisplay::Flex || getDisplay() == CSSDisplay::Grid )
return;
if ( mRichText.getSize().getWidth() > 0.f ) {
Rectf contentOffset = getPixelsContentOffset();
if ( isClipped() ) {
@@ -1592,11 +1608,19 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
Float maxWidth = 0;
bool isInlineBlockTextSpan =
container->isType( UI_TYPE_TEXTSPAN ) && container->asType<UITextSpan>()->isInlineBlock();
Node* parentNode = container->getParent();
bool parentIsFlexOrGrid = parentNode && parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
( parentNode->asType<UIHTMLWidget>()->isFlex() ||
parentNode->asType<UIHTMLWidget>()->isGrid() );
if ( isInlineBlockTextSpan && mode == IntrinsicMode::None &&
container->getPixelsSize().getWidth() > 0 ) {
maxWidth = container->getPixelsSize().getWidth() -
container->getPixelsContentOffset().Left -
container->getPixelsContentOffset().Right;
} else if ( parentIsFlexOrGrid &&
container->getLayoutWidthPolicy() == SizePolicy::WrapContent &&
mode == IntrinsicMode::None ) {
maxWidth = 0;
} else if ( container->getLayoutWidthPolicy() == SizePolicy::WrapContent ) {
maxWidth = container->getMatchParentWidth() - container->getPixelsContentOffset().Left -
container->getPixelsContentOffset().Right;
@@ -1641,10 +1665,12 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
if ( container->isType( UI_TYPE_TEXTSPAN ) ) {
UITextSpan* selfSpan = container->asType<UITextSpan>();
Node* parentNode = container->getParent();
bool parentIsFlex = parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
bool parentIsFlex = parentNode && parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
parentNode->asType<UIHTMLWidget>()->isFlex();
if ( !selfSpan->getText().empty() && ( !selfSpan->isInline() || parentIsFlex ) &&
bool parentIsGrid = parentNode && parentNode->isType( UI_TYPE_HTML_WIDGET ) &&
parentNode->asType<UIHTMLWidget>()->isGrid();
if ( !selfSpan->getText().empty() &&
( !selfSpan->isInline() || parentIsFlex || parentIsGrid ) &&
NULL != selfSpan->getFontStyleConfig().Font ) {
auto collapse = getEffectiveWhiteSpaceCollapse( selfSpan, containerRichText );
richText.setWhiteSpaceWrapMode( toRichTextWhiteSpaceWrapMode( collapse ) );
@@ -2018,6 +2044,22 @@ void UIRichText::rebuildRichText( UILayout* container, RichText& richText, Intri
}
};
if ( container->isType( UI_TYPE_HTML_WIDGET ) ) {
auto* htmlContainer = container->asType<UIHTMLWidget>();
// RichText is the inline-formatting stream for normal block containers. Block-level
// flex/grid containers are different: every in-flow child becomes a flex/grid item and
// must not be folded into the parent's text stream. Keeping the parent stream empty
// prevents duplicate paint and keeps item geometry sourced from the layouter, not from
// inline fragments.
//
// Inline-flex/inline-grid are intentionally excluded here for the same reason as in
// UIRichText::draw(): isFlex()/isGrid() includes inline display variants, but these
// ownership exits are only valid for block-level flex/grid containers.
CSSDisplay display = htmlContainer->getDisplay();
if ( display == CSSDisplay::Flex || display == CSSDisplay::Grid )
return;
}
Node* child = container->getFirstChild();
while ( NULL != child ) {
bool isOutOfFlow =
+18 -6
View File
@@ -90,12 +90,24 @@ void UITextSpan::onDisplayChange() {
}
void UITextSpan::draw() {
// When a UITextSpan is a flex item it is laid out independently by the
// flex container (blockification per CSS Flexbox §4). In that case the
// parent flex container does NOT render its text via rebuildRichText(),
// so the span must draw itself.
if ( !isInline() || ( getParent() && getParent()->isType( UI_TYPE_HTML_WIDGET ) &&
getParent()->asType<UIHTMLWidget>()->isFlex() ) ) {
bool parentIsBlockFlexOrGrid = false;
if ( getParent() && getParent()->isType( UI_TYPE_HTML_WIDGET ) ) {
CSSDisplay parentDisplay = getParent()->asType<UIHTMLWidget>()->getDisplay();
parentIsBlockFlexOrGrid =
parentDisplay == CSSDisplay::Flex || parentDisplay == CSSDisplay::Grid;
}
// When a UITextSpan is an item in a block-level flex/grid container, it is laid out
// independently by the container (blockification per CSS Flexbox §4 / CSS Grid §6). In that
// case the parent container must not render its text via rebuildRichText(), so the span must
// draw itself. This must stay in sync with UIRichText::draw() and
// UIRichText::rebuildRichText(): block-level parent flex/grid containers skip their own text
// stream, and item spans are the single paint owner for their text.
//
// Do not use UIHTMLWidget::isFlex()/isGrid() for the parent test. Those include inline-flex
// and inline-grid, which are atomic inline-level boxes and do not use this self-paint ownership
// rule.
if ( !isInline() || parentIsBlockFlexOrGrid ) {
UIRichText::draw();
}
}
File diff suppressed because it is too large Load Diff
+84
View File
@@ -215,6 +215,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" );
}
@@ -3619,6 +3620,32 @@ UTEST( UIHTML, BlockSizeInfDoesNotHang ) {
Engine::destroySingleton();
}
UTEST( UIHTML, KittyHomeSmallDoesNotHang ) {
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Kitty Home Small Test",
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/" );
std::string html;
FileSystem::fileGet( "assets/html/kitty_home_small.html", html );
ASSERT_FALSE( html.empty() );
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto body = sceneNode->getRoot()->findByTag( "body" );
ASSERT_TRUE( body != nullptr );
auto bodyWidget = body->asType<UIWidget>();
EXPECT_GT( bodyWidget->getPixelsSize().getWidth(), 0.f );
EXPECT_GT( bodyWidget->getPixelsSize().getHeight(), 0.f );
Engine::destroySingleton();
}
UTEST( UIHTML, FlexFormLayout ) {
Engine::instance()->createWindow( WindowSettings( 1024, 653, "Flex Form Layout Test",
WindowStyle::Default, WindowBackend::Default,
@@ -3655,6 +3682,13 @@ UTEST( UIHTML, FlexFormLayout ) {
Float inputRight = inputWidget->getPixelsPosition().x + inputWidget->getPixelsSize().getWidth();
EXPECT_GT( buttonWidget->getPixelsPosition().x, inputRight - 1.f );
auto* buttonRichText = buttonWidget->isType( UI_TYPE_RICHTEXT )
? buttonWidget->asType<UIRichText>()->getRichTextPtr()
: nullptr;
ASSERT_TRUE( buttonRichText != nullptr );
buttonRichText->updateLayout();
EXPECT_EQ( buttonRichText->getLines().size(), 1u );
Engine::destroySingleton();
}
@@ -3756,6 +3790,19 @@ UTEST( UIHTML, FlexMediaQueriesLayout ) {
EXPECT_GT( essayNavLinkWidget->getPixelsSize().getHeight(),
labelWidget->getPixelsSize().getHeight() +
titleWidget->getPixelsSize().getHeight() - 1.f );
auto* labelRichText = essayLabel->isType( UI_TYPE_TEXTSPAN )
? essayLabel->asType<UITextSpan>()->getRichTextPtr()
: nullptr;
auto* titleRichText = essayTitle->isType( UI_TYPE_TEXTSPAN )
? essayTitle->asType<UITextSpan>()->getRichTextPtr()
: nullptr;
ASSERT_TRUE( labelRichText != nullptr );
ASSERT_TRUE( titleRichText != nullptr );
labelRichText->updateLayout();
titleRichText->updateLayout();
EXPECT_EQ( labelRichText->getLines().size(), 1u );
EXPECT_EQ( titleRichText->getLines().size(), 1u );
}
Engine::destroySingleton();
@@ -3799,6 +3846,43 @@ UTEST( UIHTML, FlexAnchorInFlexNavVisible ) {
Engine::destroySingleton();
}
UTEST( UIHTML, FlexTextSpanWrapContentUsesItemWidth ) {
Engine::instance()->createWindow( WindowSettings( 1280, 720, "Flex Text Span Width Test",
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/" );
std::string html;
FileSystem::fileGet( "assets/html/newsblur_home_prices_small.html", html );
sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( html ) );
sceneNode->update( Seconds( 1 ) );
sceneNode->updateDirtyLayouts();
auto priceAmount = sceneNode->getRoot()->findByClass( "NB-pricing-tier-price-amount" );
ASSERT_TRUE( priceAmount != nullptr );
ASSERT_TRUE( priceAmount->isType( UI_TYPE_TEXTSPAN ) );
auto* amountSpan = priceAmount->asType<UITextSpan>();
auto* amountRichText = amountSpan->getRichTextPtr();
ASSERT_TRUE( amountRichText != nullptr );
amountRichText->updateLayout();
const auto& lines = amountRichText->getLines();
ASSERT_EQ( lines.size(), 1u );
ASSERT_EQ( lines.front().spans.size(), 1u );
ASSERT_EQ( lines.front().spans.front().type, RichText::RenderSpan::Type::Text );
ASSERT_TRUE( lines.front().spans.front().text != nullptr );
EXPECT_STRINGEQ( lines.front().spans.front().text->getString(), "$36" );
EXPECT_NEAR( lines.front().spans.front().position.x, 0.f, 1.f );
EXPECT_NEAR( amountSpan->getPixelsSize().getWidth(), lines.front().spans.front().size.x, 2.f );
Engine::destroySingleton();
}
UTEST( UIHTML, FlexLiItemsWrapContentWidth ) {
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Flex LI Width Test",
WindowStyle::Default, WindowBackend::Default,