mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 11:10:31 +03:00
Merge branch 'feature/grid-layout' into develop
This commit is contained in:
@@ -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 & 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 & 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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" );
|
||||
};
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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 )
|
||||
|
||||
@@ -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 ) );
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user